신사업에서 Event Sourcing Pattern을 왜 사용했나요?

서론
소프트웨어 개발에서는 데이터 저장 방식이 시스템의 확장성과 유지보수성에 큰 영향을 미친다. 특히, 변화가 빈번하고 복잡한 비즈니스 요구사항이 있는 환경에서는 단순한 CRUD 기반의 상태 저장 방식으로는 한계에 봉착하곤한다.
필자가 현재 기여하고있는 신사업 B2B SaaS 제품(이하 KOS)도 마찬가지였다. 병원 내에서는 고객의 내원부터 귀가까지 다양한 이벤트가 발생하며, 이 과정에서 실시간으로 예약 변경, 시술 추가, 환불 처리 등 여러 상황이 동시다발적으로 이루어진다. 이러한 복잡한 프로세스를 모두 데이터로 기록하고, 추적 가능한 방식으로 관리하는 것은 병원 운영의 효율성을 높이고 직원들의 업무 평가에 활용할 수 있는 중요한 과제였다.
이벤트 소싱?
모든 상태 변화를 이벤트(Event)라는 형태로 기록하고, 이를 기반으로 시스템의 현재 상태를 재화(Rehydration)시키는 방식이다. 단순한 CRUD 방식의 상태(State) 저장 방식과 달리, 과거의 모든 이벤트를 영속적으로 유지하며 감사 로그(Audit Log)와 데이터 추적을 용이하게 만들어 준다.
현실 세계의 문제는 생각보다 더 복잡했다
미용의료 병원에서 고객의 내원 ~ 귀가 프로세스를 바라보면, 병원 내원 -> 데스크 접수 -> 시술 -> 귀가
정도로 보일 수 있겠지만, 이 과정을 좀 더 톺아보면 정말 많은 과정들이 숨어있다. 하나의 예시로는 시술을 받는 과정에서 업셀링을 통해 새로운 시술을 실시간으로 추가하여 받을 수 있고, 받고 나와서 서비스가 불만족스러워 환불 처리를 밟을 수도 있다. 또는 피부 케어 서비스가 들어가 추가 시술들이 언제든지 추가될 수 있다.
무엇보다 이 복잡한 모든 과정을 데이터로 영속하여 사실에 기반한 데이터로 통계를 내고, 해당 통계를 기반하여 병원의 서비스적인 품질과 다른 제품에선 제공할 수 없는 여러 인사이트를 혁신적으로 제공하는것이 우리 팀이 그린 KOS 제품의 최종적인 모습이였다.
도입하여 제품팀이 얻은 가치는?
신사업에서 많은 레퍼런스가 없는 이벤트 소싱을 도입하는것이 챌린징적인 요소이긴했지만, 도입 이후 결과적으론, 사실 기반의 정량적 데이터로 효과적인 결과를 거둘 수 있었다. (특히, Product Owner와 Product Designer가 제품의 새로운 문제 영역을 식별하고 정책을 마련하는데있어 가장 크게 도움이 되었던것같다)
도입하여 고객이 얻은 가치는?
병원의 담당 부서마다 얻은 가치가 다르긴하지만 가장 반응이 좋았던것은 병원 관리자가 누가, 왜, 언제, 어떤 작업을 수행한것인지 파악하는게 가장 큰 골칫덩어리였는데 하나의 Stream에서 발생한 모든 사건(Event)를 기록하고있기때문에 이는 자연스럽게 해결할 수 있었다. 예를들어 다음과 같이 말이다.
e.g. 어딧로그
병원 CS 담당자: 1.6일 내원한 안정균 고객님의 예약 정보를 누가 변경한것인지 알 수 있을까요? 가능하면, 변경한 시각도 알고싶어요 !
KOS 제품의 예약 현황 View
KOS: 예약 현황 or 예약 상세에서 어딧 로그 기능을 제공하고있습니다. (위 사진 참고)
병원 담당자: 혹시 그렇다면 예약별로 ‘어느’ 시술을 받는데 ‘얼마나’ 시간이 소요되었고, ‘언제’ 귀가했는지를 알 수있을까요?
KOS: 네, 예약 통계에서 요청 주신 데이터 모두 확인가능합니다.
병원 담당자: 와우!
위 과정이 가능하게된것은 병원에서 수행된 모든 이벤트가 기록되어있음으로, 어떤 상품으로 시술을 받았고, 언제부터 시술을 받았고, 종료되었는지, 또한 귀가는 언제했는지 등 모든것을 제공할 수 있었다.
그럼 Event Sourcing를 사용안했다면 해결못하는 문제였나?
(당연히) 아니다. 문제를 식별하고 이를 해결하는 과정에는 팀의 환경, 구성원들의 역량, 제품 특성 등 모든것을 고려하여 다양한 솔루션을 내세울 수 있다. 이 과정에서 KOS 제품이 Event Sourcing을 진행하기에 적합한 환경이였을뿐이다.
적합했던 이유라하면, KOS 제품은 도메인 주도 설계(Domain-Driven Design) 방법론에서 여러 전략, 개념들을 이용하여 제품 설계를 초기부터 진행했었기에, 명확한 명령(Commands), 사건(Events), 집계(Aggregates)가 정의를 해놓은 상태였고 필요 시 이벤트 스토밍도 진행하여 필요한 재료들은 세팅이 되어있었기 때문인것같다.
적용하면서 발생한 문제는?
이벤트 재화(Rehydration)에 따른 성능 비용
이벤트 소싱은 모든 상태(State)를 이벤트의 흐름(Event Stream)으로부터 재구성해야 한다. 그러나 다수의 이벤트가 포함된 스트림을 조회할 때, 각 이벤트를 순차적으로 재생하는 작업이 성능 병목을 초래했다. 특히 예약 현황을 조회하거나 변경 이력을 확인하는 기능에서 조회 시간이 급격히 늘어나는 문제가 발생했다.
e.g. 예약현황 뷰
위와 같이 예약현황이 있고, 만약 각 내원객의 예약마다 100개의 이벤트가 존재한다면, 예약 수 x 이벤트의 재화 연산을 거쳐야한다. 단순하게 예약이 100개 있고, 각 예약마다 100번의 이벤트가 발생했었다면 최소한 예약 현황을 제공하기위해 10,000번의 연산이 애플리케이션에서 이루어져야한다는 말이다. (또한 Stream마다 Query I/O 비용도 추가로 발생한다)
이러한 문제를 CQRS 패턴을 도입하여, 재화 연산 비용에 대한 문제를 해결했다.
명령과 조회를 분리하여 이벤트 소스 데이터는 Write Model로 유지하고, 조회는 별도의 Read Model을 통해 처리하는 방식으로 개선하였다. 이때 Read Model은 필요한 데이터를 이벤트로부터 미리 연산하여 물리적 테이블로 저장해 두는 방식으로 성능 최적화를 진행했다.
CQRS 패턴 도입 외 또 다른 솔루션으론 특정 시점의 상태를 스냅샷으로 저장하고, 이후 이벤트만 재생시켜 최종 상태를 재구성하는 전략도 있었으나, 실시간으로 여러 Read Model을 구성해야하는 요구사항이 있어 CQRS 패턴을 택하게되었다.
이벤트 순서 보장
이벤트 기반 아키텍쳐(Event-Driven Architecture)에서도 중요하게 다뤄지는 이벤트의 순서 보장에 대한 내용이다.
이벤트가 시스템에 저장되거나 소비될 때 순서가 보장되지 않으면, 잘못된 상태가 만들어질 수 있다. 예를 들어, “예약 변경됨”이라는 이벤트가 “예약 생성됨“ 이벤트보다 먼저 처리되는 경우 비지니스 로직이 올바르게 동작하지 않을것이다.
해결안
이러한 문제를 방지하기위해 Message Broker의 파티션 키를 이용하여 순차처리하도록 진행하였다.
Message Broker로는 AWS Kinesis Data Stream을 사용하였고, 순서 보장과 더불어 온디멘드 샤딩을 지원하여 Stream 처리량이 매우 뛰어난 서비스이기에 채택하게 되었다.
중복 메세지 처리
메세징 시스템을 구성에 있어 메세지 전달 전략들이 여러가지가 있는데, 가장 쉽게 구성할 수 있는것이 At Least Once 방식이다. 말그대로 최소한 한번 이벤트가 발생할 수 있다는 말이다. 그냥 한번만 생산(Produce)하면 되는것아닌가? 싶을 수 있는데, 이것은 이번 주제와 무관하다 생각하여 자세한 이야기는 생략한다. (이벤트 전달 전략은 조만간 작성해 볼 예정)
해결안
중복 메세지가 소비될 수 있는 부분에 대해선 소비자 워커(Consumer Worker)에서 멱등성 논리를 구성하였다. 모든 소비자에 멱등성 논리가 들어간것은 아니고, 요구사항에 따라 소비자 모델의 고유 ID를 통해 이미 처리된 이벤트는 무시되도록 설계한 방식도 존재한다.
최종적 일관성으로 인한 동기화
이벤트를 생산(Produce)하고 Write Model은 수행을 끝낸다. Read Model의 워커에서 비동기로 이벤트를 소비하여 데이터를 처리한다. 즉, 강한 일관성(strong consistency)을 보장하지못하며, 비동기로 인해 최종적으로 데이터 일관성을 보장한다.
이러한 최종적 일관성에 대해 사용자는 다음과 같은 불편감이 생길 수 있다.
병원 담당자: 예약 생성 버튼 클릭 → 성공 ! → 응? 성공인데 왜 화면에 생성이 안되는거지? (버그인가?)
시스템적으로 보았을 땐, 당연한 이야기처럼 보이지만 사용자 입장에선 황당한 일이고 버그로 인지하기 딱 좋은 케이스이다.
해결안
위와 같은 이슈를 해결하기 위해 WebSocket을 지원하는 이벤트 워커를 구현하여, 생산된 이벤트를 워커에서 소비하면 WebSocket에 연결되어있는 모든 웹 브라우저에게 변경된 사항을 실시간으로 전달하도록했다.
동시성 문제
여러 사용자가 동시에 하나의 스트림(e.g. 예약 데이터)에 수정을 가하면 갱신 손실 문제가 발생할 수 있다. 제품 특성 상 병원 데스크에서 동일한 예약 정보에 동시에 변경을 가하는일이 간혹 일어난다. 이런 상황에서 동시성 문제가 제대로 관리되지않으면 고객은 예상치 못한 혼란스러움을 겪을 수 있다.
e.g. 동시성 상황
[동시 요청]
(데스크 직원 A) a, b, c 시술 테스크 저장(데스크 직원 B) c, d, e 시술 테스크 저장
시술 테스크의 Sequence Key는 애플리케이션에서 1씩 증가하는 논리를 갖고 있다. 하지만 동시 요청이 발생하면, 동일한 Sequence Key를 가진 테스크가 저장될 가능성이 있다. 이는 데이터 충돌을 초래하며, 결과적으로 잘못된 시술 테스크 정보가 저장될 위험이 있다.
해결안
동시성 문제를 제어하는 방법은 요구사항마다 다르긴하지만, KOS에서 이벤트 소싱을 다루고있는 영역안에선 Event Schema에 이벤트가 쌓일 때마다 1씩 증가하는 version이라는 속성을 유니크 색인으로 정의하여 낙관적 잠금(Optimistic Lokcing)을 통해 제어하고있다. 이벤트 소싱에선 이벤트가 쌓인 순서가 매우 중요한 요소이기때문에 version이 꼭 필요하다. 그런데 만약 제품 특성 상 동시성 문제가 빈번하게 일어나며 이로인해 고객의 서비스 품질이 저하된다 생각하면 다른 잠금 전략들을 고려하였을것이다.
마치며
이벤트 소싱을 신사업 B2B SaaS 제품에 적용하면서 얻은 가치와 직면했던 문제들을 정리해보았다. 이벤트 소싱은 복잡한 도메인 문제를 해결하고, 과거 데이터를 기반으로 명확한 인사이트를 제공하는 데 강력한 도구가 될 수 있었다.
그러나 이벤트 소싱은 제시한 모든 문제를 해결하는 은탄환이 절대 아니다.
결국, 기술의 선택은 팀 또는 개인의 환경과 목표에 따라 달라진다 생각한다. 이벤트 소싱이 적합한 상황에서는 강력한 도구가 될 수 있지만, 단순한 상태 저장이나 CRUD 중심의 애플리케이션에서는 오히려 과도한 설계 복잡성을 초래할 수 있다.
Subscribe to my newsletter
Read articles from Jeongkyun An directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
