์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ FastAPI๋กœ ๋งŒ๋“œ๋Š” ์ดˆ๊ณ ์† REST API ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿš€ FastAPI๋กœ ๋งŒ๋“œ๋Š” ์ดˆ๊ณ ์† REST API ์™„๋ฒฝ ๊ฐ€์ด๋“œ

Python์œผ๋กœ ๊ตฌ์ถ•ํ•˜๋Š” ํ˜„๋Œ€์ ์ด๊ณ  ๋น ๋ฅธ API ๊ฐœ๋ฐœ์˜ ๋ชจ๋“  ๊ฒƒ

๐ŸŽฏ ์™œ ์ง€๊ธˆ FastAPI์ธ๊ฐ€?

์•ˆ๋…•! ์˜ค๋Š˜์€ Python ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๊ผญ ์•Œ์•„์•ผ ํ•  FastAPI์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ์š”์ฆ˜ ๊ฐœ๋ฐœ ์ปค๋ฎค๋‹ˆํ‹ฐ์—์„œ FastAPI๊ฐ€ ์ •๋ง ํ•ซํ•˜์ž–์•„? ๊ทธ๋ƒฅ ์œ ํ–‰์ด ์•„๋‹ˆ๋ผ ์ง„์งœ ์ด์œ ๊ฐ€ ์žˆ์–ด์„œ ๊ทธ๋ž˜. ๐Ÿ”ฅ

์‚ฌ์‹ค ๋‚˜๋„ ์ฒ˜์Œ์—” "Flask๋‚˜ Django ์ž˜ ์“ฐ๊ณ  ์žˆ๋Š”๋ฐ ๊ตณ์ด ์ƒˆ๋กœ์šด ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋ฐฐ์›Œ์•ผ ํ•˜๋‚˜?" ์‹ถ์—ˆ์–ด. ๊ทผ๋ฐ ๋ง‰์ƒ ์จ๋ณด๋‹ˆ๊นŒ ์™„์ „ ๋‹ค๋ฅธ ์„ธ๊ณ„๋”๋ผ๊ณ . ํŠนํžˆ API ๊ฐœ๋ฐœ์— ํŠนํ™”๋˜์–ด ์žˆ์–ด์„œ REST API ๋งŒ๋“ค ๋•Œ ์ •๋ง ํŽธํ•˜๊ณ  ๋น ๋ฅด๊ฑฐ๋“ .

๐Ÿ’ก FastAPI์˜ ํ•ต์‹ฌ ํŠน์ง•

โ€ข ์†๋„: NodeJS, Go์™€ ๋น„์Šทํ•œ ์ˆ˜์ค€์˜ ๊ณ ์„ฑ๋Šฅ (Starlette ๊ธฐ๋ฐ˜)
โ€ข ์ƒ์‚ฐ์„ฑ: ๊ฐœ๋ฐœ ์†๋„๊ฐ€ ์•ฝ 200~300% ํ–ฅ์ƒ
โ€ข ์ž๋™ ๋ฌธ์„œํ™”: Swagger UI์™€ ReDoc ์ž๋™ ์ƒ์„ฑ
โ€ข ํƒ€์ž… ํžŒํŒ…: Python 3.6+ ํƒ€์ž… ํžŒํŠธ ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ
โ€ข ๋น„๋™๊ธฐ ์ง€์›: async/await ๋„ค์ดํ‹ฐ๋ธŒ ์ง€์›

ํŠนํžˆ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋‹ค์–‘ํ•œ ์„œ๋น„์Šค๋ฅผ API๋กœ ์—ฐ๊ฒฐํ•  ๋•Œ, FastAPI๋Š” ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๊ฐ€ ๋ผ. ๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์ด๋ฉด์„œ๋„ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์ด ์ข‹์œผ๋‹ˆ๊นŒ ๋ง์ด์•ผ.

๐Ÿ—๏ธ REST API์˜ ๊ธฐ๋ณธ ๊ฐœ๋…๋ถ€ํ„ฐ ์ฐจ๊ทผ์ฐจ๊ทผ

๋ณธ๊ฒฉ์ ์œผ๋กœ FastAPI๋ฅผ ๋‹ค๋ฃจ๊ธฐ ์ „์—, REST API๊ฐ€ ๋ญ”์ง€๋ถ€ํ„ฐ ์งš๊ณ  ๋„˜์–ด๊ฐ€์ž. ์ด๋ฏธ ์•Œ๊ณ  ์žˆ๋‹ค๋ฉด ๋ณต์Šต ์ฐจ์›์—์„œ ๋ด์ค˜! ๐Ÿ˜Š

REST API๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

REST(Representational State Transfer)๋Š” ์›น์˜ ์ฐฝ์‹œ์ž ์ค‘ ํ•œ ๋ช…์ธ ๋กœ์ด ํ•„๋”ฉ์ด 2000๋…„์— ์ œ์•ˆํ•œ ์•„ํ‚คํ…์ฒ˜ ์Šคํƒ€์ผ์ด์•ผ. ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด ์›น์˜ ์žฅ์ ์„ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋„คํŠธ์›Œํฌ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋ผ๊ณ  ๋ณด๋ฉด ๋ผ.

๐ŸŽจ REST์˜ 6๊ฐ€์ง€ ์›์น™

1. ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ๊ตฌ์กฐ: ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค์™€ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ๋ฅผ ๋ถ„๋ฆฌ
2. ๋ฌด์ƒํƒœ์„ฑ(Stateless): ๊ฐ ์š”์ฒญ์€ ๋…๋ฆฝ์ ์ด๋ฉฐ ์„œ๋ฒ„๋Š” ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š์Œ
3. ์บ์‹œ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ: ์‘๋‹ต์€ ์บ์‹œ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ๋ช…์‹œํ•ด์•ผ ํ•จ
4. ๊ณ„์ธตํ™” ์‹œ์Šคํ…œ: ํด๋ผ์ด์–ธํŠธ๋Š” ์ค‘๊ฐ„ ์„œ๋ฒ„์˜ ์กด์žฌ๋ฅผ ์•Œ ํ•„์š” ์—†์Œ
5. ์ธํ„ฐํŽ˜์ด์Šค ์ผ๊ด€์„ฑ: ์ž์› ์‹๋ณ„, ํ‘œํ˜„์„ ํ†ตํ•œ ์ž์› ์กฐ์ž‘ ๋“ฑ
6. ์ฝ”๋“œ ์˜จ ๋””๋งจ๋“œ(์„ ํƒ): ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ ์ „์†ก ๊ฐ€๋Šฅ

HTTP ๋ฉ”์„œ๋“œ์™€ CRUD

REST API๋Š” HTTP ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•ด์„œ ์ž์›์— ๋Œ€ํ•œ ์ž‘์—…์„ ํ‘œํ˜„ํ•ด. ์ด๊ฒŒ ๋ฐ”๋กœ ๊ทธ ์œ ๋ช…ํ•œ CRUD์•ผ!

๐Ÿ“ POST (Create)

์ƒˆ๋กœ์šด ๋ฆฌ์†Œ์Šค ์ƒ์„ฑ
์˜ˆ: ์ƒˆ ์‚ฌ์šฉ์ž ๋“ฑ๋ก, ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ

๐Ÿ“– GET (Read)

๋ฆฌ์†Œ์Šค ์กฐํšŒ
์˜ˆ: ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ, ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ

โœ๏ธ PUT/PATCH (Update)

๋ฆฌ์†Œ์Šค ์ˆ˜์ •
PUT: ์ „์ฒด ์ˆ˜์ •, PATCH: ๋ถ€๋ถ„ ์ˆ˜์ •

๐Ÿ—‘๏ธ DELETE (Delete)

๋ฆฌ์†Œ์Šค ์‚ญ์ œ
์˜ˆ: ๊ณ„์ • ์‚ญ์ œ, ๊ฒŒ์‹œ๊ธ€ ์ œ๊ฑฐ

์ด๋Ÿฐ ๋ฉ”์„œ๋“œ๋“ค์„ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด, API๋ฅผ ๋ณด๋Š” ์‚ฌ๋žŒ์ด "์•„, ์ด๊ฑด ์ด๋Ÿฐ ๋™์ž‘์„ ํ•˜๊ฒ ๊ตฌ๋‚˜" ํ•˜๊ณ  ์ง๊ด€์ ์œผ๋กœ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์–ด. ๋งˆ์น˜ ์ž˜ ์ •๋ฆฌ๋œ ๋„์„œ๊ด€์ฒ˜๋Ÿผ ๋ง์ด์•ผ! ๐Ÿ“š

โšก FastAPI ์‹œ์ž‘ํ•˜๊ธฐ: ์„ค์น˜๋ถ€ํ„ฐ ์ฒซ API๊นŒ์ง€

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ FastAPI๋ฅผ ์‹œ์ž‘ํ•ด๋ณด์ž! ์„ค์น˜๋Š” ์ •๋ง ๊ฐ„๋‹จํ•ด.

ํ™˜๊ฒฝ ์„ค์ •

# ๊ฐ€์ƒํ™˜๊ฒฝ ์ƒ์„ฑ (๊ถŒ์žฅ)
python -m venv venv

# ๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™”
# Windows
venv\Scripts\activate
# Mac/Linux
source venv/bin/activate

# FastAPI ์„ค์น˜
pip install fastapi

# ASGI ์„œ๋ฒ„ ์„ค์น˜ (Uvicorn)
pip install "uvicorn[standard]"

๐Ÿ’š Pro Tip!

๊ฐ€์ƒํ™˜๊ฒฝ์€ ์„ ํƒ์ด ์•„๋‹ˆ๋ผ ํ•„์ˆ˜์•ผ! ํ”„๋กœ์ ํŠธ๋งˆ๋‹ค ๋…๋ฆฝ์ ์ธ ํ™˜๊ฒฝ์„ ์œ ์ง€ํ•˜๋ฉด ๋‚˜์ค‘์— ์˜์กด์„ฑ ์ถฉ๋Œ๋กœ ๋จธ๋ฆฌ ์•„ํ”Œ ์ผ์ด ์—†์–ด. ํŠนํžˆ ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ๋ฅผ ๋™์‹œ์— ์ง„ํ–‰ํ•  ๋•Œ๋Š” ๋”๋”์šฑ ์ค‘์š”ํ•˜์ง€.

์ฒซ ๋ฒˆ์งธ FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

์ด์ œ ์ง„์งœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! ๐ŸŽ‰ main.py ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•ด๋ด:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI!"}

@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

๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:8000์œผ๋กœ ์ ‘์†ํ•˜๋ฉด {"message": "Hello, FastAPI!"}๊ฐ€ ๋ณด์ผ ๊ฑฐ์•ผ. ๊ทธ๋ฆฌ๊ณ  ์—ฌ๊ธฐ์„œ ์ง„์งœ ๋งˆ๋ฒ• ๊ฐ™์€ ์ผ์ด ์ผ์–ด๋‚˜๋Š”๋ฐ... โœจ

๐ŸŽ ์ž๋™ ์ƒ์„ฑ๋˜๋Š” API ๋ฌธ์„œ

โ€ข Swagger UI: http://localhost:8000/docs
โ€ข ReDoc: http://localhost:8000/redoc

์ฝ”๋“œ ํ•œ ์ค„ ์ถ”๊ฐ€ ์•ˆ ํ–ˆ๋Š”๋ฐ ์™„์ „ํ•œ API ๋ฌธ์„œ๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋ผ! ์ด๊ฒŒ ๋ฐ”๋กœ FastAPI์˜ ๊ฐ€์žฅ ํฐ ์žฅ์  ์ค‘ ํ•˜๋‚˜์•ผ. ๋ฌธ์„œ ์ž‘์„ฑ์— ์‹œ๊ฐ„ ์“ธ ํ•„์š” ์—†์ด ์ฝ”๋“œ๋งŒ ์ž‘์„ฑํ•˜๋ฉด ๋!

์ฝ”๋“œ ๋ถ„์„ํ•˜๊ธฐ

์œ„ ์ฝ”๋“œ๋ฅผ ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณด์ž:

1. FastAPI ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
app = FastAPI()๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด. ์ด๊ฒŒ ์šฐ๋ฆฌ API์˜ ์ค‘์‹ฌ์ด ๋˜๋Š” ๊ฑฐ์ง€.

