YS's develop story

spring jpa에 Querydsl 적용하기 [spring 3.1.5, java 17] 본문

Spring

spring jpa에 Querydsl 적용하기 [spring 3.1.5, java 17]

Yusang 2023. 12. 6. 22:23

 

개발하고 있는 프로젝트에 Querydsl을 적용해 보려고 합니다.

 

Querydsl의 장점

 

유연성 및 강력한 검색 기능 :
Query DSL을 사용하면 복잡한 검색 조건을 표현할 수 있습니다.
진행하고 있는 프로젝트 메인페이지 및 예약 검사 시

jpa repository를 통해 여러 복잡한 조건을 거쳐 DB에 여러 번 접근하게 되는데
QueryDSL을 활용하게 된다면 그럴 필요 없이 검색 조건과 필터를 조합해 정확한 결과를 얻을 수 있습니다.

 

가독성 및 이해도 향상 :
Query DSL은 일반적으로 사람이 이해하기 쉬운 구조를 가지고 있기에 이는 쿼리를 작성하고 이해하는 데 도움이 되며,

코드의 가독성을 높여 유지보수를 쉽게 만들 수 있습니다.

 

성능 향상 :
적절한 쿼리를 작성함으로써 성능을 향상할 수 있습니다.

 

그럼 그냥 JPQL 쓰면 안 되나? QueryDSL을 사용하는 이유는 뭘까요?

 

 

성능 차이

  1. JPQL:
    • JPQL은 JPA를 사용하여 작성된 객체 지향 쿼리 언어입니다.
    • JPQL 쿼리는 **런타임(애플리케이션 실행 중)**에 문자열로 해석되어 SQL로 변환되는데,
      이 과정에서 쿼리의 오타나 잘못된 필드명으로 인한 오류가 런타임에만 발견됩니다.
    • 복잡한 쿼리의 경우 JPQL로 작성하기 어려울 수 있고, 유지보수에 어려움이 있을 수 있습니다.
  2. Querydsl:
    • Querydsl은 타입 안전한 쿼리를 작성할 수 있는 프레임워크입니다.
    • 컴파일 시점에 쿼리의 문법 오류를 발견할 수 있어, 안정성이 높습니다.
    • 동적 쿼리 작성이 용이하며, 복잡한 쿼리도 더 가독성 있게 작성할 수 있습니다.
    • JPQL에 비해 약간 더 빠른 성능을 제공할 수 있지만,
      이는 상황에 따라 다를 수 있으며 대체로 차이는 미미합니다.

실무에서는?

  • JPQL:
    • 간단한 쿼리나, 프로젝트에서 추가적인 의존성을 피하고자 할 때 선호됩니다.
    • 표준 JPA만을 사용하고 싶은 경우에 적합합니다.
  • Querydsl:
    • 복잡한 쿼리나 동적 쿼리가 필요한 경우에 선호됩니다.
    • 타입 안전성과 가독성이 중요한 프로젝트에서 유리합니다.
    • 초기 설정 필요하고, 학습 곡선이 높지만,
      장기적으로 보았을 때 유지보수와 개발 효율성 측면에서 이점을 제공합니다.
    결국 성능적인 측면에서 JPQL과 Querydsl 사이에는 큰 차이가 없습니다.
    주된 차이점은 가독성, 유지보수성, 그리고 타입 안전성에 있습니다.
    실무에서는 복잡한 쿼리를 다루어야 하고 유지보수성, 런타임시 에러를 회피하기 위해
    Querydsl을 선호하는 경향이 있습니다.

 

장단점 비교:

  • JPQL:
    • 장점: JPA 표준의 일부이므로 JPA 구현체에서 지원되는 범위 내에서 사용할 수 있습니다.
    • 단점: 동적 쿼리 작성이 어렵고, 문자열 기반으로 작성되어 오타가 발생할 가능성이 있습니다.
  • QueryDSL:
    • 장점: 정적으로 쿼리를 작성하므로 컴파일 시간에 오류를 확인할 수 있고, 동적 쿼리 작성이 편리합니다.
      코드 자동 완성과 IDE 지원이 우수합니다.
    • 단점: JPQL이나 SQL과 달리 학습 곡선이 존재하며, 프로젝트에 라이브러리를 추가해야 합니다.

그렇기에 복잡한 쿼리를 추가하여 사용하는 경우에는

유지 보수 측면에서 용이한 QueryDsl을 추가해서 사용해 보려고 합니다.

 

 

gradle 추가

dependencies {
    // Querydsl 추가
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
//querydsl
def querydslDir = "src/main/generated"

sourceSets {
    main.java.srcDirs += [ querydslDir ]
}

tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}

clean.doLast {
    file(querydslDir).deleteDir()
}

 

 

추가 후 gradle - other - compileJava 실행

 

 

실행하고 나면 아래와 같이 Qclass가 생성됩니다.

이를 통해 Querydsl을 사용할 수 있습니다.

 

 

customRepository interface 생성

@Repository
public interface AccommodationRepositoryCustom {

    Page<Long> findAccommodationIds(AccommodationCategory category, boolean isDomestic,
        Pageable pageable, LocalDate startDate, LocalDate endDate,int numberOfPeople);
}

 

 

메인페이지에서 업체를 조회하는데 아래와 같은 쿼리로 조건을 필터링해서

조건에 맞는 업체만을 response 하게 됩니다.

