[FastAPI] 11. Dependency Injector를 이용한 의존성 관리

반응형

서버 애플리케이션을 개발하다보면 규모가 커지게 되어 이를 효율적으로 관리할 필요가 생기게 됩니다. 이에 적용하는 것으로 대표적인 아키텍처인 Layered Architecture가 있습니다.

 

Layered Architecture는 서버 애플리케이션을 운용하기 위해 사용하는 현대 대표적인 방법으로, Application, Domain, Infrastructure의 3개로 나눌 수 있는데, 이들을 전부 객체 지향 프로그래밍으로 구현하게 되면 각각의 의존성이 늘어나게 되고, 그 로직이 커지면 이 역시 관리가 힘들어집니다.

 

Python에서는 이러한 의존성 관리를 유연하게 하기 위해 다양한 DI 프레임워크가 존재하는데, 그 중에서도 Dependency Injector를 사용 해보고자 합니다.

 

 

 

 

Dependency Injector

Python에서 Django 혹은 DRF(Django REST Framework)를 사용해보신 분들이라면 그들에 내장된 DI 프레임워크를 보셨을 것입니다. Django의 경우는 Dictionary를 이용하여 주입하고, DRF의 경우는 class를 이용하여 주입하게 되는데요. 문제는 프레임워크에 강하게 종속적이기 때문에 다른 프레임워크에서는 사용하기 어렵다는 단점이 있습니다.

 

Dependency Injector는 어느 한 프레임워크에 종속적이지 않고, 일반 Python에서도 유용히 사용할 수 있는 Python의 DI 프레임워크이며 Dependency Injector의 주 기능은 다음과 같습니다.

 

https://python-dependency-injector.ets-labs.org/introduction/di_in_python.html

 

Dependency injection and inversion of control in Python — Dependency Injector 4.36.0 documentation

Dependency injection and inversion of control in Python Originally dependency injection pattern got popular in the languages with a static typing, like Java. Dependency injection is a principle that helps to achieve an inversion of control. Dependency inje

python-dependency-injector.ets-labs.org

 

  • 유연성 (Flexibility)

    각 컴포넌트가 느슨한 결합으로 되어 있어 기능의 변경과 확장이 쉬움.

  • 테스트 가능성 (Testability)

    컴포넌트의 실제 객체를 사용하지 않고 Mocking하여 주입해 테스트에 용이

  • 명확성과 유지보수 (Clearness and maintainability)

    명시적 의존성 주입을 이용하여 애플리케이션 구조를 쉽게 파악해 유지보수하기 용이

각 특징에 대해 좀 더 부연설명하자면, 비즈니스 로직이 구현된 클래스에서 Database에 있는 데이터를 사용하기 위해 Database 객체를 사용하게 됩니다. 이 때 객체를 직접 생성해서 사용하면 해당 객체에 강하게 의존하기 때문에 이 객체에 맞는 인터페이스 혹은 프로토콜을 구현해놓고, 객체를 인스턴스화하여 사용하면 컴포넌트가 느슨하게 결합되어 변경에 용이해집니다.

 

Python에서는 Unittest라는 테스트 라이브러리가 존재하고, 해당 라이브러리는 Mocking(모조품화) 기능을 제공합니다. 여기서 Mocking이란, 우리가 애플리케이션 실행시 사용하는 인스턴스의 실체가 아닌 모조품을 제공하며 우리가 여기서 구현해야 할 기능을 그대로 제공해주지 않고, 그 겉모습만을 제공하여 테스트할 수 있어 Flow 테스트에 유리합니다.

 

마지막으로 명시적 의존성 주입(explicit dependency)을 이야기했는데, 잠깐 설명을 드리자면, 명시적 의존성 주입은 의존 대상을 생성자의 인자로 전달하여 주입하는 방식을 말합니다. 반대말로 암묵적 의존성(hidden dependency)이 있는데, 암묵적 의존성은 생성자 내 또다른 인스턴스를 생성해 어떤 것에 의존하는지를 감추는 주입 방식입니다.

 

후자는 또 다시 그 의존성의 코드가 어떻게 구현되었는지를 파악해야하기 때문에 애플리켸이션이 복잡해짐에 따라 그 애플리케이션 구조를 파악하기 어렵고 유지보수가 그만큼 어려워지므로 명시적 의존성 주입을 통해 유지보수하기 쉽도록 코드를 구현해야 합니다.

 

 

 

 

with FastAPI

Dependency Injector는 FastAPI 프레임워크에서 의존성 관리를 하기 아주 좋은 라이브러리입니다. 먼저 FastAPI의 기본 애플리케아션 구조를 아래와 같이 작성한다고 가정하겠습니다.

 

src에는 애플리케이션의 소스, tests에는 이에 대한 테스트 코드를 구현하는 곳입니다. Dependency Injector는 테스트 코드 작성에 용이하다는 특징을 가지고 있으니 테스트 코드도 같이 작성해보도록 하겠습니다.

 

