핵심 요약
- 이번 실습의 목표는 주문/결제/주문내역 도메인을 “대규모 트래픽을 견디는 구조”로 설계하는 감각을 익히는 것입니다.
- CQRS의 핵심은 쓰기(Command)와 읽기(Query)를 분리하고, 각각을 최적화하는 것입니다. (쓰기: MySQL, 읽기: Redis)
- 서비스 간 연결은 이벤트 기반(예: Kafka)으로 비동기 파이프라인을 만들며, 로컬에서는 Docker로 비용 없이 실습합니다.
- 1편에서는 “완벽한 구현”보다 스코프를 줄인 최소 단위로 시작합니다. (1단계: 주문 생성/조회 중심)
목차
- 0) 이 시리즈는 무엇을 만들까?
- 1) 목표와 기본 가정
- 2) 서비스 분리: 경계(Responsibility)부터 정하자
- 3) 멀티모듈 구조 설계: “한 레포, 여러 Spring Boot 앱”
- 4) 1편 체크포인트
- 다음 편 예고
이 시리즈는 “CQRS를 책으로만 이해하는 수준”에서 멈추지 않고, 손으로 직접 주문/결제/주문내역 시스템을 쪼개고 붙이며 감각을 익히는 실습형 강의입니다. 중요한 전제는 하나예요. 우리는 처음부터 모든 걸 완벽하게 만들지 않습니다. 대신 서비스 분리 → 로컬 실행 기반 → 데이터 흐름(이벤트/프로젝션) → 운영 이슈 순서로 난이도를 올립니다.
1편의 역할
- “CQRS를 붙일 수 있는 구조”를 먼저 만든다
- 서비스 경계를 코드/폴더/실행 단위로 고정한다
- 다음 편에서 Docker + 3개 앱 동시 실행으로 토대를 검증한다
1) 목표와 기본 가정
1-1. 실습 목표: “대규모 트래픽을 견디는 구조”를 경험한다
이번 프로젝트로 얻어가야 하는 핵심은 기능 그 자체가 아니라, “왜 이렇게 나누는지”를 설명할 수 있는 구조적 이해입니다. 주문 도메인은 시간이 지날수록 조회(주문내역/주문상세) 트래픽이 폭발하기 쉬워서, CQRS를 연습하기에 가장 현실적인 주제입니다.
- 서비스 경계를 먼저 잡습니다. “주문 생성/변경”, “결제 처리”, “주문내역 조회”가 한 덩어리로 뭉치지 않도록 분리합니다.
- CQRS의 전형적인 분리인 쓰기(MySQL) / 읽기(Redis)를 “이론”이 아니라 “손의 감각”으로 익힙니다. 여기서 핵심은 Redis가 빠르다는 사실이 아니라, 읽기 모델을 읽기에 맞게 따로 설계한다는 관점입니다.
- 이벤트 기반으로 시스템을 연결하는 사고방식을 연습합니다. 다음 편부터 Kafka를 붙이면서, “왜 동기 호출을 줄이려 하는지”가 체감될 겁니다.
- 실무에서 흔한 형태인 한 레포 안에서 여러 Spring Boot 앱을 운영하는 방식을 멀티모듈로 훈련합니다.
1-2. 기본 가정: “이 정도는 인정하고” 시작하자
CQRS는 강력한 대신 복잡도가 올라갑니다. 따라서 무엇을 감수하고 무엇을 얻는지부터 합의해야 합니다. 이 실습은 아래 가정 위에서 진행합니다.
- 비용 0원: 로컬 Docker 컨테이너로 인프라(MySQL/Redis, 이후 Kafka)를 띄웁니다. 유료 매니지드 서비스에 의존하지 않습니다.
- 개발 환경: IntelliJ + Gradle 멀티모듈로 진행합니다. “하나의 프로젝트로 여러 서비스를 동시에 운영”하는 형태를 목표로 합니다.
- 최종적 일관성(Eventual Consistency) 허용: 주문을 생성한 직후 주문내역 화면 반영이 0.5초 ~ 몇 초 늦을 수 있다고 가정합니다. 대신 읽기 시스템을 강하게 확장하는 구조를 얻습니다.
- 스코프 관리(가장 중요): 첫 단계에서 “재고/배송/복잡한 결제 상태 머신”까지 완성하려 하지 않습니다. 1단계는 주문 생성/조회에 집중하고, 결제는 껍데기부터 점진적으로 채웁니다.
여기서 핵심은 “최종적 일관성”을 두려워하지 않는 것입니다. 실무에서도 주문 생성 직후 주문내역이 약간 늦게 보이는 경험은 흔합니다. 중요한 건 그 지연을 숨기기보다 UX/운영 정책으로 흡수하고, 대신 트래픽 급증에도 안정적으로 버티는 구조를 만드는 것입니다.
2) 서비스 분리: 경계(Responsibility)부터 정하자
“서비스 분리”는 폴더를 나누는 작업이 아닙니다. 무엇을 누가 책임질지(경계)를 먼저 정하고, 그 다음에 코드/모듈/배포 단위로 고정해야 합니다. 이번 실습의 기본 서비스는 아래 3개입니다.
2-1. order-command (쓰기 전용)
- 역할: 주문 생성/취소처럼 “주문 상태를 바꾸는 요청”을 처리합니다.
- 데이터 기준점: 정합성의 기준은 MySQL 트랜잭션(쓰기 모델)입니다.
- 예시 API:
POST /commands/orders
2-2. payment-command (쓰기 전용)
- 역할: 결제 승인/실패/환불 등 결제 상태 변경 요청을 처리합니다.
- 실습 전략: 초반에는 Mock(가짜 결제)로 시작해도 충분합니다. 핵심은 결제를 “주문과 분리된 책임”으로 고정하는 것입니다.
- 예시 API:
POST /commands/payments
2-3. order-query (읽기 전용)
- 역할: 주문내역/주문상세 같은 조회 요청만 처리합니다.
- 원칙: 최종 형태에서는 Redis 읽기 모델을 주로 조회하도록 설계합니다.
- 예시 API:
GET /queries/users/{userId}/orders,GET /queries/orders/{orderId}
“왜 결제 서비스까지 지금부터 분리하나?”라는 질문이 자연스럽게 나옵니다. 답은 간단합니다. 1편에서 결제를 완성하려는 게 아니라, 서비스 경계를 먼저 확정해야 이후 단계(이벤트/읽기 모델)를 얹을 때 흔들리지 않습니다. 경계가 흐리면 CQRS는 금방 “한 서비스가 모든 걸 하면서 DB만 두 개 쓰는” 이상한 형태가 되기 쉽습니다.
3) 멀티모듈 구조 설계: “한 레포, 여러 Spring Boot 앱”
이제 경계를 “코드 구조”로 고정해봅시다. 멀티모듈의 장점은 서비스 경계가 폴더 구조로 강제된다는 점입니다. 아래는 최소 형태의 추천 구조입니다.
cqrs/
infra/
docker-compose.yml
libs/
event-contracts/ (선택) 이벤트 DTO/스키마 같은 “계약”만 공유
services/
order-command/ (Spring Boot App)
payment-command/ (Spring Boot App)
order-query/ (Spring Boot App)
settings.gradle
build.gradle
gradle.properties
여기서 중요한 운영 감각이 하나 있습니다. 공통 라이브러리를 너무 많이 만들기 시작하면 서비스 분리의 의미가 금방 흐려집니다. 그래서 공유는 최소화합니다. event-contracts처럼 “경계가 명확한 계약”만 공유하고, 그 외 비즈니스 로직은 서비스 안에 두는 방식이 장기적으로 안전합니다.
3-1. 포트 분리 원칙(운영 감각 미리 심기)
멀티모듈로 서비스를 나누면, 실행 단위도 분리됩니다. 1편에서는 “실행은 다음 편에서 검증”하지만, 포트 분리 원칙은 미리 정해두는 게 좋습니다. 예를 들어 아래처럼 역할별로 포트를 고정해두면, 호출 흐름이 명확해집니다.
- order-command: 8081
- payment-command: 8082
- order-query: 8083
4) 1편 체크포인트
1편의 도착점은 “기능 구현”이 아니라 “구조의 고정”입니다. 아래 항목이 만족되면, 다음 편에서 Docker와 실행 검증으로 자연스럽게 넘어갈 수 있습니다.
- 주문/결제/조회가 서로 다른 Spring Boot 앱으로 분리될 준비가 되어 있다
- 멀티모듈 폴더 구조가 잡혀 있고, “공유는 최소화” 원칙이 적용되어 있다
- 포트 분리 원칙이 합의되어 있다(8081/8082/8083)
다음 편 예고
2편에서는 1편에서 만든 구조를 실제로 로컬에서 띄워 봅니다. Docker로 MySQL/Redis를 올리고, 3개 서비스를 동시에 실행한 뒤, curl로 “연결이 되는 상태”를 확인합니다. 이 토대가 있어야 3편부터 MySQL 저장, 이벤트(Kafka), Redis 읽기 모델(Projection)을 안정적으로 붙일 수 있습니다.