나만 모르고 있던 CQRS & EventSourcing

CQRS (Command and Query Responsibility Segregation) 란?

CQRS는 네이밍에서 알 수 있듯이 명령과 쿼리의 역할을 구분 한다는 것이다. 즉 커맨드 ( Create – Insert, Update, Delete : 데이터를 변경) 와 쿼리 ( Select – Read : 데이터를 조회)의 책임을 분리한다는 것이다. CQRS는 새롭게 등장한 개념은 아니고 등장한지 조금된 패턴 이다.

cqrs

(버란트 마이어(Bertrand Meyer) 의 CQS가 CQRS의 출발 이었다는 것에 대해 모두들 이의가 없는듯 하고 CQRS를 처음 소개한 이는 Greg Young임.)

왜 생겨 난거지?

전통적인 CRUD 아키텍처 기반에서 Application을 개발 및 운영하다 보면 자연스레 Domain Model의 복잡도가 증가되기 마련이고 이로 인해 유지보수 Cost는 지속적으로 증가하게 되며 Domain Model은 점점 설계 시 의도한 방향과는 다르게 변질 되게 된다. 특히나 요즘처럼 고차원적인 UX(User eXperience), 급변하는 Business, 시도때도 없이 달라지는 요구사항을 충족하는 Model을 만드는건 더욱더 어려운일이 되어버렸다.
이런 일련의 변경사항과 흐름을 곰곰히 관찰해 보니 Application의 Business정책이나 제약(흔히 비지니스 로직이라 부르는것들)은 거의 대 부분 데이터 변경 (C,U,D) 작업에서 처리되고 데이터 조회(R)작업은 단순 데이터 조회가 대 부분인데 이 두 업무를 동일한 Domain Model로 처리하게 되면 각 업무 영역에 필요치 않은 Domain 속성들로 인해 복잡도는 한 없이 증가하고 Domain Model은 애초 설계 의도와는 다른 방향으로 변질되게 된다.
그럼 어떻게 이 문제를 해결 할 수 있을까? 우리 선배 개발자님들은 고민을 시작했고 의외로 해법은 간단했다. 바로 명령을 처리하는 책임조회를 처리하는 책임을 분리 구현 하면 되는거 아닌가? 그렇다 이게 바로 CQRS 인것이다.
(사실 CQRS는 도메인 주도 개발 – DDD (Domain-driven design) 기반의 Object Model 방법론 적용시 나타났던 문제점들을 해결하기 위해 등장했다고 보는것이 보다 더 명확할듯 합니다.)

어떻게 써야 하지?

일반적으로 전통적인 CRUD 시스템은 다음 그림과 같은 계층 구조로 처리되고 있다.

전통적인 CRUD 시스템 구조조

전통적인 CRUD 시스템 구조 – 이미지 출처 : http://auconsil.blogspot.com/2013/08/cqrs-command-query-responsibility.html

이런 전통적인 계층 구조에 CQRS 패턴을 적용하기 위한 방법에는 최소(크게 구분) 3가지의 방법이 있다.  이는 마치 호텔의 객실 등급과 같은 개념으로 나누어 볼 수 있는데 이 세 가지에 대해 임의로 일반/프리미엄/디럭스 로 이름을 붙여보자.  대 부분의 CQRS의 소개 글들은 디럭스급 적용을 예시로 들고 있고 이러다 보니 CQRS가 복잡하고 적용하기 어렵다는 오해를 불러왔다고 생각한다. (물론 CQRS적용이 쉽다는 의미는 아님)

1) 일반

첫번째로 가장 간단하게 적용할 수 있는 일반 방식에 대해 알아보자.
이는 단일 Data Store에 Command와 Query Model을 분리된 계층으로 나누는 방식이다.

cqrs2

1) Simple한 CQRS적용 구조 – 이미지 출처 : http://auconsil.blogspot.com/2013/08/cqrs-command-query-responsibility.html

1)번 그림 처럼 Database(RDBMS) 는 분리하지 않고 기존 구조 그대로 유지 시키고 Model Layer 부분만 Command와 Query Model로 분리하는 수준으로 간단하게 적용할 수 있다. 이렇게 분리된 Model은 각자의 Domain Layer에 대해서 만 모델링하고 코딩하기 때문에 훨씬 단순하게 구현/적용 할 수 있다. 하지만 동일 Database사용에 따른 성능상 문제점은 개선하지 못한다.

2) 프리미엄

cqrs3

