티스토리 뷰
이벤트 기반 아키텍처 구조
현재 Gloddy 서비스는 그룹 시스템과 알림 시스템으로 구성되어 있습니다. 시스템 간 소통을 위해 이벤트 드리븐 방식을 사용하고 있습니다. 아래는 Gloddy 서비스의 아키텍처를 간단히 그림으로 나타낸 것입니다. 도메인의 비관심사는 메시징 시스템으로 이벤트를 발행하는 것입니다. 따라서, Spring Application Event를 활용해 도메인은 행위에 대한 이벤트를 발행하고, 이를 구독하고 있는 Sns Publish Listener에서 메시징 시스템으로 이벤트를 발행하도록 설계했습니다. 이를 통해 도메인에 영향 없이 메시징 시스템에 대한 연결을 변경 및 확장할 수 있습니다. 메시징 시스템으로는 SNS-SQS를 사용했습니다. 여러 도메인 행위에 대해 SNS 토픽을 생성하고, SQS에서 SNS 토픽을 구독해 메시지를 가져갑니다. 알림 시스템에서는 SQS의 메시지를 consume해 알림 저장 및 알림 전송을 처리합니다.
이벤트 기반 아키텍처 문제점
SNS-SQS는 높은 신뢰성을 제공합니다. 하지만, 그룹 시스템에서 SNS로 이벤트를 발행하는 구간은 HTTP 통신을 사용하기 때문에 이벤트를 발행하는 과정에서 문제가 발생할 수 있습니다. 이를 해결하기 위해 아래와 같이 두가지 방식으로 구현해봤습니다.
1. 하나의 트랜잭션에서 도메인 행위와 이벤트 발행 수행하기
이벤트 발행을 보장하기 위해 아래와 같이 하나의 트랜잭션에서 도메인 행위와 이벤트 발행을 수행하도록 설계했습니다. 이벤트 발행에 실패하면 도메인 행위도 실패하게 됩니다.
Spring Application Event는 트랜잭션 제어 기능을 제공합니다. TransactionalEventListener을 통해 트랜잭션 상태에 따라 Application Event를 consume할 수 있습니다. 따라서, 이를 활용해 트랜잭션 커밋 직전 메시징 시스템으로 이벤트가 발행되도록 구현했습니다. '지원서가 생성되면 모임장에게 알림을 전송한다' 라는 요구사항이 있다고 해보겠습니다. 이를 구현한 코드는 아래와 같습니다.
@Transactional
public void createApply(Long userId, Long groupId, ApplyRequest.Create request) {
User user = userQueryHandler.findById(userId);
Group group = groupQueryHandler.findById(groupId);
Apply apply = applyCommandHandler.save(Apply.create(user, group, request.getIntroduce());
applyEventProducer.produceEvent(new ApplyCreateEvent(apply.getId()));
}
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleApplyCreateEvent(ApplyCreateEvent event) {
ApplyAdapterEvent adapterEvent = ApplyEventMapper.from(event);
snsTemplate.sendNotification(snsProperties.getApplyTopic(), adapterEvent, subject);
}
지원서 저장 후 트랜잭션 커밋 직전 지원서 생성 행위에 대한 도메인 이벤트를 발행합니다. 그 후, 이벤트 구독자에서 해당 이벤트를 consume해 메시징 시스템으로 이벤트를 발행합니다. 이벤트가 정상적으로 발행되었다면 트랜잭션을 커밋합니다. 이벤트 발행에 실패했다면 도메인 행위까지 롤백됩니다. 하지만, 이는 메시징 시스템의 장애가 시스템 전체의 장애로 번질 수 있는 문제가 있습니다. 또한, 발행되지 않아야할 메시지가 발행될 위험이 있습니다. 만약, 메시징 시스템으로 이벤트 발행 후 트랜잭션을 커밋하는 과정에서 커밋에 실패한다면 도메인 행위는 롤백되지만, 외부 이벤트는 이미 발행된 상태입니다. 따라서, 도메인 행위와 이벤트 발행의 정합성을 보장할 수 없습니다.
2. 도메인 행위와 이벤트 발행 트랜잭션 분리하기
메시징 시스템의 장애 전파를 막기 위해 도메인 행위와 이벤트 발행 트랜잭션을 분리했습니다. 하지만, 이 경우 메시징 시스템으로의 이벤트 발행을 보장할 수 없게됩니다. 도메인 행위는 정상적으로 처리되어 트랜잭션이 커밋되었지만, 외부 이벤트 발행에 실패하는 경우 메시지가 유실됩니다. 즉, 발행되어야 하는 메시지가 발행되지 않을 위험이 있습니다. 트랜잭션을 분리하는 방법은 위 코드에서 TransactionalEventListener의 phase 옵션을 AFTER_COMMIT으로 변경하면 됩니다.
도메인 행위와 이벤트 발행 트랜잭션을 분리하면서, 도메인 행위와 이벤트 발행의 정합성을 보장할 수 없게 되었습니다. 이를 해결하기 위해선 이벤트 저장소가 필요합니다. 이벤트 저장소에 대한 자세한 내용은 아래 이어집니다.
Transactional Outbox Pattern
트랜잭션 내부에서 동일한 데이터베이스에 이벤트를 저장해 도메인 행위와 이벤트 발행의 정합성을 보장하는 방식입니다. 도메인 행위를 수행하는 트랜잭션에서 데이터베이스에 이벤트를 저장합니다. 트랜잭션이 커밋되면 데이터베이스에 저장된 이벤트를 조회해 메시징 시스템으로 이벤트를 발행합니다. 도메인 행위가 정상적으로 수행되었다면, 이벤트가 저장될 것이기 때문에 이벤트 저장 여부에 따라 메시징 시스템으로의 이벤트 발행을 처리할 수 있습니다. 즉, 발행되지 않아야할 메시지 발행의 문제를 해결할 수 있습니다. 또한, 이벤트 발행 시 이벤트 발행 여부를 업데이트함으로써 이벤트 발행 여부값에 따라 이벤트를 재발행할 수 있습니다. 이를 통해 발행되어야 하는 메시지가 발행되지 않는 문제를 해결할 수 있습니다. 이처럼, Transactional Outbox Pattern을 통해 도메인 행위와 이벤트 발행의 정합성을 보장할 수 있습니다. 구조는 아래와 같습니다.
왜 동일한 데이터베이스를 사용해야 할까?
도메인 행위와 이벤트 발행의 정합성을 보장하기 위해선 필수적으로 트랜잭션을 사용해야 합니다. 즉, 도메인 행위와 이벤트 저장소로의 이벤트 저장은 하나의 트랜잭션에서 수행되어야 합니다. 만약, 이벤트 저장소의 데이터베이스를 분리한다면 다중 데이터베이스의 분산 트랜잭션을 구현해야 합니다. 분산 트랜잭션을 구현하는 것은 어렵기 때문에 동일한 데이터베이스에 이벤트를 저장해 정합성을 보장하려 합니다.
이벤트 저장소에 저장할 데이터 형태
Zero Payload 방식으로 이벤트를 설계했기 때문에 이벤트의 데이터 형태를 쉽게 정의할 수 있었습니다. 추가로, 이벤트 재발행을 위해 이벤트 발행 여부, 이벤트 발행 시간 컬럼을 추가했습니다.
create table apply_event (
id bigint primary key,
apply_id bigint not null, // 누가(식별자)
event_type varchar(50) not null, // 무엇을 위하여(행위)
created_at datetime not null // 언제(시간)
published tinyint not null, // 이벤트 발행여부
published_at datetime not null // 이벤트 발행시간
)
이벤트 발행 과정
아래는 '지원서 생성 시 모임장에게 알림을 전송한다' 라는 요구사항에 대해 Transactional Outbox Pattern을 적용한 이벤트 발행 과정입니다. 1. 도메인 행위 트랜잭션에서 Spring Application Event를 발행합니다. 2. 지원서 이벤트 저장 구독자에서 해당 이벤트를 consume해 지원서 이벤트를 저장합니다. 그 후, Spring Application Event를 발행합니다. 3. 외부 이벤트 발행 구독자는 해당 이벤트를 consume해 지원서 저장 트랜잭션이 커밋된 후 메시징 시스템으로 이벤트를 발행합니다. 이벤트 발행 후 이벤트 발행 여부를 업데이트 합니다. 4. 배치 작업을 통해 이벤트 발행 여부가 false인 이벤트들에 대해 메시징 시스템으로 이벤트를 재발행합니다.
References
https://techblog.woowahan.com/7835/
https://www.youtube.com/watch?v=b65zIH7sDug&t=1094s