현재 단일 트랜잭션으로 처리되던 구조에서 트랜잭션의 원자성을 자연스럽게 보장할 수 있었습니다.
아래와 같이 분산 처리로 구성하게 되면, 트랜잭션을 하나로 묶을 수 없어 문제가 발생합니다.
Pub Application -> Kafka (Stream Application) -> Consumer Application
분산 트랜잭션이 이루어지는 상황에서 원자성을 보장하는 방법을 정리하고 비교해보려 합니다.
트랜잭션의 개념
데이터베이스 시스템에서 하나의 논리적인 작업 단위를 의미. (여러 개의 SQL 명령어를 하나의 작업으로 묶어서 실행하는 것)
작업을 모두 성공하거나, 모두 실패해야 하며, 부분적으로만 성공하는 것은 허용되지 않도록 하는 것.
아래 트랜잭션 패턴을 이해하는데 어려웠던 부분.
위에 내용처럼 DB 트랜잭션은 원자성을 보장하기 위해, 하나라도 실패하면 해당 작업이 물리적으로 처리되지 않습니다.
그래서 MSA 환경에서 사용되는 트랜잭션 패턴들이 논리적인 작업 단위에서 실패하면 데이터를 물리적으로 반영하지 않는다고 생각했기 때문에 아래 패턴 개념들이 잘 이해가 가지 않았고 개발 플로우가 상상이 가지 않았습니다. (2PC 제외)
그러나 아래 내용들을 찾아보고 정리하는 과정에서 MSA 환경에서의 트랜잭션 패턴들은 전통적인 방식의 원자성을 보장하지 않고 다른 방식으로 원자성을 보장하려는 대체적인 접근법을 취하고 있다는 걸 알았습니다. (물리적으로 데이터 처리 후 롤백 등)
MSA 환경의 트랜잭션 패턴 3가지
2PC Pattern (2Phase Commit Pattern)
분산 환경에서 트랜잭션을 조율하기 위해 2단계 커밋을 진행하는 방식.
1단계(Prepare) : 모든 참여자가 트랜잭션 준비 상태를 응답 (Commit 가능 여부)
2단계(Commit) : 모든 참여자가 준비 완료 응답을 보내면 최종 커밋. 실패 시 롤백
장점
- 트랜잭션 일관성이 강력하게 보장됨.
단점
- 각 서비스가 응답할 때까지 대기해야 하므로 지연 발생.
- 모든 요청을 처리할 때까지 관련한 모든 DB에 Lock이 설정.
- 서비스 간 강결합
OutBox Pattern
로컬 트랜잭션과 이벤트 발행을 동시에 보장하는 방식. 데이터베이스에 업데이트 작업과 함께 이벤트 메시지를 Outbox 테이블에 기록. 별도의 프로세스가 Outbox 테이블에서 메시지를 읽고, 메시지 브로커(Kafka 등)에 발행.
장점
- 서비스 간 결합도 감소.
- 로컬 트랜잭션을 활용하므로 복잡한 분산 트랜잭션 관리 불필요
단점
- Outbox 테이블 관리 비용 증가
- 메시지 처리 시 중복 방지 로직 필요.
Saga Pattern
분산 트랜잭션을 작은 단위의 트랜잭션으로 나누고, 각 단계에서 독립적으로 커밋하는 방식.
실패 시 보상 트랜잭션을 실행해 이전 상태로 복구. Saga 패턴은 Choreographed Saga, Orchestrated Saga의 2가지 종류의 패턴이 있음.
Choreographed Saga
- 각 서비스가 자율적으로 트랜잭션을 관리하고, 다른 서비스에게 이벤트를 발행하여 후속 작업을 트리거하는 방식.
Orchestrated Saga
- 중앙 관리 시스템이 존재하여 전체 트랜잭션을 관리하고, 각 서비스에 명령을 전달하는 방식.
장점
- 서비스 간 독립성 유지 (느슨한 결합)
- 개별 서비스 단위로 확장 가능
단점
- 보상 트랜잭션 설계 필요.
- 모든 상태 관리와 흐름을 중앙에서 처리해야 함.
패턴 정리
높은 일관성이 필요한 경우 2Phase Commit.
이벤트 기반 설계를 고려한다면 Outbox Pattern.
트랜잭션 단계별 처리가 유연한 경우 Saga Pattern.
보상 트랜잭션 (Compensation Transaction)
실패한 트랜잭션의 영향을 되돌리기 위한 트랜잭션 처리.
특정 작업을 수행한 후 문제가 발생하면 그 작업의 반대되는 작업을 수행.
특징
- 서비스의 특성에 맞는 다양한 보상 로직을 구현할 수 있음.
- 보상 로직을 설계하고 구현하는 것이 복잡함.
로컬 트랜잭션 (Local Transaction)
단일 데이터베이스 내에서 수행되는 트랜잭션.
특징
- 구현이 간단하고 관리하기 쉬움.
- 글로벌 트랜잭션보다 성능이 좋음.
- 단일 데이터베이스 내에 제한됨.
글로벌 트랜잭션 (Global Transaction)
여러 개의 데이터베이스 또는 시스템에 걸쳐 수행되는 트랜잭션
분산 시스템에서 여러 서비스를 호출하는 복잡한 트랜잭션 처리.
특징
- 구현이 어렵고 관리하기 어려움.
- 로컬 트랜잭션에 비해 성능이 저하될 수 있음.
적용 방안
Saga 패턴의 Choreographed Saga 패턴을 사용.
각 서비스의 트랜잭션을 처리하고 보상 트랜잭션으로 처리해 주는 부분을 구현해야 함
예시 플로우
- 각 서비스의 트랜잭션 완료 시 이벤트 Pub -> @TransactionalEventListener
- 실패 시 보상 트랜잭션 개발 -> DLQ
@TransactionalEventListener - 트랜잭션 상태와 연계되어 이벤트를 처리.
- AFTER_COMMIT
- 트랜잭션이 성공적으로 커밋된 후에 이벤트를 처리.
- DB에 변경 사항이 확정된 후에 후속 작업을 실행하고자 할 때 사용.
- BEFORE_COMMIT
- 트랜잭션이 커밋되기 전에 이벤트를 처리.
- 커밋 직전에 추가적인 검증이나 변경 작업을 하고자 할 때 사용.
//PUB
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Transactional
public void placeOrder(Order order) {
// 주문 처리 로직
// 예: 주문 데이터 저장
// 트랜잭션 커밋 후 이벤트 발행
eventPublisher.publishEvent(new OrderPlacedEvent(order));
}
}
//SUB
@Service
public class InventoryService {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleOrderPlaced(OrderPlacedEvent event) {
// 트랜잭션이 성공적으로 커밋된 후 재고를 갱신하는 로직
// 예: 재고 감소
}
}
DLQ (Dead Letter Queue)
재처리 이후 메시지를 처리하지 못하면 DLQ에 담아 DLQ를 SUB 하는 애플리케이션에서 각 메시지 별로 보상 트랜잭션 개발
retry 이후 최종 실패 이벤트는 Queue에 담아 Sub 하는 보상 트랜잭션 로직 개발.
'MQ' 카테고리의 다른 글
CDC (Change Data Capture) 구현해보기 (0) | 2024.12.20 |
---|---|
Consumer Lag 이란 (1) | 2024.12.12 |
Kafka 공통 메시지 포맷을 위한 제네릭 적용하기 (0) | 2024.09.12 |
Kafka Pub CircuitBreaker 적용하기 (0) | 2024.09.07 |
Kafka Pub 동적 Batch 관리 (0) | 2024.08.18 |