Dependency injection is a technique where
an object receives other objects that it depends on.
client
: the receiving object
service
: the passed-in(= injected) object
injector
: the code passes the service to the client
1) Instead of the client specifying which service it will use,
the injector tells the client what service to use.
2) Passing the service to the client,
rather than allowing the client to build or find the service,
is the fundamental requirement of the pattern.
데이터베이스의 의존성에 대해서 살펴보자.
UoW에 대한 명시적 의존성을 갖는 핸들러
def allocate(
cmd: commands.Allocate,
uow: unit_of_work.AbstractUnitOfWork
):
따라서, 테스트 코드 작성은 쉬워졌다.
아래 injector 과정을 보자.
# 테스트 환경에서 가짜 UoW로 대체 가능
uow = FakeUnitOfWork()
messagebus.handle([...], uow)
클라이언트는 서비스를 생성하거나 어떻게 구현되어 있는지
알 필요가 없게 된 것이다!
꼬리에 꼬리를 무는 질문이 있다.
위에서 allocate
이란 클라이언트는 uow
서비스를 의존하고 있다.
allocate
->uow
->?
그럼 UoW가 의존하는 object는 무엇일까
class SqlAlchemyUnitOfWork(AbstractUnitOfWork):
def __init__(self, session_factory=DEFAULT_SESSION_FACTORY):
self.session_factory = session_factory
MySQL을 DB엔진으로 사용하고 있다면 아래와 같을 것이다.
class MySqlUnitOfWork(AbstractUnitOfWork):
def __init__(self, session_factory=DEFAULT_SESSION_FACTORY):
self.session_factory = session_factory
allocate
->uow
->session_factory
테스트 코드도 변경도 매우 쉽다.
def test_rolls_back_uncommited_work_by_default(mysql_session_factory):
uow = unit_of_work.MySqlUnitOfWork(mysql_session_factory)
요약: 핸들러는 uow를 의존하고, uow는 세션 팩토리를 의존한다.
DI를 통해 세션 팩토리 변경이나 uow의 변경이 핸들러에 영향을 미치지 않는다.
우리는 이 작업을 왜 하고 있을까?
부트스트랩 패턴을 도입하기 위해서는 초기화 단계에서 각 상황별 DI가 다르게 적용돼야 하기 때문이고, 그 변경이 의존하고 있던 하위 모듈까지 전파되는 걸 막기 위해서다.
파이썬에서 의존성을 처리하는 표준 방법은 임포트를 통해 모든 의존성을 암시적으로 선언한다.
from unittest.mock import patch
class ManagerDepositTest(TestCase):
"""
매니저 입금 기능 테스트
"""
def setUp(self):
self.client = APIClient()
def tearDown(self):
"""테스트 객체 삭제"""
Manager.objects.all().delete()
@patch("api.views_v2.validate_request", return_value=True)
@patch("api.views_v2.send_slackmessage", return_value=None)
def test_http_get_just_return_200_ok(self, mock_validate_request, mock_send_slackmessage):
"""Get 요청은 staus_code 200만 리턴"""
res = self.client.get(MANAGER_DEPOSIT_URL)
error_msg = res.json()["result"]
# 검증
self.assertEqual(res.status_code, 200)
self.assertTrue("Method Not Allowed" in error_msg)
(이하 생략)
patch
를 통해 실제 코드의 오염을 막을 수 있긴 하다.
문제는 모든 테스트 케이스마다 patch를 사용해야 한다는 것이다.
1)투스콥스오브 장고 책에서는 테스트 코드에서의 반복은 괜찮다고 한다.
2) 멍키 패치란
A monkey patch is a way for a program to extend or modify supporting system software locally (affecting only the running instance of the program).
즉, 프로그램의 런타임 동안 국소적으로 시스템의 작동 방식을 변경하는 것이다.
3) 왜 멍키 패치라 부를까
정의에 걸맞게 원래는 게릴라 패치라고 불렀다. 하지만 사람들은 이내 단어가 비슷한 '고릴라'를 떠올렸고, 좀 더 친숙한 원숭이라고 바꿔 부르게 됐다.
추상적인 명시적 의존성이 왜 더 좋을까
def send_out_of_stock_notification(
event: events.OutOfStock,
send_mail: Callable
):
send_mail(
"stock@made.com",
f"Out of stock for {event.sku}"
)
위의 코드가 왜 구체적인 암시적 의존성이 아니라
추상화된 명시적 의존성일까?
뭔말인지도 모를 정도로 어려운 말이다.
여기 의존성이 있는 함수가 있다.
나중에 호출될 수 있는 의존성이 이미 주입된 함수로 대체해보는 건 어떨까?
# uow 의존성 주입된 함수
def allocate(
cmd: commands.Allocate,
uow: unit_of_work.AbstractUnitOfWork
):
line = OrderLine(cmd.orderid, cmd.sku, cmd.qty)
with uow:
...
# 나중에 호출되는 의존성 함수
def bootstrap(..):
uow = unit_of_work.SqlAlchemyUnitOfWork()
# 방법1
allocate_compose = lambda cmd: allocate(cmd, uow)
# 방법2
def allocate_composed(cmd):
return allocate(cmd, uow)
# partial 사용법
import functools
allocate_composed = functools.partial(allocate, uow=uow)
allocate_composed(cmd)
다른 예
def send_out_of_stock_notification(
event: events.OutOfStock,
send_mail: Callable
):
send_mail(
'stock@made.com',
...
# 의존성을 지정한 send_out_of_stock_notification 버전 준비
sosn_composed = lambda event: send_out_of_stock_notification(
event,
email.send_mail
)
...
# 나중에 실행 시점에 다음 코드 실행
# 이미 주입된 email.send_mail 사용
sosn_composed(event)
# `def allocate(cmd, uow)`였던 핸들러를 다음과 같이 바꾼다.
class AllocateHandler:
def __init__(self, uow: unit_of_work.AbstractUnitOfWork):
self.uow = uow
def __call__(self, cmd: commands.Allocate):
line = OrderLine(cmd.orderid, cmd.sku, cmd.qty)
with self.uow:
# 이전과 같은 나머지 핸들러 메서드
...
# 실제 UoW를 준비하는 부트스트랩 스크립트
uow = unit_of_work.SqlAlchemyUnitOfWork()
# 이미 의존성이 주입된 allocate 함수 버전을 준비
allocate = AllocateHandler(uow)
...
# 나중에 실행 시점에 다음 코드를 실행하면 이미 주입된 UoW가 사용됨
allocate(cmd)
부트스트랩 스크립트는 다음과 같은 일을 한다.
⓵ 디폴트 의존성을 선언하지만 원하는 경우 이를 오버라이드 할 수 있어야 한다.
⓶ 앱을 시작하는 데 필요한 '초기화'를 수행한다.
⓷ 모든 의존성을 핸들러에 주입한다.
⓸ 앱의 핵심 객체인 메시지 버스를 반환한다.
# 부트스트랩 함수
def bootstrap(
start_orm: bool = True,
uow: unit_of_work.AbstractUnitOfWork = unit_of_work.SqlAlchemyUnitOfWork(),
send_mail: Callable = email.send,
publish: Callable = redis_eventpublisher.publish,
) -> messagebus.MessageBus:
if start_orm:
orm.start_mappers()
dependencies = {
'uow': uow,
'send_mail': send_mail,
'publish': publish
}
# 의존성 주입된 이벤트 핸들러
injected_event_handlers = {
event_type: [
inject_dependencies(handler, dependencies)
for handler in event_handlers
]
for event_type, event_handlers in handler.EVENT_HANDLERS.itesm()
}
# 의존성 주입된 커맨드 핸들러
injected_command_handlers = {
command_type: inject_dependencies(handler, dependenciees)
for command_type, handler in handlers.COMMAND_HANDLERS.items()
}
# 의존성 주입된 메시지 버스 리턴
return messagebus.MessageBus(
uow=uow,
event_handlers=injected_event_handlers,
command_handlers=injected_command_handlers,
)
이 책에서는 테스트 환경
과 DB엔진
을 변경하는 관점에서
DI 및 부트스트랩 스크립트(script)를 설명했다.
장고로 치면 settings.py라고 이해하면 되나라는 의문이 든다.
책의 표현을 그대로 빌리면
"의존성 주입을 설정하는 것은 앱을 시작할 때 한 번만 수행하면 되는 전형적인 설정/초기화 활동의 일부다."
# 테스트 환경
python manage.py test --settings=bootstrap.unit_test_settings
# 로컬 서버 환경
python manage.py test --settings=bootstrap.local_settings
# 테스트 서버 배포 환경
python manage.py test --settings=bootstrap.test_server_settings
# 스테이징 서버 배포 환경
python manage.py test --settings=bootstrap.staging_server_settings
# production 서버 배포 환경
python manage.py test --settings=bootstrap.production_server_settings