'A 하면 최대 언제까지 B 하라'로 바꿀 수 있는 요구사항은 이벤트를 비동기로 처리하는 방식으로 구현할 수 있다.
구현해야 하는 것 중에서 'A 하면 이어서 B 하라'는 내용을 담고 있는 요구사항은 실제로 **'A 하면 최대 언제까지 B 하라'**인 경우가 많다.
즉, 일정 시간 안에만 후속 조치를 처리하면 되는 경우가 적지 않다.
게다가 'A 하면 이어서 B 하라'는 요구사항에서 B를 하는 데 실패하면 일정 간격으로 재시도를 하거나 수동 처리를 해도 상관없는 경우가 있다.
'A 하면 일정 시간 안에 B 하라'는 요구사항에서 'A 하면'은 이벤트로 볼 수 있다.
따라서 'B 하라' 기능은 A 이벤트를 처리하는 핸들러에서 보낼 수 있다.
대표적으로 다음 네 가지 방식으로 비동기 이벤트 처리를 구현할 수 있다.
- 로컬 핸들러를 비동기로 실행하기
- 메시지 큐를 사용하기
- 이벤트 저장소와 이벤트 포워더 사용하기
- 이벤트 저장소와 이벤트 제공 API 사용하기
이벤트 핸들러를 비동기로 실행하는 방법은 별도 스레드를 통해 실행하는 것이다.
@Async 어노테이션을 사용해 손쉽게 비동기로 이벤트 핸들러를 실행할 수 있다.
이를 위해 @EnableAsync 어노테이션으로 비동기 기능을 활성화시키고, 이벤트 핸들러 메서드에 @Async 어노테이션을 붙인다.
카프카(Kafka)나 레빗MQ(RabbitMQ)와 같은 메시징 시스템을 사용해 비동기로 이벤트를 처리할 수 있다.
다음과 같은 흐름을 통해 메시지 큐가 이벤트를 처리한다.
필요하다면 글로벌 트랜잭션을 통해 이벤트를 발생시키는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶을 수 있다.
글로벌 트랜잭션을 사용하면 안전하게 이벤트를 메시지 큐에 전달할 수 있지만 전체 성능이 떨어지고 지원하지 않는 메시징 시스템도 있다.
메시지 큐를 사용하면 보통 이벤트를 발생시키는 주체와 이벤트 핸들러가 별도 프로세스에서 동작한다.
이는 이벤트 발생 JVM과 이벤트 처리 JVM이 다르다는 것을 의미한다.
이벤트를 일단 DB에 저장한 뒤 별도 프로그램을 이용해 이벤트 핸들러에 전달할 수도 있다.
이벤트가 발생하면 핸들러는 스토리지에 이벤트를 저장한다.
포워드는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 실행한다.
이 방식은 도메인의 상태와 이벤트 저장소로 동일한 DB를 사용하므로 도메인의 상태 변화와 이벤트 저장이 로컬 트랜잭션으로 처리된다.
이벤트를 물리적 저장소에 보관하기 때문에 이벤트 처리에 실패할 경우 포워더는 다시 이벤트 저장소에서 이벤트를 읽어와 핸들러를 실행하면 된다.
또는 다음과 같이 외부 API를 사용할 수도 있다.
API 방식과 포워더 방식의 차이는 이벤트를 전달하는 방식에 있다.
포워더 방식이 포워더를 이용해 이벤트를 외부에 전달한다면, API 방식은 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져간다.
포워더 방식은 이벤트를 어디까지 처리했는지 추적하는 역할이 포워더에 있다면, API 방식은 외부 핸들러 자신이 기억해야 한다.
이벤트 저장소를 구현한 코드 구조는 다음과 같다.
- EventEntry - 이벤트 저장소에 보관할 데이터이다.
- EventStore - 이벤트를 저장하고 조회하는 인터페이스를 제공한다.
- JdbcEventStore - JDBC를 이용한 EventStore 구현 클래스이다.
- EventApi - REST API를 이용해 이벤트 목록을 제공하는 컨트롤러이다.
EventEntry를 저장할 테이블의 DDL이다.
create table evententry (
id int not null AUTO_INCREMENT PRIMARY KEY,
`type` varchar(255),
`content_type` varchar(255),
payload MEDIUMTEXT,
`timestamp` datetime
) character set utf8mb4;
EventStoreHandler의 handle() 메서드는 eventStore.save() 메서드를 이용해 이벤트 객체를 저장한다.
offset과 limit의 웹 요청 파라미터를 이용해 EventStore의 get() 메서드를 실행하고 결과를 JSON으로 반환하면 된다.
EventApi가 처리하는 URL에 연결하면 JSON 형식으로 EventEntry 목록을 구할 수 있다.
API를 사용하는 클라이언트는 일정 간격으로 다음 과정을 실행한다.
- 가장 마지막에 처리한 데이터의 offset인 lastOffset을 구한다. 저장한 lastOffset이 없으면 0을 사용한다.
- 마지막에 처리한 lastOffsetdmf offset으로 사용해 API를 실행한다.
- API 결과로 받은 데이터를 처리한다.
- offset + 데이터 개수를 lastOffset으로 저장한다.
클라이언트 API를 이용해 이벤트가 실패하면 다시 실패한 이벤트부터 읽어와 재처리할 수 있다.
또한 API 서버에 장애가 발생한 경우에도 주기적으로 재시도를 해서 서버가 살아나면 이벤트를 처리할 수 있다.
포워더는 API 방식의 클라이언트와 유사하게 일정 주기로 EventStore에서 이벤트를 읽어와 이벤트 핸들러에 전달하면 된다.
마지막으로 전달한 이벤트의 offset을 기억해 두었다가 다음 조회 시점에 마지막으로 처리한 offset부터 이벤트를 가져오면 된다.
비동기 방식으로 이벤트를 처리하는 여러 가지 방법에 대해 알아보았다.
가장 간단하게 로컬 핸들러를 @Async 어노테이션을 활용해 처리하는 방법이 있고, 메시징 시스템을 이용해 별도 프로세스에서 작업할 수 있도록 하는 방법이 있다.
또한 이벤트를 물리적으로 저장해 이벤트 실패에 안전하게 이벤트를 실행할 수 있는 방법이 있었다.
지금 당장 도전해볼 만한 것은 로컬 핸들러 비동기 처리와 메시징 시스템을 이용해 처리하는 것을 해보고 싶다.
이벤트와 후속처리에 대한 개념을 'A 하면 이어서 B 하라'라는 문장을 통해 확실하게 이해할 수 있었다.