ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA 기반 스프링 애플리케이션에서의 CQRS 패턴 적용
    Spring 2025. 7. 14. 00:13

     

    CQRS란?

    CQRS(Command Query Responsibility Segregation)는 명령(Command)과 조회(Query)의 책임을 분리하는 설계 패턴입니다. 이 개념은 단순한 객체지향 원칙을 넘어서 어플리케이션 아키텍처 수준에서의 분리를 의미합니다.

     

     

     

    CQRS가 필요한 이유는?

    제가 개발한 JPA 기반의 서비스 클래스는 다음과 같은 작업을 모두 처리합니다.

    @Service
    @Transactional
    public class OrderService {
        public Order createOrder(...) { ... }
        public void cancelOrder(...) { ... }
        public OrderDto getOrderDetail(Long orderId) { ... }
    }

     

    이 구조는 다음과 같은 문제점들이 있습니다.

    • 비즈니스 로직화면 로직이 한 서비스 클래스 안에 있음
    • 조회 성능을 위한 최적화 코드(Fetch Join, 페이징 등)가 서비스 코드 내부에 있음
    • 전체적인 코드 응집도와 가독성이 좋지 않음

     

     


     

     

     

    CQRS 패턴 적용

    CQRS 패턴은 서비스 계층을 핵심 비즈니스 로직화면 출력 로직으로 나누는 것으로 시작합니다.

     

    1. 비즈니스 로직

    시스템이 '무엇을 해야 하는가'를 정의합니다. 따라서 데이터 상태를 변경합니다.

    ex) 주문 생성, 주문 취소, 재고 감소 등

     

    → 시스템의 상태를 바꾸는 행위입니다.

     

     

    OrderService (Command Service)

    비즈니스 규칙을 따라는 명령 작업만 담당합니다. (트랜잭션 사용)

    @Service
    @Transactional
    public class OrderService {
    
        public Order createOrder(Member member, List<OrderItem> items) {
            Order order = Order.createOrder(member, items);
            orderRepository.save(order);
            return order;
        }
    
        public void cancelOrder(Long orderId) {
            Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new IllegalArgumentException("주문을 찾을 수 없습니다."));
            order.cancel(); 
        }
    }
    • 비즈니스 로직 역할
    • @Transactional 
    • Entity또는 void 반환
    • 내부 로직 중심
    • 내부 비즈니스 로직에 민감

     

     

    2. 화면 출력 로직

    사용자가 보고자 하는 데이터를 '어떻게 보여줄지'를 정의합니다. 따라서 상태를 변경하지 않고, 필요한 데이터를 조회만 합니다.

    ex) 주문 사세보기, 상품 목록 조회, 필터링 검색, 정렬 등

     

    → 도메인 규칙을 변경하지 않고, 데이터를 '표시'하기 위한 행위입니다. 

     

     

    OrderQueryService (Query Service)

    화면에 맞춘 조회 전용 로직을 담당합니다. (DTO매핑 및 성능 최적화 기능)

    @Service
    @Transactional(readOnly = true)
    public class OrderQueryService {
    
        private final EntityManager em;
    
        public OrderQueryService(EntityManager em) {
            this.em = em;
        }
    
        public OrderDto getOrderDetail(Long orderId) {
            return em.createQuery(
                "select new com.example.dto.OrderDto(o.id, m.name, o.status, o.orderDate) " +
                "from Order o join o.member m where o.id = :orderId", OrderDto.class)
                .setParameter("orderId", orderId)
                .getSingleResult();
        }
    }
    • 데이터 조회 (API, 화면) 역할
    • @Transactional(readOnly = true)
    • API or 화면 스펙에 맞는 DTO 반환
    • 외부 응답(API, View) 중심
    • UI 요구사항에 민감

     

     


     

     

     

    Command와 Query를 분리하는 이유?

     

    OrderService안에 다음과 같이 등록, 취소, 조회가 동시에 들어있다고 가정해 보겠습니다.

    public class OrderService {
        public Order createOrder(...) { ... }       // 상태 변경
        public void cancelOrder(...) { ... }        // 상태 변경
        public OrderDto getOrderDetail(...) { ... } // 화면용 조회
    }

     

     

    이렇게 Command와 Query가 섞이면 다음과 같은 문제점들이 발생합니다.

    • 조회 성능 최적화가 들어오면 서비스 가독성이 좋지 않음
      • JOIN FETCH, QueryDSL, DTO 매핑 등 조회 관련 로직이 추가됨
      • 조회용 쿼리와 비즈니스 로직이 한 클래스에 혼재되어 책임이 흐려짐
    • 단일 책임 원칙 위배
      • 하나의 클래스에서 조회와 비즈니스 로직처리라는 서로 다른 책임을 가지게 됨
    • 유지보수 어려움
      • 조회화면의 변경과 비즈니스 로직의 변경이 서로 영향을 미침
      • API스펙이 변경될 시 비즈니스 서비스가 영향을 받음, 반대로 비즈니스 로직이 수정될 때 조회 로직이 영향을 받음

     

     

     

    따라서 다음과 같이 Command와 Query를 구분하는 패턴으로 어플리케이션을 개발했습니다. 

    상태 변경 OrderService 도메인 모델 중심, 트랜잭션으로 묶임
    조회 OrderQueryService 화면 맞춤 DTO 반환, 성능 최적화 가능

     

     

     

     


     

     

     

     

    항상 Command와 Query를 분리해야 할까?

    저는 항상 분리할 필요는 없다고 생각합니다. 

     

    CQRS는 복잡한 시스템에서 명령(Command)과 조회(Query)의 책임을 분리함으로써 유지보수성과 성능 최적화를 도모하는 아키텍처 패턴입니다.

    하지만 단순한 CRUD 애플리케이션이나, 로직이 간단한 서비스에서는 오히려 구조만 복잡해지고 개발 생산성이 떨어질 수 있습니다.

     

     

    1. CQRS를 도입하지 않는 경우

    • 생성과 조회 정도의 간단한 기능만 있을 때
    • 화면 요구사항이 복잡하지 않고, 성능 최적화가 필요 없는 경우
    • 프로젝트 규모가 작거나, 빠르게 만들어야 할 MVP 단계

    이럴 땐 굳이 Command와 Query를 나눌 필요 없이 하나의 서비스 클래스에서 처리해도 충분합니다.

     

     

    2. CQRS를 도입하는 경우

    • 조회 로직이 점점 복잡해지고, 여러 화면이나 API에 맞춘 최적화가 필요한 경우
    • 비즈니스 로직이 복잡하고 변경이 잦아, 책임 분리가 필요한 경우
    • 도메인마다 읽기와 쓰기를 담당하는 팀이나 모듈이 분리되어야 하는 대규모 시스템
    • 성능 개선이나 확장성이 중요한 경우

     

     


     

     

    정리

    "선택적 적용"이 가장 중요합니다. 

     

    CQRS는 도구일 뿐, 무조건 따라야 하는 규칙이 아닙니다.
    서비스의 규모, 복잡도, 변경 가능성 등을 고려해 필요할 때 선택적으로 적용하는 것이 가장 중요합니다. 

     

     

     

     

     

Designed by Tistory.