본문 바로가기

카테고리 없음

QueryDSL - 2

 

QueryDSL

QueryDSL - 래퍼런스 문서 QueryDSL 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해주는 오픈소스 프레임워크. QueryDSL이 제공하는 Fluent API를 이용해 코드 작성의 형식으로 쿼리를 생성할

eleunadeu.tistory.com

 

 

페이지네이션 Offset vs Cursor

개요팀 프로젝트에서 주문관리 시스템을 개발했는데 각종 정보에 대한 조회 기능이 필요했고, 프로젝트 요구사항에 QueryDSL 등을 활용한 pagination이 필요했다. Pagination(페이지네이션)?대량의 데

eleunadeu.tistory.com

 

개요

프로젝트에서 조회 시 성능 향상을 위해 QueryDSL을 사용해 pagenation 구현했다.

심화 과정의 특강을 통해 QueryDSL을 더 효율적으로 사용할 수 있는 방법에 대해 배웠다.

강의 중 배운 내용을 정리하고 프로젝트에 적용해 보는 것이다.

 

QueryDSL

Java 애플리케이션에서 타입 안전하고 간결한 SQL 쿼리 작성을 지원하는 오픈소스 플레임워크.

 

사용 이유

  • Method Query로 작성할 수 없는 복잡한 쿼리를 작성할 수 있음
  • JPQL로는 작성할 수 없는 동적 쿼리를 작성할 수 있음

사용 시 주의사항

  • 비효율을 방지하기 위해 LAZY 옵션을 사용하는데 N + 1 문제가 발생할 수 있음

해결 방법

  • 트랜잭션 readOnly = true 옵션과 yml에서 open-in-view = false, default_batch_fetch_size 사용으로 위 문제를 해결할 수 있다.

  • 또는 fetchJoin을 사용하는 방법이 있다.

 

QueryDSL 동적 쿼리

 

1. Predicate

  • 역할: 조건을 나타내는 인터페이스. where() 절에 사용.
  • 주요 사용처: JPAQuery의 where() 메서드에 전달해 조건 필터링을 수행.

2. BooleanBuilder

  • 역할: 조건을 동적으로 조합하는 빌더 클래스. 조건을 누적해서 구성할 수 있다.
  • 특징: 초기 값이 없을 수 있으며, 조건을 점진적으로 추가할 때 유용하다.

3. BooleanExpression

  • 역할: Predicate의 하위 인터페이스로 불리언 논리를 수행하는 조건 표현식.
  • 특징: 조건을 메서드 체인으로 조합할 수 있어 가독성이 좋음.

 

QueryDSL 대용량 처리 최적화

동적 쿼리 구현 시 BooleanBuilder 대신 BooleanExpression

  • 동적 쿼리를 작성할 때 흔히 BooleanBuilder를 쓰지만, 이 경우 조건이 많아질수록 쿼리 가독성이 떨어짐
  • BooleanExpression을 사용하면 특정 조건이 null일 경우 조건절이 자동으로 제거되므로, 보다 명시적이고 깔끔한 동적 쿼리 구성이 가능

 

exists() vs count()

  • count() 쿼리는 조건에 맞는 모든 로우를 세어야 하므로 비용이 크다.
  • exists() 쿼리는 첫 번째 매칭되는 로우를 찾으면 바로 종료하기 때문에 더 빠르다.

JPA와 QueryDSL에서 제공하는 fetchOne()이나 특정 메서드들은 내부적으로 count 쿼리를 사용한다.

이를 개선하려면 직접 limit 1을 사용한 쿼리를 작성해 "데이터 존재 여부"만 빠르게 확인할 수 있다(대안: fetchFirst() != null).

 

묵시적 조인(implicit join)의 함정

public List<Member> crossJoin() {
    return queryFactory
            .selectFrom(member)
            .where(member.team.id.gt(member.team.leader.id))
            .fetch();
}
  • 묵시적 조인을 사용하면 쉽게 크로스 조인이 발생할 수 있고, 이로 인해 성능 문제가 생김.
  • 명시적 조인(join()  메서드 사용)으로 전환하면 불필요한 크로스 조인을 피하고 성능을 개선할 수 있음.

 

엔티티 대신 DTO 조회하기

엔티티를 직접 조회하면:

  • 필요 없는 컬럼까지 전부 조회
  • 1:1 관계 시 N+1 문제 발생
  • 2차 캐시 등 불필요한 기능 작동으로 오버헤드 증가

단순 조회나 대용량 조회에서는 DTO를 통해 필요한 컬럼만 가져오는 것이 성능에 유리

또한, SELECT 절에 엔티티를 바로 넣지 말고, 필요한 ID 값만 조회한 뒤 DTO로 매핑하는 방식으로 N+1 문제를 회피할 수 있음.

 

Group By, 정렬 최적화

  • Group By 시 정렬(sort) 비용이 발생할 수 있음.
  • MySQL 5.6 기반 환경에서는 order by null과 같은 문법으로 파이소트(filesort) 제거가 가능하지만, QueryDSL에서 직접적으로 지원하지 않는다.
  • 우회 방법으로 “OrderByNull”과 같은 유틸 클래스를 만들어 직접 적용할 수 있다.
  • 대용량 정렬은 DB보다 WAS(애플리케이션 레벨)에서 처리하는 것이 자원 효율적일 수 있다.
  • 페이징 없는 단순 그룹 통계 쿼리라면 DB 정렬 부담을 최소화하고 WAS에서 후처리하는 방식 고려.

 

