Spring/Spring 문법

QueryDsl - Projections 의 4가지 방식

열심히 해 2024. 11. 21. 13:05

QueryDsl - Projections

 

QueryDsl 을 사용해서 엔티티 전체를 조회하는 것이 아니라 특정 대상만 조회하는 것을 말합니다.

 

 

@Entity
@Table(name = "team")
@Getter
@NoArgsConstructor
public class Team extends Timestamped {
    @Id
    private Long id;
    private String name;
    private Integer trophyCount;
}
@Entity
@Table(name = "member")
@Getter
@NoArgsConstructor
public class Member extends Timestamped {
    @Id
    private Long id;
    private String name;
    private Integer age;
    private Long teamId;
}
@Data
@NoArgsConstructor
public class TeamMemberDto {

    private Long teamId;
    private String teamName;
    private Integer trophyCount; // 소속팀의 우승 횟수 입니다.
    private Long memberId;
    private String memberName;
    private Integer memberAge;
    
}

 

Team 과 Member 을 Join 하여 '팀 + 선수' 에 관한 정보를 한 번에 조회하고 싶다고 가정하겠습니다.

 

 

 

1. Projections.fields 

: Projections.fields(DTO.class, ...) 방식은 필드에 직접 접근하여 값을 주입합니다.

 

public List<TeamMemberDto> findTeamMember() {
    return queryFactory
        .select(Projections.fields(TeamMemberDto.class,
            team.id.as("teamId"),
            team.name.as("teamName"),
            team.trophyCount,
            member.id.as("memberId"),
            member.name.as("memberName"),
            member.age.as("memberAge")
        ))
        .from(team)
        .leftJoin(member).on(team.id.eq(member.teamId))
        .fetch();
}

 

특징 및 장점

  • 직접 필드 접근: DTO의 필드를 리플렉션으로 직접 접근하여 값을 주입하므로, 세터 메서드가 필요하지 않습니다.
  • 객체 초기화 간편함: 필드 주입만으로 값을 할당하기 때문에 코드가 간결하고, DTO 클래스가 간단할 경우 유용합니다.
  • 성능: 세터 호출 오버헤드가 없으므로, 약간의 성능 이점이 있을 수 있습니다.
  • 기본 생성자 필요: 리플렉션을 통해 인스턴스를 생성하므로 기본 생성자가 필요합니다.

단점

  • 필드 접근 제어 문제: 필드를 public으로 열어두거나 protected 이상으로 두어야 하므로, 캡슐화 원칙이 일부 깨질 수 있습니다.
  • 필드 접근 제한: final 필드에는 값을 할당할 수 없기 때문에, 상수 또는 불변 필드를 사용하는 경우 문제가 발생합니다.

 

 

 

2. Projections.bean

: Projections.bean(DTO.class, ...) 방식은 세터 메서드를 이용해 필드에 값을 주입합니다.

 

public List<TeamMemberDto> findTeamMember() {
    return queryFactory
        .select(Projections.bean(TeamMemberDto.class,
            team.id.as("teamId"),
            team.name.as("teamName"),
            team.trophyCount,
            member.id.as("memberId"),
            member.name.as("memberName"),
            member.age.as("memberAge")
        ))
        .from(team)
        .leftJoin(member).on(team.id.eq(member.teamId))
        .fetch();
}

 

특징 및 장점

  • 세터 기반 주입: 각 필드에 대해 세터 메서드를 호출해 값을 주입하므로, 필드를 private로 유지하면서도 캡슐화를 지킬 수 있습니다.
  • 가독성: 필드 주입보다는 메서드 기반 주입이 가독성이 좋을 수 있으며, 유지보수에도 유리할 수 있습니다.
  • 기본 생성자 필요: 기본 생성자를 통해 객체를 생성한 후 세터로 값을 주입하기 때문에, 별도의 파라미터를 가진 생성자가 필요하지 않습니다.

단점

  • 세터 메서드 필요: 세터 메서드를 정의해야 하므로, DTO 클래스에 세터가 없거나 세터를 원하지 않는 경우 사용할 수 없습니다.
  • 추가 메서드 호출로 인한 약간의 성능 오버헤드: 각 필드에 대해 세터 메서드를 호출하므로 필드에 직접 접근하는 것보다 약간의 성능 오버헤드가 있을 수 있습니다.

 

 

위 두 코드를 비교하면 필터 체인으로 되어있는 부분이 ** Projections.fields**와 **  Projections.bean ** 을 제외하고는 동일하다는 것을 알 수 있습니다. 하지만 동작 방식에 차이가 있습니다. 성능 상의 이점과 세터 메서드를 사용하지 않으려는 경우는 전자를, 캡슐화가 중요하다면 후자를 선택하면 될 것 같습니다.

 

 

 

3. Projections.constructor

: 생성자를 통해 필드에 값을 할당합니다. TeamMemberDto가 모든 필드를 받는 생성자를 갖고 있어야 합니다..

 

public List<TeamMemberDto> findTeamMember() {
    return queryFactory
        .select(Projections.constructor(TeamMemberDto.class,
            team.id,
            team.name,
            team.trophyCount,
            member.id,
            member.name,
            member.age
        ))
        .from(team)
        .leftJoin(member).on(team.id.eq(member.teamId))
        .fetch();
}
// Projections.constructor 방식에서 사용하는 생성자
public TeamMemberDto(Long teamId, String teamName, Integer trophyCount,
                     Long memberId, String memberName, Integer memberAge) {
    this.teamId = teamId;
    this.teamName = teamName;
    this.trophyCount = trophyCount;
    this.memberId = memberId;
    this.memberName = memberName;
    this.memberAge = memberAge;
}

 

