1. 시스템 간 강결합 문제
주문 바운디드 컨텍스트와 결제 바운디드 컨텍스트처럼 강한 결합을 가지는 바운디드 컨텍스트들이 있다. 주문 로직과 결제 로직이 코드상으로도 뒤섞여 여러 문제를 야기한다. 이럴 때는 이벤트
를 사용해 강결합을 약화시킬 수 있다. 특히 비동기 이벤트를 사용하면 두 시스템 간의 결합도를 크게 낮출 수 있다.
2. 이벤트 개요
이벤트 관련 구성 요소
- 이벤트
- 이벤트 생성 주체
- 도메인 모델에서는 엔티티, 밸류, 도메인 서비스 같은 도메인 객체에 해당한다.
- 이벤트 디스패처(퍼블리셔)
- 이벤트 생성 주체와 이벤트 핸들러를 연결한다.
- 이벤트 생성 주체는 이벤트를 생성해 디스패처에 전달하고, 디스패처는 전달받은 이벤트를 핸들러에 전파한다.
- 이벤트 핸들러(구독자)
- 생성 주체가 발생한 이벤트를 전달 받아 이벤트에 담긴 데이터를 이용해 원하는 기능을 실행한다.
이벤트의 구성
- 이벤트 종류 : 클래스 이름으로 이벤트 종류를 표현
- 이벤트 발생 시간
- 추가 데이터
이벤트는 이벤트 핸들러가 수행할 때 필요한 데이터를 담아야 한다.
만약 이 데이터가 부족하다면 관련 API 호출하거나 DB로부터 데이터를 읽어와야 한다.
이벤트의 용도
- 트리거
: 도메인의 상태가 바뀔 때 후처리가 필요하면 후처리를 실행하기 위해 사용
- 서로 다른 시스템 간의 데이터 동기화
: 예를 들어, 배송지를 변경하며 외부 배송 서비스에 바뀐 배송지 정보를 전송해야 함
이벤트의 장점
- 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있음
- 기능 확장에 용이
3. 이벤트, 핸들러 , 디스패처 구현
다음과 같은 순서로 이벤트를 처리한다. 해당 책에서는 스프링이 제공하는 ApplicationEventPublisher를 사용해 설명한다.
1. 도메인 기능을 실행
2. 도메인 기능은 Events.raise()를 이용해 이벤트를 발생시킴
3. Event.raise()는 스프링이 제공하는 ApplicationEventPublisher를 이용해 이벤트를 발행
4. ApplicationEventPublisher는 @EventListener 애노테이션이 붙은 메서드를 찾아 실행
4. 동기 이벤트 처리 문제
- 이벤트를 사용하여 강결합 문제는 해소할 수 있다.
- 하지만 외부 서비스에 영향을 받는 문제는 남아있다.
- 외부시스템과의 연동을 동기로 처리할때 발생하는 성능과 트랜잭션 범위 문제를 해결하기 위해 1) 이벤트를 비동기로 처리하거나, 2) 이벤트와 트랜잭션을 연계할 수 있다.
5. 비동기 이벤트 처리
주문을 취소하는 경우를 생각해보자. 주문을 취소하자마자 바로 결제를 취소해야 할까? 짧게는 수십초 내에 길게는 며칠 뒤에 결제가 취소되기만 하면 되는 경우가 많다.
이렇게 'A가 발생하면 B를 이어서 하라'는 내용의 요구사항은 대체로 'A가 발생하면 최대 언제까지 B 하라'는 뜻일 경우가 많다.
여기서 'A'를 이벤트로 볼 수 있다. 'B'는 이벤트 핸들러에서 수행할 수 있다.
결론적으로 'A가 발생하면 B를 이어서 하라'는 요구사항 중 'A가 발생하면 최대 언제까지 B 하라'로 바꿀 수 있는 요구사항은 이벤트를 비동기로 처리하는 방식으로 구현할 수 있다.
이벤트를 비동기로 구현하는 방법
1. 로컬 핸들러를 비동기로 실행
- 이벤트 핸들러를 별도 스레드로 실행하는 방법
- 스프링이 제공하는 @Async 애노테이션을 사용할 수 있다.
- 스프링 설정 클래스에 @EnableAsynce 애노테이션을 붙여 비동기 기능 활성화
- 이벤트 핸들러 메소드에 @Asynce 애노테이션을 붙인다.
2. 메시징 시스템(메시지 큐)를 사용
- Kafka나 RabbitMQ를 사용할 수 있다.
- 사용 로직
- 이벤트가 발생하면 이벤트 디스패처는 이벤트를 메시지 큐에 보낸다.
- 메시지 큐는 이벤트를 이벤트 리스너에 전달한다.
- 메시지 리스너는 알맞은 이벤트 핸들러를 이용해 이벤트를 처리한다.
- 이때, 이벤트를 메시지 큐에 저장하는 과정과 메시지 큐에서 이벤트를 읽어와 처리하는 과정은 별도 스레드나 프로세스로 처리한다.
- 메시지큐를 사용하면 보통 이벤트를 발생시키는 주체와 이벤트 핸들러가 별도 프로세스에서 동작한다.
- RabbitMQ vs Kafka
- RabbitMQ
- 글로벌 트랜잭션 지원
- 클러스터가 고가용성 지원 -> 안정적으로 메시지 전달 가능
- Kafka
- 글로벌 트랜잭션 지원x
- 다른 메시징 시스템에 비해 높은 성능
3. 이벤트 저장소와 이벤트 포워더 사용하기
- 이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용해서 이벤트 핸들러에 전달
- 포워드를 이용한 방식
- 포워더는 주기적으로 이벤트를 저장에서 가져와 이벤트 핸들러를 실행한다.(별도의 스레드를 이용하므로 비동기)
- 이벤트 핸들러가 이벤트 처리에 실패하면 다시 포워더가 이벤트 저장소에서 이벤트를 읽어와 핸들러를 실행
4. 이벤트 저장소와 이벤트 제공 API 사용하기
- 이벤트를 외부에 제공하는 API 사용
- 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져간다.
포워더 방식
-> 포워더가 이벤트를 어디까지 처리했는지 추적
API 방식
-> 외부 핸들러가 이벤트를 어디까지 처리했는지 추적
6. 이벤트 적용시 추가 고려 사항
이벤트 구현시 고려할 점
- 이벤트 소스를 EventEntry에 추가할 것인지
- 포워더에서 전송 실패를 얼마나 허용할 것인지
- 이벤트 손실에 대한 것
- 로컬 핸들러를 이용할 경우 이벤트 처리에 실패하면 이벤트 유실
- 이벤트 순서에 대한 것
- 메시징 시스템은 사용 기술에 따라 이벤트 발생 순서와 전달 순서가 다를 수 있다.
- 이벤트 재처리
- 동일한 이벤트를 다시 처리해야 할 때 이벤트를 어떻게 할지
이벤트 처리와 DB 트랜잭션
- 이벤트 처리를 동기로 하든 비동기로 하든 이벤트 처리 실패와 트랜잭션 실패를 함께 고려해야 한다.
- 경우의 수를 줄이기 위해, 트랜잭션이 성공할 때만 이벤트 핸들러를 실행한다.
- 스프링의
@TransactionEventListener
은 스프링 트랜잭션 상태에 따라 이벤트 핸들러를 실행할 수 있게 한다.