DDD START! 도메인 주도 설계 구현과 핵심 개념 익히기 책 내용 요약 정리 내용입니다.
- 저자
- 최범균
- 출판
- 지앤선
- 출판일
- 2016.05.27
# 도메인 모델
도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념모델
개념 모델을 이용해서 바로 코드를 작성할 수 있는 것은 아니기에 구현 기술에 맞는 구현 모델 따로 필요함
## 도메인 모델 패턴 아키텍처 구성
표현 -> 응용 -> 도메인 -> 인프라스트럭처 -> 데이터베이스, 메시징 시스템
## 엔티티와 밸류
엔티티 객체마다 고유한 식별자는 갖는다
밸류는 엔티티 필드를 의미에 맞게 생성함 (배송지 등)
도메인 모델에 습관적인 get/set 메서드를 넣지 말자
# 아키텍처 개요
표현 영역
- (웹 애플리케이션이라면) HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환해서 전달
- 응용 영역 응답을 HTTP 응답으로 변환해서 전송함
응용 서비스
- 표현 영역을 통해 사용자 요청 전달받아 사용자에게 제공해야할 기능 구현
- 기능 구현을 위해 도메인 영역의 도메인 모델을 사용
- 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임함
도메인 영역
- 도메인의 핵심 로직을 구현하는 도메인 모델을 구현한다
인프라스트럭처 영역
- 구현 기술에 대한 것을 다룬다
- RBDMS 연동, 메시징 큐 메시지 전송/수신 등
도메인/응용/표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다
## DIP (Dependency Inversion Principal)
고수준 모듈이 저수준 모듈에 의존성이 생기는 것을 막기 위한 방법
응용 서비스가 인프라스트럭처의 구현 기술에 종속적인 인터페이스를 사용하면
저수준 모듈에 의존성이 생기게 된다
고수준 모듈의 관점에서 저수준 모듈에서 구현할 인터페이스를 정의하여
이 문제를 해결하는 방법을 DIP라고 한다.
## 도메인 영역의 주요 구성요소
- 엔티티
- 밸류
- 애그리거트
- 리포지터리
- 도메인 서비스
## 모듈 구성
아키텍처의 각 영역은 별도 패키지에 위치
ui, application, domain, infrastructure
도메인이 크면 하위 도메인으로 나누고 각 하위 도메인에서 다시 패키지로 분리
# 애그리거트
한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다.
도메인 규칙에 따라 함께 생성되는 구성요소는 한 애그리거트에 속할 가능성이 높음
함께 생성되고 함께 변경되는 엔티티가 같이 묶여야한다.
처음 도메인 모델을 만들기 시작하면 큰 애그리거트로 보이는 것이 많지만
경험이 생기고 규칙을 제대로 이해할수록 크기는 줄어든다.
다수의 애그리거트가 하나의 엔티티 객체만 갖고 드물게 두개 이상 갖는다.
## 애그리거트 루트
애그리거트 루트는 다른 연관 엔티티를 가지고 있는 엔티티
애그리거트 루트의 핵심 역할은 애그리거트의 일관성 유지다.
이를 위해 애그리거트가 제공해야 할 도메인 기능을 구현한다.
애그리거트 루트가 아닌 다른 객체가 애그리거트에 속한 객체를 직접 변경하면 안된다.
다음 두 가지를 습관적으로 적용
- 단순 set 메서드를 public 범위로 만들지 않는다
- 밸류 타입은 불변으로 구현한다
## 트랜잭션 범위
범위는 작을수록 좋다.
한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다.
두 개 이상 수정 시 트랜잭션 충돌 발생 가능성이 더 높아져서 전체 처리량이 떨어진다.
도메인 이벤트를 사용해서 한 트랜잭션에서 다른 애그리거트 변경 처리를 할 수 있다.
## 리포지터리와 에그리거트
리포지터리는 애그리거트 단위로 존재함
애그리거트내 엔티티를 물리적으로 별도 테이블에 저장하더라도 리포지터리는 같이 사용
리포지터리 구현 기술에 따라 애그리거트 구현도 영향을 받는다.
## ID를 이용한 애그리거트 참조
한 애그리거트에서 다른 애그리거트 루트를 가지고 있는 것은 좋지 않음
- 한 트랜잭션에서 여러 애그리거트 변경 가능
- 성능 관련 고민 필요 (JPA 사용 시 지연 로딩, 즉시 로딩 결정 필요 등)
- 확장성 (도메인마다 다른 DBMS나 기술 사용 불가)
ID를 이용해서 다른 애그리거트 참조하면 이런 문제 완화 가능함!
### 조회 성능
N+1 조회 문제 발생 가능해서 전용 조회 쿼리 사용 필요하다.
세타 조인을 사용해서 한 번의 쿼리로 필요한 데이터 로딩하는 방식
## 애그리거트를 팩토리로 사용
애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성한다면
애그리거트에 팩토리 메서드를 구현하는 것을 고려해보자.
(예, 상점 애그리거트에서 상품 애그리거트 생성)
# 리포지터리와 모델구현(JPA 중심)
인터페이스는 애그리거트 루트를 기준으로 작성
@AttributeOverrides
AttributeConverter interface
@ElementCollection, @CollectionTable : 밸류 컬렉션을 별도 테이블로 매핑할 때 사용
밸류 타입으로 식별자 구현하면 식별자에 기능 추가할 수 있는 장점있음
별도 테이블로 저장되고 테이블에 PK가 있다고 테이블과 매핑되는
애그리거트 구성요소가 고유 식별자를 갖는 것은 아니다.
@SecondaryTable
# 리포지터리의 조회 기능(JPA 중심)
CriteriaBuilder, Predicate
JPA 정적 메타 모델
@StaticMetamodel
## 리포지터리 구현 기술 의존
도메인 모델은 구현 기술에 의존하지 않아야 한다.
스펙 구현을 추상화하기 위해 많은 노력이 필요한데 실제 얻는 이점이 크지 않음
왜냐하면 리포지터리 구현 기술을 바꿀 정도의 변화는 드물기 때문
@Subselect, @Synchronize
# 응용 서비스와 표현 영역
## 응용 서비스
사용자 요청 처리를 위해 리포지터리로부터 도메인 객체를 구하고 도메인 객체를 사용한다.
주로 도메인 객체 간의 흐름 제어를 하므로 단순한 로직을 갖는다.
주된 역할 중 하나는 트랜잭션 처리이다.
응용 서비스에는 도메인 로직을 넣지 않는다. (도메인 영역에 넣기)
넣으면 코드 품질에 문제 발생 (응집성 떨어짐, 중복 코드 발생)
응용 서비스 역할 중 하나는 도메인 영역에서 발생시킨 이벤트를 처리하는 것
## 표현 영역
원칙적으로 모든 값에 대한 검증은 응용 서비스에서 처리한다.
하지만 표현 영역은 잘못된 값 존재 시 사용자에게 알려주고 다시 입력받아야 함
응용 서비스에서 각 값 항목에 대해 형식 확인 후 예외 발생 시키면 사용자는 한번에
하나의 값 오류가 있다는 것만을 알게되어 UX가 좋지 않다.
=> 응용 서비스에 값 전달 전에 표현 영역에서 모든 값을 검사한다.
표현 영역에서 필수 값과 값의 형식을 검사하면
응용 서비스는 아이디 중복 여부와 같은 논리적 오류만 검사하면 된다.
표현 영역 : 필수 값, 값의 형식, 범위 등 검증
응용 서비스 : 데이터의 존재 유무와 같은 논리적 오류 검증
경험상 응용 서비스 실행 주체가 표현 영역이면 논리적 오류 위주로 검증해도 문제없었지만
주체가 다양하면 응용 서비스에서 반드시 논리적 외의 오류도 검사해야 한다.
# 도메인 서비스
한 애그리거트에 넣기 애매한 도메인 기능(예, 결제 금액 계산 기능은 주문/회원/쿠폰 도메인 필요)을 특정 애그리거트에 억지로 구현하면 안된다. (코드가 길어지고 외부 의존도가 높아짐)
복잡도가 높아지고 수정이 어려워진다.
이런 경우 도메인 서비스를 별도로 구현하는게 가장 쉬운 방법이다.
도메인 서비스 클래스를 구현하고 (예, DiscountCalculationService)
애거리거트의 기능 메소드에서 도메인 서비스 객체를 받아 도메인 로직을 구현한다.
도메인 서비스 객체 생성해서 애그리거트에 전달하는 것은 응용 서비스의 책임
도메인 서비스는 도메인 로직을 수행하지 응용 로직을 수행하지는 않는다. 트랜잭션 처리와 같은 로직은 응용 서비스에서 처리해야 한다.
# 애그리거트 트랜잭션 관리
애그리거트 트랜잭션을 위해서는 DBMS 트랜잭션과 함께 추가적인 트랜잭션 처리 기법이 필요하다.
## 선점 잠금 (비관적 락)
보통 DBMS의 행 단위 잠금(for update) 사용해서 구현한다.
JPA에서는 find 메서드에 LockModeType.PESSIMISTIC_WRITE 전달해서 적용
데드락 방지를 위해 javax.persistence.lock.timeout 값 활용 가능
## 비선점 잠금 (낙관적 락)
선점 잠금 방식으로 모든 트랜잭션 충돌 문제 해결할 수 없음
선점 잠금 방식만으로는 update 처리 스레드 간의 충돌 문제만 방지 가능
수정 폼 -> 수정 요청 흐름의 전체적인 잠금이 되지는 않기 때문
version 필드 테이블에 추가해서 수정 시 마다 +1하고 조회한 version과 update 시 version이 동일한지 체크하는 방법
트랜잭션 충돌 시 OptimisticLockingFailureException 발생
이 방법은 위 두 가지 케이스에 모두 적용 가능함
(수정 폼에 버전 정보 같이 전달)
### 강제 버전 증가
애그리거트 루트 외의 다른 엔티티의 값만 변경되는 경우 루트 엔티티의 버전 증가하지 않음 (JPA 특징)
증가하는게 애그리거트 관점에서 맞으므로 find 시 LockModeTypel.OPTIMISTIC_FORCE_INCREMENT 전달하여 강제 버전 증가 가능함
## 오프라인 선점 잠금
여러 사용자가 한 문서를 동시 수정하는 것을 방지하는 것(수정 화면 동시 진입 막기)은
선점 잠금, 비선점 잠금으로는 불가능, 오프라인 선점 잠금으로는 가능하다.
잠금 획득 후 해제하지 않고 프로그램 종료할 수 있기 때문에
잠금의 유효 기간을 두고 주기적으로 연장하는 방식으로 해야한다.
DB로 락 정보를 만들어서 처리하는 방식으로 구현할 수 있다.
# 도메인 모델과 BOUNDED CONTEXT
하위 도메인마다 같은 용어라도 의미가 다르고 같은 대상이어도 지칭하는 용어가
다를 수 있음
따라서 한 개의 모델로 모든 하위 도메인을 표현하는 것은 어렵고 하위 도메인마다
모델을 만들어야 한다.
(예, 회원의 Member는 애그리거트 루트지만 주문의 Orderer는 밸류임)
모델은 특정한 컨텍스트하에서 완전한 의미를 갖는다. 이 경계를 BOUNDED CONTEXT라고 한다.
BOUNDED CONTEXT는 표현 영역, 응용 서비스, 인프라 영역 등을 모두 포함한다.
(도메인 모델만 포함하는 것이 아님)
모든 BOUNDED CONTEXT를 도메인 주도로 개발할 필요는 없다.
상품 리뷰 같은 단순한 도메인은 CRUD 방식으로 구현해도 유지보수하는데 큰 문제가 없다.
## BOUNDED CONEXT 간 통합, 관계
## 컨텍스트 맵
BOUNDED CONTEXT 간의 전체적인 관계를 표현한 맵. 시스템 전체 구조를 보여줌
# 이벤트
도메인 모델에 이벤트 도입을 위해 4개 구성요소를 구현해야 한다.
- 이벤트
- 이벤트 생성 주체
- 이벤트 디스패처
- 이벤트 핸들러
이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다.
## 비동기 이벤트 처리
외부 시스템 연동을 동기로 할 때 성능, 트랜잭션 범위 문제를 해소하기 위해 비동기 처리하는 방법이 있다.
이벤트 비동기 구현 방법
- 로컬 핸들러를 비동기로 실행하기
- 메시지 큐를 사용하기
- 이벤트 저장소와 이벤트 포워더 사용하기
- 이벤트 저장소와 이벤트 제공 API 사용하기
# CQRS
Command Query Responsibillity Segregation
상태 변경을 위한 모델과 조회를 위한 모델을 분리하는 것
시스템 상태 변경할 때와 조회할 때 단일 도메인 모델을 사용하면
JPA 즉시 로딩, 지연 로딩 관련 고민거리가 생긴다.
JPA는 도메인 상태 변경 구현할 때는 적합하지만 여러 애그리거트를 조회하는 것은 고려할게 많아 구현을 복잡하게 한다.
복잡한 도메인에 적합하다.
예) 상태 변경을 위한 도메인 모델 : Order (Orderer, OrderLine, ShippingInfo 등 포함)
주문 목록 조회를 위한 모델 : OrderSummary (주문 관련 요약 정보만 포함)
명령 모델과 조회 모델이 서로 다른 DB를 사용하게 구현할 수도 있다.
두 DB 간 데이터 동기화는 이벤트를 활용해서 처리한다. (상황에 따라 동기, 비동기 이벤트로 처리)
## 장단점
장점
- 명령 모델 구현 시 도메인 자체에 집중할 수 있다
- 조회 성능을 향상시키는데 유리하다
단점
- 구현해야 할 코드가 더 많다
- 더 많은 구현 기술이 필요하다
이런 장단점을 고려해서 도입 여부를 결정해야 한다.
'개발' 카테고리의 다른 글
NGINX rate limit 기능이 뭔가요? (0) | 2023.09.08 |
---|---|
Colima Docker DNS 오류 (0) | 2023.01.31 |
Dockerfile 에서 CMD, ENTRYPOINT 사용시 환경 변수 로딩 문제 (0) | 2022.01.14 |
자주 사용하는 Docker 명령어 모음 (0) | 2021.10.22 |
카프카, 데이터 플랫폼의 최강자 - 6장 카프카 운영 가이드 (0) | 2020.09.12 |