특징 및 장점

  • 생성자 기반 주입: 생성자를 통해 필요한 필드를 주입하여 DTO를 생성하므로, 불변 객체나 final 필드가 있는 DTO를 사용하는 데 적합합니다.
  • 필드 순서와 타입 일치 필요: 생성자 파라미터 순서와 타입이 정확히 일치해야 하므로, 타입 안전성 측면에서 상대적으로 안전하지만 필드 불일치 위험도 존재합니다.
  • 설정 및 의존성 불필요: DTO에 별도의 애너테이션이나 설정을 추가할 필요가 없어, QueryDSL에 종속되지 않은 DTO를 사용할 수 있습니다.
  • 기본 생성자 불필요: 생성자를 통해 값을 주입하기 때문에, DTO 클래스에 기본 생성자를 필요로 하지 않습니다.

단점

  • 컴파일 타임 검증 불가: Projections.constructor는 필드 타입 및 순서가 맞지 않으면 런타임 에러를 발생시키므로, 오류 발생 위치를 추적하기 어려울 수 있습니다.
  • 생성자 순서와 필드 수 관리 필요: 필드 순서가 중요하므로 DTO 필드가 많아질수록 관리가 어려워지고, 가독성이 떨어질 수 있습니다.
  • 의존성 없는 DTO 수정 어려움: QueryDSL에 종속되지 않은 DTO를 사용하더라도, 필드 추가 및 수정 시 필드 순서와 타입을 맞춰야 하는 부담이 큽니다.

 

 

4. @QueryProjection

: @QueryProjection을 사용하려면 TeamMemberDto 클래스의 모든 필드를 받는 생성자에 @QueryProjection 애너테이션을 추가해야 합니다.

 

public List<TeamMemberDto> findTeamMember() {
    return queryFactory
        .select(new QTeamMemberDto(
            team.id,
            team.name,
            team.trophyCount,
            member.id,
            member.name,
            member.age
        ))
        .from(team)
        .leftJoin(member).on(team.id.eq(member.teamId))
        .fetch();
}

 

// @QueryProjection이 적용된 생성자
@QueryProjection
public TeamMemberDto(Long teamId, String teamName, Integer trophyCount,
                     Long memberId, String memberName, Integer memberAge) {
    this.teamId = teamId;
    this.teamName = teamName;
    this.trophyCount = trophyCount;
    this.memberId = memberId;
    this.memberName = memberName;
    this.memberAge = memberAge;
}

 

특징 및 장점

  • 파일 타임 타입 안전성: @QueryProjection은 DTO 생성자를 통해 필드 주입이 이루어지며, QueryDSL 코드 생성기를 통해 컴파일 시점에 타입과 필드 매핑을 검증합니다.
  • IDE 지원: DTO가 @QueryProjection을 사용하면 QDTOClass로 자동 생성되어, QueryDSL 쿼리 내에서 IDE의 자동 완성과 같은 편리한 지원을 받을 수 있습니다.
  • 간결한 코드: QDTOClass를 직접 사용할 수 있기 때문에 Projections를 호출하는 방식보다 코드가 간결해집니다.
  • 유지보수성: 컴파일 타임 검증을 통해 필드 추가, 삭제 시 DTO와 쿼리 간 불일치 문제를 미리 확인할 수 있어, 유지보수에 유리합니다.

단점

  • QueryDSL 의존성 추가: @QueryProjection을 사용하면 DTO 클래스가 QueryDSL에 종속되며, QueryDSL 코드 생성기를 설정하고 빌드해야 합니다.
  • 코드 생성 필요: @QueryProjection을 사용하려면 QueryDSL 코드 생성기를 통해 QDTOClass를 빌드해야 하므로, 빌드 설정과 생성된 코드를 관리해야 합니다.
  • 유연성 저하: QueryDSL에 의존하므로 다른 환경에서 DTO를 사용하거나 QueryDSL을 사용하지 않는 프로젝트에서는 활용이 제한될 수 있습니다.

 

 

  • **Projections.constructor**는 의존성 없는 DTO를 사용할 수 있고, 기본 생성자가 필요 없으며, 필드의 순서와 타입이 일치하면 사용할 수 있습니다. 하지만 런타임 오류 발생 가능성이 크므로, 필드가 많아지면 관리가 어려워집니다.
  • **@QueryProjection**은 타입 안전성을 보장하며 컴파일 타임 검증이 가능해 유지보수성이 높지만, QueryDSL에 대한 의존성이 추가되므로 코드 생성 및 설정이 필요합니다.

 

**Projections.constructor**과 **@QueryProjection**의 코드도 상당 부분 비슷합니다. 하지만 전자에서는 DTO와 QueryDsl가 독립적이지만 런타임 오류의 위험이 있습니다. 후자에서는 DTO가 QueryDsl에 의존적-QueryDsl 전용 DTO가 되지만 컴파일 타임 검증이 가능해 집니다.

 

 

 

 

'Spring > Spring 문법' 카테고리의 다른 글

AWS S3 버킷 사용하기  (0) 2024.11.25
Transaction Propagation  (0) 2024.11.25
Spring Security 적용  (0) 2024.11.14
QueryDSL이란 무엇일까??  (0) 2024.11.14
@RequestParam 에서 매개 변수에 null 넣기  (0) 2024.11.14