4. 트랜잭션 관리: 사가
4. 트랜잭션 관리: 사가
많은 애플리케이션이 교과서대로 ACID 트랜잭션을 사용하지 않는 경우가 많으며, 성능을 높이기 위해 하위 트랜잭션 격리 수준을 사용한다.
WHY?
💡 1. 성능과 확장성의 트레이드오프
- 높은 격리 수준일수록 데이터베이스는 더 많은 잠금을 걸고, 충돌을 방지하기 위해 동시 접근을 제한합니다.
- 예를 들어,
Serializable
격리 수준에서는 트랜잭션 간 순차적인 실행과 거의 비슷한 효과를 내기 때문에 동시성(concurrency)이 크게 떨어집니다. - 낮은 격리 수준(예:
Read Committed
,Repeatable Read
)을 사용하면 동시성을 높여 TPS(Transactions per Second)를 끌어올릴 수 있어, 고부하 환경에서 선호됩니다.
💡 2. 실제 요구사항은 "절대적 일관성"까지는 필요하지 않음
- 많은 비즈니스 로직에서 약간의 일관성 손해는 감수할 수 있습니다.
- 예: SNS에서 좋아요 수가 101로 보였다가 100으로 바뀌는 건 심각한 문제가 아님.
- 이런 경우 낮은 격리 수준 + 보완 로직으로 충분히 해결됩니다.
💡 3. 어플리케이션 레벨에서의 정합성 보장
- 데이터베이스에 ACID를 모두 맡기지 않고, 어플리케이션 코드에서 정합성을 보장하는 전략을 많이 씁니다.
- 예:
Optimistic Locking
,Versioning
,Eventual Consistency
(이벤트 기반 비동기 처리) 등
💡 4. 트랜잭션 격리 수준도 비용이 다르다
- 격리 수준별 비용 차이:
Read Uncommitted
: 거의 없음 (dirty read 허용)Read Committed
: 기본 설정 (dirty read 방지, 성능 양호)Repeatable Read
: Phantom Read 방지 (InnoDB 기본)Serializable
: 가장 느림, 거의 사용 안 함 (단기 배치성이나 회계 프로그램에서만 사용)
은행 계좌 간 송금처럼 중요한 비즈니스 프로세스에 최종 일관성(Eventual Consistency)을 적용한 경우도 많고, 스타벅스조차 2단계 커밋(2pc)을 쓰지 않는다.
4-1. MSA에서의 트랜잭션 관리
앤터프라이즈 애플리케이션은 거의 모든 요청을 하나의 DB 트랜잭션으로 처리한다.
단일DB에 접근하는 모놀리식 애플리케이션의 트랜잭션 관리는 어렵지 않다.
하지만 다중DB, 다중 메시지 브로커를 사용하는 모놀리식 애플리케이션이나, 자체 DB를 가진 여러 MSA 서비스는 트랜잭션 관리가 어렵다.
MSA는 서비스 마다 DB가 따로 존재하기 때문에 여러 DB에 걸쳐 데이터 일관성을 유지할 수 있는 수단이 필요하다.
이는 사가 패턴을 통해서 해결할 수 있다.
4-1-1. 분산 트랜잭션의 문제점
예전에는 분산 트랜잭션을 이용해 여러 서비스, DB, 메시지 브로커에 걸쳐 데이터 일관성을 유지했다.
2pc방법이 주로 사용되었는데 이는 문제점이 있다.
- NoSQL DB(몽고DB, 카산드라)와 현대 메시지 브로커(RabbitMQ, 카프카)는 분산 트랜잭션을 지원하지 않으므로 최근 기술 상당수는 포기해야 한다.
- 동기 IPC 형태로 가용성이 떨어진다.
- 분산 트랜잭션은 참여한 서비스 모두 가동 중이어야 커밋할 수 있다.
에릭 브루어의 CAP 정리에 따르면, 시스템은 일관성(consistency), 가용성(availability), 분할 허용성(partition tolerance) 중 2가지 속성만 가질 수 있다. 요즘 아키텍트들은 일관성 보다 가용성을 더 우선시하는 편이다.
WHY? 가용성을 더 우선시할까?
✅ 요즘 아키텍트들이 가용성을 우선시하는 이유
1. 사용자 경험(UX)이 우선됨
- 현대 서비스는 글로벌하고, 24/7 무중단 서비스를 요구합니다.
- 데이터가 잠깐 이전 상태로 보여도 "빠르게 응답하는 것"이 "정확하지만 느린 응답"보다 낫습니다.
- 예: 아마존 장바구니에서 상품 수량이 잠시 정확하지 않아도 주문이 가능한 게 더 중요.
2. Eventual Consistency로 충분한 경우가 많음
- 완벽한 일관성(C) 대신 "일시적인 불일치"를 허용하고, 최종적으로 일치하게 만드는 전략 (Eventual Consistency)
- 예: NoSQL 시스템 (Cassandra, DynamoDB, Couchbase 등) → AP 시스템
3. 서비스 중단은 막대한 손실
- 일관성을 보장하려고 가용성을 희생하면, 서비스 중단이 발생할 수 있음.
- 대부분의 비즈니스에서 일관성보다 고객 이탈·수익 손실을 방지하는 것이 더 중요함.
- 특히 쇼핑, SNS, 스트리밍 등에서는 수 초의 다운타임도 치명적.
4. 일관성은 이후에 보완 가능
- 데이터 정합성은 백그라운드로 재처리하거나 수동 정정이 가능한 경우가 많음.
- 반면 사용자가 빠져나간 건 되돌릴 수 없음.
이런점 때문에 요즘에는 분산 트랜잭션을 잘 사용하지 않고 메시지를 DB 트랜잭션의 일부로 전송하는 방법으로 사용한다던지 사가 패턴을 주로 사용한다.
4-1-2. 데이터 일관성 유지: 사가 패턴
사가는 MSA에서 분산 트랜잭션 없이 데이터 일관성을 유지하는 메커니즘이다.
사가는 비동기 메시징을 이용하여 편성한 일련의 로컬 트랜잭션이다. 서비스 간 데이터 일관성은 사가로 유지한다.
여러 서비스의 데이터를 업데이트하는 시스템 커맨드마다 사가를 하나씩 정의한다.
사가와 ACID 트랜잭션간은 차이가 있다.
- ACID 트랜잭션에 있는 격리성(I)이 사가에는 없다.
- 사가는 로컬 트랜잭션마다 변경분을 커밋하므로 보상 트랜잭션을 걸어 롤백해야 한다.

