QueryDSL 적용하기

자라나는 ㅇㅅㅇ개발자·2024년 10월 15일
0

TIL

목록 보기
172/183

프로젝트의 검색 기능에 QueryDSL을 적용해보기로 했다.

QueryDSL(Query Domain Specific Language)

  • Domain도메인 Specific특화 Language언어
  • 데이터를 다룰 때 Table에 종속되지 않고 객체에 특화된 쿼리 언어
  • 쿼리를 자바 코드로 작성하여 JPA로 해결하지 못하는 복잡한 문제를 해결할 수 있다.
  • 자바 코드로 작성하기 때문에 문법의 오류를 컴파일 시점에서 잡아낼 수 있다.

우리 프로젝트의 BookingService의 검색 메서드를 확인해보면

        // 사용자, 공연명으로 예약 조회 null일 경우는 메서드에서 처리함
        Page<Booking> bookingList = bookingRepository.findByBookingSearch(
                userList.stream().map(GetUserRes::userId).toList(),
                concertList.stream().map(GetConcertRes::id).toList(), BookingStatus.PENDING, pageable);

이런 부분이 있는데 여기서 Repository의 findByBookingSearch를 따라가보면

     // 예매 대기 인 경우는 제외하고 예매 내역 조회
     @Query("SELECT b FROM Booking b WHERE "
         + "(:userIds IS NULL OR b.userId IN :userIds) "
         + "AND (:concertIds IS NULL OR b.concertId IN :concertIds) "
         + "AND b.status != :status")
     Page<Booking> findByBookingSearch(List<Long> userIds, List<Long> concertIds, BookingStatus status, Pageable pageable);

팀원이 작성해둔 쿼리문이 작성되어있다.

검색 조건이 까다로운 이 코드를 개선할 것이다.


가장 먼저 의존성을 추가한다.

    // queryDSL
    implementation 'com.querydsl:querydsl-jpa'
    annotationProcessor 'com.querydsl:querydsl-apt:jpa'
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"

Entity에 대한 Q-타입 클래스를 생성해야한다.
Q-타입 클래스는 QueryDSL을 사용하여 JPQL 또는 SQL 쿼리를 작성할 때 타입 안전성을 제공하는 클래스이다.


인텔리제이 우측 탭에서 Gradle을 눌러

Tasks - build - clean
Tasks - other - compileJava

빌드를 실행하면 각 서비스의 build 경로에 Q-타입 클래스와
BaseEntity 클래스가 있는 경로에 QBaseEntity가 ignore처리되면서 자동으로 생성된다.


QBooking이 필요한 이유

QBooking은 QueryDSL을 사용할 때 쿼리 작성에 필요한 엔티티의 메타 데이터를 제공하는 클래스이다.
QueryDSL에서는 엔티티 클래스(Booking)를 직접 참조하지 않고 자동으로 생성된 Q 클래스를 사용해 쿼리를 작성한다.

  • QBooking과 같은 클래스에서 필드에 직접 접근하여 쿼리를 작성하므로 컴파일 시점에 잘못된 쿼리 구조나 필드명을 바로잡을 수 있다.
  • QBooking은 Booking 엔티티에 있는 모든 필드에 접근할 수 있게 해주어 쿼리 생성 과정에서 엔티티의 필드를 안전하게 사용할 수 있게 한다.
  • QBooking을 통해 엔티티의 필드를 활용하여 동적 조건을 만들 수 있다.
  • SQL의 컬럼 이름을 문자열로 다루는 것보다 코드에서 자동으로 생성된 클래스와 필드를 사용하는 방식이 더 직관적이기 때문에 코드의 가독성을 높여준다.

이제 QueryDSL문을 작성하면 되는데
그 전에 JPAQueryFactory의 Bean을 관리하기 위한 QuerydslConfig를 작성해준다.

@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager entityManager;

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

}

쿼리문을 작성하기 위한 Custom Interface를 작성

public interface BookingRepositoryCustom {

