지난번 포스팅에 이어 최범균님의 도메인 주도 개발 시작하기 책 챕터 9,10,11 에 대해 정리한 내용을 포스팅 하려고 합니다.
chapter 9. 도메인 모델과 바운디드 컨텍스트
chapter 10. 이벤트
chapter 11. CQRS
지난번 포스팅은 아래 링크에서 보실 수 있습니다.
[책] 도메인 주도 개발 시작하기 - Chapter 1,2
[책] 도메인 주도 개발 시작하기 - Chapter 3,4
[책] 도메인 주도 개발 시작하기 - Chapter 5,6
[책] 도메인 주도 개발 시작하기 - Chapter 7,8
chapter 9. 도메인 모델과 바운디드 컨텍스트
9.1 도메인 모델과 경계
- 처음 도메인 모델을 만들 때 빠지기 쉬운 함정이 도메인을 완벽하게 표현하는 단일 모델을 만드는 시도를 하는 것이다.
- 한 개의 모델로 여러 하위 도메인을 모두 표현하려고 시도하면 오히려 모든 하위 도메인에 맞지 않는 모델을 만들게 된다.
- 논리적으로 같은 존재처럼 보이지만 하위 도메인에 따라 다른 용어를 사용하는 경우도 있다.
- 한 개의 모델로 모든 하위 도메인을 표현하려는 시도는 올바른 방법이 아니며, 표현할 수도 없다.
- 올바른 도메인 모델을 개발하려면 하위 도메인마다 모델을 만들어야 한다.
- 각 모델은 명시적으로 구분되는 경계를 가져서 섞이지 않도록 해야 한다.
- 모델은 특정한 컨텍스트(문법) 하에서 완전한 의미를 갖는다.
- 이렇게 구분되는 경계를 갖는 컨텍스트를 DDD 에서는 바운디드 컨텍스트라고 부른다.
9.2 바운디드 컨텍스트
- 바운디드 컨텍스트는 모델의 경계를 결정하며 한 개의 바운디드 컨텍스트는 논리적으로 한 개의 모델을 갖는다.
- 바운디드 컨텍스트는 용어를 기준으로 구분한다.
- 실제로 사용자에게 기능을 제공하는 물리적 시스템으로 도메인 모델은 이 바운디드 컨텍스트 안에서 도메인을 구현한다.
- 여러 하위 도메인을 하나의 바운디드 컨텍스트에서 개발할 때 주의할 점은 하위 도메인의 모델이 섞이지 않도록 하는 것이다.
- 도메인 모델이 개별 하위 도메인을 제대로 반영하지 못해서 하위 도메인별로 기능을 확장하기 어렵게 되고 이는 서비스 경쟁력을 떨어뜨리는 원인이 된다.
- 한 개의 바운디드 컨텍스트가 여러 하위 도메인을 포함하더라도 하위 도메인마다 구분되는 패키지를 갖도록 구현해야 하위 도메인마다 바운디드 컨텍스트를 갖는 효과를 낼 수 있다.
- 바운디드 컨텍스트는 도메인 모델을 구분하는 경계가 되기 때문에 바운디드 컨텍스트는 구현하는 하위 도메인에 알맞은 모델을 포함한다.
9.3 바운디드 컨텍스트 구현
- 바운디드 컨텍스트는 도메인 기능을 사용자에게 제공하는 데 필요한 표현 영역, 응용 서비스, 인프라스트럭처 영역을 모두 포함한다.
- 모든 바운디드 컨텍스트를 반드시 도메인 주도로 개발할 필요는 없다.
- 서비스 - DAO 구조를 사용하면 도메인 기능이 서비스에 흩어지게 되지만 도메인 기능 자체가 단순하면 서비스 - DAO 로 구성된 CRUD 방식을 사용해도 코드 유지 보수하는데 문제 되지 않는다.
- 한 바운디드 컨텍스트에서 두 방식을 혼한해서 사용할 수도 있다.
- 대표적인 예가 CQRS 패턴이다.
- 각 바운디드 컨텍스트는 서로 다른 구현 기술을 구현 기술을 사용할 수도 있다.
- 바운디드 컨텍스트가 반드시 사용자에게 보여지는 UI를 가지고 있어야 하는 것은 아니다.
- UI를 처리하는 서버를 두고 UI 서버에서 바운디드 컨텍스트와 통신해서 사용자 요청을 처리하는 방법도 있다.
- 이 구조에서 UI 서버는 각 바운디드 컨텍스트를 위한 파사드 역활을 수행한다.
9.4 바운디드 컨텍스트 간 통합
- 두 팀이 관련된 바운디드 컨텍스트를 개발하면 자연스럽게 두 바운디드 컨텍스트 간 통합이 발생한다.
- 도메인 서비스를 구현한 클래스는 인프라스트럭처 영역에 위치한다. 이 클래스는 외부 시스템과의 연동을 처리하고 외부 시스템의 모델과 현재 도메인 모델 간의 변환을 책임진다.
- 두 모델 간의 변환 과정이 복잡하면 변환 처리를 위한 별도 클래스를 만들고 이 클래스에서 변환을 처리해도 된다.
- REST API 를 호출하는 것은 두 바운디드 컨텍스트를 직접 통합하는 방법이다. 직접 통합하는 대신 간접적으로 통합하는 방법도 있다.
- 대표적인 간적 통합 방식이 메시지 큐를 사용하는 것이다.
- 어떤 도메인 관점에서 모델을 사용하느냐에 따라 두 바운디드 컨텍스트의 구현 코드가 달라지게 된다.
- 두 바운디드 컨텍스트를 개발하는 팀은 메시징 큐에 담을 데이터의 구조를 협의하게 되는데 그 큐를 누가 제공하느냐에 따라 데이터 구조가 결정된다.
- 마이크로서비스의 특징은 바운디드 컨텍스트와 잘 어울린다.
- 각 바운디드 컨텍스트는 모델의 경계를 형성하는데 바운디드 컨텍스트를 마이크로서비스로 구현하면 자연스럽게 컨텍스트별로 모델이 분리된다.
9.5 바운디드 컨텍스트 간 관계
- 바운디드 컨텍스트는 어떤 식으로든 연결되기 때문에 두 바운디드 컨텍스트는 다양한 방식으로 관계를 맺는다.
- 두 바운디드 컨텍스트 간 관계 중 가장 흔한 관계는 한쪽에서 API 를 제공하고 다른 한쪽에서 그 API 를 호출하는 관계이다.
- 상류 팀과 하류 팀은 개발 계획을 서로 공유하고 일정을 협의해야 한다.
- 상류 팀의 고객인 하류 팀이 다수 존재하면 상류 팀은 여러 하류 팀의 요구사항을 수용할 수 있는 API 를 만들고 이를 서비스 형태로 공개해서 서비스의 일관성을 유지할 수 있다.
- 이런 서비스를 가리켜 공개 호스트 서비스(OPEN HOST SERVICE)라고 한다.
- 상류 컴포넌트의 서비스는 상류 바운디드 컨텍스트의 도메인 모델을 따른다.
- 외부 시스템과의 연동을 처리하는데 외부 시스템의 도메인 모델이 내 도메인 모델을 침범하지 않도록 막아주는 역활을 한다
- 내 모델이 깨지는 것을 막아주는 안티코럽션 계층이 된다.
- 두 바운디드 컨텍스트가 같은 모델을 공유하는 경우도 있다.
- 팀이 공유하는 모델을 공유 커널이라고 부른다.
- 장점은 중복을 줄여준다는 것
- 한 팀에서 임의로 모델을 변경하면 안 되며 두 팀이 밀접한 관계를 유지해야 한다.
- 두 바운디드 컨텍스트 간에 통합하지 않으므로 서로 독립적으로 모델을 발전시킨다.
- 수동으로 통합하는 방식이 나쁜 것은 아니지만 규모가 커질수록 수동 통합에는 한계가 있으므로 규모가 커지기 시작하면 두 바운디드 컨텍스트를 통합해야 한다.
9.6 컨텍스트 맵
- 컨텍스트 맵은 시스템의 전체 구조를 보여준다.
- 하위 도메인과 일치하지 않는 바운디드 컨텍스트를 찾아 도메인에 맞게 바운디드 컨텍스트를 조절하고 사업의 핵심 도메인을 위해 조직 역량을 어떤 바운디드 컨텍스트에 집중할지 파악하는 데 도움을 준다.
chapter 10. 이벤트
10.1 시스템간 강결합 문제
- 외부에 있는 시스템이 제공하는 서비스 호출 시 두가지 문제가 발생할 수 있다.
- 외부서비스가 정상이 아닐경우 트랜잭션 처리를 어떻게 해야 할지 애매하다
- 외부시스템의 응답시간이 길어지면 그만큼 대기 시간도 길어진다.
- 도메인 객체에 서비스를 전달하면 추가로 설계상 문제가 나타날 수 있다.
- 또다른 문제는 기능을 추가할 때 발생한다.
- 지금까지 언급한 문제가 발생하는 이유는 두 바운디드 컨텍스트 간의 강결합 (high coupling) 때문이다.
- 비동기 이벤트를 사용하면 두 시스템간의 결합을 크게 낮출 수 있다.
10.2 이벤트 개요
- 이벤트가 발생한다는 것은 상태가 변경됐다는 것을 의미한다.
- 그 이벤트에 반응하여 원하는 동작을 수행하는 기능을 구현한다.
10.2.1 이벤트 관련 구성요소
- 도메인 모델에 이벤트를 도입하려면 네 개의 구성요소인 이벤트, 이벤트 생성주체, 이벤트 디스패처(퍼블리셔), 이벤트 핸들러(구독자)를 구현해야 한다.
- 도메인 모델에서 이벤트 생성 주체는 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체이다.
- 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생시킨다.
- 이벤트 핸들러는 이벤트 생성 주체가 발생한 이벤트에 반응한다.
- 이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이 이벤트 디스패쳐다.
- 이벤트 디스패처의 구현 방식에 따라 이벤트 생성과 처리를 동기나 비동기로 실행하게 된다.
10.2.2 이벤트의 구성
- 이벤트는 발생한 이벤트에 대한 정보를 담는다.
- 이벤트 종류 : 클래스 이름으로 이벤트 종류를 표현
- 이벤트 발행시간
- 추가 데이터 : 주문번호, 신규 배송지 정보 등 이벤트와 관련된 정보
- 이벤트 이름에는 과거 시제를 사용한다.
- 이벤트는 이벤트 핸들러가 작업을 수행하는 데 필요한 데이터를 담아야 한다.
10.2.3 이벤트 용도
- 이벤트는 크게 두가지 용도로 쓰인다.
- 트리거
- 서로 다른 시스템 간의 데이터 동기화
10.2.4 이벤트 장점
- 이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다.
- 이벤트 핸들러를 사용하면 기능 확장도 용이하다.
10.3 이벤트, 핸들러, 디스패처 구현
- 이벤트와 관련된 코드는 다음과 같다.
- 이벤트 클래스 : 이벤트를 표현한다.
- 디스패처 : 스프링이 제공하는 ApplicationEventPublisher 를 이용한다.
- Events : 이벤트를 발행한다. 이벤트 발행을 위해 ApplicationEventPublisher 를 이용한다.
10.3.1 이벤트 클래스
- 이벤트 자체를 위한 상위 타입은 존재하지 않는다. 원하는 클래스를 이벤트로 사용하면 된다.
- 이벤트 클래스는 이벤트 처리하는데 필요한 최소한의 데이터를 포함해야 한다.
- 모든 이벤트가 공통으로 갖는 프러퍼티가 존재한다면 관련 상위 클래스를 만들 수도 있다.
10.3.2 Events 와 ApplicationEventPublisher
- Events 클래스는 ApplicationEventPublisher 를 사용해서 이벤트를 발생시키도록 구현할 것이다.
10.3.3 이벤트 발생과 이벤트 핸들러
10.3.4 흐름 정리
- 도메인 기능을 실행한다.
- 도메인 기능은 Events.raise() 를 이용해서 이벤트를 발생시킨다.
- Events.raise() 는 스프링이 제공하는 ApplicationEventPublisher 를 이용해서 이벤트를 출판한다.
- ApplicationEventPublisher 는 @EventListener(이벤트 타입.class) 애너테이션이 붙은 메서드를 찾아 실행한다.
10.4 동기 이벤트 처리 문제
- 이벤트를 사용해서 강결합 문제는 해소했지만 아직 남아 있는 문제가 하나 있다. 바로 외부 서비스에 영향을 받는 문제이다.
- 외부 서비스의 성능 저하가 바로 내 시스템의 성능 저하로 연결된다.
- 트랜잭션도 문제가 된다.
- 외부 시스템과의 연동을 동기로 처리할 때 발생하는 성능과 트랜잭션 범위 문제를 해소하는 방법은 이벤트를 비동기로 처리하거나 이벤트와 트랜잭션을 연계하는 것이다.
10.5 비동기 이벤트 처리
- ‘A하면 이어서 B하다’ 는 요구사항 중에서 ‘A하면 최대 언제까지 B하라’ 로 바꿀 수 있는 요구사항은 이벤트를 비동기로 처리하는 방식으로 구현할 수 있다.
- 다음 네가지 방식으로 비동기 이벤트 처리를 구현하는 방법
- 로컬 핸들러를 비동기로 실행하기
- 메시지큐를 사용하기
- 이벤트 저장소와 이벤트 포워더 사용하기
- 이벤트 저장소와 이벤트 API 사용하기
10.5.1 로컬 핸들러 비동기 실행
- 이벤트 핸들러를 별도 스레드로 실행하는 것이다.
- 스프링이 제공하는 @Async 애너테이션을 사용하면 손쉽게 비동기로 이벤트 핸드러를 실행할 수 있다.
- @EnableAsync 애너테이션을 사용해서 비동기 기능을 활성화한다.
- 이벤트 핸들러 메서드에 @Async 애너테이션을 붙힌다.
- 스프링이 제공하는 @Async 애너테이션을 사용하면 손쉽게 비동기로 이벤트 핸드러를 실행할 수 있다.
10.5.2 메시징 시스템을 이용한 비동기 구현
- 카프카나 래빗MQ 와 같은 메시징 시스템을 사용하는 것이다.
- 필요하다면 이벤트를 발행시키는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶어야 한다 (글로벌 트랜잭션)
- 글로벌 트랜잭션을 사용하면 안전하게 이벤트를 메시지 큐에 전달할 수 있는 장점이 있지만 반대로 글로벌 트랜잭션으로 인해 전체 성능이 떨어지는 단점도 있다.
- 필요하다면 이벤트를 발행시키는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶어야 한다 (글로벌 트랜잭션)
- 메시지 큐를 사용하면 보통 이벤트를 발생시키는 주체와 이벤트 핸들러가 별도 프로세스에서 동작한다.
- 이벤트 발생 JVM 과 이벤트 처리 JVM 이 다르다는 것을 의미한다.
- 래빗MQ 처럼 많이 사용되는 메시징 시스템은 글로벌 트랜잭션 지원과 함께 클러스터와 고가용성을 지원하기 때문에 안정적으로 메시지를 전달할 수 있는 장점이 있다.
- 카프카는 글로벌 트랜잭션을 지원하진 않지만 다른 메시징 시스템에 비해 높은 성능을 보여준다.
10.5.3 이벤트 저장소를 이용한 비동기 처리
- 이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용해서 이벤트 핸들러에 전달하는 것이다.
- 이벤트가 발생하면 핸들러는 스토리지에 이벤트를 저장한다. 포워더는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 실행한다.
- 포워더는 별도 스레드를 이용하기 때문에 이벤트 발행과 처리가 비동기로 처리된다.
- 도메인 상태 변화와 이벤트 저장이 로컬 트랜잭션으로 처리된다.
- 이벤트를 외부에 제공하는 API 를 사용하는 것이다.
- API 방식과 포워더 방식의 차이점은 이벤트를 전달하는 방식에 있다.
- 포워더 방식이 포워더를 이용해서 이벤트를 외부에 전달한다면, API 방식은 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져 간다
10.6 이벤트 적용 시 추가 고려 사항
- 이벤트를 구현할 때 추가로 고려할 점이 있다.
- 이벤트 소스를 EventEntity 에 추가할지 여부이다.
- 이벤트 발생 주체에 대한 정보를 추가해야 한다.
- 포워더에서 전송 실패를 얼마나 허용할 것이냐에 대한 것이다.
- 포워더를 구현할 때는 실패한 이벤트의 재전송 횟수 제한을 두어야 한다.
- 이벤트 손실
- 이벤트 저장소를 사용하는 방식은 이벤트가 저장소에 보관된다는 것을 보장할 수 있다.
- 로컬 핸들러를 이용할 경우 이벤트 처리에 실패하면 이벤트를 유실하게 된다.
- 이벤트 순서
- 이벤트 발생 순서대로 전달해야 할 경우, 이벤트 저장소를 사용하는 것이 좋다.
- 이벤트 재처리
- 동일한 이벤트를 다시 처리해야 할 때 이벤트를 어떻게 할지 결정해야 한다.
- 마지막 처리한 이벤트 순번을 기억해 두었다가 이미 처리한 순번의 이벤트가 도착하면 해당 이벤트를 처리하지 않고 무시하는 것이다.
- 이벤트를 멱등으로 처리하는 방법도 있다.
- 연산을 여러번 적용해도 결과가 달라지지 않는 성질
- 이벤트 소스를 EventEntity 에 추가할지 여부이다.
10.6.1 이벤트 처리와 DB 트랜잭션 고려
- 이벤트를 처리할 때는 DB 트랜잭션을 함꼐 고려해야 한다.
- 이벤트 처리를 동기로 하든 비동기로 하든 이벤트 처리 실패와 트랜잭션 실패를 함께 고려해야 한다.
- 경우의 수를 줄이는 방법은 트랜잭션이 성공할 때만 이벤트 핸들러를 실핸하는 것이다.
- @TransactionalEventListener 애너테이션을 지원한다.
- phase 속성 값으로 TransactionPhase.AFTER_COMMIT 을 지정하면 트랜잭션 커밋에 성공한 뒤에 핸들러 메서드를 실행한다.
chapter 11. CQRS
11.1 단일 모델의 단점
- 조회 화면 특성상 조회 속도가 빠를수록 좋은데 여러 애그리거트의 데이터가 필요하면 구현 방법을 고민해야 한다.
- 식별자를 이용해서 애그리거트를 참조하는 방식을 사용하면 즉시 로딩 방식과 같은 JPA의 쿼리 관련 최적화 기능을 사용할 수 없다.
- 직접 참조하는 방식으로 연결하면, 조회 화면 특성에 따라 같은 연관도 즉시 로딩이나 지연 로딩으로 처리해야 하기 때문이다.
- 조회 기능을 구현할 때 DBMS 가 제공하는 전용 기능이 필요하면 JPA 의 네이티브 쿼리를 사용해야 할 수도 있다.
- 이런 고민이 발생하는 이유는 시스템 상태를 변견할 때와 조회할 때 단일 도메인 모델을 사용하기 때문이다.
- 이런 구현 복잡도를 낮추는 간단한 방법이 있는데 그것은 바로 상태 변경을 위한 모델과 조회를 위한 모델을 분리하는 것이다.
11.2 CQRS
- 시스템이 제공하는 기능은 크게 두 가지로 나눌 수 있다.
- 상태를 변경하는 기능
- 도메인 모델 관점에서 상태 변경 기능은 주로 한 애그리거트의 상태를 변경한다.
- 사용자 입장에서 상태 정보를 조회하는 기능
- 조회 기능에 필요한 데이터를 표시하려면 두 개 이상의 애그리거트가 필요할 때가 많다.
- 단일 모델로 두 종류의 기능을 구현하면 모델이 불필요하게 복잡해진다.
- 단일 모델을 사용할 때 발생하는 복잡도를 해결하기 위해 사용하는 방법이 바로 CQRS 다
- 상태를 변경하는 기능
- CQRS 는 Command Query Responsibility Segregation 의 약자로 상태를 변경하는 명령을 위한 모델과 상태를 제공하는 조회를 위한 모델을 분리하는 패턴이다.
- 복잡한 도메인에 적합하다.
- 각 모델에 맞는 구현 기술을 선택할 수 있다.
- 명령 모델과 조회 모델이 서로 다른 데이터 저장소를 사용할 수도 있다.
- 명령 모델은 트랜잭션을 지원하는 RDMS 를 사용
- 조회 모델은 조회 성능이 좋은 메모리기반 NoSQL 을 사용할 수 있을 것이다.
- 두 데이터 저장소 간 데이터 동기화는 이벤트를 활용해서 처리한다.
- 명령 모델에 데이터가 바뀌자마자 변경 내역을 바로 조회 모델에 반영해야 한다면 동기 이벤트와 글로벌 트랜잭션을 사용해서 실시간으로 동기화할 수 있다.
- 두 저장소의 데이터를 특정 시간 안에만 동기화해도 된다면 비동기로 데이터를 전송하면 된다.
11.2.1 웹과 CQRS
- 일반적인 웹 서비스는 상태를 변경하는 요청보다 상태를 조회하는 요청이 많다.
- 쿼리를 최적화해서 쿼리 실행 속도 자체를 높이고,
- 메모리에 조회 데이터를 캐싱 해서 응답 속도를 높이기도 한다.
- 화면에 맞는 모양으로 변환한 데이터를 캐싱 할 때 성능에 더 유리하다. (조회 전용 모델)
- 조회 전용 저장소를 따로 사용하기도 한다.
11.2.2 CQRS 장단점
- 장점
- CQRS 패턴을 적용할 때 얻을 수 있는 장점은 명령 모델을 구현할 때 도메인 자체에 집중할 수 있다는 점이다.
- 조회 성능을 향상시키는 데 유리하다는 점이다.
- 단점
- 구현해야 할 코드가 더 많다.
- 더 많은 구현 기술이 필요
마무리
그럼 이만. 🥕👋🏼🖐🏼