[DDD] DDD START! 정리
내용 정리
- 유튜브 : DDD START! 도메인 주도 설계 구현과 핵심 개념 익히기 (최범균)
- 영상 출처
정의
DDD(Domain Driven Design) - 도메인 주도 설계란?
- 설계 방법론 중 하나로 비즈니스 도메인별로 설계하는 방법
특징
MSA와 관계
- MSA의 특성이 Bounded Context를 구성 또는 언어의 경계를 찾는데 도움을 줌
MSA(Micro Service Architecture)의 특성
- 서비스로서 컴포넌트화
- 독립 배포 -> 서비스의 응집도(경계) 높아짐
- 명시적 인터페이스 -> 공개 인터페이스
- 비즈니스 수행에 따른 구성
- Conway 법칙
- 우리가 만드는 서비스(시스템 구조)는 조직(기업)의 형태를 따라가게 됨
- Conway 법칙
- 분산화된 데이터 관리
- DB가 하나라면 조인 쿼리를 통해 데이터를 가져오게 됨
- 굳이 컴포넌트 서비스 엔드포인트를 호출하지 않으려 함
- DB의 물리적, 논리적 분리를 통해 도메인(모듈, 서비스 등) 경계 구분
- DB가 하나라면 조인 쿼리를 통해 데이터를 가져오게 됨
- 진화하는 설계
- 서비스별 독립적인(목적, 빈도 등) 변경(교체, 업그레이드)
- 프로젝트가 아닌 제품
- 똑똑한 엔드 포인트와 더미 파이프
- 분산화 거버넌스
- 인프라 자동화
- 장애 방지 설계
Event Sourcing & Aggregate
- 상태 변화 증분 = 이벤트
- 증분 누적 -> 마지막 상태
이벤트 소싱
- 애플리케이션의 모든 상태 변화를 순서에 따라 이벤트로 보관
- 이벤트의 순서를 따라 실행해보면 최종 상태를 획득하게 됨
도메인 객체와 이벤트 소싱
- 도메인 객체 조회
- 저장된 이벤트로부터 도메인 객체 생성
- 도메인 객체 변경
- 모든 상태 변화에 대한 이벤트를 저장
- 일관성의 기준인 애그리거트 단위로 이벤트 소스를 생성
- 애그리거트(Aggregate)의 경계를 찾는데 도움을 줌
이벤트 구성
- 구성
- 애그리거트 타입
- 애그리거트 식별자
- 버전
- 이벤트 타입
- 이벤트 시간
- 증분(변경) 내역(payload)
- 이벤트 구분(unique idx)
- 애그리거트 타입, 애그리거트 식별자, 버전
애그리거트
애그리거트 조회
리포지토리
public class OrderRepository {
public Order findById(String orderId) {
List<Event> events = eventsStore.select(Order.class, orderId);
if (events.isEmpty()) {
return null;
}
Order order = new Order();
for (Event event : events) {
order.handle(event);
}
addUOW(order); // Unit Of Work - 트랜잭션 범위에 Order 삽입
return order;
}
}
애그리거트는 이벤트 반영
public abstract class Aggregate {
private Integer version;
public void handle(Event event) {
this.version = event.getVersion();
Events.handle(this, event);
}
}
public class Order extends Aggregate {
public void on(OrderPlacedEvent event) {
this.lines = event.getOrderLines();
this.shippingInfo = event.getShippingInfo();
}
public void on(ShippingInfoChangedEvent event) {
this.shippingInfo = event.getShippingInfo();
}
public void on(ShippedEvent event) {
this.state = SHIPPED;
}
}
애그리거트 변경
애그리거트에서 이벤트 발생
public abstract class Aggregate {
List<Event> uncommittedEvents;
public void apply(Event event) {
// 커밋되지 않은 이벤트 목록에 저장
addUncommittedEvent(event);
// 이벤트 적용
handle(event);
}
public List<Event> getUnCommittedEvent() {
return uncommittedEvents;
}
}
public class Order extends Aggregate {
public void cancel() {
// 바로 상태 변경을 하지 않고, 상태 변경 이벤트를 생성해 상위에 전달
...
OrderCanceledEvent event = new OrderCanceledEvent(getId(), version + 1);
super.apply(event);
}
}
트랜잭션에서 이벤트 저장
public class CancleOrderService {
@Transactional
public void cancel(String orderId) {
Order order = orderRepository.findById(orderId);
checkNull(order);
order.cancel();
}
}
class TransactionalHandler {
public void commit() {
// uncommitted 목록에 있던 모든 이벤트를 읽어 이벤트 스토어에 모두 저장
List<Aggregate> aggs = getAllUOW();
for (Aggregate agg : aggs) {
eventStore.append(agg.getClass(), agg.getUncomittedEvents());
}
}
}
애그리거트 생성
애그리거트 생성자 : 생성 이벤트 발생
public class Order extends Aggregate {
public Order(...) {
...
OrderCreatedEvent event = new OrderCreatedEvent(getId(), lines, shippingInfo, 1);
super.apply(event);
}
}
public class PlaceOrderService {
@Transactional
public void cancel(String orderId) {
Order order = new Order(...);
repository.save(order);
}
}
이벤트 소싱과 유지보수
새로운 데이터 추가
public class Order extends Aggregate {
private String note;
public Order(...) {
...
OrderCreatedV2Event event = new OrderCreatedV2Event(...);
super.apply(event);
}
public void on(OrderCreatedEvent event) {
...
this.note = "";
}
public void on(OrderCreatedV2Event event) {
...
this.note = event.getNote();
}
}
- 없어지는 것
- DB 작업 일정
- 매핑 설정 변경
이벤트 소싱 적용한 애그리거트
- 임피던스 미스매치(impedance mismatch) 없음
- 코드의 도메인 모델과 DB의 데이터 모델의 불일치 간극을 줄이는 작업이 필요 없음
- 애그리거트 간 참조는 ID
- 객체 레퍼런스보다는 ID 참조
- 비선점 잠금
- 낙관적 잠금(Optimistic lock)이 자연스럽게 적용
성능
- 단일 애그리거트 조회 -> 스냅샷
- 이벤트가 많아지면 이벤트의 개수 등을 기준으로 스냅샷을 저장하여 조회
- 복합 조회 -> CQRS
SQL(ORM) vs 이벤트 소싱
- 변화되는 것
- 적용 전 (RDBMS/ORM)
- 메인 객체 로딩
- SQL : Select 쿼리
- ORM : 프레임워크가 Select 쿼리 실행(매핑 설정을 이용)
- 메인 기능
- SQL : 서비스 클래스
- ORM : (일부) 엔티티 클래스
- 상태 변경 반영 (데이터 변경)
- SQL : Insert/Update/Delete 쿼리
- ORM : 엔티티 프로퍼티를 변경하면 ORM 프레임워크가 알맞은 쿼리 실행
- 메인 객체 로딩
- 적용 후
- 메인 객체 로딩
- 이벤트로부터 도메인 객체 생성
- 도메인 객체의 이벤트 핸들러를 이용해 상태 변경 반영
- 메인 기능
- 도메인 객체가 수행
- 상태 변경을 위한 이벤트 생성
- 상태 변경 반영 (데이터 변경)
- 도메인 객체가 발생한 이벤트를 저장소에 보관
- 메인 객체 로딩
- 적용 전 (RDBMS/ORM)
장점
- DB에 의존적이지 않은 도메인 코드 구현
- 테이블, ORM 기술의 제약에서 벗어남
- 기능 변경
- 하위 호환 처리가 상대적으로 쉬움
- 이벤트로부터 완전히 새로운 도메인 객체의 생성도 가능
- 버그 추적 용이
- 이벤트를 차례대로 검사하면서 버그 원인 추적 가능
- 객체 지향/DDD와 좋은 궁함
- 복잡한 도메인을 객체 지향적으로 구현하기에 좋음
- CQRS와 좋은 궁합
- 조회 관련 코드를 도메인에서 분리
- 조회 모델 분리로 조회 성능 향상 가능
단점
- 익숙하지 않음
- 기존에는 SQL(DB 데이터 중심) 개발 성향이 다수
- 단순 모델에는 적합하지 않음
- 구현이 복잡해짐
- 도구 부족
- 이벤트 소싱과 CQRS 지원 프레임워크 부족
- Java Axon 프레임워크
- 운영시 어려움
- 이벤트 데이터만으로는 최신 상태의 빠른 확인 불가능
- CQRS 필수
SAGA
- 여러 하위 트랜잭션 집합으로 구성된 LLT
- LLT : Long Lived Transaction
- 단위 실행 단위
- 각 하위 트랜잭션은 단독 트랜잭션
- 따라서 하위 트랜잭션 단위로 일관성 보장
- 각 하위 트랜잭션은 서로 연관
- 하위 트랜잭션 실패시, 보상 트랜잭션
- 일부만 성공해도 끝나지 않음
Long running process
- 상황, 문맥 등에 따라 SAGA와 동일한 개념으로 보는 경우도 있음
길게 실행되는 프로세스/트랜잭션 특징
- 여러 개의 작은 프로세스(트랜잭션)로 구성
- 하위 프로세스가 순서대로 실행되지 않을 수 있음
- 여러 모듈/서비스가 (비동기로) 엮임
- 한 트랜잭션이 아닐 수 있음
- 실패는 롤백 대신 보상(compensation)으로 처리
프로세스 매니저
중계, 프로세스 상태 보관, 지시 및 알림
- 여러 애그리거트/외부 시스템이 엮이는 건 프로세스의 흐름 제어
- 각 구성 요소 간의 메시지 라우팅이 주된 역할
- 프로세스의 상태 보관
- 비즈니스 로직은 개별 구성 요소에서 처리
구현
- 특정 이벤트에 SAGA/프로세스 매니저 시작
- 수신한 이벤트에 따라 다음 기능 실행
- 실패시 후속 보상 기능 실행
- 전체 프로세스가 끝나면 SAGA/프로세스 매니저 종료
상태 존재 -> 프로세스별 인스턴스
에어카텔 상품 구매 예시
public class BookingProcessManager {
private BookingId bookingId;
private BookingState hotelState;
private BookingState airState;
private BookingState carState;
public void handle(BookingCreatedEvent event) {
this.bookingId = event.getBookingId();
sendBookingRequest();
}
public void handle(HotelBookedEvent event) {
hotelState = BookingState.BOOKED;
notifyIfAllBooked();
}
private void notifyIfAllBooked() {
if (hotelState == BOOKED && airState == BOOKED && carState == BOOKED) {
notifyBookingCompletion();
}
}
}
실패 보상 처리
에어카텔 호텔 예약 실패 예시
public class BookingProcessManager {
private BookingId bookingId;
private BookingState hotelState;
private BookingState airState;
private BookingState carState;
public void handle(HotelBookingFailedEvent event) {
cancelAirBooking();
cancelCar();
}
private void cancelAirBooking() {
if (airState == BOOKED) {
cancelAirBooking();
} else {
airState = CANCEL_REQUIRED;
}
}
public void handle(AirBookedEvent event) {
if (airState == CANCEL_REQUIRED) {
cancelAirBooking();
} else {
airState = BOOKED;
notifyIfAllBooked();
}
}
}
필요한 것
- SAGA/매니저 상태 영속화
- 상태가 유실되지 않아야 함
- 이벤트 -> 해당 SAGA/매니저
- 효과적인 SAGA 검색 수단(+매핑 인덱스) 필요
- SAGA 타임아웃
특징
- 개별 애그리거트는 자신의 도메인 로직에만 집중하면 됨
- 한 비즈니스 프로세스와 관련된 흐름 제어가 한 곳에 모임
- 프로세스 변경 용이
- (주로) 비동기 이벤트 기반
- SAGA/프로세스 매니저 인스턴스 관리 위한 기반 프레임워크 필요
댓글남기기