FastAPI로 “빨리” 만들고, Django처럼 “오래” 버티는 2025 백엔드 베스트 프랙티스
들어가며
2025년의 Python 백엔드 개발은 “프레임워크 선택”보다 아키텍처 일관성 + 운영 가능성(Observability/보안/배포)이 성패를 가릅니다. FastAPI는 ASGI 기반의 높은 처리량과 타입 기반 개발 경험으로 API 개발을 가속하지만, 팀 규모가 커질수록 “빨리 만든 코드”가 “빨리 망가지는 코드”가 되기 쉽습니다.
반대로 Django는 강력한 Admin/ORM/보안 기본값으로 장기 운영에 유리하지만, 순수 API 서버로서의 DX(문서/스키마 중심 설계)와 비동기 I/O 확장성은 FastAPI가 더 낫습니다. 그래서 최근 현업에서는 Django를 System of Record(도메인/권한/관리)로 두고 FastAPI를 고성능 API 레이어로 두는 혼합 아키텍처가 자주 등장합니다. (sunscrapers.com)
🔧 핵심 개념
1) “Dependency Injection이 곧 아키텍처” (FastAPI의 Depends)
FastAPI의 Depends는 단순 편의 기능이 아니라 레이어 분리의 강제 장치입니다.
- Route는 HTTP/validation에 집중
- Service는 유스케이스/트랜잭션 경계 담당
- Repository/DB session은 DI로 주입
이 구조를 지키면 테스트가 쉬워지고, Django와의 경계(“어디까지 Django 로직을 재사용할 것인가”)도 명확해집니다.
2) Lifespan으로 “앱 단위 리소스”를 안전하게 관리
DB Engine, HTTP client, 캐시 커넥션 같은 전역 리소스는 request마다 만들면 비용이 큽니다. FastAPI는 ASGI Lifespan(Startup/Shutdown)으로 이를 관리합니다. 다만 Mount된 sub-application에는 Lifespan이 실행되지 않을 수 있다는 점이 함정입니다. (fastapi.tiangolo.com)
즉, Django+FastAPI를 한 프로세스에서 합성(mount)할 때 “누가 메인 앱인가”에 따라 초기화 코드 위치가 달라져 장애로 이어질 수 있습니다.
3) Django + FastAPI 통합의 현실적 결론: “경계와 프로세스”
통합 패턴은 크게 (A) 분리 배포, (B) 도메인 단일/프로세스 분리, (C) 한 프로세스에서 mount 등으로 나뉘는데, 공통 핵심은:
- Auth는 Django(또는 IdP)가 소스, FastAPI는 토큰 검증자로 동작
- FastAPI가 Django ORM을 직접 만질 경우 트랜잭션/동시성 모델을 반드시 이해해야 함
이 패턴별 장단점을 명시적으로 비교하고 선택하는 것이 2025 “베스트 프랙티스”에 가깝습니다. (sunscrapers.com)
4) DB 레이어: SQLAlchemy 2.0 AsyncSession의 “동시성 함정”
SQLAlchemy 2.0 asyncio 문서에서 특히 중요한 포인트는 AsyncSession을 concurrent task에서 공유하면 안 된다는 류의 제약(세션은 요청/유스케이스 단위)입니다. (docs.sqlalchemy.org)
따라서 FastAPI에서는 “요청마다 세션 생성/종료”를 DI로 묶고, 트랜잭션 범위는 service 계층에서 명확히 잡는 방식이 안정적입니다.
💻 실전 코드
아래 예시는 “2025년형 기본 골격”을 보여줍니다:
- Lifespan에서 Engine/Sessionmaker 준비
Depends로 요청 단위AsyncSession주입- Service에서 트랜잭션 경계 설정
- 테스트에서는
dependency_overrides로 DB를 갈아끼움 (fastapi.tiangolo.com)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
from __future__ import annotations
from contextlib import asynccontextmanager
from dataclasses import dataclass
from typing import AsyncGenerator
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
# ---------------------------
# DB / Model
# ---------------------------
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
email: Mapped[str] = mapped_column(unique=True, index=True)
DATABASE_URL = "sqlite+aiosqlite:///./app.db"
@dataclass(frozen=True)
class AppState:
sessionmaker: async_sessionmaker[AsyncSession]
@asynccontextmanager
async def lifespan(app: FastAPI):
# 앱 단위 리소스 초기화 (Engine/Sessionmaker)
engine = create_async_engine(DATABASE_URL, echo=False, pool_pre_ping=True)
sm = async_sessionmaker(engine, expire_on_commit=False)
# FastAPI app.state에 보관 (주의: mount 구성에 따라 main app에만 lifespan이 실행될 수 있음)
app.state.state = AppState(sessionmaker=sm)
yield
# 종료 시 정리
await engine.dispose()
app = FastAPI(title="2025 Best Practice API", version="1.0.0", lifespan=lifespan)
async def get_db(app_: FastAPI = app) -> AsyncGenerator[AsyncSession, None]:
# 요청 단위 세션 생성/종료
sm: async_sessionmaker[AsyncSession] = app_.state.state.sessionmaker
async with sm() as session:
yield session
# ---------------------------
# Schema / Service
# ---------------------------
class UserOut(BaseModel):
id: int
email: str
class CreateUserIn(BaseModel):
email: str
class UserService:
def __init__(self, db: AsyncSession):
self.db = db
async def create_user(self, email: str) -> User:
# 트랜잭션 경계를 service에서 명확히: 유스케이스 단위로 commit/rollback
user = User(email=email)
self.db.add(user)
try:
await self.db.commit()
except Exception:
await self.db.rollback()
raise
await self.db.refresh(user)
return user
async def get_user(self, user_id: int) -> User | None:
return await self.db.get(User, user_id)
async def get_by_email(self, email: str) -> User | None:
q = await self.db.execute(select(User).where(User.email == email))
return q.scalar_one_or_none()
def get_user_service(db: AsyncSession = Depends(get_db)) -> UserService:
return UserService(db=db)
# ---------------------------
# Routes (HTTP 레이어는 얇게)
# ---------------------------
@app.post("/users", response_model=UserOut)
async def create_user(payload: CreateUserIn, svc: UserService = Depends(get_user_service)):
exists = await svc.get_by_email(payload.email)
if exists:
raise HTTPException(status_code=409, detail="email already exists")
user = await svc.create_user(payload.email)
return UserOut(id=user.id, email=user.email)
@app.get("/users/{user_id}", response_model=UserOut)
async def read_user(user_id: int, svc: UserService = Depends(get_user_service)):
user = await svc.get_user(user_id)
if not user:
raise HTTPException(status_code=404, detail="not found")
return UserOut(id=user.id, email=user.email)
# ---------------------------
# Testing tip (개념 예시)
# - FastAPI는 app.dependency_overrides로 의존성을 테스트용으로 교체 가능
# ---------------------------
# app.dependency_overrides[get_db] = override_get_db # 공식 문서 패턴
⚡ 실전 팁
1) “Route는 얇게, Domain은 두껍게”를 강제하라
FastAPI는 handler에 로직을 넣기 쉬운 구조라서, 초반엔 빠르지만 6개월 뒤 유지보수가 무너집니다. Depends로 service/repo를 주입하고 HTTP 레이어를 얇게 두면, Django와 병행(점진적 마이그레이션)도 쉬워집니다. (seequ.dev)
2) Lifespan 초기화 위치가 운영 장애를 만든다
FastAPI 문서에서 말하듯 Lifespan 이벤트는 “메인 앱에만” 적용될 수 있습니다. mount 구조에서 “DB engine 초기화가 안 된 채로 트래픽을 받는” 사고가 흔합니다. 통합 아키텍처에서는 메인 앱을 명확히 정의하고, 상태 공유 전략(app.state vs 별도 컨테이너)을 결정하세요. (fastapi.tiangolo.com)
3) 테스트는 dependency_overrides로 ‘속도’와 ‘격리’를 얻는다
외부 인증, 결제, DB 같은 의존성은 테스트에서 비용/불안정성을 유발합니다. FastAPI는 app.dependency_overrides로 의존성을 교체할 수 있어, service 계층 테스트가 특히 쉬워집니다. (fastapi.tiangolo.com)
4) Django + FastAPI 조합은 “인증/권한의 단일화”가 핵심 통합 글들에서 공통적으로 강조하는 포인트는 “Django auth를 진실의 원천으로 두고 API는 토큰 기반으로”입니다. 세션 쿠키를 그대로 공유하려면 CSRF/CORS/도메인 정책까지 함께 설계해야 하므로, 보통은 JWT/OAuth2로 경계를 세우는 편이 안전합니다. (bix-tech.com)
5) SQLAlchemy AsyncSession은 ‘요청 공유 금지’가 기본값 AsyncSession을 전역으로 돌리거나 background task에서 공유하면 예측 불가능한 문제가 생깁니다. “요청/유스케이스 단위 세션 + service 트랜잭션 경계”를 규칙으로 못 박으세요. (docs.sqlalchemy.org)
🚀 마무리
2025년 FastAPI 베스트 프랙티스의 핵심은 “기술 스택 나열”이 아니라, DI로 경계를 강제하고(Layering), Lifespan으로 리소스를 통제하며, Django와의 책임 분리를 명문화하는 것입니다.
다음 단계로는 (1) OpenAPI 기반의 버저닝/에러 모델 표준화, (2) Observability(OpenTelemetry/구조화 로깅) 도입, (3) 인증/인가를 IdP 중심(OAuth2/OIDC)으로 정리하는 흐름을 추천합니다. Django를 운영 기반으로 두되 FastAPI를 API 경험의 전면에 세우면, “빠르게 출시하고 오래 운영하는” 구조에 가장 가깝게 도달합니다. (sunscrapers.com)