본문 바로가기

기술 블로그 (Tech Blog)/Dev Notes

사가패턴은 왜 사가라고 하는걸까? 🍎아님

 

주말 모각코에서 프론트 개발자에게 주문/결제 페이지를 만들어봐! 문제를 내고,

요즘 스타일을 알려주다가(요즘은 주문과 결제를 한 페이지에서 함)

MSA와 트랜잭션 결국 사가패턴까지 나오게 되서 설명해주는데

 

'왜 사과패턴이라고 하나요?'라며 궁금해했다.
(사실 프론트개발자는 🍎 사과로 들어서 궁금해했다)

 

그래서 처음에 나는 4개의 queue가 떠올라서 4가 패턴인건가?라고 생각했다.

 

 


 

 

내가 학습한 과정에서 SAGA Pattern은

 

rabbitMQ로 이벤트 처리를 할때 주문을 하게 되면 상품을 감소 시키고, 결제를 완료하는 이벤트를 보내는 로직을 간단하게 연습해봤다.

SAGA Pattern을 공부할 때는 에러 처리를 추가 해줘서 배운 경험이 있어서

error처리를 위한 queue가 추가되어서 결국 총 queue의 개수가 4개이면 4가패턴인가보다! 하고 생각했었는데,

 

막상 질문에 답을 하려니까 절대로 아닌것 같았다.

 

따라서 SAGA Pattern의 이름을 찾아보려고 한다.

간단 이벤트 호출, queue 2개
saga pattern을 적용, queue 4개

 

 

RabbitMQ 실습 코드(더보기)

더보기

실습 코드

큐 설정

 

따라서 order 서버에서 product 서버 2개와 payment 서버를 각각 메세지를 전달하는 큐를 연결하는 설정을 해야했다.

exchange(market 이름을 가지는)에 큐를 바인딩한다.

 

익스체인지(Exchange)

  • 메시지를 적절한 큐로 라우팅하는 역할을 합니다. 프로듀서는 메시지를 직접 큐에 보내지 않고, 익스체인지에 보내며, 익스체인지는 메시지를 적절한 큐로 전달합니다.
  •  
서비스

order서버에서는 payment 큐와 product 큐에게 이벤트를 보낸다.

 

 

 

컨트롤러

테스트 호출을 위한 orderController

 

 

 

 

이벤트가 생성된 결과

그러면 이렇게 payment 큐와 product 큐에 이벤트가 들어와있는 것을 확인할 수 있다.

 

 

 

product service

서비스에 rollbackProduct 함수를 보면 Order 서버에 메세지를 전달한다 

 

 

order controller

order에서는 리스너로 rollbackProduct이 보낸 이벤트를 받아 처리한다.

 

 

order service

그러면 order service에서 rollbackOrder 를 실행해서 order의 상태를 취소로 변경하면 된다. 

 

 

에러 큐를 볼 수 있다.

 

 

 


Saga?

 

 

사가(Saga) 패턴의 이름은 **"saga"**라는 단어가 긴 이야기서사시를 뜻하는 영어 단어에서 유래.

 

이 패턴은 분산 시스템에서의 트랜잭션 관리 방법을 설명하며, 각 트랜잭션 단계가 마치 이야기를 구성하는 장(chapter)처럼 연속적으로 이어진다는 의미에서 "사가"라는 이름이 붙었습니다.

 

어원의 기원

  1. 고대 노르드어/Norse Saga:
    • "Saga"는 고대 북유럽, 특히 아이슬란드에서 전해지는 이야기 또는 서사시를 의미합니다.
    • 주로 영웅의 모험, 신화, 역사적 사건을 담은 긴 이야기를 뜻합니다.
  2. 영어에서의 사용:
    • 영어에서는 "saga"가 긴 이야기, 모험담, 또는 복잡한 사건의 연속을 뜻합니다.
    • 소설이나 영화의 시리즈물에도 종종 사용됩니다(예: "스타워즈 사가").

