Post

5장 비즈니스 로직 설계

5장 비즈니스 로직 설계

목차

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
5장 비즈니스 로직 설계
5.1 비즈니스 로직 구성 패턴
__5.1.1 비즈니스 로직 설계: 트랜잭션 스크립트 패턴
__5.1.2 비즈니스 로직 설계: 도메인 모델 패턴
__5.1.3 도메인 주도 설계 개요
5.2 도메인 모델 설계: DDD 애그리거트 패턴
__5.2.1 불분명한 경계 문제
__5.2.2 애그리거트는 경계가 분명하다
__5.2.3 애그리거트 규칙
__5.2.4 애그리거트 입도
__5.2.5 비즈니스 로직 설계: 애그리거트
5.3 도메인 이벤트 발행
__5.3.1 변경 이벤트를 발행하는 이유
__5.3.2 도메인 이벤트란 무엇인가?
__5.3.3 이벤트 강화
__5.3.4 도메인 이벤트 식별
__5.3.5 도메인 이벤트 생성 및 발행
__5.3.6 도메인 이벤트 소비
5.4 주방 서비스 비즈니스 로직
__5.4.1 Ticket 애그리거트
5.5 주문 서비스 비즈니스 로직
__5.5.1 Order 애그리거트
__5.5.2 OrderService 클래스
5.6 마치며



정리 전략?

  • 전부 정리는 NO
  • 핵심중에서 꼭 적고 가야겠다 싶은 것만!
  • 나의 생각을 섞어서 적을것..!
  • 떠오르는 것 먼저 간단하게..!

5장 비즈니스 로직 설계

5장 비즈니스 로직 설계

5.1 비즈니스 로직 구성 패턴

  • 인바운드 패턴 : 클라이언트의 요청을 받아 비즈니스 로직을 호출한다.
  • 아웃바운드 패턴 : 다른 서비스 및 애플리케이션을 실행한다.
  • 이미지의 4가지 요소에 주목하자.

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

  • 우리가 일반적으로 아는 패턴. 절차지향 방식의 코딩.

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

  • 객체지향에서 도메인 설계까지 포함한 개발 방식.
  • 비교적 작은 클래스가 그물망처럼 얽힌 객체 모델로 구성된다.
  • 장점들
    • 이해, 관리가 쉽다.
    • 테스트 하기 쉽다.
    • 잘 알려진 설계 패턴을 응용할 수 있기 때문에 확장하기 쉽다.

5.1.3 도메인 주도 설계 개요

  • 엔티티 : 동일성 기준으로 구분하는 객체
  • 벨류 객체 : 여러 값을 모아둔 것이고, 이 값들이 같으면(동등하면) 같다고 취급하는 객체
  • 팩토리 : 복잡한 객체생성 로직이 구현된 객체
  • 리포지터리 : DB 접근 로직을 캡슐화한 객체
  • 서비스 : 비즈니스 로직 구현 개체(엔티티, 벨류에 속하지 않는)

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

5.2.1 불분명한 경계 문제

  • 경계가 분명하지 않으면 업데이트 할때 문제임. 왜냐면 경계까지 업데이트 하는것이 보통이기 때문임.

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

  • 애그리거트는 생명주기가 거의 동일한, 한단위로 취급 가능한 엔티티들의 집합.

5.2.3 애그리거트 규칙

  • 애그리거트는 생명주기가 거의 동일한 엔티티들의 집합.
  • 규1 : 애그리거트 루트만 참조해라!
  • 규2 : 애그리거트 간 참조는 반드시 기본키를 사용해라. 객체 래퍼런스 노(클래스 참조 변수방식 말고, integer나 long처럼 pk로 참조하라는 것.)
  • 규3 : 하나의 트랜잭션으로 하나의 애그리거트를 생성, 수정하라 : 복수개의 에그리거트를 생성 수정하고 싶을수는 있지만, Nosql같은 제한된 트랜잭션 모델이 있을 수도 있음. 이러면 큰일나기 때문에 그것은 안됨.

