IT/기초 지식

[DDD] CQRS 입문

개발자 두더지 2023. 5. 17. 21:19
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

 

DDD의 참조 처리에서 발생하는 과제


 DDD에서 정의되어 있는 구현 패턴을 사용하고 있다면, 기본적으로는 영속화층마다의 입출력은 Repository를 사용할 것이다. 갱신계의 처리에서는 Entity나 ValuObject로 도메인의 지식을 표현하고, Repository를 사용하여 집약 단위로 영속화한다는 구성하게 되면 매우 유지/보수가 편리해진다.

 한편, 참조 처리 특히 리스트 화면과 같은 처리에서는 여러 개의 집약 값을 엮거나 합쳐서 화면에 나타내는 경우가 많다.

 예를 들어, 태스크, 유저, 라벨이라는 세 개의 집약이 있으며, 각각 Repository가 있다고 하자. 이 세 가지를 합쳐서 1개의 UseCas(ApplicationService)로 구현하고자하면, 3개의 Repository에서 각각 값을 획득하고 반환 값의 오브젝트로 엮어주는 처리를 구현해야할 필요가 있다. 이때 다음과 같은 문제가 발생한다.

  • 여러개의 집약으로부터 값을 획득하여 반환 값의 형태로 엮는 처리는 반복문의 사용이 늘기 때문에 읽기 힘든 코드가 된다.
  • 화면에 반환할 필요가 없는 값을 한 번에 획득하므로 퍼포먼스가 악화된다.
  • 여러 개의 집약 조건(where)으로 엮기 때문에, 페이징이 되지 않는다.

 

 

해결책


 이러한 문제는 CQRS(Command Query Reponsibility Segregation)이라는 방법을 도입하면 해결할 수 있다. CQRS는 "정보의 참조에 사용할 모델과 갱신에 사용할 모델을 나눠서 사용하는" 구조이다.

 모델이라는 단어가 애매한데, 문맥상으로 어플리케이션의 모델, 즉 갱신계의 오브젝트와 참조계의 오브젝트를 나누는 것으로 알면 된다.

이름 구체적인 예 UseCase가 DB에 액세스할 때에
사용하는 것
갱신계 모델 DDD의 Entity, ValueObject 등 Repository
참조계 모델 특정의 유스케이스에 특화된 값의 형태. SQL의 결과 1 레코드를 하나의 형태로 하는 등(DTO이라는 명명으로 한다) 전용 오브젝트
(QueryService이라는 명명으로 한다)

 또한, QueryService, DTO 명명 규약은 사실상 강제가 아니므로, 프로젝트에 맞게 이름을 붙이면 된다.

 앞선 예에서는 다음과 같이 참조계 모델을 정의할 수 있다.

public class TaskDto {
  private String taskId;
  private String taskName;
  private String userName;
  private String labelName;
}
public interface TaskQueryService {
  public List<TaskDto> fetchByUserId(UserId userId);
}

 DDD 구조상 어떠한 위치에 두면 될까라고 한다면, QueryService의 Interface와 반환형태는 UseCase층에, 구현 클래스는 Repositroy와 동일한 인프라층에 배치하면 된다. 

 UseCase로 부터는 "이러한 조건을 지정하면, 이러한 이러한 형태로 반환된다"라는 추정적인 정의만 해놓고, 실제의 구현은 인프라층에 숨긴다.

 그리고 (이번 예제에서는) QueryService의 구현 클래스내에서 여러 개의 테이블을 Join하여 한번에 원하는 데이터를 획득할 수 있는 쿼리를 쓰고, 간단하게 결과를 DTO에 채워넣는다. 쿼리 실행에 사용하는 방법은 필요에 따라 선택할 수 있다.  직접 String으로 SQL를 써도 좋고, 쿼리 빌더와 같은 라이브러리를 사용해도 좋다.

 

 

