Projections(Dto 조회)와 @QueryProjection
특정 Entity를 조회할 때 Entity의 모든 값들이 필요하지 않은 경우가 있습니다. 예를 들면 게시판 목록을 보여줄 때 게시판의 내용(contents)이 필요하지 않고, 게시판의 제목, 작성자, 작성일과 같이 일부 데이터만 필요한 경우가 있습니다. 때문에 게시판 목록 조회 시 게시판 내용과 같은 데이터들을 DB에서 같이 조회해 오는 것은 불필요한 자원낭비가 되어 성능이 저하되는 원인이 됩니다. 이러한 문제를 해결하기 위해 QueryDSL에서는 불필요한 값들은 조회하지 않고, 필요한 값들만 조회할 수 있도록 성능을 최적화시켜 주는 Projections을 제공합니다.
Projections은 쿼리 결과를 원하는 형식(Dto)으로 변환하기 위해 사용되는 기능으로 다음과 같은 네 가지 방법이 있습니다.
- Projections.bean(..)
- Projections.field(..)
- Projections.construct(..)
- @QueryProjection
예제 코드는 Github에서 확인하실 수 있습니다.
1. Projections.bean(..)
JavaBeans Property를 이용한 Projections.bean(..)
은 Setter와 기본 생성자가 필요합니다. 하지만 Dto 객체는 불변 객체로 사용하는 것을 지향하기 때문에 Setter가 필요한 Projections.bean(..)
은 좋은 방법이 아니라고 생각합니다.
- Setter와 기본생성자 필요
@Getter
@Setter
@NoArgsConstructor
public class ArticleBeanDto {
private Long id;
private String title;
private LocalDateTime lastModifiedDate;
private Long lastModifiedBy;
}
@DisplayName("Projections.bean 사용")
@Test
public void projections_bean() {
Article savedArticle = articleRepository.save(Article.builder()
.title("타이틀 테스트")
.contents("내용 테스트")
.build());
ArticleBeanDto articleBeanDto = queryFactory
.select(Projections.bean(ArticleBeanDto.class,
article.id,
article.title,
article.lastModifiedBy,
article.lastModifiedDate
))
.from(article)
.where(article.id.eq(savedArticle.getId()))
.fetchOne();
Assertions.assertThat(articleBeanDto.getId()).isEqualTo(savedArticle.getId());
Assertions.assertThat(articleBeanDto.getTitle()).isEqualTo(savedArticle.getTitle());
Assertions.assertThat(articleBeanDto.getLastModifiedBy()).isEqualTo(savedArticle.getLastModifiedBy());
}
2. Projections.fields(..)
Dto의 필드 명과 Projection의 컬럼 명으로 일치하는 경우 매핑하는 Projections.fields(..)
은 Setter가 없어도 필드명으로 매핑되기 때문에 Dto 객체를 불변 객체로 사용할 수 있어 제가 추천하는 방법입니다. 만약 필드명과 컬럼 명이 다를 경우에는 별칭(alias)을 사용하여 매핑할 수 있습니다.
- 필드명과 매핑, 기본 생성자 필요
@Getter
public class ArticleFieldsDto {
private Long id;
private String title;
private LocalDateTime lastModifiedDate;
private Long lastModifiedBy;
}
@DisplayName("Projections.fields 사용")
@Test
public void projections_field() {
Article savedArticle = articleRepository.save(Article.builder()
.title("타이틀 테스트")
.contents("내용 테스트")
.build());
ArticleFieldsDto articleFieldsDto = queryFactory
.select(Projections.fields(ArticleFieldsDto.class,
article.id.as("id"), // as("id")는 생략이 가능하다(필드명과 일치)
article.title.as("title"), // as("title")는 생략이 가능하다(필드명과 일치)
article.lastModifiedBy,
article.lastModifiedDate
))
.from(article)
.where(article.id.eq(savedArticle.getId()))
.fetchOne();
Assertions.assertThat(articleFieldsDto.getId()).isEqualTo(savedArticle.getId());
Assertions.assertThat(articleFieldsDto.getTitle()).isEqualTo(savedArticle.getTitle());
Assertions.assertThat(articleFieldsDto.getLastModifiedBy()).isEqualTo(savedArticle.getLastModifiedBy());
}
주의할 점은 @AllArgsConstructor
를 단독으로 사용할 경우 기본 생성자가 없기 때문에 Exception이 발생할 수 있습니다. 따라서 @AllArgsConstructor
와 같이 매개변수가 있는 생성자를 만들었을 경우에는 기본 생성자를 추가해 주셔야 합니다.
Exception 예시
@AllArgsConstructor // 기본 생성자가 없으므로 예외 발생
public class ArticleFieldsDto {
private Long id;
private String title;
private LocalDateTime lastModifiedDate;
private Long lastModifiedBy;
}
3. Projections.constructor(..)
생성자를 사용하여 필드에 매핑시켜 주는 방법입니다. 즉, 매핑할 필드들을 생성자의 매개변수에 추가하여 매핑할 수 있습니다. 일반적으로 사용할 때 Dto에 모든 필드들을 매핑하므로 @AllArgsConstructor
를 사용합니다. 주의할 점은 생성자의 파라미터 순서와 Projection의 순서가 일치하지 않는다면 Exception이 발생할 수 있습니다. 때문에 Projection의 순서와 Dto의 매핑되는 필드와 순서가 일치해야 하므로 조회하는 Projection이 많게 되면 실수가 발생할 수도 있고, 유지보수가 어려워져 권장하지 않는 방법입니다.
- 매핑할 필드를 매개변수로 받는 생성자 필요
- 생성자의 매개변수 순서와 Projection의 순서가 다를 경우 예외 발생
@Getter
@AllArgsConstructor // 필드의 순서대로 매개변수를 갖는 생성자를 만들어준다.
public class ArticleConstructDto {
private Long id; // 1
private String title; // 2
private LocalDateTime lastModifiedDate; // 3
private Long lastModifiedBy; // 4
}
@DisplayName("Projections.constructor 사용")
@Test
public void projections_constructor() {
Article savedArticle = articleRepository.save(Article.builder()
.title("타이틀 테스트")
.contents("내용 테스트")
.build());
ArticleConstructDto articleConstructDto = queryFactory
.select(Projections.constructor(ArticleConstructDto.class,
article.id.as("id"), // 1
article.title.as("title"), // 2
article.lastModifiedDate.as("lastModifiedDate"),// 3
article.lastModifiedBy.as("lastModifiedBy") // 4
))
.from(article)
.where(article.id.eq(savedArticle.getId()))
.fetchOne();
Assertions.assertThat(articleConstructDto.getId()).isEqualTo(savedArticle.getId());
Assertions.assertThat(articleConstructDto.getTitle()).isEqualTo(savedArticle.getTitle());
Assertions.assertThat(articleConstructDto.getLastModifiedBy()).isEqualTo(savedArticle.getLastModifiedBy());
}
참고로 @AllArgsConstructor
는 필드의 순서대로 생성자를 만들어줍니다. 따라서 @AllArgsConstructor
를 사용 시 Dto의 필드 순서에 맞게 Projection을 구현해야 합니다.
4. @QueryProjection
Dto의 Q-Type을 만들어 사용하는 방법입니다. Dto 생성자에 @QueryProjection
어노테이션을 사용하게 되면 Q-Type이 생성할 수 있으며 컴파일 시점에서 에러를 잡아낼 수 있다는 강력한 장점이 있습니다. 하지만 Dto도 Q-Type을 생성한다는 점과 Dto가 QueryDSL에 대한 의존성을 갖게 되기 때문에 좋은 방법이라고 생각하지 않습니다.
@Getter
public class ArticleQueryProjectionDto {
private Long id;
private String title;
private LocalDateTime lastModifiedDate;
private Long lastModifiedBy;
@QueryProjection // QArticleQueryProjectionDto 생성
public ArticleQueryProjectionDto(Long id, String title, LocalDateTime lastModifiedDate, Long lastModifiedBy) {
this.id = id;
this.title = title;
this.lastModifiedDate = lastModifiedDate;
this.lastModifiedBy = lastModifiedBy;
}
}
@DisplayName("@QueryProjection 사용")
@Test
public void query_projection_annotation() {
Article savedArticle = articleRepository.save(Article.builder()
.title("타이틀 테스트")
.contents("내용 테스트")
.build());
ArticleQueryProjectionDto articleQueryProjectionDto = queryFactory
.select(new QArticleQueryProjectionDto(
article.id,
article.title,
article.lastModifiedDate,
article.lastModifiedBy)
)
.from(article)
.where(article.id.eq(savedArticle.getId()))
.fetchOne();
Assertions.assertThat(articleQueryProjectionDto.getId()).isEqualTo(savedArticle.getId());
Assertions.assertThat(articleQueryProjectionDto.getTitle()).isEqualTo(savedArticle.getTitle());
Assertions.assertThat(articleQueryProjectionDto.getLastModifiedBy()).isEqualTo(savedArticle.getLastModifiedBy());
}
결론
제가 생각하기에 가장 추천하는 방법은 Projections.fields(..)
를 사용하는 방법입니다. Dto 객체를 불변 객체로 만들어 사용할 수 있고, 유지보수가 용이하다는 점 등에서 많은 장점이 있기 때문에 제가 실제로 실무에서 사용하는 방법입니다.
Projections를 사용하는 다른 분들은 어떤 방법을 사용하고, 그 이유가 무엇인지 댓글로 의견을 남겨주시면 감사하겠습니다!
'Backend > JPA & QueryDSL' 카테고리의 다른 글
[JPA] JPA 관련 설정(properties, yml) (0) | 2023.07.15 |
---|---|
[JPA, QueryDSL] JPA와 QueryDSL의 exists 성능 테스트 (0) | 2023.07.15 |
[QueryDSL] Spring Boot 3.X 버전에 따른 QueryDSL 설정 (0) | 2023.07.15 |
[JPA] Entity 계층 구조에 따른 상속 관계 매핑 - @Inheritance (0) | 2023.07.15 |
[JPA] 영속성 컨텍스트 (0) | 2023.07.09 |