
CQRS는 언제 과할까라는 질문부터 잡는 편이 안전합니다. 읽기와 쓰기를 나누는 구조는 꽤 정답처럼 보이지만, 결론부터 말하면 CQRS는 기본값이 아니라, 읽기와 쓰기의 비대칭이 정말 큰 시스템에서만 빛나는 선택지입니다. 많은 서비스에서는 오히려 코드와 운영 경로만 길어집니다.
이번 글에서는 CQRS가 무엇인지부터 설명하되, 더 중요한 질문인 지금 정말 읽기와 쓰기를 분리해야 하는가에 집중하겠습니다. 단순 개념 소개가 아니라, 언제 도움이 되고 언제 과설계가 되는지까지 실무 기준으로 정리합니다.

핵심 개념은 Martin Fowler의 CQRS 글과 Microsoft Learn CQRS 패턴 문서를 바탕으로 정리했습니다. 구조 비교 관점은 기존의 레이어드 아키텍처, 클린 아키텍처, 헥사고날 아키텍처 글과 연결해서 보면 더 이해가 쉽습니다.
CQRS는 언제 과할까를 이해하려면 먼저 CQRS를 한 문장으로 정리해야 한다
CQRS는 Command Query Responsibility Segregation의 줄임말입니다. 쉽게 말하면 데이터를 바꾸는 쪽과 데이터를 읽는 쪽에 같은 모델을 억지로 쓰지 말자는 발상입니다. 쓰기 쪽은 비즈니스 규칙, 검증, 상태 전이, 트랜잭션 경계를 중심으로 설계하고, 읽기 쪽은 화면에 필요한 형태로 빠르게 조회하는 데 집중하게 만듭니다.
여기서 중요한 점은 단순히 메서드 이름을 나누는 것이 아니라는 것입니다. 같은 주문 데이터라도 쓰기 쪽은 승인 가능 여부와 상태 전이를 엄격하게 다루고, 읽기 쪽은 관리자 화면에서 주문·고객·결제 상태를 한 줄로 보여주는 데 더 관심이 있습니다. 이 요구 차이가 커질수록 CQRS가 자연스러워집니다.

왜 같은 모델 하나로 처리하면 점점 어색해질까
작은 CRUD 서비스에서는 하나의 엔티티와 하나의 저장 모델로도 충분히 잘 돌아갑니다. 문제는 시스템이 커지면서 읽기와 쓰기의 요구가 서로 다른 방향으로 자라기 시작할 때입니다.
- 쓰기 쪽은 검증, 상태 전이, 동시성 제어가 중요하다
- 읽기 쪽은 join, 집계, 검색, 페이지네이션, 대시보드 응답 속도가 중요하다
- 같은 모델 하나로 둘 다 맞추려 하면 조회용 필드와 쓰기 규칙이 서로를 끌어당긴다
예를 들어 주문 승인 로직은 취소 상태, 결제 완료 여부, 재고 차감 같은 규칙이 중요합니다. 반면 관리자 목록 화면은 고객 이름, 최근 결제 실패 사유, 배송 상태, 환불 요청 여부를 한 번에 보여줘야 할 수 있습니다. 이 둘을 같은 모델 하나에 계속 우겨 넣으면 읽기 모델도 무거워지고 쓰기 모델도 흐려집니다.
CRUD와 CQRS의 차이는 command를 어떻게 보느냐에서 커진다
CRUD식 사고에서는 보통 필드를 만들고 읽고 수정하고 삭제하는 흐름이 중심이 됩니다. 하지만 CQRS에서는 command를 단순 필드 업데이트가 아니라 비즈니스 의도가 드러나는 작업 단위로 보는 편이 더 자연스럽습니다.
- ReservationStatus 변경보다 객실 예약 확정
- Order.status 업데이트보다 주문 승인
- User.level 변경보다 회원 등급 승급
이 감각은 단순 DTO 조작보다 비즈니스 작업을 더 또렷하게 보이게 합니다. 또한 Application Service와 Domain Service 차이에서 말한 흐름과 규칙의 분리와도 잘 이어집니다.
public class ApproveOrderCommand {
private final Long orderId;
private final Long approverId;
public ApproveOrderCommand(Long orderId, Long approverId) {
this.orderId = orderId;
this.approverId = approverId;
}
public Long orderId() { return orderId; }
public Long approverId() { return approverId; }
}
public class OrderCommandService {
private final OrderRepository orderRepository;
@Transactional
public void handle(ApproveOrderCommand command) {
Order order = orderRepository.findById(command.orderId())
.orElseThrow(() -> new IllegalArgumentException("주문이 없습니다."));
order.approveBy(command.approverId());
orderRepository.save(order);
}
}이 코드에서 핵심은 status 필드를 바꾸는 행위 자체가 아니라, 주문 승인이라는 비즈니스 작업이 독립된 문장으로 드러난다는 점입니다. CQRS는 이런 표현을 더 자연스럽게 만들 수 있습니다.
CQRS가 실제로 빛나는 순간
CQRS가 어울리는 대표 장면은 읽기와 쓰기의 비대칭이 정말 큰 경우입니다. 이때는 같은 모델 하나를 억지로 끌고 가는 편이 오히려 더 복잡해질 수 있습니다.

