책/도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지

[도메인 주도 개발 시작하기] 10장: 이벤트

2022. 11. 4. 14:52
목차
  1. 10.1 시스템 간 강결합 문제
  2. 10.2 이벤트 개요
  3. 10.3 이벤트, 핸들러, 디스패처 구현
  4. 10.4 동기 이벤트 처리 문제
  5. 10.5 비동기 이벤트 처리
  6. 10.6 이벤트 적용 시 추가 고려 사항
본문은 [도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지]를 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.

 

10.1 시스템 간 강결합 문제

  • 여러 시스템이 섞여서 하나의 기능을 수행하게 되면 다음과 같은 문제들이 발생한다
    • 1) 트랜잭션 처리가 애매해지는 문제
    • 2) 외부 서비스의 성능에 영향을 받는 문제
    • 3) 도메인 객체에 서로 다른 도메인 로직이 섞이는 문제
    • 4) 다른 기능을 추가할 때 위 문제들이 더욱 복잡해진다
  • 위 문제들이 발생하는 이유는 바운디드 컨텍스트 간의 강결합(high coupling) 때문이다
  • 이벤트를 사용함으로써 강결합을 없앨 수 있다
  • 특히 비동기 이벤트를 사용하면 두 시스템 간의 결합을 크게 낮출 수 있다

 

 

10.2 이벤트 개요

  • 이벤트란 '과거에 벌어진 어떤 것'을 의미한다
  • 이벤트가 발생한다는 것은 상태가 변경됐다는 것을 의미한다
  • 도메인 모델에서 도메인의 상태 변경을 이벤트로 표현할 수 있다

 

1) 이벤트 관련 구성요소

  • 도메인 모델에 이벤트를 도입하려면 이벤트, 이벤트 생성 주체, 이벤트 디스패처 (퍼블리셔), 이벤트 핸들러(구독자)라는 네 가지 구성요소를 구현해야 한다
  • 1) 이벤트 생성 주체
    • 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체
    • 이들 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생시킨다
  • 2) 이벤트 핸들러
    • 이벤트 생성 주체가 발생한 이벤트에 반응한다
    • 생성 주체가 발생한 이벤트를 전달받아 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다
  • 3) 이벤트 디스패처
    • 이벤트 생성 주체와 이벤트 핸들러를 연결해주는 것
    • 이벤트 생성 주체가 이벤트를 생성하여 디스패처에 이를 전달하면, 디스패처는 다시 핸들러에 이벤트를 전달한다
    • 이벤트 디스패처의 구현 방식에 따라 이벤트 생성과 처리를 동기나 비동기로 실행한다

 

2) 이벤트의 구성

  • 이벤트는 발생한 이벤트에 대한 정보를 담는다
    • 이벤트 종류: 클래스 이름으로 이벤트 종류를 표현
    • 이벤트 발생 시간
    • 추가 데이터: 주문번호, 신규 배송지 정보 등 이벤트와 관련된 정보
  • 이벤트는 현재 기준으로 과거에 벌어진 것을 표현하므로 이벤트 이름에는 과거 시제를 사용한다
  • 이벤트는 이벤트 핸들러가 작업을 수행하는 데 필요한 데이터를 담아야 하는데, 이 데이터가 부족하면 핸들러는 관련 API를 호출하거나 DB에서 데이터를 직접 읽어온다

 

3) 이벤트 용도

  • 이벤트는 크게 두 가지 용도로 쓰인다
  • 1) 트리거
    • 도메인의 상태가 바뀔 때 다른 후처리가 필요하면 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다
  • 2) 서로 다른 시스템 간의 데이터 동기화

 

4) 이벤트 장점

  • 1) 이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다
  • 2) 이벤트 핸들러를 사용하면 기능 확장도 용이하다
    • 기존 로직에 기능을 추가하고 싶으면 이벤트 핸들러를 추가해서 해당 기능을 수행하면 된다

 

 

10.3 이벤트, 핸들러, 디스패처 구현

