본문 바로가기
🔬아키텍처/- Event-Driven-Architecture

[Event-Driven-Architecture] CQRS 패턴에 대한 오해 풀기

by Wonit 2022. 6. 11.

목차

  • 도입
  • Query 와 Command 란?
  • CQRS 란
  • CQRS 의 장단점

도입

 

회사 시스템이 전통적인 CRUD 애플리케이션에서 Event 기반의 시스템으로 바뀌어 가는 과정에 팀에 합류를 하게 되어 나의 최근 가장 큰 관심사가 바로 이 CQRS 이다.

 

학부 시절에도 마이크로서비스를 공부하며 잠깐 잠깐 봤던 CQRS 는 이름 부터 생소하기에 겁을 먹었던 기억이 난다.

 

하지만 CQRS 의 원리 자체는 사실 되게 간단하다

 

CommandQuery 를 분리하자!

 

Command 와 Query 를 먼저 정의하고 이야기를 계속 해보자

Query 와 Command 란?

 

Query 와 Command 에 대해서 이야기 하기 위해서는 CQS 에 대해서 먼저 알아볼 필요가 있다.

 

 

CQS 는 Design By Contract 라는 용어를 만든 버트란드 메이어, Betrand Meyer 가 소개한 개념이다.

 

함수는 특정 동작을 수행하는 코드 블록을 의미하는데, 함수의 목적에 따라서 두가지로 분류할 수 있다.

 

그것이 바로 CommandQuery 이다.

 

Command

 

Command 는 시스템에 어떠한 side effect, 즉 변경을 가하는 행위를 하는 것을 말한다.

 

그래서 Command 성 함수 라고 한다면 변경을 가하는 함수를 말할 수 있다.

 

Command 성 함수는 시스템의 상태를 변경시키는 대신 값을 반환하지 않아야 한다.

 

// O, 상태만 변경시킴
void updateUser(User user) {
  user.updateAge(12);
}

// X, 값을 반환
User updateUser(User user) {
  return user.updateAge(12);
}

 

Query

 

이에 반해서 Query 는 시스템의 상태를 관찰할 수 있는 행위를 하는 것을 말한다.

 

마찬가지로 Query 성 함수라고 한다면 단지 시스템의 상태만 확인하는 함수라고 할 수 있다.

 

Query 성 함수는 시스템의 상태를 단지 반환하기만 하고 상태를 변경시키지 않아야 한다.

 

// O, 값만 반환
User getUser(Long userId) {
  return users.get(userId);
}

// X, 상태를 변경
void getUser(Long userId) {
  User user = users.get(userId);
  user.updateLastQueriedAt();
  return user;
}

 

CQS 는 Command Query Separation 이다

 

 

버트란드 마이어는 위의 Command 와 Query 를 분리해야 하며 하나의 함수는 이 성격을 띄어야 한다고 했다.

 

즉, 어떠한 함수가 있다면 그 함수는 Command 또는 Query 중 하나의 역할만 수행해야 한다.

 

만약 하나의 함수에서 Command 와 Query 가 모두 동시에 일어나게 된다면, 이는 소프트웨어의 3가지 원칙 중 복잡하지 않아야 한다는 KISS 가 지켜지지 않을 것이다.

 

이런 관점에서 연장선상에 있는 것이 바로 Command Query Responsibility Separation 이다.

 

CQRS 란?

 

CQRS 는 Greg Young 이 소개한 말이고, CQS에 비해 조금 더 큰 레벨에서의 Command 모듈과 Query 모듈의 책임을 분리하자는 말이다.

 

CQS 는 코드 레벨에서의 분리를 말한다면 CQRS 는 모듈 레벨에서의 분리를 의미한다.

 

좀 더 리얼 월드 수준으로 이야기 하자면

 

데이더 저장소에 대한 읽기업데이트 작업을 구분한다는 것이다

 

왜 이렇게 할까?

 

기존의 아키텍쳐

 

기존 아키텍처에서 도메인이 갖는 의미에 대해서 생각해보자.

 

도메인이란 곧 비즈니스이다.

 

비즈니스는 보통 특정한 데이터의 상태를 변경 (create, update, delete) 을 하는 것이다.

 

이러한 비즈니스는 시간이 증가하면서 점점 복잡도가 올라가게 되고, 많은 요구사항들을 포함할 수 있어야 했다.

 

하지만 query 는 어떠할까?

 

query 는 단순 데이터 조회이기 때문에 비즈니스와 무관하지만 가끔 query 를 위한 처리가 도메인에 침투하는 경우가 생긴다.

 

안 그래도 도메인 자체는 비대해져 가는데, 비즈니스 자체를 표현해야 하는 도메인에 query 가 침투한다?

 

이를 해결하기 위해 CQS 를 더 높은 수준에서 적용시킨 조회의 책임명령의 책임 을 분리하는 CQRS 를 탄생시킨 것이다.

 

아래의 그림은 다음과 같은 비즈니스를 포함하고 있다.

 

  • 사용자는 정답을 입력한다.
  • 정답이라면 점수를 올리고 오답이면 점수를 내린다.
  • 사용자의 랭킹 확인할 수 있다.

 

 

