FastAPI Shallow Dive (3) — Dependency Injection은 왜 함수인가
Published:
FastAPI Shallow Dive 시리즈
- ASGI부터 시작
- Pydantic — 타입 힌트를 런타임 validator로
- Dependency Injection ← 현재
- async와 worker 개수
2편까지는 “타입 힌트가 런타임 계약이 된다”는 큰 전제가 깔렸다. 이번 편 주제인 Dependency Injection은 그 전제를 아키텍처 수준으로 끌어올리는 장치다.
Depends() 하나로 DB 세션, 인증 유저, 설정, rate limiter, 캐시 — 전부 타입으로 주입한다. 포인트는 이게 Spring/NestJS 스타일의 IoC 컨테이너가 아니라는 것. 그냥 Python 함수다. 왜 그렇게 설계됐는지부터 본다.
기본 모양
from fastapi import FastAPI, Depends
app = FastAPI()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/items/{item_id}")
async def read_item(item_id: int, db = Depends(get_db)):
return db.query(Item).filter(Item.id == item_id).first()
get_db는 그냥 제너레이터 함수. Depends(get_db)는 “이 함수를 호출해서 결과를 주입해라”라는 마커. 특별한 기반 클래스도, 데코레이터도, 등록 단계도 없다.
요청이 오면 FastAPI가 이렇게 처리한다:
- 핸들러의 시그니처를 읽고
Depends(...)가 붙은 파라미터를 찾는다. - 각 dependency를 순서 있는 그래프로 풀어 호출한다 (
get_db()실행). - 결과를 해당 파라미터에 넘겨 핸들러 호출.
- 요청 종료 시
yield이후 코드를 실행 (db.close()).
왜 함수인가 — Pythonic DI의 설계 판단
Java/TypeScript에서 오면 이게 어색할 수 있다. Spring은 @Autowired, NestJS는 constructor(private db: DbService). 둘 다 클래스 + 컨테이너 등록이 기본값.
FastAPI는 이걸 의도적으로 버렸다. 이유는 시그니처 세 줄로 요약된다:
- Python은 타입 힌트만 있지 인터페이스가 제한적. Java의 interface, TS의 interface처럼 “계약 타입”을 컴파일러가 강제하진 않는다. IoC 컨테이너가 타입으로 해석기를 고르는 모델이 자연스럽지 않다.
- 함수는 이미 1급 객체. “주입 가능한 단위”를 굳이 클래스로 감싸지 않아도 함수가 그 역할을 한다.
yield한 줄로 setup/teardown. 클래스에__enter__/__exit__두 개 쓸 일을 제너레이터가 자연스럽게 대체.
결과: 등록 단계가 없다. get_db를 어딘가에 register 하지 않아도 Depends(get_db) 하면 끝. 테스트에서 갈아끼울 때는 app.dependency_overrides[get_db] = fake_get_db 한 줄.
트레이드오프는 있다. 함수 시그니처에 dependency를 나열해야 해서, 많아지면 파라미터 목록이 길어진다. 이걸 줄이려고 “Depends 객체 하나로 묶기” 패턴을 쓰는데, 뒤에서 본다.
서브 의존성 — Dependency of Dependency
Dependency도 dependency를 가질 수 있다.
from fastapi import Depends, Header, HTTPException
def get_settings() -> Settings:
return Settings()
def get_db(settings: Settings = Depends(get_settings)):
db = create_engine(settings.database_url)
try:
yield db.connect()
finally:
db.dispose()
def get_current_user(
db = Depends(get_db),
token: str = Header(...),
) -> User:
user = lookup(db, token)
if not user:
raise HTTPException(401)
return user
@app.get("/me")
async def me(user: User = Depends(get_current_user)):
return user
그래프는 이렇게 풀린다:
me
└ get_current_user
├ get_db
│ └ get_settings
└ Header(token)
FastAPI가 위상 정렬해서 호출한다. get_settings → get_db → get_current_user → me. 순서가 결정적이라 예측 가능.
요청 범위 캐시 — 같은 dependency는 한 번만
같은 요청 안에서 같은 dependency를 여러 번 선언해도 한 번만 실행된다.
@app.get("/orders")
async def list_orders(
user: User = Depends(get_current_user),
audit = Depends(audit_log), # audit_log 내부에서도 get_current_user 씀
):
...
list_orders에서 한 번, audit_log 안에서 한 번 — 두 번 쓰였지만 DB 조회는 한 번만 일어난다. FastAPI가 요청 scope에서 결과를 캐시.
끄려면 Depends(fn, use_cache=False). 단, 캐시 키는 dependency 함수의 identity + 서브 의존성 결과라, 같은 함수라도 파라미터가 다르면 별개로 친다.
yield 기반 dependency — 리소스 수명 관리
이게 실전에서 제일 자주 쓰인다.
def get_db():
db = SessionLocal()
try:
yield db
except Exception:
db.rollback()
raise
finally:
db.close()
yield db가 dependency 반환점. yield 이후 코드가 응답 후 정리 단계로 실행된다. Python의 with 문과 같은 패턴이지만, 핸들러가 끝날 때까지 열려있어야 하니 제너레이터로 풀어낸 꼴.
예외 처리 순서가 중요하다. FastAPI는 핸들러 실행 중 예외가 나면 dependency의 except 블록에도 예외를 재주입한다. 위 코드처럼 rollback 후 raise하면 트랜잭션이 자동으로 되돌려진다. SQLAlchemy 쓸 때 이 패턴이 사실상 표준.
클래스도 dependency가 될 수 있다
함수가 기본이지만, __init__ 시그니처를 DI 대상으로 쓸 수 있다.
class Pagination:
def __init__(self, skip: int = 0, limit: int = 100):
self.skip = skip
self.limit = limit
@app.get("/items")
async def list_items(page: Pagination = Depends()):
return items[page.skip : page.skip + page.limit]
Depends() 안에 아무것도 안 넣으면 타입 애너테이션을 보고 Pagination을 dependency로 해석. 이게 “파라미터 묶기” 패턴. skip, limit을 핸들러 시그니처에 늘어놓는 대신 하나의 객체로 받아 핸들러가 깔끔해진다.
실제로 팀에서 쓰던 패턴:
class CommonContext:
def __init__(
self,
user: User = Depends(get_current_user),
db = Depends(get_db),
settings: Settings = Depends(get_settings),
):
self.user = user
self.db = db
self.settings = settings
@app.post("/orders")
async def create_order(payload: OrderIn, ctx: CommonContext = Depends()):
...
핸들러 시그니처는 두 줄. 내부에서 ctx.user, ctx.db. “자주 같이 쓰이는 dependency 묶음”을 표현하는 방식으로 잘 맞는다.
보안 dependency — OAuth2, API Key
FastAPI의 security 모듈은 Depends로 감싼 보안 scheme를 제공한다.
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
return decode_token(token)
@app.get("/me")
async def me(user: User = Depends(get_current_user)):
return user
oauth2_scheme은 callable 객체다(__call__ 구현). FastAPI 입장에선 함수든 클래스 인스턴스든 callable이면 같다. 이게 보안 스펙을 OpenAPI에 자동 반영해주고, Swagger UI에 “Authorize” 버튼도 따라온다.
테스트 — override가 한 줄
DI의 제일 큰 실용적 이득이 여기서 나온다.
# tests/test_orders.py
from fastapi.testclient import TestClient
from app.main import app
from app.deps import get_db
def fake_get_db():
yield InMemoryDB()
app.dependency_overrides[get_db] = fake_get_db
client = TestClient(app)
def test_list_orders():
res = client.get("/orders")
assert res.status_code == 200
app.dependency_overrides가 함수 → 함수 dict. 테스트에서 DB를 in-memory로, 결제 API를 mock으로, 외부 호출을 stub로 — 한 줄씩 바꾼다. 운영 코드에는 손을 대지 않는다.
Spring처럼 @MockBean이나 profile 전환을 준비하지 않아도 된다. 이 단순함이 FastAPI DI의 진짜 강점이라고 본다.
Spring / NestJS와의 비교
| 항목 | Spring | NestJS | FastAPI |
|---|---|---|---|
| 주입 단위 | 클래스 (@Component) | 클래스 (@Injectable()) | 함수 또는 callable |
| 등록 | 컨테이너 자동 스캔 | 모듈 providers 배열 | 없음 — Depends(fn)으로 직접 참조 |
| Scope | singleton / prototype / request | default / request / transient | per-request + 같은 요청 내 캐시 |
| 설정값 주입 | @Value, @ConfigurationProperties | ConfigService | Depends(get_settings) + Pydantic Settings |
| 테스트 교체 | @MockBean, profile | providers 배열 오버라이드 | app.dependency_overrides[...] |
| Setup/Teardown | @PostConstruct / @PreDestroy, try-with-resources | lifecycle hook | generator yield 패턴 |
정리하면: FastAPI DI = Python 함수 + Depends 마커 + 요청 범위 캐시. 그 이상도 이하도 아니다. 복잡한 컨테이너를 깔 필요가 없어서 러닝 커브가 낮고, Spring을 몰라도 큰 서비스를 DI 잘 붙여 끌고 갈 수 있다.
정리
- FastAPI DI는 함수 기반. 등록 단계 없음,
Depends(fn)한 줄이면 주입. - 서브 의존성이 DAG로 풀린다. 순서 결정적, 같은 dependency는 요청 범위에서 한 번만 실행.
yield기반 dependency가 setup/teardown의 표준 패턴. SQLAlchemy 세션 수명 관리에 자연스럽게 맞음.- 클래스 dependency는 파라미터 묶기에 쓴다. “공통 컨텍스트” 패턴이 유용.
- 보안도
Depends로. 테스트 override는app.dependency_overrides한 줄. - Spring/NestJS 대비 단순함이 설계 의도. 그게 Python 스타일에 맞춰 나온 결과.
다음 편은 4편, async와 worker 개수. def 핸들러와 async def 핸들러가 내부에서 어떻게 다르게 실행되는지, Uvicorn worker를 몇 개로 설정할지, 그리고 그 결정이 왜 GIL 시리즈의 프레임워크로 귀결되는지.