2. ๊ฒฝ๋กœ ์ž‘์—… ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ
@app.get("/")๋Š” GET ๋ฉ”์„œ๋“œ๋กœ ๋ฃจํŠธ ๊ฒฝ๋กœ์— ์ ‘๊ทผํ•  ๋•Œ ์‹คํ–‰๋  ํ•จ์ˆ˜๋ฅผ ์ง€์ •ํ•ด. POST๋ฉด @app.post(), PUT์ด๋ฉด @app.put() ์ด๋Ÿฐ ์‹์ด์•ผ.

3. ๊ฒฝ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜
{item_id}์ฒ˜๋Ÿผ ์ค‘๊ด„ํ˜ธ๋กœ ๊ฐ์‹ธ๋ฉด ๋™์  ๊ฒฝ๋กœ๊ฐ€ ๋ผ. ๊ทธ๋ฆฌ๊ณ  ํ•จ์ˆ˜ ๋งค๊ฐœ๋ณ€์ˆ˜์— item_id: int๋ผ๊ณ  ํƒ€์ž…์„ ์ง€์ •ํ•˜๋ฉด, FastAPI๊ฐ€ ์ž๋™์œผ๋กœ ๊ฒ€์ฆ๊นŒ์ง€ ํ•ด์ค˜!

4. ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜
q: str = None์€ ์„ ํƒ์  ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์•ผ. ?q=something ํ˜•ํƒœ๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์ง€.

FastAPI ์š”์ฒญ ์ฒ˜๋ฆฌ ํ๋ฆ„ ํด๋ผ์ด์–ธํŠธ HTTP ์š”์ฒญ GET /items/5 FastAPI ๋ผ์šฐํŒ… + ๊ฒ€์ฆ ํƒ€์ž… ์ฒดํฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ JSON ์‘๋‹ต ์ž๋™ ์ง๋ ฌํ™” ์‘๋‹ต ์ „์†ก ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋Š” ๊ฒƒ๋“ค โœ“ ํƒ€์ž… ๊ฒ€์ฆ ๋ฐ ๋ณ€ํ™˜ โœ“ ๋ฐ์ดํ„ฐ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” โœ“ API ๋ฌธ์„œ ์ž๋™ ์ƒ์„ฑ โœ“ ์—๋Ÿฌ ํ•ธ๋“ค๋ง โœ“ ์š”์ฒญ ๊ฒ€์ฆ โœ“ OpenAPI ์Šคํ‚ค๋งˆ ์ƒ์„ฑ

๐ŸŽจ Pydantic ๋ชจ๋ธ: ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ์˜ ํ•ต์‹ฌ

FastAPI์˜ ์ง„์งœ ํŒŒ์›Œ๋Š” Pydantic์—์„œ ๋‚˜์™€. Pydantic์€ Python ํƒ€์ž… ํžŒํŠธ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ์„ ์ž๋™์œผ๋กœ ํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. ์ด๊ฒŒ ์ •๋ง ํ˜๋ช…์ ์ด๊ฑฐ๋“ !

๊ธฐ๋ณธ Pydantic ๋ชจ๋ธ

์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ฐ›๋Š” API๋ฅผ ๋งŒ๋“ ๋‹ค๊ณ  ํ•ด๋ณด์ž:

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime

app = FastAPI()

class User(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: EmailStr
    full_name: Optional[str] = None
    age: int = Field(..., ge=0, le=150)
    created_at: datetime = Field(default_factory=datetime.now)
    
    class Config:
        schema_extra = {
            "example": {
                "username": "johndoe",
                "email": "john@example.com",
                "full_name": "John Doe",
                "age": 30
            }
        }

@app.post("/users/")
def create_user(user: User):
    return {
        "message": "User created successfully",
        "user": user
    }

์ด ์ฝ”๋“œ์˜ ๋ฉ‹์ง„ ์ ์€ ๋ญ๋ƒ๋ฉด:

๐ŸŽฏ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋Š” ๊ฒƒ๋“ค

โ€ข username์ด 3์ž ์ด์ƒ 50์ž ์ดํ•˜์ธ์ง€ ๊ฒ€์ฆ
โ€ข email์ด ์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ธ์ง€ ๊ฒ€์ฆ
โ€ข age๊ฐ€ 0~150 ์‚ฌ์ด์ธ์ง€ ๊ฒ€์ฆ
โ€ข ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๋ฉด ์ž๋™์œผ๋กœ 422 ์—๋Ÿฌ ๋ฐ˜ํ™˜
โ€ข API ๋ฌธ์„œ์— ์˜ˆ์‹œ๊นŒ์ง€ ์ž๋™์œผ๋กœ ํ‘œ์‹œ

์ค‘์ฒฉ ๋ชจ๋ธ๊ณผ ๋ณต์žกํ•œ ๊ตฌ์กฐ

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋” ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๊ฐ€ ํ•„์š”ํ•˜์ž–์•„? Pydantic์€ ์ด๊ฒƒ๋„ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€์›ํ•ด:

from typing import List
from pydantic import BaseModel

class Address(BaseModel):
    street: str
    city: str
    country: str
    postal_code: str

class Skill(BaseModel):
    name: str
    level: int = Field(..., ge=1, le=10)

class UserProfile(BaseModel):
    user: User
    address: Address
    skills: List[Skill]
    bio: Optional[str] = None

@app.post("/profiles/")
def create_profile(profile: UserProfile):
    return profile

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ค‘์ฒฉ๋œ JSON ๊ตฌ์กฐ๋„ ์ž๋™์œผ๋กœ ๊ฒ€์ฆ๋˜๊ณ  ํŒŒ์‹ฑ๋ผ. ์˜ˆ๋ฅผ ๋“ค์–ด ์ด๋Ÿฐ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด:

{
  "user": {
    "username": "developer123",
    "email": "dev@example.com",
    "age": 28
  },
  "address": {
    "street": "123 Tech Street",
    "city": "Seoul",
    "country": "South Korea",
    "postal_code": "12345"
  },
  "skills": [
    {"name": "Python", "level": 9},
    {"name": "FastAPI", "level": 8}
  ],
  "bio": "Passionate developer"
}

๋ชจ๋“  ํ•„๋“œ๊ฐ€ ์ž๋™์œผ๋กœ ๊ฒ€์ฆ๋˜๊ณ , ํƒ€์ž…์ด ๋งž์ง€ ์•Š์œผ๋ฉด ์ •ํ™•ํžˆ ์–ด๋””๊ฐ€ ๋ฌธ์ œ์ธ์ง€ ์•Œ๋ ค์ค˜. ์ด๊ฑฐ ์ง„์งœ ํŽธํ•ด! ๐Ÿ˜

๐Ÿ’ก ์‹ค์ „ ํŒ: ๋ชจ๋ธ ์žฌ์‚ฌ์šฉ

๊ฐ™์€ ๋ชจ๋ธ์„ ์š”์ฒญ๊ณผ ์‘๋‹ต์— ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ๋ณดํ†ต์€ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ์˜ˆ๋ฅผ ๋“ค์–ด UserCreate, UserUpdate, UserResponse ์ด๋Ÿฐ ์‹์œผ๋กœ ๋ง์ด์•ผ. ์™œ๋ƒํ•˜๋ฉด ์ƒ์„ฑํ•  ๋•Œ๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ•„์š”ํ•˜์ง€๋งŒ ์‘๋‹ต์—๋Š” ํฌํ•จํ•˜๋ฉด ์•ˆ ๋˜์ž–์•„?

๐Ÿ” ๊ฒฝ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜์™€ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜ ๋งˆ์Šคํ„ฐํ•˜๊ธฐ

API๋ฅผ ๋งŒ๋“ค๋‹ค ๋ณด๋ฉด ๋‹ค์–‘ํ•œ ๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์•ผ ํ•ด. FastAPI๋Š” ์ด๊ฑธ ์ •๋ง ์ง๊ด€์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜.

๊ฒฝ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜ (Path Parameters)

URL ๊ฒฝ๋กœ์˜ ์ผ๋ถ€๋กœ ์ „๋‹ฌ๋˜๋Š” ๊ฐ’๋“ค์ด์•ผ. RESTful API์—์„œ ๋ฆฌ์†Œ์Šค๋ฅผ ์‹๋ณ„ํ•  ๋•Œ ์ฃผ๋กœ ์‚ฌ์šฉํ•˜์ง€:

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
def read_item(
    item_id: int = Path(..., title="์•„์ดํ…œ ID", ge=1, le=1000)
):
    return {"item_id": item_id}

@app.get("/users/{user_id}/posts/{post_id}")
def read_user_post(
    user_id: int = Path(..., description="์‚ฌ์šฉ์ž ID"),
    post_id: int = Path(..., description="๊ฒŒ์‹œ๊ธ€ ID")
):
    return {
        "user_id": user_id,
        "post_id": post_id,
        "message": f"User {user_id}์˜ Post {post_id}"
    }

์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜ (Query Parameters)

URL ๋’ค์— ?key=value ํ˜•ํƒœ๋กœ ๋ถ™๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋“ค์ด์•ผ. ํ•„ํ„ฐ๋ง, ์ •๋ ฌ, ํŽ˜์ด์ง€๋„ค์ด์…˜์— ์ž์ฃผ ์“ฐ์—ฌ:

from fastapi import Query
from typing import Optional, List

@app.get("/items/")
def list_items(
    skip: int = Query(0, ge=0, description="๊ฑด๋„ˆ๋›ธ ํ•ญ๋ชฉ ์ˆ˜"),
    limit: int = Query(10, ge=1, le=100, description="๊ฐ€์ ธ์˜ฌ ํ•ญ๋ชฉ ์ˆ˜"),
    search: Optional[str] = Query(None, min_length=3, max_length=50),
    tags: List[str] = Query([], description="ํƒœ๊ทธ ํ•„ํ„ฐ")
):
    return {
        "skip": skip,
        "limit": limit,
        "search": search,
        "tags": tags
    }

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์š”์ฒญ๋“ค์„ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด:

GET /items/ โ†’ ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ
GET /items/?skip=20&limit=50 โ†’ ํŽ˜์ด์ง€๋„ค์ด์…˜
GET /items/?search=fastapi โ†’ ๊ฒ€์ƒ‰
GET /items/?tags=python&tags=api โ†’ ๋‹ค์ค‘ ํƒœ๊ทธ ํ•„ํ„ฐ

์š”์ฒญ ๋ณธ๋ฌธ (Request Body)

POST, PUT ๊ฐ™์€ ๋ฉ”์„œ๋“œ์—์„œ๋Š” ์š”์ฒญ ๋ณธ๋ฌธ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์ง€. ์ด๋•Œ Pydantic ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ:

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

@app.post("/items/")
def create_item(item: Item):
    item_dict = item.dict()
    if item.tax:
        price_with_tax = item.price + item.tax
        item_dict.update({"price_with_tax": price_with_tax})
    return item_dict

@app.put("/items/{item_id}")
def update_item(
    item_id: int,
    item: Item,
    q: Optional[str] = None
):
    result = {"item_id": item_id, **item.dict()}
    if q:
        result.update({"q": q})
    return result

์—ฌ๊ธฐ์„œ ์ฃผ๋ชฉํ•  ์ ์€ ๊ฒฝ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜, ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜, ์š”์ฒญ ๋ณธ๋ฌธ์„ ๋™์‹œ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์•ผ! FastAPI๊ฐ€ ์ž๋™์œผ๋กœ ๊ตฌ๋ถ„ํ•ด์„œ ์ฒ˜๋ฆฌํ•ด์ค˜.

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ

GET ์š”์ฒญ์—๋Š” ์š”์ฒญ ๋ณธ๋ฌธ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒŒ HTTP ํ‘œ์ค€์ด์•ผ. ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•  ๋•Œ๋Š” ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ์ƒ์„ฑ/์ˆ˜์ •ํ•  ๋•Œ๋งŒ ์š”์ฒญ ๋ณธ๋ฌธ์„ ์‚ฌ์šฉํ•˜์ž!

โšก ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ: async/await์˜ ํž˜

FastAPI์˜ ๊ฐ€์žฅ ํฐ ์žฅ์  ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์•ผ. ์ด๊ฒŒ ์™œ ์ค‘์š”ํ•˜๋ƒ๋ฉด, ๋™์‹œ์— ๋งŽ์€ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ๋•Œ ์„ฑ๋Šฅ์ด ์—„์ฒญ๋‚˜๊ฒŒ ์ฐจ์ด ๋‚˜๊ฑฐ๋“ !

๋™๊ธฐ vs ๋น„๋™๊ธฐ

์‰ฝ๊ฒŒ ์„ค๋ช…ํ•ด๋ณผ๊ฒŒ. ์นดํŽ˜์—์„œ ์ปคํ”ผ๋ฅผ ์ฃผ๋ฌธํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ด:

โ˜• ๋™๊ธฐ ๋ฐฉ์‹

์†๋‹˜ A์˜ ์ปคํ”ผ๊ฐ€ ์™„์ „ํžˆ ๋งŒ๋“ค์–ด์งˆ ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€, ๊ทธ ๋‹ค์Œ ์†๋‹˜ B์˜ ์ฃผ๋ฌธ์„ ๋ฐ›์Œ. ํ•œ ๋ฒˆ์— ํ•œ ๋ช…์”ฉ๋งŒ ์ฒ˜๋ฆฌ.

โšก ๋น„๋™๊ธฐ ๋ฐฉ์‹

์†๋‹˜ A์˜ ์ปคํ”ผ๋ฅผ ๋งŒ๋“œ๋Š” ๋™์•ˆ ์†๋‹˜ B, C, D์˜ ์ฃผ๋ฌธ๋„ ๋ฐ›๊ณ , ์ปคํ”ผ ๋จธ์‹ ์ด ๋Œ์•„๊ฐ€๋Š” ๋™์•ˆ ๋‹ค๋ฅธ ์ผ์„ ์ฒ˜๋ฆฌ. ํ›จ์”ฌ ํšจ์œจ์ !

FastAPI์—์„œ ๋น„๋™๊ธฐ ์‚ฌ์šฉํ•˜๊ธฐ

์ผ๋ฐ˜ ํ•จ์ˆ˜์™€ ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ๋น„๊ตํ•ด๋ณด์ž:

import time
import asyncio
from fastapi import FastAPI

app = FastAPI()

# ๋™๊ธฐ ํ•จ์ˆ˜
@app.get("/sync")
def sync_endpoint():
    time.sleep(2)  # 2์ดˆ ๋Œ€๊ธฐ (๋ธ”๋กœํ‚น)
    return {"message": "๋™๊ธฐ ์ฒ˜๋ฆฌ ์™„๋ฃŒ"}

# ๋น„๋™๊ธฐ ํ•จ์ˆ˜
@app.get("/async")
async def async_endpoint():
    await asyncio.sleep(2)  # 2์ดˆ ๋Œ€๊ธฐ (๋…ผ๋ธ”๋กœํ‚น)
    return {"message": "๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์™„๋ฃŒ"}

๊ฒ‰๋ณด๊ธฐ์—” ๋น„์Šทํ•ด ๋ณด์ด์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” ์—„์ฒญ๋‚œ ์ฐจ์ด๊ฐ€ ์žˆ์–ด. ๋™๊ธฐ ํ•จ์ˆ˜๋Š” 2์ดˆ ๋™์•ˆ ์•„๋ฌด๊ฒƒ๋„ ๋ชป ํ•˜๊ณ  ๊ธฐ๋‹ค๋ฆฌ์ง€๋งŒ, ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋Š” ๊ทธ ์‹œ๊ฐ„์— ๋‹ค๋ฅธ ์š”์ฒญ๋“ค์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ !

์‹ค์ „ ์˜ˆ์ œ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ

์‹ค์ œ๋กœ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ํŒจํ„ด์„ ๋ณด์—ฌ์ค„๊ฒŒ. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…์€ I/O ์ž‘์—…์ด๋ผ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ผ:

from fastapi import FastAPI
from typing import List
import asyncio

app = FastAPI()

# ๊ฐ€์ƒ์˜ ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ•จ์ˆ˜
async def fetch_user_from_db(user_id: int):
    await asyncio.sleep(0.1)  # DB ์ฟผ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    return {"id": user_id, "name": f"User {user_id}"}

async def fetch_posts_from_db(user_id: int):
    await asyncio.sleep(0.1)  # DB ์ฟผ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    return [
        {"id": 1, "title": "Post 1"},
        {"id": 2, "title": "Post 2"}
    ]

