← Posts

Event-Driven · CQRS · Read Model

쓰는 모델과 읽는 모델을 가른다 - CQRS

쓸 때 좋은 데이터 모양과 읽을 때 좋은 모양은 다르다. 한 모델로 둘 다 하려면 한쪽이 고생한다. 명령과 조회를 아예 갈라 각자 최적의 모양을 갖게 하는 CQRS, 그 대가인 동기화 지연까지.

목차
  1. 쓸 때 좋은 모양과 읽을 때 좋은 모양이 다르다
  2. 한 모델로 둘 다 하면
  3. CQRS: 명령과 조회를 가른다
  4. 두 모델을 어떻게 맞추나
  5. 읽기 모델은 잠깐 뒤처진다
  6. CQRS는 이벤트 소싱이 아니다
  7. 언제 과한가
  8. 실무에서: 읽기 모델은 언제든 다시 만든다
  9. 정리

같은 데이터라도 쓸 때 편한 모양과 읽을 때 편한 모양이 다르다. 하나의 모델로 둘 다 감당하려다 양쪽이 다 어정쩡해지는 문제를, 아예 갈라서 푸는 게 CQRS다.

쓸 때 좋은 모양과 읽을 때 좋은 모양이 다르다

주문을 쓸 때는 작고 정확한 모양이 좋다. 주문·주문항목·상품·회원이 각자 제 테이블에 정규화돼 있어야 중복 없이 안전하게 바뀐다.

주문을 읽을 때, 특히 “내 주문 목록” 화면은 다르다. 주문번호·상품명·썸네일·배송상태·결제금액이 한 줄에 다 있으면 좋다. 그런데 이건 방금 그 정규화된 테이블 네다섯 개를 조인해야 나온다.

쓰기는 쪼개진 걸 원하고, 읽기는 합쳐진 걸 원한다. 방향이 반대다.

한 모델로 둘 다 하면

정규화된 한 모델로 읽기까지 감당하면, 목록 화면 한 번에 무거운 조인이 매번 돈다. 화면이 복잡할수록 조인이 늘고, 트래픽이 많을수록 그 비용이 쌓인다. 조회는 대개 쓰기보다 훨씬 잦아서, 이 비용이 시스템 전체를 짓누른다.

반대로 읽기에 맞춰 미리 합쳐 저장하면, 이번엔 쓰기가 괴롭다 - 상품명 하나 바뀌면 그게 박혀 있는 수많은 주문 줄을 다 고쳐야 한다. 한 모델로는 어느 쪽을 택해도 다른 쪽이 운다.

CQRS: 명령과 조회를 가른다

CQRS는 Command Query Responsibility Segregation - “명령(쓰기)과 조회(읽기)의 책임을 나눈다”는 말이다. 발상은 단순하다. 모델을 둘로 만든다.

diagramdiagram
  • 쓰기 모델 - 정규화된 채로 정확하게 바꾸는 데 최적화. 명령만 받는다.
  • 읽기 모델 - 화면에 바로 뿌릴 모양으로 미리 합쳐둠. 조회만 받는다.

쓰기는 작은 테이블을 안전하게 고치고, 읽기는 이미 합쳐진 줄을 조인 없이 그냥 꺼낸다. 각자 제일 좋은 모양을 가진다.

두 모델을 어떻게 맞추나

모델이 둘이면 문제가 생긴다 - 쓰기 모델이 바뀌면 읽기 모델도 따라 바뀌어야 한다. 여기서 이벤트가 등장한다.

diagramdiagram

쓰기 모델이 바뀔 때마다 “주문 생성됨” 같은 이벤트를 낸다. 그 이벤트를 듣는 갱신기가 읽기 모델을 다시 만든다 - 필요한 걸 미리 합쳐 한 줄로 박아둔다. 이 이벤트도 아웃박스로 안전하게 내고, 갱신기는 멱등하게 받는다. 앞 글들의 장치가 그대로 쓰인다.

읽기 모델은 잠깐 뒤처진다

