ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Data JPA에서 동적 검색 해결: 사용자 정의 리포지토리와 Querydsl의 도입기
    Jpa 2025. 7. 21. 16:48

     

    문제: Spring Data JPA만으로는 부족했던 복잡한 검색 조건

    처음엔 Order 엔티티에 대해 기본적인 CRUD와 간단한 검색 기능을 구현하는 데는 Spring Data JPA만으로도 충분했습니다. findByCustomerNameAndStatus()와 같은 메서드 이름 기반 쿼리 메서드도 직관적이고 잘 작동했습니다.

    하지만 프로젝트가 진행되면서 점점 더 다양하고 조합 가능한 검색 조건이 필요해졌습니다. 예를 들어 다음과 같은 요구사항이 생겼습니다:

    • 고객명, 주문 상태, 최소/최대 금액을 조합해 검색
    • 특정 조건이 없을 경우 해당 조건은 무시
    • 조건이 유동적으로 바뀌므로 쿼리를 조립할 수 있어야 함

    Spring Data JPA에서 이 요구사항을 처리하려면 @Query를 사용해야 했지만, 다음과 같은 문제점이 발생했습니다

     

     

    Spring Data JPA만 사용할 때 겪은 한계

    1. 메서드 이름이 너무 길어짐→ 조합이 늘어날수록 메서드가 기하급수적으로 늘어났습니다.
      • 예: findByCustomerNameAndStatusAndTotalAmountGreaterThanEqualAndTotalAmountLessThanEqual
    2. 동적 쿼리 구현이 어렵고 유지보수가 힘듦
      • @Query를 조건별로 분기해서 작성하기는 불가능에 가까웠습니다.
      • 파라미터가 선택적일 경우 null 체크를 수동으로 처리해야 했고, 로직이 매우 지저분해졌습니다.
    3. 타입 안정성 부족
      • JPQL 문법 오류는 런타임에나 알 수 있어 디버깅이 힘들었습니다.

     

     


     

     

    해결책: Querydsl + 사용자 정의 리포지토리

    이 문제를 해결하기 위해 Querydsl과 사용자 정의 리포지토리를 도입했습니다. 특히 Querydsl은 다음과 같은 이유로 적합했습니다.

     

     

    Querydsl 도입 후

    1. 동적 쿼리를 자바 코드로 자연스럽게 작성

    기존에는 SQL 또는 JPQL 문자열을 조합해야 했지만, Querydsl에서는 자바 코드로 조건을 조립할 수 있습니다.

     
    BooleanExpression statusEq = StringUtils.hasText(status) ? order.status.eq(status) : null;

     

     

    2. IDE 자동완성 및 컴파일 단계 오류 검출

    Querydsl은 엔티티를 기반으로 Q타입 클래스를 생성합니다. 덕분에 다음과 같은 장점이 있습니다

    • 오타를 IDE에서 즉시 잡아줌
    • 잘못된 조인, 잘못된 컬럼명 등을 컴파일 단계에서 오류로 인식

     

    3. 가독성과 유지보수성 향상

    기존의 if-else나 StringBuilder 기반 쿼리 조립은 가독성이 떨어졌지만, Querydsl을 사용하면서 다음처럼 깔끔해졌습니다.

    return queryFactory
        .select(new QOrderInfoDto(...))
        .from(order)
        .leftJoin(order.customer, customer)
        .where(
            customerNameEq(condition.getCustomerName()),
            orderStatusEq(condition.getStatus()),
            amountGoe(condition.getMinAmount()),
            amountLoe(condition.getMaxAmount())
        )
        .fetch();
     

    조건 조합도 별도 메서드로 분리되어 깔끔하고 테스트도 쉬워졌습니다. 조건이 추가되거나 변경될 때의 확정성 또한 증가했습니다. 

     

     

     


     

     

    그렇다면, Querydsl을 Spring Data JPA에 어떻게 적용할까?

    Querydsl의 장점을 프로젝트에 도입하기로 결정했지만, 단순히 라이브러리를 추가하는 것만으로 끝나지 않습니다.
    Querydsl은 기본적으로 JPAQueryFactory를 이용해 쿼리를 생성하지만, Spring Data JPA의 표준 Repository 인터페이스에는 이를 직접 사용할 수 있는 구조가 없습니다.

    이 문제를 해결하기 위해 필요한 것이 바로 사용자 정의 리포지토리(Custom Repository)입니다.

     

     

    사용자 정의 리포지토리가 필요한 이유

     

    Querydsl을 활용한 복잡한 쿼리를 작성하려면 JPAQueryFactory를 사용해야 하고, 이를 담을 수 있는 구현 클래스가 필요합니다.
    하지만 스프링에서는 Repository 구현체를 직접 구현할 수 없기 때문에, 별도로 사용자 정의 인터페이스와 구현 클래스를 만들어야 합니다.

     

     

     

     

    사용자 정의 리포지토리란?

    간단히 말해, 기존 JpaRepository가 처리하지 못하는 복잡한 로직(Querydsl 등)을 처리하기 위해 우리가 직접 만든 인터페이스 + 구현체를 말합니다.

    Spring Data JPA는 매우 유연하게 설계되어 있어서, 우리가 다음과 같은 구조로 사용자 정의 리포지토리를 만들면 자동으로 연결해 줍니다.

     

    구조 요약

    [1] 사용자 정의 인터페이스
       └ OrderRepositoryCustom ← Querydsl 메서드 정의
    
    [2] 사용자 정의 구현체
       └ OrderRepositoryImpl ← Querydsl 쿼리 작성
    
    [3] 메인 Repository
       └ OrderRepository extends JpaRepository + OrderRepositoryCustom
     

     

     

     


     

     

    사용자 정의 리포지토리를 통해 Querydsl 통합하기

    Querydsl을 스프링 리포지토리에서 사용하기 위해서는 다음과 같은 3단계가 필요합니다

     

     

    1. 사용자 정의 인터페이스 작성

    먼저, Querydsl 기반의 검색 메서드를 담을 사용자 정의 인터페이스를 생성합니다.

    public interface OrderRepositoryCustom {
        List<OrderInfoDto> search(OrderSearchCondition condition);
    }

     

     

    2. 사용자 정의 인터페이스 구현

    그다음, 위 인터페이스를 실제로 구현하는 클래스를 작성합니다.
    반드시 OrderRepositoryImpl처럼, 기존 Repository 이름 + Impl로 만들어야 Spring Data JPA가 이를 자동으로 인식해 연결해 줍니다.

    public class OrderRepositoryImpl implements OrderRepositoryCustom {
    
        private final JPAQueryFactory queryFactory;
    
        public OrderRepositoryImpl(EntityManager em) {
            this.queryFactory = new JPAQueryFactory(em);
        }
    
        @Override
        public List<OrderInfoDto> search(OrderSearchCondition condition) {
            return queryFactory
                .select(new QOrderInfoDto(
                    order.id,
                    order.status,
                    order.totalAmount,
                    customer.name
                ))
                .from(order)
                .leftJoin(order.customer, customer)
                .where(
                    customerNameEq(condition.getCustomerName()),
                    orderStatusEq(condition.getStatus()),
                    amountGoe(condition.getMinAmount()),
                    amountLoe(condition.getMaxAmount())
                )
                .fetch();
        }
    
        private BooleanExpression customerNameEq(String name) {
            return StringUtils.hasText(name) ? customer.name.eq(name) : null;
        }
    
        private BooleanExpression orderStatusEq(OrderStatus status) {
            return status != null ? order.status.eq(status) : null;
        }
    
        private BooleanExpression amountGoe(Integer min) {
            return min != null ? order.totalAmount.goe(min) : null;
        }
    
        private BooleanExpression amountLoe(Integer max) {
            return max != null ? order.totalAmount.loe(max) : null;
        }
    }

     

     

    3. 스프링 리포지토리에 사용자 정의 인터페이스 상속

    마지막으로, 기존의 OrderRepository에 사용자 정의 인터페이스를 함께 상속시킵니다.

     
    public interface OrderRepository extends JpaRepository<Order, Long>, OrderRepositoryCustom {
    }

     

     


     

     

     

    정리

    Spring Data JPA는 간단한 CRUD나 정적 쿼리에는 매우 강력한 도구입니다. 하지만 프로젝트가 커지고 검색 조건이 복잡하고 유동적이 될수록, 기존 방식으로는 한계에 부딪히게 됩니다.

    이러한 문제를 해결하기 위해 도입한 Querydsl + 사용자 정의 리포지토리를 도입했고 다음과 같은 이점이 있습니다.

    • 복잡한 동적 조건을 자바 코드로 간결하게 표현
    • IDE 자동완성과 컴파일 타임 오류 검출로 안정성 확보
    • 쿼리 가독성과 유지보수성 크게 향상
    • 확장 가능한 구조 덕분에 조건 추가/변경도 유연하게 대응

     

    복잡한 검색이 필요한 상황이라면, Querydsl은 필수가 될 수 있습니다.
    그리고 Spring Data JPA와의 자연스러운 연결 고리는 사용자 정의 리포지토리가 담당합니다.

     

     

     

     

     

     

     

Designed by Tistory.