1) 이벤트 클래스

  • 이벤트 자체를 위한 상위 타입은 존재하지 않으며, 원하는 클래스를 이벤트로 사용하면 된다
    • 모든 이벤트가 공통으로 갖는 프로퍼티가 존재한다면 상위 클래스를 만들어 이벤트들이 해당 상위 클래스를 상속받게끔 구현할 수 있다
  • 이벤트는 과거에 벌어진 상태 변화나 사건을 의미하므로 이벤트 클래스 이름은 과거 시제를 사용한다
  • 이벤트 클래스는 이벤트를 처리하는 데 필요한 최소한의 데이터를 포함해야 한다

 

2) Events 클래스와 ApplicationEventPublisher

  • 이벤트 발생과 출판을 위해 스프링이 제공하는 ApplicationEventPublisher를 사용한다
public class Events {
    private static ApplicationEventPublisher publisher;

    static void setPublisher(ApplicationEventPublisher publisher) {
        Events.publisher = publisher;
    }

    public static void raise(Object event) {
        if (publisher != null) {
            publisher.publishEvent(event);
        }
    }
}
  • Events#setPublisher() 메서드에 이벤트 퍼블리셔를 전달하기 위해 스프링 설정 클래스를 작성한다
    • ApplicationContext는 ApplicationEventPublisher를 상속하고 있으므로 Events 클래스를 초기화할 때 ApplicationContext를 전달한다
@Configuration
public class EventsConfiguration {
    @Autowired
    private ApplicationContext applicationContext;

    @Bean
    public InitializingBean eventsInitializer() {
        return () -> Events.setPublisher(applicationContext);
    }
}

 

3) 이벤트 발생과 이벤트 핸들러

  • 이벤트를 발생시킬 코드는 Event.raise() 메서드를 사용한다
public class Order {

    public void cancel() {
        verifyNotYetShipped();
        this.state = OrderState.CANCELED;
        Events.raise(new OrderCanceledEvent(number.getNumber()));
    }
    ...
  • 이벤트를 처리할 핸들러는 스프링이 제공하는 @EventListener 애너테이션을 사용해서 구현한다
@Service
public class OrderCanceledEventHandler {
    private RefundService refundService;

    public OrderCanceledEventHandler(RefundService refundService) {
        this.refundService = refundService;
    }

    @EnventListener(OrderCanceledEvent.class)
    public void handle(OrderCanceledEvent event) {
        refundService.refund(event.getOrderNumber());
    }
}

 

4) 흐름 정리

  • 이벤트 처리 흐름
    • 1) 도메인 기능 실행
    • 2) 도메인 기능이 Event.raise()를 이용해서 이벤트를 발생
    • 3) Event.raise()는 스프링의 ApplicationEventPublisher를 이용해서 이벤트를 출판
    • 4) ApplicationEventPublisher는 @EventListener 애너테이션이 붙은 메서드를 찾아 실행

 

 

10.4 동기 이벤트 처리 문제

// 1. 응용 서비스 코드
@Transactional // 외부 연동 과정에서 익셉션이 발생하면 트랜잭션 처리는?
public void cancel(OrderNo orderNo) {
    Order order = findOrder(orderNo);
    order.cancel(); // 이벤트 발생
}

// 2. 이벤트를 처리하는 코드
@Service
public class OrderCanceledEventHandler {
    ...

    @EnventListener(OrderCanceledEvent.class)
    public void handle(OrderCanceledEvent event) {
        // refundService.refund()가 느려지거나 익셉션이 발생하면?
        refundService.refund(event.getOrderNumber());
    }
}
  • 이벤트를 사용해서 강결합 문제를 해결하더라도 외부 서비스에 영향을 받는 문제(성능 및 트랜잭션 범위 문제)는 해소되지 않는다
  • 이벤트를 비동기로 처리하거나 이벤트와 트랜잭션을 연계함으로써 관련 문제를 해소할 수 있다

 

 

10.5 비동기 이벤트 처리

  • 'A하면 이어서 B하라'는 내용을 담고 있는 요구사항은 실제로 'A 하면 최대 언제까지 B 하라'인 경우가 많다
  • 이 경우엔 이벤트를 비동기로 처리하는 방식으로 구현할 수 있다
    • A 이벤트가 발생하면 별도 스레드로 B를 수행하는 핸들러를 실행하는 방식으로 요구사항을 구현할 수 있다
  • 이벤트를 비동기로 구현할 수 있는 방법은 다양한데, 여기서는 다음 네 가지 방식을 알아본다
    • 로컬 핸들러로 비동기 실행하기
    • 메시지 큐를 사용하기
    • 이벤트 저장소와 이벤트 포워더 사용하기
    • 이벤트 저장소와 이벤트 제공 API 사용하기

 

