Skip to main content

Dependency Injector - Wiring (연결)

dependency injectioninversion of controlpythonv4.41.0About 3 min

원문 : https://python-dependency-injector.ets-labs.org/wiring.htmlopen in new window

Wiring 기능은 컨테이너 Provider를 함수나 메소드에 주입시키는 방법을 제공합니다.

Wiring을 사용할 때 필요한 것:

  • @inject 데코레이터를 사용 : 데코레이터 @inject가 의존성을 주입합니다.
  • 마커를 사용 : Wiring 마커는 어떤 의존성(예, Provide[Container.bar])을 주입할지 지정합니다. 이것은 컨테이너가 주입을 찾는데 도움을 줍니다.
  • 코드의 마커로 컨테이너를 연결 : 연결하고자 하는 특정 모듈과 패키지로 container.wire()를 호출합니다.
  • 평소처럼 함수와 클래스를 사용 : 프레임워크가 지정된 주입을 제공합니다.
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject

dd
class Service:
    ...


class Container(containers.DeclarativeContainer):

    service = providers.Factory(Service)


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


if __name__ == "__main__":
    container = Container()
    container.wire(modules=[__name__])

    main()

데코레이터 @inject

데코레이터 @inject는 의존성을 주입합니다.
주입하기위해 모든 함수와 메소드에 데코레이트할 수 있습니다.

from dependency_injector.wiring import inject, Provide


@inject
def foo(bar: Bar = Provide[Container.bar]):
    ...

데코레이터 @inject는 적절한 연결동작이 확실하도록 함수의 가장 첫번째 데코레이터로 지정되어야 합니다.
또한 연결 프로세스의 성능에 기여합니다.

from dependency_injector.wiring import inject, Provide


@decorator_etc
@decorator_2
@decorator_1
@inject
def foo(bar: Bar = Provide[Container.bar]):
    ...

또한 FastAPI나 유사하게 데코레이터를 사용하는 다른 프레임워크, 클로저, 주입이 있는 사용자 지정의 데코레이터 유형에 @inject를 첫번째 데코레이터로 지정하는게 중요합니다.

FastAPI 예제는 다음과 같습니다.

app = FastAPI()


@app.api_route("/")
@inject
async def index(service: Service = Depends(Provide[Container.service])):
    value = await service.process()
    return {"result": value}

데코레이터 예제는 다음과 같습니다.

def decorator1(func):
    @functools.wraps(func)
    @inject
    def wrapper(value1: int = Provide[Container.config.value1]):
        result = func()
        return result + value1
    return wrapper


def decorator2(func):
    @functools.wraps(func)
    @inject
    def wrapper(value2: int = Provide[Container.config.value2]):
        result = func()
        return result + value2
    return wrapper

@decorator1
@decorator2
def sample():
    ...

함께보기

이슈 #404open in new window에서 @inject 데코레이터에 대해 좀더 자세하게 설명합니다.

마커

Wiring 기능은 주입을 만들기 위해 마커를 사용합니다.
주입 마커는 함수나 메소드 인자의 값으로 명시됩니다.

from dependency_injector.wiring import inject, Provide


@inject
def foo(bar: Bar = Provide[Container.bar]):
    ...

어노테이션 명시는 선택적입니다.

Provider 자체를 주입하려면 Provide[foo.provider]를 사용합니다.

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


@inject
def foo(bar_provider: Factory[Bar] = Provide[Container.bar.provider]):
    bar = bar_provider(argument="baz")
    ...

또한 Provide[foo] 를 사용하여 Provider 자체를 주입할 수 있습니다.

from dependency_injector.providers import Factory
from dependency_injector.wiring import inject, Provider


@inject
def foo(bar_provider: Factory[Bar] = Provider[Container.bar]):
    bar = bar_provider(argument="baz")
    ...

평소하는 것 처럼 구성, 제공되는 인스턴스, 하위 컨테이너 Provider를 사용하면 됩니다.

@inject
def foo(token: str = Provide[Container.config.api_token]):
    ...


@inject
def foo(timeout: int = Provide[Container.config.timeout.as_(int)]):
    ...


