쿼리 성능 최적화 (QueryDSL)

ksngh·2024년 11월 14일

자바스프링

목록 보기
5/8

QueryDSL 도입기와 쿼리 최적화 배경

주문 상세 내역을 작성하는 로직을 최적화하기 위해 QueryDSL을 도입하였습니다. 복잡한 엔티티 간 조인을 사용해 menu, orders, users, restaurant 등 다양한 테이블의 데이터를 조회해야 했기 때문입니다.

기존 JPA가 제공하는 메서드만으로는 모든 컬럼 값을 가져오므로 비효율적이라 판단하여 직접 쿼리를 작성하기로 했습니다. 이때, 쿼리 작성 방식은 두 가지로 구분되었습니다: @Query 어노테이션과 QueryDSL 사용.

특성@Query 어노테이션QueryDSL
쿼리 작성 방식SQL/JPQL을 직접 작성메서드 체인을 통한 코드 기반 빌더 패턴 쿼리
타입 안전성낮음 - 쿼리 오류를 컴파일 타임에 체크할 수 없음높음 - 엔티티 기반 Q 클래스 사용으로 컴파일 타임 오류 체크 가능
동적 쿼리 작성제한적 - 동적 쿼리를 작성하기 어려움용이 - 조건을 쉽게 추가/변경 가능
간단한 쿼리효율적 - 간단한 조회나 정적 쿼리에 적합과도할 수 있음 - 복잡한 코드 구조가 필요할 수 있음
복잡한 쿼리비효율적 - 복잡한 조건의 쿼리 작성에 한계가 있음효율적 - 다양한 조건과 조인, 서브쿼리에 유리함

복잡한 쿼리와 동적 쿼리가 필요한 상황에 맞춰 QueryDSL을 도입하였고, 간단한 쿼리 메서드는 기존 JPA 메서드와 함께 사용하기로 결정했습니다.

querydsl 설정

우선, build.gradle에 다음과 같은 의존성을 추가해주어야 한다.

    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"
  
  • implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta': QueryDSL의 JPA 모듈을 추가합니다. 이는 JPA를 사용하는 프로젝트에서 QueryDSL을 활용하여 타입 안전한 쿼리를 작성할 수 있게 합니다.
  • annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta": querydsl-apt는 Q 클래스를 자동으로 생성하기 위한 annotation processor 역할을 합니다.
  • annotationProcessor "jakarta.annotation:jakarta.annotation-api"annotationProcessor "jakarta.persistence:jakarta.persistence-api": Jakarta API 의존성으로, QueryDSL에서 엔티티 주석을 해석하는 데 필요한 어노테이션을 제공합니다.

QueryDSL 코드 생성기 설정

QueryDSL에서 사용하는 Q 클래스를 src/main/generated 경로에 자동으로 생성하기 위한 설정입니다.

// QueryDSL 코드 생성기 설정
def generated = 'src/main/generated'

// QueryDSL 코드 생성기 태스크 추가
tasks.register("compileQuerydsl", JavaCompile) {
    source = sourceSets.main.java
    classpath = sourceSets.main.compileClasspath
    destinationDirectory = file(generated)
    options.annotationProcessorPath = configurations.annotationProcessor
}
  • def generated = 'src/main/generated': QueryDSL이 생성하는 파일들의 위치를 정의합니다.
  • tasks.register("compileQuerydsl", JavaCompile) {...}: compileQuerydsl이라는 커스텀 태스크를 등록하여 QueryDSL 관련 코드가 자동으로 생성되도록 합니다.
    • source: QueryDSL의 생성 대상이 되는 Java 소스 파일을 지정합니다.
    • classpathdestinationDirectory: 컴파일 경로 및 출력 디렉토리를 설정합니다.
    • options.annotationProcessorPath: annotationProcessor를 통해 QueryDSL의 Q 클래스가 생성되도록 설정합니다.

QueryDSL 코드 생성된 경로 설정

tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(generated))
    options.compilerArgs << '-parameters'
}

sourceSets {
    main.java.srcDirs += [ generated ]
}
  • tasks.withType(JavaCompile) {...}: QueryDSL을 통해 생성된 파일을 Java 컴파일 시 소스 디렉토리로 지정합니다.
    • options.getGeneratedSourceOutputDirectory().set(file(generated)): generated 디렉토리에 QueryDSL 소스가 생성되도록 설정합니다.
    • options.compilerArgs << '-parameters': 런타임 시 파라미터 정보를 유지하여 리플렉션을 활용할 수 있도록 설정합니다.
  • sourceSets {...}: src/main/generated 디렉토리를 Java 메인 소스셋에 추가하여, QueryDSL이 생성한 Q 클래스를 프로젝트에서 사용할 수 있도록 합니다.

clean 태스크와 QueryDSL 디렉토리 연동

