콘텐츠로 이동

Querydsl

QueryDSL과 JPA 연동: 커스텀 리포지토리 구현 심층 분석

1. 기본 개념 및 용어 정의

  • QueryDSL: 타입 안전한 SQL 쿼리를 생성하는 프레임워크. 컴파일 시점에 오류 검출 가능.
  • JPA (Java Persistence API): 자바 객체와 관계형 데이터베이스 매핑을 위한 표준 인터페이스.
  • Spring Data JPA: JPA를 추상화하여 CRUD 작업을 간소화하는 프레임워크.
  • 커스텀 리포지토리: Spring Data JPA의 기본 메서드로 처리할 수 없는 복잡한 쿼리를 구현하기 위한 확장 패턴.

2. 잘못된 코드 예시 및 문제점

// ❌ 문제점 1: QuerydslRepositorySupport의 과도한 의존
public class UserRepositoryImpl extends QuerydslRepositorySupport implements UserRepositoryCustom {

    public UserRepositoryImpl() {
        super(User.class);  // EntityManager 주입 누락
    }

    @Override
    public List findUsersWithComplexCriteria(String firstName, Integer minAge) {
        QUser user = QUser.user;
        JPQLQuery query = from(user);  // ❌ 비효율적 쿼리 생성

        if (firstName != null) {
            query.where(user.firstName.eq(firstName));  // ❌ 동적 조건 처리 미흡
        }

        if (minAge != null) {
            query.where(user.age.gt(minAge));
        }

        return query.fetch();
    }
}
주요 문제점
  1. EntityManager 주입 누락: QuerydslRepositorySupport는 내부적으로 EntityManager를 사용하지만, 명시적 주입이 없어 NPE 발생 가능.
  2. 동적 쿼리 처리 미흡: if 문을 통한 조건 추가는 가독성을 해치고 유지보수 어려움.
  3. JPQLQuery 직접 사용: JPAQueryFactory를 사용하지 않아 타입 안전성과 유연성 저하.

3. 올바른 구현 방식 및 최적화 전략

3.1 JPAQueryFactory 사용
@Repository
public class UserRepositoryImpl implements UserRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public UserRepositoryImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);  // ✅ EntityManager 주입
    }

    @Override
    public List findUsersWithComplexCriteria(String firstName, Integer minAge) {
        QUser user = QUser.user;

        // ✅ BooleanBuilder로 동적 쿼리 구성
        BooleanBuilder builder = new BooleanBuilder();
        if (firstName != null) {
            builder.and(user.firstName.eq(firstName));
        }
        if (minAge != null) {
            builder.and(user.age.gt(minAge));
        }

        return queryFactory
            .selectFrom(user)
            .where(builder)
            .fetch();
    }
}
개선된 점
  • 의존성 주입: EntityManager를 통해 JPAQueryFactory 생성.
  • BooleanBuilder 활용: 동적 쿼리 구성이 명확해지고 확장성 증가.
  • 타입 안전성 강화: selectFrom()을 사용한 컴파일 시점 검증.

3. Spring Data JPA 통합
// ✅ 기본 리포지토리 확장
public interface UserRepository 
    extends JpaRepository, UserRepositoryCustom {
}

// ✅ 설정 클래스에 JPAQueryFactory 빈 등록
@Configuration
public class QuerydslConfig {

    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager em) {
        return new JPAQueryFactory(em);
    }
}
중요 포인트
  • 빈 등록 필수: JPAQueryFactory는 스프링 빈으로 등록해야 의존성 주입 가능.
  • 커스텀 인터페이스 분리: 비즈니스 로직과 기본 CRUD 작업을 명확히 분리.

4. 성능 최적화 팁

  1. 컴파일된 쿼리 사용: QClassstatic final로 선언되었는지 확인.
    private static final QUser user = QUser.user;
    
  2. 페이징 처리: offset(), limit()을 활용한 페이지네이션.
    .offset(pageable.getOffset())
    .limit(pageable.getPageSize())
    
  3. 벌크 연산: update(), delete() 절에서 execute() 호출 시 영속성 컨텍스트 초기화 필수.
    queryFactory.update(user).set(user.age, 30).where(...).execute();
    em.flush();
    em.clear();
    

5. 자주 묻는 질문 (FAQ)

Q. QuerydslRepositorySupport vs JPAQueryFactory 어떤 것을 사용해야 하나요?
A. JPAQueryFactory가 더 현대적인 접근 방식이며, 코드 가독성과 유지보수성이 우수합니다.

Q. 동적 쿼리를 구현할 때 BooleanBuilder 외 다른 방법은?
A. WhereClause와 람다를 결합한 메서드 체이닝 방식도 가능합니다.

return queryFactory
    .selectFrom(user)
    .where(
        firstName == null ? null : user.firstName.eq(firstName),
        minAge == null ? null : user.age.gt(minAge)
    )
    .fetch();

Q. fetch() vs fetchOne() vs fetchFirst() 차이는?
A. - fetch(): 전체 결과 리스트 반환 - fetchOne(): 단일 결과 반환 (결과 없거나 둘 이상이면 예외) - fetchFirst(): 첫 번째 결과 반환 (결과 없으면 null)


📌 심화 학습 제안

"QueryDSL에서 서브쿼리와 윈도우 함수를 효율적으로 사용하는 방법은 무엇인가요?"
복잡한 분석 쿼리 작성 시 서브쿼리와 윈도우 함수(ROW_NUMBER(), RANK())를 활용하면 성능을 크게 향상시킬 수 있습니다. PostgreSQL의 FILTER (WHERE ...) 절이나 MySQL의 OVER() 구문과의 통합 사례를 연구해 보세요.