2) Database까지 분리한 CQRS적용 구조 – 이미지 출처 : http://auconsil.blogspot.com/2013/08/cqrs-command-query-responsibility.html

2) 번의 형태는 Command용 Database와 Query용 Database를 분리하고 별도의 Broker를 통해서 이 둘 간의 Data를 동기화 처리 하는 방식이다. 이 Case는 데이터를 조회 하려는 대상 서비스(시스템)들은 각자 자신의 시스템(서비스)에 맞는 저장소를 선택 할 수 있기에 폴리글랏 저장 구조 (참고 : 다수의 Database 혼용하여 사용 하는 것을 폴리글랏 저장소 라고 함)로 구성 할 수 도 있다. 이 경우 각각의 Model에 맞게 저장소(RDBMS, NoSql, Cache)를 튜닝하여 사용할 수 있다는 이점이 있고 이는 1)일반 유형에서 거론된 동일 Database사용에 따른 성능 관점의 문제점을 해결 할 수 도 있다. 하지만 동기화 처리를 위한 Broker의 가용성과 신뢰도가 보장되어야 하는 Risk가 존재한다.

3) 디럭스

cqrs_6_cqrs_es-300x140

3) EventSourcing 모델 – 이미지 출처 : https://www.future-processing.pl/blog/cqrs-simple-architecture/

3)번째 형태는 이벤트 소싱(EventSourcing)을 적용한 구조이다.
이벤트 소싱이란 Application내의 모든 Activity를 이벤트로 전환해서 이벤트 스트림(Event Stream)을 별도의 Database에 저장하는 방식을 말한다. 이벤트 스트림을 저장하는 Database에는 오직 데이터 추가만 가능하고 계속적으로 쌓인 데이터를 구체화(Materialized) 시키는 시점에서 그때 까지 구축된 데이터를 바탕으로 조회대상 데이터를 작성하는 방법을 말한다. (Application내의 상태 변경을 이력으로 관리하는 패턴이 발전된 형태로 보면 이해가 빠를것 같다.)
이벤트 소싱의 이벤트 스트림은  오직 추가만 가능하고 이를 필요로 하는 시점에서 구체화 단계를 거치게 되고 이런 처리 구조가 CQRS의 Model분리 관점과 굉장히 궁합이 잘 맞기에 대 부분 CQRS 패턴을 적용하고자 할 때 이벤트 소싱이 적용된 구조를 선택하게 된다.
주의) CQRS패턴에 이벤트 소싱은 필수가 아니지만 이벤트 소싱에 CQRS는 필수임

마치며

필자는 CQRS을 Production 레벨에 적용한 경험은 없고 학습을 통해 익힌 지식을 개인 Example Project를 통해서 구현한 경험이 있을 뿐이다. 그러기에 Production 레벨에 적용 시 나타날 수 있는 문제점들에 대한 know-how는 전무하다. CQRS를 적용하기에 앞서 어떤게 Command이고 어떤게 Query 인지를 명확하게 분리하는것도 녹록치 않다고 생각한다. (Martin Fowler는 스택 자료구조의 pop() 연산을 예로 들었고, 또한 본인이 읽은 또 다른 글의 저자 이규원님은 사용자 인증토큰 발행 로직의 예를 들었음.)
하지만 지속적으로 확장되고 변경되는 요즘 서비스들의 특성상 CQRS패턴은 매우 유용한 해법이 될 수 있을거라 조심스럽게 판단해 본다. (단 Business 로직이 매우 간단한 서비스라면 분리로 인해 발생하는 서비스 구조 복잡도가 오히려 더 클 수 있으므로 이런 경우 굳이 도입할 필요는 없을 것 같다)
다만 모든 약이 그렇듯이 약의 효능과 복용법에 대해 제대로 알고 복용을 해야지 남들이 좋다고 하니깐 무조건 복용하는 오남용은  오히려 독이 될 수 있음을 잊지 않길 바란다. 필자가 좀 더 구체적으로 이해 할 수는 글을 작성(번역)해 주신 Bapul.net 의 김영재 CTO 님과 이규원 님에게 감사에 말을 전하고 싶다.

참고자료

http://martinfowler.com/bliki/CQRS.html

http://martinfowler.com/eaaDev/EventSourcing.html

[번역] 최신기술 – CQRS 처음 도입하기

CQRS란 무엇인가?

http://auconsil.blogspot.com/2013/08/cqrs-command-query-responsibility.html