사가(Saga) 패턴과의 연결

사가 패턴에서 "사가(Saga)"라는 이름을 사용한 이유는:

  • 긴 이야기가 여러 장(chapter)으로 구성되듯이, 분산 트랜잭션이 여러 단계(step)로 나뉘어 관리되는 구조를 비유한 것입니다.
  • 각 단계는 독립적으로 수행되며, 실패 시 이전 단계로 돌아가는 복잡한 흐름을 포함하므로 서사적인 전개 방식에 비유된 것입니다.

 

 

MSA(Microservices Architecture) 환경에서는 분산된 서비스 간의 트랜잭션을 처리하기 어렵기 때문에 사가 패턴이 유용하게 적용됩니다. RabbitMQ나 Kafka 같은 메시징 시스템은 비동기 이벤트 기반 통신을 지원하지만, 전통적인 의미의 ACID 트랜잭션은 지원하지 않습니다. 이러한 이유 때문에 사가 패턴이 등장하게 된 것입니다.

 

  • 분산 시스템의 특성:
    • 각 서비스는 독립적으로 배포되고 실행되므로, 트랜잭션을 분산 서비스 전체에 걸쳐 원자적으로 처리하는 것이 불가능합니다.
    • 예를 들어, 항공편 서비스, 호텔 서비스, 렌터카 서비스가 각각 독립된 데이터베이스를 사용한다면, 이 데이터베이스 간에 단일 트랜잭션을 설정할 수 없습니다.
  • 메시징 시스템의 한계:
    • RabbitMQ나 Kafka는 메시지 전달 보장, 이벤트 처리 순서, 중복 제거 등의 기능을 제공하지만, 트랜잭션 롤백이나 동기적인 상태 관리를 기본적으로 지원하지 않습니다.
    • 메시지가 전달된 이후의 상태 변경은 각 서비스에서 개별적으로 처리해야 합니다.

 


 

 

ACID 트랜잭션?

데이터베이스에서 트랜잭션의 신뢰성과 일관성을 보장하기 위한 네 가지 주요 속성

 

1. Atomicity (원자성)

  • 설명: 트랜잭션 내의 작업이 모두 성공하거나, 모두 실패해야 함을 보장합니다.
  • 의미: 중간에 작업이 실패하면 트랜잭션이 전부 롤백되어 이전 상태로 복원됩니다.
  • 예시:
    • 은행에서 계좌 이체를 할 때, A 계좌에서 돈이 빠져나가고 B 계좌에 돈이 들어가는 작업은 하나의 트랜잭션으로 간주됩니다. 한쪽만 실행되고 다른 쪽이 실패한다면 전체 작업은 취소됩니다.

2. Consistency (일관성)

  • 설명: 트랜잭션이 실행된 후 데이터베이스가 항상 일관된 상태로 유지됨을 보장합니다.
  • 의미: 트랜잭션 전과 후의 데이터는 데이터베이스 스키마와 비즈니스 규칙을 위반하지 않아야 합니다.
  • 예시:
    • 은행 계좌의 총 금액이 트랜잭션 전후로 동일해야 함. (A 계좌에서 빠진 금액은 B 계좌로 정확히 더해져야 함)

3. Isolation (격리성)

  • 설명: 여러 트랜잭션이 동시에 실행되더라도 서로 간섭하지 않도록 보장합니다.
  • 의미: 한 트랜잭션의 중간 결과가 다른 트랜잭션에 노출되지 않아야 합니다.
  • 예시:
    • 한 사용자가 상품 재고를 업데이트하는 동안 다른 사용자가 같은 상품을 조회하면, 업데이트가 완료되기 전에는 이전 상태의 재고만 보게 됩니다.

