목차
“주문한다”는 한 줄이 결제·재고·배송 세 서비스를 건드린다. 한 트랜잭션으로 묶고 싶지만 데이터가 갈려 그럴 수 없다. 여러 서비스에 걸친 하나의 일을 어떻게 안전하게 끝내느냐 - 그 답이 사가다.
하나의 일이 세 서비스에 걸친다
주문이 성립하려면 셋이 다 되어야 한다.
- 결제 서비스가 돈을 받고,
- 재고 서비스가 물건을 빼두고,
- 배송 서비스가 배송을 잡는다.
한 서비스, 한 DB였다면 이 셋을 하나의 트랜잭션으로 묶어 “다 되거나 다 안 되거나”를 공짜로 얻었다. 재고가 없으면 결제도 자동으로 롤백됐다.
큰 트랜잭션으로 못 묶는다
서비스를 쪼개면서 데이터가 갈렸다. 결제는 결제 DB, 재고는 재고 DB다. 트랜잭션은 한 DB 안에서만 “다 되거나 다 안 되거나”를 보장한다. 세 DB에 걸친 하나의 트랜잭션은 없다.
그러니 현실은 이렇게 된다 - 결제는 성공했는데 재고를 빼려다 품절이면? 결제한 돈이 붕 뜬다. 누군가 그 결제를 되돌려야 한다. 이 어긋난 걸 누가 되돌리나가 사가가 푸는 문제다.
사가: 여러 로컬 트랜잭션을 잇는다
사가의 발상은 “하나의 큰 트랜잭션”을 포기하고 “여러 개의 작은 로컬 트랜잭션을 순서대로 잇는 것”이다.
- 각 서비스는 자기 DB 안에서 자기 몫만 트랜잭션으로 처리한다(결제는 결제만, 재고는 재고만).
- 한 단계가 끝나면 이벤트를 발행해 다음 단계를 깨운다.
- 이렇게 이어 붙인 전체 흐름 하나가 사가다.
각 화살표는 이벤트다. 큰 자물쇠 하나로 셋을 묶는 대신, 작은 성공들을 이벤트로 이어 나간다.
실패하면 되돌린다: 보상 트랜잭션
문제는 중간에 실패했을 때다. 결제는 됐는데 재고가 품절이면, 이미 끝난 앞 단계를 되돌려야 한다. 이때 앞으로 나아가던 걸 취소하는 반대 방향의 트랜잭션을 보상 트랜잭션(compensating transaction) 이라 부른다.
앞으로 갈 땐 결제 → 재고 → 배송, 되돌릴 땐 그 역순으로 각 단계의 취소를 실행한다. 사가는 이렇게 “끝까지 가거나, 되돌려서 없던 일로 만들거나” 둘 중 하나로 수렴한다.
보상은 롤백이 아니라 취소다
여기서 오해하기 쉬운 게 있다. 보상은 DB의 롤백과 다르다. 롤백은 “아예 일어나지 않았던 것처럼” 흔적 없이 되돌린다. 하지만 결제는 이미 커밋됐고, 어쩌면 “결제 완료” 문자까지 나갔다. 없던 일로 만들 수 없다.
그래서 보상은 새로운 반대 행위다 - “결제 취소” 트랜잭션을 새로 실행하고, 필요하면 “취소되었습니다” 문자를 또 보낸다. 장부엔 결제와 취소가 둘 다 남는다. 이건 잠깐 어긋났다 되돌아오는 결과적 정합성의 전형이다 - 순간적으로는 “결제됨” 상태가 보였다가, 보상이 돌면 “취소됨”으로 수렴한다.
그래서 보상을 설계할 수 있는 일에만 사가가 맞는다. “이미 발송한 실물을 회수”처럼 되돌리기가 현실적으로 불가능하면, 그 단계는 되돌릴 수 있을 때까지 뒤로 미루는 식으로 순서를 짜야 한다.
코레오그래피: 코디네이터 없이 서로 반응한다
지금까지 그린 게 코레오그래피(choreography) 방식이다. 이름 그대로 군무처럼, 지휘자 없이 각자 자기 앞 이벤트를 듣고 반응한다.
- 결제 서비스는 “주문 생성됨”을 듣고 결제한 뒤 “결제 완료됨”을 낸다.
- 재고 서비스는 “결제 완료됨”을 듣고 차감한 뒤 “재고 확보됨”을 낸다.
- 아무도 전체를 지휘하지 않는다. 각자 발행/구독으로 느슨하게 이어질 뿐이다.
장점은 결합이 낮다는 것이다. 새 단계(예: 적립금)를 끼우고 싶으면, 그 서비스가 “결제 완료됨”을 몰래 구독하면 된다 - 기존 서비스는 아무것도 안 바뀐다.
대가: 흐름이 코드 어디에도 없다
코레오그래피의 대가는 pub-sub이 경고한 그것이다 - 전체 흐름이 어느 코드에도 안 적혀 있다. “주문이 어떤 단계를 거치나”를 알려면 각 서비스가 무엇을 듣고 무엇을 내는지를 머릿속으로 이어 붙여야 한다.
단계가 서넛일 땐 견딜 만하지만, 예닐곱으로 늘고 보상 경로까지 얽히면 아무도 전체를 못 본다. 어디서 멈췄는지 추적하기도 어렵다. 이 문제를 푸는 반대편 방식이 다음 글의 오케스트레이션이다 - 흐름을 한곳에 쥔 지휘자를 둔다.
실무에서: 순서와 관측
- 각 단계는 멱등해야 한다 - 사가의 이벤트도 적어도 한 번이라 재전송된다. 보상까지 포함해 모든 단계를 두 번 받아도 안전하게.
- 이벤트는 아웃박스로 낸다 - 로컬 트랜잭션 커밋과 다음 단계 이벤트 발행이 어긋나면 사가가 중간에 멈춘다. 아웃박스로 묶는다.
- 어디까지 갔는지 보이게 - 사가는 걸쳐 있어 추적이 어렵다. 각 단계에 같은 주문 id를 달아, 멈춘 사가를 찾아 손볼 수 있게 한다.
정리
- 여러 서비스에 걸친 하나의 일은 한 트랜잭션으로 못 묶는다 - 데이터가 갈렸기 때문이다.
- 사가는 큰 트랜잭션 대신 작은 로컬 트랜잭션들을 이벤트로 잇고, 실패하면 보상 트랜잭션으로 되돌린다.
- 보상은 롤백이 아니라 새로운 반대 행위 - 장부엔 둘 다 남고, “잠깐 어긋났다 수렴”하는 결과적 정합성이다.
- 코레오그래피는 지휘자 없이 서로 반응해 결합이 낮지만, 전체 흐름이 코드에 없어 커지면 안 보인다.
다음 글은 그 흐름을 한곳에 쥐는 반대편 방식 - 오케스트레이션이다.