    Page<Booking> findByBookingSearch(List<Long> userIds, List<Long> concertIds, Pageable pageable);

}

BookingRepositoryCustom을 implements하는 쿼리문을 작성한다.

@RequiredArgsConstructor
public class BookingRepositoryImpl implements BookingRepositoryCustom{

    private final JPAQueryFactory queryFactory;

    @Override
    public Page<Booking> findByBookingSearch(List<Long> userIds, List<Long> concertIds, Pageable pageable) {
        // Booking Entity에 대응되는 QBooking 객체를 통해 매핑하여 필드에 접근
        QBooking booking = QBooking.booking;

        // BooleanExpression : QueryDSL에서 조건을 표현하는 객체, where()절에 사용된다.
        // userIds 리스트가 존재할 경우 booking.userId가 userIds에 포함된 데이터만 필터링
        BooleanExpression userCondition = userIds != null && !userIds.isEmpty() ? booking.userId.in(userIds) : null;
        // concertIds 리스트가 존재할 경우 booking.concertId가 concertIds에 포함된 데이터만 필터링
        BooleanExpression concertCondition = concertIds != null && !concertIds.isEmpty() ? booking.concertId.in(concertIds) : null;

        // 전체 예약을 조회할 조건
        boolean isUserConditionEmpty = userCondition == null;
        boolean isConcertConditionEmpty = concertCondition == null;

        List<Booking> results;

        if (isUserConditionEmpty && isConcertConditionEmpty) {
            // nickname과 concertName이 모두 비어있을 때: 전체 예약 조회
            results = queryFactory.selectFrom(booking)
                    .where(booking.status.ne(BookingStatus.PENDING)) // BookingStatus가 PENDING은 제외
                    .offset(pageable.getOffset())
                    .limit(pageable.getPageSize())
                    .fetch();
        } else if (isUserConditionEmpty) {
            // nickname만 비어있고 concertName이 있을 때: concertName에 해당하는 조건만 조회
            results = queryFactory.selectFrom(booking)
                    .where(concertCondition, booking.status.ne(BookingStatus.PENDING)) // BookingStatus가 PENDING은 제외
                    .offset(pageable.getOffset())
                    .limit(pageable.getPageSize())
                    .fetch();
        } else if (isConcertConditionEmpty) {
            // concertName만 비어있고 nickname이 있을 때: nickname에 해당하는 조건만 조회
            results = queryFactory.selectFrom(booking)
                    .where(userCondition, booking.status.ne(BookingStatus.PENDING)) // BookingStatus가 PENDING은 제외
                    .offset(pageable.getOffset())
                    .limit(pageable.getPageSize())
                    .fetch();
        } else {
            // nickname과 concertName 둘 다 있을 때: 두 조건에 해당하는 예약 조회
            results = queryFactory.selectFrom(booking)
                    .where(userCondition, concertCondition, booking.status.ne(BookingStatus.PENDING)) // BookingStatus가 PENDING은 제외
                    .offset(pageable.getOffset())
                    .limit(pageable.getPageSize())
                    .fetch();
        }

        // 쿼리의 총 결과 수
        long total = queryFactory.selectFrom(booking)
                .where(userCondition, concertCondition, booking.status.ne(BookingStatus.PENDING))
                .fetch().size();

        return new PageImpl<>(results, pageable, total);
    }

}

이제 BookingRepository로 와서 BookingRepositoryCustom를 extends하고 원래 있던 쿼리를 지운다.

public interface BookingRepository extends JpaRepository<Booking, Long>, BookingRepositoryCustom {

     // 예매 대기 인 경우는 제외하고 예매 내역 조회
//     @Query("SELECT b FROM Booking b WHERE "
//         + "(:userIds IS NULL OR b.userId IN :userIds) "
//         + "AND (:concertIds IS NULL OR b.concertId IN :concertIds) "
//         + "AND b.status != :status")
//     Page<Booking> findByBookingSearch(List<Long> userIds, List<Long> concertIds, BookingStatus status, Pageable pageable);
}

0개의 댓글

관련 채용 정보