클린 아키텍처에서는 애플리케이션 계층(Application Layer)이 인프라스트럭처 계층(Infrastructure Layer)에 의존해서는 안된다. 대신 추상화(인터페이스)에 의존해야 하고, 구체적인 구현체는 실행 시점에 주입되어야 한다.
# 도메인 계층 (추상화)
from abc import ABC, abstractmethod
class UserRepositoryInterface(ABC):
@abstractmethod
def get_by_email(self, email: str) -> Optional[User]:
pass
@abstractmethod
def create(self, user: User) -> User:
pass
# 애플리케이션 계층
class UserService:
def __init__(self, user_repo: UserRepositoryInterface): # 추상화에 의존
self.user_repo = user_repo
def authenticate_user(self, email: str, password: str):
user = self.user_repo.get_by_email(email)
# 인증 로직...
# 인프라스트럭처 계층
class UserRepository(UserRepositoryInterface): # 구체적인 구현
def __init__(self, db: Session):
self.db = db
def get_by_email(self, email: str) -> Optional[User]:
return self.db.query(User).filter(User.email == email).first()
# 표현 계층
@router.post("/login/")
def login(
credentials: LoginCredentials,
user_service: UserService,
):
return user_service.authenticate_user(credentials.email, credentials.password)
하지만 FastAPI의 Depends()
만으로는 이런 구조를 자연스럽게 구현하기 어렵다. user_repo: UserRepositoryInterface = Depends(SupabaseUserRepository)
라고 작성하는 순간, UserService
가 SupabaseUserRepository
에 의존해 버리는 모순이 발생한다.
class UserRepositoryInterface(ABC):
...
# 애플리케이션 계층
class UserService:
def __init__(
self,
**user_repo: UserRepositoryInterface = Depends(UserRepository)**
):
...
# 인프라스트럭처 계층
def get_db():
db = SessionLocal()
try:
yield db
db.commit()
except:
db.rollback()
raise
finally:
db.close()
class UserRepository(UserRepositoryInterface):
def __init__(
self,
**db: Session = Depends(get_db)**
):
...
# 표현 계층
def get_service():
return UserService()
@router.post("/login/")
def login(
credentials: LoginCredentials,
**user_service: UserService = Depends(get_service),**
):
...
이 문제를 해결하려면 IoC(Inversion of Control) 컨테이너를 사용해야 한다(ref1). Python에서는 dependency-injector
라이브러리가 이런 목적으로 널리 사용된다.
pip install dependency-injector
IoC 컨테이너를 설정한다.
# app/container.py
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject
from app.db.session import get_db
from app.repositories.user_repository import SupabaseUserRepository
from app.services.user_service import UserService
class Container(containers.DeclarativeContainer):
# 설정
config = providers.Singleton(Config)
# 데이터베이스 세션 (FastAPI에서 자주 등장하는 get_db 활용)
db_session = providers.Resource(get_db)
# 리포지토리 계층
user_repository = providers.Factory(
SupabaseUserRepository,
db=db_session
)
# 서비스 계층
user_service = providers.Factory(
UserService,
user_repo=user_repository
)
<aside> 💡
dependency-injector
는 여러 종류의 프로바이더를 제공한다.
먼저 FastAPI 엔드포인트에서 구현체 대신 IoC 컨테이너를 연결한다.
# app/api/endpoints/users.py
from dependency_injector.wiring import Provide, inject
from app.container import Container
from app.services.user_service import UserService
@router.post("/login/")
@inject
def login(
credentials: LoginCredentials,
**user_service: UserService = Depends(Provide[Container.user_service]),**
):
return user_service.authenticate_user(credentials.email, credentials.password)
변화된 부분인 Depends(Provide[Container.user_service])
에 주목해 보자. 이는 FastAPI의 Depends()
와 dependency-injector
의 Provide
를 결합한 것이다. Provide[Container.user_service]
는 IoC 컨테이너에서 UserService
인스턴스를 가져오는 함수를 생성하고, Depends()
는 이 함수를 FastAPI의 의존성 주입 시스템에 등록한다.
Before:
---
title: Before - FastAPI Depends()만 사용 (의존성 역전 없음)
---
classDiagram
class LoginController {
+login(credentials)
}
class UserService {
+authenticate_user()
-user_repo: UserRepository
}
class UserRepository {
+get_by_email()
+create()
-db: Session
}
LoginController --> UserService : Depends(UserService)
UserService --> UserRepository : Depends(UserRepository)
note for LoginController "컨트롤러가 구체 클래스에 직접 의존"
After:
---
title: After - IoC 컨테이너 사용, 소스코드 PoV
---
classDiagram
class LoginController {
+login(credentials)
}
class UserService {
+authenticate_user()
-user_repo: UserRepositoryInterface
}
class SupabaseUserRepository {
+get_by_email()
+create()
-db: Session
}
class Container {
+user_service: Factory
+user_repository: Factory
}
LoginController --> Container : Depends(Provide[Container.user_service])
UserService --> Container : Depends(Provide[Container.user_repository])
SupabaseUserRepository --> Container : Depends(Provide[Container.db_session])
---
title: After - IoC 컨테이너 사용, 컨테이너 내부 PoV
---
classDiagram
class LoginController {
+login(credentials)
}
class UserService {
+authenticate_user()
-user_repo: UserRepositoryInterface
}
class SupabaseUserRepository {
+get_by_email()
+create()
-db: Session
}
class Container {
+user_service: Factory
+user_repository: Factory
}
Container ..> UserService : creates
Container ..> SupabaseUserRepository : creates
LoginController --> UserService
UserService --> SupabaseUserRepository
<aside> 💡
소스코드 입장에서 논리적으로는 컨테이너에 의존하는 모양새가 된다. 그 덕분에 컨테이너 내부에서는 마치 조물주가 피조물들에 영혼을 불어넣는 과정을 제어하듯, 어떤 구현체를 사용할지를 선택할 수 있어 소스코드의 강력한 중앙 관리자의 역할을 할 수 있게 된다.
</aside>
마지막으로 FastAPI 앱을 초기화할 때 컨테이너를 설정한다.
# app/main.py
from fastapi import FastAPI
from app.container import Container
from app.api.endpoints import users
def create_app() -> FastAPI:
container = Container()
container.config.database_url.from_env("DATABASE_URL")
app = FastAPI()
app.container = container
# 의존성 주입 와이어링
container.wire(modules=[users])
app.include_router(users.router)
return app
app = create_app()