사가는 보상 트랜잭션으로 변경분을 롤백한다.
- ACID 트랜잭션은 비즈니스 로직 실행 도중 규칙에 위배되면 쉽게 롤백이 가능하다.
- 하지만 사가는 단계마다 로컬 DB에 변경분을 커밋하므로 자동 롤백은 불가하다.
- 롤백 상황을 위해서는 보상 트랜잭션을 미리 작성해야 한다.
ex_) 소비자가 음식 주문을 했을때
- 소비자 정보가 올바르지 않거나, 주문을 할 수 없는 소비자이다.
- 음식점 정보가 올바르지 않거나, 주문 접수가 불가능한 음식점이다.
- 소비자 신용카드가 한도 초과가 되었다.
이럴경우 주문은 취소되어야 하며 사가는 주문을 무효화하는 보상 트랜잭션을 가동한다.

모든 단계에 보상 트랜잭션이 필요한 것은 아니다.
주문 생성 사가의 1~3번째 단계는 실패할 가능성이 있으므로 보상 트랜잭션,
4번째 단계는 절대로 실패하지 않는 단계이므로 피봇 트랜잭션,
5~6번째 단계는 항상 성공하기 때문에 재시도 가능 트랜잭션 이라고 한다.
4.2. 사가 편성
사가는 단계를 편성하는 로직으로 구성된다.
시스템 커맨드가 사가를 시작할 때 이 편성 로직은 첫 번재 사가 참여자를 정하여 로컬 트랜잭션 실행을 지시하고,
트랜잭션이 완료되면 그다음 사가 참여자를 호출하는 과정이 모든 단계가 실행될 때까지 반복된다.
도중 하나라도 로컬 트랜잭션이 실패하면 사가는 보상 트랜잭션을 역순으로 실행한다.
사가 편성 로직은 2가지 종류가 있다.
- 코레오그래피
- 의사 결정과 순서화를 사가 참여자에게 맡긴다. 사가 참여자는 직접 서로간 이벤트 메시지 교환 방식으로 통신한다.
- 오케스트레이션
- 사가 편성 로직을 사가 오케스트레이터에 중앙화한다. 사가 오케스트레이터는 사가 참여자에게 커맨드 메시지를 보내 수행할 작업을 지시한다.
4.2.1 코레어그래피 사가
사가 참여자가 할 일을 알려주는 중앙 편성자가 없으며, 대신 사가 참여자가 서로 이벤트를 구독해서 그에 따라 반응하는 것이다.
주문 생성 사가 구현

