분산 시스템에서 각각의 서비스를 분리할수록 트랜잭션을 보장하기 어려워지는 문제점이 있다. 하지만 데이터 정합성의 중요도는 누구라도 공감하는 부분일 것이다. 이번 포스트에서는 서비스간의 소통을 위해 메시징 방식을 사용하고 데이터의 정합성을 최대한 유지할 수 있는 방법을 알아보고자 한다.
일반적인 기능의 데이터 변경은 트랜잭션 내에서 이루어진다. 문제는 이벤트 메시지의 발행으로 전달되는 데이터는 트랜잭션을 보장 받지 못한다는 점이다. 발행되어야 할 메시지가 발행되지 않기도 하고 발행되지 말아야 할 메시지가 발행됨으로서 데이터의 정합성을 해치는 주된 원인이 된다.
기존에는 메시지 발행 시 트랜잭션의 마지막에서 발행하여 롤백 위험을 최소화하는 방법을 선택했었다. (메시지 발행 실패보다 서비스 로직의 롤백 확률이 높다고 판단)
트랜잭션이 커밋된다면 메시지가 반드시 발행되어야 하고, 트랜잭션이 롤백된다면 메시지가 발행되지 않아야 한다.
Outbox
보낼 편지함 (새로 작성한 이메일을 보내기 전에 그 메시지가 저장되는 곳)
Outbox Pattern은 트랜잭션 내에서 메시지를 즉시 발행하는 것이 아닌 보낼 메시지를 담는 공간을 마련한다.
Outbox 테이블을 생성하여 서비스 로직과 함께 보낼 메시지 정보가 트랜잭션 내에서 원자성을 보장받기 때문에 모두 저장되거나 모두 저장되지 않게 된다.
create table outbox
(
outbox_id bigint auto_increment primary key,
created_date_time datetime default current_timestamp not null,
topic varchar(255) not null,
message varchar(255) not null,
send_yn tinyint default 0 not null
);
이제 Outbox 테이블에서 보낼 메시지를 꺼내 발행해야 한다. Message Relay을 구현하는 방법에는 Polling publisher
, Transaction log tailing
두 가지 방식이 있지만 Polling publisher 방식을 예시로 살펴보고자 한다.
@Scheduled(fixedRate = 1000)
public void publish() {
Outbox message = outboxRepository.findByNotSendOrderById();
publisher.publish(message);
message.updateSendYn(true);
}
스케줄링을 통해 Outbox의 메시지를 가져와 발행하고 발송 완료
처리를 합니다.
Q. 처리 속도가 늦으면 메시지가 중복 발송될 수 있지 않나요?
맞다. 메시지 발행에 성공할 때까지 반복하게 된다. 어떤 메시지는 중복으로 발행될 수 있지만 최소 한 번 이상의 발행을 가져간다. 이 방식을 At-Least-Once Delivery
전략이라고 한다.
메시지의 중복 발행을 허용하는 전략을 사용하기 때문에 컨슈머에서는 데이터가 중복으로 쌓이지 않도록 유의해야 하고 멱등 처리가 반드시 필요하다. 즉, 전송할 때와는 다르게 Exactly-Once Processing
전략을 사용해야 한다.
참고자료
https://microservices.io/patterns/data/transactional-outbox.html
https://blog.gangnamunni.com/post/transactional-outbox
아주 유용했어요. 고마워요 구상님