-
[FastAPI] 한국에는 없는 새로운 표준을 제안합니다. Depends를 넘어 IoC Container로개발/Python (파이썬) 2025. 12. 10. 15:25
FastAPI는 현재 대한민국 Python 백엔드 생태계에서 사랑받는 프레임워크로 자리 잡았습니다. 직관적인 문법, 비동기 처리에 최적화된 성능, 그리고 무엇보다 Depends라는 강력한 의존성 주입 도구가 그 인기의 비결이라고 할 수 있습니다.
하지만 프로젝트 규모가 커지고 비즈니스 로직이 고도화될수록, 우리는 불편한 진실을 마주하게 됩니다. "과연 Depends가 엔터프라이즈 아키텍처의 만능열쇠일까요?"
많은 개발팀이 서비스 계층의 순수성을 지키기 위해 소위 Provider 패턴(Factory 함수)을 표준처럼 사용합니다. 하지만 이 방식은 프로젝트가 성장하는 순간, 유연한 아키텍처가 아니라, 끝없는 막노동을 시작하게 하는 시작점이 되어버립니다.
저는 이 문제를 근본적으로 해결하고 국내 FastAPI 생태계에 새로운 아키텍처 표준을 제안하고자 합니다. 바로 Python Dependency Injector를 활용한 선언적 IoC(Inversion of Control) Container 패턴입니다.
기존의 표준: Provider 패턴과 "관리 비용의 지옥"
많은 시니어 개발자들은 Service 코드를 프레임워크로부터 독립된 순수 객체 (POJO 스타일)로 유지하기를 원합니다. 이를 위해 객체의 생성과 주입을 담당하는 공장(Factory)함수를 별도로 만드는 방식을 주로 채택합니다.
서비스 코드는 깨끗해 보이지만, 그 뒤편의 dependencies.py (조립 공장) 파일은 처참해집니다.
코드와 다이어그램으로 보는 Provider 패턴의 현실
서비스가 20개가 넘어가고, 서비스끼리 서로 의존하기 시작하면 어떤 일이 벌어질까요? 우리는 아래와 같은 수동 배선(Manual Wiring)의 늪에 빠지게 됩니다.
# dependencies.py - 끝없이 늘어나는 Factory 함수들 from fastapi import Depends # 기초 부품 생성 def get_repository(): return UserRepository() def get_email_sender(): return EmailSender() # 의존성 사슬의 시작 (Service A) # 개발자가 의존성을 일일이 기억해서 Depends로 명시해야 합니다. def get_user_service( repo: UserRepository = Depends(get_repository), email: EmailSender = Depends(get_email_sender) ) -> UserService: return UserService(repo, email) # 꼬리에 꼬리를 무는 의존성 (Service B -> Service A) def get_auth_service( #AuthService를 만들려면 UserService가 필요하고 그려면 get_user_service를 불러와야 한다... user_service: UserService = Depends(get_user_service) ) -> AuthSerivce: return AuthService(user_service) # 복잡도 증가 def get_notification_service( auth_service: AuthService = Depends(get_auth_service), email_sender: EmailSender = Depends(get_email_sender), ) -> NotificationService: return NotificationService(auth_service, email_sender) # 이런 함수를 서비스 개수만큼 20개, 30개 계속 만들어야 한다.
이 방식의 치명적인 단점은 다음과 같습니다.
- 무한 반복 노동
서비스 클래스를 하나 만들 때마다, 그에 1:1로 매핑되는 get_XXX함수를 기계적으로 생산해야 한다. - 휴먼 에러의 온상
get_notification_service를 정의할 때, 실수로 Depends(get_auth_service) 대신 다른 의존성을 주입하거나 누락하면 런타임 에러가 발생한다. - 가독성 실종
전체 의존성 구조를 파악하려면 Depends 체인을 따라가며 스무고개 하듯 코드를 뒤져야 한다. "누가 누구를 참조하는가?"를 한눈에 볼 수 없습니다.
이것은 잘 짜여진 아키텍처가 아닌, 그저 반복 노동일뿐입니다.
새로운 표준의 제안: Declarative IoC Container
이제 우리는 수동 조립(Manual Wiring)의 시대를 끝내고 선언적(Declarative) 관리의 시대로 넘어가야 합니다.
Java의 Spring Framework가 거대해질 수 있었던 핵심 이유는 강력한 IoC Container 덕분입니다. Python에서도 dependency-injeector 같은 라이브러리를 통해 이 강력함을 FastAPI에 이식할 수 있습니다.
지도: Containers.py
더 이상 수십 개의 get_XXX 함수를 만들지 마세요. containers.py 파일 하나에 프로젝트의 모든 의존성 지도를 그리세요
# containers.py from dependency_injector import containers, providers class Container(containers.DeclarativeContainer): # 싱글톤 관리: lru_cache 없이 명시적이고 완벽한 싱글톤을 보장합니다. user_repo = providers.Singleton(UserRepository) email_sender = providers.Singleton(EmailSender) # 자동 연결: 함수를 짤 필요 없이 관계만 선언합니다. user_service = providers.Factory( UserService, repo=user_repo, email=email_sender ) auth_service = providers.Factory( AuthService, user_service=user_service # 위에서 정의한 providers를 그대로 주입합니다. ) notification_service = providers.Factory( NotificationService, auth_service=auth_serivce, email_sender=email_sender )
수십 줄의 Depends 코드가 사라지고, 어떤 객체가 무엇을 필요로 하는지 한눈에 보이는 명확한 청사진이 완성되었습니다.
순수한 비즈니스 로직
이제 서비스 코드는 프레임워크가 무엇인지, 객체가 어떻게 주입되는지 전혀 신경 쓰지 않습니다. 오직 비즈니스에만 집중합니다.
# service/notification_service.py class NotificationService: # 오직 필요한 부품만 생성자에 명시합니다. (Pure Python Object) def __init__(self, auth_servie: AuthService, email_sender: EmailSender): self.auth_servcie = auth_service self.email_sender = email_sender def send alert(self, user_id: str): user = self.auth_service.get_user(user_id) self.email_sender.send(user.email, "Alert!")우아한 사용
컨테이너를 통해 라우터도 깔끔해집니다. @inject 데코레이터와 Provide 마커를 사용하면, 복잡한 조립과정은 컨테이너에 위임하고 결과물만 받아올 수 있습니다.
# routers/notification.py from dependency_injector.wiring import inject, Provide from my_app.containers import Container @router.post("/alert") @inject def send_notification( user_id: str service: NotificationService = Depends(provide[Container.notification_service]) ): service.send_alert(user_id) return {"status": "ok"}왜 이것이 '표준'이 되어야 하는가?
아직 한국에서는 FastAPI에 외부 IoC 컨테이너를 붙이는 것이 낯선 시도일 수 있습니다. 하지만 이는 단순히 "멋있어 보이는 기술"을 도입하는 것이 아닙니다. 이것은 소프트엔지니어링의 원칙을 지키기 위한 선택입니다.
완벽한 관심사의 분리 (Separation of Concerns)
객체의 생성과 사용을 완벽하게 분리합니다. 비즈니스 로직은 어떻게 만들어지는가를 몰라야 합니다. IoC Container는 이 책임을 전담하여 클린 아키텍처를 실현합니다.
테스트 혁명
Depends를 오버라이드하여 테스트하는 것은 깊은 의존성 트리를 가질 때 매우 고통스럽습니다. 하지만 IoC Container를 사용하면 단 한 줄로 특정 컴포넌트를 Mocking 할 수 있습니다.
# 테스트 코드 예시 container.email_sender.override(mock_email_sender) # 이후 실행되는모든 로직은 Mock 객체를 사용하게 됩니다.확장성
서비스가 100개가 되어도, container.py라는 지도만 보면 전체 구조가 파악됩니다. 흩어져 있는 Factory 함수 100개를 뒤적거릴 필요가 없습니다. 중앙화된 설정 관리는 대규모 프로젝트의 유지보수성을 극적으로 향상합니다.
핵심은 '도구'가 아니라 '아키텍처'입니다.
이 글에서 예시로 든 dependency-injector 라이브러리는 현재 유지보수 모드에 가깝습니다.
하지만 제가 강조하고 싶은 것은 특정 라이브러리가 아닙니다. 바로 IoC Container라는 아키텍처 패턴 그 자체입니다.
Depends에 의존하여 비즈니스 로직과 프레임워크를 강결합시키는 관성에서 벗어나야 합니다. Dishka, Punq 같은 최신 라이브러리를 사용하든, 혹은 직접 간단한 컨테이너 클래스를 구현하든 그것은 중요하지 않습니다.
중요한 것은 다음의 가치를 지키는 것입니다.- 관심사의 분리 (Separation of Concerns)
객체의 '생성'과 '사용'을 완벽히 분리하세요 - 테스트 용이성
컨테이너 레벨에서 의존성을 교체하여, 거대한 의존성 트리 없이도 단위 테스트가 가능하게 만드세요 - 구조적 단순함
수십 개의 get_XXX 함수 대신, 하나의 명확한 지도(Container)를 가지세요
FastAPI의 Depends는 훌륭한 기능이지만, 아키텍처의 종착역은 아닙니다. 여러분의 프로젝트가 단순한 API 서버를 넘어 복잡한 도메인을 다루는 시스템으로 진화하고 있다면, 이제 IoC Container라는 새로운 표준을 도입할 때입니다.
마치며
FastAPI의 Depends는 훌륭하고 간편합니다. 하지만 간편함과 단순함은 다릅니다.
수많은 Depends 체인으로 얽힌 코드는 당장 작성하기엔 '간편'할지 몰라도, 구조적으로 '단순'하지 않습니다. 복잡하게 얽힌 실타래와 같습니다. 반면, IoC Container를 도입하는 것은 초기 학습과 설정이 필요하지만, 시스템의 구조를 명확하고 '단순'하게 유지해 줍니다.
이제 막 FastAPI로 서비스를 시작하거나, 복잡해지는 의존성 때문에 스파게티 코드를 경험하고 계신다면, 이 새로운 표준을 도입해 보시기를 강력히 추천합니다.
여러분의 코드는 더 견고하고 우아해질 자격이 있습니다.
함께 보기
제 작은 프로젝트에서 FastAPI와 IoC 컨테이너를 사용한 사례가 있습니다.
관심이 있다면 함께 봐주세요.
https://github.com/BCD-q/pencil-me-fastapi'개발 > Python (파이썬)' 카테고리의 다른 글
Python으로 REST API 구현 (1) 2022.05.04 - 무한 반복 노동