@app.get("/users/{user_id}/complete")
async def get_user_complete(user_id: int):
    # ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰!
    user, posts = await asyncio.gather(
        fetch_user_from_db(user_id),
        fetch_posts_from_db(user_id)
    )
    
    return {
        "user": user,
        "posts": posts
    }

asyncio.gather()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ๋Ÿฌ ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋™์‹œ์— ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด. ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ํ•˜๋ฉด 0.2์ดˆ๊ฐ€ ๊ฑธ๋ฆฌ์ง€๋งŒ, ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•˜๋ฉด 0.1์ดˆ๋งŒ ๊ฑธ๋ ค!

๐Ÿš€ ์„ฑ๋Šฅ ํŒ

โ€ข I/O ์ž‘์—…(DB, API ํ˜ธ์ถœ, ํŒŒ์ผ ์ฝ๊ธฐ)์€ ๋น„๋™๊ธฐ๋กœ
โ€ข CPU ์ง‘์•ฝ์  ์ž‘์—…(๋ณต์žกํ•œ ๊ณ„์‚ฐ)์€ ๋™๊ธฐ๋กœ
โ€ข ์™ธ๋ถ€ API ํ˜ธ์ถœํ•  ๋•Œ๋Š” httpx ๊ฐ™์€ ๋น„๋™๊ธฐ HTTP ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ
โ€ข ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” SQLAlchemy์˜ ๋น„๋™๊ธฐ ๋ฒ„์ „์ด๋‚˜ Tortoise ORM ์‚ฌ์šฉ

์–ธ์ œ async๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ• ๊นŒ?

์ด๊ฒŒ ๋งŽ์ด ํ—ท๊ฐˆ๋ฆฌ๋Š” ๋ถ€๋ถ„์ธ๋ฐ, ๊ฐ„๋‹จํ•œ ๊ฐ€์ด๋“œ๋ฅผ ์ค„๊ฒŒ:

โœ… async ์‚ฌ์šฉํ•˜๊ธฐ ์ข‹์€ ๊ฒฝ์šฐ โ€ข ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ
โ€ข ์™ธ๋ถ€ API ํ˜ธ์ถœ
โ€ข ํŒŒ์ผ I/O
โ€ข ๋„คํŠธ์›Œํฌ ์ž‘์—…
โ€ข ์—ฌ๋Ÿฌ ์ž‘์—…์„ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ๋•Œ

โŒ async ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋˜๋Š” ๊ฒฝ์šฐ
โ€ข ๊ฐ„๋‹จํ•œ ๊ณ„์‚ฐ
โ€ข ๋ฉ”๋ชจ๋ฆฌ ๋‚ด ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
โ€ข CPU ์ง‘์•ฝ์  ์ž‘์—…
โ€ข ๋™๊ธฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์„ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค๋ฉด, ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์€ ์„ ํƒ์ด ์•„๋‹ˆ๋ผ ํ•„์ˆ˜์•ผ! ๐Ÿ’ช

๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™: SQLAlchemy์™€ ํ•จ๊ป˜

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•ด์•ผ ํ•˜์ž–์•„? FastAPI์™€ SQLAlchemy๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ์กฐํ•ฉ์ด ๋ผ.

๊ธฐ๋ณธ ์„ค์ •

๋จผ์ € ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋“ค์„ ์„ค์น˜ํ•˜์ž:

pip install sqlalchemy databases asyncpg

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณด์ž (database.py):

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# SQLite ์‚ฌ์šฉ (๊ฐœ๋ฐœ์šฉ)
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"

# PostgreSQL ์‚ฌ์šฉ (ํ”„๋กœ๋•์…˜)
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/dbname"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL,
    connect_args={"check_same_thread": False}  # SQLite์šฉ
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

# ์˜์กด์„ฑ
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

๋ชจ๋ธ ์ •์˜ํ•˜๊ธฐ

์ด์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ์„ ๋งŒ๋“ค์–ด๋ณด์ž (models.py):

from sqlalchemy import Boolean, Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from database import Base

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    username = Column(String, unique=True, index=True)
    hashed_password = Column(String)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    posts = relationship("Post", back_populates="author")

class Post(Base):
    __tablename__ = "posts"
    
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    content = Column(String)
    author_id = Column(Integer, ForeignKey("users.id"))
    created_at = Column(DateTime, default=datetime.utcnow)
    
    author = relationship("User", back_populates="posts")

Pydantic ์Šคํ‚ค๋งˆ ์ •์˜

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ๊ณผ ๋ณ„๋„๋กœ API ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•ด์•ผ ํ•ด (schemas.py):

from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import List, Optional

# ๊ธฐ๋ณธ ์Šคํ‚ค๋งˆ
class PostBase(BaseModel):
    title: str
    content: str

class PostCreate(PostBase):
    pass

class Post(PostBase):
    id: int
    author_id: int
    created_at: datetime
    
    class Config:
        orm_mode = True  # SQLAlchemy ๋ชจ๋ธ๊ณผ ํ˜ธํ™˜

class UserBase(BaseModel):
    email: EmailStr
    username: str

class UserCreate(UserBase):
    password: str

class User(UserBase):
    id: int
    is_active: bool
    created_at: datetime
    posts: List[Post] = []
    
    class Config:
        orm_mode = True

orm_mode = True๊ฐ€ ์ค‘์š”ํ•ด! ์ด๊ฒŒ ์žˆ์–ด์•ผ SQLAlchemy ๋ชจ๋ธ์„ Pydantic ๋ชจ๋ธ๋กœ ์ž๋™ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ .

CRUD ์ž‘์—… ๊ตฌํ˜„

์ด์ œ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…์„ ํ•˜๋Š” ํ•จ์ˆ˜๋“ค์„ ๋งŒ๋“ค์ž (crud.py):

from sqlalchemy.orm import Session
from typing import List, Optional
import models, schemas
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def get_password_hash(password: str) -> str:
    return pwd_context.hash(password)

def get_user(db: Session, user_id: int) -> Optional[models.User]:
    return db.query(models.User).filter(models.User.id == user_id).first()

def get_user_by_email(db: Session, email: str) -> Optional[models.User]:
    return db.query(models.User).filter(models.User.email == email).first()

def get_users(db: Session, skip: int = 0, limit: int = 100) -> List[models.User]:
    return db.query(models.User).offset(skip).limit(limit).all()

def create_user(db: Session, user: schemas.UserCreate) -> models.User:
    hashed_password = get_password_hash(user.password)
    db_user = models.User(
        email=user.email,
        username=user.username,
        hashed_password=hashed_password
    )
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user

def create_post(db: Session, post: schemas.PostCreate, user_id: int) -> models.Post:
    db_post = models.Post(**post.dict(), author_id=user_id)
    db.add(db_post)
    db.commit()
    db.refresh(db_post)
    return db_post

def get_posts(db: Session, skip: int = 0, limit: int = 100) -> List[models.Post]:
    return db.query(models.Post).offset(skip).limit(limit).all()

API ์—”๋“œํฌ์ธํŠธ ๊ตฌํ˜„

๋“œ๋””์–ด ๋ชจ๋“  ๊ฑธ ํ•ฉ์ณ์„œ API๋ฅผ ๋งŒ๋“ค ์‹œ๊ฐ„์ด์•ผ! (main.py):

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
import crud, models, schemas
from database import SessionLocal, engine, get_db

# ํ…Œ์ด๋ธ” ์ƒ์„ฑ
models.Base.metadata.create_all(bind=engine)

app = FastAPI(title="๋ธ”๋กœ๊ทธ API", version="1.0.0")

# ์‚ฌ์šฉ์ž ๊ด€๋ จ ์—”๋“œํฌ์ธํŠธ
@app.post("/users/", response_model=schemas.User, status_code=201)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
    db_user = crud.get_user_by_email(db, email=user.email)
    if db_user:
        raise HTTPException(status_code=400, detail="์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค")
    return crud.create_user(db=db, user=user)

