MSA, SAGA 얘기를 하다 보면 대부분 일관성(Consistency)과 보상 트랜잭션에 집중한다.
하지만 실제로 대규모 분산 시스템을 설계할 때 더 골치 아프게 만드는 문제는 따로 있다. 바로 격리성(Isolation)이다.
다음과 같은 의문을 가질 수 있다.
“SAGA를 쓰면 일관성은 어떻게든 맞출 수 있을 것 같은데…
그 사이에 다른 요청이 끼어들면 상태가 꼬이지 않을까?”
전통적인 ACID가 제공하던 "격리성"을 SAGA는 제공하지 않기에 그렇다. 꼬일 수 있다.
ACID에서의 격리성(Isolation)이란?
ACID의 I, Isolation(격리성) 은 다음과 같이 정의할 수 있다.
여러 트랜잭션이 동시에 실행되어도 서로의 "중간 상태"를 보지 못하게 하여, 결과적으로 순차 실행과 동일한 결과를 보장하는 성질
즉, 트랜잭션 간 간섭을 막아 데이터 일관성을 지키는 핵심 메커니즘이며, 이 성질 덕분에 단일 데이터베이스 환경에서는 중간 상태가 다른 트랜잭션에 노출되지 않는다.
데이터베이스는 이를 위해 여러 격리 수준(Isolation Level) 을 제공한다.
대표적인 격리 수준 (Isolation Levels)
1. READ UNCOMMITTED
- 커밋되지 않은 변경사항도 읽기 가능
- Dirty Read 발생 가능
- 가장 낮은 격리 수준
2. READ COMMITTED
- “커밋된 데이터”만 읽기
- Dirty Read 방지
- 하지만 같은 트랜잭션 내에서 두 번 조회하면 값이 달라질 수 있음 → Non-repeatable Read 가능
3. REPEATABLE READ
- 한 트랜잭션 내에서 같은 행을 다시 읽어도 항상 동일한 값
- Non-repeatable Read 방지
- DB 종류에 따라 Phantom Read 발생할 수도 있음
4. SERIALIZABLE
- 모든 트랜잭션을 완전히 순차적으로 실행한 것처럼 보이게 함
- 가장 강력한 격리 수준이지만, 그만큼 가장 성능이 낮음
주요 이상 현상 (Anomalies)
1. Dirty Read: 커밋되지 않은 값을 읽는 문제
2. Non-repeated Read: 같은 트랜잭션에서 두 번 읽을 때 값이 달라지는 문제
3. Phantom Read: 같은 조건으로 조회했을 때 두 번째 조회에 새로운 행이 나타나는 문제
전통적인 모놀로식 + DB 환경에서는 DB가 이러한 문제를 대부분 해결해주기 때문에 개발자는 복잡한 동시성 문제를 크게 신경쓰지 않아도 된다.
그러나 MSA + SAGA에서는 더 이상 DB가 전체 트랜잭션을 감싸줄 수 없기 때문에 격리성 문제는 애플리케이션 레벨에서 해결해야 하는 중요한 과제가 된다.
SAGA에서 발생할 수 있는 격리성 문제 예시
Lost Update(갱신 손실)와 유사한 동시성 문제
SAGA에서는 서로 다른 인스턴스(Saga A, Saga B)가 동시에 같은 데이터를 읽고 업데이트할 수 있다.
각 단계는 로컬 트랜잭션이기 때문에 글로벌 락이나 전역 트랜잭션이 존재하지 않는다.
그 결과, MSA 환경에서 흔히 발생하는 문제가 바로 동일한 자원(예: 재고)을 서로 다른 SAGA 인스턴스가 동시에 읽고 업데이트하면서 발생하는 격리성 문제이다.
시나리오: 동시에 같은 상품 재고를 조회하여 잘못된 결과가 만들어지는 경우

