Architecture

이벤트 소싱을 사용한 이유, 그리고 적용하면서 겪은 문제들 (feat. CQRS)

JeongKyun 2024. 5. 2.
반응형

이번 글에서는 Event Sourcing 사용한 이유와 그 과정에서 겪은 문제에 대해 정리해 볼 생각이고,

생각한 목차는 다음과 같다.

 

목차

1. 풀고자 하는 비지니스 문제

2. 왜 이벤트 소싱?

3. 적용하면서 겪은 문제들

4. 마무리

 

 

풀고자 하는 비지니스 문제

현재 미용 의료 병원의 내원부터 귀가까지 책임지는 올인원 B2B SaaS 제품을 만들고있다. 

 

고객의 내원 ~ 귀가 프로세스에서 큰 틀로보면,

내원 -> 접수 -> 시술 받음 -> 귀가 정도일 수 있겠지만, 이 과정을 좀 더 톺아보면정말 많은 과정들이 숨어있다.

하나의 예시로는 시술을 받는 과정에서 업셀링을 통해 새로운 시술을 실시간으로 추가하여 받을 수 있고, 받고 나와서 서비스가 불만족스러워 환불 처리를 밟을 수도 있다. 또는 피부 케어 서비스가 들어가 추가 시술들이 언제든지 추가될 수 있다.

 

이 복잡한 모든 과정을 데이터로 영속하여 사실에 기반한 데이터로 통계를 내고, 해당 통계를 기반하여 병원의 서비스적인 품질과 직원들의 노동량 평가 등 개선하고자 하는 니즈가 있었다.

 

 

왜 이벤트 소싱?

위 문제에서 말했듯이 우리의 고객사인 병원은 어떤 담당자가 예약 시간을 변경했고, 접수를 담당하고, 상담을 처리하고, 수납을 누가 했고, 어떤 담당의가 시술을 했고, 누가 시술을 받았고 등등 정신없이 운영되는 병원에서 문제가 발생했을 때, 실시간으로 빠르게 대처하기위해 모든 audit log를 보고싶어하는 니즈가 있었다.

 

e.g. 고객이 병원 내원을 했는데, 실제 예약은 해당 시간으로 잡혀져있지 않았다. -> 병원측) 누가 해당 예약을 담당했고, 시간을 변경했고 등 어디서 문제가 발생한건지 알고싶음.

 

따라서, 이런 비지니스 요구사항을 해결하기 위해 결제, 예약 도메인에 과거의 사실 기반으로 도메인 이벤트를 설계하여 상세한 감사 로그를 지원할 수 있도록 이벤트 소싱을 적용해서 풀고자했다.

 

 

적용하면서 겪은 문제들

예약 현황 뷰

위 자료는 고객들의 예약별로 제공해주는 audit log view이다.

누가, 언제 예약 시간을 수정했고, 시술을 추가했고 등등 로그를 제공해줄 수 있다.

 

위 로그를 제공하기 위해선 아래와 같은 도메인 이벤트 재생(Rehydrate) 과정을 거치게되는데,

설계한 도메인 이벤트들을 version 1 부터 재생(rehydrate)하여 최종 상태(state)인 Reservation 애그리것을 만들어낸다.

이 과정에서 기술적 성능 문제가 발생한다.

 

문제

예약 현황(위 이미지 참고)의 예약 데이터들을 가지고오기 위해선 n개의 예약들을 모두 재생을 통해 가지고와야한다.

예상되겠지만, 이 모든 내역을 가지고오기 위해선 정말 많은 연산을 필요로하게된다.

e.g. 100개의 예약을 가지고온다 => 저장되어있는 도메인 이벤트들을 재생하여 최종 예약 상태를 만든다. x 100

만약 n개의 예약의 도메인 이벤트가 1000개 이상이 쌓여있다면 현황 데이터를 가지고오는데만 10000번의 넘는 연산이 필요해질 수 있다.

 

따라서, 이러한 조회 시 성능 문제를 해결하기 위해 CQRS를 적용하여 해결하고자했다.

CQRS는 명령과 조회를 분리하는 패턴을 말한다.

 

CQRS를 우리의 이벤트 소싱 적용한 시스템에 투영하여 프로세스를 도식화해보면 다음과 같다.

프로세스를 글로 표현하면 다음과 같다.

 

Write Model

1. 명령 요청 발생 (API)

2. 이벤트 스토어에서 조회한 이벤트들을 이벤트 핸들러를 통해 재생하여 최신 상태(state)로 만든다.

