ShimSeongbo / study

0 stars 0 forks source link

[YOUTUBE] 수십억건에서 QUERYDSL 사용하기 #10

Open ShimSeongbo opened 1 year ago

ShimSeongbo commented 1 year ago

[BooleanBuilder]

image

[BooleanExpression]

image

JPA는 Java Persistence API의 약자로, Java에서 데이터베이스와의 작업을 추상화하여 처리할 수 있도록 제공되는 API입니다. JPQL(Java Persistence Query Language)이라는 질의 언어를 제공하는데, 이를 통해 데이터베이스에 쿼리를 날릴 수 있습니다. 그런데 실제 개발을 진행하다 보면 동적으로 쿼리의 조건을 생성하고 싶은 경우가 많습니다.

여기서 동적 쿼리는 런타임 시점에서 쿼리의 조건이나 내용이 동적으로 결정되는 쿼리를 의미합니다. 예를 들면 사용자 입력에 따라 검색 조건이 변경되는 경우, 동적 쿼리를 사용하게 됩니다.

BooleanBuilder는 Querydsl라는 라이브러리에 포함된 클래스입니다. Querydsl은 JPA의 동적 쿼리 문제를 해결하기 위해 나온 라이브러리 중 하나로, 타입 세이프한 쿼리 작성을 지원합니다.

BooleanBuilder의 주요 특징과 사용 이유는 다음과 같습니다:

조건식 빌더: BooleanBuilder는 여러 조건을 동적으로 조합할 수 있게 도와줍니다. 예를 들어, 검색 필터마다 조건을 추가하거나 제거하는 로직을 쉽게 구현할 수 있습니다. 타입 세이프: Querydsl을 사용하면 쿼리의 문법 오류나 타입 불일치 같은 문제를 컴파일 시점에 발견할 수 있습니다. 가독성: Querydsl의 쿼리 문법은 자바의 문법과 유사하므로, SQL 쿼리를 작성하는 것보다 더 직관적이고 가독성이 좋습니다. BooleanBuilder를 사용하면 여러 조건을 동적으로 결합하거나 제외하는 것이 쉬워지므로, 동적 쿼리 작성 시 큰 장점을 제공합니다.

  1. BooleanExpression BooleanExpression은 사실상 Querydsl에서의 조건(또는 조건의 조합)을 표현하는 주체입니다. BooleanExpression 인스턴스 자체가 조건을 나타내며, 여러 조건을 and, or, not 등의 메서드를 사용하여 결합할 수 있습니다. 예: QUser.user.name.eq("John")는 "user의 이름이 John인지"를 확인하는 BooleanExpression을 반환합니다.

  2. BooleanBuilder BooleanBuilder는 여러 BooleanExpression들을 동적으로 추가하거나 결합하는 용도로 사용됩니다. BooleanBuilder는 내부적으로 BooleanExpression을 가지고 있으며, 초기 상태에서는 null입니다. 이후에 조건을 추가하면 그 조건이 BooleanBuilder 내의 BooleanExpression으로 설정됩니다. 여러 조건을 BooleanBuilder에 추가하면 내부의 BooleanExpression이 업데이트되어 여러 조건이 결합된 상태를 나타냅니다.

QUser user = QUser.user;
BooleanBuilder builder = new BooleanBuilder();

if (someCondition) {
    builder.and(user.name.eq("John"));
}

if (anotherCondition) {
    builder.and(user.age.gt(25));
}

// 쿼리 실행 시 사용
List<User> results = queryFactory.selectFrom(user)
                                .where(builder)
                                .fetch();

위의 코드는 모든 User를 조회하는 쿼리로 동작합니다.

반면에, BooleanExpression 자체가 null인 경우에는 주의해야 합니다. 만약 null BooleanExpression을 where 절에 직접 넣게 되면 NullPointerException이 발생할 수 있습니다. 따라서, 동적 쿼리 작성 시 BooleanExpression의 null 여부를 항상 확인해야 합니다.

즉, BooleanBuilder의 초기 상태를 걱정할 필요는 없지만, BooleanExpression의 null 여부는 주의 깊게 다뤄야 합니다. 결론적으로, BooleanExpression은 조건 자체를 나타내는 반면, BooleanBuilder는 여러 BooleanExpression들을 동적으로 조합하여 하나의 최종 조건을 생성하는 역할을 합니다. 동적 쿼리 작성 시, 조건의 유무나 조건의 종류가 런타임에 결정될 때 BooleanBuilder를 사용하면 편리합니다.

exist 메소드 금지

image

하지만, Querydsldml exists는 count() > 0 을 사용함.

image

exists vs count():