5.2.4 애그리거트 입도

  • 애그리거트는 작으면 작을 수록 좋음. -> 각 애그리거트의 업데이트는 직렬화되므로 잘게 나뉘어져 있으면 그만큼 애플리케이션이 동시 처리 가능한 요청 개수가 늘고 확장성이 좋아진다.

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

  • 사가: 로컬 트랜잭션을 오케스트레이션하여 데이터 일관성을 맞춤
  • 인바운드 어댑터: 비즈니스 로직의 진입점인 서비스를 호출함.
  • 서비스: 리포지토리로 DB에서 애그리거트를 조회하거나 저장함.
  • 리포지터리: 각각 DB에 접근하는 아웃바운드 어댑터를 구현함.
  • 비즈니스 로직: Order 애그리거트 OrderService, OrderRepository, 하나 이상의 사가
  • OrderService는 OrderRepository를 이용하여 Order를 조회/저장한다.
  • 주문 서비스에 국한된 간단한 요청은 Order 애그리거트를 직접 업데이트하고, 여러 서비스에 걸친 업데이트 요청은 사가를 생성해서 처리한다.

5.3 도메인 이벤트 발행

5.3.1 변경 이벤트를 발행하는 이유

  • 여러 서비스에 걸쳐 데이터 일관성을 유지하기 위해
  • 소스 데이터가 변경되었음을 알리기 위해
  • 다른 서비스, 플램폼에 그 변경을 알리기 위해

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

  • 도메인과 관련된 특별한 사건.(애그리거트는 뭔가 생성되거나 중요한 변경이 발생했을 때 도메인 이벤트를 발행한다.)
  • 도메인 이벤트는 과거 분사형 동사로 명명한 클래스
  • DomainEvent: 자신을 구현한 클래스가 도메인 이벤트임을 알리는 마커 인터페이스 이 인터페이스를 상속한 OrderDomainEvent는 Order 애그리거트가 발행한 OrderCreatedEvent의 마커 인터페이스
  • DomainEventEnvelope: 이벤트 객체 및 메타데이터를 조회하는 메서드가 존재. 이 인터페이스는 DomainEvent를 상속한 매개변수화 객체를 받음

5.3.3 이벤트 강화

  • 여기서 말하는 이벤트 강화는 이벤트에 포함된 정보를 풍성하게 한다(enrichment)라는 뉘앙스가 품어 있음.
  • 이벤트 컨슈머에게 필요한 정보까지 이벤트가 가지고 다니는 것을 의미
  • 이렇게 하면, 이벤트 컨슈머는 서비스를 쿼리해서 애그리거트를 조회할 필요가 없는 장점이 있지만, 컨슈머 요건이 바뀌면 이벤트 클래스가 바뀌어야해서 안정성이 떨어짐.

5.3.4 도메인 이벤트 식별

  • 이벤트 스토밍 (Event Storming): 복잡한 도메인을 이해하기 위해 이벤트 중심으로 워크숍을 하는 것 각계 도메인 전문가들이 한자리에 모여 큼지막한 화이트 보드나 긴 종이 두루마리에 수많은 점착식 메모지를 붙이고, 몇 시간 동안 이벤트 스토밍을 하면 애그리거트와 이벤트로 구성된 이벤트 중심적인 도메인 모델이 완성됨.
  • 이벤트 스토밍은 다음 3단계를 거친다.
    • 이벤트 브레인스토밍 (Event Brainstorming): 도메인 이벤트를 머릿속에서 쥐어 짜낸다. 오렌지색 점착식 메모지로 구분된 도메인 이벤트를 모델링 화면에 대략 그려놓은 타임라인에 배치한다.
    • 이벤트 트리거 (event trigger) 식별: 각각의 이벤트를 일으키는 트리거를 식별한다.
      • 사용자 액션: 파란색 점차식 메모지로 커맨드를 표시
      • 외부 시스템: 자주색 점착식 메모지로 표시
      • 기타 도메인 이벤트
      • 시간 경과
    • 애그리거트 식별: 각 커맨드를 소비 후 적절한 이벤트를 발생시키는 애그리거트를 식별해서 노란색 점착식 메모지로 표시한다.