@app.get("/users/", response_model=List[schemas.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    users = crud.get_users(db, skip=skip, limit=limit)
    return users

@app.get("/users/{user_id}", response_model=schemas.User)
def read_user(user_id: int, db: Session = Depends(get_db)):
    db_user = crud.get_user(db, user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
    return db_user

# ๊ฒŒ์‹œ๊ธ€ ๊ด€๋ จ ์—”๋“œํฌ์ธํŠธ
@app.post("/users/{user_id}/posts/", response_model=schemas.Post, status_code=201)
def create_post_for_user(
    user_id: int,
    post: schemas.PostCreate,
    db: Session = Depends(get_db)
):
    db_user = crud.get_user(db, user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
    return crud.create_post(db=db, post=post, user_id=user_id)

@app.get("/posts/", response_model=List[schemas.Post])
def read_posts(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    posts = crud.get_posts(db, skip=skip, limit=limit)
    return posts

๐ŸŽฏ ์˜์กด์„ฑ ์ฃผ์ž… (Dependency Injection)

Depends(get_db)๊ฐ€ ๋ฐ”๋กœ FastAPI์˜ ์˜์กด์„ฑ ์ฃผ์ž… ์‹œ์Šคํ…œ์ด์•ผ. ์ด๊ฑธ ์‚ฌ์šฉํ•˜๋ฉด:
โ€ข ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ธ์…˜์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ  ์ •๋ฆฌ
โ€ข ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ
โ€ข ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์›Œ์ง
โ€ข ์ธ์ฆ, ๊ถŒํ•œ ์ฒดํฌ ๋“ฑ์„ ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„ ๊ฐ€๋Šฅ

์ด์ œ ์™„์ „ํ•œ CRUD API๊ฐ€ ์™„์„ฑ๋์–ด! ์‚ฌ์šฉ์ž๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์กฐํšŒํ•˜๊ณ , ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์ง€. ๐ŸŽ‰

๐Ÿ”’ ์ธ์ฆ๊ณผ ๋ณด์•ˆ: JWT ํ† ํฐ ๊ตฌํ˜„ํ•˜๊ธฐ

API๋ฅผ ๋งŒ๋“ค์—ˆ์œผ๋ฉด ์ด์ œ ๋ณด์•ˆ์„ ์‹ ๊ฒฝ ์จ์•ผ ํ•ด. ์•„๋ฌด๋‚˜ ์ ‘๊ทผํ•˜๋ฉด ์•ˆ ๋˜๋Š” ์—”๋“œํฌ์ธํŠธ๋“ค์ด ์žˆ์ž–์•„? JWT(JSON Web Token)๋ฅผ ์‚ฌ์šฉํ•œ ์ธ์ฆ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ด๋ณด์ž!

ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜

pip install python-jose[cryptography] passlib[bcrypt] python-multipart

๋ณด์•ˆ ์„ค์ •

๋จผ์ € ๋ณด์•ˆ ๊ด€๋ จ ์„ค์ •์„ ๋งŒ๋“ค์ž (security.py):

from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session

# ์„ค์ •๊ฐ’ (์‹ค์ œ๋กœ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌ!)
SECRET_KEY = "your-secret-key-keep-it-secret"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        return payload
    except JWTError:
        return None

โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ

โ€ข SECRET_KEY๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋ง๊ณ  ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌ!
โ€ข ์ถฉ๋ถ„ํžˆ ๊ธด ๋žœ๋ค ๋ฌธ์ž์—ด ์‚ฌ์šฉ (์ตœ์†Œ 32์ž ์ด์ƒ)
โ€ข HTTPS ์‚ฌ์šฉ ํ•„์ˆ˜
โ€ข ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์ ์ ˆํžˆ ์„ค์ •

๋กœ๊ทธ์ธ ์—”๋“œํฌ์ธํŠธ

์ด์ œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด๋ณด์ž:

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from datetime import timedelta
import crud, models, schemas
from database import get_db
from security import (
    verify_password,
    create_access_token,
    ACCESS_TOKEN_EXPIRE_MINUTES
)

app = FastAPI()

@app.post("/token")
def login(
    form_data: OAuth2PasswordRequestForm = Depends(),
    db: Session = Depends(get_db)
):
    # ์‚ฌ์šฉ์ž ํ™•์ธ
    user = crud.get_user_by_email(db, email=form_data.username)
    if not user or not verify_password(form_data.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
    # ์•ก์„ธ์Šค ํ† ํฐ ์ƒ์„ฑ
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.email},
        expires_delta=access_token_expires
    )
    
    return {
        "access_token": access_token,
        "token_type": "bearer"
    }

ํ˜„์žฌ ์‚ฌ์šฉ์ž ๊ฐ€์ ธ์˜ค๊ธฐ

ํ† ํฐ์œผ๋กœ๋ถ€ํ„ฐ ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์ž:

from fastapi import Depends, HTTPException, status
from sqlalchemy.orm import Session
from security import oauth2_scheme, decode_access_token
from database import get_db
import crud

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: Session = Depends(get_db)
):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="์ธ์ฆ ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค",
        headers={"WWW-Authenticate": "Bearer"},
    )
    
    payload = decode_access_token(token)
    if payload is None:
        raise credentials_exception
    
    email: str = payload.get("sub")
    if email is None:
        raise credentials_exception
    
    user = crud.get_user_by_email(db, email=email)
    if user is None:
        raise credentials_exception
    
    return user

async def get_current_active_user(
    current_user: models.User = Depends(get_current_user)
):
    if not current_user.is_active:
        raise HTTPException(status_code=400, detail="๋น„ํ™œ์„ฑํ™”๋œ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค")
    return current_user

๋ณดํ˜ธ๋œ ์—”๋“œํฌ์ธํŠธ ๋งŒ๋“ค๊ธฐ

์ด์ œ ์ธ์ฆ์ด ํ•„์š”ํ•œ ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด:

@app.get("/users/me", response_model=schemas.User)
async def read_users_me(
    current_user: models.User = Depends(get_current_active_user)
):
    return current_user

@app.post("/posts/", response_model=schemas.Post)
async def create_post(
    post: schemas.PostCreate,
    current_user: models.User = Depends(get_current_active_user),
    db: Session = Depends(get_db)
):
    return crud.create_post(db=db, post=post, user_id=current_user.id)

@app.delete("/posts/{post_id}")
async def delete_post(
    post_id: int,
    current_user: models.User = Depends(get_current_active_user),
    db: Session = Depends(get_db)
):
    post = db.query(models.Post).filter(models.Post.id == post_id).first()
    if not post:
        raise HTTPException(status_code=404, detail="๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
    
    if post.author_id != current_user.id:
        raise HTTPException(status_code=403, detail="๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค")
    
    db.delete(post)
    db.commit()
    return {"message": "๊ฒŒ์‹œ๊ธ€์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Depends(get_current_active_user)๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋ณดํ˜ธํ•  ์ˆ˜ ์žˆ์–ด. ์ •๋ง ๊ฐ„๋‹จํ•˜์ง€? ๐Ÿ›ก๏ธ

๐Ÿ’ก ์‹ค์ „ ํŒ: ๊ถŒํ•œ ๊ด€๋ฆฌ

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์—ญํ•  ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด(RBAC)๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ์˜ˆ๋ฅผ ๋“ค์–ด admin, user, guest ๊ฐ™์€ ์—ญํ• ์„ ๋งŒ๋“ค๊ณ , ๊ฐ ์—ญํ• ์— ๋”ฐ๋ผ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ œํ•œํ•˜๋Š” ๊ฑฐ์ง€. ์ด๊ฒƒ๋„ ์˜์กด์„ฑ ์ฃผ์ž…์œผ๋กœ ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด!

JWT ์ธ์ฆ ํ๋ฆ„ ํด๋ผ์ด์–ธํŠธ ๋กœ๊ทธ์ธ ์š”์ฒญ POST /token email + password ์„œ๋ฒ„ ์ธ์ฆ ํ™•์ธ DB ์กฐํšŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ์ž ํ™•์ธ ์‚ฌ์šฉ์ž ์ •๋ณด JWT ์ƒ์„ฑ ํ† ํฐ ๋ฐœ๊ธ‰ access_token ๋ฐ˜ํ™˜ ํด๋ผ์ด์–ธํŠธ ํ† ํฐ ์ €์žฅ ํด๋ผ์ด์–ธํŠธ API ์š”์ฒญ GET /users/me Authorization: Bearer token ์„œ๋ฒ„ ํ† ํฐ ๊ฒ€์ฆ ํ† ํฐ ๋””์ฝ”๋”ฉ ์‘๋‹ต ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ JSON ์‘๋‹ต JWT ํ† ํฐ ๊ตฌ์กฐ Header: ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ •๋ณด Payload: ์‚ฌ์šฉ์ž ์ •๋ณด (sub, exp ๋“ฑ) Signature: ์„œ๋ช… (๋ณ€์กฐ ๋ฐฉ์ง€)

๐Ÿ“ ์—๋Ÿฌ ํ•ธ๋“ค๋ง๊ณผ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

์™„๋ฒฝํ•œ API๋Š” ์—๋Ÿฌ๋„ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด. ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๊ฒŒ ์ •๋ง ์ค‘์š”ํ•˜๊ฑฐ๋“ !

๊ธฐ๋ณธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

FastAPI๋Š” HTTPException์„ ์ œ๊ณตํ•ด. ์ด๊ฑธ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ„๋‹จํ•˜๊ฒŒ ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์–ด:

from fastapi import FastAPI, HTTPException, status

app = FastAPI()

@app.get("/items/{item_id}")
def read_item(item_id: int):
    if item_id not in items_db:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Item {item_id}๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค",
            headers={"X-Error": "Item not found"}
        )
    return items_db[item_id]

์ปค์Šคํ…€ ์˜ˆ์™ธ ๋งŒ๋“ค๊ธฐ

ํ”„๋กœ์ ํŠธ์— ํŠนํ™”๋œ ์˜ˆ์™ธ๋ฅผ ๋งŒ๋“ค๋ฉด ๋” ๊น”๋”ํ•ด์ ธ:

from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError

app = FastAPI()

# ์ปค์Šคํ…€ ์˜ˆ์™ธ ํด๋ž˜์Šค
class ItemNotFoundException(Exception):
    def __init__(self, item_id: int):
        self.item_id = item_id

class InsufficientPermissionException(Exception):
    def __init__(self, message: str = "๊ถŒํ•œ์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"):
        self.message = message

# ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก
@app.exception_handler(ItemNotFoundException)
async def item_not_found_handler(request: Request, exc: ItemNotFoundException):
    return JSONResponse(
        status_code=status.HTTP_404_NOT_FOUND,
        content={
            "error": "ItemNotFound",
            "message": f"Item {exc.item_id}๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค",
            "item_id": exc.item_id
        }
    )

@app.exception_handler(InsufficientPermissionException)
async def permission_handler(request: Request, exc: InsufficientPermissionException):
    return JSONResponse(
        status_code=status.HTTP_403_FORBIDDEN,
        content={
            "error": "InsufficientPermission",
            "message": exc.message
        }
    )

# ๊ฒ€์ฆ ์—๋Ÿฌ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content={
            "error": "ValidationError",
            "message": "์ž…๋ ฅ ๋ฐ์ดํ„ฐ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค",
            "details": exc.errors()
        }
    )

# ์‚ฌ์šฉ ์˜ˆ์‹œ
@app.get("/items/{item_id}")
def read_item(item_id: int):
    if item_id not in items_db:
        raise ItemNotFoundException(item_id)
    return items_db[item_id]

๐ŸŽฏ ์—๋Ÿฌ ์‘๋‹ต ์ผ๊ด€์„ฑ ์œ ์ง€ํ•˜๊ธฐ

๋ชจ๋“  ์—๋Ÿฌ ์‘๋‹ต์„ ์ผ๊ด€๋œ ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์‰ฌ์›Œ์ ธ:

{
  "error": "ErrorType",
  "message": "์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ๋ฉ”์‹œ์ง€",
  "details": {...}  // ์„ ํƒ์ 
}

์ „์—ญ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ

์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์—๋Ÿฌ๋„ ์žก์•„๋‚ผ ์ˆ˜ ์žˆ์–ด:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import logging

logger = logging.getLogger(__name__)

app = FastAPI()

@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    logger.error(f"Unexpected error: {exc}", exc_info=True)
    return JSONResponse(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content={
            "error": "InternalServerError",
            "message": "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."
        }
    )

โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ƒ์„ธํ•œ ์—๋Ÿฌ ์ •๋ณด๋ฅผ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœํ•˜์ง€ ๋ง์ž! ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๋‚˜ ๋‚ด๋ถ€ ๊ตฌ์กฐ ์ •๋ณด๊ฐ€ ์œ ์ถœ๋˜๋ฉด ๋ณด์•ˆ ์œ„ํ—˜์ด ๋  ์ˆ˜ ์žˆ์–ด. ๋กœ๊ทธ์—๋งŒ ๋‚จ๊ธฐ๊ณ  ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ์ผ๋ฐ˜์ ์ธ ๋ฉ”์‹œ์ง€๋งŒ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด.

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

API๋ฅผ ๋งŒ๋“ค์—ˆ์œผ๋ฉด ์ด์ œ ๋น ๋ฅด๊ฒŒ ๋งŒ๋“ค์–ด์•ผ์ง€! ์„ฑ๋Šฅ ์ตœ์ ํ™”๋Š” ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ์ง๊ฒฐ๋˜๋Š” ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ. โšก

1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์ตœ์ ํ™”

๊ฐ€์žฅ ํฐ ๋ณ‘๋ชฉ์€ ๋ณดํ†ต ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์•ผ. N+1 ๋ฌธ์ œ๋ฅผ ์กฐ์‹ฌํ•ด์•ผ ํ•ด:

from sqlalchemy.orm import Session, joinedload

# โŒ ๋‚˜์œ ์˜ˆ: N+1 ๋ฌธ์ œ
@app.get("/users-with-posts-bad")
def get_users_bad(db: Session = Depends(get_db)):
    users = db.query(User).all()
    result = []
    for user in users:  # N๋ฒˆ์˜ ์ถ”๊ฐ€ ์ฟผ๋ฆฌ ๋ฐœ์ƒ!
        result.append({
            "user": user,
            "posts": user.posts  # ๊ฐ ์‚ฌ์šฉ์ž๋งˆ๋‹ค ์ฟผ๋ฆฌ ์‹คํ–‰
        })
    return result

# โœ… ์ข‹์€ ์˜ˆ: Eager Loading
@app.get("/users-with-posts-good")
def get_users_good(db: Session = Depends(get_db)):
    users = db.query(User).options(
        joinedload(User.posts)  # ํ•œ ๋ฒˆ์˜ JOIN ์ฟผ๋ฆฌ๋กœ ํ•ด๊ฒฐ!
    ).all()
    return users

2. ์บ์‹ฑ ํ™œ์šฉํ•˜๊ธฐ

์ž์ฃผ ์กฐํšŒ๋˜์ง€๋งŒ ์ž˜ ๋ณ€ํ•˜์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ๋Š” ์บ์‹ฑํ•˜์ž:

from functools import lru_cache
from fastapi import FastAPI
import redis
import json

app = FastAPI()

# ๋ฉ”๋ชจ๋ฆฌ ์บ์‹ฑ (๊ฐ„๋‹จํ•œ ๊ฒฝ์šฐ)
@lru_cache(maxsize=128)
def get_expensive_data(key: str):
    # ๋น„์šฉ์ด ํฐ ์—ฐ์‚ฐ
    return expensive_computation(key)

# Redis ์บ์‹ฑ (๋ถ„์‚ฐ ํ™˜๊ฒฝ)
redis_client = redis.Redis(host='localhost', port=6379, db=0)

@app.get("/products/{product_id}")
async def get_product(product_id: int, db: Session = Depends(get_db)):
    # ์บ์‹œ ํ™•์ธ
    cache_key = f"product:{product_id}"
    cached = redis_client.get(cache_key)
    
    if cached:
        return json.loads(cached)
    
    # DB์—์„œ ์กฐํšŒ
    product = db.query(Product).filter(Product.id == product_id).first()
    if not product:
        raise HTTPException(status_code=404)
    
    # ์บ์‹œ ์ €์žฅ (1์‹œ๊ฐ„)
    redis_client.setex(
        cache_key,
        3600,
        json.dumps(product.dict())
    )
    
    return product

3. ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ตฌํ˜„

๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋А๋ ค์ ธ. ํŽ˜์ด์ง€๋„ค์ด์…˜์€ ํ•„์ˆ˜์•ผ:

from typing import Generic, TypeVar, List
from pydantic import BaseModel
from math import ceil

T = TypeVar('T')

class PaginatedResponse(BaseModel, Generic[T]):
    items: List[T]
    total: int
    page: int
    page_size: int
    total_pages: int

@app.get("/items/", response_model=PaginatedResponse[schemas.Item])
def list_items(
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    db: Session = Depends(get_db)
):
    # ์ „์ฒด ๊ฐœ์ˆ˜ ์กฐํšŒ
    total = db.query(Item).count()
    
    # ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ ์šฉ
    items = db.query(Item)\
        .offset((page - 1) * page_size)\
        .limit(page_size)\
        .all()
    
    return PaginatedResponse(
        items=items,
        total=total,
        page=page,
        page_size=page_size,
        total_pages=ceil(total / page_size)
    )

4. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…

์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฒ˜๋ฆฌํ•˜์ž:

from fastapi import BackgroundTasks
import time

def send_email(email: str, message: str):
    time.sleep(3)  # ์ด๋ฉ”์ผ ์ „์†ก ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    print(f"Email sent to {email}: {message}")

@app.post("/send-notification/")
async def send_notification(
    email: str,
    background_tasks: BackgroundTasks
):
    # ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰
    background_tasks.add_task(send_email, email, "Welcome!")
    
    # ์ฆ‰์‹œ ์‘๋‹ต ๋ฐ˜ํ™˜
    return {"message": "์•Œ๋ฆผ์ด ์ „์†ก๋ฉ๋‹ˆ๋‹ค"}

๐Ÿ’ก ์„ฑ๋Šฅ ์ธก์ •ํ•˜๊ธฐ

์ตœ์ ํ™”ํ•˜๊ธฐ ์ „์— ๋จผ์ € ์ธก์ •ํ•˜์ž! ์–ด๋””๊ฐ€ ๋А๋ฆฐ์ง€ ๋ชจ๋ฅด๋ฉด ์ตœ์ ํ™”ํ•  ์ˆ˜ ์—†์–ด. FastAPI๋Š” ๋ฏธ๋“ค์›จ์–ด๋กœ ์‰ฝ๊ฒŒ ์„ฑ๋Šฅ์„ ์ธก์ •ํ•  ์ˆ˜ ์žˆ์–ด:

import time
from fastapi import Request

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    print(f"{request.method} {request.url.path}: {process_time:.4f}s")
    return response

5. ์••์ถ• ์‚ฌ์šฉํ•˜๊ธฐ

์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ์••์ถ•ํ•˜๋ฉด ๋„คํŠธ์›Œํฌ ์ „์†ก ์‹œ๊ฐ„์„ ์ค„์ผ ์ˆ˜ ์žˆ์–ด:

from fastapi.middleware.gzip import GZipMiddleware

app.add_middleware(GZipMiddleware, minimum_size=1000)

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด 1KB ์ด์ƒ์˜ ์‘๋‹ต์€ ์ž๋™์œผ๋กœ gzip ์••์ถ•๋ผ. ํŠนํžˆ JSON ๋ฐ์ดํ„ฐ๋Š” ์••์ถ•๋ฅ ์ด ์ข‹์•„์„œ ํšจ๊ณผ๊ฐ€ ํฌ์ง€!

๐Ÿงช ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ

์ข‹์€ API๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์–ด์•ผ ํ•ด. FastAPI๋Š” ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์ •๋ง ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ ธ ์žˆ์–ด! โœ…

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ •

pip install pytest httpx

ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณด์ž (test_main.py):

from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from main import app, get_db
from database import Base

# ํ…Œ์ŠคํŠธ์šฉ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# ํ…Œ์ด๋ธ” ์ƒ์„ฑ
Base.metadata.create_all(bind=engine)

# ์˜์กด์„ฑ ์˜ค๋ฒ„๋ผ์ด๋“œ
def override_get_db():
    try:
        db = TestingSessionLocal()
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db

# ํ…Œ์ŠคํŠธ ํด๋ผ์ด์–ธํŠธ
client = TestClient(app)

# ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค
def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello, FastAPI!"}

def test_create_user():
    response = client.post(
        "/users/",
        json={
            "email": "test@example.com",
            "username": "testuser",
            "password": "testpass123"
        }
    )
    assert response.status_code == 201
    data = response.json()
    assert data["email"] == "test@example.com"
    assert "id" in data

def test_create_user_duplicate_email():
    # ์ฒซ ๋ฒˆ์งธ ์‚ฌ์šฉ์ž ์ƒ์„ฑ
    client.post(
        "/users/",
        json={
            "email": "duplicate@example.com",
            "username": "user1",
            "password": "pass123"
        }
    )
    
    # ๊ฐ™์€ ์ด๋ฉ”์ผ๋กœ ๋‹ค์‹œ ์ƒ์„ฑ ์‹œ๋„
    response = client.post(
        "/users/",
        json={
            "email": "duplicate@example.com",
            "username": "user2",
            "password": "pass456"
        }
    )
    assert response.status_code == 400
    assert "์ด๋ฏธ ๋“ฑ๋ก๋œ" in response.json()["detail"]

def test_read_user():
    # ์‚ฌ์šฉ์ž ์ƒ์„ฑ
    create_response = client.post(
        "/users/",
        json={
            "email": "read@example.com",
            "username": "readuser",
            "password": "pass123"
        }
    )
    user_id = create_response.json()["id"]
    
    # ์‚ฌ์šฉ์ž ์กฐํšŒ
    response = client.get(f"/users/{user_id}")
    assert response.status_code == 200
    data = response.json()
    assert data["id"] == user_id
    assert data["email"] == "read@example.com"

def test_read_nonexistent_user():
    response = client.get("/users/99999")
    assert response.status_code == 404

์ธ์ฆ์ด ํ•„์š”ํ•œ ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ

def test_login():
    # ์‚ฌ์šฉ์ž ์ƒ์„ฑ
    client.post(
        "/users/",
        json={
            "email": "login@example.com",
            "username": "loginuser",
            "password": "password123"
        }
    )
    
    # ๋กœ๊ทธ์ธ
    response = client.post(
        "/token",
        data={
            "username": "login@example.com",
            "password": "password123"
        }
    )
    assert response.status_code == 200
    assert "access_token" in response.json()
    return response.json()["access_token"]

def test_protected_endpoint():
    token = test_login()
    
    # ์ธ์ฆ๋œ ์š”์ฒญ
    response = client.get(
        "/users/me",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 200
    assert response.json()["email"] == "login@example.com"

def test_protected_endpoint_without_token():
    response = client.get("/users/me")
    assert response.status_code == 401

ํ”ฝ์Šค์ฒ˜ ํ™œ์šฉํ•˜๊ธฐ

pytest์˜ ํ”ฝ์Šค์ฒ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ๋” ๊น”๋”ํ•ด์ ธ:

import pytest

@pytest.fixture
def test_user():
    response = client.post(
        "/users/",
        json={
            "email": "fixture@example.com",
            "username": "fixtureuser",
            "password": "pass123"
        }
    )
    return response.json()

@pytest.fixture
def auth_token(test_user):
    response = client.post(
        "/token",
        data={
            "username": test_user["email"],
            "password": "pass123"
        }
    )
    return response.json()["access_token"]

def test_with_fixtures(test_user, auth_token):
    response = client.get(
        f"/users/{test_user['id']}",
        headers={"Authorization": f"Bearer {auth_token}"}
    )
    assert response.status_code == 200

ํ…Œ์ŠคํŠธ ์‹คํ–‰์€ ๊ฐ„๋‹จํ•ด:

# ๋ชจ๋“  ํ…Œ์ŠคํŠธ ์‹คํ–‰
pytest

# ํŠน์ • ํŒŒ์ผ๋งŒ ์‹คํ–‰
pytest test_main.py

# ์ƒ์„ธ ์ถœ๋ ฅ
pytest -v

# ์ปค๋ฒ„๋ฆฌ์ง€ ํ™•์ธ
pytest --cov=main

๐ŸŽฏ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ํŒ

โ€ข ๊ฐ ์—”๋“œํฌ์ธํŠธ๋งˆ๋‹ค ์„ฑ๊ณต ์ผ€์ด์Šค์™€ ์‹คํŒจ ์ผ€์ด์Šค ๋ชจ๋‘ ํ…Œ์ŠคํŠธ
โ€ข ๊ฒฝ๊ณ„๊ฐ’ ํ…Œ์ŠคํŠธ (๋นˆ ๋ฌธ์ž์—ด, ์ตœ๋Œ€๊ฐ’, ์ตœ์†Œ๊ฐ’ ๋“ฑ)
โ€ข ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ์‹ค์ œ DB์™€ ๋ถ„๋ฆฌ
โ€ข ํ…Œ์ŠคํŠธ ๊ฐ„ ๋…๋ฆฝ์„ฑ ์œ ์ง€ (๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์— ์˜ํ–ฅ X)
โ€ข CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ํ…Œ์ŠคํŠธ ์ž๋™ํ™”

๐Ÿ“š API ๋ฌธ์„œํ™”์™€ ๋ฒ„์ „ ๊ด€๋ฆฌ

FastAPI์˜ ์ž๋™ ๋ฌธ์„œํ™”๋Š” ์ •๋ง ํ›Œ๋ฅญํ•˜์ง€๋งŒ, ๋” ๋‚˜์€ ๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์–ด!

์ƒ์„ธํ•œ API ๋ฌธ์„œ ์ž‘์„ฑ

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(
    title="๋ธ”๋กœ๊ทธ API",
    description="""
    ## ์†Œ๊ฐœ
    ์ด API๋Š” ๋ธ”๋กœ๊ทธ ํ”Œ๋žซํผ์„ ์œ„ํ•œ RESTful API์ž…๋‹ˆ๋‹ค.
    
    ## ์ฃผ์š” ๊ธฐ๋Šฅ
    * **์‚ฌ์šฉ์ž ๊ด€๋ฆฌ**: ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ํ”„๋กœํ•„ ๊ด€๋ฆฌ
    * **๊ฒŒ์‹œ๊ธ€**: CRUD ์ž‘์—…, ๊ฒ€์ƒ‰, ํ•„ํ„ฐ๋ง
    * **๋Œ“๊ธ€**: ๊ฒŒ์‹œ๊ธ€์— ๋Œ“๊ธ€ ์ž‘์„ฑ ๋ฐ ๊ด€๋ฆฌ
    
    ## ์ธ์ฆ
    JWT Bearer ํ† ํฐ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    """,
    version="1.0.0",
    terms_of_service="https://example.com/terms/",
    contact={
        "name": "API Support",
        "url": "https://example.com/support",
        "email": "support@example.com"
    },
    license_info={
        "name": "MIT",
        "url": "https://opensource.org/licenses/MIT"
    }
)

class Item(BaseModel):
    name: str = Field(..., description="์•„์ดํ…œ ์ด๋ฆ„", example="๋…ธํŠธ๋ถ")
    price: float = Field(..., gt=0, description="๊ฐ€๊ฒฉ (์›)", example=1500000)
    description: str = Field(None, description="์ƒ์„ธ ์„ค๋ช…", example="๊ณ ์„ฑ๋Šฅ ๋…ธํŠธ๋ถ")

@app.post(
    "/items/",
    response_model=Item,
    status_code=201,
    summary="์•„์ดํ…œ ์ƒ์„ฑ",
    description="์ƒˆ๋กœ์šด ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.",
    response_description="์ƒ์„ฑ๋œ ์•„์ดํ…œ ์ •๋ณด",
    tags=["items"]
)
def create_item(item: Item):
    """
    ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค:
    
    - **name**: ํ•„์ˆ˜, ์•„์ดํ…œ ์ด๋ฆ„
    - **price**: ํ•„์ˆ˜, 0๋ณด๋‹ค ํฐ ๊ฐ€๊ฒฉ
    - **description**: ์„ ํƒ, ์ƒ์„ธ ์„ค๋ช…
    """
    return item

ํƒœ๊ทธ๋กœ ์—”๋“œํฌ์ธํŠธ ๊ทธ๋ฃนํ™”

tags_metadata = [
    {
        "name": "users",
        "description": "์‚ฌ์šฉ์ž ๊ด€๋ จ ์ž‘์—…",
    },
    {
        "name": "items",
        "description": "์•„์ดํ…œ ๊ด€๋ฆฌ",
        "externalDocs": {
            "description": "์™ธ๋ถ€ ๋ฌธ์„œ",
            "url": "https://example.com/docs/items",
        },
    },
]

app = FastAPI(openapi_tags=tags_metadata)

@app.get("/users/", tags=["users"])
def get_users():
    return []

@app.get("/items/", tags=["items"])
def get_items():
    return []

API ๋ฒ„์ „ ๊ด€๋ฆฌ

API๊ฐ€ ๋ฐœ์ „ํ•˜๋ฉด์„œ ๋ฒ„์ „ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด์ ธ. ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์ด ์žˆ๋Š”๋ฐ, URL ๊ฒฝ๋กœ์— ๋ฒ„์ „์„ ํฌํ•จํ•˜๋Š” ๊ฒŒ ๊ฐ€์žฅ ๋ช…ํ™•ํ•ด:

from fastapi import FastAPI, APIRouter

app = FastAPI()

# v1 ๋ผ์šฐํ„ฐ
v1_router = APIRouter(prefix="/api/v1", tags=["v1"])

@v1_router.get("/users/")
def get_users_v1():
    return {"version": "1.0", "users": []}

# v2 ๋ผ์šฐํ„ฐ (๊ฐœ์„ ๋œ ๋ฒ„์ „)
v2_router = APIRouter(prefix="/api/v2", tags=["v2"])

@v2_router.get("/users/")
def get_users_v2(
    page: int = 1,
    page_size: int = 20
):
    return {
        "version": "2.0",
        "users": [],
        "pagination": {
            "page": page,
            "page_size": page_size
        }
    }

# ๋ผ์šฐํ„ฐ ๋“ฑ๋ก
app.include_router(v1_router)
app.include_router(v2_router)

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด /api/v1/users/์™€ /api/v2/users/๋ฅผ ๋™์‹œ์— ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด. ๊ธฐ์กด ์‚ฌ์šฉ์ž๋Š” v1์„ ๊ณ„์† ์“ฐ๊ณ , ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ ์‚ฌ์šฉ์ž๋Š” v2๋ฅผ ์“ธ ์ˆ˜ ์žˆ์ง€!

๐Ÿ”„ ๋ฒ„์ „ ๊ด€๋ฆฌ ์ „๋žต

โ€ข URL ๊ฒฝ๋กœ: /api/v1/, /api/v2/ (์ถ”์ฒœ!)
โ€ข ํ—ค๋”: Accept: application/vnd.api+json;version=1
โ€ข ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ: /api/users?version=1
โ€ข ์„œ๋ธŒ๋„๋ฉ”์ธ: v1.api.example.com

๐Ÿ”ง ๋ฏธ๋“ค์›จ์–ด์™€ CORS ์„ค์ •

์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฏธ๋“ค์›จ์–ด ์„ค์ •์ด ์ค‘์š”ํ•ด. ํŠนํžˆ ํ”„๋ก ํŠธ์—”๋“œ์™€ ์—ฐ๋™ํ•  ๋•Œ๋Š” CORS ์„ค์ •์ด ํ•„์ˆ˜์•ผ!

CORS ์„ค์ •

CORS(Cross-Origin Resource Sharing)๋Š” ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์—์„œ API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# CORS ์„ค์ •
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:3000",  # React ๊ฐœ๋ฐœ ์„œ๋ฒ„
        "http://localhost:8080",  # Vue ๊ฐœ๋ฐœ ์„œ๋ฒ„
        "https://yourdomain.com"  # ํ”„๋กœ๋•์…˜ ๋„๋ฉ”์ธ
    ],
    allow_credentials=True,
    allow_methods=["*"],  # ๋ชจ๋“  HTTP ๋ฉ”์„œ๋“œ ํ—ˆ์šฉ
    allow_headers=["*"],  # ๋ชจ๋“  ํ—ค๋” ํ—ˆ์šฉ
)

# ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ชจ๋“  origin ํ—ˆ์šฉ (์ฃผ์˜!)
# app.add_middleware(
#     CORSMiddleware,
#     allow_origins=["*"],
#     allow_credentials=True,
#     allow_methods=["*"],
#     allow_headers=["*"],
# )

โš ๏ธ CORS ๋ณด์•ˆ ์ฃผ์˜

ํ”„๋กœ๋•์…˜์—์„œ๋Š” allow_origins=["*"]๋ฅผ ์ ˆ๋Œ€ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ! ํŠน์ • ๋„๋ฉ”์ธ๋งŒ ๋ช…์‹œ์ ์œผ๋กœ ํ—ˆ์šฉํ•ด์•ผ ํ•ด. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ๋ˆ„๊ตฌ๋‚˜ ๋‹น์‹ ์˜ API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋ณด์•ˆ ์œ„ํ—˜์ด ์ปค์ ธ.

์ปค์Šคํ…€ ๋ฏธ๋“ค์›จ์–ด

์š”์ฒญ๊ณผ ์‘๋‹ต์„ ๊ฐ€๋กœ์ฑ„์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด:

from fastapi import FastAPI, Request
import time
import logging

logger = logging.getLogger(__name__)

app = FastAPI()

@app.middleware("http")
async def log_requests(request: Request, call_next):
    # ์š”์ฒญ ์‹œ์ž‘ ์‹œ๊ฐ„
    start_time = time.time()
    
    # ์š”์ฒญ ์ •๋ณด ๋กœ๊น…
    logger.info(f"Request: {request.method} {request.url}")
    
    # ๋‹ค์Œ ๋ฏธ๋“ค์›จ์–ด ๋˜๋Š” ์—”๋“œํฌ์ธํŠธ ์‹คํ–‰
    response = await call_next(request)
    
    # ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ๊ณ„์‚ฐ
    process_time = time.time() - start_time
    
    # ์‘๋‹ต ํ—ค๋”์— ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ์ถ”๊ฐ€
    response.headers["X-Process-Time"] = str(process_time)
    
    # ์‘๋‹ต ์ •๋ณด ๋กœ๊น…
    logger.info(
        f"Response: {response.status_code} "
        f"(took {process_time:.4f}s)"
    )
    
    return response