3. 커맨드 핸들러, 만들어진 최신 상태를 기반으로 도메인 논리를 구현한다.

    3-1. 애그리것의 불변식 검증, 명령 데이터 검증, 명령에 따른 이벤트 발행 로직 구현

4. 이벤트 버스를 통해 메세지 브로커에 도메인 이벤트를 발행한다.

 

사용자 입장에서 명령에 대한 요청 처리는 이대로 끝나게된다.

조회 모델의 sync는 아래와 같이 비동기로 메세지 브로커를 통해 이루어진다.

 

Read Model

1. event consumer가 발행한 이벤트를 소비한다.

2. 소비한 이벤트를 이벤트 핸들러에서 필요한 논리들을 구현하여 리드 모델 데이터 모델에 저장한다.

 

이제 조회는 events_model을 통하는것이 아닌 read_models_model 데이터베이스를 통해 조회하도록하여 어플리케이션의 성능 이슈를 해결하였다. 그러나 최종적인 모습 구현해내기까지 몇가지 이슈들이 있었는데, 아래와 같다.

1. 메세지 순서 보장

2. 중복 메세지 발행

3. sync 과정에서 발생하는 조회 논리 이슈

4. write model의 검증식 구현

 

 

1. 메세지 순서 보장

위 아키텍처를 구성하면서, 메세지 순서 보장은 반드시 필요한 구성요소 중 하나였다.

순서 보장이 안되면, 시술 추가됨 이벤트 -> 시술 수정됨 이벤트 순서를 기대했는데 시술 수정됨 이벤트 -> 시술 추가됨 이벤트 순으로 처리하면서 올바르지않게 동작하게 된다.

따라서, 메세지 브로커 중 순서보장이 가능한 AWS의 Kinesis Data Stream을 채택하게되었다.

Kinesis는 샤딩을 지원하여 stream 처리에 아주 뛰어난 처리량 성능을 보이며, 순서보장까지 가능한 모델이였다.

 

2. 중복 메세지 발행

메세지는 최소한 한번 발행될 수 있다. (at least once)

결국 모든 이벤트 소비자에게 멱등성을 보장시켜주어야했는데, 처리 요구 사항에 따라 여러 방법들이 존재했다.

1. idempotent key를 이용한 처리 방식

2. upsert 방식

3. 유니크 색인을 통해 중복 처리 시 exception processing 방식

4. 발행시간 비교를 통해 무시 논리 구현

등등 더 있을 수 있겠지만 우리는 그 중 2번 방식인 upsert를 택하여 처리했다.

 

3. sync 과정에서 발생하는 조회 논리 이슈

명령 이후 바로 조회 데이터를 반환해야하는 케이스가 존재했다.

e.g. 예약 생성 후 저장된 예약 페이지로 넘어가서 저장된 예약 데이터를 볼 수 있어야한다.

 

결국 비동기로 sync를 하는 최종적 일관성(eventual consistency)의 구성에서는 위 유즈케이스를 해결하기 위해선 강한 일관성(strong consistency)을 보장하기위해 어쩔 수 없이 write model을 반환되도록 하였다.

추가로, 어느정도 시간차가 있어도 되는것들은 폴링을 이용하여 해결한 부분도 존재한다.

 

4. write model의 검증식 구현

commandHandler에서 검증을 할 때, 값 객체의 불변식을 검증하는 방법은 매우 쉬우나, 이전 내역들 데이터를 조회하여 검증해야 하는 경우가 있다. e.g) 동일한 내원객은 동일한 시간의 예약을 할 수 없다.

 

위 케이스를 현재 구조에서 검증하기위해선, write model에서 모든 이벤트들을 재생해야지만 검증할 수 있다.

그러면, 결국 예약 생성 명령이 발생할 때마다 모든 이벤트를 재생해야하는 큰 이슈가 존재하게되는데, 이를 해결하기 위해서 Redis를 이용하여 중복 예약 검증 저장소를 구현하였다.

 

구현 방식은 아래와 같다.

예약 생성 시, Redis에 visitorId와 reservationTime을 이용하여 저장해둔 뒤, 다른 예약을 생성할 때 해당 저장소에서 검증할 수 있도록한다. 이와 같이 모든 이벤트를 재생하지 않고 인 메모리 디비를 활용하여 검증할 수 있도록 구현하여 해결하였다.

 

 

마무리

이번글에선 이벤트 소싱을 적용하면서 발생하는 성능 이슈를 CQRS로 해결한 사례를 작성하였는데,

추 후엔 성능 이슈가 아닌 좀 더 서비스 구현에 있어서의 이슈들을 다뤄볼 예정이다.

댓글

💲 많이 본 글