QueryDSL 사용법

양성준·2025년 6월 2일

스프링

목록 보기
40/49

기본 설정

  • 의존성 추가
	implementation 'io.github.openfeign.querydsl:querydsl-jpa:6.11'
	annotationProcessor 'io.github.openfeign.querydsl:querydsl-apt:6.11:jpa'
	annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
	annotationProcessor 'jakarta.persistence:jakarta.persistence-api'

Querydsl은 2024년 부로 업데이트를 중단하여, SQL 인젝션과 관련한 취약점이 해결되지 않고있다.
다행히도, OpenFeign 팀에서 Querydsl을 지속적으로 업데이트 중이므로 해당 의존성을 가져다 쓰면 된다. (기존의 Querydsl과 사용방법 동일)

  • QueryDslConfig
package com.team1.monew.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QueryDslConfig {

  @PersistenceContext
  private EntityManager entityManager;

  @Bean
  public JPAQueryFactory jpaQueryFactory() {
    return new JPAQueryFactory(entityManager);
  }
}

QEntity 클래스

  • QEntity 클래스는 QueryDSL이 자동으로 만들어주는 "쿼리 전용 클래스".
    → 데이터베이스 테이블이나 JPA 엔티티를 Java 코드로 SQL처럼 다룰 수 있게 해줌
  • QEntity 클래스를 통해 자바코드로 SQL을 다루는 QueryDSL 문법을 사용할 수 있다.
    • 컴파일 시점에 에러를 잡을 수 있고, IDE 자동 완성도 가능해짐!
    • JPQL이나 SQL을 직접 작성하는 것 보다 훨씬 안전하다.
QInterest interest = QInterest.interest;

queryFactory.selectFrom(interest)
    .where(interest.name.contains("운동"))
    .fetch();

사용 예시

repository

public interface InterestRepository extends JpaRepository<Interest, Long>, InterestRepositoryCustom {

  @Query("SELECT i FROM Interest i LEFT JOIN FETCH i.keywords WHERE i.id = :id")
  Optional<Interest> findByIdFetch(@Param("id") Long id);

  @Query("SELECT i FROM Interest i LEFT JOIN FETCH i.keywords")
  List<Interest> findAllWithKeywords();
}
  • repository 클래스는 JpaRepository와 repositoryCustom 클래스를 상속 \
  • 단순한 정적 쿼리는 여기에 직접 작성
    -> 두 클래스의 메서드를 전부 사용할 수 있음

repositoryCustom

public interface InterestRepositoryCustom {
  CursorPageResponse<InterestDto> searchByCondition(InterestSearchCondition condition);
}
  • 동적쿼리 등 QueryDSL을 사용하고싶은 메서드를 정의

repositoryCustomImpl

package com.team1.monew.interest.repository;

import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.team1.monew.interest.dto.InterestSearchCondition;
import com.team1.monew.interest.entity.Interest;
import com.team1.monew.interest.entity.QInterest;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;

@Repository
@RequiredArgsConstructor
public class InterestRepositoryCustomImpl implements InterestRepositoryCustom {

  private final JPAQueryFactory queryFactory;

  @Override
  public Slice<Interest> searchByCondition(InterestSearchCondition condition) {
    QInterest interest = QInterest.interest;

    BooleanBuilder where = new BooleanBuilder();
    if (StringUtils.hasText(condition.keyword())) {
      where.and(
          interest.name.containsIgnoreCase(condition.keyword())
              .or(interest.keywords.any().keyword.containsIgnoreCase(condition.keyword()))
      );
    }

    if (StringUtils.hasText(condition.cursor())) {
      if ("subscriberCount".equalsIgnoreCase(condition.orderBy())) {
        Long cursorValue = Long.parseLong(condition.cursor());
        where.and("ASC".equalsIgnoreCase(condition.direction())
            ? interest.subscriberCount.gt(cursorValue)
            : interest.subscriberCount.lt(cursorValue));
      } else {
        String cursorValue = condition.cursor();
        where.and("ASC".equalsIgnoreCase(condition.direction())
            ? interest.name.gt(cursorValue)
            : interest.name.lt(cursorValue));
      }
    }

    Order direction = "ASC".equalsIgnoreCase(condition.direction()) ? Order.ASC : Order.DESC;
    OrderSpecifier<?>[] orderSpecifiers = "subscriberCount".equalsIgnoreCase(condition.orderBy())
        ? new OrderSpecifier[]{ new OrderSpecifier<>(direction, interest.subscriberCount) }
        : new OrderSpecifier[]{ new OrderSpecifier<>(direction, interest.name) };

    List<Interest> results = queryFactory
        .selectFrom(interest)
        .leftJoin(interest.keywords).fetchJoin()
        .where(where)
        .orderBy(orderSpecifiers)
        .limit(condition.limit() + 1)
        .fetch();

    boolean hasNext = results.size() > condition.limit();
    List<Interest> content = hasNext ? results.subList(0, condition.limit()) : results;

    return new SliceImpl<>(content, condition.toPageable(), hasNext);
  }
}


    }
  • JPAQueryFactory를 주입받아 QueryDSL로 동적 쿼리 구현
  • BooleanExpression으로 개별 조건을 정의하고, BooleanBuilder.and로 복잡한 조건 조합
    • 여기서는 nameCondition 또는 keywordCondition을 만족하는 조건
  • BooleanBuilder: 여러 BooleanExpression을 조합해 동적 쿼리를 만들기 위한 가변(mutable) 객체
    and, or로 추가 가능
  • BooleanExpression: 단일 조건을 표현하는 불변(immutable) 객체

=> 이렇게 반환받은 data를 Service, Controller단을 거치면서 커서 페이지네이션을 적용시키면 된다.

profile
백엔드 개발자를 꿈꿉니다.

0개의 댓글