clean {
    delete file(generated)
}
  • clean: clean 태스크가 실행될 때 generated 디렉토리의 파일을 삭제하여, 빌드 시 생성되는 파일을 관리합니다.

Q 엔티티와 QueryDSL의 원리

QueryDSL은 JPA 엔티티를 기반으로 Q 엔티티(Q 클래스)를 자동 생성합니다. 예를 들어 Order 엔티티가 있으면, QueryDSL은 QOrder 클래스를 생성하여 Order 엔티티의 모든 필드에 접근할 수 있도록 만듭니다. 이를 통해 타입 안전성을 보장하면서도 동적 쿼리 작성이 가능해집니다.

  • Q 엔티티는 각 엔티티의 필드와 구조를 반영하여 메서드 체인 방식으로 SQL을 작성할 수 있게 합니다.
  • order.status와 같은 필드를 직접 호출해 조건을 지정할 수 있어 쿼리의 오류를 컴파일 시점에 검증할 수 있습니다.

SQL 최적화와 QueryDSL 활용

초기 SQL 쿼리는 여러 엔티티와의 JOIN이 많아 N+1 문제성능 저하가 우려되었습니다.

SELECT 
    u.username,
    r.name AS restaurant_name,
    o.order_type,
    o.status,
    o.total_price,
    o.delivery_address,
    o.delivery_request,
    oi.quantity,
    m.name AS menu_name,
    m.price
FROM 
    p_orders o
JOIN 
    p_users u ON u.id = o.user_id
JOIN 
    p_restaurant r ON r.id = o.restaurant_id
JOIN 
    p_order_items oi ON oi.order_id = o.id
JOIN 
    p_menus m ON m.id = oi.menu_id;

이 문제를 해결하기 위해 FetchType을 EAGER로 변경하거나 필요한 컬럼만 가져오는 방법을 고려했습니다.

  1. FetchType을 EAGER로 변경하면 쿼리 속도가 향상될 수 있지만, 불필요한 데이터를 가져와 메모리 낭비가 발생할 수 있습니다.
  2. 필요한 컬럼만 가져오기는 메모리 사용을 줄일 수 있으나, 쿼리 속도가 다소 느려질 수 있습니다.

최종적으로 필요한 컬럼만 가져오는 방법을 선택하여, QueryDSL을 활용해 최적화된 쿼리를 작성하였습니다.


QueryDSL을 사용한 최적화 코드 예시

QueryDSL을 사용하여 필요한 정보만 조회하는 방식으로 최적화된 코드는 다음과 같습니다:

package com.delivery_project.repository.implement;

import com.delivery_project.entity.*;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.UUID;

@Repository
@RequiredArgsConstructor
public class OrderRepositoryCustomImpl implements OrderRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<Tuple> findOrderDetailsTuples(UUID orderId) {
        QOrder o = QOrder.order;
        QUser u = QUser.user;
        QRestaurant r = QRestaurant.restaurant;
        QOrderItem oi = QOrderItem.orderItem;
        QMenu m = QMenu.menu;

        return queryFactory
                .select(
                        u.username,
                        r.name,
                        o.orderType,
                        o.status,
                        o.createdAt,
                        o.totalPrice,
                        o.deliveryAddress,
                        o.deliveryRequest,
                        oi.quantity,
                        m.name,
                        m.price
                )
                .from(oi)
                .join(oi.order, o)
                .join(o.user, u)
                .join(o.restaurant, r)
                .join(oi.menu, m)
                .where(o.id.eq(orderId))
                .fetch();
    }
}

QueryDSL은 컴파일 시에 오류를 검증할 수 있어 안정성을 높여줍니다. 또한, 타입 안전성을 보장하여 코드 가독성과 유지보수성을 강화해 줍니다.


개선할 점

SQL 성능을 개선하기 위해 캐싱인덱싱을 활용할 수 있습니다.

  • 캐싱은 자주 사용되는 쿼리의 결과를 메모리에 저장해두고, 반복적인 데이터 조회 시 데이터베이스 부하를 줄일 수 있습니다.
  • 인덱싱은 자주 조회되는 컬럼의 위치를 기억하여, 빠르게 데이터에 접근할 수 있도록 최적화합니다.

쿼리 최적화는 특정 코드 작성뿐 아니라, 애플리케이션 전반에서 자주 사용되는 쿼리 패턴을 파악하여 캐싱이나 인덱싱과 같은 추가적인 최적화를 적용할 때 더욱 효과적입니다. QueryDSL을 도입함으로써 동적 쿼리와 복잡한 조인 상황을 개선하였지만, 캐싱과 인덱싱 또한 함께 고려하여 전체 성능을 개선하고 싶다는 생각을 하였습니다.

profile
백엔드 개발자입니다.

0개의 댓글