마이크로서비스 아키텍처(MSA)에서는 여러 서비스가 독립적으로 배포되고 운영되기 때문에 서비스 간의 데이터 일관성을 유지하는 것이 중요한 과제입니다. 특히, 한 서비스의 트랜잭션이 다른 서비스와 연동될 때 원자성을 보장하기 위한 방안이 필요합니다. 이를 해결하기 위해 널리 사용되는 패턴 중 하나가 트랜잭션 아웃박스(Transactional Outbox) 패턴입니다.
트랜잭션 아웃박스 패턴은 로컬 트랜잭션과 메시지 전송 간의 일관성 문제를 해결하는 데 사용됩니다. 일반적으로 서비스 간 통신에서 데이터 일관성을 유지하기 위해 트랜잭션을 확장하는 방법이 필요하지만, MSA에서는 서비스별 데이터베이스가 분리되어 있어 분산 트랜잭션을 적용하기 어렵습니다. 트랜잭션 아웃박스 패턴은 로컬 트랜잭션 내에서 데이터를 저장하면서, 비동기적으로 메시지를 전달하는 방식으로 일관성을 보장합니다.
트랜잭션 아웃박스 패턴은 크게 세 단계로 나뉩니다:
이 방식은 로컬 트랜잭션과 비동기 메시지 전송을 분리하여 서비스 간 데이터 일관성을 보장합니다.
Spring Boot에서 트랜잭션 아웃박스 패턴을 적용하는 예시를 살펴보겠습니다.
먼저 각 서비스의 데이터베이스에 Outbox 테이블을 생성합니다. 이 테이블은 전송할 이벤트 데이터를 저장하는 역할을 합니다.
CREATE TABLE outbox (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
aggregate_id VARCHAR(255),
event_type VARCHAR(255),
payload TEXT,
status VARCHAR(255),
created_at TIMESTAMP
);
트랜잭션이 성공적으로 완료되면, 같은 트랜잭션 내에서 Outbox 테이블에 이벤트를 저장합니다. 이렇게 하면 데이터베이스와 이벤트 기록이 모두 하나의 트랜잭션으로 묶여 원자성이 보장됩니다.
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
// Outbox에 이벤트 저장
OutboxEvent event = new OutboxEvent(order.getId(), "OrderCreated", order);
outboxRepository.save(event);
}
}
이벤트는 별도의 프로세스나 스케줄러를 통해 비동기로 처리됩니다. Outbox 테이블에서 PENDING 상태인 이벤트를 주기적으로 읽어, 메시지 브로커(Kafka, RabbitMQ 등)에 전송한 후 상태를 업데이트합니다.
@Scheduled(fixedDelay = 1000)
public void processOutboxEvents() {
List<OutboxEvent> events = outboxRepository.findPendingEvents();
for (OutboxEvent event : events) {
try {
// 메시지 브로커로 전송
messageBroker.send(event);
event.markAsSent(); // 이벤트 상태 업데이트
outboxRepository.save(event);
} catch (Exception e) {
// 실패 시 재시도 로직 등 예외 처리
}
}
}
메시지 브로커(Kafka, RabbitMQ 등)를 통해 다른 서비스로 이벤트를 전달하고, 해당 서비스는 이를 처리합니다. Spring Kafka 또는 Spring AMQP와 같은 라이브러리를 사용하여 메시지를 전송할 수 있습니다.
여러가지 원인이 존재하지만 트랜잭션 아웃박스 패턴을 도입하는 이유가 원자성을 보장하기 위함입니다.
간략하게 위와 같은 대처방법으로 원자성을 보장할 수 있습니다.
더 나아가 멱등성 또한 보장해야 합니다.
이벤트 간 event id 와 같은 Key 값을 공유하여 해당 Key 에 대한 이벤트 처리 여부를 체크하여
동일 이벤트에 대해 멱등성을 보장하는 방법이 존재합니다.