- 주문서비스: 주문을 APPROVAL_PENDING 상태로 생성 -> 주문 생성 이벤트를 발행
- 소비자 서비스: 주문 생성 이벤트 수신 -> 소비자가 주문을 할 수 있는지 확인 -> 소비자 확인 이벤트 발행
- 주방 서비스: 주문 생성 이벤트 수신 -> 주문 내역 확인 -> 티켓을 CREATE_PENDING 상태로 생성 -> 티켓 생성됨 이벤트 발행
- 회계 서비스: 주문 생성 이벤트 수신 -> 신용카드 승인을 PENDING 상태로 생성
- 회계 서비스: 티켓 생성 및 소비자 확인 이벤트 수신 -> 소비자 신용카드 과금 -> 신용카드 승인됨 이벤트 발행
- 주방 서비스: 신용카드 승인 이벤트 수신 -> 티켓 상태를 AWAITING_ACCEPTANCE로 변경
- 주문 서비스: 신용카드 승인됨 이벤트 수신 -> 주문 상태를 APPROVED로 변경 -> 주문 승인됨 이벤트 발행
주문 생성 사가 보상 트랜잭션 구현

- 주문서비스: 주문을 APPROVAL_PENDING 상태로 생성 -> 주문 생성 이벤트를 발행
- 소비자 서비스: 주문 생성 이벤트 수신 -> 소비자가 주문을 할 수 있는지 확인 -> 소비자 확인 이벤트 발행
- 주방 서비스: 주문 생성 이벤트 수신 -> 주문 내역 확인 -> 티켓을 CREATE_PENDING 상태로 생성 -> 티켓 생성됨 이벤트 발행
- 회계 서비스: 주문 생성 이벤트 수신 -> 신용카드 승인을 PENDING 상태로 생성
- 회계 서비스: 티켓 생성 및 소비자 확인 이벤트 수신 -> 소비자 신용카드 과금 -> 신용카드 승인 실패 이벤트 발행
- 주방 서비스: 신용카드 실패 이벤트 수신 -> 티켓 상태를 REJECTED로 변경
- 주문 서비스: 신용카드 실패 이벤트 수신 -> 주문 상태를 REJECTED로 변경
이처럼 코레오그래피 사가 참여자는 발행/구독 방식으로 소통한다. 그러나 문제점이 존재한다.
2가지 통신 이슈
- 자신의 DB를 업데이트하고 DB 트랜잭션의 일부로 이벤트를 발행해야한다. 이는 원자적으로 일어나야한다. --> 사가 참여자끼리 확실히 통신하기 위해 트랜잭셔널 메시징기법을 사용해야한다.
- 사가 참여자는 자신이 수신한 이벤트와 자신이 가진 데이터를 연관 지을 수 있어야 한다. --> 데이터를 매핑할 수 있도록 다른 사가 참여자가 상관관계 ID가 포함된 이벤트를 발행해야 한다,
✅ 코레오그래피 사가의 장단점
장점
- 단순하다: 비즈니스 객체를 생성, 수정, 삭제할 때 서비스가 이벤트를 발행한다.
- 느슨한 결합: 참여자는 이벤트를 구독할뿐 서로를 직접 알지 못한다.
단점
- 이해하기 어렵다: 사가를 한곳에 정의한 것이 아니라서 여러 서비스에 구현 로직이 흩어져 있다.
- 서비스 간 순환 의존성: 참여자가 서로 이벤트를 구독하는 특성상, 순환 의존성이 발생하기 쉽다.(주문-회계-주문) 반드시 문제는 아니지만 잠재적인 취약점이다.
- 단단히 결합될 위험성: 사가 참여자는 각자 자신에게 영향을 미치는 이벤트를 모두 구독해야 한다. 그래서 서로 영향을 미치는 서비스들간에 업데이트 주기가 종속된다.
4.2.1 오케스트레이션 사가
오케스트레이션 사가는 사가 참여자가 할 일을 알려 주는 오케스트레이터 클래스를 정의한다.
사카 오케스트레이터는 커맨드/비동기 응답 상호 작용을 하며 참여자와 통신한다.

