FastAPI의 의존성 주입 기능 활용하기
Min-jun Kim
Dev Intern · Leapcell

소개
견고하고 유지보수 가능한 API를 구축하는 것은 현대 소프트웨어 개발의 초석입니다. 애플리케이션의 복잡성이 증가함에 따라, 클래스나 함수가 작업을 수행하기 위해 필요한 객체 또는 서비스인 의존성을 관리하는 것은 중요한 과제가 될 수 있습니다. 전통적인 접근 방식은 종종 코드를 매우 밀결합시켜 테스트, 재사용 및 발전을 어렵게 만듭니다. 이것이 바로 의존성 주입(DI)이 구성 요소를 분리하고 코드 유연성을 향상시키는 강력한 패턴을 제공하는 곳입니다. FastAPI는 Python 3.7+ 기반의 표준 Python 타입 힌트를 사용하는 현대적이고 빠른(고성능) API 구축 웹 프레임워크로, 의존성 주입을 핵심 기능으로 채택하고 있습니다. 이 글에서는 FastAPI의 의존성 주입 시스템을 심층적으로 살펴보고, 그 기본 원리를 밝히고, 다양한 적용 사례를 보여주고, 효과적인 테스트를 위한 실용적인 팁을 제공할 것입니다.
의존성 주입의 핵심 개념
FastAPI의 구체적인 내용으로 들어가기 전에, 의존성 주입과 관련된 주요 용어에 대한 공통된 이해를 확립해 봅시다:
- 의존성 (Dependency): 다른 객체(종속 객체)가 올바르게 기능하기 위해 필요로 하는 객체 또는 서비스입니다. 예를 들어, 데이터베이스와 상호 작용하는 서비스의 경우 데이터베이스 클라이언트는 의존성입니다.
- 종속 객체 (Dependent): 하나 이상의 의존성이 필요한 객체 또는 함수입니다.
- 제어의 역전 (Inversion of Control, IoC): 시스템의 제어 흐름이 전통적인 절차 프로그래밍에 비해 역전되는 설계 원칙입니다. 종속 객체가 직접 의존성을 생성하거나 조회하는 대신, 외부 엔터티(주입기)가 이를 제공할 책임을 집니다. 의존성 주입은 IoC를 달성하기 위한 특정 기법입니다.
- 주입기 (Injector, DI Container): 종속 객체에 의존성을 구축하고 제공하는 책임을 맡은 구성 요소입니다. FastAPI에서는 프레임워크 자체가 주입기 역할을 합니다.
- 프로바이더 (Provider, Dependency Function): FastAPI가 의존성을 "제공"하기 위해 호출하는 함수 또는 클래스입니다. 이러한 함수는 종종
@Depends
로 장식되거나 라우트 함수의 시그니처에서 직접 참조됩니다.
FastAPI의 의존성 주입 시스템 설명
FastAPI의 의존성 주입 시스템은 표준 Python 타입 힌트와 Depends
유틸리티를 기반으로 구축됩니다. 핵심적으로, Python의 함수 인자 내성(introspection)을 활용하여 필요한 의존성을 식별합니다.
작동 방식:
요청이 도착하면 FastAPI는 경로 작업 함수 ( @app.get
, @app.post
등으로 장식된 함수)의 시그니처를 검사합니다. 경로 작업 함수의 시그니처에 있는 각 매개변수에 대해:
- 타입 힌트 확인: 매개변수에 타입 힌트가 있지만 기본값이 없는 경우, FastAPI는 이를 경로 매개변수, 쿼리 매개변수 또는 요청 본문으로 해석하려고 시도합니다.
Depends
유틸리티: 매개변수의 기본값이Depends()
로 래핑된 객체인 경우, FastAPI는 이를 의존성으로 인식합니다.Depends()
에 전달된 인자는 일반적으로 의존성 함수 또는 클래스입니다.- 의존성 해결: FastAPI는 의존성 함수를 호출하거나 클래스를 인스턴스화합니다. 의존성 함수의 반환 값(또는 인스턴스화된 객체)이 경로 작업 함수에서 해당 매개변수의 값이 됩니다.
- 의존성 체이닝: 의존성 함수 자체도 자체 의존성을 선언하여 의존성 해결 체인을 만들 수 있습니다. FastAPI는 이를 재귀적으로 처리합니다.
- 수명 주기 관리: FastAPI는 요청 후 정리해야 하는 데이터베이스 세션이나 파일 핸들과 같은 의존성을 처리하기 위해 의존성 함수에서
yield
를 사용하여 메커니즘을 제공합니다.
간단한 예제를 통해 설명하겠습니다:
from fastapi import FastAPI, Depends, HTTPException, status app = FastAPI() # 현재 사용자를 가져오는 간단한 의존성 함수 def get_current_user(token: str): if token == "secret-token": return {"username": "admin", "id": 1} raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials", ) @app.get("/users/me/") async def read_current_user(current_user: dict = Depends(get_current_user)): return current_user # 이 예제를 실행하려면: # uvicorn your_module_name:app --reload # 그런 다음 브라우저나 cURL을 사용하여 http://127.0.0.1:8000/users/me/?token=secret-token 에 액세스합니다. # 잘못된 토큰으로 시도해 보세요: http://127.0.0.1:8000/users/me/?token=wrong-token
이 예제에서:
get_current_user
는 우리의 의존성 함수입니다.token
을 예상하는데, 이는 FastAPI가 기본적으로 쿼리 매개변수에서 가져오려고 시도합니다.read_current_user
는 경로 작업 함수입니다.Depends(get_current_user)
를 기본값으로 하는current_user
를 매개변수로 선언합니다.GET /users/me/
가 호출되면 FastAPI는 먼저get_current_user
를 호출합니다.get_current_user
가 사용자를 반환하면 해당 사용자 객체가read_current_user
의current_user
인자로 전달됩니다.get_current_user
가HTTPException
을 발생시키면 해당 예외는 FastAPI에 의해 처리됩니다.
일반적인 사용 사례 및 적용
FastAPI의 DI 시스템은 매우 다재다능하며 다양한 시나리오에 적용할 수 있습니다:
-
데이터베이스 세션 관리: 데이터베이스와 상호 작용해야 하는 모든 경로 작업 함수에 데이터베이스 세션 또는 연결을 제공합니다. 이렇게 하면 올바른 세션 처리(열기, 커밋/롤백, 닫기)가 보장됩니다.
from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session from fastapi import Depends, FastAPI SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def get_db(): db = SessionLocal() try: yield db finally: db.close() app = FastAPI() @app.get("/items/") def read_items(db: Session = Depends(get_db)): # 데이터베이스 작업을 위해 db 사용 # 예: return db.query(models.Item).all() return {"message": "Database session provided"}
여기서
get_db
는yield
를 사용하여 데이터베이스 세션의 수명 주기를 관리합니다. 세션은 라우트 함수 실행 전에 열리고, 오류가 발생하더라도 이후에 닫힙니다. -
인증 및 권한 부여:
get_current_user
예제에서 본 것처럼 DI는 사용자 자격 증명을 추출하고, 유효성을 검사하고, 인증된 사용자 객체를 경로 작업 함수에 주입하는 데 완벽합니다. -
구성 설정 주입: 애플리케이션의 관련 부분에 애플리케이션 전반의 구성 매개변수(예: API 키, 환경 변수)를 제공합니다.
from pydantic_settings import BaseSettings, SettingsConfigDict from fastapi import Depends, FastAPI from functools import lru_cache class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore") app_name: str = "My Awesome App" admin_email: str = "admin@example.com" items_per_page: int = 10 @lru_cache() # 성능을 위해 설정 객체 캐싱 def get_settings(): return Settings() app = FastAPI() @app.get("/info/") def get_app_info(settings: Settings = Depends(get_settings)): return { "app_name": settings.app_name, "admin_email": settings.admin_email, "items_per_page": settings.items_per_page, }
pydantic-settings
와Depends
를 사용하면 구성 관리가 깔끔하고 테스트 가능해집니다.@lru_cache
는 생성 비용이 많이 들고 각 요청마다 변경되지 않는 의존성에 좋은 최적화입니다. -
비즈니스 로직/서비스 주입: 서비스 클래스나 함수를 주입하여 비즈니스 로직을 라우트 핸들러와 분리합니다.
from fastapi import FastAPI, Depends class ItemService: def get_all_items(self): return [{"id": 1, "name": "Item A"}, {"id": 2, "name": "Item B"}] def create_item(self, name: str): # DB 저장 시뮬레이션 return {"id": 3, "name": name, "status": "created"} def get_item_service(): return ItemService() app = FastAPI() @app.get("/items_service/") def list_items(item_service: ItemService = Depends(get_item_service)): return item_service.get_all_items() @app.post("/items_service/") def add_item(name: str, item_service: ItemService = Depends(get_item_service)): return item_service.create_item(name)
여기서
ItemService
는 항목 관련 비즈니스 로직을 캡슐화하여 라우트 핸들러를 더 깔끔하고 독립적으로 테스트할 수 있게 합니다.
테스트를 위한 의존성 오버라이드
FastAPI의 DI 시스템의 가장 강력한 기능 중 하나는, 특히 개발 및 테스트에서 의존성을 오버라이드할 수 있다는 것입니다. 이를 통해 실제 의존성(예: 데이터베이스 연결)을 테스트 중인 모의 객체 또는 단순화된 버전으로 대체하여 테스트가 빠르고, 격리되고, 안정적인지 확인할 수 있습니다.
app.dependency_overrides
관리자는 이를 위한 주요 메커니즘입니다:
from fastapi.testclient import TestClient from fastapi import FastAPI, Depends, HTTPException, status app = FastAPI() # 원래 의존성 def get_current_user_prod(token: str): if token == "prod-secret": return {"username": "prod_user", "id": 1} raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) @app.get("/protected/") def protected_route(user: dict = Depends(get_current_user_prod)): return {"message": f"Hello, {user['username']}!"} # --- 테스트 설정 --- # 테스트를 위한 모의 의존성 def get_current_user_mock(): return {"username": "test_user", "id": 99} client = TestClient(app) def test_protected_route_with_mock_user(): # get_current_user_prod를 get_current_user_mock로 오버라이드 app.dependency_overrides[get_current_user_prod] = get_current_user_mock response = client.get("/protected/") assert response.status_code == 200 assert response.json() == {"message": "Hello, test_user!"} # 오버라이드 정리 # 특히 여러 테스트가 실행되고 오버라이드가 후속 테스트에 영향을 미칠 수 있는 # 테스트 스위트에서 이것은 중요합니다. app.dependency_overrides = {} # 오버라이드가 없는 경우에도 원래 로직을 사용하는 테스트 예제 def test_protected_route_prod_user(): app.dependency_overrides = {} # 확실히 남아있는 오버라이드가 없는지 확인 response = client.get("/protected/", headers={"Authorization": "Bearer prod-secret"}) # FastAPI는 간결성을 위해 헤더에서 'token'을 자동으로 구문 분석하지 않습니다. 이전 예제와 일관성을 위해 GET 매개변수를 사용한다고 가정합니다. assert response.status_code == 401 # 원래대로 'token' 쿼리 매개변수가 없으면 실패해야 합니다. response = client.get("/protected/", params={"token":"prod-secret"}) assert response.status_code == 200 assert response.json() == {"message": "Hello, prod_user!"}
테스트 예제에서:
- 우리는
get_current_user_prod
를 "실제" 의존성으로 정의합니다. - 우리는
get_current_user_mock
를 테스트를 위한 단순화된 버전으로 정의하며, 외부 요인이나 특정 토큰 값에 의존하지 않습니다. test_protected_route_with_mock_user
내부에서app.dependency_overrides
의get_current_user_prod
를get_current_user_mock
로 일시적으로 교체합니다. 이제client.get("/protected/")
가 호출되면 FastAPI는get_current_user_mock
를 사용합니다.- 매우 중요하게도, 테스트 후
app.dependency_overrides
는 재설정되어 (app.dependency_overrides = {}
) 이 오버라이드가 테스트 스위트의 다른 테스트에 영향을 미치지 않도록 합니다. 테스트 픽스처에서 더 강력한 방법을 선호할 수 있습니다.
고급 오버라이드 기법 (pytest 픽스처 사용)
대규모 테스트 스위트의 경우 app.dependency_overrides
를 수동으로 설정하고 지우는 것이 번거로울 수 있습니다. pytest
픽스처는 더 깔끔한 방법을 제공합니다:
import pytest from fastapi.testclient import TestClient from fastapi import FastAPI, Depends, HTTPException, status app = FastAPI() def get_current_user_prod(token: str): if token == "prod-secret": return {"username": "prod_user", "id": 1} raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) @app.get("/protected/") def protected_route(user: dict = Depends(get_current_user_prod)): return {"message": f"Hello, {user['username']}!"} # 테스트를 위한 모의 의존성 def get_current_user_mock(): return {"username": "test_user", "id": 99} @pytest.fixture(name="client") def test_client_fixture(): with TestClient(app) as client: yield client # 테스트 함수에 클라이언트 양도 @pytest.fixture(autouse=True) # "autouse=True"는 이 픽스처가 모든 테스트에 대해 자동으로 실행되도록 합니다. def override_get_current_user(): # 테스트 실행 전에 오버라이드 설정 app.dependency_overrides[get_current_user_prod] = get_current_user_mock yield # 테스트 실행 # 테스트 완료 후 오버라이드 정리 app.dependency_overrides = {} def test_protected_route_with_mock_user(client): response = client.get("/protected/") assert response.status_code == 200 assert response.json() == {"message": "Hello, test_user!"} # 원래 의존성 로직을 특정 테스트에서 테스트해야 하는 경우, # autouse 픽스처를 일시적으로 비활성화하거나 # 다른 클라이언트 인스턴스를 사용하기 위한 메커니즘이 필요합니다.
이 pytest
설정은 client
픽스처를 사용하는 모든 테스트에 대해 get_current_user_prod
가 get_current_user_mock
로 자동으로 대체되며, 각 테스트 후 오버라이드가 올바르게 정리되도록 합니다.
결론
FastAPI의 의존성 주입 시스템은 확장 가능하고, 테스트 가능하며, 유지보수 가능한 API를 구축하기 위한 강력하고 우아한 솔루션입니다. Python의 타입 힌트와 Depends
유틸리티를 활용하여 관심사의 명확한 분리를 촉진하고, 코드 구성을 단순화하며, 테스트 용이성을 크게 향상시킵니다. 그 원리를 이해하고 의존성 오버라이드를 마스터하는 것은 FastAPI의 잠재력을 최대한 발휘하는 데 핵심이며, 개발자가 고품질의 강력한 Python 웹 애플리케이션을 자신 있게 작성할 수 있도록 합니다. 구성 요소를 테스트를 위해 원활하게 전환할 수 있는 능력은 판도를 바꾸는 기능이며, 의존성 주입을 현대 API 개발 환경에서 없어서는 안 될 도구로 확고히 합니다.