티스토리 뷰

⑨ IT Wordbook

Event Sourcing & CQRS

GodNR 2020. 5. 12. 12:51
728x90
반응형

개요

마이크로서비스 특히 분산DB 환경에서 CQRS는 매우 중요한 개념이다. CQRS는 CUD(쓰기)와 R(읽기)의 책임을 분리하는데 착안한 방식으로 아키텍처 패턴이 아닌 코드 패턴이다.

인터넷의 성장으로 인해 소수의 사용자를 위해 응용 프로그램을 만드는 것은 생각할 수 없다. 대부분의 새로운 애플리케이션은 확장성, 성능 및 가용성을 전제로 하며, 이러한 유형의 아키텍처는 서버를 확장함으로써 해소할 수 있다. 클라우드 컴퓨팅으로 마이그레이션하고 수요에 따라 Auto Scaling을 적용할 수 있다. 이는 응용 프로그램의 성능과 가용성을 해결하는 손쉬운 방법 중 하나이다.

다만, 애플리케이션 서버를 확장하는 것만으로 모든 문제가 해결되는 것은 아니다. DeadLock, TimeOut 및 속도 저하로 인해 데이터베이스에 대한 문제가 발생할 수 있으며, 대부분의 애플리케이션 서버는 이에 영향을 받는다. 데이터베이스 확장성은 애플리케이션 서버를 확장하는 것보다 훨씬 더 복잡하고 비용이 많이 들 수 있다. 따라서 이를 해소하기 위한 여러 방안들이 검토되어야 하며, 마이크로서비스 아키텍처에 적용하기 용이한 패턴 중 하나인 CQRS를 소개하고자 한다.


CQRS 이해

모놀리식 시스템의 단일 데이터베이스환경은 데이터 쓰기와 조회가 함께 발생하여 조회 성능에 문제가 발생할 수 있다. CQRS는 물리노드를 분리하여 데이터 쓰기 및 읽기에 대한 책임을 분할한다. 읽기는 별도의 정규화되지 않은 데이터베이스에서 동기적으로 수행되며 쓰기는 RDB에 비동기로 기록된다.

CQRS의 구현은 애플리케이션에 따라 간단하거나 매우 복잡할 수 있다. 구현 방법에 관계없이 CQRS는 항상 추가 복잡성을 가져오므로 이 패턴으로 개발하는데 필요한 시나리오를 평가해야 한다. 평가 기준은 프로젝트 기준으로 수립되어야 하며, 목표에 따라 상이할 수 있다.

CommandStack(CUD) DB와 QueryStack(R) DB간 데이터 동기화 방안

  • 자동 업데이트 : CommandStack DB에 CUD가 발생할 경우 모든 변경사항은 동기 프로세스로 QueryStack DB에 업데이트하는 방식 (CUD 성능 지연, 즉시 일관성)
  • 업데이트 가능 : CommandStack DB에 CUD가 발생할 경우 모든 변경사항은 비동기 프로세스를 트리거하여 최종 데이터 일관성을 제공 (CUD 성능 향상, 최종 일관성)
  • 제어된 업데이트 : 데이터베이스를 동기화하기 위해 배치 처리 (실시간 처리가 필요하지 않은 경우)
  • 요청 시 업데이트 : QueryStack DB에 조회 시점에 일정 시간이 경과된 데이터의 경우 강제 업데이트 (업데이트 되는 주기가 명확할 경우)
 

CQRS 구현의 장점과 단점

CQRS는 쓰기와 읽기의 전체 프로세스가 동일한 계층에서 처리되는 기존 모놀리식과는 다른 개념을 제시한다. CQRS와 관련된 개념은 더 큰 확장성과 가용성을 제공한다.

