Skip to main content

10. 이벤트

시스템 간 강결합의 문제#

  • 쇼핑물에서 구매를 취소하면 환불을 처리해야 합니다.
  • 보통 결제 시스템은 외부에 존재하므로 외부의 환불 서시스템 서비스를 호출하는데 두가지 문제가 발생합니다.
    • 외부 서비스가 정상이 아닐 경우 트랜잭션 처리를 어떻게 해야 할지 애매하다는 것입니다.
    • 환불을 처리하는 외부 시스템의 응답시간이 길어질수록 대기시간이 발생하는 성능에 대한 이슈가 발생합니다.
    • 이 문제 외에도 설계상 문제가 나타날 수 있습니다.
  • 위와 같은 강한 결함을 없앨 수 있는 방법으로 이벤트를 사용해 결합을 나출 수 있습니다.

이벤트 개요#

  • 해당 챕터에서 이벤트(event)는 "과거에 벌어진 어떤 것"을 의미합니다.
  • 이벤트가 발생하면 상태가 변경됐다를 의미합니다.

이벤트 관련 구성요소#

  • 도메인 모델에 이벤트를 도입하려면 같은 네 개의 구성요소를 구현해야 합니다.

이벤트 관련 구성요소

  • 도메인 모델에서 이벤트 주체는 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체입니다.
    • 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 실행합니다.
  • 이벤트 핸들러(handler)는 이벤트 생성 주체가 발생한 이벤트에 반응합니다.
  • 이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이 이벤트 디스패처(dispatcher)입니다.

이벤트의 구성#

  • 이벤트는 발생한 이벤트에 대한 정보를 담습니다.
    • 이벤트 종류 : 클래스 이름으로 이벤트 종류를 표현
    • 이벤트 발생 시간
    • 추가 데이터 : 주문번호, 신규 배송지 정보 등 이벤트와 관련된 정보
  • 이벤트는 이벤트 핸들러가 작업을 수행하는 데 필요한 최소한의 데이터를 담아야합니다.
  • 이벤트는 데이터를 담아야 하나 이벤트 자체와 관련 없는 데이터를 포함할 필요는 없습니다.

이벤트 용도#

  • 이벤트는 크게 두 가지 용도로 쓰입니다.
    • 첫번째 용도는 트리거입니다.
    • 두번째 용도는 서로 다른 시스템 간의 데이터 동기화입니다.

이벤트 장점#

  • 이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있습니다.

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

  • 이벤트와 관련된 코드는 다음과 같습니다.
    • 이벤트 클래스
    • 이벤트 핸들러(EventHandler) : 이벤트 핸들러를 위한 상위 타입으로 모든 핸들러는 이 인터페이스를 구현합니다.
    • 이벤트(Events) : 이벤트 디스패처, 이벤트 발행, 이벤트 핸들러 등록, 이벤트를 핸들러에 등록하는 등의 기능을 제공

이벤트 클래스#

  • 이벤트 자체를 위한 상위 타입은 존재하지 않습니다.
  • 이벤트 구성에서 설명한 것처럼 이벤트 클래스는 이벤트를 처리하는 데 필요한 최소한의 데이터를 포함해야합니다.
  • 모든 이벤트가 공통으로 갖는 프로퍼티가 존재한다면 관련 상위 클래스를 만들 수도 있습니다.
public abstract class Event {
private long timestamp;
public Event() {
this.timestamp = System.currentTimeMillis();
}
public long getTimestamp(){
return timestamp;
}
}

EventHandler 인터페이스#

  • EventHandler 인터페이스는 이벤트 핸들러를 위한 상위 인터페이스입니다.
public interface EventHandler<T> {
void handle(T event);
default boolean canHandle(Object event) {
Class<?>[] typeArgs = TypeResolver.resolveRawArguments(EventHandler.class, this.getClass());
return typeArgs[0].isAssignableFrom(event.getClass());
}
}
  • EventHandler 인터페이스를 상속받는 클래스는 handle() 메서드를 이용해서 필요한 기능을 구현하면 됩니다.

이벤트 디스패처인 Events 구현#

  • 이벤트 핸들러 등록을 쉽게 하기 위해 정적 메서드를 이용해서 구현합니다.
  • 이벤트를 발생시킬 때에는 Events.raise() 메서드를 사용합니다.
  • Events.raise()를 이용해서 이벤트를 발생시키면 Events.raise() 메서는 이벤트를 처리할 핸들러를 찾아 handle()메서드를 실행합니다.

흐름 정리#

이벤트 처리 흐름

AOP를 이용한 Events.reset() 실행#

  • 응용 서비스가 끝나면 ThreadLocal에 등록된 핸들러 목록을 초기화하기 위해 Events.reset() 메서드를 실행합니다.

동기 이벤트 처리 문제#

  • 이벤트를 사용하면 강결합 문제는 해결이 되지만, 외부 서비스에 영향을 받는 문제가 있습니다.
  • 외부 서비스의 성능 저하가 시스템의 성능 저하로 연결됩니다.
  • 또한 이는 성능 저하뿐만 아니라 트랜잭션도 문제가 됩니다.
  • 외부 시스템과의 연동을 동기로 처리할 때 발생하는 성능과 트랜잭션 범위 문제를 해소하는 방법 중 하나가 이벤트를 비동기로 처리하는 것입니다.