5.3.5 도메인 이벤트 생성 및 발행

  • 도메인 이벤트 생성
    • 도메인 이벤트는 에그리거트가 발행.
  • 도메인 이벤트 발행
    • 서비스는 애그리거트 루트 메서드를 호출 후 이벤트를 발행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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 클래스를 애그리거트 루트의 특정 필드에 이벤트를 쌓아두고 서비스가 이벤트를 가져다 발행하는 방법도 존재함.
1
2
3
4
5
6
7
8
9
10
11
// 예제 5-5 Ticket은 도메인 이벤트를 기록하는 상위 클래스 AbstractAggregateRoot를 상속한다.
public class Ticket extemds AbstractAggregateRoot {
    public void accept(LocalDateTime readyBy) {
		...
       this.acceptTime = LocalDateTime.now(); // Ticket 업데이트
       this.readyBy = readyBy;


      registerEvent(new TicketAcceptedEvent(readBy));
    }
}
  • 도메인 이벤트를 확실하게 발행하는 방법
    • 메시지를 로컬 DB 트랜잭션의 일부로 확실하게 전달하는 방법은 트랜잭셔널 메시징을 이용하는 방법이 있었고, 도메인 이벤트도 다를 바 없다.
    • 서비스는 DB에서 애그리거트를 업데이트하는 트랜잭션의 일부로 이벤트를 발행하기 위해 트랜잭셔널 메시징을 사용한다. DB 업데이트 트랜잭션의 일부로 이벤트를 OUTBOX 테이블에 삽입하고, 트랜잭션이 커밋되면 이 테이블에 삽입된 이벤트를 메시지 브로커에 발행한다.
    • 애그리거트 타입/ID와 도메인 이벤트 목록을 매개변수로 받음. publish()는 이 프레임워크에 탑재된 MessageProducer 인터페이스를 통해 트랜잭션을 걸어 이벤트를 발행한다. DomainEventPublisher 이벤트 발행기를 서비스가 직접 호출할 수도 있지만, 그러면 서비스가 유효한 이벤트만 발행하리라는 보장이 없음. 가령 KithchenService는 Ticket 애그리거트의 이벤트 마커 인터페이스를 구현한 TicketDomainEvent를 구현한 이벤트 더 좋은 방법은 AbstractAggregateDomainEventPublisher의 하위 클래스를 구현하는 것