장점

  • 대기 시간을 줄이기 위해 CommandStack(CUD)은 비동기식으로 큐에서 처리
  • 데이터를 쓰는 요청과 읽는 요청은 동일한 자원을 두고 경쟁하지 않음
  • QueryStack에 대한 쿼리는 별도로 독립적으로 수행되며 CommandStack 처리와 독립적
  • CommandStack 프로세스와 QueryStack 프로세스 별도 확장 가능
  • 비즈니스 의도 및 기타 DDD 개념에서 유비쿼터스 언어(보편 언어)를 사용한 도메인 표현

단점

  • 구현의 어려움
  • 응용 개발자의 도메인 및 유비쿼터스 언어(보편 언어)에 대한 명확한 이해를 요구하며, 복잡가 높음
  • 최종 일관성을 보장하기 위해 아키텍처 측면에서 심도있는 고려가 필요
  • 추가 솔루션을 도입할 경우 관리 요소 증가에 따른 유지보수 영역 증가

CQRS는 아키텍처 패턴이 아니며 응용 프로그램의 일부를 구성하는 형태이다. 일반적인 오해는 CQRS가 항상 이벤트 소싱과 함께 사용되어야 한다는 것이다. 이벤트 소싱은 CQRS와 강한 연관성을 가지고 있으며 CQRS도 있기 때문에 쉽게 구현할 수 있지만, CQRS와는 독립적으로 이벤트 소싱을 구현할 수 있다.


이벤트 소싱 – 데이터 무결성

기존의 데이터베이스의 CUD는 기록과 결과를 각 Command 별로 로그를 기록함으로써 데이터를 추적하고 감사해 왔다. 현재 데이터베이스의 상태만을 확인할 수 있으며, 로그를 통하더라도 특정 이벤트에 대한 흐름을 추적관리할 수 없는 것이 사실이다. 때로는 과정이 중요한 비즈니스의 경우 각 트랜잭션의 전체 과정을 기록하고 관리할 필요가 있으며, 우리는 이를 데이터 관점이 아닌 이벤트 관점에서 접근하여 이벤트 소싱이라는 개념을 생성하게 되었다.

이벤트 소싱 이해

이벤트 소싱은 Insert만 존재한다. Insert, Update, Delete가 발생할 경우 현재 데이터를 변경하는 것이 아닌 변경된 데이터를 insert 하는 방식이라고 이해하면 된다. 테이블의 각 업데이트는 상태 변경이라는 새 행을 생성하고, 잘못된 상태로 변경한 경우 이 상태를 수정하여 새 라인을 생성한다. 이는 최종 데이터와 일정 시작 시점을 기준으로 모든 변경 사항을 기록하게 된다.

이벤트 소싱에는 긍정적인 요소와 부정적인 요소가 있다. 긍정적인 점에는 과거 기록을 관리할 수 있는 반면, 부정적인 점에는 데이터베이스의 데이터가 기하급수적으로 증가할 수 있다는 점이다. 이러한 이유로 이벤트 소싱이 CQRS와 함께 사용되는 것이 일반적이다. 정확히는 동일한 레코드의 수가 많기 때문에 데이터베이스에서 검색을 방해하지 않기 위해 독립적인 Query를 수행할 수 있는 환경을 제공하는 것이다.


Event Sourcing & CQRS

예를 들어 야구 티켓을 구매할 경우를 생각해 보자.

야구 티켓 예매를 위해 우리는 잔여 좌석 수와 예매 가능 수 등의 결과를 위주로 데이터를 저장한다. 잠실 야구장 LG 야구 경기 관람을 위해 티켓을 구매하려 한다. 최초 가족 모두 편안한 관람을 위해 가족석을 3매를 예매하려고 선택을 했다. 이때 딸이 우리 신나게 응원하면서 관람하자고 하여 해당 좌석을 취소하고 다시 응원석 쪽 예매를 진행하였다.

이때, 기존 애플리케이션은 다음과 같은 정보를 저장한다.

 

  - 응원석 x열x줄x번째 좌석 3개 예매

 

