[Project] QueryDSL 사용하기 (설정 방법 및 장점)
Categories: Project
📌 개인적인 공간으로 공부를 기록하고 복습하기 위해 사용하는 블로그입니다.
정확하지 않은 정보가 있을 수 있으니 참고바랍니다 :😸
[틀린 내용은 댓글로 남겨주시면 복받으실거에요]
QueryDSL
Order에서 get 요청시 검색기능을 날짜와 함께 itemCd, 또는 buyerCd 그리고 주문 번호 등
사용자가 다양한 조건으로 검색을 사용할 수 있도록 해야하는데 엘라스틱서치는 다른 분이 고생하는 걸 보기도 했고 기획 때 후순위로 미뤄놓아서
API를 여러개 만들거나 메서드를 여러 개 구현해야 하나 생각하다가
날짜 기간 동안의 검색은 어떻게 해야할지 구글링 했더니 queryDSL이라는 것을 발견했고 ,
동적 쿼리라 null이 들어오면 조건을 걸지 않는 좋은 기능인 것 같아서 바로 적용해보았다.
Build.gradle설정
사실 여기부터 헤맸는데, build.gradle 설정도 springboot 버전에 따라 다르게 적용해야 해야한다는 걸 알게되었다.
몇 시간을 싸우다가 버전 별로 잘 정리해두신 분이 있어서 참고했다.
-
아래는 내가 적용한 SpringBoot 2.7.0 버전일 때 사용한 설정 파일이다.
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
plugins { id 'org.springframework.boot' version '2.7.0' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } def querydslDir = "$buildDir/generated/querydsl" sourceSets { main { java { srcDirs = ['src/main/java', querydslDir] // QueryDSL로 생성된 경로 추가 } } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' implementation "com.querydsl:querydsl-jpa" annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" }
- 뭐 하나 해결하면 이제는 Q파일이 없다고 에러가 무진장 발생해서 경로를 추가했다.
querydslDir
경로를 추가해서 QueryDSL로 생성된 Q 클래스를 찾을 수 있게 해주는 것이다.- 그런 다음,
clean
→build
과정을 거치면 Q[Entity이름] 형태로 자동으로 생성된다. - 예를 들어,
Order
라는 엔티티가 있으면QOrder
클래스가 생성되는 식이다. 이걸 가지고 쿼리를 타입 안전하게 짤 수 있다.
CustomRepository, RepositoryImpl 생성
-
CustomRepository : interface 생성
1 2 3 4
public interface OrderQueryRepositoryCustom { Page<OrderHeaders> findByCreatedAtBetweenAndOrderStatusAndBuyer_BuyerCdAndOrderItems_ItemCdAndOrderCd (OrderDto.OrderSearchRequest orderSearchRequest, Pageable pageable); }
여기서는 특정 기간 내에 주문 상태, 구매자 코드, 주문 항목 코드, 그리고 주문 코드를 기반으로
OrderHeaders
를 검색하는 쿼리를 작성할 예정이다.Pageable
을 사용해서 페이징 처리까지 할 수 있게 구성했다. -
RepositoryImpl 위에서 정의한 인터페이스의 구현체를 만들어야 한다. 이때 중요한 점은
JPAQueryFactory
를 의존성 주입받아 사용한다는 것이다.
JPAQueryFactory
는 QueryDSL로 동적 쿼리를 만들 때 매우 유용하게 사용된다.
-
의존성 주입
1 2 3 4 5 6 7 8
@Repository public class OrderQueryRepositoryImpl implements OrderQueryRepositoryCustom { private final JPAQueryFactory queryFactory; public OrderQueryRepositoryImpl(JPAQueryFactory queryFactory) { this.queryFactory = queryFactory; }
-
구현 메서드
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
@Override public Page<OrderHeaders> findByCreatedAtBetweenAndOrderStatusAndBuyer_BuyerCdAndOrderItems_ItemCdAndOrderCd (OrderDto.OrderSearchRequest orderSearchRequest, Pageable pageable) { QOrderHeaders orderHeaders = QOrderHeaders.orderHeaders; QBuyer buyer = QBuyer.buyer; QOrderItems orderItems = QOrderItems.orderItems; //BooleanBuilder를 활용해 다양한 조건을 조합할 수 있다. BooleanBuilder builder = new BooleanBuilder(); //필터를 적용해서 null이라면 조건을 걸지 않고, //null 이 아니면 조건을 걸어 데이터를 조회할 수 있도록 동적으로 구현할 수 있다 // 날짜 필터 if (orderSearchRequest.getSearchStartDate() != null && orderSearchRequest.getSearchEndDate() != null) { LocalDateTime searchStartDate = orderSearchRequest.getSearchStartDate().atStartOfDay(); LocalDateTime searchEndDate = orderSearchRequest.getSearchEndDate().atTime(23, 59, 59); builder.and(orderHeaders.requestDate.between(searchStartDate, searchEndDate)); } // 상태 필터 if (orderSearchRequest.getStatus() != null) { builder.and(orderHeaders.orderStatus.eq(orderSearchRequest.getStatus())); } // BuyerCode 필터 if (orderSearchRequest.getBuyerCd() != null && !orderSearchRequest.getBuyerCd().trim().isEmpty()) { builder.and(buyer.buyerCd.eq(orderSearchRequest.getBuyerCd())); } // ItemCode 필터 if (orderSearchRequest.getItemCd() != null && !orderSearchRequest.getItemCd().trim().isEmpty()) { builder.and(orderItems.itemCd.eq(orderSearchRequest.getItemCd())); } //orderCode 필터 if (orderSearchRequest.getOrderCd() != null) { builder.and(orderHeaders.orderCd.eq(orderSearchRequest.getOrderCd())); } List<OrderHeaders> results = queryFactory .selectDistinct(orderHeaders) .from(orderHeaders) .leftJoin(orderHeaders.buyer, buyer) .leftJoin(orderHeaders.orderItems, orderItems).fetchJoin() .where(builder) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .orderBy(orderHeaders.createdAt.desc()) .fetch(); long total = queryFactory .select(orderHeaders.countDistinct()) .from(orderHeaders) .leftJoin(orderHeaders.buyer, buyer) .leftJoin(orderHeaders.orderItems, orderItems) .where(builder) .fetchOne(); return new PageImpl<>(results, pageable, total); } }
우여곡절 끝에 작성하긴 했는데 동적으로 잘 실행된다.
JPQL VS QueryDSL
이렇게 하다 보니 기존에 사용하던 JPQL과 어떤 차이점이 있는지 정확하게 알고 싶어서 공부 후 정리해보았다.
런타임 에러보다는 컴파일 에러가 좋은데, 매번 JPQL에서 런타임 에러 발생해서 조마조마 했는데 QueryDSL 사용할 때는 그럴 일이 없어서 좋았다,.
-
JPQL (Java Persistence Query Language)
-
사용 방식: JPQL은 SQL과 비슷한 구문을 사용해서 엔티티 객체를 대상으로 쿼리를 작성하는 방식이다. 쿼리를 문자열로 직접 작성하고,
@Query
같은 어노테이션을 통해 실행한다. 그런데 이 방식은 문자열로 쿼리를 작성하는 만큼, 컴파일 타임에 쿼리 오류를 잡을 수 없다는 단점이 있다. - 특징:
- SQL과 유사한 문법으로 쿼리를 작성하니 익숙한 느낌이다.
- 하지만 쿼리를 문자열로 작성하다 보니, 런타임에야 오류를 발견할 수 있다.
- 문자열 기반이라 가독성도 떨어지고, 유지보수도 힘들다.
- 특히 동적 쿼리 작성이 어렵다. 동적 쿼리를 만들려면
if
문을 통해 조건을 일일이 관리해야 하는데, 그게 또 번거롭다.
-
예시:
1 2
@Query("SELECT o FROM Order o WHERE o.status = :status") List<Order> findOrdersByStatus(@Param("status") String status);
위 코드는
status
에 따라Order
데이터를 조회하는 간단한 JPQL 쿼리다. 문자열로 쿼리를 직접 작성했기 때문에 동적 쿼리를 만들거나 조건을 추가할 때는 조금 귀찮을 수 있다.
-
-
QueryDSL
-
사용 방식: QueryDSL은 쿼리를 타입 안전하게 작성할 수 있다. 쿼리를 문자열이 아니라 자바 코드로 작성하는데, 엔티티의 필드나 조건을 직접 메서드 형태로 사용하니까 쿼리 오류를 컴파일 타임에 바로 확인할 수 있다.
이런 덕분에 복잡한 쿼리나 동적 쿼리 작성할 때 진짜 편리하다.
- 특징:
- 타입 안전성: 컴파일 타임에 쿼리 오류를 바로 잡아준다.
- 가독성: 쿼리를 자바 코드로 작성하니까 유지보수하기도 훨씬 수월하다.
- 동적 쿼리: 복잡한 조건을 유연하게 처리할 수 있어서 동적 쿼리 작성이 훨씬 쉽다.
- 복잡한 쿼리도 깔끔하게 처리할 수 있다. 특히 조건에 따라 쿼리를 동적으로 생성할 수 있는 것이 가장 큰 특징이자 장점이다
- 예시:
1 2 3 4
QOrder order = QOrder.order; List<Order> orders = queryFactory.selectFrom(order) .where(order.status.eq(OrderStatus.REQUEST_TEMP)) .fetch();
자바 코드로 쿼리를 작성하니 조건을 추가하거나 수정할 때도 훨씬 직관적이다. 특히 복잡한 쿼리일수록 QueryDSL을 쓰면 훨씬 깔끔해진다.
-
-
정리
특징 JPQL QueryDSL 쿼리 방식 문자열 기반 쿼리 ( @Query
)자바 코드 기반 (메서드 체인 형태) 타입 안전성 없음 (런타임 시 오류 발생 가능) 있음 (컴파일 시점에 오류 감지 가능) 동적 쿼리 복잡한 동적 쿼리 작성이 어려움 동적 쿼리 작성에 매우 유리 가독성 복잡한 쿼리는 가독성이 떨어짐 가독성이 좋고 코드로 작성해서 직관적임 쿼리 오류 감지 컴파일 시점에 오류 감지 불가 컴파일 시점에 오류 감지 가능 사용 복잡도 간단한 쿼리 작성에 적합 동적/복잡한 쿼리 작성에 적합
내가 생각하기에 가장 큰 장점은 바로 타입 안정성 인 것 같다
조회해야 할 것들이 많아지면서 JPQL 사용하다가 맨날 에러나서 실행하다가 중단되고 빨간 글씨 잔뜩 보는데
QueryDSL 사용하면서 거의 만나본적 없는듯!
간단하게 구현과 JPQL에 대해서 공부했는데
다음에는 QueryDSL 사용하다 만난 trouble에 대해서 리뷰 해야겠다.
Leave a comment