QueryDSL

이경헌·2024년 8월 26일

N + 1 문제

  • N+1 문제는 데이터베이스 쿼리 최적화 문제로, 하나의 쿼리가 추가적인 N개의 쿼리를 유발하는 상황을 말한다.
  • 주로 ORM(Object-Relational Mapping) 도구에서 발생하며, 데이터 조회 시 불필요한 다수의 쿼리가 실행되어 성능 저하를 초래한다.

해결 방법

  • JPQL을 이용한 페치 조인, 엔티티 그래프, Querydsl의 .fetchJoin(), Batch Size 설정 등이 있다.
  • JPQL 페치 조인, 엔티티 그래프, Querydsl의 .fetchJoin() 모두 내부적으로 JPQL의 페치 조인을 사용하여 기본 엔티티와 연관된 엔티티를 한 번의 쿼리로 가져와 N+1 문제를 방지한다.

QueryDSL 사용이유

  • 컴파일 타임 검증: QueryDSL은 타입 안전한 쿼리를 작성할 수 있도록 돕습니다. 쿼리를 작성하는 과정에서 컴파일 타임에 SQL 문법 오류를 감지할 수 있어, 런타임 오류를 줄일 수 있습니다. 이는 코드 작성 시점에서 쿼리의 유효성을 검증해 줍니다.
  • 코드 가독성 및 유지보수성 향상: QueryDSL은 쿼리를 객체 지향적으로 표현할 수 있게 해 주므로, SQL 문자열을 직접 작성하는 것보다 코드의 가독성과 유지보수성이 높습니다. 쿼리가 객체 모델을 기반으로 작성되므로, 쿼리의 의미를 더 쉽게 이해하고 수정할 수 있습니다.
  • SQL Injection 공격 방지
  • 특정 엔티티의 객체를 하나만 조회하는 경우 JPA Repository를 사용해도 상관 없는데 그 이외의 정보도 같이 표현하려고 하는 경우 즉, Join이 필요한 경우의 쿼리 같은 경우에는 N+1 문제가 발생할 확률이 있어서 QueryDSL을 사용하는게 나은거 같다. JPA Repository를 사용하여 외래키를 가진 객체를 여러개 조회하는 경우도 N+1 문제가 발생

Projects 사용 이유
: QueryDSL에서 쿼리 결과를 DTO로 매핑하는 데 유용합니다. 특히, Projections.constructor()는 DTO의 생성자에 맞춰 쿼리 결과를 매핑할 수 있도록 합니다. 이 방법을 사용하면 Tuple 대신 원하는 DTO 객체를 직접 반환받을 수 있습니다.

public Page<GetBookSimpleResponse> findAllBooksByCategoryName(Pageable pageable, String categoryName) {
        List<GetBookSimpleResponse> result = queryFactory
                .select(Projections.constructor(
                        GetBookSimpleResponse.class,
                        book.bookId,
                        book.author.authorName,
                        book.bookTitle,
                        book.bookPrice,
                        book.bookSalePrice,
                        book.bookSalePercent,
                        image.imageUrl))
                .from(book)
                .join(bookCategory).on(bookCategory.book.eq(book))
                .join(bookCategory.category, category)
                .leftJoin(bookImage).on(bookImage.book.eq(book))
                .where(category.categoryName.eq(categoryName))
                .offset(pageable.getOffset()) // 페이지 시작점
                .limit(pageable.getPageSize()) // 페이지 크기
                .fetch();

        Long total = Optional.ofNullable(queryFactory
                .select(book.count())
                .from(book)
                .join(bookCategory).on(bookCategory.book.eq(book))
                .join(bookCategory.category, category)
                .where(category.categoryName.eq(categoryName))
                .fetchOne()).orElse(0L);

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

페이징 처리시 total을 따로 구하는 이유

  • JPARepository의 경우에도 조회할 쿼리와 전체의 개수를 구하는 쿼리를 따로 조회한다고 한다.
  • 하나의 쿼리로 요청을할 수 있으나 COUNT 쿼리는 데이터가 많은 경우 성능 저하가 있을 수 있으며, 별도의 쿼리로 수행함으로써 성능을 최적화할 수 있습니다.
  • 쿼리의 복잡성: 한 쿼리에서 COUNT와 DATA를 함께 조회하면 쿼리가 복잡해질 수 있으며, 유지보수 및 디버깅이 어려울 수 있습니다.
  • result의 값은 페이지 크기만큼만 추출하므로 전체 개수를 구할 수 없다.

0개의 댓글