프로젝션 결과 반환은 데이터베이스로 부터 조회된 데이터를 어플리케이션에서 사용하기 위한 특정 형식의 객체나 구조를 매핑하는 과정입니다. 이 과정을 통해, 전체 데이터 대신 필요한 데이터만 포함하는 객체를 반환할 수 있습니다.
단일 필드 프로젝션
단일 필드를 조회할 경우는 해당 필드의 type를 선언하면 됩니다.
@Test
void 단일_필드_프로젝션() {
List<String> result = queryFactory
.select(student.studentName)
.from(student)
.where(student.score.goe(10))
.fetch();
assertThat(result.size()).isEqualTo(4);
}
다중 필드 프로젝션 Tuple 사용하기
다중 필드를 조회하려면, Tuple를 사용하거나 DTO를 사용할 수 있습니다. 먼저 Tuple 사용 방법에 대해 알아보겠습니다.
Tuple를 사용하면 여러 필드를 한 번에 조회할 수 있습니다. 각 필드는 tuple.get 메서드를 통해 접근할 수 있습니다.
@Test
void 다중_필드_프로젝션_TUPLE() {
List<Tuple> result = queryFactory
.select(student.studentName,
student.age,
student.count().as("cnt"),
student.count()
)
.from(student)
.where(student.score.goe(10))
.groupBy(student.studentName, student.age)
.fetch();
NumberPath<Long> cntPath = Expressions.numberPath(Long.class, "cnt");
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple.get(student.studentName));
System.out.println("tuple = " + tuple.get(student.age));
System.out.println("tuple.get(cntPath) = " + tuple.get(cntPath));
System.out.println("tuple.get(student.count()) = " + tuple.get(student.count()));
}
}
위 코드를 보면 집계 함수 student.count().as("cnt") 부분은 별칭으로 지정된 cnt를 Tuple에서 바로 꺼낼 수 없기 때문에 별칭을 사용하여 결과를 꺼내기 위해서는 Expressions 를 활용해 해당 필드에 대한 Expression을 생성하고 이를 get 메소드에 전달해야 합니다. 별칭이 없는 경우에는 student.count() 함수를 그대로 사용하여 꺼내야 합니다.
또한, Tuple를 사용하여 조회한 결과는 단순히 값을 집합으로 반환되며, 이 값들은 영속성 컨텍스트의 관리되지 않습니다. 이는 DTO를 사용할 경우도 마찬 가지 인데요. 자세한 내용은 아래에서 설명하겠습니다.
다중 필드 프로젝션 DTO 사용하기
Tuple 사용이 특정 상황에서 불편함을 줄 수 있다는 점을 고려한다면, DTO를 사용한는 것이 좋은 대안이 될 수 있습니다.
QueryDsl을 사용하여 DTO로 프로젝션 결과를 반환할 때, 필드(Field), 생성자(Constructor), 빈(Bean)을 통해 데이터를 매핑할 수 있습니다. 이 방법들은 데이터를 DTO로 변환하여 어플리케이션에서 사용하기 위한 방법입니다. Member 엔티티와 Team 엔티티가 있고 이둘 간의 관계는 N:1 이며 DTO로 아래 처럼 MemberDto가 있다고 가정하겠습니다.
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
private String teamName;
}
필드(Field)를 통한 반환
필드 방식은 DTO 필드에 직접 값을 할당하는 방식입니다. 이를 위해서는 DTO에 기본 생성자와 Setter가 필요합니다. QueryDsl에서는 Projections.fields() 메소드를 사용합니다.
@Test
void 다중_필드_프로젝션_FILED() {
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age,
team.name.as("teamName")
)
)
.from(member)
.join(member.team, team)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto.getUsername() = " + memberDto.getUsername());
System.out.println("memberDto.getAge() = " + memberDto.getAge());
System.out.println("memberDto.getTeamName() = " + memberDto.getTeamName());
}
}
생성자(Constructor)를 통한 반환
생성자 방식은 DTO에 값을 전달하기 위해 생성자를 사용합니다. 이 방법은 타입 안전성이 더 높고, 코드의 가독성이 좋아집니다. QueryDsl에서는 Projections.constructor() 메소드를 사용합니다.
// MemberDto.java 생성자 추가
public MemberDto(String username, int age, String teamName) {
this.username = username;
this.age = age;
this.teamName = teamName;
}
@Test
void 다중_필드_프로젝션_CONSTRUCTOR() {
List<MemberDto> result = queryFactory
.select(
Projections.constructor(MemberDto.class,
member.username,
member.age,
team.name.as("teamName")
)
)
.from(member)
.join(member.team, team)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto.getUsername() = " + memberDto.getUsername());
System.out.println("memberDto.getAge() = " + memberDto.getAge());
System.out.println("memberDto.getTeamName() = " + memberDto.getTeamName());
}
}
빈(Bean)을 통한 반환
빈 방식은 리플렉션을 사용하여 DTO의 setter 메소드를 통해 값을 할당합니다. 이 방법은 기본 생성자가 필요하며, 필드 이름을 기반으로 자동 매핑 됩니다. QueryDsl에서는 Projections.bean() 메소드를 사용합니다.
@Test
void 다중_필드_프로젝션_BEAN() {
List<MemberDto> result = queryFactory
.select(
Projections.bean(MemberDto.class,
member.username,
member.age,
team.name.as("teamName")
)
)
.from(member)
.join(member.team, team)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto.getUsername() = " + memberDto.getUsername());
System.out.println("memberDto.getAge() = " + memberDto.getAge());
System.out.println("memberDto.getTeamName() = " + memberDto.getTeamName());
}
}
각 방식은 사용 사례와 필요에 따르 선택할 수 있으며, 필드와 생성자 방식은 컴파일시간에 타입 안전성을 제공하는 반면, 빈 방식은 런타임 시 리플렉션을 사용합니다. 따라서, 성능과 가독성, 타입 안전성 중 어떤 것을 우선시하는지에 따라 적절한 방법을 선택할 수 있습니다.
QueryDsl과 영속성 컨텍스트
JPA에서 영속성 컨텍스트(Persistence Context)는 엔티티를 저장하는 환경을 말합니다. 이 컨텍스트는 엔티티의 생명주기를 관리하며, 엔티티의 상태 변화를 추적합니다. 영속성 컨텍스트를 통해 엔티티는 다음과 같은 이점을 얻습니다.
- 1차 캐시
- 동일성 보장
- 트랙잭션 지원
- 변경 감지
- 지연 로딩
QueryDsl은 복잡한 쿼리를 쉽게 구성하기 위한 프레임워크입니다. QueryDsl 쿼리의 결과를 엔티티 객체를 직접 반환 받으면, 이 엔티티는 영속성 컨텍스트에 의해 관리됩니다. 그러나 Tuple, DTO를 사용하는 경우는 다릅니다.
Tuple과 DTO는 단순 데이터 홀더입니다. Tuple은 여러 타입의 값을 하나의 객체에 담기 위해, DTO는 어플리케이션 계층 간 데이터 전송을 위해 사용됩니다. 이들 객체는 다음과 같은 이유로 영속성 컨텍스트에 의해 관리되지 않습니다.
- 엔티티의 부분 집합 : Tuple나 DTO는 종종 엔티티의 모든 정보가 아닌, 선택된 필드만 담으므로 JPA에서 관리하는 완전한 엔티티가 아닙니다.
- 타입의 불일치 : JPA 영속성 컨텍스트는 엔티티 클래스의 인스턴스를 관리합니다. Tuple 또는 DTO는 사용자가 정의한 특정 타입으로 JPA가 관리하는 타입과 일치하지 않습니다.
- 엔티티 생명주기 : 영속성 컨텍스트는 엔티티의 생명주기(영속, 준영속, 삭제 등) 관리합니다. Tuple 또는 DTO는 이러한 생명 주기의 일부가 아니며, 따라서 영속성 컨텍스트에 의해 관리 될 수 없습니다.
위와 같은 이유들로 영속성 컨텍스트에 의해 관리되지 않습니다. Tuple, DTO는 단순히 데이터 전송 객체이며, 엔티티의 전체 상태나 생명주기와 관련이 없기 때문입니다. 그러므로 Tuple, DTO는 변경감지나 지연로딩 같은 JPA 핵심 기능을 사용할 수 없으며, 데이터의 가공 및 전달에 초점을 맞춥니다.
'QueryDSL' 카테고리의 다른 글
QueryDsl에서 함수 사용 (0) | 2024.03.27 |
---|---|
QueryDsl 동적쿼리 작성 방법 (1) | 2024.03.26 |
QueryDsl 서브쿼리 (0) | 2024.03.20 |
QueryDsl 조인 (0) | 2024.03.20 |
Querydsl 기본 문법 (0) | 2024.03.15 |