SELECT DISTINCT a.accommodation_id
FROM accommodation_rooms
JOIN accommodation a ON a.accommodation_id = accommodation_rooms.accommodation_id
LEFT JOIN reservations r ON accommodation_rooms.room_id = r.room_id AND (r.start_date > NOW() OR r.start_date IS NULL)
WHERE a.category = :category
AND (
    NOT (
        (:startDate <= r.end_date AND :endDate >= r.start_date)
        OR (:startDate >= r.start_date AND :endDate <= r.end_date)
    )
    OR (r.start_date IS NULL AND r.end_date IS NULL)
    OR (r.payment_completed IS FALSE OR r.payment_completed IS NULL)
    OR (r.deleted_at is not null)
)
and a.is_domestic = :isDomestic
and (:numberOfPerson>=accommodation_rooms.min_capacity and :numberOfPerson<= accommodation_rooms.max_capacity)
limit :page,:size

 

 

customRepository을  implement 한 클래스 작성 후

위 쿼리를 Querydsl로 아래와 같이 반영해서 작성할 수 있습니다.

public class AccommodationRepositoryImpl implements AccommodationRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public AccommodationRepositoryImpl(EntityManager entityManager) {
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    @Override
    public Page<Long> findAccommodationIds(AccommodationCategory category, boolean isDomestic,
        Pageable pageable,LocalDate startDate, LocalDate endDate,int numberOfPeople) {
        QAccommodation a = QAccommodation.accommodation;
        QAccommodationRooms ar = QAccommodationRooms.accommodationRooms;
        QReservations r = QReservations.reservations;

        BooleanExpression reservationCondition = r.startDate.after(LocalDate.now())
            .or(r.startDate.isNull());

        BooleanExpression conflictingCondition = r.deletedAt.isNotNull()
            .or(r.paymentCompleted.isFalse().or(r.paymentCompleted.isNull()))
            .or(r.endDate.before(startDate))
                .or(r.startDate.after(endDate));

        BooleanExpression capacityCondition =
            ar.minCapacity.loe(numberOfPeople).and(ar.maxCapacity.goe(numberOfPeople));

        List<Long> result = queryFactory.selectDistinct(a.accommodationId)
            .from(ar)
            .join(a).on(ar.accommodation.accommodationId.eq(a.accommodationId))
            .leftJoin(r).on(ar.roomId.eq(r.room.roomId).and(reservationCondition))
            .where(a.category.eq(category)
                .and(a.isDomestic.eq(isDomestic))
                .and(conflictingCondition)
                .and(capacityCondition))
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

        return new PageImpl<>(result, pageable, result.size());
    }
}

 

 

 

Querydsl 적용 후 service단의 코드가 매우 간소화되었습니다.

적용 전

@Transactional
    public List<AccommodationFindResponse> getAccommodationsInMainPage(
        PrincipalDetails principalDetails, String categoryStr,
        boolean isDomestic, Pageable pageable, LocalDate startDate, LocalDate endDate,
        int numberOfPeople) {
        AccommodationCategory category = AccommodationCategory.valueOf(categoryStr.toUpperCase());

        Page<Accommodation> accommodations = accommodationRepository.findByCategoryAndIsDomestic(
            category, isDomestic, pageable);
        List<Accommodation> accommodationList = new ArrayList<>();

        boolean capacityAvailable = false;
        for (Accommodation accommodationContents : accommodations) {
            List<AccommodationRooms> roomlist = accommodationContents.getRoomlist();
            int fullRoomCount = 0;

            for (AccommodationRooms accommodationRoomContents : roomlist) {
                if (accommodationRoomContents.getMaxCapacity() >= numberOfPeople &&
                    accommodationRoomContents.getMinCapacity() <= numberOfPeople) {
                    capacityAvailable = true;
                }
                //예약 충돌 검사
                if (reservationRepository.findConflictingReservations(
                    accommodationRoomContents.getRoomId(), startDate, endDate).isPresent()) {
                    fullRoomCount++;
                }
            }
            if (fullRoomCount < roomlist.size() && capacityAvailable) {
                accommodationList.add(accommodationContents);
            }
        }
        
        ...
        ...
        ...
        
        
        return accommodationFindResponses;
    }

 

 

적용 후

@Transactional
public List<AccommodationFindResponse> getAccommodationsInMainPage(
    PrincipalDetails principalDetails, AccommodationCategory category,
    boolean isDomestic, Pageable pageable, LocalDate startDate, LocalDate endDate,
    int numberOfPeople) {
    Page<Long> acIds = accommodationRepositoryCustom.findAccommodationIds(
        category, isDomestic, pageable, startDate, endDate, numberOfPeople);

    List<Accommodation> accommodationList =
        acIds.stream().map(accommodationRepository::findByAccommodationId).toList();
   ...
   ...
   ...

    return accommodationFindResponses;
}

 

 

진행하고 있는 프로젝트에서 메인페이지에서 클라이언트의 요구사항에 맞게 업체를 여러 가지 조건으로 필터링해서

업체정보를 전달해야 했었는데 Querydsl 적용으로 인해 검색 조건과 필터를 조합하여

정확한 결과를 얻을 수 있었고 service 로직이 매우 간소화되었습니다!

또한 Querydsl은 일반적으로 사람이 이해하기 쉬운 구조를 가지고 있어

쿼리를 작성하고 이해하는 데 도움이 되며 IDE에서도 체크가 가능하기 때문에 유지보수가 매우 용이합니다! 

 

필요한 데이터에 접근할 때 어쩔 수 없이 복잡한 쿼리를 사용해야 하는 경우

Querydsl을 적극적으로 활용하면 좋을 것 같습니다.

 

 

Comments