-
소프트웨어 아키텍처 시리즈 10화 – CQRS와 이벤트 소싱: 복잡성을 다루는 아키텍처 전략기술과 산업/아키텍처 2025. 6. 9. 19:22728x90
CQRS와 이벤트 소싱은 복잡한 도메인 로직과 데이터 변경 이력을 효과적으로 다루기 위한 구조입니다. 이 글에서는 두 패턴의 개념, 설계 이유, 적용 시 주의점까지 실무 중심으로 풀어봅니다.
CQRS란 무엇인가?
CQRS는 Command와 Query를 분리하자는 아키텍처 패턴입니다.
- Command: 데이터를 변경하는 요청 (예: 주문 생성, 비밀번호 변경)
- Query: 데이터를 조회하는 요청 (예: 주문 목록 보기, 사용자 상태 확인)
전통적인 CRUD 시스템에서는 동일한 모델(예: OrderService, OrderRepository)이 읽기와 쓰기를 모두 처리합니다.
하지만 CQRS는 이 책임을 분리하여 더 명확하고 독립적으로 설계하자는 철학을 가집니다.
왜 분리하는가? CQRS의 목적
- 복잡한 쓰기 로직을 단순화
- Command 로직은 종종 검증, 권한, 비즈니스 규칙 등이 복잡함
- Query는 성능 중심 최적화가 필요함
- 성능 최적화 및 스케일링 전략 분리 가능
- Query는 Read Replica로, Command는 트랜잭션 위주로 분리 운영 가능
- 모델이 용도에 따라 다르게 최적화됨
- Query는 Projection 기반의 ViewModel 사용
- Command는 도메인 모델 중심
실전 구조
[ UI ] ┣━━ [ Command Service ] ━━━▶ [ 도메인 모델 + 이벤트 생성 ] ┃ ┗━━ [ Query Service ] ━━━▶ [ 별도 Read Model(DB/View 등) ]
Command 처리는 도메인 로직에 집중하고,
Query는 Projection된 결과만 제공하는 방식으로 책임이 분리됩니다.
이벤트 소싱(Event Sourcing)이란?
전통적인 방식은 **현재 상태(state)**를 DB에 저장합니다. 예를 들어, 주문 상태가 "완료"라면, 테이블에 그 상태만 남아 있게 됩니다.
이벤트 소싱은 그 반대입니다.
모든 상태 변화의 이벤트를 순차적으로 저장하고,
현재 상태는 이 이벤트들을 재생(replay)해서 만들어냅니다.예:
1. 주문 생성됨(OrderPlaced) 2. 결제 완료됨(PaymentCompleted) 3. 배송 시작됨(DeliveryStarted) → 현재 상태: 배송 중
즉, 상태는 **이력(history)**의 결과일 뿐이며,
데이터의 진실(truth)은 이벤트 로그에 있다는 발상입니다.
CQRS + Event Sourcing은 어떤 시너지를 내는가?
- CQRS는 읽기와 쓰기를 분리해 책임을 나누고
- Event Sourcing은 쓰기의 저장 방식을 이벤트로 전환
따라서 다음과 같은 구조가 만들어집니다:
- Command 처리 시 도메인 모델이 이벤트를 생성
- 이 이벤트는 Event Store에 저장됨
- 별도의 Subscriber가 이 이벤트를 구독하여 Read Model을 업데이트
이 구조는 헥사고날 아키텍처 및 DDD 기반의 고급 설계에 자주 등장하며,
특히 다음 상황에 매우 유용합니다:- 상태 이력 관리가 필수인 금융/보험 시스템
- 주문, 배송 등 상태가 복잡하게 변화하는 이커머스
- 실시간 알림, 스트리밍 기반 비즈니스
적용 예시 – 간단한 주문 시스템
도메인 이벤트 정의
public class OrderPlaced { private final UUID orderId; private final List<OrderLine> items; private final LocalDateTime placedAt; }
Command 처리 흐름
public class PlaceOrderHandler { public void handle(PlaceOrderCommand command) { Order order = Order.create(command); eventStore.append(new OrderPlaced(order.getId(), ...)); } }
Read Model 갱신 흐름
public class OrderProjection { @EventHandler public void on(OrderPlaced event) { orderReadRepository.save(new OrderSummary(event.orderId, ...)); } }
장점 요약
- 완전한 변경 이력 확보 (컴플라이언스, 감사 추적에 유리)
- 읽기 성능 최적화 가능 (읽기 모델을 별도로 구성)
- 비동기 이벤트 중심 확장 용이 (Microservices, Kafka 연계 등)
현실적인 한계와 주의점
- 복잡도 증가
- 시스템 구조가 단순 CRUD보다 훨씬 복잡함
- 인프라(이벤트 저장소, 메시지 브로커 등)가 필수화됨
- 개발 생산성 초기 저하
- 설계/테스트/디버깅 모두 난이도 상승
- 이벤트 재생, Rollback 시나리오 고려 필요
- 이벤트 변경이 곧 데이터 스키마 변경
- 이벤트 버저닝(Event Versioning) 전략 필요
헥사고날 아키텍처와의 통합
- Command와 Query는 각각 별도의 Inbound Port로 정의
- Event Store는 Outbound Port를 통해 구현
- Projection과 Subscriber는 어댑터로 구성되며, 비동기 이벤트 기반 처리에 적합
즉, 포트를 통해 명확히 구분된 책임,
어댑터를 통해 외부 시스템과 연결,
도메인을 통해 비즈니스 로직 수행이라는 구조적 정합성을 유지할 수 있습니다.
마무리하며 – 단순히 멋진 구조가 아니라, 필요할 때만
CQRS와 Event Sourcing은 시스템을 더 유연하게, 더 강력하게 만들 수 있는 도구입니다.
하지만 그것은 비용을 수반하는 선택이며, 언제나 필요한 것은 아닙니다.다음 질문에 YES가 많다면 고려해볼 만합니다:
- 복잡한 상태 변화가 많고, 상태 흐름 추적이 중요하다
- 읽기와 쓰기의 성격이 매우 다르다 (예: 쓰기는 드물고 읽기는 잦다)
- 이벤트 기반 아키텍처를 염두에 두고 있다
그렇지 않다면, 오히려 단순한 CRUD 설계가 훨씬 효율적일 수 있습니다.
복잡성을 다루기 위한 구조가 오히려 복잡성을 만들지 않도록, 항상 ‘왜 필요한가’를 먼저 물어야 합니다.728x90'기술과 산업 > 아키텍처' 카테고리의 다른 글
소프트웨어 아키텍처 시리즈 12화 – 이벤트 소싱의 개념과 도입 시 고려사항 (2) 2025.06.27 소프트웨어 아키텍처 시리즈 11화 – 유즈케이스 중심 서비스 설계와 포트 단위 테스트 전략 (0) 2025.06.22 소프트웨어 아키텍처 시리즈 9화 – 헥사고날 아키텍처와 DDD의 통합 전략: 아키텍처와 도메인이 만나는 지점 (1) 2025.06.05 소프트웨어 아키텍처 시리즈 8화 – 헥사고날 아키텍처의 입출력 포트와 어댑터 설계 전략 (1) 2025.06.05 소프트웨어 아키텍처 시리즈 7화 – 헥사고날 아키텍처란 무엇인가? 진짜 중요한 건 방향이다 (0) 2025.06.03