프로바이더 프레임워크 없이 파이썬에서 빠른 의존성 주입

발행: (2026년 6월 9일 PM 04:54 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

Python은 기본적으로 의존성 주입 컨테이너가 필요하지 않습니다.

repo = UserRepository(settings.database_url)
email = EmailSender(settings.smtp_url)
use_case = RegisterUser(repo, email)

이렇게 하면 한 번은 괜찮습니다.
문제는 같은 객체 그래프가 여러 곳에서 나타날 때 시작됩니다:

  • FastAPI 시작 시점
  • Typer 명령
  • 백그라운드 워커
  • 스크립트
  • 테스트

그 시점에서 질문은 “모든 것을 어떻게 주입할까?”가 아니라
더 나은 질문은:
애플리케이션 배선은 어디에 두어야 할까?

웹 애플리케이션의 경우 나는 다음과 같이 구분합니다:

  • FastAPI는 HTTP 어댑테이션을 담당
  • Typer는 CLI 어댑테이션을 담당
  • 워커는 작업 어댑테이션을 담당
  • 애플리케이션은 서비스 배선을 담당

즉, 레포지토리, 게이트웨이, 유스케이스는 프레임워크 기본형이 아니라 일반 Python 타입에 의존해야 합니다.
서비스는 자신이 HTTP 요청, CLI 명령, 큐 워커, 혹은 테스트 중 어디서 호출됐는지 알 필요가 없어야 합니다.

FastAPI Depends는 요청 경계에서 뛰어나지만, 같은 서비스 그래프가 HTTP 외부에서도 사용된다면 실제 구성 루트는 순수 Python에 있어야 합니다.

유용한 형태는 다음과 같습니다:

def build_services(settings: Settings) -> Services:
    client = ApiClient(settings)
    repo = UserRepository(client)
    email = EmailSender(client)

    return Services(
        register_user=RegisterUser(repo, email),
    )

FastAPI는 그 그래프를 다음과 같이 어댑트할 수 있습니다:

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.services = build_services(load_settings())
    yield

def get_register_user(request: Request) -> RegisterUser:
    return request.app.state.services.register_user

워커와 CLI도 같은 빌더를 직접 사용할 수 있습니다:

services = build_services(load_settings())
services.register_user.execute("ada@example.com")

경험 법칙

  • FastAPI는 HTTP를 어댑트한다.
  • 애플리케이션은 서비스 배선을 소유한다.

나는 여전히 plain factories가 가장 좋은 기본이라고 생각합니다.
그래프가 작다면 어떤 컨테이너도 추가하지 않는 것이 더 낫습니다:

def build_register_user(settings: Settings) -> RegisterUser:
    client = ApiClient(settings)
    repo = UserRepository(client)
    email = EmailSender(client)

    return RegisterUser(repo, email)

DI 컨테이너는 동일한 그래프가 여러 진입점과 테스트에 반복될 때 유용해집니다.
나는 그 중간 지점을 위해 Injex를 만들었습니다.
전체 프로바이더 프레임워크는 아니지만, 다음과 같은 작은 Python 앱에 적합합니다:

  • 명시적 등록
  • 타입 힌트 기반 생성자 주입
  • 싱글톤, 트랜지언트, 스코프드 라이프타임
  • 테스트 오버라이드
  • 시작 전 그래프 검증
  • 런타임 의존성 제로

예시:

from injex import Container

container = Container()

container.add_instance(Settings, settings)
container.add_singleton(ApiClient)
container.add_transient(UserRepository)
container.add_transient(EmailSender)
container.add_transient(RegisterUser)

container.assert_valid()

use_case = container.resolve(RegisterUser)

애플리케이션 클래스는 그대로 둡니다:

class RegisterUser:
    def __init__(self, repo: UserRepository, email: EmailSender):
        self.repo = repo
        self.email = email

생성자 주입을 위해 데코레이터가 필요하지 않습니다.

Injex 1.3.0의 주요 변화

  • 내부 정리
  • 반복 resolve 속도 향상

내부적으로 패키지는 이제 다음과 같은 모듈로 나뉩니다:

  • container.py
  • planning.py
  • registry.py
  • errors.py

성능을 위해 Injex는 의존성 플랜을 캐시하고, 일반적인 생성자 주입 그래프에 대해 빠른 경로를 사용합니다.

작은 서비스 그래프에 대한 재현 가능한 벤치마크를 추가했습니다:

  • 싱글톤 Settings
  • 싱글톤 ApiClient(settings)
  • 트랜지언트 UserRepository(client)
  • 트랜지언트 EmailSender(client)
  • 트랜지언트 AuditLog(settings)
  • 트랜지언트 RegisterUser(repo, email, audit)
LibraryMedian resolve time
manual wiring0.265 µs/op
Injex0.818 µs/op
Wireup, same scope0.879 µs/op
Wireup, scope per operation1.559 µs/op
dependency-injector1.727 µs/op
lagom9.794 µs/op
punq56.795 µs/op

이는 보편적인 순위가 아닙니다.
다른 그래프, 라이프타임, 비동기 리소스, 프레임워크 통합, 요청 스코프 모델에 따라 결과가 달라질 수 있습니다.

벤치마크는 다음과 같은 좁은 질문에 답하기 위해 존재합니다:
명시적 타입 배선이 작고 빠르게 유지될 수 있는가?
이 그래프에서는 입니다.

재현 방법:

uv run --with punq --with lagom --with dependency-injector --with wireup \

언제 Injex를 사용하지 않을까

  • 몇 번의 생성자 호출만으로도 충분히 명확할 때
  • 프레임워크의 의존성 시스템이 모든 진입점을 커버할 때
  • 앱에 큰 프로바이더/설정 DSL이 필요할 때
  • 팀이 컨테이너 자체를 원하지 않을 때

수동 배선이 여전히 기준점입니다.

언제 Injex를 고려할까

  • 서비스 레이어가 API, CLI, 워커, 테스트에서 재사용될 때
  • 생성자가 이미 타입 힌트로 의존성을 설명하고 있을 때
  • 테스트에서 외부 서비스에 대한 임시 오버라이드가 필요할 때
  • 시작 시점에 누락된 등록을 첫 요청/작업 전에 잡아내고 싶을 때
  • 팀이 큰 DI 프레임워크 없이 명시적인 배선을 원할 때

Repo: https://github.com/vshulcz/injex
Docs: https://vshulcz.github.io/injex/
Performance notes: https://vshulcz.github.io/injex/docs/performance.html
Compared to FastAPI Depends: https://github.com/vshulcz/injex/blob/main/docs/fastapi-depends.md

0 조회
Back to Blog

관련 글

더 보기 »

Eidentic 소개

Today we're releasing Eidentic, an open-source TypeScript SDK for building AI agents with self-improving memory and the production fundamentals built in — not b...

Typescript의 타입

Introdução Tipos são uma forma de definir a “forma” ou o contrato dos dados que estamos usando no código. Pensando em Javascript puro, ele é dinâmico: você pode...