Projection 연산이란?
한 Relation의 Attribute들의 부분 집합을 구성하는 연산자입니다.
결과로 생성되는 Relation은 스키마에 명시된 Attribute들만 가집니다.
결과 Relation은 기본 키가 아닌 Attribute에 대해서만 중복된 tuple들이 존재할 수 있습니다.
-> 테이블에서 원하는 컬럼만 뽑아서 조회하는 것
- Relation 데이터를 원자 값으로 갖는 이차원 테이블
- Column = Attribute
Querydsl DTO로 조회하는 4가지 방법
- Projection.bean
- Projection.fields
- Projection.constructor
- @QueryProjection
사전 작업
spring boot3.0
build.gradle
buildscript { ext { queryDslVersion = "5.0.0" } }
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.0'
id 'io.spring.dependency-management' version '1.1.0'
// querydsl관련 명령어를 gradle탭에 생성해준다. (권장사항)
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
group = 'com.demo'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// querydsl 디펜던시 추가
// == 스프링 부트 3.0 이상 ==
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
tasks.named('test') {
useJUnitPlatform()
}
/*
- queryDSL 설정 추가
- /
// querydsl에서 사용할 경로 설정
def querydslDir = "$buildDir/generated/querydsl"
// JPA 사용 여부와 사용할 경로를 설정
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
// build 시 사용할 sourceSet 추가
sourceSets {
main.java.srcDir querydslDir
}
// querydsl 컴파일시 사용할 옵션 설정
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
// querydsl 이 compileClassPath 를 상속하도록 설정
configurations {
compileOnly {
}extendsFrom annotationProcessor
querydsl.extendsFrom compileClasspath
}
// gradle clean 시에 QClass 디렉토리 삭제
clean{
delete file(querydslDir)
}
- Entity
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class Users {
@Id
private Long userNo;
private String userId;
private String userName;
}
@Getter @Setter
@NoArgsConstructor
@RequiredArgsConstructor
public class Board {
@Id
private Long id;
private String title;
private String content;
}
- DTO
@Getter @Setter
@NoArgsConstructor
public class UsersDto {
private Long userNo;
private String userId;
private String userName;
@QueryProjection
public UsersDto(Long userNo, String userId, String userName) {
this.userNo = userNo;
this.userId = userId;
this.userName = userName;
}
}
- JpaQueryFactory Config
@Configuration
@RequiredArgsConstructor
public class QuerydslConfiguration {
private final EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(em);
}
}
#### 1. Projections.bean (Setter 메서드를 이용한 조회 방법)
@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {
private final JPAQueryFactory queryFactory;
public void findAll(){
List<UsersDto> users = queryFactory
.select(Projections.bean(UsersDto.class,
users.userNo,
users.userId,
users.userName
))
.from(users)
.fetch();
}
}
- Projection.bean 방법은 setter 메서드를 기반으로 동작합니다.
DTO 객체의 각 필드에 setter 메서드가 있어야 합니다.
만약 영속화된 데이터를 변경하는 책임을 가진 객체라면 문제가 될 수 있기 때문에 권장되는 패턴은 아닙니다.
#### 2. Projections.fields (Reflection을 이용한 조회 방법)
@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {
private final JPAQueryFactory queryFactory;
public void findAll(){
List<UsersDto> users = queryFactory
.select(Projections.fields(UsersDto.class,
users.userNo,
users.userId,
users.userName.as("userName")
))
.from(users)
.fetch();
}
}
- Porjections.fields를 이용한 방법은 field에 값을 직접 주입해주는 방식입니다.
Projections.bean 방식과 마찬가지로 Type이 다를 경우 매칭되지 않으며, 컴파일 시점에서는 에러를 잡지 못하고 런타임 시점에서 에러가 잡힙니다.
- 만약 Users Entity의 필드가 userName이 아니라면 users.userName.as("userName") 처럼 alias(별칭)을 사용하여 매핑 문제를 해결할 수 있습니다.
#### 3. Projections.constructor (생성자를 이용한 조회 방법)
@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {
private final JPAQueryFactory queryFactory;
public void findAll(){
List<UsersDto> users = queryFactory
.select(Projections.constructor(UsersDto.class,
users.userNo,
users.userId,
users.userName
))
.from(users)
.fetch();
}
}
- Projections.constructor는 생산자 기반 바인딩입니다. 생성자 기반 바인딩이기 때문에 객체의 불변성을 가져갈 수 있다는 장점이 있지만 바인딩 과정에서 문제가 생길 수 있습니다.
public static
return new ConstructorExpression(type, exprs);
}
- Projections의 constructor 메서드입니다. 해당 메서드를 보면 DTO 객체의 생성자에게 직접 바인딩하는 것이 아니라 Expression<?>... exprs 값을 넘기는 방식으로 작동됩니다.
따라서 값을 넘길 때 생성자와 필드의 순서를 일치시켜야 합니다.
필드의 개수가 적을 때는 문제가 되지 않지만 필드의 개수가 많아지는 경우 오류로 이어질 수 있습니다.
하지만 생성자를 이용한 방식은 필드명이 달라도 해당 순서에 위치한 필드의 타입만 서로 일치한다면 정상적으로 동작한다는 특징이 있습니다.
#### 4. @QueryProjection
DTO
@Getter @Setter
@NoArgsConstructor
public class UsersDto {
private Long userNo;
private String userId;
private String userName;
@QueryProjection
public UsersDto(Long userNo, String userId, String userName) {
this.userNo = userNo;
this.userId = userId;
this.userName = userName;
}
}
@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {
private final JPAQueryFactory queryFactory;
public void findAll(){
List<UsersDto> dtos = queryFactory
.select(new QUsersDto(
users.userNo,
users.userId,
users.userName
))
.from(users)
.fetch();
}
}
```
@QueryProjection을 이용하면 불변 객체 선언, 생성자를 그대로 사용할 수 있기 때문에 권장되는 패턴입니다.
(정확하게는 DTO의 생성자를 사용하는 것이 아니라 DTO 기반으로 생성된 QDTO 객체의 생성자를 사용하는 것입니다.)작동 방법은 먼저 DTO 생성자에 @QueryProjection 어노테이션을 추가하여 QType의 클래스를 생성하여 위의 예시 코드(UsersDto)와 같이 사용합니다. 이 방식은 new QDTO로 사용하기 때문에 런타임 에러뿐만 아니라 컴파일 시점에서도 에러를 잡아주고, 파라미터로 확인할 수 있다는 장점이 있습니다.
반면 해당 기능을 사용하려는 DTO마다 QType의 Class를 추가로 생성해줘야 하는 것도 있지만, DTO는 Repository 계층의 조회 용도뿐만 아니라 Service, Controller 모두 사용되기 때문에 @QueryProjection 어노테이션을 DTO에 적용시키는 순간, 모든 계층에서 쓰이는 DTO가 Querydsl에 의존성을 가지기 때문에 아키텍처적으로 적용을 생각해 볼 필요는 있습니다.
'개발 > Spring' 카테고리의 다른 글
AOP란? AOP 내용, AOP 예제 (0) | 2024.01.11 |
---|---|
응집도와 결합도란? (1) | 2023.12.26 |
OAuth2란? OAuth2 예시 (1) | 2023.12.15 |
DTO <-> Entity 변환 이유! (0) | 2023.12.14 |