[FastAPI] 4. FastAPI Dependency Injection 이해하기
1. Dependency란?
Dependency(의존성)란 어떤 코드가 자신의 기능을 수행하기 위해 필요로 하는 다른 코드, 객체, 기능 또는 자원을 의미합니다. 예를 들어 UserService가 사용자 정보를 조회하기 위해 UserRepository를 사용해야 한다면 다음과 같은 의존 관계가 존재합니다.
UserService
↓ depends on
UserRepository
Dependency는 특정 Framework의 기능이나 Design Pattern이 아니라 프로그램을 여러 컴포넌트로 나누어 개발하면 자연스럽게 발생하는 관계입니다. 대표적인 Dependency의 예는 다음과 같습니다.
- Service가 사용하는 Repository
- Repository가 사용하는 Database Connection
- Endpoint가 필요로 하는 인증된 사용자 정보
- 여러 API가 공통으로 사용하는 인증 및 권한 확인 로직
2. Dependency Injection이란?
Dependency를 사용하는 코드가 Dependency를 직접 생성하게 되면 사용하는 코드가 Dependency의 구체적인 구현과 생성 방법까지 알아야 합니다.
class UserService:
def __init__(self):
self.repository = MySQLUserRepository()
위 구조에서는 UserService와 MySQLUserRepository가 강하게 결합되어 있습니다. 이를 다음과 같이 변경할 수 있습니다.
class UserService:
def __init__(self, repository):
self.repository = repository
이제 UserService는 Repository를 직접 생성하지 않고 외부에서 전달받습니다. 이와 같이 코드가 필요로 하는 Dependency를 직접 생성하지 않고 외부에서 제공받도록 하는 설계 방식을 Dependency Injection(DI)이라고 합니다. DI를 이용하면 Dependency를 사용하는 책임과 생성하고 구성하는 책임을 분리할 수 있습니다. 이를 통해 다음과 같은 장점을 얻을 수 있습니다.
- 코드 간 결합도 감소
- 공통 기능의 재사용
- Dependency 구현 교체 용이
- 테스트 시 Fake/Mock Dependency 사용 용이
- Database, 인증, 권한 검사 등의 공통 처리 분리
Dependency Injectino은 IoC(Inversion of Control)를 구현하는 대표적인 방식 중 하나입니다.
3. FastAPI의 Depends()
FastAPI에서는 Depends()를 이용하여 Path Operation Function이 필요로 하는 Dependency를 선언합니다.
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_pagination(skip: int = 0, limit: int = 100):
return {"skip": skip, "limit": limit}
@app.get("/items/")
async def read_items(
pagination: Annotated[dict, Depends(get_pagination)]
):
return pagination
위 코드에서 “Depends(get_pagination)”은 get_pagination()을 직접 호출하는 코드가 아닙니다. FastAPI에게 “read_items()를 실행하기 위해 get_pagination이라는 Dependency가 필요하다”라고 선언하는 것입니다.
FastAPI는 Request가 들어오면 Dependency를 실행하고 반환값을 pagination Parameter에 전달합니다.
Request
↓
FastAPI
↓
get_pagination 실행
↓
Dependency 결과 반환
↓
pagination에 주입
↓
read_items 실행
따라서 개발자는 Dependency를 직접 호출하고 결과를 전달하는 코드를 작성할 필요가 없습니다.
4. Dependency의 Parameter는 어떻게 전달되는가?
Dependency 함수 역시 일반적인 Path Operation Function과 동일한 방식으로 Parameter를 선언할 수 있습니다.
def get_user(user_id: int):
...
다음과 같이 사용할 수 있습니다.
@app.get("/users/{user_id}")
def read_user(
user: Annotated[dict, Depends(get_user)]
):
return user
Client가 다음과 같이 요청한다면,
GET /users/10
FastAPI는 get_user()의 함수 Signature를 분석합니다. 개념적으로 다음과 같은 처리를 FastAPI가 대시 수행하는 것입니다.
user = get_user(user_id=10)
read_user(user=user)
Dependency의 Parameter가 Query Parameter라면 Query String에서 값을 가져오며, 또 다른 Depends()가 선언되어 있다면 해당 Dependency를 먼저 해결합니다.
5. Sub-dependency와 Dependency Graph
Dependency 자체가 또 다른 Dependency를 필요로 할 수도 있습니다. 이를 Sub-dependency라고 합니다.
def get_token():
...
def get_current_user(
token: Annotated[str, Depends(get_token)]
):
...
@app.get("/users/me")
def read_user(
user: Annotated[User, Depends(get_current_user)]
):
...
의존 관계는 다음과 같습니다.
get_token
↓
get_current_user
↓
read_user
FastAPI는 이러한 Dependency 관계를 분석하여 필요한 순서대로 Dependency를 해결합니다. 이와 같은 Dependency간의 관계를 Dependency Graph라고 볼 수 있습니다. FastAPI에서는 Sub-dependency를 여러 단계로 구성할 수 있으며, 동일한 Request에서 동일한 Dependency가 여러 곳에서 필요할 경우 기본적으로 한 번 계산한 결과를 재사용합니다.
6. yield란?
yield는 FastAPI의 기능이 아니라 Python 언어에서 Generator를 만들기 위해 제공하는 기능입니다. 일반적인 return은 값을 반환한 뒤 함수를 종료합니다. 반면 yield는 값을 외부로 전달하면서 현재 함수의 실행 상태를 유지한 채 일시 정지합니다.
def number_generator():
print("start")
yield 1
print("middle")
yield 2
yield 1에 도달하면 Generator는 해당 위치에서 일시 정지합니다. 이후 다시 Generator를 실행하면 처음부터 시작하는 것이 아니라 yield 1 이후부터 실행을 이어갑니다. 따라서 yield 자체는 Dependency Injection과는 직접적인 관계가 있는 기능은 아닙니다.
7. FastAPI에서 yield를 사용하는 이유
FastAPI에서는 yield의 특성을 이용하여 사용 전 준비와 사용 후 정리가 모두 필요한 Dependency를 처리할 수 있습니다. 대표적인 예가 Database Session입니다.
def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
개념적인 흐름은 다음과 같습니다.
get_db 실행
↓
DB Session 생성
↓
yield db
↓
Endpoint에 DB Session 제공
↓
Endpoint에서 사용
↓
Dependency 실행 재개
↓
db.close()
여기서 yield가 Database를 직접 종료하는 것은 아닙니다. 실제로 정리하는 것은 db.close()이며, yield는 자원을 외부에서 사용하는 동안 Dependency 함수의 실행을 일시 정지했다가 이후 다시 실행할 수 있도록 해주는 역할을 합니다.
FastAPI는 이러한 Generator의 실행과 종료 과정을 관리하므로 개발자가 직접 next()를 호출할 필요는 없습니다.
8. return Dependency와 yield Dependency
모든 Dependency에 yield를 사용할 필요는 없습니다.
return |
yield |
|---|---|
| 값을 생성하여 전달 | 값을 전달한 뒤 후처리 가능 |
| 별도 정리가 필요 없는 Dependency | 자원의 정리가 필요한 Dependency |
| 일반적인 공통 로직 | DB Session, Connection 등의 Lifecycle 관리 |
따라서 yield는 주로 획득한 뒤 반드시 해제해야 하는 Resource를 Dependency로 사용할 때 유용합니다.
9. 실습
기본 Dependency와 Sub-dependency
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
users = {
1: {"id": 1, "name": "Alice", "active": True},
2: {"id": 2, "name": "Bob", "active": False},
}
def get_user(user_id: int):
user = users.get(user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
def verify_active_user(
user: Annotated[dict, Depends(get_user)]
):
if not user["active"]:
raise HTTPException(status_code=403, detail="Inactive user")
return user
@app.get("/users/{user_id}")
async def read_user(
user: Annotated[dict, Depends(verify_active_user)]
):
return user
위 코드의 Dependnecy Graph는 다음과 같습니다.
Request
↓
get_user
↓
verify_active_user
↓
read_user
↓
Response
다음 요청을 통해 동작을 확인할 수 있습니다.
-
“/users/1 → 정상 Response”
위 이미지를 보면 코드에서 동작하도록 한 것과 같이 정상적으로 Response를 받아오는 것을 확인할 수 있습니다.
-
“/users/2 → 403”
위 이미지를 보면 코드에서 동작하도록 한 것과 같이
403에러가 발생하는 것을 확인할 수 있습니다. -
“/users/3 → 404”
위 이미지를 보면 코드에서 동작하도록 한 것과 같이
404에러가 발생하는 것을 확인할 수 있습니다.
yield Dependency
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
class Resource:
def __init__(self):
print("Resource created")
def close(self):
print("Resource closed")
def get_resource():
resource = Resource()
try:
yield resource
finally:
resource.close()
@app.get("/resource")
async def use_resource(
resource: Annotated[Resource, Depends(get_resource)]
):
print("Endpoint executed")
return {"message": "success"}
이 실습에서는 다음 Lifecycle을 확인할 수 있습니다.
Resource 생성
↓
Endpoint에서 Resource 사용
↓
Resource 정리
아래 이미지와 같이 Swagger에서 실행을 하면,
코드를 실행한 곳에서 아래 이미지와 같은 로그가 뜨는 것을 확인할 수 있습니다.
10. 정리
Dependency 관련 개념은 다음과 같이 구분하여 이해할 수 있습니다.
Dependency
→ 코드가 동작하기 위해 필요로 하는 다른 기능·객체·자원
Dependency Injection
→ Dependency를 직접 생성하지 않고 외부에서 제공받는 설계 방식
Depends()
→ FastAPI에서 Dependency가 필요하다고 선언하는 방법
Sub-dependency
→ Dependency가 다른 Dependency를 필요로 하는 관계
Dependency Graph
→ 여러 Dependency 사이의 의존 관계
yield
→ 실행 상태를 유지한 채 함수를 일시 정지하고
이후 다시 이어서 실행할 수 있도록 하는 Python 기능
FastAPI의 Dependency Injection에서 가장 중요한 것은 Depends() 문법 자체보다 Dependency를 사용하는 코드와 Dependency를 생성/준비하는 코드를 분리한다는 것입니다.
FastAPI는 Depends()에 선언된 Dependency와 그 Parameter를 분석하여 필요한 값을 해결하고 결과를 Path Operation Function에 주입합니다. 또한 사용 후 정리가 필요한 Dependency는 Python의 yield를 활용하여 Resource의 Lifecycle을 관리할 수 있습니다.
참조
- FastAPI 공식 문서 - Dependencies
- FastAPI 공식 문서 - Sub-dependencies
- FastAPI 공식 문서 - Dependencies with yield
- Python 공식 문서 - The
yieldstatement - Martin Fowler - Inversion of Control Containers and the Dependency Injection pattern
Comments