첫째, 조회 화면이 매우 복잡한데 쓰기 규칙도 두꺼운 경우입니다. 관리자 대시보드에서 주문, 고객, 환불, 배송, 쿠폰 사용 이력을 한 번에 보여줘야 한다면 read model은 집계와 검색에 최적화된 형태가 필요합니다. 반면 write 쪽은 승인, 취소, 환불 상태 전이가 더 중요합니다.
둘째, 읽기 부하와 쓰기 부하의 성격이 완전히 다른 경우입니다. 상품 조회는 매우 많지만 수정은 운영자가 가끔만 한다면 읽기 쪽은 캐시와 projection에, 쓰기 쪽은 검증과 일관성에 집중하는 편이 더 자연스럽습니다.
셋째, 복잡한 도메인 규칙이 write 쪽에 몰려 있는 경우입니다. 승인, 정산, 상태 전이, 중복 처리, 동시성 제어가 중요하고 읽기 쪽은 이미 계산된 결과를 보여주기만 하면 된다면 CQRS의 장점이 커집니다.
그런데 왜 CQRS가 자주 과해질까
많은 팀이 CQRS를 어려워하는 이유는 개념보다 비용 때문입니다. 분리로 얻는 이득보다 분리 때문에 생기는 운영 비용이 더 크면, CQRS는 좋은 설계가 아니라 부담이 됩니다.

- 읽기 모델과 쓰기 모델이 두 벌이 되면서 코드와 테스트 포인트가 늘어난다
- 별도 read store를 두면 동기화, 재처리, projection 복구 문제가 생긴다
- eventual consistency를 제품과 운영 팀이 함께 감당해야 한다
- 장애가 나면 command 처리, 이벤트 발행, 메시지 큐, read model 반영 경로까지 추적해야 한다
특히 사용자가 방금 주문을 취소했는데 목록 화면에는 몇 초 동안 이전 상태가 남아 보일 수 있다는 사실을 받아들일 수 있는지부터 생각해야 합니다. 어떤 시스템에는 괜찮지만, 어떤 시스템에는 절대 안 됩니다. 이 구분 없이 CQRS를 도입하면 구조는 멋있어 보여도 운영은 흔들릴 수 있습니다.
가벼운 CQRS와 무거운 CQRS는 완전히 다르다
실무에서 가장 많이 생기는 오해는 CQRS를 도입하면 무조건 별도 DB, 메시지 브로커, 이벤트 기반 read model까지 가야 한다고 생각하는 것입니다. 꼭 그럴 필요는 없습니다. 오히려 분리 강도를 단계적으로 보는 편이 현실적입니다.

가벼운 분리는 같은 DB를 공유하면서 write 쪽은 command service나 application service 중심으로, read 쪽은 query service와 DTO 중심으로 정리하는 수준입니다. 이 정도는 비교적 부담이 적습니다.
중간 단계 분리는 읽기 쿼리를 별도 모듈이나 서비스로 몰아주고, join과 projection을 조회 쪽에 집중시키는 방식입니다. 이때부터 구조적 이득이 생기지만 동기화와 캐시 일관성도 조금씩 고려해야 합니다.
무거운 분리는 write store와 read store를 따로 두고 이벤트나 메시지로 projection을 만드는 방식입니다. 이 단계는 이미 단순 패턴 적용이 아니라 운영 아키텍처 선택입니다. 여기까지 갈 이유가 명확하지 않다면, CQRS라는 이름보다 가벼운 분리만 유지하는 편이 더 낫습니다.
예시로 보면 더 분명하다: 잘 맞는 경우와 과한 경우
정산 시스템은 CQRS가 꽤 잘 맞는 대표 예시입니다. 쓰기 쪽에서는 정산 마감 상태, 중복 정산 방지, 승인과 취소 규칙이 중요합니다. 읽기 쪽에서는 월별 요약, 판매자별 합계, 보류 사유, 운영 검색 화면이 더 중요합니다. read model은 집계 중심으로 납작하게, write model은 규칙 중심으로 두는 편이 훨씬 읽기 쉽습니다.
반대로 사내 운영 툴의 공지사항 관리처럼 작성·수정·조회·삭제 정도가 대부분이고 읽기와 쓰기의 본질적 비대칭이 거의 없는 경우에는 CQRS가 과해지기 쉽습니다. 이때는 command, query, projection, read DTO를 다 만들수록 구조만 커지고 실익은 작아집니다.
public class OrderSummaryQueryService {
private final JdbcTemplate jdbcTemplate;
public List<OrderSummaryRow> findAdminList(String keyword) {
return jdbcTemplate.query(
"""
select o.id, o.status, c.name as customer_name,
p.last_failure_reason, d.delivery_status
from orders o
join customers c on c.id = o.customer_id
left join payment_summary p on p.order_id = o.id
left join delivery_summary d on d.order_id = o.id
where c.name like concat('%', ?, '%')
order by o.created_at desc
limit 50
""",
(rs, rowNum) -> new OrderSummaryRow(
rs.getLong("id"),
rs.getString("status"),
rs.getString("customer_name"),
rs.getString("last_failure_reason"),
rs.getString("delivery_status")
),
keyword
);
}
}이 조회 서비스는 도메인 상태 전이를 처리하지 않습니다. 오직 화면에 필요한 정보를 빠르게 꺼내오는 데 집중합니다. 이런 분리가 실제로 유용한지 판단하려면, 조회 요구가 이 정도로 분명하게 복잡한지부터 봐야 합니다.
체크리스트: 지금 CQRS를 고민할 이유가 있는가

