[MSA] 05. 비즈니스 로직 설계

Jimin Lim·2023년 12월 28일
0

Architecture

목록 보기
16/23

05. 비즈니스 로직 설계

비즈니스 로직이 여러 서비스에 흩어져 있는 마이크로서비스 아키텍처는 복잡한 비즈니스 로직을 개발하기 까다롭다. 문제는 다음 두가지가 있다.

  1. 도메인 모델은 대부분 상호 연관된 클래스가 복잡하게 얽혀 있다.
  2. 트랜잭션 관리 제약 조건하에서도 작동되는 비즈니스 로직을 설계해야 한다. (사가 패턴 필요)

이 문제는 DDD 애그리거트 패턴으로 해결할 수 있다.

  • 애그리거트 패턴: 한 단위로 취급 가능한 객체를 모아 놓은 것
  • 애그리거트 패턴의 장점
    • 객체 레퍼런스가 서비스 경계를 넘나들 일이 없다. 객체 참조 대신 PK를 이용해 애그리거트가 서로 참조하기 때문
    • 한 트랜잭션으로 하나의 애그리거트만 생성/수정할 수 있다.

5.1 비즈니스 로직 구성 패턴

비즈니스 로직 설계: 트랜잭션 스크립트 패턴

: 절차적인 코드 작성, 객체 지향 설계를 하지 않는 것

비즈니스 로직 설계: 도메인 모델 패턴

: 객체 지향 설계, 비즈니스 로직을 상태와 동작을 가진 클래스로 구성된 객체 모델로 구성

DDD

  • Entity: 영속성 신원을 가진 객체, JPA에서 @Entity
  • Value Object: 여러 값을 모아 놓은 객체, 속성 값이 동일한 두 밸류 객체는 서로 바꾸어 사용 가능
  • Factory: 일반 생성자로 직접 만들기에 복잡한 객체 생성 로직이 구현된 객체 또는 메서드
  • Repository: 엔티티를 저장하는 DB 접근 로직을 캡슐화한 객체
  • Service: 비즈니스 로직 구현 객체

5.2 도메인 모델 설계: DDD 애그리거트 패턴

불분명한 경계 문제

전통적인 객체 지향 설계에 기반하면 경계가 불분명하게 된다..

애그리거트는 경계가 분명하다

애그리거트는 한 단위로 취급 가능한 경계 내부의 도메인 객체들이다. 하나의 루트 엔터티와 하나 이상의 기타 엔터티 + 밸류 객체로 구성된다.

애그리거트 규칙

  1. 애그리거트 루트만 참조하라
  2. 애그리거트 간 참조는 반드시 기본키를 사용하라
  3. 하나의 트랜잭션으로 하나의 애그리거트를 생성/수정하라

애그리거트 입도

애그리거트는 작을 수록 좋지만 애그리거트 자체가 곧 트랜잭션 범위라 크게 잡아야할 수도 있다.

뭐 그래도 가급적 잘게 나누는 것이 최선이란다!

비즈니스 로직 설계: 애그리거트

  • 사가: 로컬 트랜잭션을 오케스트레이션하여 데이터 일관성을 맞춘다.
  • 인바운드 어댑터: 서비스 호출
  • 아웃바운드 어댑터: 서비스는 DB에서 애그리거트를 조회/저장하며, 접근은 아웃바운드 어댑터로 구현한다.

5.3 도메인 이벤트 발행

애그리거트는 상태가 전이될 때마다 이에 관련된 컨슈머를 위해 이벤트를 발행한다.

도메인 이벤트란 무엇인가?

  • 도메인 이벤트: 과거 분사형 동사로 명명한 클래스 (OrderCreatedEvent 같이..)
intetface DomainEvent {}

interface OrderDomainEvent extends DomainEvent{}

class OrderCreatedEvent implements OrderDomainEvent {}

interace DomainEventEnvelope<T extends DomainEvent> {
  String getAggregateId();
  Message getMessage();
  String getAggregateType();
  String getEventId();
  T getEvent();
}
  • DomainEvent: 자신을 구현한 클래스가 도메인 이벤트임을 알리는 마커 인터페이스
  • OrderDomainEvent: Order 애그리거트가 발행한 OrderCreatedEvent의 마커
  • DomainEventEnvelope: 이벤트 객체 및 메타데이터 조회