비동기 이벤트 처리#

  • "A하면 이어서 B하라"는 내용을 담고 있는 요구사항은 실제로 "A하면 최대 언제까지 B하라" 인 경우가 많습니다.
  • 이러한 요구사항은 이벤트를 비동기로 처리하는 방식으로 구현할 수 있습니다.
  • 다음의 네가지 방식으로 비동기 이벤트 처리를 구현할 수 있습니다.
    • 로컬 핸들러를 비동기로 실행하기
    • 메시지 큐를 사용하기
    • 이벤트 저장소와 이벤트 포워더 사용하기
    • 이벤트 저장소와 이벤트 제공 API 사용하기

로컬 핸들러의 비동기 실행#

  • 이벤트 핸들러를 비동기로 실행하는 방법은 이벤트 핸들러를 별도 스레드로 실행하는 것입니다.
  • 동기나 비동기로 실행할 이벤트 핸들러를 처리하는 방식은 거의 유사하나, 비동기 이벤트 핸들러는 스레드 풀에 핸들러 실행작업을 등록하나 동기로 실행할 이벤트 핸들러는 바로 실행한다는 사실 입니다.

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

  • 비동기로 이벤트 처리해야 할 때 사용하는 또 다른 방법은 RabbitMQ와 같은 메시징 큐를 사용하는 것입니다.
    • 이벤트가 발생하면 이벤트 디스패처는 이벤트를 메시지 큐에 보냅니다.
    • 메시지 큐는 이벤트를 메시지 리스너에 전달하고, 메시지 리스터는 알맞은 이벤트 핸들러를 이용해서 이벤트를 처리합니다.
    • 이벤트를 메시지 큐에 저장하는 과정과 메시지 큐에서 이벤트를 읽어와 처리하는 과정은 별도 스레드나 프로세스로 처리됩니다.
  • 필요하다면 이벤트를 발생하는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶을 수도 있습니다.
  • 글로벌 트랜잭션을 사용하면 안전하게 이벤트를 메시지 큐에 전달할 수 있는 장점이 있지만, 반대로 글로벌 트랜잭션으로 인해 전체 성능이 떨어지는 단점도 있습니다.
  • 많은 경우 메시지 큐를 사용하면 보통 이벤트를 발생하는 주체와 이벤트 핸들러가 별도 프로세스에서 동작합니다.
  • 일반적으로 많이 사용되는 메시징 시스템은 글로벌 트랜잭션 지원과 함께 클러스터와 고가용성을 지원하기 때문에 안정적으로 메시지를 전달할 수 있는 장점이 있습니다.
    • 또한 다양한 개발 언어와 통신 프로토콜을 지원하고 있습니다.

메시징 큐를 이용한 이벤트 비동기 처리

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

  • 비동기로 이벤트를 처리하기 위한 또 다른 방법은 이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용해서 이벤트 핸들러에 전달하는 것입니다.

이벤트 저장소와 포워더를 이용한 비동기 처리

  • 이벤트가 발생하면 핸들러는 스토리지에 이벤트를 저장합니다. 포워더는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 실행합니다.
  • 도메인 상태 변화와 이벤트 저장이 로컬 트랜잭션으로 처리됩니다.
  • 이벤트 저장소를 이용한 두번째 방법은 이벤트를 외부에 제공하는 API를 사용하는 것입니다.

API를 이용해서 이벤트를 외부에 제공하는 방식

  • API와 포워더의 방식의 차이는 이벤트를 전달하는 방식에 있으며, 포워더 방식에서는 포워더를 이용해서 이벤트를 이용해서 이벤트를 외부에 전달하는 방식이라면, API 방식에서는 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져오는 방식입니다.

이벤트 저장소 구현#

  • 포워드 방식과 API 방식 모두 이벤트 저장소를 사용하므로 이벤트를 저장할 저장소가 필요합니다.
  • 이벤트는 과거에 벌어진 사건이므로 데이터가 변경되지 않습니다. 따라서 EventStore 인터페이스는 새로운 이벤트를 추가하는 기능과 조회하는 기능만 제공하고 기존 이벤트 데이터를 수정하는 기능은 제공하지 않습니다.

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

  • EventStoreHandler를 이벤트 핸들러로 사용하려면 응용 서비스의 메서드 마다 Events.handle()로 등록해야 합니다.
  • 모든 응용 서비스에 대해 코드를 추가하면 많은 중복이 발생하므로 중복을 제거하기 위해 AOP를 사용하면 좋습니다.

REST API 구현#

  • REST API는 단순합니다.
  • 클라이언트 API를 이용해서 언제든지 원하는 이벤트를 가져올 수 있기 때문에 이벤트 처리에 실패하면 다시 실패한 이벤트로부터 읽어와 이벤트를 재처리할 수 있습니다.

포워드 구현#

  • 포워더는 앞의 API 방식에서 클라이언트의 구현과 유사합니다.
  • 포워더는 일정 주기로 EventStore로부터 이벤트를 읽어와 이벤트 핸들러에 전달하면 됩니다.

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

  • 이벤트를 구현할 때 추가로 고현할 점은 다섯가지입니다.
    • 첫번째는 이벤트 소스를 EventEntry에 추가할지 여부입니다.
    • 두번째는 포워더에서 전송 실패를 얼마나 허용할 것이냐에 대한 것입니다.
    • 세번째는 이벤트 손실에 대한 것입니다.
    • 네번째는 이벤트 순서에 대한 것입니다.
    • 다섯번째는 이벤트 재처리에 대한 것입니다.
Last updated on