제일 마지막 단계에서는 주문 생성 사가가 주문을 직접 업데이트해서 승인 처리 해도 되지만, 일관성 차원에서 주문 서비스가 마치 다른 참여자인 것처럼 취급하는 것이다.
위 사진의 케이스는 정상 케이스이다. 정상 케이스 외에 소비자 서비스, 주방 서비스, 회계 서비스 중 한곳에 오류가 발생하여 사가가 실패하는 경우 같은 가능한 모든 시나리오를 기술하는 상태 기계로 사가를 모델링하면 유용하다.
사가 오케스트레이터를 상태 기계로 모델링
상태 기계는 상태와 이벤트에 의해 트리거되는 상태 전이로 구성된다.
전이가 발생할 때마다 액션이 일어나는데, 사가의 액션은 사가 참여자를 호출하는 것이다.
상태 간 전이는 사가 참여자가 로컬 트랜잭션을 완료하는 시점에 트리거되고, 로컬 트랜잭션의 상태와 결과에 따라서 그 후의 액션이 결정된다.

사가 오케스트레이션과 트랜잭셔널 메시징
오케스트레이션 사가는 DB를 업데이트하는 서비스와 메시지를 발행하는 서비스가 단계마다 있다.
- 예를 들어 주문 서비스는 주문 및 주문 생성 사가 오케스트레이터를 생성한 뒤, 첫번째 사가 참여자에게 메시지를 전송한다.
- 사가 참여자는 메시지를 수신받아 자신의 DB를 업데이트 한 후 응답 메시지를 전송하는 식으로 커맨드 메시지를 처리한다.
- 그러면 주문 서비스는 사가 오케스트레이터 상태를 업데이트 한 후 커멘드 메시지를 다음 사가 참여자에게 보낸다.
이때 서비스들은 각각 트랜잭셔널 메시지를 사용해서 DB 업데이트와 메시지 발행을 원자적으로 처리해야 한다.
✅오케스트레이션 사가의 장단점
장점
- 의존 관계 단순화: 오케스트레이터는 참여자를 호출하지만, 참여자는 오케스트레이터를 호출하지 않으므로 순환 의존성이 발생하지 않는다.
- 낮은 결합도: 각 서비스는 오케스트레이터가 호출하는 API를 구현할 뿐, 사가 참여자가 발행하는 이벤트는 몰라도 된다.
- 관심사를 더 분리하고 비즈니스 로직을 단순화: 사가 편성 로직이 사가 오케스트레이터 한곳에만 있으므로 도메인 객체는 더 단순화해지고 자신이 참여한 사가에 대해서는 알지 못한다.
단점
- 중앙화: 비즈니스 로직이 자칫 오케스트레이터에 너무 중앙화 될 수도 있다. --> 오케스트레이터는 순서화만 담당하고, 비즈니스 로직은 갖지 않도록 설계하자!
단순한 사가가 아니라면 가급적 오케스트레이션 방식이 권장된다.
4.3 비격리 문제 처리
사가를 이용할땐 골치아픈 비격리 문제를 해결해야 한다.
ACID의 격리성(I)는 동시에 실행 중인 여러 트랜잭션의 결과가 어떤 순서대로 실행된 결과와 동일함을 보장하는 속성이다.
그러나 사가는 격리성이 빠져있다. 이는 2가지 문제를 야기한다.
- 한 사가가 실행 중에 접근하는 데이터를 도중에 다른 사가가 바꿔치기할 수 있다.
- 한 사가가 업데이트를 하기 이전 데이터를 다른 사가가 읽어서 데이터의 일관성이 깨질 수 있다.
원자성
- 사가는 트랜잭션을 모두 완료하거나 모든 변경분을 언두해야 한다.
일관성
- 서비스 내부의 참조 무결성은 로컬 DB가, 여러 서비스에 걸친 참조 무결성은 서비스가 처리한다.
지속성
- 로컬 DB로 처리한다.
4.3.1 비정상 개요
- 소실된 업데이트: 한 사가의 변경분을 다른 사가가 미쳐 못 읽고 덮어 쓴다.
- 더티 읽기: 사가가 업데이트 하지 않은 변경분을 다른 트랜잭션이나 사가가 읽는다.
- 퍼지/반복 불가능한 읽기: 한 사가의 상이한 두 단계가 같은 데이터를 읽어도 결과가 달라지는 현상(다른 사가가 그 사이 업데이를 했기 때문에 생기는 문제)
4.3.2 비격리 대책
주문 서비스 설명에서 나왔던 PENDING 상태 같은것이 이상 현상을 예방하는 전략 중 하나이다. 이런 방식을 시멘틱 락 이라고 부를 수 있다.
- 시멘틱 락: 애플리케이션 수준의 락
- 교환적 업데이트: 업데이트 작업은 어떤 순서로 실행해도 되게끔 설계
- 비관점 관점: 사가 단계 순서를 재조정하여 비즈니스 리스크를 최소화 하는것
- 값 다시 읽기: 데이터를 덮어 쓸 때 그 전에 변경된 내용이 없는지 값을 다시 읽고 확인하여 더티쓰기를 방지
- 버전 파일
- 값에 의한: 유형별 비즈니스 위험성을 기준으로 동시성 메커니즘을 동적 선택
시멘틱 락
트랜잭션이 생성/수정하는 레코드에 플래그를 세팅할 수도 있고, 플래그를 세팅해서 다른 트랜잭션이 레코드에 접근을 못하도록 락을 걸어둘 수도 있다.
락 같은 경우는 DB나 REDIS를 사용할 수 있다.
그러나 데드락이 발생할 수 있기 때문에 그에 대한 방안이 필요하다
- 락 획득 순서고정: 주문ID 같은 키 값의 정렬을 통해 순서대로 락을 획득한다.
- 타임아웃 설정: Redis 등에서 락 ttl을 설정하고, 일정시간 내 처리 실패 시 자동 해제 한다.
- 락 획득 실패시 즉시 실패처리 및 대기 후 재획득 시도
- 락 해제 보장
교환적 업데이트
데이터에 여러 사용자가 동시에 접근하여 업데이트할 때, 업데이트 순서에 상관없이 동일한 최종 결과가 나오는 연산을 말한다
즉, A 유저가 먼저 하든, B 유저가 먼저 하든 결과가 같음.
1 + 2 = 2 + 1
(✔️ 교환적)3 * 4 = 4 * 3
(✔️ 교환적)5 - 2 ≠ 2 - 5
(❌ 비교환적)
보상 트랜잭션이 계좌를 인출 또는 입금 후 사가를 롤백시켜야 하는 상황이라면 보상 트랜잭션은 단순히 계좌를 입급 또는 인출해서 업데이트를 언두하면 된다.
값 다시 읽기
사가가 레코드를 업데이트 하기 전에 다시 읽어 값이 변경되었는지 확인하는 것
만약 값을 읽었더니 변경되었다면 사가를 중단하고 나중에 재시작한다. 이것은 일종의 낙관적 오프라인 락 패턴이다.
이것을 적용하면, 주문이 승인되는 도중 최소되는 불상사를 방지할 수 있다.
값에 의한
애플리케이션 차원에서 각 요청의 속성을 보고 사가를 쓸지, 분산 트랜잭션을 쓸지 판단하는 것이다.
위험성이 낮은 요청은 사가를, 위험성이 높은(금전거래) 요청은 분산 트랜잭션을 실행한다.