본문 바로가기
IT/python

[python] 의존성 주입 (Dependency Injection) 기초와 FastAPI

by 통섭이 2023. 1. 27.

서두

여기서는 의존성주입(Dependency Injection) 의 기초에 대해 간단히 알아보고, Python Dependency Injector + FastAPI 를 통해 간단한 예제를 구현해보자.

기초 지식

  • Dependency Injection 은 객체지향의 5대 원칙 SOLID 중 DIP(Dependency Inversion Principle) 을 만족시키기 위해 사용하는 방식이다. DIP 는 간단히 말하면 추상 클래스가 구체 클래스에 의존하는게 아니라, 구체 클래스가 추상클래스에 의존하도록 하여 프로그램을 확장 가능하도록 개발하기 위한 방법론이다.
  • 설명이 어려우니 예를 들어보자. 우리가 게임 캐릭터를 생성하는데, 캐릭터는 여러 무기를 가질 수 있다고 가정해보자. 이 때, 만약 Character 클래스가 구체적인 무기인 Sword 에 의존을 한다면, 우리의 Character 클래스는 Sword를 들고 있는 캐릭터밖에 생성하지 못한다. 그러면 SwordCharacter 클래스, KnifeCharacter 클래스 ... 이렇게 계속 클래스를 만들어야 하고, 새로운 무기를 장착 하려면 또 코드가 반복되고 효율성이 떨어지게 된다. 하여 Character 클래스가 Weapon 클래스라는 추상 클래스에 의존하도록 하여 확장 가능한 코드가 되도록 하는 것이다.

Dependency Injection(의존성 주입)

  • 의존성 주입을 통해 DIP 를 만족할 수 있는데, 사용하는 객체가 아닌 외부의 독립 객체가 인스턴스를 생성한 뒤 이를 전달해 의존성을 해결한다. (일반적은 의존 형태는 A 클래스에서 B 클래스를 생성하지만, 여기서는 B를 외부에서 생성하고 A에 주입한다)
  • 그렇다면 왜 직접 생성하지 않을까? 이에 대한 대답은 위에서 설명한 Character 클래스가 Weapon에 의존해야 하는것과 동일하다. 만약 A에서 B를 직접 생성한다면, B를 C로 바꾸고 싶은 경우, A 도 수정해야 한다. 하지만 객체를 외부에서 생성해 넘기면 이는 마치 추상클래스에 의존하는 것과 동일한 효과로 A에서는 주입 받을 클래스에 상관없이 클래스를 유지할 수 있게 된다.
  • 또 다른 장점은? 이렇게 하면 개발시에 주입될 객체를 mocking 하여 A 클래스 테스트를 하기 쉬워진다.

Python Dependency Injector 사용법

  • 여기서는 Dependency Injector를 사용해 본다. 자세한 내용은 공식문서 를 참조하기 바란다.
  • 여기서는 주요 기능 중, providers, containers, wiring 을 다루고자 한다.
    • Providers(프로바이더) : Factory, Singleton, Callable, Coroutine, Object, List, Dict, Configuration, Resource, Dependency, Selector 를 통해 객체를 모아주는 역할을 한다. 여기서는 Configuration, Factory, Singleton 을 다루고자 한다.
      • Configuration 프로바이더 : yaml, ini, json, pydantic setting, env variables, dict 등 다양하게 가져올 수 있다
      • from dependency_injector import providers if __name__ == "__main__": container_config = providers.Configuration() container_config.from_dict( { "aws": { "access_key_id": "KEY", "secret_access_key": "SECRET", }, }, ) assert container_config.aws.access_key_id() == "KEY"
      • Factory 프로바이더 : 객체를 생성하는 프로바이더
      • from dependency_injector import containers, providers class User: ... class Container(containers.DeclarativeContainer): user_factory = providers.Factory(User) if __name__ == "__main__": container = Container() user1 = container.user_factory() user2 = container.user_factory()
      • Singleton 프로바이더 : 여러 곳에서 하나의 객체를 가지고 계속 사용 할 때는 Singleton 을 쓴다
      • from dependency_injector import containers, providers class UserService: ... class Container(containers.DeclarativeContainer): user_service_provider = providers.Singleton(UserService) if __name__ == "__main__": container = Container() user_service1 = container.user_service_provider() user_service2 = container.user_service_provider() assert user_service1 is user_service2
      • 더 있지만.. 일단 여기까지 나열하겠다... 공식 문서를 통해 나머지도 사용해보기 바란다.
    • Containers : 선언적인 컨테이너와 다이나믹한 컨테이너가 있다. 선언적 컨테이너는 선언적인 방법으로 컨테이너 내에 주입할 객체들을 묶는 방법이고, 다이나믹 컨테이너는 동적으로 주입할 객체들을 만드는 방법이다. (다이나믹 컨테이너는 안써봤습니다...;;)
    • from dependency_injector import containers, providers class Container(containers.DeclarativeContainer): factory1 = providers.Factory(object) factory2 = providers.Factory(object) if __name__ == "__main__": container = Container() object1 = container.factory1() object2 = container.factory2() print(container.providers) # { # "factory1": <dependency_injector.providers.Factory(...), # "factory2": <dependency_injector.providers.Factory(...), # }
    • Wiring : 위에서 만든 의존성을 함수, 클래스에 의존성 주입시 사용. 이때, 컨테이너와 그 안의 프로바이더로 만든 객체를 주입하려면, 주입할 모듈을 선택해줘야 한다. 주입할 함수에는 @inject 데코레이터를 붙여 사용한다..
    • # main.py import sys from containers import Container, User from dependency_injector.wiring import Provide, inject @inject def main(user: User = Provide[Container.user]): print(f"He is {user.name}") if __name__ == '__main__': container = Container() container.wire(modules=[sys.modules[__name__]]) main() # He is Humphrey
    • # containers.py from dependency_injector import containers, providers class User: def __init__(self, name: str) -> None: self.name = name class Container(containers.DeclarativeContainer): user = providers.Factory(User, name="humphrey")
  • 자 여기까지 간단한 사용법을 알아보았다. 더 있지만.... 이정도면 충분하다.... 이제 실전이다!

DI 를 사용한 FastAPI 의 실제 예제

  • 이제 FastAPI를 통해서 DI 를 주입해보자.
  • 아래의 예시에서는 main 에 endpoints 가 있어서 바로 sys.modules[name] 을 썼지만, 보통은 endpoints 는 다른 모듈에 있기 때문에 그곳을 잘 기입해주도록 하자.
# container.py
# 위에와 동일


# main.py 
import sys  
import uvicorn  
from containers import Container, User  
from dependency_injector.wiring import Provide, inject  
from fastapi import Depends, FastAPI  


app = FastAPI()  

@app.get("/")  
@inject  
async def test_class(di_test_user: User = Depends(Provide[Container.user])):  
    return di_test_user.name  

if __name__ == '__main__':  
    container = Container()  
    container.wire(modules=[sys.modules[__name__]])  
    uvicorn.run(app,host="0.0.0.0",port=8000)
  • 이렇게 하면 localhost:8000 에 들어가면 humphrey 가 잘 나온다!!

댓글