4. Durability (지속성)

  • 설명: 트랜잭션이 성공적으로 완료되면, 그 결과는 시스템 장애(예: 서버 다운) 이후에도 영구적으로 저장됨을 보장합니다.
  • 의미: 데이터가 디스크에 안전하게 기록되었음을 보장합니다.
  • 예시:
    • 전원 장애가 발생하더라도 이미 완료된 은행 계좌 이체 기록은 데이터베이스에 남아 있어야 합니다.

 


 

사가 패턴의 적용 이유

사가 패턴은 분산된 서비스 간의 트랜잭션을 일련의 단계로 나누어 처리합니다. 각 단계는 독립적인 트랜잭션이며, 실패 시 이전 단계에 대한 보상 작업을 수행하는 방식으로 전체 흐름의 일관성을 보장합니다.

  • 예: 여행 예약 시스템
    1. 항공편 예약 서비스에서 항공편 예약 요청을 처리하고 결과를 반환.
      • 성공 시 다음 단계로 진행.
      • 실패 시 트랜잭션 종료(더 이상 진행하지 않음).
    2. 호텔 예약 서비스에서 호텔 예약 요청을 처리.
      • 성공 시 다음 단계로 진행.
      • 실패 시 보상 작업으로 항공편 예약 취소 수행.
    3. 렌터카 예약 서비스에서 렌터카 예약 요청을 처리.
      • 성공 시 최종 트랜잭션 완료.
      • 실패 시 보상 작업으로 호텔 예약 및 항공편 예약 취소 수행.

 

사가 패턴이벤트 기반 처리 방식으로, 분산 시스템에서 트랜잭션을 관리하는 데 사용됩니다. 사가 패턴의 핵심은 각 단계가 독립적인 트랜잭션으로 동작하며, 이벤트를 통해 다음 작업으로 전환하거나, 실패 시 보상 작업을 트리거한다는 점입니다.

 


사가 패턴과 이벤트 처리의 관계

  1. 이벤트 기반 트랜잭션 흐름:
    • 각 서비스는 자신의 작업(로컬 트랜잭션)을 완료한 후 이벤트를 발행합니다.
    • 발행된 이벤트는 다음 단계의 서비스가 처리하도록 트리거됩니다.
  2. 이벤트를 통한 상태 전이:
    • 각 트랜잭션의 결과는 이벤트로 표현되며, 성공 또는 실패에 따라 시스템 전체의 상태가 전이됩니다.
    • 예를 들어:
      • 항공편 예약 완료 → "항공편 예약 성공" 이벤트 발행 → 호텔 예약 서비스에서 이벤트 처리 시작.
  3. 보상 작업도 이벤트로 트리거:
    • 실패 시에는 이전 작업을 되돌리기 위해 보상 작업(compensating transaction) 이벤트가 발행됩니다.
    • 예를 들어:
      • 호텔 예약 실패 → "호텔 예약 실패" 이벤트 발행 → 항공편 예약 취소.

 

 

**사가 패턴(Saga Pattern)**의 핵심은 분산 트랜잭션의 관리이며,

반드시 실패를 포함해야만 사가 패턴이라고 할 수 있는 것은 아닙니다.

다만, 실패를 처리할 수 있는 구조를 포함하는 것이 사가 패턴의 중요한 특징입니다.


사가 패턴의 본질

  1. 트랜잭션을 나누는 방식:
    • 사가 패턴은 하나의 전체 트랜잭션을 여러 개의 **작은 트랜잭션(단계)**으로 나누어 처리합니다.
    • 각 단계는 독립적인 로컬 트랜잭션으로 처리되며, 성공 여부에 따라 다음 단계로 넘어갑니다.
  2. 보상 작업(compensating action):
    • 특정 단계에서 실패가 발생할 경우, 이전 단계에서 완료된 작업을 되돌리기 위해 보상 작업을 수행합니다.
    • 이러한 보상 작업은 사가 패턴의 핵심적인 메커니즘 중 하나입니다.
  3. 성공과 실패 모두 고려:
    • 모든 단계가 성공적으로 완료되면 전체 트랜잭션이 정상적으로 끝나지만,
    • 한 단계라도 실패하면 롤백 또는 복원 작업을 통해 시스템의 상태를 일관되게 유지합니다.

 


