이름에서 알 수 있듯이 API를 빠르게 만들 수 있도록 집중한 프레임워크다. 또한 FastAPI 자체에는 ORM(Object-Relational Mapping) 기능이 포함되어 있지 않다. 하지만, FastAPI는 SQLAlchemy와 같은 외부 라이브러리를 통해 ORM 기능을 사용할 수 있도록 설계되었다.
SQLAlchemy는 Python에서 매우 강력하고 유연한 ORM 라이브러리로, 다양한 데이터베이스와의 상호작용을 쉽게 만들어줍니다. 한 번 익히면 다양한 프로젝트와 상황에서 재사용할 수 있기 때문에 매우 가치 있는 라이브러리라고 할 수 있다. FastAPI와 SQLAlchemy를 함께 사용함으로써, 개발자들은 FastAPI의 빠른 성능과 편리한 API 개발 기능과 더불어, 데이터베이스 작업을 위한 강력한 ORM 기능을 활용할 수 있게 된다.
FastAPI의 개발자인 Sebastián Ramírez는 Typer라는 별도의 라이브러리를 만들어 CLI 애플리케이션을 쉽게 생성할 수 있도록 했다. Typer는 Python의 타입 힌트를 기반으로 하여, FastAPI와 유사한 방식으로 CLI 애플리케이션을 개발할 수 있게 해주며, 사용자 친화적인 CLI를 빠르게 구축할 수 있다. 그래서 FastAPI 문서나 웹사이트에서 Typer를 소개하고 있다. FastAPI와 Typer 모두 사용자가 빠르고 쉽게 API 및 CLI 애플리케이션을 개발할 수 있도록 설계된 도구다.
FastAPI 자체로는 CLI(Command Line Interface, 명령줄 인터페이스) 애플리케이션을 직접 생성하지 않는다. CLI는 사용자가 텍스트 기반의 명령을 통해 컴퓨터와 상호 작용할 수 있게 해주는 인터페이스다.
FastAPI
: OpenAPI 표준을 기반으로 자동 API 문서를 생성한다. 이는 API 개발 및 테스트를 쉽게 해주며, Swagger UI와 ReDoc을 통해 문서를 제공한다.
Flask
: Flask 자체에는 자동 문서 생성 기능이 없다. 이를 위해서는 Swagger와 같은 별도의 확장 도구를 사용해야 한다.
FastAPI와 Flask는 각각의 사용 사례와 필요에 따라 선택할 수 있는 강력한 프레임워크다. FastAPI는 현대적인 기능과 빠른 성능을 제공하는 반면, Flask는 간결함과 확장 가능한 구조로 널리 사용된다. 프로젝트의 요구 사항과 개발자의 선호도에 따라 적합한 프레임워크를 선택하는 것이 중요하다.
먼저 새로운 Python 프로젝트를 위한 디렉토리를 생성하고 가상 환경을 설정한다.
> mkdir fast_api
> cd fast_api
>(MAC) virtualenv .venv
>(Window) python -m venv .venv
실행
>(MAC) source .venv/bin/activate
>(Window) cd .venv\Scripts\activate
FastAPI와 Uvicorn(ASGI 서버)를 설치한다.
> pip install fastapi
> pip install 'uvicorn[standard]'
main.py 파일을 생성하고 다음 코드를 작성한다.
fast_api/main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
생성한 애플리케이션을 실행하기 위해 다음 명령어를 사용한다.
> uvicorn main:app --reload
+) 추가 서버 실행 방법
프로그래밍 방식으로 직접 실행하기
FastAPI 애플리케이션을 프로그램 내부에서 직접 실행하는 가장 일반적인 방법
import uvicorn
if __name__ == "__main__":
uvicorn.run("main:app", port=8000, log_level="info")
위 코드를 main.py에 추가해준 뒤 아래 명령어 실행
> python main.py
만약, 위 내용이 파일 내에 없다면 터미널에서 아래 명령어 실행
> uvicorn main:app --port 8000 --log-level info
Config와 Server 인스턴스 사용하기
더 많은 구성 옵션과 서버 수명 주기 제어가 필요할 때 사용
import uvicorn
if __name__ == "__main__":
config = uvicorn.Config("main:app", port=8000, log_level="info")
server = uvicorn.Server(config)
server.run()
Gunicorn과 함께 사용하기
프로덕션 환경에서는 Gunicorn을 사용하는 것이 권장된다. Gunicorn은 프로세스 관리 및 부하 분산 기능을 제공한다.
gunicorn example:app -w 4 -k uvicorn.workers.UvicornWorker
브라우저를 열고 http://127.0.0.1:8000
로 이동한다. "Hello: World"라는 JSON 응답을 볼 수 있다.
http://127.0.0.1:8000/items/123?q=hi
로 이동하면 "item_id": 123,"q": "hi"라는 JSON 응답을 볼 수 있다.
또한 FastAPI는 자동으로 API 문서를 생성한다. http://127.0.0.1:8000/docs
또는 http://127.0.0.1:8000/redoc
으로 이동하여 API 문서를 볼 수 있다.
이렇게 함으로써 기본적인 FastAPI 프로젝트를 설정하고 실행할 수 있다. FastAPI는 매우 유연하고 확장 가능하므로, 추가 기능과 복잡한 API 경로를 쉽게 추가할 수 있다. FastAPI 공식 문서(FastAPI Documentation)를 참조하여 더 많은 정보를 얻을 수 있다.
Swagger UI
는 FastAPI와 함께 가장 일반적으로 사용되는 자동 생성 문서다. 사용자 친화적인 인터페이스를 제공하며, API의 각 경로와 가능한 요청 및 응답을 시각적으로 표시한다. 실시간으로 API를 테스트하고 상호 작용할 수 있는 기능을 제공한다. 예를 들어, API 엔드포인트에 대한 요청을 직접 보내고 응답을 받아볼 수 있다. 각 API 엔드포인트에 대한 파라미터, 요청 본문, 응답 스키마 등의 세부 사항을 볼 수 있다.
ReDoc
은 좀 더 간결하고 정돈된 인터페이스를 제공하는 다른 형태의 문서화 도구다. Swagger UI보다 더 간단하고 읽기 쉬운 레이아웃을 제공한다. 대규모 API에 적합하며, 복잡한 스키마를 가진 API를 좀 더 쉽게 탐색하고 이해할 수 있도록 도와준다. 실시간 API 테스트 기능은 제공하지 않는다. 대신, 문서화에 더 초점을 맞추고 있다.
개발 및 테스트
Swagger UI (/docs
)는 API 개발 및 테스트 과정에서 매우 유용하다. 개발자는 API의 각 부분을 실시간으로 테스트하고 결과를 즉시 확인할 수 있다.
문서화 및 프레젠테이션
ReDoc (/redoc
)은 API의 최종 사용자나 스테이크홀더들에게 API를 소개하거나 문서화하는 데 더 적합하다. 그것은 깔끔하고 직관적인 인터페이스로 API의 구조와 기능을 명확하게 전달한다.
결론적으로, Swagger UI와 ReDoc은 각기 다른 목적과 상황에 맞게 활용될 수 있으며, FastAPI에서 두 문서화 도구를 모두 제공하는 것은 개발자가 다양한 요구 사항과 선호도에 맞게 선택할 수 있게 한다.
APIRouter는 FastAPI에서 제공하는 클래스로, 애플리케이션의 라우트(route)를 그룹화하고 모듈화하는 데 사용된다. APIRouter를 사용하면 여러 파일로 나누어진 라우트를 조직적으로 관리할 수 있으며, 이는 애플리케이션의 유지보수성과 확장성을 향상시킨다.
라우트 그룹화
관련된 라우트를 하나의 APIRouter
인스턴스에 그룹화하여 관리할 수 있다. 예를 들어, 사용자 관련 라우트, 주문 관련 라우트 등을 각각 다른 APIRouter
인스턴스에 정의할 수 있다.
모듈화
각 APIRouter
인스턴스를 별도의 Python 파일(모듈)에 위치시켜, 코드의 모듈화를 촉진한다. 이를 통해 각 기능별로 코드를 분리하고, 팀에서 협업하기 쉬워진다.
미들웨어 및 의존성 주입
APIRouter
인스턴스에 미들웨어나 의존성을 추가하여 해당 라우터에서 사용되는 모든 라우트에 적용할 수 있다.
경로 작업 추가
HTTP 메소드별(예: GET, POST, PUT, DELETE)로 라우트를 쉽게 추가할 수 있다.
APIRouter
클래스를 초기화할 때 사용할 수 있는 몇 가지 주요 옵션들이 있다.
prefix
: 라우트 URL에 공통 접두어를 추가한다.
예를 들어, prefix="/items"
로 설정하면, 이 라우터의 모든 라우트 경로는 /items
으로 시작하게 된다.
tags
: 라우터에 태그를 추가하여 API 문서에서 관련 라우트를 그룹화할 수 있다. 예를 들어, tags=["items"]
는 이 라우터의 모든 라우트를 "items" 태그로 그룹화한다.
dependencies
: 공통 의존성을 정의하여 라우터의 모든 라우트에 적용한다. 예를 들어, 모든 라우트에 동일한 데이터베이스 세션 의존성을 추가할 수 있다.
responses
: 라우터의 모든 라우트에 공통된 응답을 정의할 수 있다. 이를 통해 특정 HTTP 상태 코드에 대한 공통 응답을 설정할 수 있다.
default_response_class
: 라우터의 모든 라우트에 대한 기본 응답 클래스를 설정할 수 있다.
model를 생성하기 전에 연습하는 것으로 각 파일마다 router를 만들어서 return 해줬다.
Fast_api/items.py (new file)
from fastapi import APIRouter
router = APIRouter() # items_router로 지정해도 된다.
@router.get( # items_router라고 지정시 여기도 router -> items_router로 변경 필요.
"/api/v1/items/{item_id}",
status_code=200,
tags=["item", "payment"],
summary="Bring a certificate item",
description="Item model에서 item_id 값을 가지고 특정 아이템 조회",
)
def get_item(item_id: int):
return {"items": item_id}
@router.get("/items/{item_id}")
: /items/{item_id}
경로에 대한 GET 요청을 처리한다.
여기서 {item_id}
는 경로 매개변수
status_code=200
: 이 라우트는 성공적으로 처리되었을 때 HTTP 200 상태 코드를 반환
tags=["items"]
: 이 라우트는 "items" 태그.
이 태그는 API 문서에서 이 라우트를 분류하는 데 사용
summary
와 description
: 이 라우트에 대한 요약과 자세한 설명을 제공한다.
이 정보는 API 문서에 표시
Fast_api/users.py (new file)
from fastapi import APIRouter
router = APIRouter(
prefix="/api/v1/users",
tags=["users"],
responses={
200: {"msg": "Success to get data"},
400: {"msg": "404 Not Found"},
},
)
@router.get("/{user_id}")
def get_user(user_id: int):
return {"data": user_id}
위에서 각자 만들 table을 main에 route 등록을 해준다.
Fast_api/main.py
import uvicorn
from fastapi import FastAPI
from items import router as items_router # as는 추후 다른 model에서의 router와 비교하기 위해.
from users import router as users_router
# router 이름을 users_router라고 이미 정의해줬다면 as를 쓸 필요가 없다.
app = FastAPI()
app.include_router(items_router)
app.include_router(users_router)
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
# http://127.0.0.1:8000/items/123?q=hi
if __name__ == "__main__":
uvicorn.run("main:app", port=8000, log_level="info")
그리고 임의의 local memory DB를 만들어준다.
그 books 데이터만 불러오도록 코드를 작성해본다.
Fast_api/books.py
from fastapi import APIRouter, FastAPI
BOOKS = [{"id": 1, "title": "수도꼭지가 쏴아아", "author": "채은혜", "url": "https://www.yes24.com/수도꼭지"}]
app = FastAPI()
router = APIRouter()
@router.get("/", status_code=200)
def main():
return {"Message": "Welcome to the Book World~!"} # Template Code
# 전체 책 데이터 조회
@router.get("/api/v1/books", status_code=200)
def get_all_books() -> list:
return BOOKS
app.include_router(router)
# books data만 따로 부르기.
if __name__ == "__main__":
import uvicorn
uvicorn.run("books:app", port=8001, reload=True)
book 데이터를 가지고 CRUD 하는 REST API를 작성해보면 아래와 같이 추가할 수 있다.
Fast_api/books.py
# 전체 책 데이터 조회
@router.get("/api/v1/books", status_code=200)
def get_all_books() -> list:
return BOOKS
# 여기서 부터 추가.
# 특정 책 데이터 조회
@router.get("/api/v1/books/{book_id}", status_code=200)
def get_book(book_id: int):
# for book in BOOKS:
# if book["id"] == book_id:
# book
# break
# 위 코드를 아래 코드 한 줄로 표현.
book = next((book for book in BOOKS if book["id"] == book_id), None)
# next는 data 하나를 찾고 나서 break
if book:
return book
return {"error": f"book not found, ID: {book_id}"}
# 책 생성
@router.post("/api/v1/books")
def create_book(book: dict):
BOOKS.append(book)
return book
# 특정 책 수정
@router.put("/api/v1/books/{book_id}")
def update_book(book_id: int, book_update: dict):
# 수정할 book data 가져오기
book = next((book for book in BOOKS if book["id"] == book_id), None)
for key, value in book_update.items():
if key in book:
book[key] = value
return book
# 특정 책 삭제
@router.delete("/api/v1/books/{book_id}")
def delete_book(book_id: int):
global BOOKS
# for item in BOOKS:
# if item['id'] != book_id:
# item
# 위 코드를 아래 코드 한 줄로 표현.
BOOKS = [item for item in BOOKS if item["id"] != book_id]
return {"Message": f"Book {book_id} deleted successfully"}
이 내용들을 각각의 table 별로 정의하는 방법을 아래와 같이 정리했다.
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
from fastapi import Depends, HTTPException
def verify_token(token: str = Depends()):
if token != "secret-token":
raise HTTPException(status_code=400, detail="Invalid token")
return token
from fastapi import APIRouter
router = APIRouter()
@router.get("/items/{item_id}",
response_model=Item,
status_code=200,
tags=["items"],
summary="특정 아이템 가져오기",
description="아이템 ID를 사용하여 특정 아이템의 세부 정보를 가져옵니다.",
response_description="아이템 세부 정보 반환",
dependencies=[Depends(verify_token)])
async def read_item(item_id: int):
# 임의의 아이템 반환 예시
return {
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2
}
response_model=Item
: 이 라우트의 응답은 Item
모델을 따른다.
이는 응답 데이터가 Item
클래스의 형식을 갖추어야 함을 의미
response_description
: 이 라우트의 응답에 대한 설명
dependencies=[Depends(verify_token)]
: 이 라우트에는 verify_token
함수에 의존성이 설정되어 있다. 이 의존성은 라우트가 실행되기 전에 검증된다.
from fastapi import APIRouter
router = APIRouter(
prefix="/items",
tags=["items"],
dependencies=[...], # 여기에 의존성을 추가할 수 있습니다
responses={404: {"description": "Not found"}},
)
@router.get("/users")
def get_users():
return {"message": "사용자 목록"}
from fastapi import FastAPI
from users import router as user_router
from items import router as item_router
app = FastAPI()
app.include_router(user_router)
app.include_router(item_router)
이렇게 구조화하면 애플리케이션이 커질수록 각 기능별로 코드를 분리하여 관리하기 쉬워지고, 각 모듈이 독립적으로 작동하여 유지보수가 용이해진다.
Pydantic은 FastAPI에서 데이터 검증 및 설정 관리를 위해 널리 사용된다. Pydantic의 모델을 사용하여 클라이언트로부터 받은 데이터의 형식을 검증하고, API 응답으로 데이터를 구조화하여 반환할 수 있다.
아래의 예제에서는 간단한 도서 정보 관리 API를 구현한다. 이 API에서는 도서를 추가하고, 특정 도서 정보를 조회하는 기능을 포함한다.
Book
모델은 도서 정보를 나타낸다.CreateBook
모델은 새로운 도서를 추가할 때 사용할 입력 데이터 형식을 정의한다./books/
엔드포인트로 POST
요청을 보내 도서를 추가한다./books/{book_id}
엔드포인트로 GET
요청을 보내 특정 도서 정보를 조회한다.위에서 했던 main.py, items.py, users.py, books.py는 01.basic
이란 폴더를 만들어 넣어준다.
아래 코드들은 해당 파일을 새로 만들어 작성해준다.
교재
from pydantic import BaseModel
from typing import Optional
from uuid import UUID
class Book(BaseModel):
id: UUID
title: str
author: str
description: Optional[str] = None
class CreateBook(BaseModel):
title: str
author: str
description: Optional[str] = None
class BookSearch(BaseModel):
results: Sequence[Book]
실습
from typing import List, Optional
from pydantic import BaseModel
class Book(BaseModel):
id: int
title: str
author: str
description: Optional[str] = None
# 따로 Create Book class 생성 필요 (data validation)
# docs에 데이터 그대로 넣어진다.
class CreateBook(BaseModel):
title: str
author: str
description: Optional[str] = None
class SearchBook(BaseModel):
results: Optional[Book]
class SearchBookList(BaseModel):
results: List[Book]
교재
from fastapi import APIRouter, HTTPException
from typing import List, Optional
from .models import Book, CreateBook, BookSearchResults
from uuid import uuid4
router = APIRouter()
books: List[Book] = []
@router.post("/books/", response_model=Book, status_code=201)
def create_book(book_data: CreateBook) -> Book:
book = Book(id=uuid4(), **book_data.dict())
books.append(book)
return book
@router.get("/books/{book_id}", response_model=Book)
def fetch_book(book_id: UUID) -> Book:
book = next((b for b in books if b.id == book_id), None)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
return book
@router.get("/search/", response_model=BookSearchResults)
def search_books(
*,
keyword: Optional[str] = None,
max_results: int = 10
) -> BookSearchResults:
result = [book for book in books if keyword.lower() in book.title.lower()] if keyword else books
return BookSearchResults(results=result[:max_results])
실습 (books.py)
from typing import List, Optional
from fastapi import APIRouter
from models import Book, CreateBook, SearchBookList
route = APIRouter()
books: List[Book] = []
@route.post("/")
def create_book(book: CreateBook) -> Book:
book = Book(id=len(books) + 1, **book.model_dump())
books.append(book)
return book
@route.get("/search/")
# 1개의 책만 조회
# def search_book() -> SearchBook:
# pass
# 여러개 책 조회
def search_book_list(keyword: Optional[str], max_results: int = 10) -> SearchBookList:
# 이 코드를
# for book in books:
# if keyword in book.title:
# book
# 이렇게 한 줄로 표시
search_result = [book for book in books if keyword in book.title] if keyword else books
return SearchBookList(results=search_result[:max_results])
from fastapi import FastAPI
from .routers import router
app = FastAPI()
app.include_router(router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
실습
from fastapi import FastAPI
from books import route as books_route
app = FastAPI()
app.include_router(books_route)
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", reload=True)
이렇게 분리하여 코드를 관리하면 각 부분이 명확하게 구분되어 프로젝트의 확장성과 유지보수성이 향상된다.
데이터베이스 연결, 사용자 인증 등 추가 기능을 쉽게 통합할 수 있다.
FastAPI에 Jinja2 템플릿을 적용하기 위해서는 몇 가지 단계를 거쳐야한다. 우선, Jinja2Templates
클래스를 사용하여 템플릿 디렉토리를 설정한다. 그런 다음, FastAPI 라우터에 템플릿을 렌더링하는 엔드포인트를 추가한다. 아래 예제는 기존의 책 관리 코드에 템플릿 렌더링을 추가한 것이다.
템플릿 디렉토리 설정
: templates
폴더에 HTML 템플릿 파일을 저장한다. 예를 들어, index.html
이라는 파일을 만들고, 그 안에 책 목록을 렌더링하는 HTML 코드를 작성한다.
FastAPI 애플리케이션에 템플릿 렌더링 추가
: Jinja2Templates
를 사용하여 템플릿 디렉토리를 지정하고, 라우터에 HTML 페이지를 렌더링하는 경로를 추가한다.
먼저, templates/index.html 파일을 생성한다.
<!DOCTYPE html>
<html>
<head>
<title>Books</title>
</head>
<body>
<h1>Books</h1>
<ul>
{% for book in books %}
<li>{{ book.title }} by {{ book.author }}</li>
{% endfor %}
</ul>
</body>
</html>
그런 다음, main.py
파일을 업데이트 한다.
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from .routers import router
app = FastAPI()
app.include_router(router)
# 템플릿을 위한 디렉토리 설정
templates = Jinja2Templates(directory="templates")
@app.get("/books/", response_class=HTMLResponse)
async def read_books(request: Request):
return templates.TemplateResponse("index.html", {"request": request, "books": books})
이 코드는 /books/
경로에 접근할 때, 저장된 책 목록을 HTML 형식으로 렌더링한다. Jinja2 템플릿 엔진은 templates/index.html
파일을 사용하여 동적으로 HTML 페이지를 생성한다.
위의 예제에서 books
는 앞서 routers.py
에서 정의한 책 목록이다. FastAPI가 이 목록을 템플릿 엔진으로 전달하고, Jinja2는 이를 활용해 HTML 페이지에 동적으로 데이터를 삽입한다.
before test
main.py
from fastapi import FastAPI
from routes import router
app = FastAPI()
app.include_router(router)
routes.py
from fastapi import APIRouter
import asyncio
import time
router = APIRouter()
@router.get("/slow-async-ping")
async def slow_async_ping():
time.sleep(10) # 차단 I/O: 메인 이벤트 루프를 10초 동안 차단
pong = service.get_pong() # 차단 I/O: DB에서 pong을 가져옴
return {"pong": pong}
이 함수는 async
로 선언되어 있지만 내부에서 차단 I/O 작업(time.sleep
및 service.get_pong
)을 수행한다. 이로 인해 이벤트 루프가 다른 작업을 수행할 수 없게 되고, 이 함수가 처리되는 동안 서버는 다른 요청을 처리할 수 없다.
@router.get("/efficient-sync-ping")
def efficient_sync_ping():
time.sleep(10) # 차단 I/O, 하지만 다른 스레드에서 실행
pong = service.get_pong() # 차단 I/O, 다른 스레드에서 실행
return {"pong": pong}
이 함수는 일반 동기 함수로, 차단 I/O 작업을 수행한다. Fast API는 이를 자동으로 별도의 스레드에서 실행하여, 메인 이벤트 루프를 차단하지 않고 다른 작업을 계속 처리할 수 있게 한다.
def
로 정의되며, 차단 I/O 작업을 수행할 때 사용될 수 있다.@router.get("/fast-async-ping")
async def fast_async_ping():
await asyncio.sleep(10) # 비차단 I/O
pong = await service.async_get_pong() # 비차단 I/O DB 호출
return {"pong": pong}
이 함수는 비차단 I/O 작업을 사용합니다. asyncio.sleep
과 service.async_get_pong
는 비동기로 실행되며, 이벤트 루프가 다른 작업을 계속 처리할 수 있게 해줍니다.
asyncio
라이브러리를 사용하여 비동기 작업을 쉽게 관리할 수 있게 해준다.async def
로 정의되며, await
를 사용하여 비동기 함수를 호출한다.정의
즉, 피보나치 수열은 0, 1, 1, 2, 3, 5, 8, 13, 21, ...과 같이 이어진다.
여기서 주어진 파이썬 함수는 재귀적 방법으로 피보나치 수열을 계산한다. 함수는 매개변수로 숫자 n
을 받아서 n
번째 피보나치 수를 반환한다.
만약 n
이 1 이하인 경우에는 n
자체를 반환한다. 왜냐하면 0번째와 1번째 피보나치 수는 각각 0과 1이기 때문이다.
그 외의 경우에는 fibonacci(n-1)
과 fibonacci(n-2)
를 더한 값을 반환한다. 이는 피보나치 수열의 성질을 따르는 재귀적인 방법이다.
이 함수를 호출하면 n
번째 피보나치 수를 구할 수 있다. 하지만 이 함수는 재귀적으로 자신을 호출하기 때문에 중복 계산이 발생할 수 있고, n
이 커질수록 계산 시간이 길어진다. 이런 문제를 해결하기 위해서는 아래 코드와 같이 동적 계획법 등의 방법을 사용할 수 있다.
@router.get("/cpu-bound-async")
async def cpu_bound_async():
result = await some_cpu_intensive_task() # CPU 집약적 작업
return {"result": result}
def some_cpu_intensive_task():
# 예시: 피보나치 수열의 35번째 항을 계산하는 함수
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)
# 피보나치 수열의 35번째 항 계산
result = fibonacci(35)
return result
이 함수에서 some_cpu_intensive_task
는 CPU 집약적 작업을 나타낸다. 비동기 함수에서 이러한 작업을 수행하는 것은 권장되지 않는다. CPU 집약적 작업은 이벤트 루프를 차단할 수 있으며, 이는 FastAPI의 성능에 부정적인 영향을 미친다. 이러한 작업은 별도의 프로세스에서 실행하는 것이 좋다.
CPU 바운드(CPU-bound)
작업이란 주로 CPU의 계산 능력에 의존하는 작업을 말한다. 이러한 작업은 CPU의 계산 속도에 따라 그 성능이 결정되며, 예로는 복잡한 수학적 계산, 데이터 압축, 이미지 또는 비디오 처리, 머신 러닝 모델의 학습 등이 있다.
위 문장에서의 핵심은, 비동기 프로그래밍은 주로 I/O 바운드 작업에 적합하다는 것이다. I/O 바운드 작업은 데이터를 읽거나 쓰는데 대부분의 시간을 소모하는 작업으로, 예를 들면 웹 페이지에서 데이터를 다운로드하거나 데이터베이스에 접근하는 것과 같은 작업들이 여기에 속한다. 이러한 작업들은 실제 CPU가 계산을 수행하는 시간보다 대기 시간이 더 길기 때문에, 비동기 프로그래밍을 통해 CPU가 다른 작업을 수행할 수 있도록 하여 전체적인 프로그램의 효율을 높일 수 있다.
반면에, CPU 바운드 작업은 CPU의 계산 능력이 주요한 성능 요소이므로, 비동기 프로그래밍을 사용해도 큰 효율 향상을 기대하기 어렵다. CPU가 계속해서 계산 작업에 몰두해야 하기 때문에, 다른 작업으로의 전환으로 인한 이득이 거의 없거나 오히려 오버헤드가 발생할 수 있다. 따라서, CPU 바운드 작업을 처리할 때는 멀티프로세싱과 같은 다른 접근 방식을 사용하는 것이 좋다. 멀티프로세싱은 여러 CPU 코어를 동시에 활용하여 여러 작업을 병렬로 처리할 수 있게 해준다.
from concurrent.futures import ProcessPoolExecutor
import asyncio
# 비동기 라우트 내에서 멀티프로세싱을 사용하는 함수
@router.get("/cpu-intensive-task")
async def run_cpu_intensive_task():
with ProcessPoolExecutor() as executor:
# 비동기적으로 프로세스 실행 및 결과 대기
result = await asyncio.get_event_loop().run_in_executor(executor, cpu_bound_operation)
return {"result": result}
이 코드는 /cpu-intensive-task
경로에 GET 요청을 보낼 때마다 cpu_bound_operation
함수를 별도의 프로세스에서 실행한다. ProcessPoolExecutor
는 Python의 표준 라이브러리로, 여러 프로세스에서 병렬 실행을 가능하게 한다. 이 경우, CPU 집약적인 피보나치 수열 계산이 메인 이벤트 루프를 차단하지 않도록 별도의 프로세스에서 실행된다.
async/await
를 사용하더라도, CPU 집약적 작업은 메인 스레드의 이벤트 루프를 차단할 수 있다. 비동기 프로그래밍은 I/O 바운드 작업에 적합하며, 이벤트 루프가 I/O 작업이 완료될 때까지 기다리는 동안 다른 작업을 처리할 수 있게 해준다. 그러나 CPU 바운드 작업은 계속해서 CPU 리소스를 사용하기 때문에, 이벤트 루프가 다른 비동기 작업을 수행할 기회를 얻지 못한다. 따라서, 이러한 종류의 작업은 멀티프로세싱과 같은 다른 접근 방법을 통해 처리하는 것이 좋다.
확실히 django보다 API를 만드는데 미리 작업해야할게 적고 반환되는 데이터 형에 대해서도 언급을 해주기 때문에 가시적으로 좋다.
[오즈스쿨 스타트업 웹 개발 초격차캠프 백엔드 Fast API 실시간 강의]