1) 로컬 핸들러 비동기 실행

  • 이벤트 핸들러를 별도 스레드로 실행
  • @EnableAsync 애너테이션을 스프링 설정 클래스에 붙여서 비동기 기능을 활성화
  • 이벤트 핸들러 메서드에 @Async 애니터이션을 붙인다

 

2) 메시징 시스템을 이용한 비동기 구현

  • 카프카나 래빗MQ와 같은 메시징 시스템을 사용하여 비동기로 이벤트를 처리할 수 있다
    • 이벤트가 발생하면 이벤트 디스패처는 이벤트를 메시지 큐에 보낸다
    • 메시지 큐는 이벤트를 메시지 리스너에 전달한다
    • 리스너는 알맞은 이벤트 핸들러를 이용해서 이벤트를 처리한다
    • 이때 이벤트를 메시지 큐에 저장하는 과정과 메시지 큐에서 이벤트를 읽어와 처리하는 과정은 별도 스레드나 프로세스로 처리된다
  • 필요하다면 이벤트를 발생시키는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶어야 한다
    • 이 경우엔 글로벌 트랜잭션이 필요하다
    • 글로벌 트랜잭션은 안전하게 이벤트를 메시지 큐에 전달할 수 있지만, 반대로 전체 성능이 떨어진다는 단점이 있다
    • 글로벌 트랜잭션을 지원하지 않는 메시징 시스템도 있다
  • 메시지 큐를 사용하면 보통 이벤트를 발생시키는 주체와 이벤트 핸들러가 별도 프로세스에서 동작한다
    • 이는 이벤트 발생 JVM과 이벤트 처리 JVM이 다르다는 것을 의미한다
    • 한 JVM에서 메시지 큐를 이용해서 이벤트를 주고받을 수 있지만, 이는 시스템을 복잡하게 만들 뿐이다

 

3) 이벤트 저장소를 이용한 비동기 처리

  • 이벤트 저장소를 이용한 첫 번째 방식은 포워더를 사용하는 것이다
  • 이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용해서 이벤트 핸들러에 전달할 수 있다
    • 이벤트가 발생하면 핸들러는 스토리지에 이벤트를 저장한다
    • 포워더는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러에 실행한다
    • 포워더는 별도 스레드를 이용하기 때문에 이벤트 발행과 처리가 비동기로 처리된다 
  • 이 방식은 도메인의 상태와 이벤트 저장소로 동일한 DB를 사용한다
    • 즉, 도메인의 상태 변화와 이벤트 저장이 로컬 트랜잭션으로 처리된다
    • 이벤트를 물리적 저장소에 보관하므로 핸들러가 이벤트 처리에 실패하면 포워더는 다시 이벤트 저장소에서 이벤트를 읽어와 핸들러를 실행하면 된다

 

  • 이벤트 저장소를 이용한 두 번째 방법은 이벤트를 외부에 제공하는 API를 사용하는 것이다
  • API 방식과 포워더 방식의 차이점은 이벤트를 전달하는 방식에 있다
    • 포워더 방식은 포워더를 이용해서 이벤트를 외부에 전달한다
    • API 방식은 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져간다
    • 따라서 각기 다른 주체가 이벤트를 어디까지 처리했는지 추적한다 (포워더 vs 외부 핸들러)

 

  • 포워더 방식과 API 방식 모두 이벤트 저장소를 사용하므로 이벤트를 저장할 저장소가 필요하다
  • 이벤트 저장소를 구현한 코드 구조는 위와 같다

 

 

