Ver3.0 ๐ 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 ํํ๋ก ์ ๋ฌํ ์ ์์ง.
๐จ 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 ๊ฐ์ ์ญํ ์ ๋ง๋ค๊ณ , ๊ฐ ์ญํ ์ ๋ฐ๋ผ ์ ๊ทผ ๊ฐ๋ฅํ ๋ฆฌ์์ค๋ฅผ ์ ํํ๋ ๊ฑฐ์ง. ์ด๊ฒ๋ ์์กด์ฑ ์ฃผ์ ์ผ๋ก ๊น๋ํ๊ฒ ๊ตฌํํ ์ ์์ด!
๐ ์๋ฌ ํธ๋ค๋ง๊ณผ ์์ธ ์ฒ๋ฆฌ
์๋ฒฝํ 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
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.