커버링 인덱스(Covering Index) 활용

페이징 시 커버링 인덱스를 사용하면 성능을 대폭 개선할 수 있다.

다만, QueryDSL JPA는 서브쿼리나 특정 쿼리 구조를 제한하기 때문에

  1. 인덱스 컬럼으로 PK만 빠르게 조회한 뒤
  2. 그 PK 리스트를 바탕으로 다시 필요한 컬럼들을 가져오는두 단계 쿼리 방식을 통해 커버링 인덱스 효과를 낼 수 있다.

두 번 쿼리를 실행하지만 전체 성능은 거의 커버링 인덱스 사용 시와 비슷한 성능 향상을 기대할 수 있다.

 

대량 업데이트(1 벌크 업데이트) vs 더티 체킹(Dirty Checking)

  • 더티 체킹: 엔티티를 전부 로딩해 수정 시 자동 반영. 1만건, 10만건 업데이트 시 성능 저하 심각.
  • QueryDSL을 통한 1회성 벌크 업데이트: 한 번의 쿼리로 대량 업데이트 가능, 성능 우수.

단, 벌크 업데이트는 2차 캐시나 엔티티 상태 관리 부분을 직접 갱신하지 않으므로 캐시 정리 등의 추가 작업이 필요

  • 실시간 변동이 많은 소규모 업데이트 더티 체킹
  • 대규모 배치 업데이트 벌크 업데이트

 

벌크 Insert 최적화와 EntityQL

JPA는 기본적으로 JDBC의 “insert batch 옵션”을 제대로 활용하지 못함.

수만 건 인서트를 JPA로 처리하면 성능 문제가 심각해짐.

  • JDBC 템플릿: 문자열로 직접 쿼리를 작성 타입 세이프티(Type-safety) 부족
  • EntityQL: 엔티티 기반 메타 정보로 QueryDSL Native SQL 생성 타입 세이프 + 벌크 Insert 가능

EntityQL을 사용하면 JPA 엔티티를 이용해 QueryDSL Native SQL QClass를 생성, 마치 QueryDSL로 JPQL 쓰듯 네이티브 Insert Batch를 타입 안전하게 작성할 수 있다.

다만, EntityQL은 설정 복잡, Gradle 플러그인 의존, 임베디드 타입 미지원 등의 단점이 있어 아직 널리 쓰기엔 제약이 있다.

JDBC 템플릿 vs EntityQL 중 어떤 불편함이 더 낫느냐를 판단해 상황에 따라 선택

 

QueryProjection

 

QueryProjection은 QueryDSL에서 DTO(데이터 전송 객체)를 생성할 때 사용되는 어노테이션으로 DTO에 직접 매핑할 생성자를 만들어 주며, 타입 안전한 쿼리를 지원한다.

 

사용 이유

  • 타입 안전성 보장: DTO의 필드를 정확하게 매핑해 런타임 오류를 줄임
  • 코드 가독성 향상: 쿼리 결과를 DTO로 직접 매핑하여 변환 로직 제거

주의사항

  1. DTO 생성자에 @QueryProjection을 반드시 붙여야 한다.
  2. DTO 클래스는 QueryDSL 프로세스가 빌드 시 Q-클래스를 생성하도록 설정해야 한다.
  3. querydsl-apt 의존성을 추가하고 annotationProcessor 설정이 필요하다.

 

PagedModel

스프링 Web에서 권장하는 Page용 DTO 객체

직렬화, 역직렬화 시 발생하는 문제를 최소화

 

Page 객체 사용 시 꼭 확인해야할 세팅

@Transactional(readOnly = true) 또는 @Transactional 어노테이션 필수!

 

JPA와 QueryDsl 조합 Search 구현

JPA에서 일반적으로 사용하는 Method Query에 QueryDsl을 연동하여 Search를 간편하게 구현할 수 있다.

 

1. QuerydslPredicateExecutor

QuerydslPredicateExecutor는 Predicate를 JpaRepository에서 추출할 수 있도록 해준다.

 

Controller에서 Predicate와 Pageable을 매개변수로 받는다.

 

Service에서 Repository를 호출 할 때 Predicate와 Pageable을 전달한다.

 

2. BooleanBuilder

BooleanBuilder를 이용하여 검색조건 추가하거나 제한할 수 있다.

 

Service에서 BooleanBuilder를 선언하고 Predicate를 매개변수로 포함한다.

idList가 null이 아니라면 리스트를 In절로 검색하도록 추가한다.

stock (재고량)이 0개보다 큰 것만 조회하도록 제한한다.

 

3. QuerydslBinderCustomizer

QuerydslBinderCustomizer는 추출된 Predicate의 조건을 수정할 수 있다.

1, 2는 문자 검색 시 equals 조건만 검색이 되었다.

문자 검색 시 contains 조건으로 변경.

 

정리

  • QueryDSL의 다양한 메서드와 기능을 사용해서 Search를 쉽게 구현할 수 있는 방법과 대용량 처리 및 동적 쿼리 방법에 대해 배웠다.
  • 진행 중인 프로젝트에 위 예시들을 보면서 수정 적용하는 과정이 익숙하지 않아 어렵게 느껴졌다, 익숙해질 수 있도록 연습이 필요할 것 같다.