간단 시나리오

  1. 회원가입:
    • 유저 서버에서 user 테이블에 새로운 유저를 생성.
  2. 입금 처리:
    • 은행 서버에서 bank 테이블에 유저의 입금 기록 생성.

단계별 흐름

성공 케이스:

  1. 유저 서버에서 회원가입 요청 처리 (user 테이블에 유저 생성).
  2. 회원가입 성공 시, "회원가입 완료" 이벤트를 발행.
  3. 은행 서버에서 이벤트를 받아 입금 요청 처리 (bank 테이블에 입금 기록 생성).
  4. 모든 단계가 성공적으로 완료되면 작업 종료.

실패 케이스:

  1. 회원가입 성공 → "회원가입 완료" 이벤트 발행 → 은행 서버로 전달.
  2. 은행 서버에서 입금 처리 실패:
    • 은행 서버는 "입금 실패" 이벤트를 발행.
    • 유저 서버가 "입금 실패" 이벤트를 구독하고, user 테이블에 생성된 유저 데이터를 삭제(보상 작업).

 


이벤트 기반 사가 패턴의 작동 방식

1. 코레오그래피 방식:

  • 이벤트 기반으로 처리:
    • 유저 서버는 "회원가입 완료" 이벤트 발행.
    • 은행 서버는 이 이벤트를 구독하고 입금 처리를 시도.
  • 보상 작업 처리:
    • 은행 서버가 "입금 실패" 이벤트를 발행.
    • 유저 서버는 이를 받아 회원가입 취소 작업 수행(예: user 테이블에서 유저 삭제).

장점:

  • 서비스 간 결합도가 낮아 확장성 높음.
  • 이벤트만으로 비동기 트랜잭션 흐름 처리 가능.

단점:

  • 이벤트 순서 관리 및 중복 처리 문제.

2. 오케스트레이션 방식:

  • 중앙 오케스트레이터가 트랜잭션 흐름 관리:
    • 회원가입 요청을 처리한 후, 유저 서버는 오케스트레이터에게 성공 여부를 응답.
    • 오케스트레이터가 은행 서버에 입금 요청.
  • 보상 작업 처리:
    • 은행 서버에서 입금 실패 시, 오케스트레이터가 유저 서버에 보상 작업 요청(회원가입 취소).

장점:

  • 트랜잭션 흐름과 오류 처리가 중앙에서 관리되므로 단순화.
  • 로직 변경이 쉬움.

단점:

  • 중앙 오케스트레이터가 병목 지점이 될 수 있음.
  • 서비스 간 의존성이 증가.

 

 

예시 코드 (오케스트레이션 방식)

유저 서버

java
public class UserService {
    public User createUser(UserDto userDto) {
        User user = userRepository.save(userDto.toEntity());
        eventPublisher.publish(new UserCreatedEvent(user.getId()));
        return user;
    }

    public void rollbackUserCreation(Long userId) {
        userRepository.deleteById(userId);
    }
}

은행 서버

public class BankService {
    public void handleUserCreatedEvent(UserCreatedEvent event) {
        try {
            BankAccount account = bankAccountRepository.createForUser(event.getUserId());
        } catch (Exception e) {
            eventPublisher.publish(new BankTransactionFailedEvent(event.getUserId()));
        }
    }
}

오케스트레이터

public class SagaOrchestrator {
    public void handleBankTransactionFailedEvent(BankTransactionFailedEvent event) {
        userService.rollbackUserCreation(event.getUserId());
    }
}

 

 

rollbackUserCreation 메서드는 주어진 userId를 사용해 해당 유저 데이터를 찾아 삭제하는 작업을 수행합니다.