엔드포인트(API), 비즈니스 로직(Service), 인프라스트럭쳐(Repository 이하 DB) 3 Layer의 아키텍처를 사용하는 경우의 Dependency Injector를 사용하는 것에 대해 알아볼 것입니다.

 

먼저 containers.py 파일을 만들어 의존성을 관리할 IoC를 만들어줘야 합니다.

 

 

 

 

Declarative Container

Declarative Container는 Dependency Injector에서 의존성을 관리하는 기본 컨테이너입니다. 이 컨테이너에서는 의존성 관리 뿐 아니라 애플리케이션의 설정도 관리할 수 있습니다.

 

DelclarativeContainer를 상속하여 컨테이너를 구현할 수 있으며 여기에 애플리케이션의 설정을 추가하고자 하는 경우 Provider 패키지에 있는 Configuration 클래스를 이용할 수 있습니다.

 

Provider에서 제공하는 Configuration에서는 아래의 5가지 방법으로 환경을 구성할 수 있습니다.

 

  • ini 파일
  • Python의 Dict 자료형
  • yaml 파일
  • Pydantic의 Settings 클래스
  • OS Environment

 

Python으로 개발해보신 분들이라면 Pydantic을 잘 알겠지만 Pydantic은 Python에서 Type annotation을 사용해 데이터 유효성 검사와 설정을 관리해주는 라이브러리입니다. 실제로 FastAPI에서는 요청과 응답 클래스의 데이터 유효성 검사를 위해 해당 라이브러리를 사용하며 이 외에도 데이터 유효성 검사를 위한 클래스가 필요하다면 Python에서는 이를 많이 사용합니다.

 

위 코드는 Pydantic의 Settings 클래스를 사용해 애플리케이션을 설정한 코드입니다. Database 뿐만 아니라 CORS, AUTH-KEY 등의 설정도 클래스화하여 관리하기 쉽고 데이터 유효성 검사를 지원하기 때문에 dotenv와 OS environment를 같이 써서 설정을 관리할 수도 있습니다. 

 

이 외에도 다양한 컨테이너들이 있지만 Dependency Injector에서 가장 기본적이고 쉽게 사용할 수 있는 컨테이너이기 때문에 FastAPI를 사용하는 데 있어서는 선언적 컨테이너까지만 아시면 될 것 같네요.

 

자 그럼 이제 여기서 의존성 주입은 어떻게 할 수 있을까요?

 

 

 

 

Dependency Injector가 제공하는 의존성 생성 매커니즘 

Dependency Injector에서는 의존성을 주입할 때 사용하는 매커니즘을 Provider로 명시합니다. 이는 의존성을 생성하는 역할을 하며 그 종류는 10가지가 넘지만 우리는 여기서 3가지 정도를 다룰 것입니다. 먼저 1가지는 위에서 다룬 Configuration Provider였습니다.

 

두 번째는 Singleton Provider입니다. Singleton Provider는 여러분들이 알고 계시는 싱글턴(Singleton) 패턴을 말하며 객체를 싱글턴으로 생성하여 어느 컴포넌트에서 사용하든 같은 의존성을 사용하도록 하겠다는 것입니다.

 

providers의 Singleton을 이용하여 객체를 생성할 때는 Singleton 인자에 객체를 넣어주면 됩니다. 만약 생성자 인자가 있는 경우, kwargs를 사용하여 넣어줄 수 있습니다.

 

이렇게 생성된 객체는 싱글턴으로 동작하여 어떤 컴포넌트, 어떤 주기에 호출해도 같은 객체를 호출하게 됩니다.만약, 해당 객체를 다시 생성하기 원하는 경우, reset 메소드를 이용해서 객체를 초기화 할 수 있습니다.

 

세 번째는 Factory Provider입니다. Factory Provider는 Singleton과는 반대로 매번 객체를 생성하는 매커니즘입니다. 팩토리는 싱글턴처럼 생성자의 인자를 kwargs를 이용하는 방법과 더불어 Factory Provider Chaining 즉, 연결 고리를 이용해서도 의존성을 주입할 수 있습니다.

 

간단하게 예시를 들어보도록 하죠.

 

MemoService 생성자에는 Memo 데이터 모델이 필요하며 이를 Factory로 생성하는 경우, argument 이름을 정의하고 그 모델 인스턴스를 넣어주면 됩니다.

 

만약 생성자에 들어가는 의존성 또한 Factory로 생성하고자 하는 경우, 이들을 Factory로 불러서 사용할 수도 있습니다. 이런식으로 계속 이어젹서 사용하면 Chaining 코드가 됩니다.

 

자 의존성 생성이 끝났습니다. 이제 FastAPI에서 이를 어떻게 사용할 수 있을까요?

 

 

 

 

Wiring

Dependency Injector의 Provider를 이용하여 의존성 주입을 마친 경우, 우리는 이를 사용할 컴포넌트 대상을 wire해줘야 합니다. 여기서 wire 란, 연결을 뜻하며 컨테이너에 있는 의존성을 함수 혹은 메소드에 주입하는 역할을 합니다.

 