@app.middleware("http")
async def add_security_headers(request: Request, call_next):
    response = await call_next(request)
    
    # ๋ณด์•ˆ ํ—ค๋” ์ถ”๊ฐ€
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
    
    return response

์š”์ฒญ ์ œํ•œ (Rate Limiting)

API ๋‚จ์šฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์š”์ฒญ ์ œํ•œ์„ ๊ฑธ ์ˆ˜ ์žˆ์–ด:

from fastapi import FastAPI, Request, HTTPException
from collections import defaultdict
from datetime import datetime, timedelta
import asyncio

app = FastAPI()

# ๊ฐ„๋‹จํ•œ ์ธ๋ฉ”๋ชจ๋ฆฌ rate limiter
class RateLimiter:
    def __init__(self, requests: int, window: int):
        self.requests = requests  # ํ—ˆ์šฉ ์š”์ฒญ ์ˆ˜
        self.window = window  # ์‹œ๊ฐ„ ์œˆ๋„์šฐ (์ดˆ)
        self.clients = defaultdict(list)
    
    async def check_rate_limit(self, client_id: str):
        now = datetime.now()
        # ์˜ค๋ž˜๋œ ์š”์ฒญ ์ œ๊ฑฐ
        self.clients[client_id] = [
            req_time for req_time in self.clients[client_id]
            if now - req_time < timedelta(seconds=self.window)
        ]
        
        # ์š”์ฒญ ์ˆ˜ ํ™•์ธ
        if len(self.clients[client_id]) >= self.requests:
            return False
        
        # ์ƒˆ ์š”์ฒญ ๊ธฐ๋ก
        self.clients[client_id].append(now)
        return True

