본문 바로가기
Back-End/도메인 주도 개발 시작하기

[DDD] 도메인 주도 개발 시작하기_Ch10

by ChaSso 2023. 5. 8.

Chapter 10 이벤트

10.1 시스템 간 강결합 문제

외부 시스템의 서비스를 호출할 때의 문제

- 외부 서비스가 정상이 아닐 때 트랜잭션 처리가 애매함

- 트랜잭션을 롤백할지 말지

- 외부 서비스의 성능에 직접적인 영향을 받음

- 설계상 서로 다른 로직이 섞일 수 있음

 

컨텍스트 간의 강결합이 있으면 결합된 컨텍스트끼리 영향을 주고받게 됨

이벤트를 사용하면 강결합 제거 가능

 

 

10.2 이벤트 개요

이벤트 : 과거에 벌어진 어떤 것. 상태가 변경됐음을 의미.

이벤트가 발생하면 그에 따른 동작이 수행됨

 

10.2.1 이벤트 관련 구성요소

이벤트 도입을 위해서는 이벤트, 이벤트 생성 주체, 이벤트 디스패처(퍼블리셔), 이벤트 핸들러(구독자)를 구현해야 됨

- 이벤트 생성 주체 : 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체

- 이벤트 핸들러 : 이벤트 생성 주체가 발생한 이벤트에 반응

- 이벤트 디스패처 : 이벤트 생성 주체와 이벤트 핸들러를 연결. 구현 방식에 따라 이벤트 생성과 처리를 동기나 비동기로 실행하게 됨

 

10.2.2 이벤트의 구성

이벤트가 담는 정보

- 이벤트 종류 : 클래스 이름으로 이벤트 종류를 표현

- 이벤트 발생 시간

- 추가 데이터 : 주문번호, 신규 배송지 정보 등 이벤트와 관련된 정보

 

이벤트 이름에는 과거 시제 사용

핸들러는 디스패처로부터 이벤트를 전달받아 필요한 작업 수행

이벤트는 핸들러가 작업을 수행하는 데 필요한 데이터를 담아야 하고, 이벤트와 관련 없는 데이터를 담지 않아도 됨

 

10.2.3 이벤트 용도

이벤트의 용도

- 트리거 : 도메인 상태가 바뀔 때 그 후의 동작 수행이 필요하면 이벤트를 사용할 수 있음

- 서로 다른 시스템 간의 데이터 동기화

 

10.2.4 이벤트 장점

이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있음. 이벤트를 사용함으로써 의존 제거 가능.

이벤트를 사용하면 기능 확장에 용이. 이벤트를 사용하면 기능을 확정하더라도 로직은 수정하지 않아도 되는 경우가 있음.

 

 

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

이벤트 클래스 : 이벤트를 표현함

디스패처 : 스프링이 제공하는 ApplicationEventPublisher를 이용함

Events : 이벤트를 발행함. 이벤트 발행을 위해 ApplicationEventPublicher를 사용함

이벤트 핸들러 : 이벤트를 수신해서 처리함. 스프링이 제공하는 기능을 사용함

 

10.3.1 이벤트 클래스

과거시제 클래스명을 갖는 클래스를 이벤트로 사용하면 됨 ex. OrderCanceledEvent

이벤트 클래스는 핸들러가 이벤트를 처리하는 데 필요한 최소한의 데이터를 포함해야 됨

모든 이벤트가 공통으로 갖는 프로퍼티가 있으면 관련 상위 클래스를 만들어서 쓸 수 있음

 

10.3.2 Events 클래스와 ApplicationEventPublisher

Events 클래스는 ApplicationEventPublisher를 사용해서 이벤트를 발생시키도록 구현

eventsInitializer()메서드는 InitializingBean 타입 객체를 빈으로 설정함

InitializingBean : 스프링 빈 객체를 초기화할 때 사용하는 인터페이스

 

10.3.3 이벤트 발생과 이벤트 핸들러

Events.raise() 메서드로 이벤트를 발생시킴

이벤트를 처리할 핸들러에는 @EventListener 애너테이션을 붙임

 

10.3.4 흐름 정리

이벤트 처리 흐름

1. 도메인 기능을 실행한다.

2. 도메인 기능은 Events.raise()를 이용해서 이벤트를 발생시킨다.

3. Events.raise()는 스프링이 제공하는 ApplicationEventPublisher를 이용해서 이벤트를 출판한다.

4. ApplicationEventPublisher@EventListener(이벤트타입.class) 애너테이션이 붙은 메서드를 찾아 실행한다.

 

도메인 상태 변경과 이벤트 핸들러는 같은 트랜잭션 범위에서 실행됨

 

 

10.4 동기 이벤트 처리 문제

연동을 동기로 처리해서 외부 서비스의 성능 저하가 내 시스템의 성능 저하로 이어지는 문제와 트랜잭션 범위 관련 문제를 해결하는 방법

- 이벤트를 비동기로 처리

- 이벤트와 트랜잭션을 연계

 

 

10.5 비동기 이벤트 처리

후속 조치를 일정 시간 안에만 하면 되는 이벤트가 있음

‘A 하면 일정 시간 안에 B 하라는 요구사항에서 ‘A 하면은 이벤트

