CQRS는 Command Query Responsibility Segregation의 약자로,
“명령(Command)”과 “조회(Query)”의 책임을 분리하는 아키텍처 패턴을 말한다.
일반적인 시스템에서는 하나의 서비스나 레포지토리가
save, update, delete)과find, getList)그러나 CQRS는 이 두 가지를 분리하는 것이다.
| 구분 | Command | Query |
|---|---|---|
| 목적 | 데이터 변경 | 데이터 조회 |
| 예시 | 회원가입, 주문 생성, 결제 취소 | 회원 상세 조회, 주문 목록 조회 |
| 트랜잭션 | 강한 일관성 (ACID) | 약한 일관성 (Eventually Consistent) |
| 데이터베이스 | 보통 RDB (쓰기 최적화) | 조회 전용 DB (읽기 최적화, 캐시/NoSQL 등) |
| 설계 패턴 | DDD의 CommandHandler | DDD의 QueryService 또는 ReadModel |
📦 com.example.order
┣ 📂 command
┃ ┣ 📂 application
┃ ┃ ┗ 📜 OrderCommandService.java
┃ ┣ 📂 domain
┃ ┃ ┗ 📜 Order.java
┃ ┣ 📂 infrastructure
┃ ┃ ┗ 📜 OrderCommandRepository.java
┗ 📂 query
┣ 📂 application
┃ ┗ 📜 OrderQueryService.java
┣ 📂 dto
┃ ┗ 📜 OrderDetailResponse.java
┣ 📂 infrastructure
┃ ┗ 📜 OrderQueryRepository.java
OrderCommandService: 주문 생성/취소/수정 등 "쓰기" 담당OrderQueryService: 주문 목록, 주문 상세 조회 등 "읽기" 담당Order 엔티티를 직접 공유하지 않을 수도 있다.| 항목 | CQRS 방식 | 전통적 방식 |
|---|---|---|
| 읽기/쓰기 로직 | 분리됨 | 통합됨 |
| 모델 구조 | Command Model / Query Model | 단일 Model |
| 성능 | 고성능 확장 가능 | 전체 로직이 얽힘 |
| 일관성 | 최종 일관성 (Eventual Consistency) | 즉시 일관성 |
| 복잡도 | 높음 | 낮음 |
| 장점 | 설명 |
|---|---|
| 확장성 향상 | 읽기/쓰기를 각각 다른 서버로 확장 가능 (읽기 트래픽이 많을 때 유리) |
| 성능 최적화 | Query 쿼리를 캐시, Redis, Elasticsearch 등으로 별도 최적화 가능 |
| 명확한 책임 분리 | 코드 구조가 명확해지고, 도메인 로직이 깔끔해짐 |
| DDD와 궁합 좋음 | CommandHandler, Aggregate Root, Event 등을 명확히 구분 가능 |
| 단점 | 설명 |
|---|---|
| 구조 복잡도 증가 | Command/Query 계층을 따로 관리해야 함 |
| 데이터 일관성 관리 | 쓰기 후 읽기 모델로 복제 시 지연 발생 (Eventual Consistency) |
| 학습 곡선 | 단순 CRUD 애플리케이션엔 과한 설계일 수 있음 |
대규모 트래픽 서비스
DDD + 이벤트 기반 시스템
// Command Service (쓰기)
@Service
@RequiredArgsConstructor
public class OrderCommandService {
private final OrderRepository orderRepository;
public UUID createOrder(CreateOrderRequest request) {
Order order = new Order(request.getUserId(), request.getItems());
return orderRepository.save(order).getId();
}
}
// Query Service (읽기)
@Service
@RequiredArgsConstructor
public class OrderQueryService {
private final OrderReadRepository orderReadRepository;
@Transactional(readOnly = true)
public OrderDetailResponse getOrder(UUID orderId) {
return orderReadRepository.findOrderDetail(orderId);
}
}