Lined Notebook

Python Dependency Injection

by Understand

Python을 주로 사용하다가 백엔드 업무를 하게 되면서 Java Spring 을 사용하게 되었다. Java Spring에서는 의존성 주입(Dependency Injectio)을 잘 활용하면서 객체간 결합도를 낮추는 방법을 활용하고 있었다. 이 부분을 ML이나 RL에 대한 연구와 같이 변동성이 매우 큰 업무에 잘 활용하면 좋을 것 같아서 Python에서는 어떤 방식으로 의존성 주입을 구현하고 사용하고 있는지 확인해보았다.

 

의존성 주입

의존성 주입은 객체가 의존성을 조립하는 책임을 의존성 주입 객체가 갖게 된다.

의존성 주입을 잘 활용하면 클래스간 결합도를 낮춰서 확장할 때 편리하다. 테스트할 때도 목을 사용하기 더 편리해진다.

 

하지만 의존성 주입을 위한 새로운 클래스를 만들기 때문에 전체 클래스 수는 늘어난다. 그리고 해당 클래스에 어떤 구체 클래스가 주입되는지 명시적으로 표현이 안되기 때문에 코드 추적이 더 어려워진다. 다만 전체 클래스 수는 늘어나도 코드라인 수는 줄어드는 경우가 많고, 주입을 받는 클래스에서는 주입하는 클래스에 대한 구체적인 정보를 몰라도 구현할 수 있기 때문에 많은 경우 단점보다 장점이 크다.

 

파이썬에서의 의존성 주입

Java와 같은 정적언어에서는 의존성 주입을 많이 사용한다. Python에서는 어떨까?


의존성 주입은 객체에 대한 의존성을 컴파일 시간이 아닌 런타임 시간에 결정하는 것으로도 생각할 수 있다. 여기서 질문이 생긴다.

 

Python과 같은 동적언어는 런타임에 구체 클래스가 결정되는 것 아닌가? 그러면 Python에서 굳이 의존성 주입을 사용해야 할까?

 

의존성 주입을 하게 되면 오히려 클래스가 많아지기 때문에 코드가 복잡해 질 수 있다. 또한 타입 힌팅이 제대로 되어 있지 않으면 어떤 의존성 객체가 주입되고 있는지 파악하기 어려워 오동작하거나 런타임 오류 발생 위험이 생길수 도 있다. 이러한 이유로 파이썬과 같은 동적 언어에서는 의존성 주입을 굳이 사용해야 하냐는 질문 글이 올라오기도 한다.

 

경우에 따라 다르겠지만, 나는 코드를 변경하거나 확장하는 경우가 많을 때는 의존성 주입을 활용하는 것이 좋다고 생각한다. 초기에는 불편함이 생길 수 있지만 이러한 불편함보다 의존성주입을이후 소프트웨어가 커지면서 생기는 유지보수 비용이 더 크다고 생각하기 때문이다. 또한 Python에서는 라이브러리를 활용해서 이러한 불편함을 줄일 수 있다. 

 

Python Dependency Injector

나는 Python 여러 의존성 주입 툴 중에서 가장 유명한 Python dependency injector를 공부했다. Python depedency Injector는 여러가지 장점이 있다.

  1. Cython으로 핵심 로직이 구현되어 있어 상대적으로 오버헤드가 작다
  2. Dependency Injection 기능만을 제공하기 때문에 많은 분야에 사용하기 편하다.
  3. FastAPI와 같은 검증된 다른 라이브러리에서 이미 활용하고 있다.
  4. 사용하기 편리하다. 문서화가 잘되어 있고, 테스트하기 편하다.

코드를 통해 Python dependency injector의 간단한 사용법을 알아보자

 

먼저 다음과 같은 ApiClient와, Service 객체 그리고 main함수가 있다. 아래 코드에 python dependency injector를 적용해보자

from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject

class ApiClient:

    def __init__(self, api_key: str, timeout: int) -> None:
        self.api_key = api_key  # <-- dependency is injected
        self.timeout = timeout  # <-- dependency is injected


class Service:

    def __init__(self, api_client: ApiClient) -> None:
        self.api_client = api_client  # <-- dependency is injected


def main(service: Service) -> None:  # <-- dependency is injected
    ...

 

먼저 Java의 @Configuration과 같이 각 객체의 의존성이 어떻게 주입되어야 하는지 정의해야 한다. 

from dependency_injector import containers, providers


class Container(containers.DeclarativeContainer):

    config = providers.Configuration()

    api_client = providers.Singleton(
        ApiClient,
        api_key=config.api_key,
        timeout=config.timeout,
    )

    service = providers.Factory(
        Service,
        api_client=api_client,
    )

 

Provider는 객체의 프록시가 되어 객체를 생성하거나 의존성 주입을 하는 모듈이다.
`provider.Configuration()` 과 같이 환경변수나 yaml, json, pydantic과 같은 자체적인 config 파일을 자동으로 읽어줄수도 있고,
`provider.Singleton`이나 `provider.Factory`와 같이 싱글톤객체나 팩토리를 쉽게 생성할 수 있다.

Container는 Provider의 집합체이다.

 

이렇게 정의된 Container를 활용해 다음과 같이 의존성을 주입할 수 있다.
main 함수는 @inject 데코레이터를 사용해 인자에 있는 service를 호출할 때 자동으로 주입되어, 따로 입력하지 않는다.

from dependency_injector.wiring import Provide, inject


@inject
def main(service: Service = Provide[Container.service]) -> None:
    ...


if __name__ == "__main__":
    container = Container()
    container.config.api_key.from_env("API_KEY", required=True)
    container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
    container.wire(modules=[__name__])

    main()  # <-- dependency is injected automatically

 

이후 테스트할 때는 아래와 같이 Mock을 쉽게 override 할 수 있다.

    with container.api_client.override(mock.Mock()):
        main()  # <-- overridden dependency is injected automatically

 


간단하게 Python에서 dependency injection에 대한 여러 의견과 그럼에도 부룩하고 dependency injector를 쓰는 이유를 알아보았고, python dependency injector의 간단한 사용법을 확인했다.

 

다음에는 각 모듈의 내부 구현을 확인해보고 facebook에서 최근에 발표한 Pearl이라는 강화학습 모듈화 라이브러리를 python dependency injector를 사용해 리팩토링 후 후기를 작성하도록 하겠다.

블로그의 정보

BookStoreDiary

Understand

활동하기