‘A 하면 이어서 B 하라중에서 ‘A 하면 최대 언제까지 B 하라는 이벤트를 비동기로 처리하는 방식으로 구현할 수 있음. A 이벤트가 발생하면 별도 스레드로 B를 수행하는 핸들러를 실행하는 방식으로 요구사항을 구현할 수 있음

 

비동기 이벤트 처리를 구현하는 방법

- 로컬 핸들러를 비동기로 실행하기

- 메시지 큐를 사용하기

- 이벤트 저장소와 이벤트 포워더 사용하기

- 이벤트 저장소와 이벤트 제공 API 사용하기

 

10.5.1 로컬 핸들러 비동기 실행

이벤트 핸들러를 별ㄹ도 스레도르 샐행

스프링이 제공하는 @Async 애너테이션 사용

 

- @EnableAsync 애너테이션을 사용해서 비동기 기능을 활성화

- 비동기로 실행할 이벤트 핸들러 메서드에 @Async 애너테이션을 붙임

 

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

카프카나 래빗MQ 같은 메시징 시스템 사용

이벤트 발생 -> 이벤트 디스패처가 이벤트를 메시지 큐에 보냄 -> 메시지 큐는 이벤트를 메시지 리스너에 전달 -> 메시지 리스너는 이벤트 핸들러로 이벤트 처리

이벤트를 발생시키는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶으려면 글로벌 트랜잭션이 필요

글로벌 트랜잭션을 사용하면 이벤트를 메시지 큐에 안전하게 전달할 수 있지만 전체 성능이 떨어짐

동일 JVM에서 비동기 처리를 위해 메시지 큐를 사용하면 시스템이 복잡해지기 때문에 동일 JVM에서는 메시지 큐를 잘 사용하지 않음

 

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

이벤트를 DB에 저장한 후 별도 프로그램을 이용해서 이벤트 핸들러에 전달

이벤트가 발생하면 핸들러는 스토리지에 이벤트 저장 -> 포워더는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 실행

포워더는 별도 스레드를 이용하므로 이벤트 발생과 처리가 비동기로 처리됨

도메인의 상태 변화와 이벤트 저장이 로컬 트랜잭션으로 처리됨

 

API 방식과 포워더 방식은 이벤트 전달 방식이 다름

- API 방식 : 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져감. 어디까지 처리했는지를 외부 핸들러가 기억.

- 포워더 방식 : 포워더를 이용해서 이벤트를 외부에 전달. 어디까지 처리했는지를 포워더가 기억.

 

이벤트 저장소 구현

- EventEntry : 이벤트 저장소에 보관할 데이터

- EventStore : 이벤트를 저장하고 조회하는 인터페이스를 제공

- JdbcEventStore : JDBC를 이용한 EventStore 구현 클래스

- EventApi : REST API를 이용해서 이벤트 목록을 제공하는 컨트롤러

 

이벤트를 과거에 일어난 사건이므로 이벤트 데이터를 수정하는 기능은 없음

 

이벤트 저장을 위한 이벤트 핸들러 구현

EventStoreHandlerhandle() 메서드는 eventStore.save() 메서드를 이용해서 이벤트 객체 저장

 

REST API 구현

offsetlimit의 웹 요청 파라미터를 이용해서 EventStore#get을 실행하고 그 결과를 JSON으로 리턴

 

API를 사용하는 클라이언트가 일정 간격으로 수행하는 과정

1. 가장 마지막에 처리한 데이터릐 offsetlastOffset을 구한다. 저장한 lastOffest이 없으면 0을 사용한다.

2. 마지막에 처리한 lastOffsetoffset으로 사용해서 API를 실행한다.

3. API 결과로 받은 데이터를 처리한다.

4. offset + 데이터 개수를 lastOffset으로 저장한다.

 

포워더 구현

포워더는 일정 주기로 EventSotre에서 이벤트를 읽어와 이벤트 핸들러에 전달하면 됨

마지막으로 전달한 이벤트의 offset을 기억해두었다가 다음 조회 시점에 마지막으로 처리한 offset부터 이벤트를 가져옴

getAndSend() 메서드의 주기적 실행을 위해 스프링의 @Scheduled 애너테이션 사용

OffsetStore를 구현한 클래스는 offset 값을 DB 테이블에 저장하거나 로컬 파일에 보관해서 마지막 offset 값을 물리적 저장소에 보관

sendEvent() 메서드는 파라미터로 전달받은 이벤트를 eventSender.send()를 이용해서 차례대로 발송

 

 

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

- 이벤트 소스를 EventEntry에 추가할지 여부

- 포워더에서 전송 실패를 얼마다 허용할 것인지

- 이벤트 손실 : 로컬 핸들러를 이용해서 이벤트를 비동기로 처리하는 경우 이벤트 처리에 실패하면 이벤트 유실

- 이벤트 순서 : 이벤트 저장소는 이벤트를 발생한 순서대로 목록에 저장하지만 메시징 시스템은 사용 기술에 따라 이벤트 저장 순서가 바뀔 수 있음

 

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

@TransactionalEventListener : 스프링 트랜잭션 상태에 따라 이벤트 핸들러를 실행할 수 있게 함

트랜잭션이 성공할 때만 이벤트 핸들러를 실행하게 되면 경우의 수를 줄일 수 있음