기존 우리는 중간에 어떠한 동작을 했던 상관없이 마지막 결과에 대한 데이터의 변화를 추적하는데 초점을 맞추고 있다. 이때 우리는 가족석에서 응원석으로 Update가 발생하였으며, 마지막 구매로 인한 Add도 발생했다. 이벤트 소싱은 이와 다르게 이벤트가 발생하기 전 이벤트가 발생하는데 어떠한 과정이 있었는지를 주목한다. 동일한 과정이 발생했을 때 이벤트 소싱은

 

  - 가족석 추가
  - 가족석 삭제
  - 응원석 추가
  - 응원석 구매

 

이 전체를 하나의 데이터로 관리한다. 이와 같은 방식은 Update, Delete가 없는 오직 Add만 가능한 방식이라 할 수 있다.

Event Sourcing은 이와 같이 데이터의 누적으로 하나의 이벤트를 하나의 데이터로 처리하다보니 수많은 데이터가 누적되게 된다. 이로 인한 문제도 발생할 수 있는데, 바로 다음과 같은 상황이다.

위 첫번째 예시의 경우 총 4개의 데이터가 저장(Add)되지만, 만약이와 같은 반복이 수백번 반복되었다고 가정해 보자. Event Sourcing은 단순히 데이터를 순차적으로 읽어가는 역할을 하기때문에, 만약 마지막번째 데이터를 찾기 위해서는 우리는 수백번의 읽기 작업을 반복해야 할 것이다.

읽기 문제 해소 방안

  • 스냅샷 : 첫번째는 스냅샷을 활용하는 방안이다. 데이터의 중간 중간을 스냅샷으로 저장하여 빠르게 데이터를 찾아가는 것이다. 예를 들어 1000번의 기록이 저장된 이벤트 소싱 방식의 데이터에 마지막 데이터를 찾기 위해 900번째 데이터 값을 스냅샷으로 저장한다면, 100번의 조회만으로 결과 값을 찾아 낼 수 있을 것이다.
  • CQRS 조합 : CQSR는 CUD와 R을 분리하여 조합하는 구조이다. 이벤트 소싱은 쓰기에 적합한 방식이라면, CQRS는 읽기에 보다 최적화 된 조합이라 할 수 있어 조합하여 활용하기 좋은 방식이다.

EventSource & CQRS 프로세스

  • Command(CUD)가 발생하면 Command API에 요청이 전달된다.
  • Command를 처리하는 서비스는 Event Store에 API 처리 결과를 저장한다.
  • 이벤트가 처리된 후 이벤트 브로커로 결과가 전송된다.
  • 이벤트는 제한적이며, 이벤트 기반의 독립적인 형태로 통합된다.
  • 이 이벤트는 Queries API로 전달되며, View Store에 저장된다. 이후 Client로부터 Query(R)가 요청되면 이 View Store에 동기화 된 데이터를 조회하게 된다.

결론

CQRS와 EventStorming은 분산DB 환경에서 반드시 고려되어야 할 코딩 패턴이다. 특히 이 패턴은 보상트랜잭션을 처리하는데에도 유용하게 활용할 수 있다. 이전 데이터에 대한 정보를 저장하고 관리하기 때문에 손쉽게 타 마이크로서비스의 의미상의 롤백을 처리하는 방법에 대해 관리할 수 있을 것이다.

다만, 여러번 언급했듯이 CQRS를 설계하고 구현하는데에는 많은 노력과 고통이 수반될 수 있다. 따라서 비즈니스 프로세스에 대한 충분한 이해와 마이크로서비스 아키텍처를 이해하는 아키텍트 간의 협업을 통해 적용 방안을 설계하는 것이 중요하며, 가능하다면, CQRS를 설계하기 보다, 복제, 쿼리 개선, 비즈니스 개선으로 해소할 수 있다면, 이를 우선시 하는 것이 바람직할 수 있다.

728x90
반응형