여기에 대가가 있다. 쓰기가 일어나고 이벤트가 흘러 읽기 모델이 갱신될 때까지 아주 짧은 틈이 있다. 그동안 읽기 모델은 옛 값을 보여준다. 방금 주문했는데 목록에 아직 안 뜰 수 있다.

이게 바로 결과적 정합성이다. 쓰기와 읽기가 잠깐 어긋났다가 곧 맞는다. 그래서 CQRS를 쓰려면 “방금 쓴 게 즉시 안 보여도 되는가”를 먼저 물어야 한다 - 은행 잔액처럼 즉시 일치가 필수인 곳엔 안 맞고, 상품 목록·검색·대시보드처럼 잠깐의 지연을 견디는 곳에 맞는다.

CQRS는 이벤트 소싱이 아니다

가장 흔한 오해를 걷어내자. CQRS와 이벤트 소싱자주 같이 쓰이지만 별개다.

  • CQRS - 읽기 모델과 쓰기 모델을 나눈다. 쓰기 모델은 여전히 평범한 현재 상태(정규화된 테이블)여도 된다.
  • 이벤트 소싱 - 상태 대신 이벤트 로그를 진실로 저장한다(다음 글).

CQRS는 이벤트 소싱 없이도 완전히 성립한다. 쓰기 모델이 보통 DB이고, 그저 바뀔 때 이벤트를 내 읽기 모델을 갱신하면 그게 CQRS다. “CQRS 하려면 이벤트 소싱부터 해야 한다”는 오해가 사람을 필요 이상으로 겁준다.

언제 과한가

CQRS는 공짜가 아니다 - 모델이 둘이 되고, 동기화 장치가 붙고, 잠깐의 불일치를 감수해야 한다. 대부분의 화면은 이게 필요 없다.

  • 단순한 CRUD엔 안 쓴다 - 조회가 가벼운 조인 몇 개로 끝나면 한 모델로 충분하다. 모델을 둘로 만드는 게 순손해다.
  • 읽기와 쓰기의 요구가 크게 갈릴 때 꺼낸다 - 조회가 압도적으로 잦거나, 화면이 무거운 조인을 요구하거나, 읽기와 쓰기를 따로 확장해야 할 때.
  • 일부 화면에만 적용해도 된다 - 시스템 전체를 CQRS로 만들 필요 없다. 무거운 목록·검색 화면만 읽기 모델을 따로 두고, 나머지는 평범하게.

실무에서: 읽기 모델은 언제든 다시 만든다

  • 읽기 모델은 버려도 되는 것으로 본다 - 진실은 쓰기 모델에 있고, 읽기 모델은 그로부터 파생된 것이다. 망가지거나 모양을 바꾸고 싶으면, 이벤트를 다시 흘려 처음부터 새로 만들면 된다.
  • 화면마다 다른 읽기 모델 - 목록용, 검색용, 통계용을 각각 다른 모양으로 둘 수 있다. 하나의 쓰기에서 여러 읽기 모델이 갱신된다.
  • 지연을 사용자에게 숨긴다 - 방금 쓴 사용자에겐 화면에서 그 결과를 낙관적으로 미리 보여주고, 뒤에서 읽기 모델이 따라오게 하면 지연이 안 느껴진다.

정리

  • 쓸 때 좋은 모양(정규화·정확)과 읽을 때 좋은 모양(합쳐둠·빠름)은 반대라, 한 모델로는 한쪽이 운다.
  • CQRS는 쓰기 모델과 읽기 모델을 아예 갈라 각자 최적의 모양을 갖게 하고, 이벤트로 둘을 맞춘다.
  • 대가는 잠깐의 불일치(결과적 정합성) - 즉시 일치가 필수인 곳엔 안 맞는다.
  • 이벤트 소싱과는 별개이고, 대부분의 CRUD엔 과하다 - 읽기·쓰기 요구가 크게 갈리는 곳에만 꺼낸다.

다음 글은 자주 짝지어 오는 그 개념 - 상태 대신 일어난 일을 진실로 저장하는 이벤트 소싱이다.