@inject
def foo(baz: Baz = Provide[Container.bar.provided.baz]):
    ...


@inject
def foo(bar: Bar = Provide[Container.subcontainer.bar]):
    ...

함수단위 실행범위를 구현하기 위해 Wiring과 Resource Provider를 조합할 수 있습니다.
상세한 내용은 Resource와 Wiring, 함수단위 실행 범위open in new window에서 볼 수 있습니다.

또한 컨테이너를 주입하기 위해 Provide를 사용할 수 있습니다.

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


class Service:
    ...


class Container(containers.DeclarativeContainer):

    service = providers.Factory(Service)


@inject
def main(container: Container = Provide[Container]):
    service = container.service()
    ...


if __name__ == "__main__":
    container = Container()
    container.wire(modules=[__name__])

    main()













 
 
 
 







문자열 식별자

문자열 식별자를 사용해서 연결할 수 있습니다.
문자열 식별자는 컨테이너에 Provider 이름과 일치해야합니다.

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


class Service:
    ...


class Container(containers.DeclarativeContainer):

    service = providers.Factory(Service)


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


if __name__ == "__main__":
    container = Container()
    container.wire(modules=[__name__])

    main()













 
 
 







문자열 식별자를 사용하면 주입을 지정하기 위해 컨테이너를 사용할 필요가 없습니다.

중첩된 컨테이너에서 주입을 지정하려면 점 .을 구분자로 사용합니다.

@inject
def foo(service: UserService = Provide["services.user"]) -> None:
    ...

또한 주입 수정자를 사용할 수 있습니다.

from dependency_injector.wiring import (
    inject,
    Provide,
    as_int,
    as_float,
    as_,
    required,
    invariant,
    provided,
)


@inject
def foo(value: int = Provide["config.option", as_int()]) -> None:
    ...


@inject
def foo(value: float = Provide["config.option", as_float()]) -> None:
    ...


@inject
def foo(value: Decimal = Provide["config.option", as_(Decimal)]) -> None:
    ...

@inject
def foo(value: str = Provide["config.option", required()]) -> None:
    ...

@inject
def foo(value: int = Provide["config.option", required().as_int()]) -> None:
    ...


@inject
def foo(value: int = Provide["config.option", invariant("config.switch")]) -> None:
    ...

@inject
def foo(value: int = Provide["service", provided().foo["bar"].call()]) -> None:
    ...

컨테이너를 주입할 때는 특수 식별자인 <container>를 사용합니다.

@inject
def foo(container: Container = Provide["<container>"]) -> None:
    ...

모듈과 클래스 속성으로 주입 만들기

모듈과 클래스 속성으로 주입을 만들기 위해 Wiring을 사용할 수 있습니다.

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


class Service:
    ...


class Container(containers.DeclarativeContainer):

    service = providers.Factory(Service)


service: Service = Provide[Container.service]


class Main:

    service: Service = Provide[Container.service]


if __name__ == "__main__":
    container = Container()
    container.wire(modules=[__name__])

    assert isinstance(service, Service)
    assert isinstance(Main.service, Service)










 




 











또한 Container에 대한 의존성을 피하기 위해 문자열 식별자를 사용할 수 있습니다.

service: Service = Provide["service"]


class Main:

    service: Service = Provide["service"]
 




 

모듈과 패키지 Wiring

모듈에 컨테이너를 연결하려면 container.wire() 메소드를 호출해야 합니다.

container.wire(
    modules=[
        "yourapp.module1",
        "yourapp.module2",
    ],
)

container.wire() 메소드는 상대경로도 가능합니다.

# In module "yourapp.main":

container.wire(
    modules=[
        ".module1",  # Resolved to: "yourapp.module1"
        ".module2",  # Resolved to: "yourapp.module2"
    ],
)

또한 from_package 인자를 사용하여 상대경로를 해석할 기본 패키지를 수동으로 식별할 수 있습니다.

# In module "yourapp.main":

container.wire(
    modules=[
        ".module1",  # Resolved to: "anotherapp.module1"
        ".module2",  # Resolved to: "anotherapp.module2"
    ],
    from_package="anotherapp",
)

