-
Transactional Outbox Pattern카테고리 없음 2024. 7. 7. 20:45
Transactional Outbox 패턴은 데이터베이스와 메시지 브로커 간의 데이터 일관성을 유지하기 위해 사용된다. 핵심 아이디어는 트랜잭션 내에서 데이터베이스에 변경 사항을 기록할 때, 동시에 "아웃박스" 테이블에 메시지(또는 이벤트)를 기록하는 것이다. 그런 다음, 별도의 프로세스가 이 아웃박스 테이블에서 메시지를 가져와 메시지 브로커에 전송하게 된다.
이 패턴을 사용하면 데이터베이스와 메시지 브로커 간의 일관성을 유지하면서도, 트랜잭션을 통해 데이터의 원자성을 보장할 수 있다.
이 방식은 데이터 손실을 방지하고 시스템의 신뢰성을 높이는 데 유용하다.
기존에 트랜잭션 내에서 메시지 브로커로 메시지를 먼저 보내고 데이터베이스에 커밋을 하는 방식에서는, 메시지가 먼저 전송되고 나서 데이터베이스 커밋이 실패하면, 메시지는 이미 브로커로 전송되었기 때문에 롤백이 불가능해지는 문제가 발생한다. 이로 인해 데이터 일관성에 문제가 생길 수 있다.
트랜잭셔널 아웃박스 패턴은 이 문제를 해결하기 위해, 트랜잭션 내에서 메시지를 직접 브로커로 보내는 대신, 메시지를 아웃박스 테이블에 기록하는 방식으로 처리한다. 데이터베이스에 커밋이 성공하면, 그 후에 별도의 프로세스가 아웃박스 테이블에서 메시지를 읽어 브로커로 전송하게 된다. 이렇게 하면 데이터베이스 커밋과 메시지 전송 간의 일관성을 유지할 수 있다.
따라서, 아웃박스 테이블에 메시지가 기록되고 데이터베이스 커밋이 성공적으로 이루어졌다면, 그 이후에만 메시지가 브로커로 전송되므로, 롤백 상황에서도 데이터의 일관성이 보장된다.
이 패턴을 사용하면, 데이터베이스 커밋과 메시지 전송은 분리되지만, 데이터 커밋과 메세지 전송에 대한 커밋 모두 데이터베이스를 이용하기 때문에 하나의 트랜잭션에 대한 일관성을 유지할 수 있으며 메세지 전송에 대한 테이블에 row 가 삭제될때까지 재시도할 수 있기 때문에 신뢰성을 보장할 수 있다.
데이터 베이스를 먼저 커밋한다면?
단순히 순서를 바꾸는 방법으로는 문제가 있다.
메시지 전송을 데이터베이스 커밋 뒤에 두면, 메시지 전송 과정에서 네트워크 문제나 메시지 브로커 자체의 문제로 인해 실패할 가능성이 있. 이 경우 데이터베이스에는 커밋이 완료되었지만 메시지가 전달되지 않아서 시스템 간의 일관성 문제가 생길 수 있다. 다시 말해, 수신자는 메시지를 받지 못했는데 데이터베이스에는 성공적으로 처리된 데이터가 남아있는 상황이 발생할 수 있다.
즉 순서만 바꾸는 것은 일관성 문제를 완전히 해결하지 못한다. 메시지 전송이 실패하거나 네트워크 문제가 생길 수 있기 때문. 반면, Transactional Outbox Pattern은 데이터베이스 커밋과 메시지 전송을 안전하게 분리해주고, 메시지 전송 실패 시에도 데이터를 잃지 않도록 도와준다.
메세지가 at least once 를 보장한다면?
기본적으로 at least once 메시징을 보장한다면 트랜잭셔널 아웃박스 패턴을 구현하지 않아도 될 수 있을 것 같은데 이 방식에도 몇 가지 고려해야 할 점이 있다. at least once 메시징은 메시지가 적어도 한 번 이상 전송됨을 보장하는 방식이다. 만약 메시지 전송 중에 실패하면, 메시지를 재전송하여 브로커가 메시지를 결국 수신하게 된다. 이 방식의 장점은 메시지 손실을 최소화할 수 있다는 점이다. 하지만 at least once 메시징에는 다음과 같은 단점도 있다
- 중복 메시지 처리 필요: 메시지가 재전송되는 과정에서 브로커가 동일한 메시지를 여러 번 수신할 수 있다. 이를 처리하려면, 수신 측(혹은 메시지 소비자)이 중복 메시지를 감지하고 적절히 처리해야 한다.
- 복잡성 증가: 메시지를 재전송하는 로직이나 중복 메시지를 감지하는 로직을 추가로 구현해야 하므로, 시스템의 복잡성이 증가할 수 있다.
트랜잭셔널 아웃박스 패턴은 데이터베이스와 메시징 시스템 간의 일관성을 유지하는 데 중점을 두기 때문에, 시스템이 특정한 비즈니스 요구사항을 가지고 있을 때 적합하다. 반면, at least once 메시징은 일관성보다는 메시지 손실을 피하는 데 더 초점을 맞춘 접근법. 따라서, 시스템 요구사항에 따라 트랜잭셔널 아웃박스 패턴을 사용할지, 아니면 at least once 메시징으로 충분할지 결정해야 한다. 둘 다 상황에 따라 유용할 수 있지만, 목표로 하는 일관성 수준과 메시지 전달 보장을 고려해 선택하는 것이 중요할 듯 하다.
Trasnactional outbox pattern
- Outbox 테이블:
- 트랜잭션이 발생할 때, 변경된 데이터를 직접 다른 서비스에 전송하지 않고, 변경 내용을 별도의 Outbox 테이블에 기록한다. 이 테이블은 같은 데이터베이스 트랜잭션 내에서 업데이트된다.
- 폴링(Polling) 프로세스:
- 백그라운드 작업이나 별도의 서비스가 주기적으로 Outbox 테이블을 확인하고, 새로운 변경 내역을 메시지 브로커나 다른 서비스로 전송한다. 이 전송이 성공하면 Outbox 테이블에서 해당 기록을 삭제한다.
- 데이터 일관성:
- Outbox 패턴은 데이터베이스 트랜잭션의 일부분으로 처리되기 때문에, 원자성을 보장한다. 즉, 트랜잭션이 성공하면 Outbox 테이블에 데이터가 기록되고, 실패하면 기록되지 않는다.
작동 방식
- 쓰기 트랜잭션:
- 애플리케이션이 어떤 데이터를 변경할 때, 이 변경 내역을 Outbox 테이블에 기록한다. 이 과정은 데이터베이스 트랜잭션의 일부분으로 처리된다.
- Outbox 테이블 확인:
- 백그라운드 프로세스가 주기적으로 Outbox 테이블을 확인한다.
- 메시지 전송:
- 새로운 기록이 있으면, 이를 메시지 브로커(예: Kafka, RabbitMQ)나 다른 서비스로 전송한다.
- 기록 삭제:
- 메시지 전송이 성공하면 Outbox 테이블에서 해당 기록을 삭제한다.
예시
- 주문 서비스가 주문을 생성하면, 주문 내역을 데이터베이스에 저장하고, 동시에 Outbox 테이블에 주문 생성 이벤트를 기록한다.
- 백그라운드 프로세스가 Outbox 테이블을 확인하고, 새로운 주문 생성 이벤트를 메시지 브로커를 통해 배송 서비스로 전송한다.
- 배송 서비스는 메시지를 수신하고, 주문을 처리하여 배송을 준비한다.
장점
- 데이터 일관성 보장: 데이터베이스 트랜잭션의 일부분으로 처리되기 때문에 데이터 일관성이 보장된다.
- 내결함성: 메시지 전송 실패 시, 재시도를 통해 신뢰성 있는 메시지 전달을 보장한다.
- 분산 시스템에서의 효율성: 서비스 간의 직접적인 통신을 줄이고, 메시지 브로커를 통해 간접적으로 통신함으로써 시스템의 복잡도를 줄인다.
Transactional Outbox Pattern은 마이크로서비스 환경에서 데이터 일관성과 메시지 전달의 신뢰성을 보장하는 데 유용한 패턴이며 이를 통해 복잡한 분산 시스템에서의 데이터 동기화를 효과적으로 관리할 수 있다.
예)
@Entity public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String description; // Getters and Setters } @Entity public class OutboxEvent { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String eventType; private String payload; private LocalDateTime createdAt; // Getters and Setters }
... @Transactional public Order createOrder(String description) { // 1. 주문 생성 Order order = new Order(); order.setDescription(description); order = orderRepository.save(order); // 2. Outbox 이벤트 기록 OutboxEvent event = new OutboxEvent(); event.setEventType("ORDER_CREATED"); event.setPayload("Order ID: " + order.getId()); event.setCreatedAt(LocalDateTime.now()); outboxEventRepository.save(event); return order; } ... @Service public class OutboxEventProcessor { @Autowired private OutboxEventRepository outboxEventRepository; @Scheduled(fixedRate = 5000) public void processOutboxEvents() { List<OutboxEvent> events = outboxEventRepository.findAll(); for (OutboxEvent event : events) { // 메시지 브로커로 이벤트 전송 (여기서는 단순히 로그로 대체) System.out.println("Processing event: " + event.getPayload()); // 이벤트 처리 후 Outbox 테이블에서 삭제 outboxEventRepository.delete(event); } } }
설명
- Order 생성: OrderService에서 주문을 생성하고, 이 주문 생성 이벤트를 OutboxEvent 테이블에 기록한다. 이 과정은 하나의 트랜잭션으로 처리된다.
- OutboxEvent 처리: OutboxEventProcessor가 주기적으로 Outbox 테이블을 확인하고, 새로운 이벤트가 있으면 이를 메시지 브로커(여기서는 단순히 로그)로 전송한다. 전송이 완료되면 해당 이벤트를 Outbox 테이블에서 삭제한다.
이 예제를 통해 Transactional Outbox Pattern의 기본적인 개념과 작동 방식을 이해할 수 있다. 이 패턴을 사용하면 마이크로서비스 간 데이터 일관성을 유지하면서 신뢰성 있는 메시지 전달을 구현할 수 있다.
Transactional Outbox Pattern 적용 방법 CDC?
CDC(Change Data Capture)는 트랜잭셔널 아웃박스 패턴에서 자주 사용되는 또 다른 방법이다. CDC는 데이터베이스의 변경 사항을 실시간으로 감지하고, 이를 기반으로 다른 시스템(예: 메시지 브로커)으로 데이터를 전송하는 기술이다. 즉 데이터베이스에 기록된 변경 사항을 CDC 도구가 감지하는 것인데 CDC 도구는 데이터베이스 로그(예: MySQL의 binlog, PostgreSQL의 WAL)를 모니터링하면서, 데이터베이스 테이블에 발생한 변경 사항을 실시간으로 추적할 수 있다.
CDC를 활용한 트랜잭셔널 아웃박스 패턴은 실시간 데이터 전송이 필요하거나, 아웃박스 테이블을 주기적으로 조회하는 방식이 비효율적일 때 유용할 수 있다. 이를 통해 시스템의 복잡성을 줄이고, 실시간성을 확보할 수 있다. 대표적인 CDC 도구로는 Debezium, Apache Kafka의 Kafka Connect 등이 있다. CDC를 활용하면 메시지를 보다 빠르게, 그리고 더 정확하게 브로커로 전송할 수 있어 많은 대규모 시스템에서 선호된다고 한다.
출처:
https://pradeepl.com/blog/transactional-outbox-pattern/