KR_FastAPI - somaz94/python-study GitHub Wiki

Python FastAPI ๊ฐœ๋… ์ •๋ฆฌ


1๏ธโƒฃ FastAPI ๊ธฐ์ดˆ

FastAPI๋Š” ํ˜„๋Œ€์ ์ด๊ณ  ๋น ๋ฅธ ์›น ํ”„๋ ˆ์ž„์›Œํฌ์ด๋‹ค.

from fastapi import FastAPI, HTTPException, Query, Path
from pydantic import BaseModel, Field, EmailStr
from typing import List, Optional

app = FastAPI(
    title="My API",
    description="API ์„ค๋ช…",
    version="0.1.0"
)

class User(BaseModel):
    id: int
    name: str
    email: EmailStr
    is_active: bool = True
    
    class Config:
        schema_extra = {
            "example": {
                "id": 1,
                "name": "ํ™๊ธธ๋™",
                "email": "[email protected]",
                "is_active": True
            }
        }

# ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
users_db = {}

# ๊ธฐ๋ณธ ๋ผ์šฐํŠธ
@app.get("/")
async def root():
    return {"message": "Hello World"}

# ๊ฒฝ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜
@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int = Path(..., title="์‚ฌ์šฉ์ž ID", ge=1)):
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
    return users_db[user_id]

# ์š”์ฒญ ๋ณธ๋ฌธ
@app.post("/users/", response_model=User, status_code=201)
async def create_user(user: User):
    if user.id in users_db:
        raise HTTPException(status_code=400, detail="์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž ID์ž…๋‹ˆ๋‹ค")
    users_db[user.id] = user
    return user

# ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜
@app.get("/users/", response_model=List[User])
async def list_users(
    skip: int = Query(0, ge=0),
    limit: int = Query(10, ge=1, le=100)
):
    return list(users_db.values())[skip : skip + limit]

# PUT ์š”์ฒญ
@app.put("/users/{user_id}", response_model=User)
async def update_user(user_id: int, user: User):
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
    users_db[user_id] = user
    return user

# DELETE ์š”์ฒญ
@app.delete("/users/{user_id}", status_code=204)
async def delete_user(user_id: int):
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
    del users_db[user_id]
    return None

โœ… ํŠน์ง•:

  • ๋น ๋ฅธ ์„ฑ๋Šฅ
  • ์ž๋™ API ๋ฌธ์„œํ™”
  • ํƒ€์ž… ํžŒํŠธ ์ง€์›
  • OpenAPI ๋ฐ JSON ์Šคํ‚ค๋งˆ ์ž๋™ ์ƒ์„ฑ
  • Python 3.6+ ๋น„๋™๊ธฐ ๊ธฐ๋Šฅ ํ™œ์šฉ

๋ฌธ์„œํ™” ์ž๋™ ์ƒ์„ฑ:

FastAPI๋Š” ์ž๋™์œผ๋กœ /docs์™€ /redoc ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ œ๊ณตํ•˜์—ฌ API ๋ฌธ์„œ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

  • /docs: Swagger UI๋ฅผ ์ด์šฉํ•œ ๋Œ€ํ™”ํ˜• API ๋ฌธ์„œ
  • /redoc: ReDoc์„ ์ด์šฉํ•œ ๋ฌธ์„œ


2๏ธโƒฃ ์˜์กด์„ฑ ์ฃผ์ž…

FastAPI์˜ ๊ฐ•๋ ฅํ•œ ์˜์กด์„ฑ ์ฃผ์ž… ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•œ ์ฝ”๋“œ ๊ตฌ์„ฑ ๋ฐฉ๋ฒ•์ด๋‹ค.

from fastapi import Depends, HTTPException, FastAPI
from sqlalchemy.orm import Session
from typing import Annotated, List, Optional
from database import SessionLocal, engine, Base
from models import UserModel
from schemas import UserCreate, UserResponse

app = FastAPI()

# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์˜์กด์„ฑ
async def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

DB = Annotated[Session, Depends(get_db)]

