-
[파이썬으로 살펴보는 아키텍처 패턴] Part1. 도메인 모델링을 지원하는 아키텍처 구축카테고리 없음 2023. 9. 24. 13:16
1장. 도메인 모델링
1장에서는 도메인 모델 패턴으로 비즈니스 계층을 만드는 방법을 보여준다.
자신의 비즈니스 로직이 여러 곳에 퍼지면 안된다는 사실을 알고 있지만, 이를 고치는 방법에 대해서는 전혀 모른다.
대부분 개발자가 새로운 시스템을 설계하라는 요청을 받으면 즉시 데이터베이스 스키마를 그리기 시작하고 그다음 객체 모델을 생각한다. 여기서부터 모든 것이 잘못되기 시작한다. 대신 먼저 행동하고 저장에 대한 요구 사항은 행동에 맞춰 정해져야 한다.
4가지 핵심 설계 패턴
- 저장소 패턴
- 서비스 계층 패턴
- 작업 단위 패턴
- 애그리게이트 패턴
값객체는 내부에 있는 데이터에 의해 결정되며 오랫동안 유지되는 정체성이 존재하지 않는다.
엔티티는 오랫동안 정체성이 존재하는 도메인 객체이다. 이는 참조 번호에 의해 구분된다. (영속적인 정체성!) 엔티티의 값을 바꿔도, 바뀐 엔티티는 이전과 같은 엔티티로 인식된다.
에번스는 엔티티나 값 객체로 자연스럽게 표현할 수 없는 도메인 서비스 연산이라는 개념에 대해 이야기 했다.
41 페이지의 새로운 혁신 내용을 재미있게 읽었다.
기존 시스템에서, 모든 배송과 도착 날짜를 추적하여 창고로 배송 중인 상품을 실제 재고로 간주에 창고에 존재하는 제품처럼 취급할 수 있다는 이야기.
이는 재고가 없다고 표시되는 상품이 감소하여 더 많은 상품을 팔 수 있게 된다. 즉, 이 비즈니스는 돈을 절약하게 된다.
이런 시스템을 구현하려면 단순한 모델링에 그치지 않고, 비즈니스를 잘 표현한 도메인 모델링이 필요하다.
도메인 모델을 이해하기 위해서는 시간과 인내, 수많은 포스트잇 메모가 필요하다. 비즈니스 전문가들과의 대화를 통해 최초로 만들 최소한의 도메인 모델에 사용할 용어와 규칙을 몇가지 정해야 한다!
도메인 모델은 비즈니스를 수행하는 사람이 자신의 비즈니스에 대해 가지고 있는 지도와 같다.
데이터 클래스
데이터는 있지만 유일한 식별자가 없는 비즈니스 개념이 있다. 이를 표현하기 위해 값 객체 패턴을 선택하는 경우가 종종 있다. 이는 안에 있는 데이터에 따라 유일하게 식별될 수 있는 도메인 객체를 의미한다.
데이터 클래스는 값동등성을 부여할 수 있다. 이러한 값객체는 값들이 실제로 어떤 역할을 하는지에 대해 실세계에서 갖는 직관과 부합하다. 예를 들어, 10파운드를 말할 때 10파운드라는 값이 중요하지, 어떤 지폐인지는 중요하지 않다.
엔티티
값객체는 내부에 있는 데이터에 의해 결정되며 오랫동안 유지되는 정체성이 존재하지 않는다.
그렇다면 이 책에서의 배치 모델은 어떨까? 이는 참조 번호에 의해 구분된다. 이를 엔티티라고 한다.
엔티티는 오랫동안 정체성이 존재하는 도메인 객체이다. 이는 영속적인 정체성이 있다.
엔티티의 값을 바꿔도, 바뀐 엔티티는 이전과 같은 엔티티로 인식하는 정체성 동등성이 있다. (값과 달리)
2장. 저장소 패턴 (Repository Pattern)
핵심 로직과 인프라 관련 사항을 분리하는 방법으로 의존성 역전 원칙을 사용해보는 챕터이다.
저장소 패턴은 데이터 저장소를 더 간단히 추상화한 것으로 이 패턴을 사용하면 모델 계층과 데이터 계층을 분리할 수 있다.
도메인 모델에는 그 어떤 의존성도 없기 바란다.
하부 구조와 관련된 문제가 도메인 모델에 지속적으로 영향을 끼쳐서 단위 테스트를 느리게 하고 도메인 모델을 변경할 능력이 감소되지 않는 것을 원하지 않는다.
의존성 역전: 모델의 의존하는 ORM
스키마를 별도로 정의하고, 스키마와 도메인 모델을 상호 변환하는 명시적인 매퍼를 정의하는 것
ORM과 도메인 모델의 의존성을 역전시키는 단계를 거치고 나면 아주 작은 단계를 하나만 통과해도 테스트를 작성하는 게 쉬워진다. 테스트에서 가짜로 대치할 수 있는 간단한 인터페이스를 제공하는 저장소 패턴이라는 추상화를 구현할 수 있게 된다.
저장소 패턴
저장소 패턴은 영속적 저장소를 추상화 한 것이다. 저장소 패턴은 모든 데이터가 메모리상에 존재하는 것처럼 가정해 데이터 접근과 관련된 지루한 세부 사항을 감춘다.
추상화를 대신하는 가짜 객체를 만드는 것은 설계에 대한 피드백을 얻는 아주 좋은 방법이다. 가짜 객체를 만들기 어렵다면 추상화를 너무 복잡하게 설계했기 때문이다.
포트와 어댑터는 객체 지향 세계에서 나온 용어다. 이 책에서는, 포트는 애플리케이션과 추상화하려는 대상 사이의 인터페이스이며(추상 기반 클래스를 사용한다면 포트다.), 어댑터는 이 인터페이스나 추상화가 뒤에 있는 구현이라는 정의를 채택한다.
아키텍처 패턴을 제시할 때마다 항상 이런 질문을 던질 것이다. “이로 인해 얻는 이익은 무엇인가? 이 패턴을 채택하면 치뤄야 하는 댓가는 무엇인가?”
2장 정리
- ORM에 의존성 역전을 적용하자
도메인 모델은 인프라에 대해 걱정할 필요가 없어야 한다. ORM은 모델을 임포트해야 하며 모델이 ORM을 임포트해서는 안된다. - 저장소 패턴은 영속적 저장소에 대한 단순한 추상화다.
저장소는 컬렉션이 메모리상에 있는 객체라는 환상을 제공한다. 저장소를 사용하면 핵심 애플리케이션에는 영향을 미치지 않으면서 인프라를 이루는 세부 구조를 변경하거나 FakeRepository를 쉽게작성할 수 있다.
3장. 결합과 추상화
B 컴포넌트가 깨지는 게 두려워서 A 컴포넌트를 변경할 수 없는 경우를 이 두 컴포넌트가 서로 결합되었다고 한다. 지역적인 결합은 좋은 것이다. 결합된 요소들 사이에 응집이 있다는 용어로 이런 경우를 표현한다.
하지만 전역적 결합은 성가신 존재다. 코드를 변경하는 데 드는 비용을 증가시킨다.
4장. 첫 번째 유스 케이스: 플라스크 API와 서비스 계층
엔드투엔드 테스트: 실제 API 엔드포인트와 실제 데이터베이스를 사용하는 테스트
오케스트레이션: 저장소에서 여러 가지를 가져오고, 데이터베이스 상태에 따라 입력을 검증하여 오류를 처리하고, 성공적인 경우 데이터를 데이터베이스에 커밋하는 작업을 포함한다. 이 작업 대부분은 웹 API 엔드포인트와는 관련이 없다. 오케스트레이션은 엔드투엔드 테스트에서 실제로 테스트해야하는 대상이 아니다.
오케스트레이션 계층이나 유스케이스 계층이라고 부르는 서비스 계층으로 분리하는 것이 타당한 경우가 종종 있다.
(E2E 테스트 개수가 많아진다면, 서비스 계층과의 분리가 잘되어있는지 확인이 필요해보인다.)
“의존성 역전 원칙”은 “추상화에 의존해야 한다”라는 말의 의미와 같다. 여기서 고수준 모듈인 서비스 계층은 저장소라는 추상화에 의존한다. 구현의 세부 내용은 어떤 영속 저장소를 선택했느냐에 따라 다르지만 같은 추상화에 의존한다.
서비스 계층으로 분리를 한다면, 플라스크 앱은 이제 훨씬 더 깔끔해보일 것이다. 그리고 플라스크 앱의 책임은 표준적인 웹 기능 뿐이다.
(→ 요청 전 상태를 관리하고, POST 파라마티로부터 정보를 파싱하며 상태 코드를 응답하고 JSON을 처리한다.)
이렇게 되면 E2E 테스트를 단 두가지로 정리할 수 있다. 하나는 정상 경로를 테스트하고, 다른 하나는 비정상 경로를 테스트한다.
애플리케이션 서비스와 도메인 서비스
- 애플리케이션 서비스 (서비스 계층): 외부 세계에서 오는 요청을 처리해 연산을 오케스트레이션한다.
- 도메인 서비스: 도메인 모델에 속하지만, 근본적으로 상태가 있는 엔티티나 값객체에 속하지 않는 로직을 부르는 이름이다. ex) TaxCalculator 도메인의 성격을 잘 나타낼 수 있다.
모든 요소를 폴더에 넣고 각 부분이 어떤 위치에 있는지 살펴보기
애플리케이션이 커질때마다 디렉토리 구조를 깔끔하게 다듬을 필요가 있다. (모듈들의 계층 구조를 다듬는 것!)
서비스 계층을 추가할 때의 장점
- 서비스 계층을 사용하면 테스트를 “높은 기어비”로 작성할 수 있고, 도메인 모델을 적합한 형태로 마음껏 리팩터링할 수 있다. 서비스 계층을 활용하면 같은 유스케이스를 제공할 수 있는 한 이미 존재하는 수많은 테스트를 재작성하지 않고도 새로운 설계를 테스트할 수 있다.
- 작성한 테스트의 피라미드도 좋아보인다. 테스트 상당 부분은 빠른 단위 테스트이며 E2E나 통합 테스트는 최소화된다.
5장. 높은 기어비와 낮은 기어비의 TDD
테스트에 넣는 코드는 한줄, 한줄이 마치 본드 방울같다. 이 본드 방울은 시스템을 특정 모양으로 만든다. 테스트가 더 저수준일수록 시스템 각부분을 변경하기가 더 어려워진다.
- API 테스트
이는 전체 애플리케이션을 다시 작성해도 URL이나 요청 방식을 바꾸지 않는 한, 앱은 HTTP 테스트를 계속 통과한다.
API 테스트는 훨씬 더 높은 수준의 추상화를 사용하므로 객체의 세부 설계에 대한 피드백을 제공하지 않는다. - 도메인 테스트
도메인 테스트를 하면 테스트가 도메인 언어로 작성되므로 이 테스트는 모델의 살이있는 문서 역할을 한다. 새로운 팀원은 이런 테슽,를 읽고 시스템이 어떻게 동작하는지 빠르게 이해하고 핵심 개념이 서로 어떻게 연관됐는지 이해할 수 있다.
도메인 테스트가 있으면 필요한 객체에 대한 이해를 증진시킬 때 동무이 된다.
도메인으로부터 완전히 분리된 서비스 계층을 만들기 위해서는 API를 원시 타입만 사용하도록 다시 작성해야한다.
다음 코드에서는 서비스 계층은 OrderLine 도메인 객체를 받는다.
def allocate(line: OrderLine, repo: AbstractRepository, session) -> str:
이 함수의 파라미터를 모두 원시 타입으로 바꾸면 어떤 모양이 될까?
def allocate( orderid: str, sku: str, qty: int, repo: AbstractRepository, session ) -> str:
정리 (여러 유형의 테스트를 작성하는 간단한 규칙)
- 특성당 엔드투엔드 테스트를 하나씩 만든다는 목표를 세워야한다.
목표는 어떤 특성이 잘 작동하는지 보고 움직이는 모든 부품이 서로 잘 연결되어 움직이는지 살펴보는 것이다. - 테스트 대부분은 서비스 계층을 사용해 만드는 걸 권한다.
이런 테스트는 커버리지, 실행 시간, 효율 사이를 잘 절충할 수 있게 해준다. 각 테스트는 어떤 기능의 한 경로를 테스트하고 I/O에 가짜 객체를 사용하는 경향이 있다. 이 테스트는 모든 에지 케이스를 다루고, 비즈니스 로직의 모든 입력과 출력을 테스트해볼 수 있는 좋은 장소다. - 도메인 모델을 사용하는 핵심 테스트를 적게 작성하고 유지하는 걸 권한다.
이런 테스트는 좀 더 커버리지가 작고(좁은 범위를 테스트), 더 깨지기 쉽다. 하지만 이런 테스트가 제공하는 피드백이 가장 크다. 이런 테스트를 나중에 서비스 계층 기반 테스트로 대신할 수 있따면 테스트를 주저하지 말고 삭제하는 걸 권한다. - 오류 처리도 특성으로 취급하자.
이상적인 경우 애플리케이션은 모든 오류가 진입점으로 거슬러 올라와서 처리되는 구조로 되어있다. 단지 각 기능의 정상 경로만 테스트하고 모든 비정상 경로를 테스트하는 엔드투엔드 테스트를 하나만 유지하면 된다는 의미다.
도움될 만한 몇가지 사항
- 서비스 계층을 도메인 객체가 아니라 원시 타입을 바탕으로 기술하라.
- 이상적인 경우라면 테스트해야 할 모든 서비스를저장소나 데이터베이스를 통해 상태를 해킹할 필요 없이 오직 서비스 게층 기반으로 테스트할 수 있다. 이렇게 노력한다면 나중에 엔드투엔드 테스트에서도 이익을 얻을 수 있다.
6장. 작업 단위 패턴 (UoW)
저장소와 서비스 계층 패턴을 하나로 묶어주는 마지막 퍼즐 조각 → 작업 단위(UoW) 패턴이다.
UoW 패턴을 사용하면 서비스 계층과 데이터 계층을 완전히 분리할 수 있다
저장소 패턴이 영속적 저장소 개념에 대한 추상화라면 UoW는 원자적 연산(Atomic operation) 개념의 추상화다.
UoW가 없는 경우: API는 서비스 계층, 저장소 계층, 데이터베이스와 직접 소통한다.
UoW가 있는 경우 : UoW가 데이터베이스 상태를 관리한다.
명시적인 커밋을 요구하면 시스템의 상태를 바꾸는 경로가 단 하나(완전히 성공해서 커밋을 명시적으로 하는 경로)만 존재하므로 코드를 추론하는 것이 쉬워진다.
정리
- 작업 단위 패턴은 데이터 무결성 중심 추상화다.
- 작업 단위 패턴은 저장소와 서비스 계층 패턴과 밀접하게 연관되어 작동한다.
- 콘텍스트 관리자를 사용하는 멋진 유스케이스다.
- SQLAlchemy는 이미 작업 단위 패턴을 제공한다.
7장. 애그리게이트와 일관성 경계
애그리게이트는 다른 도메인 객체들을 포함하여 이 객체 컬렉션 전체를한꺼번에 다룰 수 있게 해주는 도메인 객체이다.
애그리게이트에 있는 객체를 변경하는 유일한 방법은 애그리게이트와 그 안의 객체 전체를 불러와서 애그리게이트 자체에 대해 메서드를 호출하는 것이다.
모델이 더 복잡해지고 엔티티와 값 객체가 늘어나면서 각각에 대한 참조가 서로 얽히고설킨 그래프가 된다. 따라서 누가 어떤 객체를 변경할 수 있는지 추적하기가 어려워진다.
모델 안에 컬렉션이 있으면 어떤 엔티티를 선정해서 그 엔티티와 관련된 모든 객체를 변경할 수 있는 단일 진입점으로 삼으면 좋다. 이렇게 하면 시스템이 개념적으로 더 간단해지고 어떤 객체가 다른 객체의 일관성을 책임지게 하면 시스템에 대해 추론하기가 쉬워진다.
애그리게이트는 자신만의 불변 조건을 유지할 책임을 담당하는 한 동시성 경계다.
애그리게이트는 데이터 변경이라는 목적을 위해 한 단위로 취급할 수 있는 연관된 객체의 묶음이다. - 에릭 에번스, “도메인 주도 설계”
에번스에 따르면, 애그리게이트에는 원소에 대한 접근을 캡슐화한 루트 엔티티가 있다. 원소마다 고유한 정체성이 있지만, 시스템의 나머지 부분은 루트 엔티티를 나눌 수 없는 단일 객체처럼 참조해야한다.
애그리게이트가 될 엔티티를 정의하고 나면 외부 세계에서 접근할 수 있는 유일한 엔티티가 되어야 한다는 규칙을 적용해야 한다. 즉, 허용되는 모든 저장소는 애그리게이트만 반환해야 한다.
저장소가 애그리게이트만 반환해야 한다는 규칙은 애그리게이트가 도메인 모델에 접근할 수 있는 유일한 통로여야 한다는 관례를 강제로 지키게 하는 핵심 규칙이다.
정리
- 애그리게이트는 도메인 모델에 대한 진입점이다.
도메인에 속한 것을 바꿀 수 있는 방식을 제한하면 시스템을 더 쉽게 추론할 수 있다. - 애그리게이트는 일관성 경계를 책임진다.
애그리게이트의 역할은 관련된 여러 객체로 이루어진 그룹에 적용할 불변조건에 대한 비즈니스 규칙을 관리하는 것이다. 자신이 담당하는 객체 사이와 객체와 비즈니스 규칙 사이의 일관성을 검사하고, 어떤 변경이 일관성을 헤친다면 이를 거부하는 것도 애그리게이트의 역할이다. - 애그리게이트와 동시성 문제는 공존한다.
동시성 검사 구현 방법을 고민하다보면 결국에는 트랜잭션과 락에 도달하게 된다.
Part1의 최종 구조
참고
반응형