10.6 이벤트 적용 시 추가 고려 사항

  • 이벤트를 구현할때 추가로 고려할 점이 있다
  • 1) 이벤트 소스를 EventEntry에 추가할지 여부
    • 이벤트를 발생시킨 주체에 따라 분기 처리할 수 있다
  • 2) 포워더에서 전송 실패를 얼마나 허용할 것인가
    • 포워더는 이벤트 전송에 실패하면 실페한 이벤트부터 다시 읽어와 전송을 시도한다
    • 특정 이벤트에서 계속 전송이 실패하면 나머지 이벤트를 전송할 수 없다
    • 포워더를 구현할 때는 실패한 이벤트의 재전송 횟수 제한을 두어야 한다
  • 3) 이벤트 손실
    • 이벤트 저장소를 사용하는 방식은 트랜잭션에 성공하면 이벤트가 저장소에 보관된다는 것을 보장할 수 있다
    • 로컬 핸들러를 이용해서 이벤트를 비동기로 처리할 경우 이벤트 처리에 실패하면 이벤트를 유실하게 된다
  • 4) 이벤트 순서
    • 이벤트 저장소를 사용하면 이벤트 발생 순서와 메시지 전달 순서가 일치함이 보장된다
    • 메시징 시스템은 사용 기술에 따라 이벤트 발생 순서와 메시지 전달 순서가 다를 수도 있다
  • 5) 이벤트 재처리
    • 동일한 이벤트를 다시 처리해야 할 때 이벤트를 어떻게 할 지 결정해야 한다
    • 가장 쉬운 방법은 마지막으로 처리한 이벤트의 순번을 기억해두고, 이미 처리했던 순번의 이벤트는 무시하는 것이다
    • 이벤트를 멱등으로 처리하는 방법도 있다
      • 멱등성이란 연산을 여러 번 적용해도 결과가 달라지지 않는 성질을 의미한다
      • 비슷하게 이벤트 처리도 동일 이벤트를 한 번 적용하나 여러 번 적용하나 시스템이 같은 상태가 되도록 핸들러를 구현할 수 있다
      • 이벤트 핸들러가 멱등성을 가지면 시스템 장애로 인해 같은 이벤트가 중복해서 발생해도 결과적으로 동일한 상태가 된다
      • 이는 이벤트 중복 발생이나 중복 처리에 대한 부담을 줄여준다

 

1) 이벤트 처리와 DB 트랜잭션 고려

  • 이벤트를 처리할 때는 DB 트랜잭션을 함께 고려해야 한다
  • 주문 취소와 환불 기능을 다음과 같이 이벤트를 이용해서 구현하면 다음과 같다
    • 주문 취소 기능은 주문 취소 이벤트를 발생시킨다
    • 주문 취소 이벤트 핸들러는 환불 서비스에 환불 처리를 요청한다
    • 환불 서비스는 외부 API를 호출해서 결제를 취소한다
  • 이벤트 발생과 처리를 모두 동기로 처리하면 실행 흐름은 다음과 같다
    • 12번 과정까지 다 성공하고 13번 과정에서 DB를 업데이트하는 데 실패하면 결제는 취소됐는데 DB에는 주문이 취소되지 않는 상태로 남게된다

  • 비동기로 처리할 때도 마찬가지로 DB 트랜잭션을 고려해야 한다
    • 이벤트 핸들러를 호출하는 5번 과정은 비동기로 실행한다
    • DB 업데이트와 트랜잭션을 다 커밋한 뒤에 환불 로직인 11~13번 과정을 실행했을 때, 12번 과정에서 외부 API 호출에 실패하면 DB에는 주문이 취소된 상태로 데이터가 바뀌었는데 결제는 취소되지 않는 상태로 남게 된다

 

  • 이벤트 처리를 어떤 방식으로 하든 간에 이벤트 처리 실패와 트랜잭션 실패를 함께 고려해야 한다
    • 트랜잭션 실패와 이벤트 처리 실패를 모두 고려하면 복잡해지므로 경우의 수를 줄이는 게 좋다
    • 트랜잭션이 성공할 때만 이벤트 핸들러를 실행시키면 경우의 수를 줄일 수 있다
  • 스프링은 @TransactionalEventListener 애너테이션을 지원하는데, 이는 스프링 트랜잭션 상태에 따라 이벤트 핸들러를 실행할 수 있게 한다
    • phase 속성 값에 TransactionPhase.AFTER_COMMIT을 지정함으로써 트랜잭션 커밋에 성공한 뒤에 핸들러 메서드를 실행하게 했다
    • 이 기능을 사용하면 이벤트를 실행했는데 트랜잭션이 롤백 되는 상황이 발생하지 않는다
