Skip to content
사고 기록
뒤로

블로그 이전과 CQRS 특강 정리

Table of contents

Open Table of contents

블로그 이전

구현과 배포

Astro 기반 개인 블로그를 세팅하고 배포했다. 티스토리 블로그를 사용했었는데 글 게시까지 과정에서 너무 피곤함이 느껴져 블로그 이전을 결심하게 됐다.

찾아보니 astro + github + cloudflare 조합이 간단하고 퀄리티도 좋아보였다. 그리고 도메인도 cloudflare에서 구매해둔 것이 있어 딱이라고 생각했다. 예상보다 쉽게 배포까지 설정이 가능했는데 cloudflare에 등록만 하면 정말 간단하게 배포가 진행된다.

블로그 템플릿은 공유되고 있는 템플릿 중 마음에 드는 것을 골라서 내 입맛에 맞게 수정했다. 원하는대로 결과가 잘 나온 것 같아 마음에 든다. 익숙하지 않은 프론트의 영역이라 AI를 통해서 수정을 진행했는데 큰 문제없이 수월하게 마무리 했다.

완성하고 나니 문득 개발자의 미래에 대한 생각이 잠깐 스쳤다. 밝은 미래일지 어두운 미래일지 상상이 잘 안된다.

특강 정리

캐시(Redis)만으로 못 푸는 조회 성능 문제가 있을 때, 아키텍처를 바꿔 조회를 개선하는 방법 중 하나가 CQRS다.

단일 모델의 한계

user 테이블과 User 클래스 하나로 CRUD를 처리하는 구조는 트래픽이 적을 때는 잘 동작한다. 하지만 데이터가 백만, 천만 단위로 커지면 읽기와 쓰기가 같은 모델, 테이블을 두고 경쟁하면서 병목이 생기기 시작한다.

원인내용
읽기/쓰기 요구 불일치이커머스: 주문 쓰기 후 주문 내역, 배송, 결제 조회가 훨씬 많다.
(일 조회 약 5천만 건 → 약 580 TPS)
조인, 집계 증가주문+상품+회원+배송 조회가 늘면 Disk I/O로 응답이 5~10배 느려질 수 있음
자원 경합 (PostgreSQL)쓰기 증가 → dead tuple → autovacuum이 I/O 점유, 잠금 경합
스키마 변경 비용예: points_used 컬럼 추가 → 수억 row backfill 시 락, 성능 저하

MVCC, 락 튜닝, READ 레플리카 등은 같은 모델 안에서의 한계 완화이지 읽기/쓰기 모델 분리가 아니다. 구조적 한계에서 CQRS를 생각해볼 수 있다.

CQRS란

  • Greg Young(2010 제안) → Martin Fowler(2011 블로그 정리)

데이터 변경(Command)과 조회(Query)의 책임, 흐름, 모델을 분리해 각 목적에 맞게 최적화하는 패턴

CommandQuery
의도시스템 상태 변경정보 제공
부수 효과있음없어야 함
책임도메인 규칙 검증응답 형태, 성능 최적화
CRUDC/U/DR
예시주문 생성/취소, 회원 수정, 판매 상태 변경, 구독 해지주문 목록/상세, 검색, 상태 확인

분리 효과

  • 쓰기는 정확성, 읽기는 성능, 응답 형태
  • 읽기 변경이 쓰기 로직에 미치는 영향 감소
  • 각각 독립 변경, 최적화

적용 4단계

  • 단계를 올릴수록 원복 비용이 커진다. 1, 2, 3 순 점진적으로 도입

1단계. 모델만 분리

  • 같은 RDBMS, 같은 트랜잭션
  • OrderCommandService / OrderQueryService 분리
  • 강한 일관성, 운영 부담 상대적 적음
  • 도메인 로직 풍부, 트래픽 아직 크지 않은 사내/초기 시스템에 적합
  • DB 하나로도 가능

2단계. Primary / Replica로 부하 분리

  • 데이터 모델은 하나, DB를 Primary(쓰기)와 Replica(읽기)로 나눔
  • PostgreSQL 스트리밍 리플리케이션, Aurora/RDS 관리형 리드 레플리카 등
  • 조회 성능, 확장성 증가
  • 복제 지연으로 읽기 일관성 약화 가능

3단계. 저장소 분리 + 메시지 동기화

  • 쓰기 → PostgreSQL, 읽기 → MongoDB / Redis / Elasticsearch
  • Kafka, RabbitMQ 등으로 쓰기 → 읽기 동기화
  • 저장소별 최적화 가능 / 결과적 일관성, 구현, 운영 난이도 크게 상승
  • 실무는 보통 1, 2, 3 순으로 진행. 처음부터 3단계는 드묾
    • 이커머스, 소셜 피드, 대규모 서비스에서 3단계 이상 흔함

4단계. Event Sourcing 결합

  • 쓰기 쪽을 이벤트 스토어로 사용
    • 현재 상태가 아니라 상태 변경 시퀀스(이벤트) 저장
  • 학습 곡선, 운영 비용 높음. 1~3을 거친 뒤 검토
  • CQRS와 자주 같이 쓰이지만 동일 패턴은 아님

읽기 모델에서 NoSQL을 고려하는 이유