CQRS의 장점과 단점


  이 내용은 아까 과제의 내용을 반복하는 것일 수 있지만, 아래와 같은 장점이 있다.

  • 여러 개의 집약에 걸친 데이터를 획득할 때, 코드가 간결해져서 유지/보수성이 좋아진다.
  • 쿼리 퍼포먼스가 높아져, 튜닝하기 편해진다.
  • 여러 개의 집약 조건(where)로 좁힌 페이징을 할 수 있게 된다.

  한편, 단점은 다음과 같다.

  • 오브젝트의 속성이 참조되어 있는 장소를 찾기 어려워진다.
    • 원래 Getter의 참조를 확인하면 됐지만, 다른 수단을 생각할 필요가 있다.
  • 구조 자체가 복잡해져서, 해설이 필요해진다.

 확실히 장점이 매우 크지만, 단점도 무시할 수 없다.

 

 

구현시의 주의사항


부분적 도입에 대해서

 중요한 문제인데, CQRS는 부분적으로 도입할 수 있다. 즉, "참조용 모델과 갱신용 모델을 완전히 나눌 필요가 없다"라는 것이다. 그러므로, "필요한 부분만 참조에 특화된 모델을 도입"하는 방법도 적용할 수 있다.

 

왜 QueryService의 정의가 UseCase층인가

 이것은 QueryService의 반환값이 유스케이스에 의존하기 때문이다. 

 

정합성은 어떻게 담보되는가

 UseCase층에 테스트를 쓰자. 참조 처리만의 테스트일지라도 쓰는 것이 좋다. 또한, 업무상 중요한 부분에 대해서는 갱신 처리와의 결합 테스트도 쓰는 것이 좋을 것이다.

 예를 들어, 승인 처리를 한 결과가 리스트에 나오지 않는 경우에 승인처리 UseCase와 참조처리 UseCase를 호출하는 테스트를 쓰도록하자. 

 

 

자주하는 오해


데이터 소스(베이스)를 분리할 필요가 있는가?

 CQRS는 데이터 소스 분리라고 생각할지도 모르겠지만, 그것은 오해이다. 꼭 분리하지 않아도 된다. 데이터 소스 분리는 다른 과제로서 모델을 분리한 후에 다음 스텝으로 생각할 수 있다. 상정된 과제는 참조계의 퍼포먼스 문제이다.

 일반적으로 참조계 요청 수는 갱신계 요청 수보다 압도적으로 많다. 그래서 참조계의 성능을 높이고 싶을 때 갱신계와는 데이터 소스를 분리함으로써 참조계 인스턴스만 스케일아웃하는 등의 퍼포먼스 튜닝이 가능하긴 하다. 또한 인스턴스 단위로 나누지 않아도 참조계 처리용으로 머티리얼 뷰를 작성하여 데이터 소스 분리의 일환으로 생각할 수 있다.

 데이터 소스 분리는 모델 분리와는 별개이므로 제대로 과제와 해결책이 대응하고 있는지 판단하여 도입 여부를 판단해야 한다.

 

이벤트 소싱과의 관계

 CQRS는 이벤트 소싱이다라는 오해를 종종받는다. 이 둘의 궁합이 잘 맞을뿐 두 개는 따로 생각 할 수 있다. 참고로 이벤트 소싱이란 데이터 영속화를 도메인 객체(Entity나 ValueObject)의 상태를 그대로 보존하는 것이 아니라 "사용자 등록", "작업 완료"와 같은 식의 이벤트 자체를 영속화하는 아키텍처이다.

 참조시 바로 쿼리가 실행되도록 집계 데이터를 영속화하는 처리를 함께 하는 경우가 있으며, 이 경우 필연적으로 참조/갱신 모델의 분리나 데이터 소스의 분리가 이루어진다. 

 즉 이벤트 소싱은 CQRS, 데이터 소스 분리를 포함하고 있긴하지만 반대로 CQRS가 가 이벤트 소싱을 포함하고 있다고 할 수 없다. 


참고자료

https://little-hands.hatenablog.com/entry/2019/12/02/cqrs

728x90