# ์„œ๋น„์Šค ํด๋ž˜์Šค
class UserService:
    def __init__(self, db: Session = Depends(get_db)):
        self.db = db
    
    def get_user(self, user_id: int):
        user = self.db.query(UserModel).filter(
            UserModel.id == user_id
        ).first()
        if not user:
            raise HTTPException(
                status_code=404, 
                detail="์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
            )
        return user
    
    def create_user(self, user: UserCreate):
        db_user = UserModel(**user.dict())
        self.db.add(db_user)
        self.db.commit()
        self.db.refresh(db_user)
        return db_user
    
    def get_users(self, skip: int = 0, limit: int = 100):
        return self.db.query(UserModel).offset(skip).limit(limit).all()

# ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์˜์กด์„ฑ ์ฃผ์ž… ํ™œ์šฉ
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(
    user_id: int,
    user_service: UserService = Depends()
):
    return user_service.get_user(user_id)

@app.post("/users/", response_model=UserResponse)
async def create_user(
    user: UserCreate,
    user_service: UserService = Depends()
):
    return user_service.create_user(user)

# ํ•จ์ˆ˜ ํ˜•ํƒœ์˜ ์˜์กด์„ฑ
async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return {"params": commons}

# ์˜์กด์„ฑ ์ฒด์ด๋‹
async def verify_token(x_token: str = Header(...)):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token ํ—ค๋”๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
    return x_token

async def verify_key(x_key: str = Header(...), token: str = Depends(verify_token)):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key ํ—ค๋”๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
    return x_key

@app.get("/secure-items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_secure_items():
    return [{"item": "Secure Item"}]

โœ… ํŠน์ง•:

  • ์˜์กด์„ฑ ๊ด€๋ฆฌ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ธ์…˜
  • ์„œ๋น„์Šค ๊ณ„์ธต ๋ถ„๋ฆฌ
  • ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์˜์กด์„ฑ
  • ์˜์กด์„ฑ ์ฒด์ด๋‹


3๏ธโƒฃ ๋ฏธ๋“ค์›จ์–ด์™€ ๋ณด์•ˆ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „๋ฐ˜์˜ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฏธ๋“ค์›จ์–ด์™€ ๋ณด์•ˆ ์„ค์ •์ด๋‹ค.

from fastapi import FastAPI, Depends, HTTPException, Security, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.middleware.cors import CORSMiddleware
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
import time
from typing import Dict

app = FastAPI()

# CORS ๋ฏธ๋“ค์›จ์–ด
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # ํ”„๋กœ๋•์…˜์—์„œ๋Š” ํŠน์ • ๋„๋ฉ”์ธ ์ง€์ •
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ์„ฑ๋Šฅ ์ธก์ • ๋ฏธ๋“ค์›จ์–ด
@app.middleware("http")
async def add_process_time_header(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)
    return response

# ๋ณด์•ˆ ์„ค์ •
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

SECRET_KEY = "YOUR_SECRET_KEY_HERE"  # ํ”„๋กœ๋•์…˜์—์„œ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์‚ฌ์šฉ
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password):
    return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: 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

# ์ธ์ฆ
fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "[email protected]",
        "hashed_password": get_password_hash("secret"),
        "disabled": False,
    }
}

async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="์ธ์ฆํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    
    user = fake_users_db.get(username)
    if user is None:
        raise credentials_exception
    return user

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

# ๋กœ๊ทธ์ธ ์—”๋“œํฌ์ธํŠธ
@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = fake_users_db.get(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["username"]}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}

# ๋ณดํ˜ธ๋œ ์—”๋“œํฌ์ธํŠธ
@app.get("/users/me", response_model=Dict)
async def read_users_me(current_user: dict = Depends(get_current_active_user)):
    return current_user

โœ… ํŠน์ง•:

  • JWT ์ธ์ฆ
  • ๋ฏธ๋“ค์›จ์–ด ์ฒ˜๋ฆฌ
  • ๋ณด์•ˆ ์„ค์ •
  • ์•”ํ˜ธํ™”
  • CORS ์„ค์ •


4๏ธโƒฃ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ

