SAGA 패턴?

MSA에서 분산 트랜잭션 처리를 위해 도입한 패턴이다.

각 서비스의 로컬 트랜잭션들이 순차적으로 업데이트를 해나가며,
트랜잭션 실패 시 보상 트랜잭션을 수행하여 이전 서비스들의 트랜잭션을 롤백한다.

 

SAGA 아키텍처 구조도

draw.io로 그림

 

Order와 Product는 Producer이고, Product와 Payment는 Consumer가 된다.

즉 Product의 경우 동시에 Producer와 Consumer의 역할을 한다.

 

SAGA 플로우

정상 흐름

  1. Order application에 클라이언트 요청이 들어오면, Order에서 필요한 로직을 수행하고 exchange에 메시지를 보낸다.
  2. exchange가 message를 product.queue로 라우팅한다.
  3. product는 product.queue의 메시지를 컨슘하여 필요한 로직을 수행하고 exchange로 보낸다.
  4. exchange가 message를 payment.queue로 라우팅한다.
  5. 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
조원준입니다