-
QueryDsl projections 자바 Record에 적용하기Spring Boot/JPA 2024. 4. 23. 01:58
QueryDsl을 이용하는 경우
반환 타입이 Entity가 아닌 다른 응답 객체(DTO)일때 Projection 기능을 제공해서 원하는 응답 객체로 변환할 수 있다.- Projections.bean
- Projections.constructor
- Projections.fields
- @QueryProjection
아니면 projection 기능을 포기하고 변환하는 로직을 작성해야하는데,
데이터 size가 크면 성능 이슈가 있을 수 있다.
위 4개 기능들의 사용법과, 장단점을 비교해보자.
결론부터 말하자면 Record는 Projections.constructor로만 데이터 바인딩이 가능하다.
constructor 방식은 버그를 만들수 있어서, QueryDSL 응답객체는 Record 대신 class를 이용하고 fields projection이 최선인 것 같다.
1. 내 경우 DTO에도 정적 팩토리 메서드 같은 걸 만들곤 한다. 이때 기본생성자를 protected으로 숨기는데 projection.bean은 기본생성자가 필수라 문제 여지가 있다.
2. DTO는 객체라기 보다 자료구조에 가까워서 Setter/Getter, 기본생성자 달고 하는 것에 문제는 없다 생각하지만, Setter를 왠만하면 안쓰고 싶은 개인의 취향이다.
Projections.bean(x.class, x.field, x.field . . .)
- 의존성 주입의 수정자 주입(Setter) 방식으로 동작한다.
- UserResponse.class에 Setter, Getter, 기본생성자가 필수로 필요하다. (Setter 주입이니 당연)
public UserResponse fetchUserByLoginId(String loginId) { return query.select( Projections.bean( UserResponse.class, jpaUserEntity.id.as("id"), jpaUserEntity.loginId.as("loginId"), jpaUserEntity.username.as("username"), jpaUserEntity.userState.as("userState"), jpaUserEntity.createdAt.as("createdAt"), jpaUserEntity.updatedAt.as("updatedAt") ) ) .from(jpaUserEntity) .where(jpaUserEntity.loginId.eq(loginId) .and(jpaUserEntity.state.eq(EntityStateType.ACTIVE))) .fetchOne(); }
- 단점
- Java17 Record나 final이 붙은 불변 객체는 사용할 수 없다. (setter 사용이 불가하기 때문)
Projections.constructor(x.class, x.field, x.field . . .)
- 생성자 주입 방식으로 데이터가 바인딩되는 패턴
- final이 선언된 불변객체도 지원된다. (Record에도 적용할 수 있다.)
public UserResponse fetchUserByLoginId(String loginId) { return query.select( Projections.constructor( UserResponse.class, jpaUserEntity.id.as("id"), jpaUserEntity.loginId.as("loginId"), jpaUserEntity.username.as("username"), jpaUserEntity.userState.as("userState"), jpaUserEntity.createdAt.as("createdAt"), jpaUserEntity.updatedAt.as("updatedAt") ) ) .from(jpaUserEntity) .where(jpaUserEntity.loginId.eq(loginId) .and(jpaUserEntity.state.eq(EntityStateType.ACTIVE))) .fetchOne(); }
- 단점
- 생성자 기반이기 때문에 UserResponse.class 뒤에 따라오는 field의 순서가 생성자의 파라미터와 정확히 일치해야한다.
- type만 검증되어서 실수로 순서를 바꿔 입력하여 치명적인 버그를 만들 수 있다.
Projections.fields(x.class, x.field, x.field . . .)
- 필드 주입(@Autowired) 방식으로 동작한다.
- getter, setter 없이도 필드명만 일치하면 데이터 바인딩을 할 수 있다.
public UserResponse fetchUserByLoginId(String loginId) { return query.select( Projections.fields( UserResponse.class, jpaUserEntity.id.as("id"), jpaUserEntity.loginId.as("loginId"), jpaUserEntity.username.as("username"), jpaUserEntity.userState.as("userState"), jpaUserEntity.createdAt.as("createdAt"), jpaUserEntity.updatedAt.as("updatedAt") ) ) .from(jpaUserEntity) .where(jpaUserEntity.loginId.eq(loginId) .and(jpaUserEntity.state.eq(EntityStateType.ACTIVE))) .fetchOne(); }
- 단점
- final이 선언된 불변 객체에는 사용할 수 없다.
@QueryProjection
- 응답 객체(ex DTO)도 QClass로 만들어 타입 검증을 하는 방식이다.
- 응답 객체 생성자 위에 @QueryProjection를 선언하고 빌드하는 식으로 QClass 제작
public UserResponse fetchUserByLoginId(String loginId) { return query.select( Projections.fields( new QUserResponse( jpaUserEntity.id, jpaUserEntity.loginId, jpaUserEntity.username, jpaUserEntity.userState, jpaUserEntity.createdAt, jpaUserEntity.updatedAt ) ) .from(jpaUserEntity) .where(jpaUserEntity.loginId.eq(loginId) .and(jpaUserEntity.state.eq(EntityStateType.ACTIVE))) .fetchOne(); }
- 단점
- 응답 객체에 QueryDSL 의존성이 생겨난다.
참고자료
- 쿼리디에스엘 record projections 문의 https://github.com/querydsl/querydsl/issues/3520- 성능 테스트 레퍼런스 https://velog.io/@ktk8916/JPA-QureyDsl-DTO-return-record%EC%97%90%EC%84%9C-QueryProjection-%EC%82%AC%EC%9A%A9
- 내부 동작 문법정리 https://cheese10yun.github.io/querydsl-projections/
반응형'Spring Boot > JPA' 카테고리의 다른 글
2024 - QueryDSL 근황 (0) 2024.08.22 JPA 주의점 (1) - OSIV false 설정 (open-in-view) (0) 2024.04.19 [JPA] 간단하게 OneToMany 데이터 API 구현하기 - @Embeddable, @Embedded, @ElementCollection 활용하기 (1) 2023.10.15 [JPA] Repository단에 Transactional을 선언하는 이유 (0) 2023.10.05 JPA 애플리케이션 데이터베이스 초기화 (0) 2023.09.17