반응형
QueryDSL이 뭐야?
- 자바 및 코틀린에서 객체지향적으로 SQL / JPQL 쿼리를 작성할 수 있도록 돕는 쿼리 빌더 라이브러리다.
- 작성자는 동적 쿼리를 처리하기 위해 QueryDSL을 도입했다.
- 가령 공지사항에서 게시물을 조회할 때 사용자는 제목 조건을 포함하여 조회할 수도 있고, 포함하지 않을 수도 있다.
- 이러한 상황을 처리하기 위한 것이 동적 쿼리이다.
- JPA에서도 Criteria API를 제공하여 동적 쿼리를 처리할 수 있게 하지만 문법이 굉장히 복잡하다.
- 코드 유지보수성도 개발에 있어 굉장히 중요하기 때문에 다른 방안이 필요했다.
- SQL Mapper Framework MyBatis를 통해 동적 쿼리를 처리할 수도 있다.
- 현업에서는 아마 MyBatis를 더 많이 쓸 것이다.
- 그 이유로는 직접 SQL을 작성하는 방식이 직관적이고 러닝 커브가 낮아 유지보수하기 좋기 때문이다.
- 그런데 본인은 MyBatis를 쓰면서 쿼리가 잘 작성되었는지 확인하기 위해 서버를 재시작하고, 사이트에 접속하고, 해당 기능을 눌러보는 것에 불편함을 느꼈다.
- 반면 QueryDSL은 컴파일 레벨에서 필드명이나 타입 작성 오류 등을 잡을 수 있다.
- 개발자가 컴파일하면 QueryDSL은 반환 DTO나 엔티티 따위에 해당하는 Q클래스를 만들고, 해당 클래스를 통해 컴파일 타임에서 오류를 검출한다.
동작 흐름을 알아보자
/* QueryDSL */
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
- 먼저 의존성을 주입하자. 작성자는 빌드 도구로 gradle을 사용한다.
@QueryProjection
public MemberResponseDto(String email, String name, String description, LocalDateTime createdAt, LocalDateTime modifiedAt) {
this.email = email;
this.name = name;
this.description = description;
this.createdAt = createdAt;
this.modifiedAt = modifiedAt;
}
- DB 값을 조회하고서 클라이언트로 엔티티를 반환하는 건 보안 측면에서 위험하다고 알고 있다.
- 물론 기능에 따라 요구사항도 다르기 때문에 유지보수 문제도 있다.
- 그렇기 때문에 반환 DTO를 작성할 텐데, 이러한 반환 DTO에 @QueryProjection 애노테이션을 명시하여 QueryDSL 라이브러리로 하여금 Q클래스를 생성할 수 있도록 유도해야 한다.
import static kr.co.co_working.invitation.QInvitation.invitation;
import static kr.co.co_working.member.QMember.member;
import static kr.co.co_working.memberWorkspace.QMemberWorkspace.memberWorkspace;
import static kr.co.co_working.workspace.QWorkspace.workspace;
@Repository
@RequiredArgsConstructor
public class MemberDslRepositoryImpl implements MemberDslRepository {
private final JPAQueryFactory factory;
@Override
public List<MemberResponseDto> readMemberListNotInWorkspace(WorkspaceRequestDto.READ dto) {
return factory
.select(
new QMemberResponseDto(
member.email,
member.name,
member.description,
member.createdAt,
member.modifiedAt,
invitation.status
)
).from(member)
.leftJoin(invitation).on(invitation.member.eq(member))
.where(
member.email.notIn(
JPAExpressions
.select(memberWorkspace.member.email)
.from(memberWorkspace)
.where(memberWorkspace.workspace.id.eq(dto.getId()))
)
).where(
emailContains(dto.getEmail()),
nameContains(dto.getName())
)
.fetch();
}
private BooleanExpression emailContains(String emailCond) {
return emailCond != null && !emailCond.trim().isEmpty() ? member.email.contains(emailCond) : null;
}
private BooleanExpression nameContains(String nameCond) {
return nameCond != null && !nameCond.trim().isEmpty() ? member.name.contains(nameCond) : null;
}
}
- 요런 식으로 메서드 체이닝 방식을 사용한다.
- 메인 쿼리에는 JPAQueryFactory를, 서브쿼리에는 JPAExpressions를 사용하면 된다.
- 조심해야 할 부분은 memberWorkspace.workspace.id 이런 식으로 조건에 넣게 되면 내부적으로 조인을 수행한다고 한다.
- 내부적으로 조인을 수행한다면 내가 의도한 대로 쿼리가 생성되지 않을 수도 있기 때문에 명시적 조인을 사용하는 게 좋을 것 같은데, 본인도 아직 서버 코드 최적화는 신경 쓸 겨를이 없어서 추후 성능 튜닝을 해보고자 한다.
- 참고로, queryDSL은 엔티티를 감지하여 Q클래스를 생성하는데, 이를 레포지토리에서 변수로 사용하기 위해 static import문을 써야 한다.
아직 복잡한 쿼리는 작성해보지 않았는데 익숙해지기에 시간이 좀 걸릴 것 같다. 성능 테스트도 아직 못 해보았다.
사용하면서 알게 된 특징들
- 조회 컬럼에 null을 반환할 수 없다.
- 조인 시 다른 타입의 엔티티를 분기칠 수 없다.
- 조인 시 다른 타입의 엔티티를 분기칠 수 없다는 것은 다음을 의미한다.
// ...
// 컴파일 오류
.leftjoin(flag ? member : workspace)
// ...
- 특정 조건에 따라 조인 엔티티를 분기치고 싶다면 아예 쿼리를 나눠야 한다.
- 이는 컴파일 시점에서 leftJoin() 메서드에 전달되는 인자가 동일한 타입이어야 하기 때문이라고 한다.
마치며
아직은 직접 SQL을 작성하는 MyBatis가 더 편하다.
그럼에도 컴파일 단계에서 오류 검출이 가능한 건 꽤 큰 이점이라고 생각하며, 토이 프로젝트에는 계속해서 사용하고 튜닝해 나갈 예정이다. MyBatis랑 QueryDSL 둘 다 쓸 수도 있다는데 그렇게 하면 유지보수하기 힘들 것 같다. 정말 복잡한 쿼리가 필요한 그때에도 QueryDSL을 유지할 수 있을지는 아직 경험이 부족해서 모르겠다.
요즘 2025년에는 많은 것이 급변할 것 같다는 생각이 든다. 당장 3개월 뒤에도 나는 웃고 있을까, 인상 쓰고 있을까 🤤
이미지 출처
Querydsl
Querydsl has 9 repositories available. Follow their code on GitHub.
github.com
'개발 > 스프링' 카테고리의 다른 글
[Trouble-shooting] 중첩 클래스명 중복으로 Swagger가 고장났다 (2) | 2025.03.21 |
---|---|
Faker가 만들어 주는 테스트 데이터, 또 나만 몰랐지 (3) | 2025.03.19 |
[Trouble-shooting] MyBatis는 카멜케이스 설정을 직접 해야 한다 (2) | 2025.03.18 |
Swagger씨, API 문서 작성해주세요 ! (2) | 2025.03.06 |
P6Spy를 도입하여 쿼리 로깅을 개선해보자 (2) | 2025.03.06 |