FastAPI์˜ ๋น„๋™๊ธฐ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•œ ๊ณ ์„ฑ๋Šฅ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌํ˜„์ด๋‹ค.

import asyncio
import httpx
from fastapi import FastAPI, BackgroundTasks, Depends
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.future import select
from typing import List

app = FastAPI()

# ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname"
async_engine = create_async_engine(DATABASE_URL)
AsyncSessionLocal = sessionmaker(
    async_engine, class_=AsyncSession, expire_on_commit=False
)

async def get_async_session():
    async with AsyncSessionLocal() as session:
        yield session

# ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…
def write_log(message: str):
    with open("log.txt", mode="a") as log:
        log.write(f"{message}\n")

async def send_email_async(email: str, message: str):
    # ์ด๋ฉ”์ผ ์ „์†ก ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    await asyncio.sleep(1)  # API ํ˜ธ์ถœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    return {"email": email, "message": message, "status": "sent"}

@app.post("/send-notification/{email}")
async def send_notification(
    email: str, 
    background_tasks: BackgroundTasks
):
    # ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋กœ๊ทธ ๊ธฐ๋ก
    background_tasks.add_task(write_log, f"์•Œ๋ฆผ ์ „์†ก: {email}")
    # ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ด๋ฉ”์ผ ์ „์†ก
    background_tasks.add_task(send_email_async, email, "์ค‘์š” ์•Œ๋ฆผ์ž…๋‹ˆ๋‹ค")
    return {"message": "์•Œ๋ฆผ์ด ์ „์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค"}

# ๋น„๋™๊ธฐ HTTP ์š”์ฒญ
@app.get("/github-stars/{username}")
async def get_github_stars(username: str):
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://api.github.com/users/{username}")
        if response.status_code != 200:
            return {"error": "GitHub API ํ˜ธ์ถœ ์‹คํŒจ"}
        
        user_data = response.json()
        # ๋ณ‘๋ ฌ API ํ˜ธ์ถœ
        repos_response = await client.get(f"https://api.github.com/users/{username}/repos")
        repos = repos_response.json()
        
        total_stars = sum(repo["stargazers_count"] for repo in repos)
        return {
            "username": username,
            "repos_count": len(repos),
            "total_stars": total_stars
        }

# ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…
async def get_user_async(async_session: AsyncSession, user_id: int):
    result = await async_session.execute(
        select(User).where(User.id == user_id)
    )
    return result.scalar_one_or_none()

@app.get("/users/{user_id}")
async def get_user(
    user_id: int,
    async_session: AsyncSession = Depends(get_async_session)
):
    user = await get_user_async(async_session, user_id)
    if not user:
        raise HTTPException(status_code=404, detail="์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
    return user

# ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
@app.get("/parallel-tasks")
async def parallel_tasks():
    start_time = time.time()
    
    # ๋ณ‘๋ ฌ๋กœ ๋น„๋™๊ธฐ ์ž‘์—… ์‹คํ–‰
    task1 = asyncio.create_task(asyncio.sleep(1))
    task2 = asyncio.create_task(asyncio.sleep(1))
    task3 = asyncio.create_task(asyncio.sleep(1))
    
    # ๋ชจ๋“  ์ž‘์—… ์™„๋ฃŒ ๋Œ€๊ธฐ
    await asyncio.gather(task1, task2, task3)
    
    end_time = time.time()
    return {
        "message": "๋ชจ๋“  ๋ณ‘๋ ฌ ์ž‘์—… ์™„๋ฃŒ",
        "time_taken": end_time - start_time  # ์•ฝ 1์ดˆ
    }

โœ… ํŠน์ง•:

  • ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
  • ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๋ณ‘๋ ฌ API ํ˜ธ์ถœ
  • ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ์‚ฐ


5๏ธโƒฃ WebSocket๊ณผ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ

WebSocket ๋ฐ ์„œ๋ฒ„-์ „์†ก ์ด๋ฒคํŠธ๋ฅผ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌํ˜„์ด๋‹ค.

from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends
from fastapi.responses import StreamingResponse
import asyncio
import json
from datetime import datetime
from typing import List, Dict

app = FastAPI()

# WebSocket ๊ด€๋ฆฌ์ž
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 send_personal_message(self, message: str, websocket: WebSocket):
        await websocket.send_text(message)
    
    async def broadcast(self, message: str):
        for connection in self.active_connections:
            await connection.send_text(message)

manager = ConnectionManager()

# WebSocket ์—”๋“œํฌ์ธํŠธ
@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.send_personal_message(f"๋‹น์‹ : {data}", websocket)
            # ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ์ „์†ก
            await manager.broadcast(f"ํด๋ผ์ด์–ธํŠธ #{client_id}์˜ ๋ฉ”์‹œ์ง€: {data}")
    except WebSocketDisconnect:
        manager.disconnect(websocket)
        await manager.broadcast(f"ํด๋ผ์ด์–ธํŠธ #{client_id}๊ฐ€ ํ‡ด์žฅํ–ˆ์Šต๋‹ˆ๋‹ค")

# ์„œ๋ฒ„-์ „์†ก ์ด๋ฒคํŠธ (SSE)
@app.get("/sse")
async def sse():
    async def event_generator():
        for i in range(10):
            # JSON ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
            data = json.dumps({
                "id": i,
                "timestamp": datetime.now().isoformat(),
                "message": f"์ด๋ฒคํŠธ #{i}"
            })
            yield f"data: {data}\n\n"
            await asyncio.sleep(1)
    
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream"
    )

