Notice
Recent Posts
Recent Comments
Link
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Tags
more
Archives
Today
Total
관리 메뉴

che01 님의 블로그

QueryDSL 본문

카테고리 없음

QueryDSL

che01 2025. 6. 25. 21:03
  • 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);
}

학습하면서 깨달은 중요한 점들

  1. Entity 수정 시 compileQuerydsl 실행 필수 - 그렇지 않으면 Q클래스가 업데이트되지 않는다
  2. fetch() 메서드 종류 구분하기
    • fetch(): 리스트로 조회
    • fetchOne(): 단건 조회 (결과가 여러 개면 예외 발생)
    • fetchFirst(): 첫 번째 결과만 조회
  3. 조인 시 fetch join 사용하기 - N+1 문제 방지
  4. 서브쿼리는 from절에 사용할 수 없다 - JPA의 한계

실제 프로젝트 적용 후기

검색 기능 구현 시 정말 유용했다. 이전에는 MyBatis로 XML에서 <if> 태그를 남발하거나 JPA로 긴 메서드명을 만들어야 했는데, QueryDSL 도입 후 코드가 훨씬 깔끔해졌다.

특히 복합 검색 조건이 많은 관리자 페이지 개발 시 그 진가를 발휘했다. 10개 이상의 검색 조건도 쉽게 처리할 수 있었다.

향후 학습 계획

  • DTO 프로젝션 활용법 익히기
  • 복잡한 조인 쿼리 연습하기
  • 성능 최적화 방법 학습하기