YS's develop story
spring jpa에 Querydsl 적용하기 [spring 3.1.5, java 17] 본문
개발하고 있는 프로젝트에 Querydsl을 적용해 보려고 합니다.
Querydsl의 장점
유연성 및 강력한 검색 기능 :
Query DSL을 사용하면 복잡한 검색 조건을 표현할 수 있습니다.
진행하고 있는 프로젝트 메인페이지 및 예약 검사 시
jpa repository를 통해 여러 복잡한 조건을 거쳐 DB에 여러 번 접근하게 되는데
QueryDSL을 활용하게 된다면 그럴 필요 없이 검색 조건과 필터를 조합해 정확한 결과를 얻을 수 있습니다.
가독성 및 이해도 향상 :
Query DSL은 일반적으로 사람이 이해하기 쉬운 구조를 가지고 있기에 이는 쿼리를 작성하고 이해하는 데 도움이 되며,
코드의 가독성을 높여 유지보수를 쉽게 만들 수 있습니다.
성능 향상 :
적절한 쿼리를 작성함으로써 성능을 향상할 수 있습니다.
그럼 그냥 JPQL 쓰면 안 되나? QueryDSL을 사용하는 이유는 뭘까요?
성능 차이
- JPQL:
- JPQL은 JPA를 사용하여 작성된 객체 지향 쿼리 언어입니다.
- JPQL 쿼리는 **런타임(애플리케이션 실행 중)**에 문자열로 해석되어 SQL로 변환되는데,
이 과정에서 쿼리의 오타나 잘못된 필드명으로 인한 오류가 런타임에만 발견됩니다. - 복잡한 쿼리의 경우 JPQL로 작성하기 어려울 수 있고, 유지보수에 어려움이 있을 수 있습니다.
- Querydsl:
- Querydsl은 타입 안전한 쿼리를 작성할 수 있는 프레임워크입니다.
- 컴파일 시점에 쿼리의 문법 오류를 발견할 수 있어, 안정성이 높습니다.
- 동적 쿼리 작성이 용이하며, 복잡한 쿼리도 더 가독성 있게 작성할 수 있습니다.
- JPQL에 비해 약간 더 빠른 성능을 제공할 수 있지만,
이는 상황에 따라 다를 수 있으며 대체로 차이는 미미합니다.
실무에서는?
- JPQL:
- 간단한 쿼리나, 프로젝트에서 추가적인 의존성을 피하고자 할 때 선호됩니다.
- 표준 JPA만을 사용하고 싶은 경우에 적합합니다.
- 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을 적극적으로 활용하면 좋을 것 같습니다.
'Spring' 카테고리의 다른 글
Spring, OAuth2 + JWT 를 활용하여 소셜 로그인 구현하기 2편 (구글 및 네이버) [Spring 3.1.5, java 17] (0) | 2023.12.11 |
---|---|
Spring, OAuth2 + JWT 를 활용하여 소셜 로그인 구현하기 1편 (구글 및 네이버) [Spring 3.1.5, java 17] (3) | 2023.12.08 |
springboot certbot으로 ssl인증서 받아서 https로 배포하기 (1) | 2023.11.30 |
GCP 에서 springboot 프로젝트 docker로 배포하기 (1) | 2023.11.25 |
[Spring] request시 notnull값 controllerAdvice로 처리해서 response보내기 (1) | 2023.11.24 |