- 초기 재고: 100개
- 사용자 A: 상품 10개 주문
- 사용자 B: 상품 5개 주문
- OrderService는 각각에 대해 독립적인 Saga A, Saga B를 시작
1) 두 SAGA가 거의 동시에 재고를 조회
- Saga A → Inventory 조회 → 100개
- Saga B → Inventory 조회 → 100개
각 SAGA는 “나만 재고를 줄일 것”이라고 가정하고 로직을 실행한다.
2) Saga B가 먼저 재고 차감
- 결제 성공
- 재고 100 -> 95
3) Saga A도 이전에 조회한 값(100)을 기준으로 차감
- 결제 성공
- 재고 95 -> 90
A 입장에서는 여전히 100에서 10을 뺀 값이라 문제를 모르지만 실제 값은 이미 95였다.
격리성 문제의 핵심
두 SAGA는 동시에 같은 초기 상태(재고 = 100) 를 읽었고, 그 후 각각 독립적으로 재고를 업데이트했다.
이로 인해 재고 차감 과정에서 논리적 충돌은 발생했지만 시스템은 이를 감지하지 못했다.
그 결과
- 기대되는 재고 = 100 - 10 - 5 = 85
- 실제 재고 = 90
👉 5개가 더 많이 남아버리는 갱신 손실(Lost Update) 발생
왜 이러한 문제가 발생하는가?
SAGA 설계는 본질적으로 다음 철학을 따른다.
- 전체 프로세스를 하나의 글로벌 트랜잭션으로 묶지 않고
- 각 서비스별로 로컬 트랜잭션 + 보상 트랜잭션으로 연결한다.
- 전체 흐름의 일관성은 시간이 지나면(Eventual Consistency) 맞춰진다.
하지만 이 구조에서는 다음 문제가 발생한다.
- 로컬 트랜잭션 간에 격리성이 없다.
- 재고 서비스는 두 요청이 서로 경쟁하는지 모른다.
- 중간 상태가 외부로 노출된다.
- Dirty Read, Non-repeatable Read와 유사한 현상이 애플리케이션 레벨에서 발생
즉,
SAGA는 데이터 변화를 순차적으로 격리시켜주지 않으며, 격리성 문제는 개발자가 직접 해결해야 한다.
의미적 격리성(Semantic Isolation): 재고 “예약" 모델
SAGA에서 격리성 문제를 해결하기 위해 많이 사용하는 방식이 바로 재고 예약(Reservation)이다.
재고 엔티티는 다음과 같은 상태를 가진다.
- AVAILABLE: 아직 누구에게도 할당되지 않은 재고 (사용 가능한 재고)
- RESERVED: 특정 주문에서 사용하기 위해 홀드된 재고
- SOLD: 결제까지 완료된 확정 재고
예약 기반 흐름
- 주문 생성
- 재고 서비스 -> 재고를 바로 차감하는 대신 RESERVED 상태로 홀드
- 결제 성공
- RESERVED -> SOLD
- 결제 실패 (보상 트랜잭션)
- RESERVED -> AVAILABLE
이렇게 하면,
- 여러 요청이 동시에 들어와도 재고 서비스는 총 재고가 아닌 AVAILABLE 재고 기준으로 판단
- 격리성 문제로 인한 재고 충돌이 사전에 차단된다
- 중간 상태를 읽더라도 비즈니스적으로 잘못된 판단을 하지 않게 된다
즉, DB 레벨에서는 여전히 중간 상태를 읽고 있지만 도메인 규칙 덕분에 이상이 발생하지 않는다.
'아키텍처 & 설계(Architecture & Design) > MSA' 카테고리의 다른 글
| Orchestration 기반 SAGA 패턴 (0) | 2025.11.23 |
|---|---|
| Choreography 기반 SAGA 패턴 이해하기 — 분산 환경에서의 트랜잭션 처리 방식 (0) | 2025.11.17 |
| SAGA 패턴과 멱등성 – 분산 트랜잭션에서 꼭 짚고 넘어가야 할 관계 (0) | 2025.11.10 |
| 분산 트랜잭션의 한계와 SAGA 패턴 (0) | 2025.11.07 |
| [API Gateway] BFF 패턴 (0) | 2025.05.22 |