이벤트 강화

(주문 이벤트 예시) 이벤트 컨슈머가 이벤트를 받아 처리하려면 주문 내역이 필요하다. 이때 필요한 정보를 OrderService 에서 직접 가져와도 되지만 이벤트 컨슈머가 서비스를 쿼리하는 것은 오버헤드를 유발한다.

따라서 컨슈머에 필요한 정보를 이벤트가 갖고 다니는 이벤트 강화 기법을 적용한다.

class OrderCreatedEvent implements OrderEvent {
    //...
    private DeliveryInformation deliveryInformation; //컨슈머가 필요로 하는 데이터 
}

단점은 컨슈머 요건이 바뀌면 이벤트 클래스도 함께 바꿔야한다는 점 .. 흠

도메인 이벤트 식별

  1. 이벤트 브레인스토밍
  2. 이벤트 트리거 식별: 사용자 액션, 외부 시스템, 기타 도메인 이벤트, 시간 경과 등 각각의 이벤트를 일으키는 트리거 식별
  3. 애그리거트 식별: 각 커맨드 소비 후 적절한 이벤트를 발생시키는 애그리거트 식별

도메인 이벤트 생성 및 발행

도메인 이벤트 생성

도메인 이벤트는 애그리거트가 발행한다. 애그리거트는 자신의 상태가 변경되는 시점과 그 결과 어떤 이벤트를 발행할지 알고 있다.

애그리거트는 상태 전이 시 이벤트를 생성하고, 이렇게 생성한 이벤트를 두 가지 방법으로 서비스에 반환한다.

//애그리거트 
public class Ticket {
    public List<TicketDomainEvent> accept(LocalDateTime readyBy) {
		...
       this.acceptTime = LocalDateTime.now(); // Ticket 업데이트
       this.readyBy = readyBy;
       
       
       return singletonList(new TicketAcceptedEvent(readyBy)); // 이벤트 반환
    }
}

//서비스
@Transactional
public class KitchenService {

  @Autowired
  private TicketRepository ticketRepository;

  @Autowired
  private TicketDomainEventPublisher domainEventPublisher;

  public void accept(long ticketId, LocalDateTime readyBy) {
    Ticket ticket = ticketRepository.findById(ticketId)
            .orElseThrow(() -> new TicketNotFoundException(ticketId));
    List<TicketDomainEvent> events = ticket.accept(readyBy); // ticket.accept 호출, 업데이트
    
    domainEventPublisher.publish(ticket, events); // 도메인 이벤트 발행
  }

}
  • Ticket 애그리거트에서 반환하지 않고 애그리거트 루트를 만들어 특정 필드에 이벤트를 쌓는 방법도 있다.

도메인 이벤트를 확실하게 발행하는 방법

애그리거트를 업데이트하는 트랜잭션의 일부로 이벤트를 발행하기 위해 트랜잭셔널 메시징을 사용해야 한다.

-> 아웃박스 패턴 사용

도메인 이벤트 소비

메뉴 변경 이벤트 들어오면 업데이트 후 도메인 이벤트 목록 반환하도록 한다..

5.4 주방 서비스 비즈니스 로직

https://github.com/microservices-patterns/ftgo-application/blob/master/ftgo-kitchen-service/src/main/java/net/chrisrichardson/ftgo/kitchenservice/messagehandlers/KitchenServiceCommandHandler.java

5.5 주문 서비스 비즈니스 로직

Order 애그리거트 상태 기계

  • Pending 상태를 두는 것은 시맨틱 락 대책을 적용한 것 (아직 완벽한 커밋 전이라서 변경될지 모른다는 표시)
  • 문제가 없다면 사가는 작업 결과의 성공을 나타내는 상태로 전이하지만 그 외에는 주문 상태를 이전으로 되돌려 놓는다.
profile
💻 ☕️ 🏝 🍑 🍹 🏊‍♀️

0개의 댓글