- 읽기 화면이 쓰기 모델보다 훨씬 복잡한가
- 조회 부하와 쓰기 부하의 성격이 크게 다른가
- write 쪽의 비즈니스 규칙과 상태 전이가 두꺼운가
- projection이나 집계 모델이 반복해서 필요한가
- eventual consistency를 제품적으로 받아들일 수 있는가
- 메시지 유실, 재처리, projection 복구까지 운영 팀이 감당할 수 있는가
반대로 기본 CRUD가 대부분이고, 읽기와 쓰기 모델 차이가 크지 않으며, 팀 규모가 작고 운영 경로를 길게 만들고 싶지 않다면 CQRS는 잠시 미루는 편이 좋습니다. 그런 상황에서는 CRUD가 부족한 것이 아니라 그냥 충분한 경우가 많습니다.
클린 아키텍처, 헥사고날 아키텍처와는 어떤 관계일까
CQRS는 클린 아키텍처나 헥사고날 아키텍처의 상위 개념이 아닙니다. 반대로 둘을 반드시 써야 CQRS를 할 수 있는 것도 아닙니다. 관계를 실무적으로 풀면, 클린 아키텍처는 의존 방향과 경계에 더 관심이 많고, 헥사고날 아키텍처는 핵심 규칙을 바깥 기술에서 분리하는 데 더 집중하며, CQRS는 읽기와 쓰기의 관심사를 같은 모델에 억지로 묶지 말자는 선택에 더 가깝습니다.
즉 함께 쓸 수는 있지만 서로를 자동으로 요구하지는 않습니다. 클린 아키텍처 안에서도 단일 CRUD 모델로 충분할 수 있고, 헥사고날 아키텍처를 쓰더라도 read/write 분리를 아주 약하게만 가져갈 수 있습니다. 중요한 것은 이름 조합이 아니라, 지금 풀어야 할 문제의 종류입니다.
실무 권장 순서: 처음부터 무겁게 가지 말자
- 먼저 write 쪽 비즈니스 규칙과 상태 전이를 분명히 정리한다
- 읽기 화면이 정말 별도 형태를 요구하는지 확인한다
- 조회 전용 DTO와 query service부터 분리한다
- 그래도 읽기 성능과 구조가 계속 버거우면 projection을 검토한다
- 마지막으로 separate read store와 비동기 동기화를 도입한다
즉 CQRS는 on/off 스위치가 아니라 분리 강도를 조절할 수 있는 스펙트럼으로 보는 편이 안전합니다. 이름을 도입하기보다, 문제를 정확히 나누는 것부터 시작하는 편이 실제로 더 잘 작동합니다.
마무리
CQRS는 읽기와 쓰기를 무조건 갈라놓는 멋진 패턴이 아닙니다. 읽기와 쓰기의 요구가 실제로 다르게 자라고 있을 때, 같은 모델 하나로 모두 해결하려다 생기는 복잡도를 줄이기 위한 선택지입니다.
그래서 더 중요한 질문은 CQRS가 좋으냐가 아닙니다. 지금 내 시스템에서 읽기와 쓰기가 정말 다른 종류의 문제인가, 그리고 그 분리에서 생기는 운영 비용까지 감당할 가치가 있는가입니다. 이 질문에 아직 자신 있게 답하기 어렵다면, 레이어드 아키텍처, 클린 아키텍처, 헥사고날 아키텍처, Application Service와 Domain Service 같은 더 기본적인 경계 감각부터 단단히 잡는 편이 더 실용적입니다.