이 메서드는 보통 사가 패턴에서 **보상 작업(compensating action)**으로 사용됩니다.

 

rollbackUserCreation 메서드

public void rollbackUserCreation(Long userId) {
    Optional<User> user = userRepository.findById(userId);
    if (user.isPresent()) {
        userRepository.deleteById(userId);
        log.info("User with ID {} has been rolled back successfully.", userId);
    } else {
        log.warn("No user found with ID {} to roll back.", userId);
    }
}

 

결론

사가 패턴에서는 트랜잭션 대신 이벤트 기반 처리로 실패를 관리하며, 보상 작업을 통해 시스템의 일관성을 유지합니다. 트랜잭션의 한계를 극복하기 위해:

  1. 이벤트 기반 설계로 각 서비스 간의 상태 전이를 관리.
  2. 실패 시 보상 작업을 트리거하여 이전 상태로 복원.

이러한 방식은 분산 환경에서 데이터 일관성을 유지하면서도 서비스 간 결합도를 줄이는 효과적인 대안입니다.

 

 


1. 카프카 같은 메시지 브로커를 사용하는 경우(내가 선택한 방식)

  • 설명: 별도의 오케스트레이터 없이 이벤트 기반 아키텍처에서 메시지 브로커(예: Kafka)가 서비스를 연결하고 조정합니다.
  • 작동 방식:
    • 서비스 간 통신은 이벤트를 통해 이루어짐.
    • A 서비스에서 작업이 완료되면 Kafka에 이벤트를 발행하고, B 서비스가 이를 구독해서 작업을 수행.
  • 예시:
    • A → 주문 생성 → Kafka → B → 결제 처리 → Kafka → C → 배송 관리
  • 특징:
    • 오케스트레이션이 아닌 코레오그래피(Choreography) 기반.
      • 각 서비스가 자신이 처리할 이벤트를 알고 있고, 다음 작업을 유발할 이벤트를 발행.
    • 상태 관리나 작업 실패에 대한 복구는 각 서비스가 개별적으로 처리.
    • 장점: 확장성 높고 유연한 구조.
    • 단점: 전체 흐름 파악이 어렵고, 서비스 간의 의존성이 증가할 수 있음.

2. 오케스트레이터 서버를 별도로 두는 경우

  • 설명: 오케스트레이터 역할을 전담하는 독립된 서버 또는 서비스가 존재합니다. 이 서버는 다양한 서비스나 작업을 조정하고, 상태를 관리하며, 각 단계의 실행을 제어합니다.
  • 예시:
    • Airflow: 워크플로우 스케줄링과 관리를 위한 도구.
    • Temporal: 분산 작업을 관리하는 워크플로우 오케스트레이터.
    • Spring Cloud Data Flow: 마이크로서비스 기반의 데이터 파이프라인 오케스트레이션 도구.
  • 특징:
    • 모든 작업의 흐름과 상태를 중앙에서 관리.
    • 장애 발생 시 복구 시점 등을 오케스트레이터가 결정.
    • 장점: 복잡한 워크플로우 관리에 적합.
    • 단점: 단일 장애 지점(Single Point of Failure)이 될 수 있음.

 

 

 

결론

  • 오케스트레이터 서버를 사용하는 경우:
    • 작업 흐름이 명확하고 중앙 제어가 필요한 경우 적합.
    • 예: 데이터 처리 파이프라인, 복잡한 비즈니스 프로세스.
  • Kafka와 같은 이벤트 브로커를 사용하는 경우:
    • 마이크로서비스 간 통신이 많고 독립적 서비스가 각자의 역할을 알아서 처리하는 분산 시스템에 적합.
    • 예: 주문, 결제, 배송 등 독립된 도메인이 협력하는 MSA.

둘 중 어떤 방식을 사용할지는 시스템의 요구사항, 서비스 간의 의존도, 관리 복잡도에 따라 결정하면 됩니다. 😊