Projection 연산이란?

한 Relation의 Attribute들의 부분 집합을 구성하는 연산자입니다.
결과로 생성되는 Relation은 스키마에 명시된 Attribute들만 가집니다.
결과 Relation은 기본 키가 아닌 Attribute에 대해서만 중복된 tuple들이 존재할 수 있습니다.
-> 테이블에서 원하는 컬럼만 뽑아서 조회하는 것

  • Relation 데이터를 원자 값으로 갖는 이차원 테이블
  • Column = Attribute

Querydsl DTO로 조회하는 4가지 방법

  1. Projection.bean
  2. Projection.fields
  3. Projection.constructor
  4. @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 ConstructorExpression constructor(Class type, Expression... exprs){
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

+ Recent posts