5.3.6 도메인 이벤트 소비

  • 도메인 이벤트는 결국 메시지로 바뀌어 카프카 같은 메시지 브로커에 발행됨.
  • 브로커가 제공하는 clientAPI를 직접 쓰는 것보다 , DomainEventDispathcer 같은 고수준 API를 써서 도메인 이벤트를 적절한 핸들러 메서드로 디스패치하는 것이 더 간편함.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class KitchenServiceEventConsumer {

  @Autowired
  private KitchenService kitchenService;

  public DomainEventHandlers domainEventHandlers() { // 이벤트와 이벤트 핸들러를 매핑
    return DomainEventHandlersBuilder
            .forAggregateType("net.chrisrichardson.ftgo.restaurantservice.domain.Restaurant")
            .onEvent(RestaurantCreated.class, this::createMenu)
            .onEvent(RestaurantMenuRevised.class, this::reviseMenu)
            .build();
  }

  public void reviseMenu(DomainEventEnvelope<RestaurantMenuRevised> de) { // RestaurantMenuRevised 이벤트 핸들러
    long id = Long.parseLong(de.getAggregateId());
    RestaurantMenu revisedMenu = de.getEvent().getRevisedMenu();
    kitchenService.reviseMenu(id, revisedMenu);
  }

}

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

  • 5.4.1 Ticket 애그리거트
  • 여기서 핵심은 도메인 이벤트를 발행하는 서비스도 있고,(KitchenService. 티켓을 찾아, 티켓의 상태를 변경하고(확정) 티켓 이벤트 생성. 티켓 도메인에서 처리할듯)
  • 도메인 이벤트를 처리하는 부분이 한 도메인내에 존재한다는 것. (KitchenServiceCommandHandler. 티켓 생성 이벤트를 받아, 티켓생성 응답을 보낸다.)

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

  • 5.5.1 Order 애그리거트
    • Order 애그리거트는 소비자가 한 주문을 나타냄.
      • version 필드 사용에 주목
    • Orderid 애그리거트 상태 기계.(주목)
      • 복잡한 상태는 이렇게 쓸수도 있다.
      • 중간에 계류 상태를 두는 것이 왜 시맨틱 락 대책과 이어진다. 충돌이 줄고,비슷한 이벤트가 중복 처리될 위험도 감소함.
  • 5.5.2 OrderService 클래스
    • 이 클래스의 메서드들은 대부분 사가를 만들어 Order 애그리거트 생성/수정을 오케스트레이션하므로 복잡함.
    • OrderRespository, OrderDomainEventPublisher, SagaManager 등 주입되는 의존성이 많음.
  • 모놀리틱과 차이점
    • 모놀리틱과 다르게 다양한 설계 제약 조건이 부과된 DDD 애그리거트로 도메인 모델을 구성하고, 상이한 애그리거트는 객채 참조가 아닌 PK 참조한다.
    • 사가를 이용하여 여러 서비스에 걸쳐 데이터 일관성을 유지한다는 중요한 차이점이 존재한다. KitchenService는 사가에 참여할 뿐 사가를 시작하지는 않지만, OrderService는 주문을 생성하고 수정할 때 사가에 전적으로 의지한다. 다른 서비스가 있는 데이터가 트랜잭션 관점에서 일관성이 보장되어야 하기 때문이다. 그래서 OrderService 메서드는 대부분 직접 Order를 업데이트 하지 않고 사가를 만든다.

5.6 마치며

  • 절차 vs 객체 지향 도메인 모델 패턴(비즈니스가 복잡하면 추천)
  • 비즈니스 로직은 DDD 애그리거트로 구성하기.pk로 서로 참조하기.
  • 애그리거트는 도메인 이벤트 생성, 수정시 이벤트를 발행한다.

나의 생각 정리.

  • 이번에 이 챕터를 다시 읽는데 상당히 오랜 시간이 걸렸다.
  • 원래 그런걸까? 스터디를 했던 분들을 보면 1주에 1개씩 한건가.
  • 좀알것같다. 그렇다면 궁금한것은 이미 서비스 중인 것들에 반영할때는 어떻게 해야할까?
    • 기존것 : 최대한 일단 클린 코드 관점에서 수정.
    • 신규 기능 : 도메인을 다시 정의해야할까? 정의해서 쓸수 있으면 해보는 것도 좋은 시도라고 생각했다.
    • 마이크로 서비스로 전환할 일이 생긴다면 이 책으로 시작하는 것이 좋은 계기가 될것이다. 코드도 이해되는 선에서 꼭 확인해보자.

참고자료

https://velog.io/@daehoon12/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%8C%A8%ED%84%B4-5.-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EC%84%A4%EA%B3%84 https://medium.com/byungkyu-ju/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4%ED%8C%A8%ED%84%B4-5-5%EC%9E%A5-f1f5ebdac05a https://bryceyangs.github.io/various/2023/03/12/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4%ED%8C%A8%ED%84%B4-5%EC%9E%A5/#google_vignette

This post is licensed under CC BY 4.0 by the author.