유형특징대표CQRS 읽기 관련
DocumentJSON 문서, 화면 단위 비정규화 저장, 조인 없이 조회MongoDB, Couchbase, Elasticsearch, OpenSearch상품/게시글/SNS 피드 등 가장 많이 사용. MongoDB, ES를 우선 알아둘 것
Key-ValueKey-Value, 인메모리 등 초저지연Redis캐시, 세션, 단순 키 조회
Wide Column로우에 많은 컬럼, 시계열/로그 분석Cassandra특강 범위에서 실사용 빈도 낮음
Graph관계 중심Neo4j소셜 그래프 등

읽기에 NoSQL을 두는 세 가지 이유

  1. 스키마 유연성: RDB 컬럼 추가+백필 부담 대신, 코드로 새 필드 채우기 시작 가능
  2. 수평 확장: 읽기 트래픽이 클 때 다중 노드 분산이 자연스러움
  3. 비정규화: Document에 한 화면 JSON을 담아 조인 제거

쓰기는 정규화(정합성, RDBMS), 읽기는 비정규화(조회 최적화, NoSQL)

결과적 일관성

쓰기 DB와 읽기 DB는 한 트랜잭션에서 동시에 갱신되지 않는다.

이벤트로 순차 동기화 → 쓰기는 됐는데 읽기에는 아직 안 보이는 구간이 생김 → 시간이 지나면 같아짐 = 결과적 일관성(eventual consistency)

  • NoSQL만의 특성이라기보다, 읽기/쓰기 DB를 나눠 이벤트로 동기화할 때 이해해야 하는 특성
  • 폴링 주기 단축, CDC 등 동기화 개선, 또는 “처리 중입니다” 같은 안내 메시지를 통해 처리

flow

  1. 사용자 쓰기 요청 → Write DB(RDBMS) 저장
  2. 변경 사실을 이벤트로 발행 → Kafka / RabbitMQ
  3. Projector(컨슈머) 가 이벤트를 받아 Read DB(Mongo, ES, Redis 등) 갱신

Write DB가 먼저 바뀌고, 이벤트 전달 후 Read DB가 바뀐다. 그 시간 차가 일관성 갭이다.

쓰기 측 책임

정합성 영역(RDBMS): 도메인 규칙 검증, ACID 트랜잭션, 도메인 이벤트 신뢰성 있는 발행

위험한 코드: 같은 흐름에서 orderRepo.save() 직후 트랜잭션 커밋 전후로 Kafka 직접 발행

시나리오결과
DB 성공, Kafka 실패주문은 됐는데 읽기 측이 영구히 모름
Kafka 성공, 비즈니스 롤백없는 주문이 이벤트로 흐름
트랜잭션 안 외부 호출트랜잭션 시간, 락 증가

Outbox 패턴

  1. 비즈니스 데이터와 발행할 이벤트를 Outbox 테이블에 같은 트랜잭션으로 저장
  2. 별도 워커가 Outbox를 읽어 Kafka 발행, 성공 후 상태 갱신

핵심은 브로커에 직접 보내지 말고 먼저 DB에 넣고 나중에 전송하는 것.

동기화(Outbox → Kafka) 흐름

방식내용장단
폴링 + 발행@Scheduled 등으로 Outbox 주기 조회구현 단순 / 주기만큼 지연
CDC (Change Data Capture)트랜잭션 로그 감지지연 짧음 / 인프라, 운영 복잡도 상승
  • 발행 성공 후 상태 갱신 전 워커 장애 시 재전송 가능

읽기 측 책임

도메인 규칙/정규화보다 화면을 빠르게 구성(조회 최적화)

  • 구체화된 뷰(Materialized View): 조회 시점에 계산하지 않고, 미리 가공한 사본을 저장. 사용자는 원본이 아니라 이 뷰를 조회
  • CQRS 읽기 모델은 쓰기와 다른 DB(NoSQL) 를 써도 됨

Event Sourcing과 CQRS

CQRSEvent Sourcing
무엇Command / Query 책임 분리상태를 저장하는 방식을 바꿈
원본(단계에 따라) 쓰기 DB 등이벤트 스트림이 원본
현재 상태쓰기 모델 등이벤트를 누적 재생한 결과(사본)

장바구니에 현재 수량만 있으면 사용자 분석이 어렵다. 이벤트(생성, 추가, 제거, 수량 변경 등)를 쌓으면 구매 유도, 쿠폰, 분석 등이 가능하다.

  • 재수화(rehydration): 이벤트를 순서대로 재생해 현재 상태 재구성
  • 정합성 불일치 시 이벤트를 원본으로 보고 상태를 다시 만듦

장점

  • 시점별 상태 재구성(특정 일자 잔액 등), 감사 로그, 운영 사고 디버깅

단점

  • 과거 이벤트와 스키마 비일치 시 호환성 처리 비용 부담
    • 스냅샷 전략 처리 비용 부담
  • 이벤트 스토어 DB/라이브러리 의존

CQRS와 잘 맞는 이유 이벤트 스토어를 화면용으로 직접 조회하기 부담 → Read DB(NoSQL)에 여러 읽기 모델 사용 (장바구니 화면 뷰, 상품 분석 뷰, 쿠폰 뷰, 운영 통계 뷰 등)