[DDD] DDD START! 정리

내용 정리

  • 유튜브 : DDD START! 도메인 주도 설계 구현과 핵심 개념 익히기 (최범균)
  • 영상 출처

정의

DDD(Domain Driven Design) - 도메인 주도 설계란?

  • 설계 방법론 중 하나로 비즈니스 도메인별로 설계하는 방법

특징

MSA와 관계

  • MSA의 특성이 Bounded Context를 구성 또는 언어의 경계를 찾는데 도움을 줌

MSA(Micro Service Architecture)의 특성

  • 서비스로서 컴포넌트화
    • 독립 배포 -> 서비스의 응집도(경계) 높아짐
    • 명시적 인터페이스 -> 공개 인터페이스
  • 비즈니스 수행에 따른 구성
    • Conway 법칙
      • 우리가 만드는 서비스(시스템 구조)는 조직(기업)의 형태를 따라가게 됨
  • 분산화된 데이터 관리
    • 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 프레임워크가 알맞은 쿼리 실행
    • 적용 후
      • 메인 객체 로딩
        • 이벤트로부터 도메인 객체 생성
        • 도메인 객체의 이벤트 핸들러를 이용해 상태 변경 반영
      • 메인 기능
        • 도메인 객체가 수행
        • 상태 변경을 위한 이벤트 생성
      • 상태 변경 반영 (데이터 변경)
        • 도메인 객체가 발생한 이벤트를 저장소에 보관

장점

  • 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/프로세스 매니저 인스턴스 관리 위한 기반 프레임워크 필요

댓글남기기