-
The SAGA Pattern📖 개발 공부 2024. 9. 29. 17:26
토스 SLASH24 컨퍼런스에 가서 보상 트랜잭션으로 분산 환경에서도 안전하게 환전하기 라는 세션을 듣고, SAGA 패턴을 처음 접했다.
연사자분이 정말 흥미롭고 이해하기 쉽게 설명을 잘해주셔서 어떤 개념인지 확 알게 되었고, 보상 트랜잭션 실행 중에 실패하는 케이스들을 어떻게 핸들링하는지 등등 정말 유익하게 들었다!!
이 세션을 이후로 SAGA 패턴에 대해 더 자세히 공부하기 위해 글을 읽고 정리해보려고 한다~!
해당 글을 읽고 SAGA 패턴에 대해 정리한 글입니다.
주요 목표
Saga Pattern이 어떻게 작동하는지, 그리고 이를 통해 실제 세계의 분산형 거래 문제를 어떻게 해결할 수 있는지 해결 방법 제시
ACID Transactions
기존의 모놀리식 애플리케이션에서는 여러 테이블에서 작동하는 로컬 데이터베이스 트랜잭션을 생성한다. 트랜잭션 사이에 오류가 발생하면 초기 상태로 롤백된다. 이러한 트랜잭션은 ACID 트랜잭션(원자성, 일관성, 고립성, 내구성)이라고 한다.
Distributed Transactions
모놀리식 애플리케이션과 달리 분산 애플리케이션은 여러 서비스를 다룬다. 이 아키텍처에서는 트랜잭션을 다루기 어려울 수 있다.
Two-Phase Commit(2PC)
분산 트랜잭션 전략 중 하나
이는 단일 atomic operation으로 여러 node의 리소스를 업데이트하는 방식이다.
2PC는 두 단계로 업데이트를 수행한다.
- Prepare
transaction에 참여하는 각 node가 2번째 단계에서 업데이트를 수행할 수 있는지 여부를 확인한다. 각 노드가 이를 보장할 수 있다면, coordinator한테 알린다. node 중 하나라도 이를 수행할 수 없는 경우, coordinate는 node에 대한 잠금을 해제하여 롤백하라는 알림을 받는다. - Commit
업데이트를 수행하고 transaction를 완료한다.
이 방식은 분산 트랜잭션 내의 동기식 강한 일관성(synchronous, strong consistency)을 제공한다. (모든 노드가 항상 최신의 데이터를 보유하도록 보장)
하지만 이는 동기 블로킹 방식이기 때문에 마이크로서비스 기반 애플리케이션에서 크게 권장되진 않는다. 이는 deadlock 상황이 벌어질 수도 있다.
더 나아가 RabitMQ, Kafka와 같은 메시지 브로커에서 지원하지 않는다. MongoDB, Cassandra에서도 지원하지 않는다.
The Saga Pattern
비즈니스 로직은 서비스 “내”에서 ACID 트랜잭션을 사용할 수 있다. 서비스 “간”에 데이터 일관성을 유지하려면 Saga Pattern을 사용해야한다.
비동기 메시징을 사용하여 조정되는 로컬 트랜잭션 sequence를 사용하여 서비스 간 데이터 일관성을 유지한다.
Saga 패턴은 로컬 트랜잭션을 순차적으로 실행하는 분산 시스템에서의 트랜잭션 관리 방법이다.
각 로컬 트랜잭션은 ACID 트랜잭션을 사용하여 local DB를 업데이트하고, Saga에서는 그 다음 로컬 트랜잭션을 trigger하는 이벤트를 발행한다. 만약 로컬 트랜잭션이 실패하면 Saga는 이전 로컬 트랜잭션에서 완료된 변경사항을 되돌리는 일련의 보상 트랜잭션을 실행한다.
이는 비동기식이고, 최종 일관성(asynchronous, eventually consistent) 방식이다.
- 장점
- Saga에서 사용하는 비동기적 메시지 덕분에, Saga에 참여하는 서비스 중 일부가 일시적으로 이용 불가능하더라도 모든 단계가 실행될 수 있다.
ex) 어떤 서비스가 잠시 다운되었더라도, 나중에 다시 이용 가능할 때 해당 메시지를 받아서 처리를 이어갈 수 있다. 이를 통해 높은 신뢰성을 제공하고, 전체 프로세스가 멈추지 않게 된다. - 장기 트랜잭션 지원: Saga는 장기간에 걸쳐 발생하는 트랜잭션을 지원할 수 있따. 각 단계가 독립적이고 비동기로 처리되기 때문에, Saga는 다른 마이크로서비스나 객체를 블로킹하지 않고 트랜잭션을 처리할 수 있다. 즉, 어떤 하나의 서비스가 긴 시간 동안 처리하는 동안에도 다른 서비스가 자유롭게 작업을 수행할 수 있다.
- Saga에서 사용하는 비동기적 메시지 덕분에, Saga에 참여하는 서비스 중 일부가 일시적으로 이용 불가능하더라도 모든 단계가 실행될 수 있다.
- 단점
- isolation 부족: 일반적인 트랜잭션에서는 isolation이 보장돼서 하나의 트랜잭션이 끝나기 전까지 다른 트랜잭션이 그 데이터를 변경할 수 없게 한다. 하지만 Saga에서는 여러 로컬 트랜잭션이 독립적으로 실행돼서, 다른 Saga가 중간에 데이터를 변경하는 경우가 발생할 수 있다.
- 롤백의 어려움: Saga는 여러 로컬 트랜잭션으로 나뉘어 있기 때문에 한 번 커밋된 트랜잭션을 되돌리려면 각 단계마다 보상 트랜잭션을 실행해야 한다. 보상 트랜잭션은 원래 트랜잭션을 취소하거나 데이터를 원상복구하는 작업인데, 이를 제대로 구현하기가 쉽지 않다. 모든 로컬 트랜잭션에 대해 보상 작업을 정의해야 하므로 복잡성과 오류의 가능성이 높아진다.
Saga Coordination
Saga는 주로 Saga의 단계를 조정하는 논리를 기반으로 "두 가지 방법"으로 구현될 수 있다.
Choreography based sagas
로컬 트랜잭션은 다른 saga participant가 로컬 트랜잭션을 실행하도록 트리거하는 이벤트를 발행한다.
중앙 coordinator가 없고, saga participant들이 서로의 이벤트를 구독하고 그에 따라 반응한다.
- 장점
- 단순함(Simplicity)
Choreography 기반의 Saga는 단순하게 이벤트 기반으로 설계되기 때문에, 각 서비스가 특정 이벤트에 대해 반응하는 방식으로 동작한다. 각 서비스는 독립적으로 정의되어 있어서 특별한 중앙 Coordinator가 필요하지 않아서 간단하게 구현할 수 있다. - 느슨한 결합(Simplicity and Loose coupling)
서비스들은 특정 이벤트가 발생할 때 반응하는 방식으로 설계되어 있어, 서로 직접적인 의존성이 없다. 각각의 서비스는 독립적으로 동작하며, 이벤트를 발행하거나 구독하는 방식으로 통신하므로 느슨한 결합을 유지할 수 있다.
- 단순함(Simplicity)
- 단점
- 전체 플로우 이해의 어려움
일반적으로 이 유형은 서비스 간에 saga 구현을 분산시킨다. 각 서비스가 특정 이벤트에 반응하는 방식으로 동작하기 때문에, 전체 Saga의 흐름을 한 곳에서 중앙에서 정의하거나 시각적으로 파악할 수 있는 방법이 없다. - 서비스 간 순환 의존성(Cyclic dependencies)
Misc 01 → Misc 02 → Misc 01 같은 순환참조를 가질 가능성이 있다. - tight coupling의 위험
영향을 미치는 모든 이벤트를 구독해야 한다면, 결국 서비스 간에 긴밀한 결합이 발생할 수 있다.
특히, 새로운 서비스가 추가되거나 기존 서비스의 이벤트가 변경될 경우, 이에 반응해야 하는 다른 서비스들 모두 수정이 필요하게 되어 확장성이나 유연성에 문제가 생길 수 있다.
- 전체 플로우 이해의 어려움
- 적용 상황: 간단한 서비스 구조에 적합. 서비스들이 몇개 안될 때는 이벤트 흐름을 쉽게 이해하고 관리할 수 있기 때문이다.
Orchestration based sagas
centralized saga orchestrator가 saga participan에게 로컬 트랜잭션을 실행하라고 알리는 명령을 내린다.
위 그림처럼 Misc 01에서 Saga Orchestrator가 구현돼있다.
이는 비동기 요청/응답 스타일을 사용하므로 메시지 브로커 내에서 별도의 요청 및 응답 채널을 사용한다.
- 장점
- 단순한 의존성(Simpler dependencies)
오케스트레이터는 항상 Saga Participant를 호출하지만 그 역은 아니다. 따라서 순환 종속성이 없다. - 결합도 감소(Less coupling)
오케스트레이터 기반 Saga에서는 각 마이크로서비스가 자신의 이벤트나 비즈니스 로직만 신경 쓰면 되므로, 다른 서비스에서 발생하는 이벤트에 대해 알 필요가 없다.
- 단순한 의존성(Simpler dependencies)
- 단점
- isolation 부족(완벽히 보장할 수 없는 구조)
Saga Anomalies
전형적인 Saga 방식에는 세 가지 유형의 이상 현상이 발견된다.
- Lost Update:
- 하나의 Saga가 다른 Saga의 업데이트를 덮어씌우는 현상.
- 예를 들어, 주문을 처리하는 두 개의 Saga가 같은 데이터를 업데이트하는 경우, 첫 번째 Saga가 한 변경 사항이 두 번째 Saga의 업데이트로 인해 사라질 수 있다. 이는 데이터 무결성을 손상시킬 수 있다.
- Dirty Reads:
- 한 Saga가 완전히 완료되지 않은 트랜잭션의 데이터를 읽는 경우이다.
- 한 Saga가 데이터를 변경하는 중인데, 다른 Saga가 그 데이터를 읽어가면 일관성이 없는 정보를 사용하게 된다. 데이터가 아직 커밋되지 않았거나 보상 트랜잭션으로 되돌려질 수 있기 때문에, 이 상황에서는 잘못된 데이터를 읽어가는 문제가 발생할 수 있다.
- Fuzzy / Non-repeatable Reads
- 두 개의 Saga가 같은 데이터를 읽을 때 서로 다른 결과를 얻는 상황이다.
이는 하나의 Saga가 데이터를 읽고 난 후, 다른 Saga가 그 데이터를 변경했기 때문에 발생한다. 첫 번째 Saga가 다시 그 데이터를 읽으면 다른 값이 반환될 수 있다. - 예를 들어, 하나의 Saga가 재고를 확인한 후, 다른 Saga가 그 재고를 줄였을 경우 첫 번째 Saga는 일관되지 않은 정보를 얻게 된다.
- 두 개의 Saga가 같은 데이터를 읽을 때 서로 다른 결과를 얻는 상황이다.
이 세 가지 중에서 Lost Update 및 Dirty Reads 시나리오가 가장 일반적이다.
위 이상 현상을 수정하려면 설계에 대책을 구현해야 한다.
- Semantic Lock (의미적 잠금)
이 플래그는 트랜잭션이 완료되거나 보상 트랜잭션이 발생했을 때 제거된다. 이를 통해 중간 단계에서 데이터에 접근하는 것을 방지하고, Dirty Reads를 줄이는 데 도움된다.
이는 애플리케이션 레벨에서의 잠금 방식이다. Saga의 각 트랜잭션이 **플래그(flag)**를 설정해 해당 데이터가 완전히 커밋되지 않았음을 나타낸다. - Commutative Updates
업데이트 작업을 교환 가능한(Commutative) 방식으로 설계하는 것이다. 즉, 순서에 관계없이 결과가 동일하게 나오는 방식으로 업데이트를 처리하는 것이다.
예를 들어, 재고 수량을 단순히 덮어쓰지 않고, 수량을 더하거나 빼는 방식으로 처리하는 것이다. 이렇게 하면 Lost Updates 문제를 방지할 수 있다. 이 방식은 업데이트가 순서와 상관없이 처리될 수 있도록 하여 충돌을 최소화한다. - Pessimistic View
특정 데이터에 대해 중요한 트랜잭션이 완료되기 전에는 다른 트랜잭션이 해당 데이터에 접근하지 않도록 제어한다. 이는 데이터베이스의 잠금을 사용하지 않지만, 트랜잭션의 순서를 조정함으로써 안전성을 확보하는 방식이다. - Reread Values
데이터를 업데이트하기 전에, 해당 데이터가 중간에 변경되지 않았는지 다시 읽어 확인하는 방식이다. 이렇게 하면 Lost Updates 문제를 줄일 수 있다. 만약 값이 변경된 경우, 새로 읽은 값에 맞춰 추가적인 작업을 수행하거나 업데이트를 중단할 수 있다. - By Value
비즈니스 위험도에 따라 동시성 관리 방법을 선택하는 방식이다. 예를 들어, 위험도가 낮은 요청은 Saga를 통해 처리하고, 위험도가 높은 요청은 2PC를 사용해 더 엄격한 동시성 관리를 적용한다. 이렇게 하면 성능과 안정성을 적절히 조율할 수 있다.
반응형'📖 개발 공부' 카테고리의 다른 글
Hikari CP 설정 알아보기 (0) 2024.11.24 Spring 트랜잭션 롤백 관리: rollback-only 이슈 분석과 해결 (3) 2024.11.10 Toss | 서버 증설 없이 처리하는 대규모 트래픽 (0) 2024.07.27 system-design-101. CAP 이론 (CAP theorem) (2) 2024.07.14 system-design-101. How to improve API performance? (0) 2024.06.22 - Prepare