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 |