SAGA 패턴?
MSA에서 분산 트랜잭션 처리를 위해 도입한 패턴이다.
각 서비스의 로컬 트랜잭션들이 순차적으로 업데이트를 해나가며,
트랜잭션 실패 시 보상 트랜잭션을 수행하여 이전 서비스들의 트랜잭션을 롤백한다.
SAGA 아키텍처 구조도

Order와 Product는 Producer이고, Product와 Payment는 Consumer가 된다.
즉 Product의 경우 동시에 Producer와 Consumer의 역할을 한다.
SAGA 플로우
정상 흐름
- Order application에 클라이언트 요청이 들어오면, Order에서 필요한 로직을 수행하고 exchange에 메시지를 보낸다.
- exchange가 message를 product.queue로 라우팅한다.
- product는 product.queue의 메시지를 컨슘하여 필요한 로직을 수행하고 exchange로 보낸다.
- exchange가 message를 payment.queue로 라우팅한다.
- payment는 payment.queue의 메시지를 컨슘하여 필요한 로직을 수행한다.
에러 흐름
세부적인 구현에 따라 다르겠지만,
Product에서 에러가 발생한 경우
Product에서 구현된 rollback 메소드를 수행(트랜잭션 롤백)한 뒤 Order에 메시지를 전달한다.
메시지를 전달받은 Order는 rollback 메소드를 수행(트랜잭션 롤백)한다.
Payment에서 에러가 발생한 경우
Payment에서 구현된 rollback 메소드를 수행(트랜잭션 롤백)한 뒤 Product에 메시지를 전달한다.
메시지를 전달받은 Product는 rollback 메소드를 수행(트랜잭션 롤백)한 뒤 Order에 메시지를 전달한다.
메시지를 전달받은 Order는 rollback 메소드를 수행(트랜잭션 롤백)한다.
구현
https://github.com/jun6292/rabbitmq-sb3-prac
위 구조도를 기반으로 RabbitMQ config를 작성한다.
QueueConfig
@Configuration
public class QueueConfig {
@Bean
public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Value("${message.exchange}")
private String exchange;
@Value("${message.queue.product}")
private String queueProduct;
@Value("${message.queue.payment}")
private String queuePayment;
@Value("${message.err.exchange}")
private String exchangeErr;
@Value("${message.queue.err.order}")
private String queueErrOrder;
@Value("${message.queue.err.product}")
private String queueErrProduct;
@Bean public TopicExchange exchange() {
return new TopicExchange(exchange);
}
@Bean public Queue queueProduct() { return new Queue(queueProduct); }
@Bean public Queue queuePayment() { return new Queue(queuePayment); }
@Bean public Binding bindingProduct() {
return BindingBuilder
.bind(queueProduct())
.to(exchange())
.with(queueProduct);
}
@Bean public Binding bindingPayment() {
return BindingBuilder
.bind(queuePayment())
.to(exchange())
.with(queuePayment);
}
@Bean public TopicExchange exchangeErr() {
return new TopicExchange(exchangeErr);
}
@Bean public Queue queueErrOrder() { return new Queue(queueErrOrder); }
@Bean public Queue queueErrProduct() { return new Queue(queueErrProduct); }
@Bean public Binding bindingErrOrder() {
return BindingBuilder
.bind(queueErrOrder())
.to(exchangeErr())
.with(queueErrOrder);
}
@Bean public Binding bindingErrProduct() {
return BindingBuilder
.bind(queueErrProduct())
.to(exchangeErr())
.with(queueErrProduct);
}
}
- exchange와 error exchange를 TopicExchange 타입으로 생성
- 4개의 큐(product, payment, product error, payment error)를 생성
- exchange와 queue를 바인딩한다.
Exchange와 Queue, Binding 세 개만 기억하자.
order > application.yml
spring:
application:
name: order
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
message:
exchange: market
err:
exchange: market.err
queue:
product: market.product
payment: market.payment
err:
order: market.err.order
product: market.err.product
product와 payment 모두 spring.applcation.name
을 제외하고 동일하다.
requirement
- order, product, payment 각각 build.gradle에
implementation 'org.springframework.boot:spring-boot-starter-amqp'
추가 - docker로 rabbitMQ 컨테이너 실행
결론
SAGA 패턴의 동작 원리는 간단해 보인다.
서비스 1 트랜잭션 수행 → 서비스 2 트랜잭션 수행 → 서비스 3 트랜잭션 수행 → …
인데 중간에 에러가 발생하면 (예를 들어, 서비스 3에서 발생 했다고 치면)
서비스 3 롤백 → 서비스 2 롤백 → 서비스 1 롤백
이런식으로 순차적으로 처리하는 구조다.
하지만, 코드로 구현하자니…
어플리케이션이 3개 밖에 안됨에도 불구하고 SAGA 패턴은 구현하는 데 상당히 복잡하다고 느꼈다.
MSA 환경에서 분산 트랜잭션 관리가 필수적일텐데… 실무에선 어떻게 쓰일까
좀 더 발전시켜서 다음엔 DB와 연동해봐야겠다.
'Backend' 카테고리의 다른 글
어플리케이션 성능 테스트 - JMeter (0) | 2024.08.30 |
---|---|
Prometheus & Grafana & Loki 모니터링 (0) | 2024.08.20 |
Kafka 기본 개념 및 연동 실험 (0) | 2024.08.19 |
RabbitMQ 기본 개념 및 실습 (0) | 2024.08.19 |
대규모 스트림 처리 (0) | 2024.08.18 |
SAGA 패턴?
MSA에서 분산 트랜잭션 처리를 위해 도입한 패턴이다.
각 서비스의 로컬 트랜잭션들이 순차적으로 업데이트를 해나가며,
트랜잭션 실패 시 보상 트랜잭션을 수행하여 이전 서비스들의 트랜잭션을 롤백한다.
SAGA 아키텍처 구조도

