Dependency Injector로 Flask 및 FastAPI 강화하기
James Reed
Infrastructure Engineer · Leapcell

소개
Flask 및 FastAPI와 같은 최신 웹 프레임워크는 개발자가 효율적이고 확장 가능한 API를 구축하는 방식을 혁신했습니다. 애플리케이션의 복잡성이 증가함에 따라 다양한 구성 요소 간의 의존성을 관리하는 것은 중요한 과제가 될 수 있습니다. 강하게 결합된 코드는 유지보수성, 테스트 용이성 및 전반적인 애플리케이션 유연성을 저해합니다. 여기서 제어 역전(IoC) 및 의존성 주입(DI)의 개념이 매우 중요해집니다. 의존성 생성 및 관리를 외부화함으로써 더 모듈화되고 탄력적인 시스템을 구축할 수 있습니다. 이 문서는 python-dependency-injector
라이브러리를 Flask 및 FastAPI 프로젝트에 원활하게 통합하여 IoC의 이점을 활용하는 방법을 살펴보고, 더 깔끔하고 테스트 용이하며 유지보수 가능한 코드베이스를 만들 수 있도록 합니다.
핵심 개념 이해
구현 세부 사항으로 들어가기 전에 논의의 기초가 되는 핵심 개념을 명확하게 이해해 봅시다.
의존성: 소프트웨어 공학에서 의존성은 구성 요소가 기능을 수행하기 위해 필요로 하는 구성 요소 또는 객체를 참조합니다. 예를 들어, UserService
는 데이터베이스와 상호 작용하기 위해 UserRepository
에 의존할 수 있습니다.
제어 역전(IoC): IoC는 객체 생성, 구성 및 라이프사이클 관리의 제어가 애플리케이션 코드 자체에서 컨테이너 또는 프레임워크로 이전되는 설계 원칙입니다. 구성 요소가 자체 의존성을 생성하는 대신 외부 소스에서 이를 받습니다.
의존성 주입(DI): DI는 구성 요소가 자체를 인스턴스화하는 대신 의존성이 구성 요소에 '주입'되는 IoC의 특정 구현입니다. 이 주입은 생성자 주입, 세터 주입 또는 인터페이스 주입을 통해 발생할 수 있습니다. 주요 이점으로는 모듈성, 테스트 용이성 향상 및 결합 감소가 있습니다.
DI 컨테이너: DI 컨테이너(예: python-dependency-injector
)는 객체와 그 의존성의 인스턴스화 및 라이프사이클을 관리하는 프레임워크입니다. 애플리케이션 전체에서 의존성을 정의, 해결 및 주입하기 위한 중앙 집중식 메커니즘을 제공합니다.
python-dependency-injector
는 Python을 위한 강력하고 유연한 DI 컨테이너입니다. 이를 통해 애플리케이션 구성 요소가 어떻게 구축되고 함께 연결되는지 선언할 수 있어 복잡한 객체 그래프를 쉽게 관리할 수 있습니다.
Dependency Injector를 사용한 제어 역전 구현
python-dependency-injector
라이브러리는 의존성을 선언적으로 정의하고 관리하는 방법을 제공하여 IoC를 용이하게 합니다. Flask와 FastAPI에서의 구현 및 적용을 살펴보겠습니다.
통합 원칙
핵심 원칙은 애플리케이션의 서비스와 그 의존성을 위한 청사진 역할을 하는 Container
클래스를 정의하는 것입니다. 그런 다음 이러한 컨테이너는 특정 서비스 인스턴스로 확인되는 프로바이더를 노출합니다. Flask 또는 FastAPI 엔드포인트가 특정 서비스를 필요로 할 때 직접 인스턴스화하는 대신 컨테이너에서 요청하며, 컨테이너는 필요한 하위 의존성의 생성 및 주입을 처리합니다.
Flask에서의 애플리케이션
사용자를 관리하는 간단한 Flask 애플리케이션을 고려해 봅시다.
먼저 라이브러리를 설치합니다:
pip install dependency-injector Flask
핵심 구성 요소를 정의합니다:
# app/services.py class UserRepository: def __init__(self, db_connection_string: str): self.db_connection_string = db_connection_string print(f"UserRepository initialized with: {db_connection_string}") def get_user(self, user_id: int) -> str: # 데이터베이스 호출 시뮬레이션 return f"User {user_id} fetched from {self.db_connection_string}" class UserService: def __init__(self, user_repository: UserRepository): self.user_repository = user_repository print("UserService initialized") def find_user(self, user_id: int) -> str: return self.user_repository.get_user(user_id)
이제 Container
를 정의합니다:
# app/containers.py from dependency_injector import containers, providers from app.services import UserRepository, UserService class ApplicationContainer(containers.DeclarativeContainer): config = providers.Configuration() user_repository = providers.Singleton( UserRepository, db_connection_string=config.db.connection_string ) user_service = providers.Factory( UserService, user_repository=user_repository )
마지막으로 Flask와 통합합니다:
# app/__init__.py from flask import Flask from app.containers import ApplicationContainer def create_app() -> Flask: app = Flask(__name__) app.config.from_mapping({"DB_CONNECTION_STRING": "sqlite:///my_database.db"}) # 컨테이너 초기화 container = ApplicationContainer() container.config.db.connection_string.from_env("DB_CONNECTION_STRING", "sqlite:///default.db") container.config.from_dict(app.config) # Flask 구성 로드하여 컨테이너에 주입 container.wire(modules=[__name__]) # 의존성 연결 @app.route("/") def index(): return "Welcome to the Flask DI example!" @app.route("/user/<int:user_id>") def get_user(user_id: int, user_service: UserService = container.user_service) -> str: # user_service는 컨테이너에 의해 주입됨 return user_service.find_user(user_id) return app
ApplicationContainer
를 초기화하고, 구성을 로드하고, container.wire
를 사용하여 의존성을 사용할 수 있도록 합니다. 라우트 핸들러 서명에 user_service
가 직접 주입된다는 점에 유의하십시오. 이는 dependency-injector
의 와이어링 기능의 힘을 통해 달성되지만, Flask 확장이 아닌 경우 Flask에서는 외부 Flask 확장 없이는 수동으로 컨테이너에서 검색해야 하는 경우가 많습니다. Flask의 단순성과 직접성을 위해, 라우트 함수에서 container.service_name
에 직접 액세스하거나 inject
데코레이터를 사용하는 것이 일반적입니다.
FastAPI에서의 애플리케이션
내장된 강력한 의존성 주입 시스템을 갖춘 FastAPI는 python-dependency-injector
와 매우 잘 어울립니다.
먼저 설치합니다:
pip install dependency-injector FastAPI uvicorn
Flask 예제의 app/services.py
및 app/containers.py
파일을 재사용할 수 있습니다.
이제 FastAPI와 통합합니다:
# main.py from fastapi import FastAPI, Depends from dependency_injector.wiring import inject, Provide from app.containers import ApplicationContainer from app.services import UserService def create_app() -> FastAPI: app = FastAPI() container = ApplicationContainer() container.config.db.connection_string.from_env("DB_CONNECTION_STRING", "sqlite:///default.db") # FastAPI에서는 일반적으로 환경 변수 또는 Pydantic 설정을 사용하여 구성을 설정합니다. # 시연을 위해 직접 설정해 보겠습니다. container.config.db.connection_string.set("postgresql://user:pass@host:port/dbname") container.wire(modules=[__name__]) @app.on_event("startup") async def startup_event(): print("FastAPI app starting up") @app.on_event("shutdown") async def shutdown_event(): print("FastAPI app shutting down") @app.get("/") def read_root(): return {"message": "Welcome to the FastAPI DI example!"} @app.get("/user/{user_id}") @inject async def get_user( user_id: int, user_service: UserService = Depends(Provide[ApplicationContainer.user_service]) ): return {"user": user_service.find_user(user_id)} return app app = create_app()
FastAPI 예제에서는 ApplicationContainer.user_service
가 Depends
에 제공됩니다. Depends
는 FastAPI의 의존성 주입 메커니즘입니다. dependency_injector.wiring
의 @inject
데코레이터는 함수 서명 내에서 선언된 모든 의존성이 컨테이너 프로바이더와 일치하는 경우 자동으로 해결되도록 보장합니다. 이는 통합을 매우 깔끔하고 강력하게 만듭니다.
프로바이더 유형 및 중요성
python-dependency-injector
는 다양한 프로바이더 유형을 제공하며, 각기 특정 목적을 수행합니다:
providers.Singleton
: 요청될 때마다 동일한 객체 인스턴스를 반환합니다. 데이터베이스 연결, 전역 캐시 또는 요청별 상태를 유지하지 않는 공유 서비스에 이상적입니다.providers.Factory
: 요청될 때마다 객체의 새 인스턴스를 생성합니다. 요청별 상태를 유지하거나 요청 간에 공유되지 않아야 하는 서비스에 적합합니다.providers.Callable
: 요청될 때마다 호출 가능(함수 또는 메서드)의 결과를 제공합니다.providers.Resource
: 리소스(예: 데이터베이스 세션)의 라이프사이클을 관리합니다. 설정 및 해제 논리를 제공합니다.providers.Configuration
: 종종 환경 변수, 파일 또는 사전에서 로드된 구성 값을 제공합니다.
올바른 프로바이더 유형을 선택하는 것은 최적의 리소스 관리 및 애플리케이션 동작에 매우 중요합니다.
이점 및 애플리케이션 시나리오
python-dependency-injector
사용의 이점은 매우 큽니다.
- 향상된 테스트 용이성: 서비스는 더 이상 자체 의존성을 인스턴스화하지 않으므로 단위 및 통합 테스트 중에 쉽게 모의(mock)하거나 테스트 대체물로 교체할 수 있습니다.
- 결합 감소: 구성 요소는 의존성이 생성되는 방식과 독립적이 되어 느슨하게 결합된 아키텍처로 이어집니다.
- 향상된 모듈성: 각 서비스가 단일 책임을 갖는 명확한 관심사 분리.
- 쉬운 구성 관리: 의존성의 중앙 집중식 구성으로, 다른 환경(개발, 스테이징, 프로덕션) 간을 쉽게 전환할 수 있습니다.
- 더 나은 유지보수성: 인터페이스가 일관성을 유지하는 한, 의존성 구현의 변경은 소비자에게 직접적인 영향을 미치는 경우가 거의 없습니다.
이 접근 방식은 특히 다음 사항에 유용합니다.
- 상호 연결된 서비스가 많은 대규모 애플리케이션.
- 다양한 배포 환경에 대한 유연한 구성이 필요한 애플리케이션.
- 강력한 단위 및 통합 테스팅이 우선 순위인 프로젝트.
- 잘 정의된 인터페이스를 통해 서비스가 통신하는 마이크로서비스 아키텍처.
결론
Flask 및 FastAPI 프로젝트에 python-dependency-injector
를 통합하는 것은 더 강력하고 테스트 가능하며 유지보수 가능한 애플리케이션을 구축하기 위한 전략적 움직임입니다. 제어 역전을 수용함으로써 시스템에 더 큰 유연성과 명확한 관심사 분리를 부여합니다. 이 방법론은 코드베이스를 정리할 뿐만 아니라 적응하고 확장하는 능력을 크게 향상시킵니다.