10. 이벤트
시스템 간 강결합 문제
도메인 객체에서 기능을 제공하는 도메인 서비스를 파라미터로 전달받고 실행하는 경우나, 응용 서비스에서 외부 서비스를 실행할 경우 크게 세 가지 문제가 발생한다.
외부 서비스가 정상이 아닐 경우 트랜잭션 처리를 어떻게 해야 할지 애매하다.
환불을 처리하는 외부 시스템의 응답 시간이 길어지면 그만큼 대기 시간이 발생한다.
도메인 객체에 서비스를 전달하면 서로 다른 도메인 로직이 섞이는 문제가 발생한다.
이런 문제가 발생하는 이유는 두 BOUNDED CONTEXT 간의 강결합(high coupling)
때문이다. 이런 강결합을 없을 수 있는 방법이 비동기 이벤트
를 사용하는 것이다.
이벤트 개요
이벤트(event)
라는 용어는 과거에 벌어진 어떤 것을 의미한다. 이베트가 발생한다는 것은 상태
가 변경됐다는 것을 의미한다. 이벤트는 발생하는 것에서 끝나지 않고 그 이벤트에 반응하여 원하는 동작을 수행하는 기능을 구현한다.
도메인 모델에서도 도메인의 상태 변경
을 이벤트로 표현할 수 있다. ~할 때, ~가 발생하면, 만약~하면과 같은 요구사항은 도메인의 상태 변경과 관련된 경우가 많고 이런 요구사항을 이벤트를 이용해서 구현할 수 있다.
이벤트 관련 구성요소
도메인 모델에 이벤트를 도입하려면 다음과 같은 네 개의 구성요소를 구현해야 한다.
이벤트 생성 주체 → 이벤트 → 이벤트 디스패쳐(퍼블리셔) → 이벤트 → 이벤트 핸들러
도메인 모델에서
이벤트 주체
는 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체다. 도메인 객체는 모데인 로직을 실행해 상태가 바뀌면 관련 이벤트를 발생시킨다.이벤트 핸들러(handler)
는 이벤트 생성 주체가 발생한 이벤트를 전달받아 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다.이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이
이벤트 디스패쳐(dispatcher)
이다. 이벤트 생성 주체는 이벤트를 생성해서 디스패처에 이벤트를 전달한다. 이벤트를 전달받은 디스패처는 해당 이벤트를 처리할 수 있는 핸들러에 이벤트를 전파한다. 이벤트 디스패쳐의 구현 방식에 따라 이벤트 생성과 처리를 동기나 비동기로 실행하게 된다.
이벤트의 구성
이벤트는 발생한 이벤트에 대한 정보를 담는다. 이 정보는 다음을 포함한다.
이벤트 종류
: 클래스 이름으로 이벤트 종류를 표현이벤트 발생 시간
추가 데이터
: 주문번호, 신규 배송지 정보 등 이벤트와 관련된 정보
이벤트의 용도
이벤트는 크게 두 가지 용도로 쓰인다.
첫 번째 용도는 트리거
이다. 도메인의 상태가 바뀔 때 다른 후처리를 해야 할 경우 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다.
두 번째 용도는 서로 다른 시스템 간의 데이터 동기화
이다.
이벤트 장점
이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다.
이벤트 핸들러를 사용하면 기능 확장이 용이하다.
동기 이벤트 처리 문제
이벤트를 사용해서 강결합 문제는 해소했지만 외부 서비스에 영향을 받는 문제가 남아있다.
이벤트를 동기식으로 처리하면 외부 서비스의 처리 완료까지 기다리므로 외부 서비스의 성능 저하는 내 시스템의 성능 저하
로 연결된다.
또한 트랜잭션
도 문제가 된다. 외부 서비스 실행에 실패했다고 반드시 트랜잭션을 롤백해야 하는지에 대한 문제다.
외부 시스템과의 연동을 동기로 처리할 떄 발생하는 성능과 트랜잭션 문제는 이벤트를 비동기
로 처리하여 해결할 수 있다.
비동기 이벤트 처리
우리가 구현해야 할 요구사항중 A하면 이어서 B하라라는 내용을 담고 있는 요구사항은 실제로 A하면 최대 언제까지 B하라인 경우가 많다. 즉, 후속 처리를 바로 할 필요 없이 일정 시간 안에만 처리하면 되는 경우가 적지 않다. A하면 최대 언제까지 B하라라는 요구사항은 이벤트를 비동기
로 처리하는 방식으로 구현할 수 있다. 다시 말해, A 이벤트가 발생하면 별도 스레드
로 B를 수행하는 핸들러를 실행하는 방식으로 요구사항을 구현할 수 있다.
이벤트를 비동기로 구현할 수 있는 방법은 매우 다양하며 다음과 같은 방법이 있다.
로컬 핸들러를 비동기로 실행하기
메세지 큐 사용하기
이벤트 저장소와 이벤트 포워더 사용하기
에벤트 저장소와 이벤트 제공 API 사용하기
로컬 핸들러의 비동기 실행
이벤트 핸들러를 비동기로 실행하는 방법은 이벤트 핸들러를 별도 스레드
로 실행하는 것이다. 별도 스레드를 이용해서 이벤트 핸들러를 실행하면 이벤트 발생 코드와 같은 트랜잭션 범위에 묶을 수 없기 때문에 한 트랜잭션으로 실행해야 하는 이벤트 핸들러는 비동기로 처리해서는 안 된다.
메시징 시스템을 이용한 비동기 구현
비동기로 이벤트를 처리할 때 사용하는 또 다른 방법은 RabbitMQ와 같은 메시징 큐를 사용하는 것이다. 이벤트가 발생하면 이벤트 디스패쳐는 이벤트를 메시지 큐에 보낸다. 메시지 큐는 이벤트를 메시지 리스너에 전달하고, 메시지 리스너는 알맞은 이벤트 핸들러를 이용해서 이벤트를 처리한다. 이떄 이벤트를 메시지 큐에 저장하는 과정과 메시지 큐에서 이벤트를 읽어와 처리하는 과정은 별도 스레드나 프로세스로 처리된다.
필요하다면 이벤트를 발생하는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶어야 한다. 도메인 기능을 실행한 결과를 DB에 반영하고 이 과정에서 발생한 이벤트를 메시지 큐에 저장하는 것을 같은 트랜잭션 범위에서 실행하려면 글로벌 트랜잭션
이 필요하다.
글로벌 트랜잭션을 사용하면 안전하게 이벤트를 메시지 큐에 전달할 수 있는 장점이 있지만, 반대로 글로벌 트랜잭션로 인해 전체 성능이 떨어지는 단점도 있다.
RabbitMQ는 글로벌 트랜잭션 지원과 함꼐 클러스터와 고가용성을 지원하므로 안정적으로 메시지를 전달할 수 있는 장점이 있다.
Kafka는 글로벌 트랜잭션을 지원하지는 않지만 다른 메시징 시스템에 비해 높은 성능을 보여준다.
이벤트 저장소를 이용한 비동기 처리
비동기로 이벤트를 처리하기 위한 또 다른 방법은 이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용해서 이벤트 핸들러에 전달하는 것이다.
이벤트 저장소와 포워더를 이용한 비동기 처리
이벤트가 발생하면 핸들러는 스토리지에 이벤트를 저장한다.
포워더
는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 실행한다. 포워더는 별도 스레드를 이용하기 때문에 이벤트 발행과 처리가 비동기로 처리된다.이 방식은 이벤트를
물리적 저장소
에 보관하기 때문에 핸들러가 이벤트 처리에 실패한 경우, 포워더는 다시 이벤트 저장소에서 이벤트를 읽어와 핸들러를 실행하면 된다.API를 이용해서 이벤트를 외부에 제공하는 방식
포워더 방식에서는 포워더를 이용해서 이벤트를 외부에 전달하는 방식이라면, API 방식에서는 외부 핸들러가
API 서버
를 통해 이벤트 목록을 가져오는 방식이다. 포워더 방식은 이벤트를 어디까지 처리했는지 추적하는 역할이 포워더에 있다면, API 방식에서는 이벤트 목록을 요구하는외부 핸들러
가 자신이 어디까지 이벤트를 처리했는지 기억해야 한다.
이벤트 적용 시 추가 고려사항
이벤트 소스를 추가할지 여부.
이벤트 소스를 추가하여 특정 주체가 발생한 이벤트만 조회하는 기능을 구현할 수 있다.
포워더에서 전송 실패를 얼마나 허용할 것인가.
포워더는 이벤트 전송에 실패하면 실패한 이벤트부터 다시 읽어와 전송을 시도한다. 그러나 특정 이벤트에서 계속 전송에 실패하면 그 이벤트 때문에 나머지 이벤트를 전송할 수 없게 된다. 따라서, 포워더를 구현할 때는 실패한 이벤트의 재전송 횟수에 제한을 두어야 한다.
이벤트 손실
이벤트 저장소가 아닌 로컬 핸들러를 이용해서 이벤트를 비동기로 처리할 경우 이벤트 처리에 실패하면 이벤트를 유실하게 된다.
이벤트 순서
이벤트를 발생 순서대로 외부 시스템에 전달할 경우 이벤트 저장소를 사용하는 것이 좋다. 반면 메시징 시스템은 사용 기술에 따라 이벤트 발생 순서와 메시지 전달 순서가 다를 수 있다.
이벤트 재처리
동일한 이벤트를 다시 처리해야 할 때 이벤트를 어떻게 할지 결정해야 한다. 가장 쉬운 방법은 마지막으로 처리한 이벤트의 순번을 기억해 두었다가 이미 처리한 순번의 이벤트가 도착하면 해당 이벤트를 처리하지 않고 무시하는 것이다. 이 외에 이벤트 처리를 멱등(연산을 여러 번 적용해도 결과가 달라지지 않는 성질)으로 처리하는 방법도 있다.
Last updated
Was this helpful?