QueryDSL의 exists 메서드는 JPA 기반에서는 count() > 0로 변환됩니다. 이는, 전체 테이블을 스캔하여 개수를 세는 작업이 발생하기 때문에 성능상의 문제가 발생할 수 있습니다. 실제로 SQL의 EXISTS는 하나만 발견되면 바로 종료되는 반면, count()는 전체를 스캔하므로 효율적이지 않습니다.

조인 최적화:

QueryDSL로 쿼리를 작성할 때 조인(join)을 너무 많이 사용하면 복잡한 SQL 쿼리가 생성되어 성능 저하를 일으킬 수 있습니다. 필요한 조인만 사용하고, 필요하지 않은 연관관계는 페치하지 않는 것이 좋습니다.

N+1 문제:

특정 엔터티와 그 연관된 엔터티들을 조회할 때, N+1 쿼리 문제에 주의해야 합니다. 이는 한 번의 쿼리로 주 엔터티를 조회하고, 그 결과 각각의 엔터티에 대해 연관된 엔터티를 조회하는 쿼리가 추가로 발생하는 문제입니다.

쿼리 로깅:

개발 중에는 QueryDSL로 생성된 SQL 쿼리를 로깅하여 확인하는 습관을 가져야 합니다. 이렇게 하면 예상치 못한 쿼리가 생성되는 것을 방지할 수 있습니다. 결국, QueryDSL이나 JPA와 같은 고수준의 쿼리 추상화 도구를 사용할 때는 항상 성능에 주의해야 합니다. 이러한 도구를 사용함으로써 생산성은 향상되지만, 동시에 내부 동작 방식을 잘 이해하고 최적화하기 위한 추가적인 노력이 필요합니다.

성능 최적화를 위해 쿼리 세팅을 따로 조절하는 것이 맞습니다. 예를 들어, JPA의 @Query 애노테이션을 사용하여 JPQL이 아닌 네이티브 쿼리를 직접 작성하는 경우, 그러한 쿼리가 최적화된 쿼리인지 항상 확인해야 합니다.

ShimSeongbo commented 1 year ago

https://www.youtube.com/watch?v=zMAX7g6rO_Y&t=1135s

ShimSeongbo commented 1 year ago

N+1 문제

시나리오

User 엔터티와 그에 연관된 Order 엔터티가 있을 때, 특정 조건에 맞는 모든 User와 그들의 모든 Order를 조회하고자 한다고 가정합시다.

N+1 문제 발생 시나리오

List<User> users = userRepository.findAll();  // 1번의 쿼리 실행 (이 때, User에 연관된 Order는 조회하지 않음)

for (User user : users) {
    List<Order> orders = orderRepository.findByUserId(user.getId());  // User의 수 만큼 쿼리 실행
}

이 코드는 User를 모두 조회하는 쿼리를 한 번 실행하고, 각 User에 대해 그와 연관된 Order를 조회하는 쿼리를 N번 더 실행합니다. 따라서 총 쿼리 수는 N+1이 됩니다.

N+1 문제 해결 시나리오 (조인을 활용)

List<User> users = userRepository.findAllWithOrders();  // 조인을 사용하여 1번의 쿼리로 모든 User와 연관된 Order 조회

findAllWithOrders 메서드 내에서 JPQL이나 QueryDSL을 사용하여 UserOrder를 조인하는 쿼리를 작성하면, 한 번의 쿼리만으로 모든 필요한 데이터를 조회할 수 있습니다.

N+1 문제의 발생 원인

N+1 문제는 ORM의 지연 로딩(Lazy Loading) 전략 때문에 발생하기 쉽습니다. 지연 로딩은 연관 엔터티를 실제로 사용할 때까지 로딩을 연기하는 전략입니다. 따라서 연관 엔터티에 처음 접근할 때 추가적인 쿼리가 실행됩니다. 이러한 동작은 N+1 문제를 초래할 수 있습니다.

해결 방법

즉시 로딩(Eager Loading): 연관 엔터티를 즉시 로딩하는 전략을 선택하면, 원 엔터티를 조회할 때 연관 엔터티도 함께 조회됩니다. 하지만 이 방법은 항상 최적의 선택은 아닙니다. 때로는 필요하지 않은 데이터까지 로딩할 수 있기 때문입니다. 조인 사용: JPQL, SQL, 또는 QueryDSL을 사용하여 필요한 엔터티들을 조인하는 쿼리를 작성합니다.

EntityGraph

JPA 2.1부터는 EntityGraph 기능을 사용하여 특정 연관 엔터티를 로딩할지 여부를 동적으로 결정할 수 있습니다. 이러한 방법들은 상황에 따라 선택하고 활용해야 합니다.

결론

N+1 문제는 성능 저하의 주요 원인 중 하나이므로 ORM을 사용할 때 항상 주의하여야 합니다.