@TransactionalEventListener(
        classes = OrderCanceledEvent.class,
        phase = TransactionPhase.AFTER_COMMIT
)
public void handle(OrderCanceledEvent event) {
    refundService.refund(event.getOrderNumber());
}
  • 이벤트 저장소로 DB를 사용해도 동일한 효과를 볼 수 있다
    • 이벤트 발생 코드와 이벤트 저장 처리를 한 트랜잭션으로 처리하면 된다
  • 트랜잭션이 성공할 때만 이벤트 핸들러를 실행하게 되면 트랜잭션 실패에 대한 경우의 수가 줄어 이벤트 처리 실패만 고민하면 된다
저작자표시 비영리 (새창열림)

'책 > 도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지' 카테고리의 다른 글

[도메인 주도 개발 시작하기] 11장: CQRS  (0) 2022.11.05
[도메인 주도 개발 시작하기] 9장: 도메인 모델과 바운디드 컨텍스트  (1) 2022.10.28
[도메인 주도 개발 시작하기] 8장: 애그리거트 트랜잭션 관리  (0) 2022.10.24
[도메인 주도 개발 시작하기] 7장: 도메인 서비스  (0) 2022.10.21
[도메인 주도 개발 시작하기] 6장: 응용 서비스와 표현 영역  (0) 2022.10.14
  1. 10.1 시스템 간 강결합 문제
  2. 10.2 이벤트 개요
  3. 10.3 이벤트, 핸들러, 디스패처 구현
  4. 10.4 동기 이벤트 처리 문제
  5. 10.5 비동기 이벤트 처리
  6. 10.6 이벤트 적용 시 추가 고려 사항
'책/도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지' 카테고리의 다른 글
  • [도메인 주도 개발 시작하기] 11장: CQRS
  • [도메인 주도 개발 시작하기] 9장: 도메인 모델과 바운디드 컨텍스트
  • [도메인 주도 개발 시작하기] 8장: 애그리거트 트랜잭션 관리
  • [도메인 주도 개발 시작하기] 7장: 도메인 서비스
코택
코택
코택
TaxFree
코택
전체
오늘
어제
  • 분류 전체보기 (369)
    • Spring (29)
      • Spring (18)
      • 스프링 핵심 원리 - 고급편 (11)
    • Spring Batch (4)
    • JPA (4)
    • CS (89)
      • 자료구조 (2)
      • 네트워크 (5)
      • 운영체제 (1)
      • 데이터베이스 (4)
      • SQL (7)
      • 알고리즘 이론 (4)
      • 알고리즘 문제 풀이 (66)
    • 웹 (28)
      • React.js (4)
      • Next.js (1)
      • Node.js (14)
      • FastAPI (4)
      • Django (5)
    • 프로그래밍 언어 (45)
      • Python (5)
      • Java + Kotlin (29)
      • JavaScript + TypeScript (11)
    • 테스트코드 (26)
      • ATDD, 클린 코드 with Spring (4)
      • 이규원의 현실 세상의 TDD: 안정감을 주는 코드.. (20)
    • 인프라 (6)
      • AWS (2)
      • Kubernetes (4)
    • 트러블슈팅 (25)
    • 책 (89)
      • Effective Java (54)
      • Effective Kotlin (14)
      • 도메인 주도 개발 시작하기: DDD 핵심 개념 정.. (11)
      • 웹 프로그래머를 위한 데이터베이스를 지탱하는 기술 (6)
      • 도메인 주도 설계 첫걸음 (4)
    • Git (10)
    • 회고 (5)
    • etc (8)

블로그 메뉴

  • 홈
  • 방명록
  • 관리
  • GitHub
  • LinkedIn

공지사항

  • 스킨 관련

인기 글

태그

  • 깊이 우선 탐색
  • 백준
  • BOJ
  • atdd
  • 장고
  • fastapi
  • mysql
  • 브루트포스
  • http
  • dp
  • 그래프 탐색
  • 파이썬
  • 그래프
  • Git
  • Shortest Path

최근 댓글

최근 글

hELLO · Designed By 정상우.
코택
[도메인 주도 개발 시작하기] 10장: 이벤트
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.