단일 DB에 접근하는 모놀리식 애플리케이션의 트랜잭션 관리는 어렵지 않습니다.
하지만 다중 DB, 다중 메시지 브로커를 사용하는 모놀리식 애플리케이션이나, 자체 DB를 가진 여러 서비스로 구성된 MSA는 트랜잭션 관리가 어렵기 때문에 좀 더 정교한 메커니즘이 필요합니다.
따라서 서비스가 분리되면서 ACID(원자성 Atomic, 일관성 Consistency, 격리성 Isolation, 지속성 Durability)
트랜잭션으로 관리해 오던 단일 DB를 ACD 사가로 관리하는 다중 DB 아키텍처로 전환해야 합니다.
💡사가(sage)는 ACID에서 Isolation이 빠진 ACD만 지원하는 로컬 트랜잭션입니다.
사가를 이용해서 여러 서비스에 걸친 트랜잭션을 구현하고 데이터 일관성을 유지하는 방법에 대해 알아보겠습니다.
Saga Pattern(사가패턴)
분산 트랜잭션의 문제점
분산 트랜잭션은 참여자가 반드시 커밋 아니면 롤백을 하도록 보장합니다.
하지만 다음과 같은 문제점을 갖고 있습니다.
- 분산 트랜잭션을 지원하지 않는 DB와 브로커들이 존재한다
- 동기 IPC 형태라서 모든 서비스가 가동 중이어야 커밋할 수 있다
👉 느슨하게 결합하지 못합니다.
사가 패턴 예시
사가는 MSA에서 분산 트랜잭션 없이 비동기 메시징을 이용하여 데이터 일관성을 유지하는 일련의 로컬 트랜잭션입니다.
사가와 ACID 트랜잭션은 두 가지 중요한 차이점이 있습니다.
- ACID 트랜잭션에 있는 격리성(Isolation)이 사가에는 없습니다.
- 사가는 로컬 트랜잭션마다 변경분을 커밋하므로 보상 트랜잭션을 걸어 롤백해야 합니다.
주문 생성 사가는 6개의 로컬 트랜잭션으로 구성됩니다.
- 주문 서비스 : 주문을
APPROVAL_PENDING
상태로 생성합니다. - 소비자 서비스 : 주문 가능한 소비자인지 확인합니다.
- 주방 서비스 : 주문 내역을 확인하고 티켓을
CREATE_PENDING
상태로 생성합니다. - 회계 서비스 : 소비자 신용카드를 승인합니다.
- 주방 서비스 : 티켓 상태를
AWAITING_ACCEPTANCE
로 변경합니다. - 주문 서비스 : 주문 상태를
APPROVED
로 변경합니다.
서비스는 로컬 트랜잭션이 완료되면 메시지를 발행하여 다음 사가 단계를 트리거합니다. 메시지를 통해 사가 참여자를 느슨하게 결합시키고 사가는 반드시 완료되도록 보장하는 것입니다.
메시지 수신자가 일시 불능 상태라면 메시지 브로커는 다시 메시지를 전달할 수 있을 때까지 메시지를 버퍼링 합니다.
시스템은 일관성(consistency) 가용성(availability) 분할 허용성(partition tolerance)중 두 가지 속성만 가질 수 있습니다. 요즘 아키텍처들은 일관성보다 가용성을 더 중요시하는 편입니다.
사가의 트랜잭션 롤백
ACID 트랜잭션은 비즈니스 로직 실행 도중 규칙에 위배되면 쉽게 롤백이 가능합니다. DB에서 ROLLBACK
하면 그 시점까지 변경된 내용은 모두 취소됩니다.
하지만 사가는 단계마다 로컬 DB에 변경분을 커밋하므로 자동 롤백은 불가능합니다.
따라서 사가는 다음 세 가지 트랜잭션으로 구성됩니다.
- 보상 가능 트랜잭션
👉 보상 트랜잭션으로 롤백 가능한 트랜잭션 - 피봇 트랜잭션
👉 사가의 진행/중단 지점. 피봇 트랜잭션이 커밋되면 사가는 완료될 때까지 실행됩니다. - 재시도 가능 트랜잭션
👉 피봇 트랜잭션 직후의 트랜잭션. 반드시 성공해야 합니다.
예를 들어, 위 예시에서 4번째 로컬 트랜잭션인 신용카드 승인(피봇 트랜잭션)이 실패하면 1~3번째 단계에서 적용된 변경분을 명시적으로 취소하고 보상 트랜잭션(compensating transaction
)을 실행해야 합니다.
신용카드 승인이 실패하면 보상 트랜잭션은 다음 순서대로 작동될 것입니다.
- 주문 서비스 : 주문을
APPROVAL_PENDING
상태로 생성합니다. - 소비자 서비스 : 주문 가능한 소비자인지 확인합니다.
- 주방 서비스 : 주문 내역을 확인하고 티켓을
CREATE_PENDING
상태로 생성합니다. - 회계 서비스 : 소비자 신용카드 승인 요청이 거부됩니다.
- 주방 서비스 : 티켓 상태를
CREATE_REJECTED
로 변경합니다. (보상 트랜잭션) - 주문 서비스 : 주문 상태를
REJECTED
로 변경합니다. (보상 트랜잭션)
만약 신용카드 트랜잭션이 성공했다면 이 사가는 반드시 완료되어야 합니다. 따라서 성공 시나리오의 5번과 6번 사가는 성공할 때까지 재시도하게 됩니다.
사가 편성
사가를 구성하는 방법은 두 가지 종류가 있습니다.
- 코레오그래피(
choreography
)
👉 의사 결정과 순서화를 사가 참여자에게 맡깁니다. 사가 참여자는 주로 이벤트 교환 방식으로 통신합니다. - 오케스트레이션(
orchestration
)
👉 사가 편성 로직을 사가 오케스트레이터에 중앙화합니다. 사가 오케스트레이터는 사가 참여자에게 커맨드 메시지를 보내 수행할 작업을 지시합니다.
두 편성중 어느 것이 정답이라고 말할 수는 없고 각각의 장단점을 확인하고 서비스에 맞는 편성을 고르시는 것이 좋습니다.
코레오그래피 사가
코레오그래피 방식은 중앙 편성자가 없습니다. 따라서 각 서비스는 자신의 DB를 업데이트하고 다음 참여자를 트리거하는 이벤트를 발행합니다.
별 문제가 없다면 다음 순서대로 진행될 것입니다.
- 주문 서비스
👉 주문을APPROVAL_PENDING
상태로 생성
👉 주문 생성 이벤트 발행 - 소비자 서비스
👉 주문 생성 이벤트 수신
👉 소비자가 주문을 할 수 있는지 확인
👉 소비자 확인 이벤트 발행 - 주방 서비스
👉 주문생성 이벤트 수신
👉 주문 내역 확인
👉 티켓을CREATE_PENDING
상태로 생성 → 티켓 생성됨 이벤트 발행 - 회계 서비스
👉 주문 생성 이벤트 수신
👉 신용카드 승인을PENDING
상태로 생성 - 회계 서비스
👉 티켓 생성 및 소비자 확인 이벤트 수신
👉 소비자 신용카드 과금
👉 신용카드 승인됨 이벤트 발행 - 주방 서비스
👉신용카드 승인 이벤트 수신
👉 티켓 상태를AWAITING_ACCEPTANCE
로 변경 - 주문 서비스
👉신용카드 승인 이벤트 수신
👉 주문 상태를APPROVED
로 변경
👉 주문 승인됨 이벤트 발행
주문을 거부해서 실패 이벤트가 발행되는 경우
- 주문 서비스
👉 주문을APPROVAL_PENDING
상태로 생성
👉 주문 생성 이벤트 발행 - 소비자 서비스
👉 주문 생성 이벤트 수신
👉 소비자가 주문을 할 수 있는지 확인
👉 소비자 확인 이벤트 발행 - 주방 서비스
👉 주문생성 이벤트 수신
👉 주문 내역 확인
👉 티켓을CREATE_PENDING
상태로 생성 → 티켓 생성됨 이벤트 발행 - 회계 서비스
👉 주문 생성 이벤트 수신
👉 신용카드 승인을PENDING
상태로 생성 - 회계 서비스
👉 티켓 생성 및 소비자 확인 이벤트 수신
👉 소비자 신용카드 과금
👉신용카드 실패 이벤트 발행 - 주방 서비스
👉신용카드 실패 이벤트 수신
👉 티켓 상태를REJECTED
로 변경 - 주문 서비스
👉신용카드 실패 이벤트 수신
👉 주문 상태를REJECTED
로 변경
👉 주문 승인됨 이벤트 발행
다음처럼 코레오그래피 사가 참여자는 발행/구독
방식으로 소통합니다.
코레오그래피 방식으로 사가를 구현하려면 두 가지 통신 이슈를 고려해야 합니다.
첫째, 사가 참여자가 자신의 DB를 업데이트하고, DB 트랜잭션의 일부로 이벤트를 발행하도록 해야 합니다.
여기서 DB를 업데이트하는 작업과 이벤트를 발행하는 작업은 원자적으로(atomically
) 일어나야 합니다. 따라서 사가 참여자가 서로 확실하게 통신하려면 트랜잭셔널 메시징을 사용해야 합니다.
둘째, 사가 참여자는 자신이 수신한 이벤트와 자신이 가진 데이터를 연관 지을 수 있어야 합니다. 가령 신용카드 승인됨 이벤트를 받은 주문 서비스는 여기에 해당하는 주문을 찾을 수 있어야 합니다.
해결책은 데이터를 매핑할 수 있도록 다른 사가 참여자가 상관관계 ID가 포함된 이벤트를 발행하는 것입니다.
이를테면 주문 생성 사가에서 각 참여자가 orderID를 상관관계 ID로 삼아 건네주면 됩니다.
장점
- 단순함
👉 비즈니스 객체를 생성, 수정, 삭제할 때 서비스가 이벤트를 발행합니다. - 느슨한 결합
👉 참여자는 이벤트를 구독할 뿐 서로를 직접 알지 못합니다. - 가용성이 높다
단점
- 이해하기 어렵다
👉 오케스트레이션 사가와 달리, 사가를 어느 한 곳에 정의한 것이 아니라서 여러 서비스에 구현 로직이 흩어져 있습니다. 어떤 사가가 어떻게 작동되는지 개발자가 이해하기 어려운 편입니다. - 서비스 간 순환 의존성
👉 참여자가 서로 이벤트를 구독하는 특성상, 순환 의존성(주문 서비스 → 회계 서비스 → 주문 서비스)이 발생하기 쉽습니다. - 모니터링이 어렵다
- 타임아웃을 구현하기 어렵다
오케스트레이션 사가
오케스트레이션 사가에서는 사가 참여자가 할 일을 알려주는 오케스트레이터 클래스를 정의합니다.
사가 오케스트레이터는 커맨드/비동기 응답 상호작용
을 하며 참여자와 통신합니다.
즉, 사가 단계를 실행하기 위해 해당 참여자가 무슨 일을 해야 하는지 커맨드 메시지에 적어 보냅니다. 사가 참여자가 작업을 마치고 응답 메시지를 오케스트레이터에 주면 오케스트레이터는 응답 메시지를 처리한 후 다음 사가 단계를 어느 참여자가 수행할지 결정합니다.
- 사가 오케스트레이터가 소비자 확인
커맨드
를 소지가 서비스에 전송합니다. - 소비자 서비스는 소비자 확인 메시지를
응답
합니다. - 사가 오케스트레이터는 티켓 생성
커맨드
를 주방 서비스에 전송합니다. - 주방 서비스는 티켓 생성 메시지를
응답
합니다. - 사가 오케스트레이터는 신용카드 승인 메시지를 회계 서비스에 전송합니다.
- 회계 서비스는 신용카드 승인됨 메시지를
응답
합니다. - 사가 오케스트레이터는 티켓 승인
커맨드
를 주방 서비스에 전송합니다. - 사가 오케스트레이터는 주문 승인
커맨드
를 주문 서비스에 전송합니다.
제일 마지막 단계에서 사가 오케스트레이터는 (자신도 주문 서비스의 한 컴포넌트이지만) 커맨드 메시지를 주문 서비스에 전송합니다.
일관성 차원에서 주문 서비스가 마치 다른 참여자인 것처럼 취급하는 것입니다.
오케스트레이션 역시 각각의 서비스에서 DB 업데이트와 메시지 발행 작업을 원자적으로 처리해야 합니다.
장점
- 의존 관계 단순화
👉 오케스트레이터는 참여자를 호출하지만 참여자는 오케스트레이터를 호출하지 않으므로 순환 의존성이 발생하지 않습니다. - 관심사를 더 분리하고 비즈니스 로직을 단순화
👉 사가 편성 로직이 사가 오케스트레이터 한 곳에만 있어 자신이 참여한 사가에 대해서는 알지 못합니다. 따라서 도메인 객체와 비즈니스 로직이 더 단순해집니다.
단점
- 잘못된 중앙화 유발
👉 비즈니스 로직을 오케스트레이터에 너무 많이 중앙화하면 깡통 서비스에 일일이 할 일을 지시하는 모양새가 될 수동 있습니다.
이 문제는 오케스트레이터가 순서화만 담당하고 여타 비즈니스 로직은 갖고 있지 않도록 설계하면 해결됩니다. - 가용성이 낮다
👉 중앙 서비스가 죽으면 더 이상 서비스를 이용할 수 없습니다.
비격리 문제 처리
ACID중 격리성(I)
은 동시에 실행 중인 여러 트랜잭션의 결과가 어떤 순서대로 실행된 결과와 동일함을 보장하는 속성입니다.
격리성이 빠지게 되면 다음과 같은 문제를 유발합니다.
첫째, 한 사가가 실행 중에 접근하는 데이터를 도중에 다른 사가가 바꿔치기할 수 있습니다.
둘째, 한 사가가 업데이트를 하기 이전 데이터를 다른 사가가 읽을 수 있어서 데이터 일관성이 깨질 수 있습니다.
따라서 비격리로 인한 비정상은 다음과 같이 정리할 수 있습니다.
- 소실된 업데이트
👉 한 사가의 변경분을 다른 사가가 미처 못 읽고 덮어씁니다. - 더티 읽기
👉 사가 업데이트를 하지 않은 변경분을 다른 트랜잭션이나 사가가 읽습니다. - 퍼지/반복 불가능한 읽기
👉 한 사가의 상이한 두 단계가 같은 데이터를 읽어도 결과각 달라지는 현상, 다른 사가가 그 사이 업데이트를 했기 때문에 생기는 문제입니다.
💡Race condition 경쟁상태 해결법을 참고하면 좀 더 이해가 쉽습니다.개발자는 비격리로 인한 비정상을 방지하고 비즈니스에 미치는 영향을 최소화하는 방향으로 사가를 작성할 의무가 있습니다.
아래와 같은 방법들로 비격리로 인한 비정상을 방지할 수 있습니다.
- 시맨틱 락
- 교환적 업데이트
- 비관적 관점
- 값 다시 읽기
- 버전 파일
- 값에 의한
시맨틱 락
보상 가능 트랜잭션이 생성/수정하는 레코드에 무조건 플래그를 세팅해서 레코드가 아직 커밋 전이라서 변경될지 모른다는 표시를 하는 것입니다.
위에서 보여줬던 *_PENDING
상태가 시맨틱 락을 구현한 것입니다. 이 필드를 이용하여 주문에 접근하는 다른 사가에 현 재 어떤 사가가 주문을 업데이트하고 있음을 알립니다.
잠근 된 레코드에 접근하는 경우도 케이스별도 처리방법을 결정해야 합니다. 예를 들어 클라이언트가 APPROVAL_PENDING
상태의 주문을 취소한다고 생각해봅시다.
첫 번째 방법은 주문 취소를 실패 처리하고 클라이언트에 나중에 다시 시도하라고 알리는 것입니다.
두 번째 방법은 잠금이 해제될 때까지 블로킹하는 방법입니다.
이처럼 시맨틱 락을 사용하면 트랜잭션 고유의 격리 기능을 되살릴 수 있습니다. 다만 데드락이 발생하지 않도록 주의해야 합니다.
교환적 업데이트
업데이트를 교환적으로, 즉 어떤 순서로도 실행 가능하게 설계하면 소실된 업데이트 문제를 방지할 수 있습니다.
특정 계산된 값으로 업데이트하는 것이 아니라 플러스와 마이너스의 관계로써 업데이트가 이루어집니다.
이를테면(마이너스 통장이 없다고 가정하면) 계좌의 인출과 입금은 서로 교환적인 작업입니다.
이때 100만 원으로 계좌를 업데이트시키는 것이 아니라 계좌에서 10만 원 인출하라고 했다면 보상 트랜잭션은 단순히 10만원 입금이 되기 때문에 다른 사가의 업데이트를 덮어쓸 일은 전혀 없습니다.
비관적 관점
비관적 관점은 더피 읽기로 인한 비즈니스 리스크를 최소화하기 위해 사가 단계의 순서를 재조성하는 것입니다.
주문 취소 사가와 생성 사가의 실행이 서로 겹쳐 실행 중인데 소비자가 배달을 취소하기는 너무 늦어서 주문 취소 사가가 롤백되는 경우를 생각해 봅시다.
- 주문 취소 사가 : 신용 잔고를 늘립니다
- 주문 생성 가사 : 신용 잔고를 줄입니다.
- 주문 취소 사가 : 신용 잔고를 줄이는 보상 트랜잭션이 가동됩니다.
주문 생성 사가는 신용 잔고를 더티 읽기 하게 되고, 소비자는 신용 한도를 초과하는 주문을 할 수 있게 될 것입니다.(신용 잔고를 2번 줄임)
이때 취소 사가 단계를 재조정하면 신용 한도를 초과하는 주문을 막을 수 있습니다.
- 주문 서비스 : 주문을 취소 상태로 변경합니다.
- 배달 서비스 : 배달을 취소합니다.
- 회계 서비스 : 신용 잔고를 늘립니다.
이렇게 순서를 바꾸면 신용 잔고는 재시도 가능 트랜잭션에서 증가하므로 더티 읽기 가능성은 사라집니다.
값 다시 읽기
값 다시 읽기는 소실된 업데이트를 방지하는 대책으로 사가가 레코드를 업데이트하기 전에 값을 다시 읽어 값이 변경되지 않았는지 확인하는 것입니다.
값을 다시 읽었더니 변경되었다면 사가를 중단하고 나중에 재시작합니다. 이 대책은 일종의 낙관적 잠금 패턴입니다.
버전 파일
버전 파일은 레코드에 수행한 작업을 하나하나 기록하는 대책입니다. 즉, 비교 환적 작업을 교환적 작업으로 변환하는 방법입니다.
가령 주문 생성 사가와 주문 취소 사가가 동시 실행된다고 합시다. 시맨틱 락 대책을 쓰지 않으면 주문 생성 사가가 소비자 신용카드를 승인하기 전에 주문 취소 사가가 해당 신용카드를 승인 취소하는 말도 안 되는 상황이 벌어질 수 있습니다.
순서가 안 맞는 요청을 회계 서비스가 받아 처리하려면, 작업이 도착하면 기록해 두었다가 정확한 순서대로 실행하면 됩니다.
방금 같은 케이스는, 회계 서비스는 일단 승인 취소 요청을 기록하고 나중에 신용카드 승인 요청이 도착하면 이미 취소 요청이 접수된 상태이니 승인 작업은 생략해도 되겠구나라고 인지하는 것입니다.
값에 의한
값에 의한 대책은 비즈니스 위험성을 기준으로 동시성 메커니즘을 선택하는 것입니다.
애플리케이션 차원에서 각 요청의 속성을 보고 사가를 쓸지, 아니면 분산 트랜잭션을 쓸지 판단하는 것입니다.
위험성이 낮은 요청은 사가를 위험성이 큰 요청(예: 액수가 큰 근점이 오가는 거래)은 분산 트랜잭션을 실행합니다.
정리
여러 서비스에 흩어져 있는 데이터를 업데이트할 때 전통적인 분산 트랜잭션은 요즘 애플리케이션에는 잘 맞지 않기 때문에 사가 패턴을 적용하는 것이 낫습니다.
사가는 메시징으로 편성한 일련의 로컬 트랜잭션입니다.
로컬 트랜잭션 각자가 한 서비스에 있는 데이터를 수정 후 커밋하기 때문에 비즈니스 규칙에 위배되어 사가를 롤백시켜야 할 경우, 보상 트랜잭션을 실행하여 변경분을 명시적으로 되돌립니다.
이때 어떻게 사가를 편성할 것인지에 대한 방법은 코레오그래피, 오케스트레이션 두가지입니다.
코레오그래피 사가는 로컬 트랜잭션이 이벤트를 발행하여 다른 참여자가 로컬 트랜잭션을 실행하도록 트리거합니다.
오케스트레이션 사가는 중앙의 사가 오케스트레이터가 참여자에게 로컬 트랜잭션을 실행하라고 커맨드 메시지를 보내 지시합니다.
마지막으로 사가는 ACID 트랜잭션과 달리 서로 격리되지 않아서 비즈니스 로직을 설계하기가 어렵습니다. 이런 비격리 ACD 트랜잭션 모델에서 나오는 동시성 문제를 해결하기 위한 전략을 채택해야 하며 데드락이 발생할 위험을 무릅쓰고라도 비즈니스 로직을 단순화하기 위해 잠금을 해야 하는 경우도 있습니다.
Reference