
개요
SAGA 패턴은 마이크로서비스 아키텍처에서 분산 트랜잭션을 관리하기 위한 패턴입니다. 각 서비스의 로컬 트랜잭션을 순차적으로
처리하고, 문제 발생 시 보상 트랜잭션을 통해 일관성을 유지하는 방식으로 동작합니다.
1. 왜 SAGA 패턴이 필요한가?
1.1 분산 트랜잭션의 문제점
- 마이크로서비스 아키텍처에서는 하나의 작업이 여러 서비스에 걸쳐 발생하며, 각 서비스는 독립적인 데이터베이스를 가지므로
전통적인 ACID 트랜잭션을 적용하기 어렵습니다. 이로 인해 데이터 일관성을 보장하기 위한 새로운 방법이 필요합니다.
1.2 SAGA 패턴의 장점
- 장애 격리 (Failure Isolation): 각 서비스의 로컬 트랜잭션 실패가 전체 시스템에 영향을 최소화.
- 서비스 간 느슨한 결합: 각 서비스는 이벤트나 중앙 오케스트레이터를 통해 간접적으로 연결.
- 확장성과 유연성 향상: 서비스 간 의존도를 낮추어 변경 및 확장이 용이.
- 데이터 일관성 보장: Eventually Consistent 모델을 통해 최종적으로 일관된 상태에 도달.
2. SAGA 패턴의 구현 방식
2.1 코레오그래피(Choreography) 방식
각 서비스가 이벤트를 발행하고 구독하여 다음 단계를 트리거하는 방식입니다. 서비스 간의 느슨한 결합을 유지하지만,
이벤트 스톰(Event Storm) 문제와 디버깅 어려움이 단점으로 꼽힙니다.
[PaymentService 리팩토링]
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentEventPublisher paymentEventPublisher;
@Transactional
public void initiatePayment(String token, long reservationId) {
PaymentInitiatedEvent event = new PaymentInitiatedEvent(token, reservationId);
paymentEventPublisher.publish(event); // 이벤트 발행
}
}
[EventPublisher 구현]
@Component
@RequiredArgsConstructor
public class PaymentEventPublisherImpl implements PaymentEventPublisher {
private final KafkaTemplate<String, SagaEvent> kafkaTemplate;
@Override
public void publish(SagaEvent event) {
kafkaTemplate.send(event.getEventTopic(), event); // 이벤트 전송
}
}
[QueueService 의 대기열 처리]
@Component
@RequiredArgsConstructor
public class QueueEventListener {
private final QueueService queueService;
@KafkaListener(topics = "payment-initiated", groupId = "QUEUE-GROUP")
public void handleQueueValidation(PaymentInitiatedEvent event) {
queueService.validateQueue(event.getToken());
// 다음 이벤트 발행
queueService.publishSeatReservationEvent(event.getReservationId());
}
}
[SeatService: 좌석 예약]
@Component
@RequiredArgsConstructor
public class SeatEventListener {
private final SeatService seatService;
@KafkaListener(topics = "seat-reservation", groupId = "SEAT-GROUP")
public void handleSeatReservation(SeatReservationEvent event) {
seatService.reserveSeat(event.getSeatId());
// 다음 이벤트 발행
seatService.publishPaymentProcessingEvent(event.getReservationId());
}
}
[PaymentService: 결제 처리]
@Component
@RequiredArgsConstructor
public class PaymentEventListener {
private final PaymentProcessor paymentProcessor;
@KafkaListener(topics = "payment-processing", groupId = "PAYMENT-GROUP")
public void handlePaymentProcessing(PaymentProcessingEvent event) {
paymentProcessor.processPayment(event.getReservationId());
// 다음 이벤트 발행
paymentProcessor.publishNotificationEvent(event);
}
}
2.2 오케스트레이션(Orchestration) 방식
중앙 오케스트레이터가 전체 트랜잭션을 조정하는 방식입니다. 로직이 중앙에서 관리되므로 디버깅과 로깅이 용이하지만,
중앙 집중화로 인한 병목현상이 단점이 될 수 있습니다.
[PaymentService 리팩토링]
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentSagaOrchestrator paymentSagaOrchestrator;
@Transactional
public PaymentConcertResult paymentConcert(String token, long reservationId) {
PaymentSagaData sagaData = new PaymentSagaData(token, reservationId);
return paymentSagaOrchestrator.startPaymentSaga(sagaData); // SAGA 트랜잭션 시작
}
}
[PaymentSagaOrchestrator 구현]
@Component
@RequiredArgsConstructor
public class PaymentSagaOrchestrator {
private final UserService userService;
private final QueueService queueService;
private final SeatService seatService;
private final PaymentProcessor paymentProcessor;
private final NotificationService notificationService;
public PaymentConcertResult startPaymentSaga(PaymentSagaData data) {
try {
// 1. 사용자 검증
userService.validateUser(data.getToken());
// 2. 대기열 처리
queueService.validateQueue(data.getToken());
// 3. 좌석 예약
seatService.reserveSeat(data.getReservationId());
// 4. 결제 처리
paymentProcessor.processPayment(data.getReservationId());
// 5. 알림 발송
notificationService.sendNotification(data.getUserMail(), data.getConcertDetails());
return new PaymentConcertResult(SuccessStatus.COMPLETED);
} catch (Exception e) {
handleCompensation(data);
throw e;
}
}
private void handleCompensation(PaymentSagaData data) {
// 보상 트랜잭션 실행
seatService.cancelReservation(data.getReservationId());
paymentProcessor.cancelPayment(data.getReservationId());
}
}
[NotificationService 예시]
@Service
@RequiredArgsConstructor
public class NotificationService {
private final MessageSender messageSender;
public void sendNotification(String userMail, ConcertDetails concertDetails) {
String message = String.format("🎫 결제 완료: %s, 좌석 정보: %s", concertDetails.getTitle(), concertDetails.getSeat());
messageSender.sendMessage(userMail, message);
}
}
2.3 코레오그래피 vs 오케스트레이션: 선택 기준
- 코레오그래피
- 서비스 간의 독립성을 극대화.
- 이벤트 스톰(Event Storm) 발생 가능.
- 비즈니스 로직이 분산되어 추적이 어려움.
- 오케스트레이션
- 비즈니스 로직이 중앙 집중화되어 관리 용이.
- 단일 장애 지점(Single Point of Failure)이 될 가능성.
- 중앙 집중화로 인한 병목 가능.
3. SAGA 패턴 구현 시 고려사항
3.1 멱등성(Idempotency) 보장
- 각 단계가 여러 번 실행되어도 결과가 동일해야 합니다.
- 이를 위해 고유 식별자(transactionId)를 사용하고, 중복 실행을 방지하는 로직이 필요합니다.
3.2 보상 트랜잭션 설계
- 각 단계별 실패 시나리오를 정의하고, 실패 순서를 역순으로 롤백해야 합니다.
- 예: 좌석 예약 실패 → 예약 취소 → 결제 취소.
3.3 데이터 일관성 관리
- 데이터 일관성은 Eventually Consistent 모델을 통해 최종적으로 보장됩니다.
- SAGA 트랜잭션 상태를 추적하기 위한 로그 저장소(예: Redis, Kafka)를 활용할 수 있습니다.
3.4 이벤트 순서 관리
- 이벤트 기반 방식에서는 순서가 어긋나는 문제를 해결하기 위해 메시지 브로커의 ordering key를 활용하거나, 수신한 이벤트의 타임스탬프를 기준으로 재정렬해야 합니다.
3.5 장애 발생 시 복구 계획
- SAGA 트랜잭션이 실패할 경우, 복구 가능한 상태로 시스템을 유지해야 합니다.
- 상태 추적과 로그를 활용하여 실패한 트랜잭션을 재처리하는 전략 필요.
4. 실무에서의 적용 사례 및 테스트 전략
4.1 Netflix와 Uber의 SAGA 패턴 활용
- Netflix: 주문 및 결제 프로세스에서 SAGA를 통해 데이터 일관성과 복구를 보장.
- Uber: 여러 서비스 간에 동시다발적으로 발생하는 이벤트를 처리하며 데이터 일관성을 유지.
4.2 테스트 전략
- 단위 테스트(Unit Test): 각 서비스의 로컬 트랜잭션 테스트.
- 통합 테스트(Integration Test): SAGA의 성공 및 보상 시나리오 검증.
- 부하 테스트(Load Test): 이벤트 처리량과 병렬 처리 성능 검증.
5. 결론
SAGA 패턴은 마이크로서비스 환경에서 분산 트랜잭션을 관리하는 강력한 솔루션으로, 특히 복잡한 워크플로우가 필요한 시스템에서
효과적입니다. 그러나 설계와 구현이 복잡하며, 장애 복구 및 데이터 일관성을 유지하기 위한 추가적인 설계 노력이 필요합니다.
이를 보완하기 위해 멱등성, 이벤트 순서 관리, 상태 추적 시스템을 철저히 고려해야 합니다.
'개발 > Java' 카테고리의 다른 글
클린 아키텍처 도입을 위한 SOLID 원칙 복습 (5) | 2024.10.01 |
---|---|
Java 애플리케이션에서 로깅 구현하기 (feat. SLF4J) (2) | 2024.07.12 |
깨굴딱지의 코드연못입니다
올챙이가 개구리로 거듭나듯, 끊임없는 노력으로 진화하는 개발자의 길을 걷습니다. 🐸