rate_limiter = RateLimiter(requests=10, window=60)  # 1๋ถ„์— 10๊ฐœ ์š”์ฒญ

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    client_ip = request.client.host
    
    if not await rate_limiter.check_rate_limit(client_ip):
        raise HTTPException(
            status_code=429,
            detail="์š”์ฒญ ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."
        )
    
    response = await call_next(request)
    return response

์‹ค์ œ ํ”„๋กœ๋•์…˜์—์„œ๋Š” Redis๋ฅผ ์‚ฌ์šฉํ•œ ๋ถ„์‚ฐ rate limiting์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ์—ฌ๋Ÿฌ ์„œ๋ฒ„๊ฐ€ ์žˆ์„ ๋•Œ๋„ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๊ฑฐ๋“ !

๐Ÿšข ๋ฐฐํฌ์™€ ํ”„๋กœ๋•์…˜ ์„ค์ •

๊ฐœ๋ฐœ์ด ๋๋‚ฌ์œผ๋ฉด ์ด์ œ ์„ธ์ƒ์— ๊ณต๊ฐœํ•  ์‹œ๊ฐ„์ด์•ผ! ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ๋Š” ๊ฐœ๋ฐœ๊ณผ๋Š” ๋‹ค๋ฅธ ๊ณ ๋ ค์‚ฌํ•ญ๋“ค์ด ์žˆ์–ด.

ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ

๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ๋„ฃ์œผ๋ฉด ์•ˆ ๋ผ. ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜์ž:

pip install python-dotenv

.env ํŒŒ์ผ ์ƒ์„ฑ:

# .env
DATABASE_URL=postgresql://user:password@localhost/dbname
SECRET_KEY=your-super-secret-key-here
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
REDIS_URL=redis://localhost:6379
ENVIRONMENT=production

์„ค์ • ํŒŒ์ผ (config.py):

from pydantic import BaseSettings
from functools import lru_cache

class Settings(BaseSettings):
    database_url: str
    secret_key: str
    algorithm: str = "HS256"
    access_token_expire_minutes: int = 30
    redis_url: str
    environment: str = "development"
    
    class Config:
        env_file = ".env"

@lru_cache()
def get_settings():
    return Settings()

# ์‚ฌ์šฉ
settings = get_settings()

Docker๋กœ ์ปจํ…Œ์ด๋„ˆํ™”

Dockerfile ์ž‘์„ฑ:

# Dockerfile
FROM python:3.9-slim

WORKDIR /app

# ์˜์กด์„ฑ ์„ค์น˜
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# ํฌํŠธ ๋…ธ์ถœ
EXPOSE 8000

# ์‹คํ–‰
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

docker-compose.yml:

version: '3.8'

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/dbname
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    volumes:
      - .:/app
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
  
  db:
    image: postgres:13
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=dbname
    volumes:
      - postgres_data:/var/lib/postgresql/data
  
  redis:
    image: redis:6-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Gunicorn๊ณผ Uvicorn Workers

ํ”„๋กœ๋•์…˜์—์„œ๋Š” Gunicorn์„ ํ”„๋กœ์„ธ์Šค ๋งค๋‹ˆ์ €๋กœ ์‚ฌ์šฉํ•˜๊ณ , Uvicorn์„ ์›Œ์ปค๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„:

pip install gunicorn

์‹คํ–‰:

gunicorn main:app \
  --workers 4 \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000 \
  --access-logfile - \
  --error-logfile -

๐Ÿ’ก ์›Œ์ปค ์ˆ˜ ์„ค์ •

์ผ๋ฐ˜์ ์œผ๋กœ (2 x CPU ์ฝ”์–ด ์ˆ˜) + 1 ๊ณต์‹์„ ์‚ฌ์šฉํ•ด. 4์ฝ”์–ด ์„œ๋ฒ„๋ผ๋ฉด 9๊ฐœ์˜ ์›Œ์ปค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์ง€. ํ•˜์ง€๋งŒ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰๋„ ๊ณ ๋ คํ•ด์•ผ ํ•ด!

Nginx ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ

Nginx๋ฅผ ์•ž๋‹จ์— ๋‘๋ฉด ์ •์  ํŒŒ์ผ ์„œ๋น™, SSL ์ฒ˜๋ฆฌ, ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ ๋“ฑ์„ ํ•  ์ˆ˜ ์žˆ์–ด:

# /etc/nginx/sites-available/fastapi
server {
    listen 80;
    server_name api.yourdomain.com;
    
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    # ์ •์  ํŒŒ์ผ
    location /static {
        alias /var/www/static;
    }
}

๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…

ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๋ฌด์Šจ ์ผ์ด ์ผ์–ด๋‚˜๋Š”์ง€ ์•Œ์•„์•ผ ํ•ด:

import logging
from logging.handlers import RotatingFileHandler

# ๋กœ๊น… ์„ค์ •
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# ํŒŒ์ผ ํ•ธ๋“ค๋Ÿฌ (๋กœ๊ทธ ๋กœํ…Œ์ด์…˜)
handler = RotatingFileHandler(
    'app.log',
    maxBytes=10485760,  # 10MB
    backupCount=10
)
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)

# ์‚ฌ์šฉ
@app.get("/")
def root():
    logger.info("Root endpoint accessed")
    return {"message": "Hello"}

Sentry ๊ฐ™์€ ์—๋Ÿฌ ํŠธ๋ž˜ํ‚น ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์ข‹์•„:

pip install sentry-sdk
import sentry_sdk
from sentry_sdk.integrations.fastapi import FastApiIntegration

sentry_sdk.init(
    dsn="your-sentry-dsn",
    integrations=[FastApiIntegration()],
    traces_sample_rate=1.0,
    environment=settings.environment
)

๐ŸŽ“ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ์™„์ „ํ•œ ๋ธ”๋กœ๊ทธ API ๋งŒ๋“ค๊ธฐ

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๊ฑธ ๋ชจ๋‘ ํ•ฉ์ณ์„œ ์‹ค์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ์™„์ „ํ•œ ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ๋ธ”๋กœ๊ทธ API์•ผ. ๐Ÿš€

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

blog-api/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ main.py
โ”‚   โ”œโ”€โ”€ config.py
โ”‚   โ”œโ”€โ”€ database.py
โ”‚   โ”œโ”€โ”€ models/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ user.py
โ”‚   โ”‚   โ”œโ”€โ”€ post.py
โ”‚   โ”‚   โ””โ”€โ”€ comment.py
โ”‚   โ”œโ”€โ”€ schemas/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ user.py
โ”‚   โ”‚   โ”œโ”€โ”€ post.py
โ”‚   โ”‚   โ””โ”€โ”€ comment.py
โ”‚   โ”œโ”€โ”€ crud/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ user.py
โ”‚   โ”‚   โ”œโ”€โ”€ post.py
โ”‚   โ”‚   โ””โ”€โ”€ comment.py
โ”‚   โ”œโ”€โ”€ api/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ deps.py
โ”‚   โ”‚   โ””โ”€โ”€ v1/
โ”‚   โ”‚       โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚       โ”œโ”€โ”€ auth.py
โ”‚   โ”‚       โ”œโ”€โ”€ users.py
โ”‚   โ”‚       โ”œโ”€โ”€ posts.py
โ”‚   โ”‚       โ””โ”€โ”€ comments.py
โ”‚   โ””โ”€โ”€ core/
โ”‚       โ”œโ”€โ”€ __init__.py
โ”‚       โ”œโ”€โ”€ security.py
โ”‚       โ””โ”€โ”€ config.py
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ test_auth.py
โ”‚   โ”œโ”€โ”€ test_users.py
โ”‚   โ””โ”€โ”€ test_posts.py
โ”œโ”€โ”€ .env
โ”œโ”€โ”€ .gitignore
โ”œโ”€โ”€ requirements.txt
โ”œโ”€โ”€ Dockerfile
โ””โ”€โ”€ docker-compose.yml

ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„

๊ฒŒ์‹œ๊ธ€ ๋ชจ๋ธ (models/post.py):

from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from datetime import datetime
from app.database import Base

class Post(Base):
    __tablename__ = "posts"
    
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(200), nullable=False, index=True)
    slug = Column(String(200), unique=True, index=True)
    content = Column(Text, nullable=False)
    excerpt = Column(String(500))
    published = Column(Boolean, default=False)
    views = Column(Integer, default=0)
    
    author_id = Column(Integer, ForeignKey("users.id"))
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # ๊ด€๊ณ„
    author = relationship("User", back_populates="posts")
    comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan")
    tags = relationship("Tag", secondary="post_tags", back_populates="posts")

๊ฒŒ์‹œ๊ธ€ ์Šคํ‚ค๋งˆ (schemas/post.py):

from pydantic import BaseModel, Field, validator
from typing import List, Optional
from datetime import datetime
import re

class PostBase(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    content: str = Field(..., min_length=1)
    excerpt: Optional[str] = Field(None, max_length=500)
    published: bool = False
    tags: List[str] = []

class PostCreate(PostBase):
    @validator('title')
    def generate_slug(cls, v):
        # ์Šฌ๋Ÿฌ๊ทธ ์ž๋™ ์ƒ์„ฑ ๋กœ์ง
        return re.sub(r'[^\w\s-]', '', v.lower()).replace(' ', '-')

class PostUpdate(BaseModel):
    title: Optional[str] = Field(None, min_length=1, max_length=200)
    content: Optional[str] = Field(None, min_length=1)
    excerpt: Optional[str] = Field(None, max_length=500)
    published: Optional[bool] = None
    tags: Optional[List[str]] = None

class PostInDB(PostBase):
    id: int
    slug: str
    views: int
    author_id: int
    created_at: datetime
    updated_at: datetime
    
    class Config:
        orm_mode = True

class Post(PostInDB):
    author: "UserBasic"
    comments_count: int = 0
    
class PostDetail(Post):
    comments: List["Comment"] = []

๊ฒŒ์‹œ๊ธ€ CRUD (crud/post.py):

from sqlalchemy.orm import Session
from sqlalchemy import or_
from typing import List, Optional
from app.models.post import Post
from app.schemas.post import PostCreate, PostUpdate

def get_post(db: Session, post_id: int) -> Optional[Post]:
    return db.query(Post).filter(Post.id == post_id).first()

def get_post_by_slug(db: Session, slug: str) -> Optional[Post]:
    return db.query(Post).filter(Post.slug == slug).first()

def get_posts(
    db: Session,
    skip: int = 0,
    limit: int = 20,
    published_only: bool = True,
    search: Optional[str] = None,
    author_id: Optional[int] = None
) -> List[Post]:
    query = db.query(Post)
    
    if published_only:
        query = query.filter(Post.published == True)
    
    if author_id:
        query = query.filter(Post.author_id == author_id)
    
    if search:
        query = query.filter(
            or_(
                Post.title.ilike(f"%{search}%"),
                Post.content.ilike(f"%{search}%")
            )
        )
    
    return query.order_by(Post.created_at.desc()).offset(skip).limit(limit).all()

def create_post(db: Session, post: PostCreate, author_id: int) -> Post:
    db_post = Post(
        **post.dict(exclude={'tags'}),
        author_id=author_id
    )
    db.add(db_post)
    db.commit()
    db.refresh(db_post)
    return db_post

def update_post(db: Session, post_id: int, post: PostUpdate) -> Optional[Post]:
    db_post = get_post(db, post_id)
    if not db_post:
        return None
    
    update_data = post.dict(exclude_unset=True)
    for field, value in update_data.items():
        setattr(db_post, field, value)
    
    db.commit()
    db.refresh(db_post)
    return db_post

def delete_post(db: Session, post_id: int) -> bool:
    db_post = get_post(db, post_id)
    if not db_post:
        return False
    
    db.delete(db_post)
    db.commit()
    return True

def increment_views(db: Session, post_id: int):
    db_post = get_post(db, post_id)
    if db_post:
        db_post.views += 1
        db.commit()

๊ฒŒ์‹œ๊ธ€ API ์—”๋“œํฌ์ธํŠธ (api/v1/posts.py):

from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import List
from app import crud, schemas
from app.api.deps import get_db, get_current_active_user
from app.models.user import User

router = APIRouter()

@router.get("/", response_model=List[schemas.Post])
def list_posts(
    skip: int = Query(0, ge=0),
    limit: int = Query(20, ge=1, le=100),
    search: str = Query(None),
    db: Session = Depends(get_db)
):
    """๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ์กฐํšŒ"""
    posts = crud.get_posts(
        db,
        skip=skip,
        limit=limit,
        search=search,
        published_only=True
    )
    return posts

@router.get("/{post_id}", response_model=schemas.PostDetail)
def get_post(
    post_id: int,
    db: Session = Depends(get_db)
):
    """๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ์กฐํšŒ"""
    post = crud.get_post(db, post_id)
    if not post or not post.published:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
        )
    
    # ์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€
    crud.increment_views(db, post_id)
    
    return post

