Programming/DDD

DDD - CQRS란 무엇인가?!

JaeHoney 2022. 8. 8. 08:28

해당 포스팅은 "도메인 주도 개발 시작하기" 라는 내용을 정리한 글입니다. 해당 도서는 아래 Link에서 확인할 수 있습니다.

- http://www.yes24.com/Product/Goods/108431347

단일 모델의 단점

주문 내역 조회 기능을 구현하려면 여러 애그리거트에서 데이터를 가져와야 한다.

 

Order에서 주문 정보를 가져와야 하고, Product에서 상품 정보를 가져와야 하고, Member에서 회원 정보를 가져와야 한다.

조회 화면 특성상 속도가 빠를수록 좋은데 여러 애그리거트의 데이터가 필요하면 구현 방식을 고민해야 한다. 식별자(ID)를 이용해서 애그리거트에 참조하게 되면 즉시 로딩(Eager loading) 방식과 같은 JPA의 쿼리 관련 최적화 기능을 사용할 수 없다.

-> 이는 한 번의 SELECT 쿼리로 조회 화면에 필요한 데이터를 읽어올 수 없어 조회 성능에 문제가 생길 수 있다.

 

식별자 참조가 아니라 직접 참조하는 방식으로 연결해도 고민거리가 생긴다. 조회 화면 특성에 따라 같은 연관도 즉시 로딩이나 지연 로딩으로 처리해야 하기 때문이다. 

 

조회 기능을 구현할 때 DBMS가 제공하는 전용 기능이 필요하면 JPA의 네이티브 쿼리를 사용해야 할 수도 있다.

 

이런 고민들이 발생하는 이유는 시스템 상태를 변경할 때와 조회할 때 단일 도메인 모델을 사용하기 때문이다. 이를 분리하면 구현 복잡도를 낮출 수 있다.

CQRS란 ?

Command Query Responsibility Segregation의 약자로 도메인 모델을 다음의 두 가지 모델로 분리하는 패턴이다.

  • 상태를 변경하는 명령(Command)을 위한 모델
  • 상태를 제공하는 조회(Query)를 위한 모델

출처: https://martinfowler.com/bliki/CQRS.html

 

CQRS는 복잡한 도메인에 적합하다. 도메인이 복잡할수록 명령 기능과 조회 기능이 다루는 데이터 범위에 차이가 난다.

 

예를 들어, 온라인 쇼핑 서비스에서 통계를 조회하기 위해서는 JPA와 관련된 다양한 성능 관련 기능을 모델에 적용해야 한다. 이런 도메인에 CQRS를 적용하면 통계를 위한 조회 모델을 별도로 만들기 때문에 조회 기능 때문에 도메인 모델이 복잡해지는 것을 막을 수 있다.

 

CQRS를 사용하면 각 모델에 맞는 구현 기술을 선택할 수 있다. 명령 모델은 객체지향에 기반해서 도메인 모델을 구현하기에 적당한 JPA를 사용하고, 조회 모델은 DB 테이블에서 SQL로 데이터를 조회할 때 좋은 마이바티스를 사용할 수 있다.

CQRS 설계

아래 그림을 보면 조회 모델에는 서비스-DAO로 구현하고 있다. 

인프라 스트럭쳐 영역은 존재하지 않는다. DAO를 사용해서 DB에서 바로 데이터를 조회했기 때문이다.

 

추가로 서비스 영역도 반드시 필요하지 않다. 조회하는 기능은 로직이 복잡하지 않기 때문에 컨트롤러에서 DAO를 실행해도 무방하다.

(데이터를 표현 영역에 전달하는 과정에서 몇 가지 로직이 필요하다면 응용 서비스를 둘 수 있다.)

 

다음 그림은 명령 모델과 조회 모델의 설계 예를 보여준다.

조회 모델

명령 모델과 조회 모델이 다른 구현 기술을 사용할 수 있지만 같은 조회 기술을 사용할 수도 있다. 예를 들어 JPA의 @Subseect를 사용해서 조회 모델을 표현하는 방법이 있다.