# WebSocket ํด๋ผ์ด์–ธํŠธ ์˜ˆ์ œ (JavaScript)
"""
const ws = new WebSocket("ws://localhost:8000/ws/1");
ws.onmessage = function(event) {
    console.log('๋ฉ”์‹œ์ง€ ์ˆ˜์‹ :', event.data);
};
ws.onclose = function(event) {
    console.log('์—ฐ๊ฒฐ ์ข…๋ฃŒ:', event);
};
ws.send("์•ˆ๋…•ํ•˜์„ธ์š”!");

// SSE ํด๋ผ์ด์–ธํŠธ
const eventSource = new EventSource("/sse");
eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log("์ด๋ฒคํŠธ ์ˆ˜์‹ :", data);
};
eventSource.onerror = (error) => {
    console.error("SSE ์˜ค๋ฅ˜:", error);
    eventSource.close();
};
"""

โœ… ํŠน์ง•:

  • ์–‘๋ฐฉํ–ฅ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ 
  • ์„œ๋ฒ„-์ „์†ก ์ด๋ฒคํŠธ
  • ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ๋“œ์บ์ŠคํŒ…
  • ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
  • ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ


6๏ธโƒฃ ํ…Œ์ŠคํŒ…๊ณผ ๋ฐฐํฌ

FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ…Œ์ŠคํŠธ ๋ฐ ๋ฐฐํฌ ๋ฐฉ๋ฒ•์ด๋‹ค.

# test_main.py
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello World"}

def test_create_user():
    user_data = {
        "id": 1,
        "name": "Test User",
        "email": "[email protected]",
        "is_active": True
    }
    response = client.post("/users/", json=user_data)
    assert response.status_code == 201
    assert response.json()["id"] == user_data["id"]
    assert response.json()["name"] == user_data["name"]

def test_get_user():
    # ๋จผ์ € ์‚ฌ์šฉ์ž ์ƒ์„ฑ
    user_data = {
        "id": 2,
        "name": "Another User",
        "email": "[email protected]",
        "is_active": True
    }
    client.post("/users/", json=user_data)
    
    # ์‚ฌ์šฉ์ž ์กฐํšŒ
    response = client.get("/users/2")
    assert response.status_code == 200
    assert response.json()["name"] == user_data["name"]

def test_get_nonexistent_user():
    response = client.get("/users/999")
    assert response.status_code == 404

Dockerfile:

FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# production ์„ค์ •
ENV PORT=8000
ENV HOST=0.0.0.0
ENV WORKERS=4

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

๋ฐฐํฌ ๊ตฌ์„ฑ:

# gunicorn_conf.py
from multiprocessing import cpu_count