Order와 Product는 Producer이고, Product와 Payment는 Consumer가 된다.
즉 Product의 경우 동시에 Producer와 Consumer의 역할을 한다.
SAGA 플로우
정상 흐름
- Order application에 클라이언트 요청이 들어오면, Order에서 필요한 로직을 수행하고 exchange에 메시지를 보낸다.
- exchange가 message를 product.queue로 라우팅한다.
- product는 product.queue의 메시지를 컨슘하여 필요한 로직을 수행하고 exchange로 보낸다.
- exchange가 message를 payment.queue로 라우팅한다.
- payment는 payment.queue의 메시지를 컨슘하여 필요한 로직을 수행한다.
에러 흐름
세부적인 구현에 따라 다르겠지만,
Product에서 에러가 발생한 경우
Product에서 구현된 rollback 메소드를 수행(트랜잭션 롤백)한 뒤 Order에 메시지를 전달한다.
메시지를 전달받은 Order는 rollback 메소드를 수행(트랜잭션 롤백)한다.
Payment에서 에러가 발생한 경우
Payment에서 구현된 rollback 메소드를 수행(트랜잭션 롤백)한 뒤 Product에 메시지를 전달한다.
메시지를 전달받은 Product는 rollback 메소드를 수행(트랜잭션 롤백)한 뒤 Order에 메시지를 전달한다.
메시지를 전달받은 Order는 rollback 메소드를 수행(트랜잭션 롤백)한다.
구현
https://github.com/jun6292/rabbitmq-sb3-prac
위 구조도를 기반으로 RabbitMQ config를 작성한다.
QueueConfig
@Configuration public class QueueConfig { @Bean public Jackson2JsonMessageConverter producerJackson2MessageConverter() { return new Jackson2JsonMessageConverter(); } @Value("${message.exchange}") private String exchange; @Value("${message.queue.product}") private String queueProduct; @Value("${message.queue.payment}") private String queuePayment; @Value("${message.err.exchange}") private String exchangeErr; @Value("${message.queue.err.order}") private String queueErrOrder; @Value("${message.queue.err.product}") private String queueErrProduct; @Bean public TopicExchange exchange() { return new TopicExchange(exchange); } @Bean public Queue queueProduct() { return new Queue(queueProduct); } @Bean public Queue queuePayment() { return new Queue(queuePayment); } @Bean public Binding bindingProduct() { return BindingBuilder .bind(queueProduct()) .to(exchange()) .with(queueProduct); } @Bean public Binding bindingPayment() { return BindingBuilder .bind(queuePayment()) .to(exchange()) .with(queuePayment); } @Bean public TopicExchange exchangeErr() { return new TopicExchange(exchangeErr); } @Bean public Queue queueErrOrder() { return new Queue(queueErrOrder); } @Bean public Queue queueErrProduct() { return new Queue(queueErrProduct); } @Bean public Binding bindingErrOrder() { return BindingBuilder .bind(queueErrOrder()) .to(exchangeErr()) .with(queueErrOrder); } @Bean public Binding bindingErrProduct() { return BindingBuilder .bind(queueErrProduct()) .to(exchangeErr()) .with(queueErrProduct); } }
- exchange와 error exchange를 TopicExchange 타입으로 생성
- 4개의 큐(product, payment, product error, payment error)를 생성
- exchange와 queue를 바인딩한다.
Exchange와 Queue, Binding 세 개만 기억하자.
order > application.yml
spring: application: name: order rabbitmq: host: localhost port: 5672 username: guest password: guest message: exchange: market err: exchange: market.err queue: product: market.product payment: market.payment err: order: market.err.order product: market.err.product
product와 payment 모두 spring.applcation.name
을 제외하고 동일하다.
requirement
- order, product, payment 각각 build.gradle에
implementation 'org.springframework.boot:spring-boot-starter-amqp'
추가 - docker로 rabbitMQ 컨테이너 실행
결론
SAGA 패턴의 동작 원리는 간단해 보인다.
서비스 1 트랜잭션 수행 → 서비스 2 트랜잭션 수행 → 서비스 3 트랜잭션 수행 → …
인데 중간에 에러가 발생하면 (예를 들어, 서비스 3에서 발생 했다고 치면)
서비스 3 롤백 → 서비스 2 롤백 → 서비스 1 롤백
이런식으로 순차적으로 처리하는 구조다.
하지만, 코드로 구현하자니…
어플리케이션이 3개 밖에 안됨에도 불구하고 SAGA 패턴은 구현하는 데 상당히 복잡하다고 느꼈다.
MSA 환경에서 분산 트랜잭션 관리가 필수적일텐데… 실무에선 어떻게 쓰일까
좀 더 발전시켜서 다음엔 DB와 연동해봐야겠다.
'Backend' 카테고리의 다른 글
어플리케이션 성능 테스트 - JMeter (0) | 2024.08.30 |
---|---|
Prometheus & Grafana & Loki 모니터링 (0) | 2024.08.20 |
Kafka 기본 개념 및 연동 실험 (0) | 2024.08.19 |
RabbitMQ 기본 개념 및 실습 (0) | 2024.08.19 |
대규모 스트림 처리 (0) | 2024.08.18 |