이 내용은 “파이썬으로 살펴보는 아키텍처 패턴” 을 읽고 작성한 내용입니다. 블로그 게시글과, 작성한 코드를 함께 보시면 더욱 좋습니다.
4장은 해당 코드를 살펴봐주세요. 코드 링크
이런 구조를 만들 것이다
이 장에서는 오케스트레이션 로직, 비즈니스 로직, 연결 코드 사이의 차이를 이해한다. 워크플로우 조정 및 시스템의 유스케이스를 정의하는 서비스 계층 패턴을 알아본다.
테스트도 살펴본다. 서비스 계층과 데이터베이스에 대한 저장소 추상화를 조합할 것이다. 이를 통해 도메인 모델 뿐 아니라 유스케이스의 전체 워크플로우를 테스트할 것이다.
테스트할 때 보면 프로덕션 코드는 SqlAlchemyRepository
를 쓰고, 테스트할 때는 FakeRepository
를 쓰게 한다.
이게 가장 빨라야한다! MVP니까…
도메인에 필요한걸 만들고 주문할당(allocate)을 하는 도메인 서비스도 만들었고, 리포지토리 인터페이스도 만들었다…
그러면 다음 할 일은 아래와 같다:
allocate
도메인 서비스 앞에 API 엔드포인트를 둔다. DB 세션과 저장소를 연결한다. 이렇게 만든 시스템은 e2e 테스트와 빠르게 만든 SQL 문으로 테스트한다.FakeRepository
를 써서 코드를 테스트한다.실제 API 엔드포인트(HTTP)와 실제 DB를 사용하는 테스트를 한, 두개정도 짜고 리팩토링 또 한다.
일단 처음엔 어쨌거나 만든다. 랜덤 문자열을 생성하고, DB에 row를 넣는 함수를 실제로 짠다.
책에선 플라스크를, 나는 FastAPI를 통해서 짰다. 그런데 이 테스트의 한계는 DB커밋을 해야한다는 점이다.
이런 케이스는 DB 측의 데이터 무결성 검사다. 도메인 서비스 호출 전에 캐치해야한다.
근데 이 방어로직을 API에 넣으면 E2E 테스트 갯수가 점점 많아지게되고 역피라미드형 테스트가 된다. 테스트코드도 꼬인다…
따라서, API에 있던 일부 로직을 유스케이스로 빼고, 이를 테스트하기 위해 FakeRepository
를 쓸 때가 왔다.
FakeRepository
사용API는 가만보면 오케스트레이션이다. 저장소에서 뭐 갖고와서 DB 상태에 맞게 검증도 하고 오류 처리도 하고, 성공적이면 DB에 값도 커밋한다. 근데 이런 작업은 API하고는 관련이 없다.
FakeRepository
를 이용해서 진짜 손쉽게 AAA 테스트코드를 구현했다FakeSession
을 이용해서 세션도 가짜로 만든다. 6장서 리팩토링할거다→ 당연하겠지만 커밋도 테스트 대상이다
이런 구성을 가져간다.
deallocate
을 만든다면?UoW 하고나서 다시 할거다…
여기서 서비스라 부르는건 두 가지가 있다:
TaxCalculator
라는 클래스나 calculate_tax
같은 함수들로 처리하면 될 것이다yield
전후로 setup, teardown으로 두자
이러면 의존성 역전이 되는건지 살펴보자
This helps in testing. This also helps in overriding API clients with stubs for the development or staging environment.
Provider overriding , Dependency Injector
되는듯!
일단 너무 많은기능을 한번에 할려는 것 같으니 구획을 좀 나눠보자
AsyncClient
로 앱 구동함 → DB처리를 여기서도 함<pt1.ch04.adapters.postgres.AsyncSQLAlchemy object at 0x000002A79A5C7670>
<pt1.ch04.adapters.postgres.AsyncSQLAlchemy object at 0x000002A79AA650C0>
<pt1.ch04.adapters.postgres.AsyncSQLAlchemy object at 0x000002A79A5C7670>
여기서는 책에서 제시하는 구조를 일단 따른다. 주관은 지식이 생긴 후에 갖추는 것이 맞다고 생각한다.
domain
→ 도메인 모델
service_layer
→ 서비스 계층
adapters
→ ‘포트와 어댑터’ 용어에 사용된 어댑터
entrypoints
→ 애플리케이션 제어 시점, ‘포트와 어댑터의’ 어댑터
포트는? 어댑터가 구현하는 추상 인터페이스이다. 포트를 구현하는 어댑터와 같은 파일 안에 포트를 넣는다.
서비스 계층이 어떻게 의존하는지 다시 살펴보자.
서비스 계층은 도메인 모델, AbstractRepository
를 받는다.
async def allocate(
line: model.OrderLine,
repo: repository.AbstractRepository,
session,
) -> str:
""" batches를 line에 할당한다.
FYI,
의존성 역전 원칙이 여기 들어감에 유의!
고수준 모듈인 서비스 계층은 저장소라는 추상화에 의존한다.
구현의 세부내용은 어떤 영속 저장소를 선택했느냐에 따라 다르지만
같은 추상화에 의존한다.
:param line:
:param repo:
:param session:
:return:
"""
batches = await repo.list()
if not is_valid_sku(line.sku, batches):
raise InvalidSku(f'Invalid sku {line.sku}')
batchref = model.allocate(line, batches)
await session.commit()
return batchref
그죠?
프로덕션 상에서는 SqlAlchemyRepository
를 플라스크가 “제공” 하면 DIP가 이루어진다.
여기까지의 트레이드오프를 살펴보자
장점 | 단점 |
---|---|
애플리케이션의 모든 유스케이스를 넣을 유일한 위치가 생긴다 | 앱이 순수한 웹앱일 경우, 컨트롤러/뷰 함수는 모든 유스케이스를 넣을 유일한 위치가 된다 |
정교한 도메인로직을 API뒤로 숨긴다. 리팩토링이 쉬워진다 | 서비스 계층도 또다른 추상화 계층이다 |
‘HTTP와 말하는 기능’을 ‘할당을 말하는 기능’으로부터 말끔하게 분리했다 | 서비스 계층이 너무 커지면 anemic domain 이 된다. 컨트롤러에서 오케스트레이션 로직이 생길 때 서비스 계층을 만드는게 낫다 |
저장소 패턴 및 FakeRepository 와 조합하면 도메인 계층보다 더 높은 수준에서 테스트를 쓸 수 있다. 통합테스트 없이 개별 테스트가 가능해진다. | |
(5장에서 더 자세히 보자) | 풍부한 도메인 모델로 얻을 수 있는 이익 대부분은 단순히 컨트롤러에서 로직을 뽑아내 모델 계층으로 보내는 것 만으로 얻을 수 있다. 컨트롤러와 모델 계층 사이에 또다른 계층을 추가할 필요가 없다. |
대부분의 경우 얇은 컨트롤러와 두꺼운 모델로 충분하기 때문이다. |
개선해야 할 점도 있다
OrderLine
객체를 사용해 표현되므로, 서비스 계층이 여전히 도메인과 연관되어있다. 이 고리를 끊자.잘 되겠지… 하는 코드를 점차 없애자. 저기서 문제가 나면 어떻게 할 거야…
SQLAlchemy 2.0 쿼리 방안을 좀 익혀두자
SQLAlchemy 2.0 - Major Migration Guide — SQLAlchemy 2.0 Documentation
SQLite는 딱히 TRUNCATE TABLE이 없다. 그래서 DELETE FROM
으로 다 날리면 된다
한편 postgres에서 DB 테스트하고 날릴거면 nextval
시퀀스 초기화라거나 그런 부분들도 생각해야한다.