@router.post("/", response_model=schemas.Post, status_code=status.HTTP_201_CREATED)
def create_post(
    post: schemas.PostCreate,
    current_user: User = Depends(get_current_active_user),
    db: Session = Depends(get_db)
):
    """๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ"""
    return crud.create_post(db, post, current_user.id)

@router.put("/{post_id}", response_model=schemas.Post)
def update_post(
    post_id: int,
    post: schemas.PostUpdate,
    current_user: User = Depends(get_current_active_user),
    db: Session = Depends(get_db)
):
    """๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •"""
    db_post = crud.get_post(db, post_id)
    if not db_post:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
        )
    
    if db_post.author_id != current_user.id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค"
        )
    
    return crud.update_post(db, post_id, post)

@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_post(
    post_id: int,
    current_user: User = Depends(get_current_active_user),
    db: Session = Depends(get_db)
):
    """๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ"""
    db_post = crud.get_post(db, post_id)
    if not db_post:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
        )
    
    if db_post.author_id != current_user.id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค"
        )
    
    crud.delete_post(db, post_id)
    return None

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์™„์ „ํ•œ CRUD ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ๊ฒŒ์‹œ๊ธ€ API๊ฐ€ ์™„์„ฑ๋ผ! ๊ฒ€์ƒ‰, ํŽ˜์ด์ง€๋„ค์ด์…˜, ๊ถŒํ•œ ์ฒดํฌ๊นŒ์ง€ ๋ชจ๋‘ ํฌํ•จ๋˜์–ด ์žˆ์ง€. โœจ

๐ŸŽฏ ์‹ค์ „ ํŒ

์ด ๊ตฌ์กฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋Œ“๊ธ€, ์ข‹์•„์š”, ํƒœ๊ทธ, ์นดํ…Œ๊ณ ๋ฆฌ ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด. ๊ฐ ๊ธฐ๋Šฅ๋งˆ๋‹ค models, schemas, crud, api ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ , ๊ฐ™์€ ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ๋ผ. ์ผ๊ด€์„ฑ ์žˆ๋Š” ๊ตฌ์กฐ๊ฐ€ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ํ›จ์”ฌ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜!

๐Ÿ’ก FastAPI ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค

๊ธฐ๋ณธ์„ ๋„˜์–ด์„œ FastAPI์˜ ๊ฐ•๋ ฅํ•œ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค์„ ์•Œ์•„๋ณด์ž!

WebSocket ์ง€์›

์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด WebSocket์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List

app = FastAPI()

class ConnectionManager:
    def __init__(self):
        self.active_connections: List[WebSocket] = []
    
    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)
    
    def disconnect(self, websocket: WebSocket):
        self.active_connections.remove(websocket)
    
    async def broadcast(self, message: str):
        for connection in self.active_connections:
            await connection.send_text(message)

manager = ConnectionManager()

@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
    await manager.connect(websocket)
    try:
        while True:
            data = await websocket.receive_text()
            await manager.broadcast(f"Client {client_id}: {data}")
    except WebSocketDisconnect:
        manager.disconnect(websocket)
        await manager.broadcast(f"Client {client_id} left")

์ด๊ฑธ๋กœ ์ฑ„ํŒ…, ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ, ๋ผ์ด๋ธŒ ์—…๋ฐ์ดํŠธ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด!

ํŒŒ์ผ ์—…๋กœ๋“œ

์ด๋ฏธ์ง€๋‚˜ ๋ฌธ์„œ ์—…๋กœ๋“œ๋„ ๊ฐ„๋‹จํ•ด:

from fastapi import FastAPI, File, UploadFile, HTTPException
from typing import List
import shutil
from pathlib import Path

app = FastAPI()

UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)

@app.post("/upload/")
async def upload_file(file: UploadFile = File(...)):
    # ํŒŒ์ผ ํฌ๊ธฐ ์ œํ•œ (10MB)
    max_size = 10 * 1024 * 1024
    contents = await file.read()
    
    if len(contents) > max_size:
        raise HTTPException(
            status_code=413,
            detail="ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค (์ตœ๋Œ€ 10MB)"
        )
    
    # ํ—ˆ์šฉ๋œ ํ™•์žฅ์ž ์ฒดํฌ
    allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.pdf'}
    file_ext = Path(file.filename).suffix.lower()
    
    if file_ext not in allowed_extensions:
        raise HTTPException(
            status_code=400,
            detail=f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค. ํ—ˆ์šฉ: {allowed_extensions}"
        )
    
    # ํŒŒ์ผ ์ €์žฅ
    file_path = UPLOAD_DIR / file.filename
    with open(file_path, "wb") as buffer:
        buffer.write(contents)
    
    return {
        "filename": file.filename,
        "size": len(contents),
        "content_type": file.content_type
    }

@app.post("/upload-multiple/")
async def upload_multiple_files(files: List[UploadFile] = File(...)):
    results = []
    for file in files:
        file_path = UPLOAD_DIR / file.filename
        with open(file_path, "wb") as buffer:
            shutil.copyfileobj(file.file, buffer)
        results.append({"filename": file.filename})
    
    return {"uploaded": results}

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘/์ข…๋ฃŒ ์‹œ ์‹คํ–‰ํ•  ์ฝ”๋“œ๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด:

from fastapi import FastAPI
import asyncio

app = FastAPI()

@app.on_event("startup")
async def startup_event():
    print("์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘!")
    # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ
    # ์บ์‹œ ์ดˆ๊ธฐํ™”
    # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์‹œ์ž‘
    asyncio.create_task(background_task())

@app.on_event("shutdown")
async def shutdown_event():
    print("์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ข…๋ฃŒ!")
    # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ •๋ฆฌ
    # ์บ์‹œ ์ €์žฅ
    # ๋ฆฌ์†Œ์Šค ํ•ด์ œ

async def background_task():
    while True:
        print("๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์‹คํ–‰ ์ค‘...")
        await asyncio.sleep(60)

GraphQL ์ง€์›

REST API ๋Œ€์‹  GraphQL์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด:

pip install strawberry-graphql
import strawberry
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter

@strawberry.type
class User:
    id: int
    name: str
    email: str

@strawberry.type
class Query:
    @strawberry.field
    def user(self, id: int) -> User:
        return User(id=id, name="John", email="john@example.com")
    
    @strawberry.field
    def users(self) -> list[User]:
        return [
            User(id=1, name="John", email="john@example.com"),
            User(id=2, name="Jane", email="jane@example.com")
        ]

schema = strawberry.Schema(query=Query)
graphql_app = GraphQLRouter(schema)

app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")

์ด์ œ /graphql์—์„œ GraphQL ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด!

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด. FastAPI๋กœ REST API๋ฅผ ๋งŒ๋“œ๋Š” ๊ฑฐ์˜ ๋ชจ๋“  ๊ฒƒ์„ ๋‹ค๋ค˜์–ด. ๐ŸŽ‰

FastAPI๋Š” ์ •๋ง ๊ฐ•๋ ฅํ•˜๊ณ  ํ˜„๋Œ€์ ์ธ ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ๋น ๋ฅธ ์„ฑ๋Šฅ, ์ž๋™ ๋ฌธ์„œํ™”, ํƒ€์ž… ์•ˆ์ •์„ฑ, ๋น„๋™๊ธฐ ์ง€์› ๋“ฑ ๊ฐœ๋ฐœ์ž๊ฐ€ ์›ํ•˜๋Š” ๊ฑฐ์˜ ๋ชจ๋“  ๊ฑธ ์ œ๊ณตํ•˜์ง€.

๐Ÿš€ ํ•ต์‹ฌ ์š”์•ฝ

โ€ข FastAPI๋Š” Python ํƒ€์ž… ํžŒํŠธ ๊ธฐ๋ฐ˜์˜ ํ˜„๋Œ€์ ์ธ ์›น ํ”„๋ ˆ์ž„์›Œํฌ
โ€ข Pydantic์œผ๋กœ ์ž๋™ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ์ง๋ ฌํ™”
โ€ข async/await๋กœ ๊ณ ์„ฑ๋Šฅ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
โ€ข ์ž๋™ API ๋ฌธ์„œ ์ƒ์„ฑ (Swagger UI, ReDoc)
โ€ข SQLAlchemy๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™
โ€ข JWT๋กœ ์•ˆ์ „ํ•œ ์ธ์ฆ ๊ตฌํ˜„
โ€ข ๋ฏธ๋“ค์›จ์–ด๋กœ CORS, ๋กœ๊น…, Rate Limiting
โ€ข Docker๋กœ ์‰ฌ์šด ๋ฐฐํฌ

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋‹ค์–‘ํ•œ ์„œ๋น„์Šค๋ฅผ API๋กœ ์—ฐ๊ฒฐํ•  ๋•Œ, FastAPI๋Š” ์ •๋ง ํ›Œ๋ฅญํ•œ ์„ ํƒ์ด์•ผ. ๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์ด๋ฉด์„œ๋„ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์ด ์ข‹์œผ๋‹ˆ๊นŒ!

์ฒ˜์Œ์—” ์–ด๋ ค์›Œ ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ๋”ฐ๋ผ ํ•˜๋‹ค ๋ณด๋ฉด ๊ธˆ๋ฐฉ ์ต์ˆ™ํ•ด์งˆ ๊ฑฐ์•ผ. ์ค‘์š”ํ•œ ๊ฑด ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๋Š” ๊ฑฐ์•ผ. ๊ฐ„๋‹จํ•œ TODO API๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์  ๋ณต์žกํ•œ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž!

๐Ÿ’ช ๋‹ค์Œ ๋‹จ๊ณ„

โ€ข ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— FastAPI ์ ์šฉํ•ด๋ณด๊ธฐ
โ€ข ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋†’์ด๊ธฐ
โ€ข CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•
โ€ข ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ํ•™์Šต
โ€ข ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์ตœ์ ํ™”
โ€ข ๋ณด์•ˆ ๊ฐ•ํ™” (OAuth2, 2FA ๋“ฑ)

API ๊ฐœ๋ฐœ์€ ๋์—†๋Š” ํ•™์Šต์˜ ์—ฐ์†์ด์•ผ. ํ•˜์ง€๋งŒ FastAPI์™€ ํ•จ๊ป˜๋ผ๋ฉด ๊ทธ ์—ฌ์ •์ด ํ›จ์”ฌ ์ฆ๊ฑฐ์šธ ๊ฑฐ์•ผ. ํ™”์ดํŒ…! ๐Ÿš€๐Ÿ’ปโœจ

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด, FastAPI ๊ณต์‹ ๋ฌธ์„œ๋‚˜ ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ํ™œ์šฉํ•ด๋ด. ์ •๋ง ์นœ์ ˆํ•˜๊ณ  ์ž์„ธํ•˜๊ฒŒ ์„ค๋ช…๋˜์–ด ์žˆ๊ฑฐ๋“ . ๊ทธ๋ฆฌ๊ณ  ์ง์ ‘ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด์„œ ๋ฐฐ์šฐ๋Š” ๊ฒŒ ๊ฐ€์žฅ ๋น ๋ฅธ ๋ฐฉ๋ฒ•์ด์•ผ!

์ž, ์ด์ œ ๋‹น์‹ ๋งŒ์˜ ๋ฉ‹์ง„ API๋ฅผ ๋งŒ๋“ค์–ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! ํ–‰์šด์„ ๋นŒ์–ด! ๐Ÿ€

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.