til
til copied to clipboard
도메인 주도 개발 시작하기
https://hanbit.co.kr/store/books/look.php?p_code=B4309942517
도메인 모델 시작하기
도메인
- 소프트웨어로 해결하고자 하는 문제 영역
- 도메인 전문가, 관계자, 개발자가 같은 지식을 공유하고 직접 소통할수록 전문가가 원하는 제품을 만들 가능성이 높아진다
도메인 모델
- 특정 도메인을 개념적으로 표현한 것
- 도메인을 이해하는데 도움이 된다면 표현 방식(UML, 수학 공식)은 중요하지 않음
- 모델의 각 구성 요소는 특정 도메인으로 한정할 때 비로소 의미가 완전해지기 때문에 각 하위 도메인마다 별도로 모델을 만들어야 한다.
- 아키텍처 상의 도메인 계층을 구현할 때 사용하는 객체 모델
- 표현(Presentation): 사용자의 요청을 처리하고 정보를 보여준다.
- 응용(Application): 사용자가 요청한 기능을 실행한다. 업무 로직을 구현하지 않는다.
- Domain: 시스템이 제공할 도메인 규칙을 구현한다.
- Infrastructure: 외부 시스템과의 연동을 처리한다.
도메인 모델링
- 요구사항으로부터 핵심 구성 요소, 규칙, 기능을 찾는다.
- 문서화를 통해 지식을 공유한다.
엔티티
- 식별자를 갖는다.
- 식별자를 가지고
equals()
,hashCode()
메서드를 구현할 수 있다. (식별자가 같으면 두 엔티티는 같다)
밸류
- 개념적으로 완전한 하나를 표현 (예: 받는 사람, 배송 정보)
- 의미를 명확하게 하기 위해 사용 (예:
int
타입을 필드로 갖는Money
타입) - 밸류 타입을 위한 기능을 추가할 수 있다.
- 특별한 이유가 없다면 불변 객체로 정의해서 사용한다.
- 모든 속성을 비교해서
equals()
,hashCode()
메서드를 구현한다. - 식별자를 위한 밸류 타입을 사용해서 의미가 잘 드러나도록 할 수 있다. (예:
OrderNo
)
set 메서드
-
set
메서드는 필드값만 변경하고 끝나기 때문에 상태 변경과 관련된 도메인 지식이 코드에서 사라지게 된다. - 객체를 생성할 때 온전하지 않은 상태가 될 수 있다. -> 생성자를 통해 필요한 데이터를 모두 받아야 한다.
- 도메인 모델에는
set
메서드를 넣지 않는다.
유비쿼터스 언어
- 전문가, 관계자, 개발자가 도메인과 관련된 공통의 언어를 만든다.
- 대화, 문서, 도메인 모델, 코드, 테스트 등에서 같은 용어를 사용한다.
- 도메인 용어에 알맞은 단어를 찾는 시간을 아까워하지 말자.
아키텍처 개요
네개의 영역
- 표현 영역: HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환
- 응용 영역: 도메인 모델을 이용해서 사용자에게 제공할 기능을 구현한다. 실제 도메인 로직 구현은 도메인 모델에 위임한다.
- 도메인 영역: 핵심 로직을 도메인 모델에서 구현한다.
- 인프라스트럭처 영역: 논리적인 개념을 표현하기보다는 실제 구현을 다룬다.
- 도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.
계층 구조 아키텍처
- 하위 계층은 상위 계층에 의존하지 않는다.
- 응용 영역과 도메인 영역이 인프라스트럭처에 의존하면
테스트 어려움
과기능 확장의 어려움
이라는 두가지 문제가 발생한다.
DIP 의존 역전 원칙
- 고수준 모듈: 의미 있는 단일 기능을 제공
- 저수준 모듈: 하위 기능을 실제로 구현한 것
- DIP: 저수준 모듈이 고수준 모듈에 의존하게 한다.
- 저수준 모듈 없이 테스트가 가능해진다.
- 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출한다. (인터페이스는 고수준 영역의 패키지에 위치)
- 응용 영역과 도메인 영역에 영향을 최소화하면서 구현체를 변경하거나 추가할 수 있다.
도메인 영역의 주요 구성 요소
- 엔티티: 고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. 데이터와 함께 도메인 기능을 제공한다.
- 밸류: 개념적으로 하나인 값을 표현한다.
- 애그리거트: 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것
- 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다.
- 내부 구현을 숨겨서 애그리거트 단위로 구현을 캡슐화한다.
- 리포지터리: 도메인 모델의 영속성을 처리한다.
- 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.
- 리포지터리 인터페이스는 도메인 모델 영역에 속하며, 실제 구현 클래스는 인프라스트럭처 영역에 속한다.
- 도메인 서비스: 특정 엔티티에 속하지 않은 도메인 로직을 제공한다.
요청 처리 흐름
- 표현 영역은 사용자가 전송한 데이터를 응용 서비스가 요구하는 형식으로 변환해서 전달한다.
- 응용 서비스는 도메인 모델을 이용해서 기능을 구현한다.
- 도메인의 상태를 변경하므로 변경 상태가 물리저장소에 올바르게 반영되도록 트랜잭션을 관리해야 한다.
인프라스트럭처 개요
- 응용 영역의
@Transactional
, 도메인 영역의@Entity
,@Table
은 편리하다. - 응용 영역과 도메인 영역이 인프라스트럭처에 대한 의존을 완전히 갖지 않도록 시도하는 것은 자칫 구현을 더 복잡하고 어렵게 만들 수 있다.
- 표현 영역은 항상 인프라스트럭처와 밀접한 관계가 있다.
모듈 구성
- 도메인이 크면 하위 도메인 별로 모듈을 나눈다.
- 애그리거트, 모델, 리포지터리는 같은 패키지에 위치시킨다.
애그리거트
애그리거트
- 상위 수준에서 도메인 모델간의 관계를 파악
- 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.
- 각 애그리거트는 자기 자신을 관리할 뿐 다른 애그리거트를 관리하지 않는다.
- 함께 변경되는 빈도가 높은 객체는 한 애그리거트에 속할 가능성이 높다.
- 두개 이상의 엔티티로 구성되는 애그리거트는 드물다.
애그리거트 루트
- 애그리거트 전체를 관리할 주체
- 도메인 규칙을 구현한 기능을 제공
- 애그리거트 외부에서 내부의 상태를 변경할 수 없도록 해야한다.
- 단순히 필드를 변경하는 set 메서드를 public 범위로 만들지 않는다.
- 밸류 타입은 불변으로 구현한다.
- 한 트랜잭션에서 한 애그리거트만 수정한다.
- 다른 애그리거트를 변경하지 않는다.
- 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 할 경우, 응용 서비스에서 각 애그리거트의 상태를 변경한다.
리포지터리와 애그리거트
- 리포지터리는 애그리거트 단위로 존재한다.
ID를 이용한 애그리거트 참조
- 필드를 이용한 애그리거트 참조의 문제점
- 편리함 오용 (다른 애그리거트 수정)
- 성능과 관련된 고민 (즉시 로딩, 지연 로딩 등)
- 확장 (하위 도메인별로 시스템을 분리할 수 없게됨)
- ID 참조를 사용하면 한 애그리거트에 속한 객체들만 참조로 연결된다.
- 참조하는 애그리거트가 필요하면 응용 서비스에서 ID를 이용해서 로딩한다.
- N+1 문제가 발생하지 않도록 하려면 조회 전용 쿼리를 사용한다.
- 조회 메서드에서 조인을 이용해 한번의 쿼리로 필요한 데이터를 로딩한다.
- 애그리거트 마다 서로 다른 저장소를 사용하면, 캐시를 적용하거나 조회 전용 저장소를 따로 구성한다.
애그리거트 간 집합 연관
- 개념적으로 양방향 M-N 관계가 존재해도, 실제 구현에서는 단방향 M-N 연관만 적용하면 된다.
- RDBMS 에서는 M-N 연관은 조인 테이블을 사용한다.
- JPA 에서는 밸류 타입에 대한 컬렉션 매핑을 사용해서 M-N 단방향 연관을 구현한다.
애그리거트를 팩토리로 사용하기
- 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면, 애그리거트에 팩토리 메서드를 구현한다.
리포지터리와 모델 구현
매핑 구현
- 애그리거트 루트는 엔티티이므로
@Entity
로 매핑 설정 한다. - 밸류는
Embeddable
로 매핑 설정한다. - 밸류 타입 프로퍼티는
Embedded
로 매핑 설정한다.-
@AttributeOverrides
를 사용해서 매핑할 컬럼 이름을 변경할 수 있다.
-
- JPA 요구사항으로
@Entity
,@Embeddable
은 기본 생성자를 제공해야 한다.- 불변 타입은 기본 생성자가 필요 없으므로, 기본 생성자를
protected
로 선언한다.
- 불변 타입은 기본 생성자가 필요 없으므로, 기본 생성자를
-
@AttributeConverter
를 사용해서 밸류 타입과 컬럼 데이터 간의 변환을 처리한다.-
@Converter(autoApply=true)
를 적용하면 컨버터를 자동으로 적용한다.
-
- 밸류 컬렉션을 별도 테이블로 매핑할 때는
@ElementCollection
과@CollectionTable
을 함께 사용한다.-
@OrderColumn
으로 인덱스를 지정할 수 있다.
-
- 밸류 타입을 식별자로 매핑하면
@Id
대신@EmbeddedId
애너테이션을 사용한다.- 식별자에 기능을 추가할 수 있다. (버전 관리 등)
- 식별자로 사용할 밸류 타입은
equals()
,hashcode()
메서드를 알맞게 구현해야 한다.
- 밸류를 별로 테이블에 저장할 경우
@SecondaryTable
과@AttributeOverride
를 사용한다.- 즉시 로딩으로 작동한다.
- 밸류를 엔티티로 매핑하고 지연 로딩하도록 설정할 수 있지만, 좋은 방법은 아니다. -> 조회 전용 쿼리를 사용하는게 낫다.
- 밸류에 상속을 사용해야 할 경우
-
@Embeddable
대신@Entity
를 이용한다. -
@Inheritance
,@DiscriminatorColumn
을 사용해서 테이블과 매핑한다. - 의존하는 객체에
cascade
설정을 적용하고,orphanRemoval
도true
로 설정한다.
-
- 컬렉션의
clear()
메서드 동작 방식이 다르다-
@OneToMany
: select 쿼리를 수행하고, 각각에 대해 delete 쿼리를 실행한다. -
@ElementCollection
: 객체를 로딩하지 않고, 한 번의 delete 쿼리를 실행한다.
-
애그리거트 로딩 전략
- 카타시안 Cartesian 조인을 사용하면 쿼리 결과에 중복이 발생한다.
- 하이버네이트가 중복된 데이터를 알맞게 제거해주지만, 애그리거트가 커지면 문제가 될 수 있다.
- 애그리거트 내의 모든 연고나을 즉시 로딩으로 설정할 필요는 없다.
- 상태 변경을 위해 지연 로딩을 사용할 때 발생하는 추가 쿼리로 인한 실행 속도 저하는 보통 문제가 되지 않는다.
애그리거트의 영속성 전파
-
@Embeddable
타입에 대한 매핑은 함께 저장되고 삭제된다. -
@Entity
타입에 대한 매핑은 cascade 설정을 해야한다.
식별자 생성 기능
- 식별자 생성 규칙은 도메인 규칙이므로, 도메인 영역에 식별자 생성 기능을 위치시켜야 한다.
- 도메인 서비스, 리포티저리에서 식별자를 생성한다.
도메인 구현과 DIP
- 도메인 모델에서
@Entity
,@Table
를 사용 - 리포지터리 인터페이스도 도메인 패키지에 위치하는데, JPA Repository 를 상속받는다.
- 원칙적으로 도메인 영역은 구현 기술에 의존하면 안된다.
- 하지만 구현 기술은 거의 바뀌지 않는다. 타협이 필요하다.
스프링 데이터 JPA를 이용한 조회 기능
스펙
- Specification
- 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스
- List 를 리턴 받을 경우 COUNT 쿼리를 실행하지 않는다.
- 여러 스펙 조합시 스펙 필더 클래스를 만들어서 사용한다.
동적 인스턴스 생성
- JPA 쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있다.
- JPQL 의 select 절에 new 키워드 사용
하이버네이트 @Subselect
사용
- 뷰를 사용하는 것처럼 쿼리 실행 결과를 Entity 에 매핑 할 수 있다.
-
@Entity
에 다음 어노테이션을 적용한다.-
@Immutable
: 필드가 변경되었을 경우에 update쿼리가 수행되는 문제를 방지한다 -
@Subselect
: 조회 쿼리를 값으로 갖는다. 값으로 지정된 쿼리가 from 절의 서브 쿼리로 적용된다. -
@Synchronize
: 트랜잭션이 커밋되기 전에 다른 엔티티의 변경 내역을 동기화 하기 위해 사용한다.
-
응용 서비스와 표현 영역
- 표현 영역은 응용 서비스가 요구하는 형식으로 사용자 요청을 변환한다.
- 응용 서비스는 표현 영역에 의존하지 않는다.
응용 서비스의 역할
- 도메인 객체 간의 흐름을 제어
- 트랜잭션 처리
- 접근 제어
- 이벤트 처리
응용 서비스의 구현
- 표현 영역과 도메인 영역을 연결하는 파사드와 같은 역할
- 기능을 한 클래스에서 모두 구현
- 코드 중복을 제거하기 쉬움 (메소드 추출)
- 클래스 크기가 커짐
- 구분되는 기능별로 서비스 클래스를 구현
- 코드 품질을 일정 수준으로 유지하는데 도움
- 별도 클래스 (
~ServiceHelper
)에 로직을 구현해서 코드 중복 방지 가능
- 인터페이스가 필요한가?
- 응용 서비스는 런타임에 교체하는 경우가 거의 없고, 한 응용 서비스의 구현 클래스가 두 개인 경우도 드물다
- 표현 영역부터 개발을 시작한다면 인터페이스가 필요함
- 도메인 영역부터 개발을 시작한다면 응용 서비스가 먼저 만들어져서 필요 없음
- 응용 서비스에서 애그리거트를 리턴할 경우, 도메인 로직 실행을 응용 서비스와 표현 영역 두 곳에서 할 수 있게 된다
- 응용 서비스는 표현 영역에서 필요한 데이터만 리턴하는 것이 기능 실행 로직의 응집도를 높이는 확실한 방법이다
표현 영역의 역할
- 사용자가 시스템을 사용하도록 알맞은 흐름을 제공
- 사용자의 요청에 맞게 응용 서비스에 기능을 실행 (입력 변환)
- 응용 서비스의 실행 결과를 사용자에게 알맞은 형식으로 제공 (응답 변환)
- 세션 관리
값 검증
- 표현 영역, 응용 영역 두 곳에서 모두 수행할 수 있음
- 원칙적으로 모든 값에 대한 검증은 응용 서비스에서 처리한다.
- 표현 영역: 응용 서비스가 발생시킨 검증 에러 목록을 뷰에서 사용할 형태로 변환한다.
- 응용 서비스를 사용하는 표현 영역 코드가 한 곳이면
- 표현 영역: 필수 값, 값의 형식, 범위 등을 검증
- 응용 서비스: 데이터의 존재 유무와 같은 논리적 오류를 검증
- 저자는 가능하면 응용 서비스에서 필수 값 검증과 논리적인 검증을 모두 하는 편
권한 검사
- 도메인에 맞게 보안 프레임워크를 확장하려면 프레임워크에 대한 높은 이해가 필요하다.
- 이해도가 높지 않으면 권한 검사 기능을 직접 구현하는 것이 코드 유지 보수에 유리하다.
-
PermissionService
를 따로 구현
조회 전용 기능과 응용 서비스
- 조회 전용 기능은 트랜잭션이 필요하지 않다
- 조회를 위한 응용 서비스가 단지 조회 전용 기능을 실행하는 코드밖에 없다면 응용 서비스를 생략해도 무방하다.
도메인 서비스
- 한 애그리거트에 넣기 애매한 도메인 기능을 억지로 특정 애그리거트에 구현하면 안된다.
- 도메인 서비스는 도메인 영역에 위치한 도메인 로직을 표현할 때 사용
- 상태 없이 로직만 구현한다.
- 애그리거트 객체에 도메인 서비스를 전달하는 것은 응용 서비스 책임
- 애그리거트의 상태를 변경하거나 계산할 경우 도메인 서비스로 구현
- 외부 시스템과 연결된 도메인 서비스는 도메인 영역에 인터페이스로 선언하고, 인프라 스트럭처 영역에 구현 클래스를 위치시키며, 응용 서비스에서 사용한다.
애그리거트 트랜잭션 관리
- DBMS가 지원하는 트랜잭션과 함께 애그리거트를 위한 추가적인 트랜잭션 처리 기법이 필요
선점 잠금 (Pessimistic Lock)
- 먼저 애그리거트를 구한 스레드가 끝날 때 까지 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식
- JPA의
@Lock(LockModeType.PESSIMISTIC_WRITE)
사용 - 하이버네이트는
for update
쿼리를 사용해서 DBMS 가 제공하는 행단위 잠금을 사용한다
선점 잠금과 교착상태
- A -> B, B -> A 순으로 잠금 기능을 사용하는 두 스레드가 있을 경우 교착 상태가 발생한다
- JPA의
@QueryHint
를 사용해서javax.persistence.lock.timeout
힌트를 밀리초 단위로 지정한다.
비선점 잠금
- 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식
- JPA의
@Version
을 사용해서 엔티티의 버전을 관리한다. - 트랜잭션 충돌이 발생하면
OptimisticLockingFailureException
이 발생한다. - 비선점 잠금 방식을 여러 트랜잭션으로 확장하려면, 버전 정보도 사용자에게 전달해야 한다.
- 사용자가 전달한 버전과 현재 버전이 맞지 않을 경우 예외로 처리한다.
- 루트가 아닌 다른 엔티티가 수정될 경우
-
@Lock
어노테이션에 비선점 강제 버전 증가 잠금 모드LockModeType.OPTIMISTIC_FORCE_INCREMENT
를 사용한다.
-
오프라인 선점 잠금
- 여러 트랜잭션에 걸쳐 동시 변경을 막는다.
-
LockManager
와 같은 인터페이스를 정의하고, 별도의 구현을 통해서 잠금 기능을 구현한다.
도메인 모델과 바운디드 컨텍스트
바운디드 컨텍스트
- 경계를 갖는 컨텍스트
- 논리적으로 같은 존재처럼 보이지만 하위 도메인에 따라 다른 용어를 사용하는 경우, 한 개의 모델로 모든 하위 도메인을 표현 할 수 없다
- 하위 도메인마다 모델을 만들고, 명시적으로 구분되는 경계를 가져서 섞이지 않도록 해야한다
- 조직 구조에 따라 바운디드 컨텍스트가 결정된다.
- 물리적인 바운디드 컨텍스트가 한 개 이더라도, 내부적으로 패키지를 활용해서 논리적으로 바운디드 컨텍스트를 만든다.
바운디드 컨텍스트 구현
- 바운디드 컨텍스트는 도메인 기능을 제공하는 데 필요한 모든 요소를 포함한다
- 표현 영역, 응용 서비스, 도메인, 인프라스트럭처, DBMS
- 모든 바운디드 컨텍스트를 도메인 주도로 개발할 필요는 없다.
- 표현 영역, 서비스, DAO, DBMS
- 각 바운디드 컨텍스트는 서로 다른 구현 기술을 사용할 수도 있다.
바운디드 컨텍스트 간 통합
- 직접 통합
- 도메인 서비스를 인터페이스로 정의하고 인프라스트럭처 영역에 구현한다
- 모델 간 변환이 복잡하면 별도의 변환기(Translator)를 정의한다.
- 간접 통합
- 큐를 사용할 수 있다.
- 큐를 누가 제공하느냐에 따라 데이터 구조가 결정된다.
- 큐를 제공 받을 경우 비동기로 데이터를 전달하는 것 이외에 REST API 를 사용해서 데이터를 전달하는 것과 차이가 없다.
바운디드 컨텍스트 간 관계
- 가장 흔한 관계는 한쪽에서 API를 제공하고, 다른 한쪽에서 그 API를 호출하는 관계
- 하류는 상류에서 제공하는 데이터와 기능에 의존한다.
- 여러 하류 팀의 요구사항을 수용할 수 있는 API를 공개호스트서비스(OHS: Open Host Service)라고 한다.
- 상류 컴포넌트는 상류 바운디드 컨텍스트의 도메인 모델을 따른다.
- 하류 컴포넌트는 상류 서비스의 모델이 자신에게 영향을 주지 않도록 완충지대를 만들어야 한다.
- 안티코럽션 계층 (ACL: Anticoruption Layer)
- 인프라스트럭처 영역에 구현
- 두 바운디드 컨텍스트가 같은 모델을 공유하는 경우
- 두 팀이 공유하는 모델을 공유 커널(Shared Kernel)이라고 부른다
- 서로 통합하지 않는 방식: 동립 방식(Separate Way)
- 수동으로 바운디드 컨텍스트를 통합한다.
- 별도의 시스템을 만들어야 할 수도 있다.
컨텍스트 맵
- 바운디드 컨텍스트 간의 관계를 표시한다.
- 시스템 전체 구조를 보여준다.
이벤트
시스템 간 강결합 문제
- 도메인 객체에 서비스를 전달하면
- 트랜잭션 처리를 어떻게 해야 할지 애매하다
- 외부 서비스의 성능에 직접적인 영향을 받는다
- 서로 다른 두가지 로직이 섞이게 된다
- 기능 추가가 어렵다
- 비동기 이벤트를 사용하면 두 시스템 간의 결합을 크게 낮출 수 있다.
이벤트 개요
- 이벤트: 과거에 벌어진 어떤 것
- 생성 주체: 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체
- 이벤트 핸들러: 이벤트 생성 주체가 발생한 이벤트에 반응
- 이벤트 디스패처: 생성 주체와 핸들러를 연결
- 구조
- 이벤트 종류
- 이벤트 발생 시간
- 추가 데이터
- 이벤트 이름은 과거 시제를 사용
- 용도
- 트리거
- 서로 다른 시스템간의 데이터 동기화
이벤트 구현
- 스프링에서 제공하는
ApplicationEventPublisher
사용- 스프링 컨테이너는
ApplicationEventPublisher
를 구현하고 있다.
- 스프링 컨테이너는
-
publishEvent()
를 사용해서 이벤트를 발생시킨다. -
@EventListener
를 사용해서 이벤트를 처리한다. - 응용 서비스와 동일한 트랜잭션 범위에서 이벤트 핸들러를 실행한다. (동기)
동기 이벤트 처리 문제
- 외부 서비스의 성능 저하가 내 시스템의 성능 저하로 연결된다.
비동기 이벤트 처리
-
A 하면 이어서 B 하라
->A 하면 최대 언제까지 B 하라
로 바꿀 수 있다. - 로컬 핸들러 비동기 실행
-
@EnableAsync
를 설정하고, 핸들러에@Async
를 지정한다.
-
- 메시징 시스템을 이용한 비동기 구현
- Kafka, RabbitMQ 와 같은 메시징 시스템을 사용한다.
- 글로벌 트랜잭션을 사용해야 될 수 있다.
- 이벤트 저장소를 이용한 비동기 처리
- 이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용해서 이벤트 핸들러에 전달한다.
- 도메인 상태 변화와 이벤트 저장이 한 트랜잭션으로 처리된다.
- 포워더: 이벤트를 주기적으로 읽어와서 핸들러에 전달한다. 어디까지 전달했는지 추적한다.
- API: 이벤트 목록을 제공한다. 클라이언트가 어디까지 처리했는지 기억해야한다.
이벤트 적용시 추가 고려사항
- 이벤트 소스를 저장할지 여부
- 포워더에서 전송 실패를 얼마나 허용할 것인지
- 이벤트 손실 (로컬 핸들러를 이용해서 비동기로 처리하는 도중 실패하면 이벤트를 유실하게 된다)
- 이벤트 순서 (메시징 시스템은 발생 순서와 전달 순서가 다를 수 있다)
- 이벤트 재처리
이벤트 트랜잭션
- 주문 취소로 외부 시스템에 결제 취소를 했는데 -> 주문 상태 업데이트 실패한 경우 문제
- 주문 취소로 주문 상태 업데이트를 했는데 -> 외부 시스템 결제 취소가 실패한 경우 문제
- 트랜잭션이 성공할 때만 이벤트 핸들러를 실행
- 스프링이 제공하는
@TransactionalEventListener
의phase = TransactionPhase.AFTER_COMIT
사용 - 이제 이벤트 처리 실패만 고민하면 된다
CQRS
- 화면 조회시 여러 애그리거트의 데이터가 필요한 경우
- 식별자를 이용해서 애그리거트를 참조하는 방식을 사용하면 즉시 로딩 방식과 같은 JPA의 쿼리 관련 최적화 기능을 사용할 수 없다.
- 이러한 고민의 원인은 변경과 조회에 단일 도메인 모델을 사용하기 때문
- 해결책은 상태 변경을 위한 모델(Command Model)과 조회를 위한 모델(Query Model)을 분리하는 것
- 조회 기능 때문에 도메인 모델이 복잡해지는 것을 막을 수 있다.
- 조회 모델에는 응용 서비스 없이 DAO를 실행한다.
- 명령 모델과 조회 모델이 서로 다른 데이터 저장소를 사용할 수도 있다.
- 이벤트를 활용
- 동기 이벤트와 글로벌 트랜잭션을 사용해서 실시간으로 동기화 할 수 있다.
- 특정 시간 안에만 동기화해도 된다면 비동기로 데이터를 전송하면 된다.
- 트래픽이 높은 서비스인데 단일 모델을 고집하면 유지 보수 비용이 오히려 높아질 수 있으므로 CQRS 도입을 고려한다
형님 좋은거 배워 갑니다. 후배들에게 좀 알려주고 싶네요.