또한 modules 인자는 가져오기한 모듈을 사용할 수 있습니다.

from yourapp import module1, module2


container = Container()
container.wire(modules=[module1, module2])

패키지를 사용하여 컨테이너와 연결시킬 수 있습니다.
컨테이너는 패키지에 있는 모듈을 재귀적으로 탐색합니다.

container.wire(
    packages=[
        "yourapp.package1",
        "yourapp.package2",
    ],
)

modulespackages 인자는 함께 사용될 수 있습니다.

Wiring이 완료되면 함수와 메소드는 호출이 될 때 주입이 제공됩니다.

@inject
def foo(bar: Bar = Provide[Container.bar]):
    ...


container = Container()
container.wire(modules=[__name__])

foo()  # <--- 인자 "bar"가 주입됩니다.

주입은 키워드 인자로 처리됩니다.

foo()  # 아래로 동일합니다
foo(bar=container.bar())

컨텍스트 키워드 인자는 주입보다 우선됩니다.

foo(bar=Bar())  # Bar() is injected

주입된 함수나 메소드를 이전으로 되돌릴려면 container.unwire() 메소드를 호출합니다.

container.unwire()

테스트할 때 각 테스트전 재생성, 재연결을 위하여 사용할 수 있습니다.

import unittest


class SomeTest(unittest.TestCase):

    def setUp(self):
        self.container = Container()
        self.container.wire(modules=["yourapp.module1", "yourapp.module2"])
        self.addCleanup(self.container.unwire)
import pytest


@pytest.fixture
def container():
    container = Container()
    container.wire(modules=["yourapp.module1", "yourapp.module2"])
    yield container
    container.unwire()

Note

많은 코드를 가지고 있으면 Wiring에 대한 소요시간이 오래 걸릴 수 있습니다.
컨테이너 인스턴스를 유지하고 테스트간에 재연결을 피하는걸 고려하세요.

Note

파이썬은 개별적으로 가져온 함수에 대한 패치에 제약이 있습니다.
에러를 방지하기 위해서 개별 함수를 가져오기 위해 모듈을 가져오거나 Wiring 후 가져오기를 하도록 합니다.

# Potential error:

from .module import fn

fn()

대신 다음을 사용하세요.

# Always works:

from . import module

module.fn()

Wiring 구성

컨테이너에 Wiring 구성을 정의할 수 있습니다.
Wiring 구성이 정의되면 컨테이너는 자동으로 인스턴스를 생성할 때 .wire() 메소드를 호출합니다.

class Container(containers.DeclarativeContainer):

    wiring_config = containers.WiringConfiguration(
        modules=[
            "yourapp.module1",
            "yourapp.module2",
        ],
        packages=[
            "yourapp.package1",
            "yourapp.package2",
        ],
    )

    ...


if __name__ == "__main__":
    container = Container()  # container.wire()가 자동으로 호출됩니다.
    ...

또한 상대적 가져오기를 사용할 수 있습니다.
컨테이너는 컨테이너 클래스의 모듈 기준으로 해석하게 됩니다.

# In module "yourapp.container":

class Container(containers.DeclarativeContainer):

    wiring_config = containers.WiringConfiguration(
        modules=[
           ".module1",  # 다음으로 해석 : "yourapp.module1"
           ".module2",  # 다음으로 해석 : "yourapp.module2"
        ],
    )
)


# In module "yourapp.foo.bar.main":

if __name__ == "__main__":
    container = Container()  # wire to "yourapp.module1" and "yourapp.module2"
    ...

Wiring 구성을 사용하고 수동으로 .wire()를 호출하려면, auto_wire=False 플래그를 설정합니다.

class Container(containers.DeclarativeContainer):

    wiring_config = containers.WiringConfiguration(
        modules=["yourapp.module1"],
        auto_wire=False,
    )


if __name__ == "__main__":
    container = Container()  # container.wire() 가 자동으로 호출되지 않습니다.
    container.wire()         # wire to "yourapp.module1"
    ...