MSA에서 서비스 간 원자성을 보장하는 트랜잭션 아웃박스 패턴

benjamin·2024년 9월 18일

MSA에서 서비스 간 원자성을 보장하는 트랜잭션 아웃박스 패턴

마이크로서비스 아키텍처(MSA)에서는 여러 서비스가 독립적으로 배포되고 운영되기 때문에 서비스 간의 데이터 일관성을 유지하는 것이 중요한 과제입니다. 특히, 한 서비스의 트랜잭션이 다른 서비스와 연동될 때 원자성을 보장하기 위한 방안이 필요합니다. 이를 해결하기 위해 널리 사용되는 패턴 중 하나가 트랜잭션 아웃박스(Transactional Outbox) 패턴입니다.

트랜잭션 아웃박스 패턴이란?

트랜잭션 아웃박스 패턴은 로컬 트랜잭션과 메시지 전송 간의 일관성 문제를 해결하는 데 사용됩니다. 일반적으로 서비스 간 통신에서 데이터 일관성을 유지하기 위해 트랜잭션을 확장하는 방법이 필요하지만, MSA에서는 서비스별 데이터베이스가 분리되어 있어 분산 트랜잭션을 적용하기 어렵습니다. 트랜잭션 아웃박스 패턴은 로컬 트랜잭션 내에서 데이터를 저장하면서, 비동기적으로 메시지를 전달하는 방식으로 일관성을 보장합니다.

트랜잭션 아웃박스 패턴의 구조

트랜잭션 아웃박스 패턴은 크게 세 단계로 나뉩니다:

  1. Outbox 테이블에 이벤트 저장: 서비스 내 트랜잭션이 성공적으로 처리될 때, 해당 트랜잭션과 함께 Outbox 테이블에 이벤트를 저장합니다.
  2. 비동기 이벤트 처리: Outbox 테이블에 저장된 이벤트는 별도의 프로세스에 의해 비동기로 처리되며, 메시지 브로커(Kafka, RabbitMQ 등)에 전송됩니다.
  3. 이벤트 상태 업데이트: 이벤트가 성공적으로 처리되면 Outbox 테이블의 상태가 업데이트됩니다.

이 방식은 로컬 트랜잭션비동기 메시지 전송을 분리하여 서비스 간 데이터 일관성을 보장합니다.

트랜잭션 아웃박스 패턴의 장점

  • 원자성 보장: 데이터베이스 트랜잭션과 메시지 전송을 하나의 트랜잭션으로 처리하지 않아도, 같은 로컬 트랜잭션 내에서 Outbox에 이벤트를 기록하여 원자성을 보장할 수 있습니다.
  • 확장성: 각 서비스는 독립적으로 이벤트를 처리하므로, 비동기 처리와 재시도 로직을 구현하여 안정성을 높일 수 있습니다.
  • 데이터 일관성 보장: 메시지 전송 중 실패하더라도 Outbox 테이블의 데이터를 기반으로 재처리가 가능해 데이터 손실을 방지할 수 있습니다.

트랜잭션 아웃박스 패턴의 단점

  • 복잡성 증가: 별도의 Outbox 테이블을 관리하고, 메시지 전송 로직을 구현해야 하기 때문에 시스템의 복잡도가 증가할 수 있습니다.
  • 지연 발생 가능성: 이벤트를 비동기로 처리하기 때문에 실시간 응답이 필요한 시나리오에서는 추가적인 지연이 발생할 수 있습니다.

Spring Boot로 트랜잭션 아웃박스 패턴 구현

Spring Boot에서 트랜잭션 아웃박스 패턴을 적용하는 예시를 살펴보겠습니다.

1. Outbox 테이블 생성

먼저 각 서비스의 데이터베이스에 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
);

2. 로컬 트랜잭션에서 Outbox에 이벤트 저장

트랜잭션이 성공적으로 완료되면, 같은 트랜잭션 내에서 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);
    }
}

3. 스케줄러로 Outbox 이벤트 처리

이벤트는 별도의 프로세스나 스케줄러를 통해 비동기로 처리됩니다. 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) {
            // 실패 시 재시도 로직 등 예외 처리
        }
    }
}

4. 메시지 브로커와 통신

메시지 브로커(Kafka, RabbitMQ 등)를 통해 다른 서비스로 이벤트를 전달하고, 해당 서비스는 이를 처리합니다. Spring Kafka 또는 Spring AMQP와 같은 라이브러리를 사용하여 메시지를 전송할 수 있습니다.

스케줄러가 이벤트를 정상적으로 처리하지 못한 경우!

여러가지 원인이 존재하지만 트랜잭션 아웃박스 패턴을 도입하는 이유가 원자성을 보장하기 위함입니다.

  • 재시도 로직 구현 (이벤트 상태를 FAIL 로 저장 후 해당 이벤트 재시도
  • Dead Letter Queue (DLQ) 사용
  • 알림 및 모니터링을 통해 수동 재시도

간략하게 위와 같은 대처방법으로 원자성을 보장할 수 있습니다.

더 나아가 멱등성 또한 보장해야 합니다.

이벤트 간 event id 와 같은 Key 값을 공유하여 해당 Key 에 대한 이벤트 처리 여부를 체크하여
동일 이벤트에 대해 멱등성을 보장하는 방법이 존재합니다.

profile
긍정적이고 항상 즐겁게

0개의 댓글