Chapter 10: 이벤트
💡 책에서 기억하고 싶은 내용
시스템 간 강결합 문제
도메인 객체를 서비스에 전달하면
설계상 문제
가 발생할 수 있다외부 서비스의
성능
에 영향을 받는 문제도메인 객체에 서로 다른 도메인 로직이
섞이는
문제
바운디드 컨텍스트 간의
강결합 (high coupling)
을 없애기 위해이벤트
를 사용한다특히
비동기 이벤트
를 사용하면 두 시스템 간의 결합을 크게 낮출 수 있다
이벤트 개요
이벤트란?
과거에 벌어진 어떤 것
을 의미한다
이벤트가 발생한다는 것은
상태가 변경됐다
는 것을 의미한다이벤트는 발생하는 것에서 끝나지 않는다
이벤트가 발생하면, 그 이벤트에
반응
하여 원하는 동작을 수행하는 기능을 구현한다
도메인 모델
에서도 UI 컴포넌트와 유사하게 도메인의상태 변경
을 이벤트로 표현할 수 있다~할 때
,~가 발생하면
,만약 ~하면
과 같은 요구사항은 도메인의 상태 변경과 관련된 경우가 많고, 이런 요구 사항을 이벤트를 이용해서 구현할 수 있다
이벤트 관련 구성요소
도메인 모델에 이벤트를 도입하려면 아래 4개의 구성요소를 구현해야 한다
이벤트
이벤트 생성 주체
이벤트 디스패처 (publisher)
이벤트 핸들러 (subscriber)
이벤트 관련 구성요소
이벤트 생성 주체
— 이벤트 —>이벤트 디스패처
— 이벤트 —>이벤트 핸들러
이벤트 생성 주체
도메인 모델에서
이벤트 생성 주체
는 엔티티, 밸류, 도메인 서비스와 같은도메인 객체
이다도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면
관련 이벤트를 발생
시킨다
이벤트 핸들러
(subscriber)이벤트 핸들러
는 이벤트 생성 주체가 발생한 이벤트에 반응한다이벤트 핸들러
는 생성 주체가 발생시킨 이벤트를 전달받아, 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다
이벤트 디스패처
(publisher)이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이
이벤트 디스패처
다이벤트 생성 주체는 이벤트를 생성해서 디스페처에 이벤트를 전달한다
이벤트를 전달받은 디스페처는 해당 이벤트를 처리할 수 있는
핸들러
에 이벤트를전파
한다
이벤트 디스패처의 구현 방식에 따라 이벤트 생성 / 처리를
동기
나비동기
로 실행하게 된다
이벤트의 구성
이벤트는
발생한 이벤트에 대한 정보
를 담으며, 다음을 포함한다이벤트 종류
클래스 이름으로 이벤트 종류를 표현
이벤트 발생 시간
추가 데이터
이벤트와 관련된 정보
ex) 주문번호, 신규 배송지 정보
이벤트는 현재 기준으로 과거에 벌어진 것을 표현하기 때문에 이름에는
과거 시제
를 사용한다이벤트는 이벤트 핸들러가 작업을 수행하는 데 필요한 데이터를 담아야 한다
이벤트
가필요한 데이터
를 담고 있지 않으면,이벤트 핸들러
는 리포지터리, 조회 API, 직접 DB 접근 등의 방식을 통해 필요한 데이터를조회
해야한다
이벤트는 데이터를 담아야 하지만, 그렇다고 이벤트 자체와 필요 없는 데이터를 포함할 필요는 없다
이벤트 용도
이벤트는 크게 두 가지 용도로 쓰인다
트리거
도메인의 상태가 바뀔 때 다른
후처리
가 필요하면, 후처리를 실행하기 위한트리거
로 이벤트를 사용할 수 있다ex)
주문 취소 시 환불 처리를 위한 주문 취소 이벤트
서로 다른 시스템 간의
데이터 동기화
ex)
배송지를 변경하면
외부 배송 서비스
에 바뀐 배송지 정보를 전송해야 한다주문 도메인은 배송지 변경 이벤트를 발생시키고, 이벤트 핸들러는 외부 배송 서비스와 배송지 정보를
동기화
할 수 있다
이벤트 장점
이벤트를 사용하면 서로 다른 도메인 로직이
섞이는 것을 방지
할 수 있다ex)
구매 취소 로직에 이벤트를 적용함으로써 환불 로직을 없앨 수 있다
환불 실행 로직은 주문 취소 이벤트를 받는
이벤트 핸들러
로 이동하게 된다
이벤트를 사용하여 주문 도메인에서 결제 (환불) 도메인으로의
의존을 제거
하게 된다
이벤트 핸들러를 사용하면
기능 확장
도 용이하다ex)
구매 취소 시 환불과 함께 이메일로 취소 내용을 보내고 싶다면, 이벤트 발송을 처리하는 핸들러를 구현하면 된다
기능을 확장해도 구매 취소 로직은 수정할 필요가 없다
이벤트, 핸들러, 디스패처 구현
: 이벤트와 관련된 코드
이벤트 클래스
이벤트를 표현한다
디스패처
스프링이 제공하는
ApplicationEventPublisher
Events
이벤트를 발행한다
이벤트 발행을 위해
ApplicationEventPublisher
를 사용한다
이벤트 핸들러
이벤트를 수신해서 처리한다
스프링이 제공하는 기능을 사용한다
이벤트 클래스
이벤트 자체를 위한
상위 타입
은 존재하지 않는다원하는 클래스를 이벤트로 사용하면 된다
이벤트 클래스의
이름
이벤트는 과거에 벌어진 상태 변화나 사건을 의미하므로, 이벤트 클래스의 이름을 결정할 때에는
과거 시제
를 사용해야 한다ex)
OrderCanceledEvent
와 같이 클래스 이름 뒤에접미사
로Event
를 사용해서 이벤트로 사용하는 클래스라는 것을 명시적으로 표현하거나,OrderCanceled
처럼 간결하게 과거 시제만 사용할 수도 있다
이벤트 클래스의
구성
이벤트 클래스는 이벤트를 처리하는 데 필요한
최소한의 데이터
를 포함해야 한다모든 이벤트가
공통으로 갖는 프로퍼티
가 존재한다면, 관련상위 클래스
를 만들 수도 있다
Events 클래스와 ApplicationEventPublisher
이벤트 발생과 출판을 위해 스프링이 제공하는
ApplicationEventPublisher
를 사용한다스프링 컨테이너
는ApplicationEventPublisher
도 된다Events
클래스는ApplicationEventPublisher
를 사용해서 이벤트를발생
시킨다
흐름 정리
응용 서비스
— 1 —>
도메인 기능을
실행
한다도메인 기능은 Events.raise() 를 이용해서 이벤트를
발생
시킨다Events.raise() 는 스프링이 제공하는 ApplicationEventPublisher를 이용해서 이벤트를 출판한다
ApplicationEventPublisher는 @EventListener(이벤트타입.class) annotation이 붙은 method를 찾아 실행한다
동기 이벤트 처리 문제
외부 서비스에 영향을 받는 문제
외부 서비스의 성능 저하가 시스템의 성능 저하로 연결되는 경우
외부 시스템과의 연동을 동기로 처리할 때 발생하는 성능과 트랜잭션 범위 문제 해소 방법
이벤트를
비동기
로 처리이벤트와
트랜잭션
을 연계
비동기 이벤트 처리
A 하면 이어서 B 하라
는 요구사항 중A 하면 최대 언제까지 B 하라
로 바꿀 수 있는 요구사항은 이벤트를비동기
로 처리하는 방식으로 구현할 수 있다즉, A 이벤트가 발생하면
별도 스레드
로 B를 수행하는 핸들러를 실행하는 방식으로 요구사항을 구현할 수 있다
이벤트로 비동기를 구현하는 방법
로컬 핸들러를 비동기로 실행하기
메시지 큐를 사용하기
이벤트 저장소와 이벤트 포워더 사용하기
이벤트 저장소와 이벤트 제공 API 사용하기
로컬 핸들러 비동기 실행
이벤트 핸들러를 비동기로 실행하는 방법은 이벤트 핸들러를
별도 스레드
로 실행하는 것이다스프링이 제공하는
@Async
annotation을 사용하면 비동기로 이벤트 핸들러를 실행할 수 있다@EnableAsync
annotation을 사용해서 비동기 기능을 활성화 한다스프링 설정 클래스 (
@SpringBootApplication
) annotation이 붙는 클래스에@EnableAsync
annotation을 붙인다
이벤트 핸들러 메서드에
@Async
annotation을 붙인다비동기로 실행할 이벤트 핸들러 method에
@Async
annotation을 붙인다
메시징 시스템을 이용한 비동기 구현
Kafka
나RabbitMQ
와 같은 메시징 시스템을 이용하는 것이벤트가 발생하면 이벤트 디스패처는 이벤트를
메시지 큐
에 보낸다메시지 큐는 이벤트를
리스너
에 전달하고,메시지 리스너는 알맞은
이벤트 핸들러
를 이용해서 이벤트를 처리한다
이벤트를 메시지 큐에 저장하는 과정과 메시지 큐에서 이벤트를 읽어와 처리하는 과정은 별도 thread나 process로 처리된다
필요하다면 이벤트를 발생시키는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를
한 트랜잭션
으로 묶어야 한다도메인 기능을 실행한 결과를 DB에 반영하고, 이 과정에서 발생한 이벤트를 메시지 큐에 저장하는 것을 같은 트랜잭션 범위에서 실행하려면
글로벌 트랜잭션
이 필요하다
메시지 큐를 사용하면 보통 이벤트를 발생시키는 주체와 이벤트 핸들러가 별도 프로세스에서 동작한다
이벤트 발생 JVM
과이벤트 처리 JVM
이 다르다는 것을 의미한다한 JVM에서 이벤트 발생 주체와 이벤트 핸들러가 메시지 큐를 이용해서 이벤트를 주고받을 수 있지만, 동일 JVM에서 비동기 처리를 위해 메시지 큐를 사용하는 것은 시스템을 복잡하게 만들 뿐이다
RabbitMQ
처럼 많이 사용되는 메시징 시스템은글로벌 트랜잭션
지원과 함께클러스터 고가용성
을 지원하기 때문에 안정적으로 메시지를 전달할 수 있는 장점이 있다Kafka
는 글로벌 트랜잭션을 지원하진 않지만, 다른 메시징 시스템에 비해 높은 성능을 보여준다
이벤트 저장소를 이용한 비동기 처리
포워더 방식
이벤트를 일단 DB에 저장한 뒤에, 별도 프로그램(포워더)을 이용해서 이벤트 핸들러에 전달하는 것이다
포워더
는 이벤트를 주기적으로 읽어와 전달하며, 어디까지 전달했는지 추적한다
이벤트가 발생하면 핸들러는
스토리지
에 이벤트를 저장한다포워더
는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를실행
한다포워더
는별도 스레드
를 이용하기 때문에 이벤트발행
과처리
가 비동기로 처리된다
이 방식은 도메인의 상태와 이벤트 저장소로
동일한 DB
를 사용한다즉, 도메인의 상태 변화와 이벤트 저장이
로컬 트랜잭션
으로 처리된다
이벤트를
물리적 저장소
에 저장하기 때문에 이벤트 처리에실패
할 경우, 포워더는 다시 이벤트 저장소에서 이벤트를 읽어와 핸들러를실행
하면 된다포워더 구현
일정 주기로 `EventStore에서 이벤트를 읽어와 이벤트 핸들러에 전달한다
API 방식 client와 마찬가지로, 마지막으로 전달한 이벤트의 offset을 기억해 두었다가 다음 조회 시점에 마지막으로 처리한 offset 부터 이벤트를 가져오면 된다
API 방식
API 방식과 포워더 방식의 차이는 이벤트를 전달하는 방식에 있다
포워더 방식이
포워더
를 이용해서 이벤트를 외부에 전달한다면,API 방식은
외부 핸들러
가 API 서버를 통해 이벤트 목록을 가져간다
포워더 방식은 이벤트를 어디까지 처리했는지 추적하는 역할이 포워더에 있다면,
API 방식에서는 이벤트 목록을 요구하는 외부 핸들러가 자신이 어디까지 이벤트를 처리했는지 기억해야 한다
API를 사용하는 client는 일정 간격으로 다음 과정을 실행한다 1. 가장 마지막에 처리한 데이터의
offset
인lastOffset
을 구한다 - 저장한lastOffset
이 없으면 0을 사용한다 2. 마지막에 처리한lastOffset
을 offset으로 사용해서 API를 실행한다 3. API 결과로 받은 데이터를 처리한다 4. offset + 데이터 개수를lastOffset
으로 저장한다 - 마지막에 처리한lastOffset
을 저장하는 이유 - 같은 이벤트를중복
해서 처리하지 않기 위해!클라이언트 API를 이용해서 언제든지 원하는 이벤트를 가져올 수 있기 때문에 이벤트 처리에
실패
하면 다시 실패한 이벤트로부터 읽어와 이벤트를재처리
할 수 있다API 서버에
장애
가 발생한 경우에도 주기적으로재시도
를 해서 API 서버가 살아나면 이벤트를처리
할 수 있다
이벤트 적용 시 추가 고려 사항
이벤트 소스를
EventEntry
에 추가할지 여부EventEntry
는 이벤트 발생 주체에 대한 정보를 갖지 않는다만약 특정 주체가 발생시킨 이벤트만 조회하는 기능을 구현하려면 이벤트에
발생 주체 정보
를 추가해야 한다
포워더
에서 전송 실패를 얼마나 허용할 것인지?포워더는 이벤트 전송에 실패하면 실패한 이벤트로부터 다시 읽어와 전송을 시도한다
but, 특정 이벤트에서 계속 전송에 실패하면 그 이벤트 때문에 나머지 이벤트를 전송활 수 없게 된다
따라서 포워더를 구현할 때는 실패한 이벤트의
재전송 횟수 제한
을 두어야 한다ex)
동일 이벤트 3회 실패 시 해당 이벤트 생략 및 다음 이벤트로 넘어가기
또는 처리에 실패한 이벤트를 생략하지 않고 별도 실패용 DB나 메시지 큐에 저장하기
이후 실패 분석이나 후처리에 도움이 된다
이벤트 손실
이벤트 저장소를 사용하면 이벤트 발생과 이벤트 저장을 한 트랜잭션으로 관리하기 때문에 트랜잭션에 성공하면 이벤트가 저장소에 보관되는 것을 보장할 수 있지만,
로컬 핸들러를 이용해서 이벤트를 비동기로 처리할 경우 이벤트 처리에 실패하면 이벤트를
유실
하게 된다
이벤트 순서
이벤트 발생 순서대로 외부 시스템에 전달해야 할 경우,
이벤트 저장소
를 사용하는 것이 좋다이벤트 저장소는 저장소에 이벤트를 발생 순서대로 저장하고 그 순서대로 이벤트 목록을 제공하기 때문!
but, 메시징 시스템은 사용 기술에 따라 이벤트 전달 순서와 메시지 전달 순서가 다를 수 있다
이벤트 재처리
가장 쉬운 방법은 마지막으로 처리한 이벤트의 순번을 기억해 두었다가, 이미 처리한 순번의 이벤트가 도착하면 해당 이벤트를 처리하지 않고 무시하는 것이 있다
또는 이벤트를
멱등
으로 처리하는 방법도 있다이벤트 핸들러가
멱등성
을 가지면 시스템 장애로 인해 같은 이벤특 중복해서 발생해도 결과적으로 동일한 상태가 된다이는 이벤트 중복 발생이나 중복 처리에 대한 부담을 줄여준다
이벤트 처리와 DB 트랜잭션 고려
이벤트 처리를 동기로 하든 비동기로 하든
이벤트 처리 실패
와트랜잭션 실패
를 함께 고려해야 한다트랜잭션이 성공할 때만 이벤트 핸들러를 실행하는 방법
스프링의
@TransactionalEventListener
annotation 사용하기스프링 트랜잭션 상태에 따라 이벤트 핸들러를 실행할 수 있게 한다
트랜잭션이 성공할 때만 이벤트 핸들러를 실행하게 되면,
트랜잭션 실패
에 대한 경우의 수가 줄어들어 이벤트 처리 실패만 고려해야 한다
이벤트 특성에 따라
재처리 방식
을 결정하면 된다
Last updated