10장 : 이벤트

시스템 간 강결합 문제

  • 강하게 결합되어 있는 바운디드 컨텍스트에서는 여러 문제가 나타날 수 있다.

  • 이벤트를 사용하면 강한 결합을 없앨 수 있다. 특히 비동기 이벤트를 사용하면 두 시스템 간의 결합을 크게 낮출 수 있다.

이벤트 개요

  • '이벤트'라는 용어는 '과거에 벌어진 어떤 것'을 의미한다. 이벤트가 발생한다는 것은 상태가 변경됐다는 것을 의미한다. 이벤트가 발생하면 그 이벤트에 반응하여 원하는 동작을 수행하는 기능을 구현한다.

이벤트 관련 구성요소

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

  • 이벤트

  • 이벤트 생성 주체 - 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체로, 이들은 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생시킨다.

  • 이벤트 디스패처(퍼블리셔) - 이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이 이벤트 디스패처다. 이벤트 생성 주체는 이벤트를 생성해서 디스패터에 이벤트를 전달하고, 이벤트를 전달받은 디스패터는 해당 이벤트를 처리할 수 있는 핸들러에 이벤트를 전파한다. 이벤트 디스패터의 구현 방식에 따라 이벤트 생성과 처리를 동기나 비동기로 실행하게 된다.

  • 이벤트 핸들러(구독) - 이벤트 생성 주체가 발생한 이벤트에 반응하며, 생성 주체가 발생한 이벤트를 전달받아 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다.

이벤트의 구성

이벤트는 발생한 이벤트에 대한 정보를 담는데 다음을 포함한다.

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

  • 이벤트 발생 시간

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

이벤트 용도

  • 트리거(Trigger) - 도메인의 상태가 바뀔 때 다른 후처리가 필요하면 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다.

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

이벤트 장점

  • 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다.

  • 기능 확장에 용이하다.

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

  • 이벤트 클래스 - 이벤트를 표현한다.

  • 디스패처 - 스프링이 제공하는 ApplicationEventPublisher를 이용한다.

  • Events - 이벤트를 발행한다. 이벤트 발행을 위해 ApplicationEventPublisher를 사용한다.

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

이벤트 클래스

  • 이벤트 자체를 위한 상위 타입은 존재하지 않으며 원하는 클래스를 이벤트로 사용하면 된다.

  • 이벤트는 과거에 벌어진 상태 변화나 사건을 의미하므로 이벤트 클래스의 이름을 결정할 때에는 과거 시제를 사용해야 한다.

  • 이벤트 클래스는 이벤트를 처리하는 데 필요한 최소한의 데이터를 포함해야 한다.

  • 모든 이벤트가 공통으로 갖는 프로퍼티가 존재한다면 관련 상위 클래스를 만들 수도 있다.

public abstract class Event {
    private long timestamp;
    
    public Event() {
        this.timestamp = System.currentTimeMillis();
    }
    
    public long getTimestamp() {
        return timestamp;
    }
}
public class OrderCanceledEvent extends Event {
    private String orderNumber;
    public OrderCanceledEvent(String number) {
        super();
        this.orderNumber = number;
    }
    ...
}

Events 클래스와 ApplicationEventPublisher

  • 이벤트 발생과 출판을 위해 스프링이 제공하는 ApplicationEventPublisher를 사용한다.

public class Events {
    private static ApplicationEventPublisher publisher;
    
    static void setPublisher(ApplicationEventPublisher publisher) {
        Events.publisher = publisher;
    }
    
    public static void raise(Object event) {
        if (publisher != null) {
            publisher.publishEvent(event);
        }
    }
}
@Configuration
public class EventConfiguration {
    @Autowired
    private ApplicationContext applicationContext;
    
    @Bean
    public InitializingBean eventInitializer() {
       return () -> Events.setPublisher(applicationContext); 
    } 
}

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

  • 이벤트를 발생시킬 코드는 Events.raise()메서드를 사용한다.

public class Order {
    
    public void cancel() {
        verifyNotYetShipped();
        this.state = OrderState.CANCELED;
        Events.raise(new OrderCanceledEvent(number.getNumber()));
    }
    ...
}
  • 이벤트를 처리할 핸들러는 스프링이 제공하는 @EventListener 애너테이션을 사용해서 구현한다.

