상태가 아니라 사실을 저장한다: Event Sourcing
쇼핑몰 재고 시스템을 만든다고 합시다.
상품이 팔리면, 보통 이렇게 저장합니다.
product
product_id 501
stock -3
현재 상태입니다.
“지금 501번 상품의 재고는 -3개다.”
상태만 남기면 “그때”를 답하지 못한다
어느 날 대시보드에 재고 -3이 찍힙니다.
셀러가 묻습니다.
“분명 재고를 채워 뒀는데,
왜 마이너스가 됐죠?
품절인데 주문이 몇 건 더 들어온 건가요,
아니면 반품 처리가 잘못된 건가요?”
담당자는 결국 이렇게 물을 수밖에 없습니다.
“그때 무슨 일이 있었죠?”
테이블을 엽니다.
stock은 -3.
그 숫자가 몇 번의 주문과 몇 번의
반품을 거쳐 나온 값인지,
동시에 들어온 주문 중 어느 것이
재고를 마지막으로 차감했는지는
어디에도 없습니다.
현재 상태만 남기고, 그 상태에 이르기까지 일어난 일들을 덮어썼기 때문입니다.
원인은 이 한 줄에 있습니다.
UPDATE product SET stock = stock - 1
WHERE product_id = 501;
UPDATE는 이전 값을 지우고 새 값을
씁니다.
직전 재고가 몇 개였는지,
그 순간 다른 주문이 동시에
같은 값을 차감하고 있었는지는
이 문장을 실행하는 순간 사라집니다.
어떤 필드를 더 추가하든,
UPDATE가 이전 값을 덮어쓴다는
사실 자체는 바뀌지 않습니다.
결국 “시스템상 확인이 어렵다”고 답할 수밖에 없습니다.
Event Sourcing이 실제로 하는 일
Martin Fowler는 Event Sourcing을
“애플리케이션 상태의 모든 변화를 일련의
이벤트로 캡처하는 것”이라고 정의합니다.1
1. StockReplenished (501번 상품, +20)
2. OrderPlaced (주문 A, 501번 상품, -5)
3. OrderPlaced (주문 B, 501번 상품, -6)
4. OrderCancelled (주문 A, 501번 상품, +2)
5. OrderPlaced (주문 C, 501번 상품, -7)
6. OrderPlaced (주문 D, 501번 상품, -3)
7. OrderPlaced (주문 E, 501번 상품, -4)
여기서 이벤트는 공식 기록(system of
record)입니다.
UPDATE로 값을 덮어쓰는 대신,
INSERT로 이벤트를 계속 쌓기만
합니다.
지운 적이 없으니 지워질 값도
없습니다.
현재 상태가 필요하면 이벤트를 처음부터
재생(replay)해서 계산합니다.
그래서 감사 로그는 Event Sourcing의
목적이 아니라 부산물입니다.2
모든 변화가 이미 이벤트로 기록되어 있으니,
무엇이 왜 바뀌었는지는 이벤트만 보면 할 수 있습니다.
재고 수량 -3의 비밀도 이벤트 로그를 재생해
보면 쉽게 풀립니다.
초기 재고량 20개에 주문 A의 취소로 돌아온
2개를 더해도,
그사이 들어온 주문 A부터 E까지를
전부 감당할 수 없어서,
최종 재고가 -3으로 집계된 것입니다.
Event Sourcing이 가능하게 하는 것
Martin Fowler는 Event Sourcing의 이점을 몇 가지 이야기합니다.1
완전 재구성(complete rebuild).
상태 저장소가 통째로 망가져도,
이벤트 로그만 있으면 처음부터 재생해
현재 상태를 되살릴 수 있습니다.
상태는 언제든 버려도 되는 캐시가 됩니다.
시간 여행 쿼리(temporal query).
“3월 1일 오전 시점에 이 상품의
재고는 몇 개였나?”
그 시점까지의 이벤트만 재생하면
바로 답이 나옵니다.
이벤트 재생(event replay).
비동기 메시징을 쓰는 시스템에서는
이벤트가 잘못된 순서로 도착하는 일이
흔합니다.
이벤트를 순서대로 다시 재생하기만
하면, 뒤죽박죽으로 적용됐던
현재 상태도 올바른 상태로
바로잡을 수 있습니다.
상태만 저장하는 시스템에서는
“지금 값을 수동으로 고치는” 수밖에 없고,
왜 고쳤는지는 또 다른 기록에 맡겨야 합니다.
공짜는 아니다
Martin Fowler는 이 패턴에 대가가 따른다고 이야기합니다.1
가장 먼저, 외부 시스템과의 상호작용이
복잡해집니다.
이벤트를 재생하면 결제 게이트웨이나
알림 시스템에 그 메시지가 다시
발송될 위험이 있습니다.
그래서 외부 호출은 게이트웨이로 감싸고,
지금이 “재생 중”인지 구분할 수 있게
만들어야 합니다.
과거 이벤트를 재생할 때 외부 시스템에
그 시점 값을 물어보면 최신 값이 돌아오는
문제도 있어서,
재생할 때는 캐시된 응답을 사용하게 해야 합니다.
코드를 바꾸는 것도 간단하지 않습니다.
버그를 고치고 재생하면 내부 상태는
자동으로 바로잡히지만,
그 버그 때문에 이미 나간 외부
메시지는 별도로 처리해야 합니다.
“이 날짜 이후로 할인 정책이
바뀐다”처럼 시간에 따라 달라지는
로직은
이벤트 시점에 맞는 규칙을
찾아 적용하도록 설계해야 합니다.
그리고 Martin Fowler는 이렇게 이야기합니다.
“모든 변화를 이벤트로 담아내는 방식을
많은 개발자가 어색하고 불편하게
느낀다.”1
따라서 이 불편함을 감수할 만한 분명한 이득이
있을 때만 선택해야 합니다.
이 트레이드오프는 왜 AI가 대신 판단하지 못하는가
AI에게 “재고 관리 기능 만들어 줘”라고 하면
위에서 본 product 테이블 같은 걸 만들어 줍니다.
상태 테이블입니다.
동작만 놓고 보면 아무 문제가 없습니다.
AI가 Event Sourcing 몰라서가 아닙니다.
“나중에 과거를 돌아볼 일이
생기는지”를 알 수 없기 때문입니다.
그건 요구사항 문서에도, 코드에도 없습니다.
이 상품이 재고 불일치 분쟁에 얼마나
자주 휘말리는지,
정산 감사 요구가 법적으로 걸려 있는지,
동시 주문이 몰리는 시즌이 있는지.
전부 도메인을 겪어 본 사람만 아는 사실입니다.
그리고 이 판단을 늦게 내리면 비용이 많이 듭니다.
상태만 저장하도록 만든 시스템은,
나중에 Event Sourcing으로
바꾼다고 해도,
애초에 버린 과거를 되살릴 방법이 없습니다.
이미 지나간 사실은 다시 쌓을 수 없으니까요.
그래서 이 판단은 구현 이전에 설계 단계에서 내려야 합니다.
상태를 저장할지 사실을 저장할지,
그 트레이드오프를 판단하는 것은 사람의 몫입니다.
AI는 그 결정에 따라 구현할 뿐입니다.
모든 곳에 쓰라는 게 아니다
Event Sourcing은 만능이 아닙니다.
단순한 CRUD에 끌어들이면 복잡도만
늘어납니다.
Martin Fowler도 감사 추적이 필요하거나,
이벤트를 재생해 디버깅해야 하거나,
이후 Retroactive Event 패턴 같은 걸
쓸 계획이 있을 때를 예로 듭니다.1
중요한 건 언제 이게 필요한지 아는
감각입니다.
이력이 곧 가치인 도메인, 감사가 필수인 도메인,
복잡한 상태 전이가 있는 도메인.
이런 곳에서 “상태가 아니라 사실을 저장한다”는
선택이 빛을 발합니다.
사고가 나기 전에 답할 수 있나요
누군가 여러분의 시스템에 대해
“그때 무슨 일이 있었죠?”라고 묻는다면,
몇 초 만에 답할 수 있나요,
아니면 “확인이 어렵다”고 말하게 되나요?
답을 못 한다면, 그건 단순히 로그를 안 남겨서가 아닙니다.
UPDATE가 이전 값을 덮어쓰는 로직을
그대로 두었기 때문이고,
애초에 과거를 물어볼
일이 있을지 판단하지 못했기 때문입니다.
이 판단을 내리려면 Event Sourcing의
트레이드오프를 알아야 합니다.
AI는 이 판단을 대신 해 주지 않고,
사고가 일어난 뒤에는 되돌릴 방법도 없습니다.
와일드 백엔드는 도메인 이벤트와 관련된 패턴을 더 깊이 다룹니다.