@Entity
@Immutable
@Subselect(
        """
        select o.order_number as number,
        o.version,
        o.orderer_id,
        o.orderer_name,
        o.total_amounts,
        o.receiver_name,
        o.state,
        o.order_date,
        p.product_id,
        p.name as product_name
        from purchase_order o inner join order_line ol
            on o.order_number = ol.order_number
            cross join product p
        where
        ol.line_idx = 0
        and ol.product_id = p.product_id"""
)
@Synchronize({"purchase_order", "order_line", "product"})
public class OrderSummary {
    @Id
    private String number;
    private long version;
    @Column(name = "orderer_id")
    private String ordererId;
    @Column(name = "orderer_name")
    private String ordererName;
    @Column(name = "total_amounts")
    private int totalAmounts;
    @Column(name = "receiver_name")
    private String receiverName;
    private String state;
    @Column(name = "order_date")
    private LocalDateTime orderDate;
    @Column(name = "product_id")
    private String productId;
    @Column(name = "product_name")
    private String productName;
    
    ...
}

@Subselect는 쿼리 결과를 @Entity로 매핑할 수 있는 유용한 기능이다.

 

뷰를 수정할 수 없듯이 @Subselect로 조회한 @Entity 역시 수정할 수 없다. 그래서 해당 엔터티의 필드를 변경하려고 하면 에러가 터진다. 이런 문제를 해결하기 위해 @Immutable을 사용한다. @Immutable을 사용하면 하이버네이트는 영속성을 무시하고 변경 사항을 DB에 반영하지 않는다.

 

엔터티의 상태 변경 트랜잭션 커밋이 완료되기 이전에 조회 모델을 사용하면 최신 값이 아닌 이전 값이 담기게 된다. 이때 @Synchonized를 사용하면 해당 엔터티를 로딩하기 전에 지정한 테이블과 관련된 변경을 플러시(flush)해서 최신 내역을 받아올 수 있도록 한다.

 

@Subselect를 사용해도 일반 @Entity와 같기 때문에 entityManager.find()나 JPQL, Criteria, Specification 등을 사용해서 조회하는 데 문제가 없다.

DBMS 분리

명령 모델과 조회 모델이 서로 다른 데이터 저장소를 사용할 수도 있다. 예를 들어 명령 모델은 트랜잭션을 지원하는 RDBMS를 사용하고, 조회 모델은 조회 성능이 좋은 메모리 기반 NoSQL을 사용할 수 있다.

이 경우 두 데이터 저장소 간 데이터 동기화가 필요하다. 이때는 저번 포스팅에서 다뤘던 이벤트를 활용해서 처리한다.

https://jaehoney.tistory.com/254

결론

대규모 트래픽이 발생하는 웹 서비스는 알게 모르게 CQRS를 적용하게 된다. 명시적으로 명령 모델과 조회 모델을 구분하지 않을 뿐이다.

 

CQRS 패턴을 적용하면 명령 모델을 구현할 때 도메인 자체에 집중할 수 있게 된다. 명령 모델에서 조회 관련 로직이 사라져 복잡도가 낮아진다.

 

다른 장점은 조회 성능을 향상시키는 데 유리하다는 점이다. 조회 단위로 캐시 기술을 적용할 수 있고, 조회에 특화된 쿼리를 마음대로 사용할 수도 있다.

캐시뿐만 아니라 조회 전용 저장소를 사용하면 처리량을 대폭 늘릴 수도 있다. 즉, 트래픽이 갑자기 대량으로 발생했을 때 시스템을 확장하기 쉬운 구조를 갖는다는 것을 의미한다.

 

단점은 첫 번째로 구현해야 할 코드가 많아진다. 즉, 단일 모델을 사용할 때 발생하는 복잡함 때문에 발생하는 구현 비용과 조회 전용 모델을 만들 때 발생하는 구현 비용을 따져봐야 한다.

도메인이 단순하거나 트래픽이 많지 않은 서비스라면 조회 전용 모델을 따로 구현할 때 얻을 이점이 있는 지 따져봐야 한다.

 

두 번째로는 더 많은 구현 기술이 필요하다. 명령 모델과 조회 모델을 다른 구현 기술을 사용해서 구현하기도 하고 경우에 따라 다른 저장소를 사용하기도 한다. 또한 데이터 동기화를 위해 메시징 시스템을 도입해야 할 수도 있다.

 


Reference