『도메인 주도 개발 시작하기 - DDD 핵심 개념 정리부터 구현까지』를 읽고 정리한 내용입니다.
CQRS
CQRS(Command Query Responsibility Segregation)는 명령(Command)과 조회(Query)의 책임을 분리하는 패턴이다.
Command
└─ 상태 변경
Query
└─ 데이터 조회
명령과 조회는 서로 목적이 다르기 때문에 하나의 모델로 처리하기보다 각각의 목적에 맞게 분리할 수 있다.
특징
- 명령 모델은 상태 변경을 담당한다.
- 조회 모델은 데이터 조회를 담당한다.
- 각각의 목적에 맞게 모델을 최적화할 수 있다.
NOTE
조회 기능은 상태 변경이 목적이 아니기 때문에 별도의 조회 모델을 사용하는 것이 더 효율적일 수 있다.
예를 들어 복잡한 화면을 구성하기 위해 여러 테이블의 데이터를 조합해야 하는 경우, 도메인 모델을 그대로 사용하는 것보다 조회에 최적화된 별도의 모델을 사용하는 것이 효율적이다.
검색과 스펙(Specification)
검색 조건이 복잡해질수록 Repository의 메서드가 계속 증가하는 문제가 발생할 수 있다.
예를 들어 다음과 같이 검색 조건마다 별도의 메서드를 만들어야 한다.
findByName()
findByCategory()
findByPrice()
findByCategoryAndPrice()
findByCategoryAndPriceAndBrand()
...
검색 조건이 추가될 때마다 새로운 메서드를 만들어야 하므로 Repository의 크기가 커지고 관리하기 어려워진다.
이러한 문제를 해결하기 위해 검색 조건을 객체로 표현하는 Specification 패턴을 사용할 수 있다.
Specification<Product>
특징
Specification을 사용하면 다음과 같은 장점이 있다.
- 검색 조건을 객체로 표현할 수 있다.
- 여러 검색 조건을 조합할 수 있다.
- 검색 조건에 따른 Repository 메서드 증가를 줄일 수 있다.
- 사용하는 기술에 맞게 검색 조건을 구현할 수 있다.
즉, 검색 조건을 Repository 메서드 이름에 직접 표현하는 대신 검색 조건 자체를 객체로 분리하는 방식이다.
스프링 데이터 JPA의 Specification
스프링 데이터 JPA에서는 Specification<T>를 제공하여 동적인 검색 조건을 구성할 수 있다.
Specification<Product>
Specification은 검색 조건을 JPA의 Predicate로 생성한다.
Specification
│
▼
Predicate
│
▼
WHERE 조건
즉, Specification으로 정의한 조건이 JPA 쿼리의 WHERE 조건으로 변환되어 실행된다.
스펙 구현
Specification은 직접 구현 클래스를 만들어 사용할 수 있다.
또는 별도의 Factory나 Specs와 같은 클래스를 만들어 검색 조건을 생성하는 방식으로 구성할 수도 있다.
검색 조건을 별도의 객체로 분리하면 Repository나 Service에서 복잡한 조건을 직접 구성하지 않아도 된다.
스펙 조합
스프링 데이터 JPA의 Specification은 여러 검색 조건을 조합할 수 있는 기능을 제공한다.
대표적으로 다음과 같은 메서드를 사용할 수 있다.
and()or()not()where()
예를 들어 카테고리와 가격, 브랜드 조건을 조합하여 검색할 수 있다.
스펙 빌더
검색 조건이 많아질 경우 Specification을 직접 조합하는 코드가 복잡해질 수 있다.
이러한 경우에는 Builder를 사용하여 검색 조건을 조합할 수 있다.
장점은 다음과 같다.
if문을 줄일 수 있다.- 검색 조건의 가독성이 향상된다.
- 조건 조합을 단순화할 수 있다.
- 동적인 검색 조건을 관리하기 쉬워진다.
즉, 검색 조건이 많고 조합이 복잡한 경우에는 Specification Builder를 활용하는 것이 유용하다.
정렬(Sort)
스프링 데이터 JPA에서는 조회 결과의 정렬을 여러 가지 방식으로 지정할 수 있다.
방법
첫 번째 방법은 메서드 이름에 OrderBy를 사용하는 것이다.
두 번째 방법은 Sort 객체를 Repository 메서드에 전달하는 것이다.
Sort.by("price").descending()
Sort 객체를 사용하면 정렬 조건을 동적으로 변경할 수 있기 때문에 메서드 이름을 사용하는 방식보다 유연하다.
페이징(Page)
스프링 데이터 JPA에서는 Pageable을 사용하여 페이징 처리를 할 수 있다.
PageRequest.of(page, size)
Pageable은 조회할 페이지 번호와 페이지 크기 등의 정보를 전달하는 역할을 한다.
반환 타입
페이징 조회에서는 반환 타입에 따라 COUNT 쿼리 실행 여부가 달라질 수 있다.
Page→ 전체 데이터 개수를 확인하기 위해 COUNT 쿼리를 실행한다.List→ 일반적으로 COUNT 쿼리를 실행하지 않는다.
Page는 전체 데이터 개수와 전체 페이지 수 등의 정보를 제공하기 위해 COUNT 쿼리가 필요하다.
NOTE
Specification + Pageable을 함께 사용하는 경우에는 반환 타입이List이더라도 COUNT 쿼리가 실행될 수 있다.COUNT 쿼리가 불필요한 경우에는 커스텀 Repository를 구현하여 조회 방식을 직접 제어할 수 있다.
Top / First
일부 데이터만 조회해야 하는 경우에는 메서드 이름에 Top 또는 First를 사용할 수 있다.
findTop10By...
findFirst5By...
이를 통해 조회 결과 중 특정 개수만 가져올 수 있다.
@Subselect
@Subselect는 조회 결과를 Entity처럼 사용할 수 있도록 하는 기능이다.
SELECT ...
│
▼
@Subselect Entity
일반적인 Entity가 테이블과 매핑되는 것과 달리 @Subselect를 사용하면 특정 SQL 조회 결과를 Entity와 매핑할 수 있다.
특징
- 조회 전용으로 사용할 수 있다.
- 데이터베이스의 View와 유사한 방식으로 사용할 수 있다.
- 일반적인 Entity처럼 수정하는 용도로 사용하지 않는다.
@Immutable을 함께 사용하면 해당 Entity에 대한 변경 감지를 하지 않도록 설정할 수 있다.
NOTE
@SubselectEntity는 조회 전용으로 사용하는 것이 적합하다.일반적인 Entity처럼 상태를 변경하고 저장하는 용도로 사용하는 대상이 아니다.
예를 들어 여러 테이블을 조인하여 화면에 필요한 데이터를 조회해야 하는 경우 @Subselect를 이용해 조회 전용 Entity를 구성할 수 있다.
@Synchronize
@Synchronize는 @Subselect와 함께 사용할 수 있는 애너테이션이다.
역할
@Synchronize를 사용하면 @Subselect에서 사용하는 테이블을 지정할 수 있다.
이를 통해 다음과 같은 역할을 수행한다.
@Subselect와 관련된 테이블을 지정한다.- Entity 조회 전에 필요한 경우
flush()를 수행한다. - 현재 트랜잭션의 변경 사항이 조회 결과에 반영될 수 있도록 한다.
즉, @Subselect는 조회 전용 Entity를 구성하고, @Synchronize는 해당 조회와 관련된 테이블을 지정하여 조회 시점의 데이터 일관성을 보장하는 데 도움을 준다.
정리
조회 기능은 단순히 Repository에서 Entity를 가져오는 것만을 의미하지 않는다. 조회 조건, 정렬, 페이징, 조회 성능 등을 함께 고려하여 조회 목적에 적합한 모델과 방식을 선택하는 것이 중요하다.
CQRS를 사용하면 명령과 조회의 책임을 분리할 수 있으며, 조회 기능을 별도의 모델로 구성하여 조회 목적에 맞게 최적화할 수 있다.
검색 조건이 복잡한 경우에는 Specification을 사용하여 조건을 객체로 표현하고 조합할 수 있다. 이를 통해 검색 조건이 증가할 때마다 Repository 메서드가 계속 추가되는 문제를 줄일 수 있다.
또한 Pageable, Sort, Top, First 등을 활용하면 페이징과 정렬, 일부 데이터 조회를 편리하게 처리할 수 있다.
마지막으로 @Subselect를 사용하면 복잡한 조회 결과를 조회 전용 Entity로 구성할 수 있다. 따라서 상태 변경이 목적이 아닌 조회 기능에서는 도메인 Entity를 무조건 사용하는 것보다 조회에 최적화된 별도의 모델을 구성하는 것이 중요하다.