서두
여기서는 의존성주입(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")
- Providers(프로바이더) :
- 자 여기까지 간단한 사용법을 알아보았다. 더 있지만.... 이정도면 충분하다.... 이제 실전이다!
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 가 잘 나온다!!
'IT > python' 카테고리의 다른 글
[python] 패키지관리 (0) | 2023.08.31 |
---|---|
[python - kubernetes] pytorchjob을 활용한 분산 데이터 병렬 처리 기초 (0) | 2023.02.03 |
[python] asyncio 비동기 프로그래밍의 기초 (0) | 2023.01.30 |
[python] pyenv 가상환경 설정 실습(feat. pyenv-virtualenv) (0) | 2023.01.02 |
댓글