che01 님의 블로그
QueryDSL 본문
- MyBatis나 JPA 기본 쿼리로 복잡한 검색 조건 만들기가 너무 어려웠다
- 문자열로 쿼리 작성하다가 오타가 나면 실행해봐야 알 수 있어서 불편했는데, QueryDSL은 컴파일 시점에 오류를 잡아준다
- 동적 쿼리 작성이 훨씬 편리하다
처음 설정하면서 겪은 시행착오들
build.gradle에 의존성 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0'
Q클래스 생성을 위한 설정 (필수)
def querydslDir = "$buildDir/generated/querydsl"
sourceSets {
main.java.srcDirs += [querydslDir]
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
중요한 점: Entity 생성 후 반드시 ./gradlew compileQuerydsl 실행해야 Q클래스가 생성된다. 이 부분을 몰라서 한참 헤맸다.
기본 사용법
Configuration 등록
@Configuration
public class QuerydslConfig {
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
Repository에서 사용
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final JPAQueryFactory queryFactory;
public List<Member> findByName(String name) {
return queryFactory
.selectFrom(member)
.where(member.name.eq(name))
.fetch();
}
}
자주 사용하는 조건들
// 같다/다르다
member.name.eq("홍길동") // =
member.name.ne("홍길동") // !=
// 크다/작다
member.age.gt(20) // >
member.age.goe(20) // >=
member.age.lt(30) // <
member.age.loe(30) // <=
// 문자열 검색
member.name.like("홍%") // like
member.name.contains("길") // %길%
member.name.startsWith("홍") // 홍%
// 여러 값 중에서 선택
member.age.in(20, 30, 40)
member.age.between(20, 30)
동적 쿼리 작성
처음에는 BooleanBuilder를 사용했지만, 나중에 BooleanExpression이 더 깔끔하다는 것을 알게 되었다.
BooleanBuilder 방식
public List<Member> search(String name, Integer age) {
BooleanBuilder builder = new BooleanBuilder();
if (name != null) {
builder.and(member.name.contains(name));
}
if (age != null) {
builder.and(member.age.goe(age));
}
return queryFactory
.selectFrom(member)
.where(builder)
.fetch();
}
BooleanExpression 방식 (더 깔끔함)
public List<Member> search(String name, Integer age) {
return queryFactory
.selectFrom(member)
.where(
nameContains(name),
ageGoe(age)
)
.fetch();
}
private BooleanExpression nameContains(String name) {
return name != null ? member.name.contains(name) : null;
}
private BooleanExpression ageGoe(Integer age) {
return age != null ? member.age.goe(age) : null;
}
페이징 처리
public Page<Member> findMembersPage(Pageable pageable) {
List<Member> content = queryFactory
.selectFrom(member)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long total = queryFactory
.select(member.count())
.from(member)
.fetchOne();
return new PageImpl<>(content, pageable, total);
}
학습하면서 깨달은 중요한 점들
- Entity 수정 시 compileQuerydsl 실행 필수 - 그렇지 않으면 Q클래스가 업데이트되지 않는다
- fetch() 메서드 종류 구분하기
- fetch(): 리스트로 조회
- fetchOne(): 단건 조회 (결과가 여러 개면 예외 발생)
- fetchFirst(): 첫 번째 결과만 조회
- 조인 시 fetch join 사용하기 - N+1 문제 방지
- 서브쿼리는 from절에 사용할 수 없다 - JPA의 한계
실제 프로젝트 적용 후기
검색 기능 구현 시 정말 유용했다. 이전에는 MyBatis로 XML에서 <if> 태그를 남발하거나 JPA로 긴 메서드명을 만들어야 했는데, QueryDSL 도입 후 코드가 훨씬 깔끔해졌다.
특히 복합 검색 조건이 많은 관리자 페이지 개발 시 그 진가를 발휘했다. 10개 이상의 검색 조건도 쉽게 처리할 수 있었다.
향후 학습 계획
- DTO 프로젝션 활용법 익히기
- 복잡한 조인 쿼리 연습하기
- 성능 최적화 방법 학습하기