내 출판물을 읽는 독자들은 마이크로서비스 개발에 API 우선 접근 방식을 사용한다는 아이디어에 익숙할 것입니다. 저는 개발이 시작되기 전에 예상되는 URI와 기본 개체 모델을 설명하는 것의 이점을 셀 수 없이 깨달았습니다.
그러나 30년 이상 기술 탐색을 하면서 나는 대체 흐름 의 현실을 기대하게 되었습니다. 즉, API First가 불가능한 상황이 발생할 것이라고 충분히 예상합니다.
이 기사에서는 마이크로서비스를 생산하는 팀이 openapi.json 파일을 수동으로 정의하지 않고도 다른 사람들이 사용할 OpenAPI 사양을 성공적으로 제공할 수 있는 방법의 예를 살펴보고 싶었습니다.
또한 저는 익숙한 영역에서 벗어나 Java, .NET 또는 JavaScript를 사용하지 않고도 이 작업을 수행하고 싶었습니다.
대부분의 기사의 끝부분에서 나는 종종 내 개인적인 사명 선언문을 언급합니다.
“지적 재산의 가치를 확장하는 특징/기능을 제공하는 데 시간을 집중하십시오. 다른 모든 것에 프레임워크, 제품, 서비스를 활용하세요.” – J. 베스터
이 사명 선언문에서 나의 요점은 더 높은 수준에서 설정된 목표와 목적을 달성하려고 노력할 때 내 시간을 최대한 활용하는 데 책임을 져야 한다는 것입니다. 기본적으로 우리의 초점이 더 많은 위젯을 판매하는 것이라면 이를 가능하게 하는 방법을 찾는 데 시간을 투자해야 합니다. 즉, 기존 프레임워크, 제품 또는 서비스로 이미 해결된 문제를 피해야 합니다.
저는 새로운 마이크로서비스의 프로그래밍 언어로 Python을 선택했습니다. 지금까지 이전 기사에서 작성한 Python 코드의 99%는 SODD( Stack Overflow Driven Development ) 또는 ChatGPT 기반 답변의 결과였습니다. 분명히 Python은 내 안전 영역을 벗어났습니다.
이제 상황이 어느 정도 수준으로 설정되었으므로 소스 언어에 대한 최소한의 경험으로 개인적인 사명 선언문을 준수하는 새로운 Python 기반 RESTful 마이크로서비스를 만들고 싶었습니다.
그때 FastAPI를 발견했습니다.
FastAPI는 2018년부터 존재해 왔으며 Python 유형 힌트를 사용하여 RESTful API를 제공하는 데 중점을 둔 프레임워크입니다. FastAPI의 가장 좋은 점은 개발자의 관점에서 추가적인 노력 없이 OpenAPI 3 사양을 자동으로 생성할 수 있다는 것입니다.
이 기사에서는 소비자가 최근 게시된 기사 목록을 검색할 수 있는 RESTful API를 제공하는 Article API에 대한 아이디어가 떠올랐습니다.
작업을 단순하게 유지하기 위해 특정 Article
다음 속성이 포함되어 있다고 가정해 보겠습니다.
id
- 단순하고 고유한 식별자 속성(숫자)title
– 기사 제목(문자열)url
– 기사의 전체 URL(문자열)year
- 기사가 출판된 연도(숫자)
Article API에는 다음 URI가 포함됩니다.
/articles
– 기사 목록을 검색합니다./articles/{article_id}
– id 속성으로 단일 기사를 검색합니다./articles
– 새 기사를 추가합니다.터미널에서 fast-api-demo라는 새로운 Python 프로젝트를 생성한 후 다음 명령을 실행했습니다.
$ pip install --upgrade pip $ pip install fastapi $ pip install uvicorn
api.py
라는 새 Python 파일을 만들고 일부 가져오기를 추가하고 app
변수를 설정했습니다.
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() if __name__ == "__main__": import uvicorn uvicorn.run(app, host="localhost", port=8000)
다음으로 Article API 사용 사례와 일치하도록 Article
객체를 정의했습니다.
class Article(BaseModel): id: int title: str url: str year: int
모델이 확립되면 URI를 추가해야 했는데... 꽤 쉬운 것으로 나타났습니다.
# Route to add a new article @app.post("/articles") def create_article(article: Article): articles.append(article) return article # Route to get all articles @app.get("/articles") def get_articles(): return articles # Route to get a specific article by ID @app.get("/articles/{article_id}") def get_article(article_id: int): for article in articles: if article.id == article_id: return article raise HTTPException(status_code=404, detail="Article not found")
외부 데이터 저장소를 사용하지 않기 위해 최근 게시된 기사 중 일부를 프로그래밍 방식으로 추가하기로 결정했습니다.
articles = [ Article(id=1, title="Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", url="https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", year=2023), Article(id=2, title="Using Unblocked to Fix a Service That Nobody Owns", url="https://dzone.com/articles/using-unblocked-to-fix-a-service-that-nobody-owns", year=2023), Article(id=3, title="Exploring the Horizon of Microservices With KubeMQ's New Control Center", url="https://dzone.com/articles/exploring-the-horizon-of-microservices-with-kubemq", year=2024), Article(id=4, title="Build a Digital Collectibles Portal Using Flow and Cadence (Part 1)", url="https://dzone.com/articles/build-a-digital-collectibles-portal-using-flow-and-1", year=2024), Article(id=5, title="Build a Flow Collectibles Portal Using Cadence (Part 2)", url="https://dzone.com/articles/build-a-flow-collectibles-portal-using-cadence-par-1", year=2024), Article(id=6, title="Eliminate Human-Based Actions With Automated Deployments: Improving Commit-to-Deploy Ratios Along the Way", url="https://dzone.com/articles/eliminate-human-based-actions-with-automated-deplo", year=2024), Article(id=7, title="Vector Tutorial: Conducting Similarity Search in Enterprise Data", url="https://dzone.com/articles/using-pgvector-to-locate-similarities-in-enterpris", year=2024), Article(id=8, title="DevSecOps: It's Time To Pay for Your Demand, Not Ingestion", url="https://dzone.com/articles/devsecops-its-time-to-pay-for-your-demand", year=2024), ]
믿거나 말거나, 이것으로 Article API 마이크로서비스 개발이 완료되었습니다.
빠른 상태 점검을 위해 API 서비스를 로컬로 가동했습니다.
$ python api.py INFO: Started server process [320774] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://localhost:8000 (Press CTRL+C to quit)
그런 다음 다른 터미널 창에서 컬 요청을 보냈습니다(그리고 이를 json_pp
로 파이프했습니다).
$ curl localhost:8000/articles/1 | json_pp { "id": 1, "title": "Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", "url": "https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", "year": 2023 }
Article API를 로컬에서 실행하는 것보다 마이크로서비스를 얼마나 쉽게 배포할 수 있는지 알 수 있을 것이라고 생각했습니다. 이전에는 Python 마이크로서비스를 Heroku 에 배포한 적이 없었기 때문에 지금이 시도해 볼 수 있는 좋은 기회라고 느꼈습니다.
Heroku를 시작하기 전에 서비스에 대한 종속성을 설명하기 위해 requirements.txt
파일을 만들어야 했습니다. 이를 위해 나는 pipreqs
설치하고 실행했습니다.
$ pip install pipreqs $ pipreqs
그러면 다음 정보가 포함된 requirements.txt
파일이 생성되었습니다.
fastapi==0.110.1 pydantic==2.6.4 uvicorn==0.29.0
또한 Heroku에게 uvicorn
사용하여 마이크로서비스를 가동하는 방법을 알려주는 Procfile
이라는 파일이 필요했습니다. 그 내용은 다음과 같았습니다.
web: uvicorn api:app --host=0.0.0.0 --port=${PORT}
(나처럼) Python을 처음 접하는 분들을 위해 Python으로 Heroku 시작하기 문서를 유용한 가이드로 사용했습니다.
이미 Heroku CLI가 설치되어 있으므로 터미널에서 Heroku 생태계에 로그인하기만 하면 되었습니다.
$ heroku login
모든 업데이트를 GitLab의 저장소에 체크인했습니다.
다음으로, 다음 명령을 통해 CLI를 사용하여 Heroku에서 새 앱을 생성할 수 있습니다.
$ heroku create
CLI는 앱의 URL 및 앱과 연결된 Git 기반 저장소와 함께 고유한 앱 이름으로 응답했습니다.
Creating app... done, powerful-bayou-23686 https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/ | https://git.heroku.com/powerful-bayou-23686.git
참고하세요. 이 기사를 읽을 때쯤에는 내 앱이 더 이상 온라인 상태가 아닐 것입니다.
이것 좀 봐. git 원격 명령을 실행하면 리모컨이 Heroku 생태계에 자동으로 추가된 것을 볼 수 있습니다.
$ git remote heroku origin
fast-api-demo
앱을 Heroku에 배포하려면 다음 명령을 사용하기만 하면 됩니다.
$ git push heroku main
모든 것이 설정되었으므로 새로운 Python 기반 서비스가 Heroku 대시보드에서 실행되고 있는지 확인할 수 있었습니다.
서비스가 실행 중인 상태에서 다음 컬 명령을 실행하여 Article API에서 id = 1
인 Article
검색할 수 있습니다.
$ curl --location 'https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/articles/1'
컬 명령은 200 OK 응답과 다음 JSON 페이로드를 반환합니다.
{ "id": 1, "title": "Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", "url": "https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", "year": 2023 }
FastAPI에 내장된 OpenAPI 기능을 활용하면 소비자는 자동으로 생성된 /docs
URI로 이동하여 완전한 기능을 갖춘 v3 사양을 받을 수 있습니다.
https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/docs
이 URL을 호출하면 널리 채택된 Swagger UI를 사용하여 Article API 마이크로서비스가 반환됩니다.
Article API를 사용하기 위한 클라이언트를 생성하기 위해 openapi.json
파일을 찾는 경우 /openapi.json
URI를 사용할 수 있습니다.
https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/openapi.json
내 예에서는 JSON 기반 OpenAPI v3 사양이 아래와 같이 나타납니다.
{ "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" }, "paths": { "/articles": { "get": { "summary": "Get Articles", "operationId": "get_articles_articles_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } } } }, "post": { "summary": "Create Article", "operationId": "create_article_articles_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Article" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/articles/{article_id}": { "get": { "summary": "Get Article", "operationId": "get_article_articles__article_id__get", "parameters": [ { "name": "article_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Article Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } } }, "components": { "schemas": { "Article": { "properties": { "id": { "type": "integer", "title": "Id" }, "title": { "type": "string", "title": "Title" }, "url": { "type": "string", "title": "Url" }, "year": { "type": "integer", "title": "Year" } }, "type": "object", "required": [ "id", "title", "url", "year" ], "title": "Article" }, "HTTPValidationError": { "properties": { "detail": { "items": { "$ref": "#/components/schemas/ValidationError" }, "type": "array", "title": "Detail" } }, "type": "object", "title": "HTTPValidationError" }, "ValidationError": { "properties": { "loc": { "items": { "anyOf": [ { "type": "string" }, { "type": "integer" } ] }, "type": "array", "title": "Location" }, "msg": { "type": "string", "title": "Message" }, "type": { "type": "string", "title": "Error Type" } }, "type": "object", "required": [ "loc", "msg", "type" ], "title": "ValidationError" } } } }
결과적으로 다음 사양을 사용하여 OpenAPI Generator를 통해 다양한 언어로 클라이언트를 생성할 수 있습니다.
이 기사의 시작 부분에서 저는 API First 접근 방식을 사용하는 데 관심이 없는 사람과 맞서 싸울 준비가 되어 있었습니다. 이 연습에서 배운 것은 FastAPI와 같은 제품이 작동하는 RESTful 마이크로서비스를 신속하게 정의하고 생성하는 동시에 완전히 소비 가능한 OpenAPI v3 사양을 자동으로 포함하는 데 도움이 될 수 있다는 것입니다.
FastAPI를 사용하면 다른 사람들이 의존할 수 있는 표준화된 계약을 생성하는 프레임워크를 활용하여 팀이 목표와 목표에 계속 집중할 수 있습니다. 그 결과, 나의 개인적인 사명 선언문을 고수하는 또 다른 길이 나타났습니다.
그 과정에서 처음으로 Heroku를 사용하여 Python 기반 서비스를 배포했습니다. 잘 작성된 문서를 검토하는 것 외에는 노력이 거의 필요하지 않은 것으로 나타났습니다. 따라서 Heroku 플랫폼에 대해서도 또 다른 사명 선언문 보너스를 언급해야 합니다.
이 기사의 소스 코드에 관심이 있다면 GitLab 에서 찾을 수 있습니다.
정말 좋은 하루 보내세요!