Backend/JPA

QueryDsl - 프로젝션을 DTO 결과로 반환하는 방법과 개념

개발자-제이 2025. 3. 17. 01:08

 

1. 프로젝션(Projection)이란?

프로젝션(Projection)은 쿼리 결과에서 특정 필드만 선택하여 반환하는 기법을 의미한다.
JPA에서 엔티티 전체를 조회하는 것이 아니라, 필요한 데이터만 선택하여 DTO로 변환하는 것이 대표적인 프로젝션 방식이다.

프로젝션을 사용하는 이유

  • 불필요한 데이터 조회 방지 → 성능 최적화
  • 비즈니스 요구 사항 반영 → 특정 데이터 구조로 변환하여 반환
  • 데이터 보호 → 민감한 정보가 포함된 엔티티 전체 조회 방지

 

2. 순수 JPA에서 DTO 조회

DTO 클래스 정의

@Data
public class MemberDto {
    private String username;
    private int age;

    public MemberDto() {}

    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

순수 JPA DTO 조회 (new 키워드 사용)

List<MemberDto> result = em.createQuery(
    "select new com.example.dto.MemberDto(m.username, m.age) " +
    "from Member m", MemberDto.class)
    .getResultList();
  • 패키지명을 직접 입력해야 하므로 코드가 길어지고 가독성이 떨어짐
  • 생성자 방식만 지원하여 필드가 많아질 경우 불편함

 

3. QueryDSL에서 DTO 조회

QueryDSL에서는 DTO 조회를 더 편리하게 하기 위해 3가지 방식을 제공한다.

 

3.1 프로퍼티 접근 (Setter 방식)

List<MemberDto> result = queryFactory
    .select(Projections.bean(MemberDto.class,
        member.username,
        member.age))
    .from(member)
    .fetch();

 

  • 장점: Setter를 활용하여 DTO 값을 설정하므로 유지보수에 유리
  • 단점: Setter가 필요한 경우 불변 객체패턴을 적용하기 어려움

 

3.2 필드 직접 접근 (Field 방식)

List<MemberDto> result = queryFactory
    .select(Projections.fields(MemberDto.class,
        member.username,
        member.age))
    .from(member)
    .fetch();
  • 장점: Setter 없이 필드에 직접 주입 가능
  • 단점: 캡슐화 위반 가능성이 있음

 

3.3 생성자 방식 (Constructor 방식)

List<MemberDto> result = queryFactory
    .select(Projections.constructor(MemberDto.class,
        member.username,
        member.age))
    .from(member)
    .fetch();
  • 장점: 불변 객체(Immutable Object) 사용 가능
  • 단점: 생성자의 매개변수 개수가 많아질 경우 가독성이 떨어짐

 

4. DTO 필드명이 다를 경우 처리

DTO 필드명이 엔티티 필드명과 다를 경우, 매핑이 되지 않는 문제가 발생할 수 있다.

해결 방법 (별칭 활용)

List<UserDto> fetch = queryFactory
    .select(Projections.fields(UserDto.class,
        member.username.as("name"), // username → name 매핑
        ExpressionUtils.as( 
            JPAExpressions
                .select(memberSub.age.max())
                .from(memberSub), "age") // 서브쿼리 결과를 age에 매핑
    ))
    .from(member)
    .fetch();
  • 필드명이 다를 경우 .as("새 필드명")을 활용하여 매핑 가능

 

 

5. @QueryProjection을 활용한 DTO 조회

@QueryProjection을 활용하면 컴파일 타임에서 타입 체크가 가능하여 안전성을 확보할 수 있다.

 

DTO에 @QueryProjection 적용

@Data
public class MemberDto {
    private String username;
    private int age;

    public MemberDto() {}

    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

QueryDSL에서 @QueryProjection 활용

List<MemberDto> result = queryFactory
    .select(new QMemberDto(member.username, member.age))
    .from(member)
    .fetch();
  • 장점: 타입 안정성을 확보할 수 있음
  • 단점: QueryDSL 어노테이션 의존성이 증가하고 Q 파일을 생성해야 함

 

6. 비교 정리

방식 특징 장점 단점
순수 JPA new 키워드 생성자 기반 DTO 매핑 간단한 방식 패키지 경로 입력 필요
QueryDSL Projections.bean() Setter 기반 매핑 유지보수 용이 불변 객체 사용 어려움
QueryDSL Projections.fields() 필드 직접 할당 Setter 없이 활용 가능 캡슐화 위반 가능성
QueryDSL Projections.constructor() 생성자 기반 매핑 불변 객체 사용 가능 생성자 매개변수 많아질 경우 가독성 저하
@QueryProjection 컴파일 타임 타입 체크 타입 안정성 보장 Q 파일 생성 필요
반응형