@Service 
public class OrderCanceledEventHandler {
    private RefundService refundService;
    
    public OrderCanceledEventHandler(RefundService refundService) {
        this.refundService = refundService;
    }
    
    @EventListener(OrderCanceledEvent.class)
    public void handle(OrderCanceledEvent event) {
        refundService.refund(event.getOrderNumber());
    }
} 

흐름 정리

동기 이벤트 처리 문제

  • 외부 서비스와 연동하는 경우, 외부 서비스의 성능 저하가 시스템의 성능 저하로 연결된다.

  • 트랜잭션 문제 또한 발생하는데, 외부 서비스 실행에 실패했다고 해서 반드시 트랜잭션을 롤백해야 하는지에 대한 문제이다.

  • 외부 시스템과의 연동을 동기로 처리할 때 발생하는 성능과 트랜잭션 범위 문제를 해소하는 방법은 이벤트를 비동기로 처리하거나 이벤트와 트랜잭션을 연계하는 것이다.

비동기 이벤트 처리

'A하면 이어서 B하라'는 요구사항 중에서 'A하면 최대 언제까지 B 하라'로 바꿀 수 있는 요구사항은 이벤트를 비동기로 처리하는 방식으로 구현할 수 있다.

이벤트를 비동기로 구현할 수 있는 방법 중 다음 네 가지를 이 책에서 다루게 된다.

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

  • 메시지 큐를 사용하기

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

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

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

  • 이벤트 핸들러를 비동기로 실행하는 방법은 이벤트 핸들러를 별도 스레드로 실행하는 것이다.

    • @EnableAsync 애너테이션을 사용해서 비동기 기능을 활성화한다.

    • 이벤트 핸들러 메서드에 @Async 애너테이션을 붙인다.

@SpringBootApplication
@EnableAsync
public class shopApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(ShopApplication.class, args);
    }
}
@Service
public class OrderCanceledEventHandler {
    
    @Async
    @EventListener(OrderCanceledEvent.class)
    public void handle(OrderCanceledEvent event) {
        refundService.refund(event.getOrderNumber());
    }
}

스프링은 OrderCanceledEvent가 발생하면 handle() 메서드를 별도 스레드를 이용해서 비동기로 실행한다.

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

비동기로 이벤트를 처리해야 할 때 사용하는 또 다른 방법은 카프카(Kafka)나 래빗MQ(RabbitMQ)와 같은 메시징 시스템을 사용하는 것이다.

  • 이벤트가 발생하면 이벤트를 디스패처가 메시지 큐에 보낸다.

  • 메시지 큐는 이벤트를 메시지 리스너에 전달하고, 메시지 리스너는 알맞은 이벤트 핸들러를 이용해서 이벤트를 처리한다.

  • 이때 이벤트를 메시지 큐에 저장하는 과정과 메시지 큐에서 이벤트를 읽어와 처리하는 과정은 별도 스레드나 프로세스로 처리된다.

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

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

  • 이벤트가 발생하면 핸들러는 스토리지에 이벤트를 저장한다.

  • 포워더는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 실행한다. 포워더는 별도 스레드를 이용하기 문에 이벤트 발행과 처리가 비동기로 처리된다.

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

이벤트 저장소 구현

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

@Component
public class EventStoreHandler {
    private EventStore eventStore;
    
    public EventStoreHandler(EventStore eventStore) {
        this.eventStore = eventStore;
    }
    
    @EventListener(Event.class)
    public void handle(Event event) {
        eventStore.save(event);
    }
}

REST API 구현

@RestController
public class EventApi {
    private EventStore eventStore;
    
    public EventApi(EventStore eventStore) {
        this.eventStore = eventStore;
    }
    
    @GetMapping("/api/events")
    public List<EventEntry> list (
            @RequestParam("offset") Long offset,
            @RequestParam("limit") Long limit) {
        return eventStore.get(offset, limit);
    }
}

포워더 구현

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

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

  • 포워더에서 전송 실패를 얼마나 허용할 것이냐에 대한 것

  • 이벤트 손실에 대한 것

  • 이벤트 순서에 대한 것

  • 이벤트 재처리에 대한 것

멱등성

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

Last updated