주입하려는 대상의 함수 혹은 메소드가 같은 모듈에서 Wiring 하는 경우에는 Dependency Injector에서 제공하는 inject 데코레이터를 사용하여 의존성을 주입할 수 있습니다.

 

원하는 API를 만들고, 해당 함수 위에 Dependency Injector의 inject 데코레이터를 넣어줍니다. 그리고 주입한 의존성 사용을 위해 Provide를 통해 주입한 의존성을 어떤 변수에 배치할지를 정해줍니다. 이 때 FastAPI의 Depends를 넣어줍니다.

 

그리고, 애플리케이션 실행 전에 반드시 Container를 연결하려는 모듈을 정해줘야합니다. 그렇지 않으면 해당 모듈에는 IoC가 배정되지 않아 import로 컨테이너 내 의존성을 가져오는 코드를 작성하더라도 불러오지를 못합니다.

 

모든 코드를 작성하고 디버깅 해보면, MemoService 객체가 주입되었음을 보여줍니다.

 

 

 

 

Test with FastAPI

이 파트에서는 pytest, unittests와 같은 Python의 테스트 환경에 대해서 자세히 설명하지는 않을 것입니다. 이 파트를 이해하기 위해서는 Python의 테스트 라이브러리의 선지식이 필요합니다.

 

FastAPI에서 잘 동작하는지를 테스트 해볼 수 있을까? Dependency Injector 문서는 FastAPI 프레임워크에 대한 자세한 사용법을 제공해줍니다. 기본적으로 FastAPI 프레임워크에서 테스트할 때는 테스트 로드맵을 잘 짜는 것이 중요한데, 개인적으로 FastAPI에서 Dependency를 테스트할 때 unittest의 AsyncMock을 사용합니다.

 

Dependency Injector가 가지는 강점은 바로 테스트 가능성입니다. 우리가 실제 의존성을 구현하지 않고도 이러한 의존성을 주입할 아키텍처가 미리 구상되어 있다면 이를 Mocking하여 테스트 해 본 다음 실제 애플리케이션이 이와 같이 동작하는지를 볼 수 있습니다.

 

위 예제는 FastAPI를 비동기 처리로 사용했을 때 테스트 코드의 예시이며 실제로 동작하기 위해서는 추가적인 코드를 필요로 합니다. test_client는 Python의 비동기 클라이언트 라이브러리인 httpx를 사용한 것이며 asgi에는 실제로 구현한 애플리케이션의 객체가 담겨져 있습니다. 

 

또한 실제 애플리케이션의 컨테이너에서 Mocking을 해야하기 때문에 FastAPI 객체의 container attribute를 추가하여 IoC를 추가해놓고, 테스트 환경에서 애플리케이션이 동작할 경우의 IoC를 만들어, 의존성을 Mocking 해줘야만 원활한 테스트 환경을 만들어낼 수 있습니다.

 

마지막으로 Python에서 객체를 Mocking할 때는 비동기 객체를 모킹하는 AsyncMock과 동기 객체를 모킹하는 Mock이 있습니다. Infrastructure 레이어에서 비동기 연결을 사용하는 경우 AsyncMock을 사용해야하지만 그렇지 않은 경우는 Mock을 사용해야 하며 여러분들이 짜시는 애플리케이션의 구현에 따라 이를 유동적으로 사용해야 합니다.

 

 

 

 

마치며..

여기까지 Dependency Injector를 이용하여 FastAPI를 예시로 의존성을 관리하는 방법에 대해 알아봤습니다. Django나 DRF(Django Rest Framework)의 경우에는 자체적으로 의존성 관리 환경을 제공하지만 Flask나 Falcon, FastAPI와 같은 마이크로 프레임워크에서는 외부 라이브러리를 사용해야 합니다.

 

우리는 여기서 장단점을 나눠 볼 수 있는데, Django에서 사용했던 의존성 관리 매커니즘을 다른 프레임워크에서 사용하기는 어렵지만 Dependency Injector와 같은 범용적인 라이브러리의 의존성 매커니즘을 사용한다면 다른 프레임워크의 이전이 쉽다는 것을 볼 수 있습니다. 더 느슨하고, 더 많은 가능성을 염두하고자 한다면 이 선택은 나쁘지 않다고 봅니다.

 

만약 FastAPI를 현업에서 사용하고 있고, Dependency Injector와 같은 의존성 라이브러리를 고려하고 있다면 아래의 링크를 통해 풍부한 예제를 경험해보고 사용해보시는 걸 추천합니다.

 

https://python-dependency-injector.ets-labs.org/examples/fastapi.html

 

FastAPI example — Dependency Injector 4.36.0 documentation

FastAPI example This example shows how to use Dependency Injector with FastAPI. The example application is a REST API that searches for funny GIFs on the Giphy. The source code is available on the Github. Application structure Application has next structur

python-dependency-injector.ets-labs.org

 

 

반응형