# Gunicorn ์„ค์ •
bind = "0.0.0.0:8000"
workers = cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
keepalive = 120
timeout = 120
graceful_timeout = 120
max_requests = 10000
max_requests_jitter = 1000
# ๋ฐฐํฌ ๋ช…๋ น์–ด
gunicorn -c gunicorn_conf.py main:app

โœ… ํŠน์ง•:

  • ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ
  • ์ปจํ…Œ์ด๋„ˆํ™”
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ํ”„๋กœ๋•์…˜ ์„ค์ •
  • CI/CD ํ†ตํ•ฉ


์ฃผ์š” ํŒ

โœ… ๋ชจ๋ฒ” ์‚ฌ๋ก€:

  • ํƒ€์ž… ํžŒํŠธ ํ™œ์šฉ: Pydantic ๋ชจ๋ธ ๋ฐ ํƒ€์ž… ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ฝ”๋“œ ์•ˆ์ •์„ฑ ํ–ฅ์ƒ
  • ์˜์กด์„ฑ ์ฃผ์ž… ํ™œ์šฉ: ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ๋ฐ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ ์ฆ๋Œ€
  • ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์ตœ์ ํ™”: ๋™์‹œ์„ฑ ๋ฐ I/O ๋ฐ”์šด๋“œ ์ž‘์—… ์ตœ์ ํ™”๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ
  • ๋ฌธ์„œํ™” ์ž๋™ํ™”: OpenAPI ์Šคํ‚ค๋งˆ ๋ฐ Swagger UI๋กœ API ๋ฌธ์„œ ์ž๋™ํ™”
  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ: TestClient๋ฅผ ํ™œ์šฉํ•œ ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ ์ž๋™ํ™”
  • ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ‘œ์ค€ํ™”: ์ผ๊ด€๋œ ์—๋Ÿฌ ์‘๋‹ต์œผ๋กœ ํด๋ผ์ด์–ธํŠธ ๊ฐœ๋ฐœ ํŽธ์˜์„ฑ ํ–ฅ์ƒ
  • ์บ์‹ฑ ์ „๋žต ์ˆ˜๋ฆฝ: Redis๋‚˜ FastAPI ์บ์‹ฑ ๋ฏธ๋“ค์›จ์–ด๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๋ณด์•ˆ ์„ค์ • ํ™•์ธ: ์ ์ ˆํ•œ ์ธ์ฆ, ๊ถŒํ•œ ํ™•์ธ ๋ฐ CORS ์„ค์ • ๊ตฌํ˜„
  • ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ: ๊ฐœ๋ฐœ, ํ…Œ์ŠคํŠธ, ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๋ถ„๋ฆฌ
  • ๋กœ๊น… ์‹œ์Šคํ…œ ๊ตฌ์ถ•: ๊ตฌ์กฐํ™”๋œ ๋กœ๊น…์œผ๋กœ ๋””๋ฒ„๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ฐ•ํ™”
  • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ถ„๋ฆฌ: ์ปจํŠธ๋กค๋Ÿฌ์™€ ์„œ๋น„์Šค ๋ ˆ์ด์–ด ๋ถ„๋ฆฌ๋กœ ์ฝ”๋“œ ์กฐ์งํ™”
  • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ด€๋ฆฌ: Alembic์„ ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๊ด€๋ฆฌ
  • API ๋ฒ„์ „ ๊ด€๋ฆฌ: ๊ฒฝ๋กœ๋‚˜ ํ—ค๋” ๊ธฐ๋ฐ˜ API ๋ฒ„์ „ ๊ด€๋ฆฌ ์ „๋žต ์ˆ˜๋ฆฝ
  • ๋ ˆ์ดํŠธ ๋ฆฌ๋ฏธํŒ…: ํŠธ๋ž˜ํ”ฝ ์ œํ•œ์œผ๋กœ ์„œ๋น„์Šค ์•ˆ์ •์„ฑ ๋ณด์žฅ
  • ํ—ฌ์Šค ์ฒดํฌ ๊ตฌํ˜„: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง ์—”๋“œํฌ์ธํŠธ ์ œ๊ณต


โš ๏ธ **GitHub.com Fallback** โš ๏ธ