위의 구조는 동일한 도메인 모델을 사용한다. 즉, 조회의 책임과 명령의 책임이 하나의 도메인에 포함되어있다는 이야기다.

 

하지만 여기서 CQRS 를 적용해서 책임에 따른 Command 와 Query 를 분리시키면 다음과 같은 형태를 띄게 된다.

 

 

그렇다면 정답과 관련된 비즈니스를 책임지는 윗쪽 도메인에게는 상태를 변경시키는 Command 의 책임만 존재하기에 비즈니스를 그대로 표현할 수 있다.

 

역시 아래의 도메인에게는 상태를 확인하는 Query 의 책임 만 존재하게 된다.

 

이렇게 되면 어떤 장점이 있을 수 있을까?

 

단순히 가장 먼저 드는 생각은 Command 와 Query 에 각기 다른 Persistence Module 을 사용할 수 있을것이다.

 

  • Command Side 에는 객체 중심적인 개발이 가능한 JPA 를 사용할 수 있다.
  • Query Side 에는 최적화된 쿼리를 위해서 MyBatis 를 사용할 수 있을 것이다.

 

CQRS 더 고도화 시켜볼 수 있다

 

Command 와 Query 의 책임이 분리되었기 때문에 Command 와 Query 는 서로 다른 인프라가 구성될 수 있다.

 

위의 구조는 단일 데이터소스에 대한 CQRS 라면, 이제는 멀티 데이터소스에 대한 이야기다.

 

 

그럼 위와 같이 Polygrat 한 Persistance Infra 가 구성될 수 있다.

 

중간에 하나의 큐를 두고, (이는 애플리케이션일 수 있고) Command 에 따라 Query 용 DB 에 상태를 업데이트 시켜주는 무언가가 필요할 수 있다.

 

그럼 또 아래와 같이 구성할 수 있다.

 

  • Command infra 에는 write 에 최적화된 DB를 사용할 수 있을 것이다.
  • Query Side 에는 더욱 빠른 쿼링을 위해서 elasticsearchopensearch 와 같은 검색 엔진을 도입할 수 있을 것이다.

 

그래서 보통 Query Side 에 Materialized View 를 이용하여 복잡한 쿼리를 방지하고 관점에 따른 정보 뷰를 생성하여 사용하곤 한다

 

CQRS 의 장점과 단점

 

위에서 우리는 CQRS 에 대해서 대략적으로 알아보았고, level 별로 CQRS 를 구분해보았다.

 

이제 CQRS 에 대한 장단점을 한번 생각해보자

 

장점

  • 도메인 로직에만 집중할 수 있게 된다
    • Command 와 Query 를 분리했기 때문에 SRP 를 준수하는 도메인 모델을 만들 수 있다.
    • 이를 통해 결국 도메인 로직에 비즈니스 로직을 집중시킬 수 있다
  • 데이터소스의 독립적인 크기 조정이 가능하다
    • 보통 read 와 write 의 비율은 8 : 2 이다.
    • 그러므로 write db 가 물리적으로 나뉘어져 있다면 해당 db 인스턴스는 작게 유지하고 read db 인스턴스에 더 높은 투자를 할 수 있다.
  • 단순한 쿼리
    • Query side 에서는 Materialized View 를 이용할 수 있는데, 이를 통해서 복잡한 조인 쿼리 없이 단순한 쿼리를 이용해서 원하는 정보를 얻어올 수 있다

 

단점

  • 복잡성이 올라간다
    • command side 와 query side 를 명시적으로 분리하기 때문에 복잡성이 올라간다.
  • 즉시적인 일관성이 보장되지 않는다
    • command 에 따른 data 의 무결성이 잠시동안 깨질 수 있다.
    • 이 말은 데이터의 consistency 가 항상 동일하지 않다
    • 하지만 최종적으로는 데이터가 맞춰질 것이니 Eventual Consistency라고 할 수 있다.

 

이런 장단점을 가지고 있기 때문에 도메인 또는 비즈니스 규칙이 단순한 곳에서는 CQRS 를 하기 힘들다.

 

하지만 많은 사용자가 동시에 동일한 데이터에 병렬로 접근하는 경우나 read 연산이 write 연산보다 많은 경우는 CQRS 를 사용하는 것이 효과적이다.

 

마치며

 

위에서 보았듯 CQRS 자체는 되게 간단하지만 마이크로소프트 문서를 포함하여 여러 곳에서 CQRS 를 설명할 때, 오해에 여지가 있게 설명을 하고 있었다.

 

CQRS 는 사실 데이터소스와 크게 연관이 있지 않다.

 

말 그대로 CQRS 는 Command 와 Query 를 분리시키는 것이다.

 

하지만 여러 곳에서 CQRS 와 Datasource 를 엮어서 혹은 Event sourcing 과 엮어서 설명을 하고 있었기에 초반에 많은 오해를 했었다..

 

오해를 풀어서 신난 기념으로 사내에 공유

 

그래서 이 글로 최대한 CQRS 를 Datasource 와 엮여있다는 오해를 풀고싶었고, 나와 같이 오해를 했던 사람들에게 이 글을 바치고싶다

댓글0