스프링 DB : Query DSL 관련

    들어가기 전

    이 글은 인프런 김영한님의 강의를 복습하며 작성한 글입니다. 

     


    Query DSL의 필요성

    JPA의 문제점은 다음과 같다.

    • 우리가 작성하는 쿼리는 문자열이다.
      • 따라서 타입 체크가 불가능하고, 잘못된 문법도 실행 전까지는 알 수 없다.
    • 에러는 크게 컴파일 에러, 런타임 에러가 있는 것으로 볼 수 있다.
      • 쿼리를 실행해서 알게 되는 것은 런타임 에러다. 런타임 에러는 나쁜 사용자 경험을 줄 수 있는 에러다.
    • SQL을 작성하기 위해서는 도메인의 필드를 모두 암기해야한다.
      • 이것은 한계가 있다. 

    만약 SQL이 클래스처럼 타입이 있고 자바 코드로 작성할 수 있다면 어떨까? 그리고 타입 체크까지 해준다면 어떨까? 이런 형태로 작성할 수 있게 된다면 컴파일 시 에러 체크가 가능해진다. Query DSL이 이런 것들을 가능하게 해준다. 쿼리를 Java로 type-safe하게 개발할 수 있게 지원하는 프레임워크다. 주로 JPA 쿼리(JPQL)에 사용한다.

     


    Query DSL 소개

    DSL은 도메인 특화 언어를 의미한다. 특정한 도메인에 초점을 맞춘 제한적인 표현력을 가진 컴퓨터 프로그래밍 언어를 의미한다. 따라서 Query DSL은 쿼리에 특화된 컴퓨터 프로그래밍 언어로 이해를 해볼 수 있다. 

    타입 세이프한 쿼리 타입을 생성하기 위해서는 추가적인 코드가 필요하다. Query DSL은 코드 생성기를 이용해서 도메인 객체를 Q도메인 객체로 생성해서 쿼리 타입을 생성해준다. 이 때 코드 생성기는 @Entity가 붙은 클래스를 읽어서 Member.java를 QMember.java 코드를 생성해준다.

    QueryDSL → JPQL 코드 생성 → JPQL 실행되면서 SQL 코드 생성 → SQL 코드 실행 

    실행은 다음과 같이 한다.

     


    QueryDSL JPA의 장/단점

    QueryDSL은 아래 장/단점이 존재한다. 

    • 장점
      • type-safe
      • 단순함
      • 쉬움.
    • 단점
      • Q코드 생성을 위한 APT(Annotation Processor Tool)를 설정해야함. 

     


    SpringDataJPA + Query DSL

    • SpringData 프로젝트의 약점은 복잡한 조회임.
    • QueryDSL로 복잡한 조회 기능 보완
      • 복잡한 쿼리
      • 동적 쿼리
    • 단순한 조회 쿼리인 경우 → SpringDataJPA를 사용
    • 복잡한 조회 쿼리인 경우 → QueryDSL를 직접 사용

    두 가지 데이터 접근 기술을 조합해서 각 기술의 장점만을 사용할 수 있게 된다. 

     


    QueryDSL 설정 및 사용

    QueryDSL은 사용하는 버전마다 설정하는 방법이 제각각이라고 한다. 현재는 QueryDSL을 사용하기 위해서 다음 작업을 수행해야한다. 

    build.gradle 설정

    먼저 build.gradle에 아래 코드를 추가해준다. 

    • QueryDSL은 APT(Annotation Processor Tool) 설정을 통해서 구현한다. 따라서 아래와 같이 필요한 Annotation Processor를 추가해줘야한다. 
    • QueryDSL을 하면 Q타입 클래스가 계속 생성된다. 그런데 Q타입 클래스는 도메인이 바뀔 때 마다 바껴야 되기 때문에 build 할 때 마다 삭제하고 다시 생성할 수 있도록 clean Task를 추가해준다. 
    // build.gradle
    
    
    dependencies {
    	
        ...
    
    	//Querydsl 추가
    	implementation 'com.querydsl:querydsl-jpa'
    	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
    	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
    
    }
    
    ...
    
    //Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
    clean {
    	delete file('src/main/generated')
    }

     


    Q타입 클래스 생성

    Q타입 클래스를 생성하는 방법은 두 가지가 존재한다.

    • Gradle로 생성
      • 아래 두 가지 작업을 순서대로 진행해야 함. 
        • Gradle -> tasks -> build -> clean : Q클래스 클린
        • Gradle -> tasks -> other -> compileJava : Q클래스 생성
      • Gradle로 생성할 경우 build/generated/sources/annotationProcessor/java/main 하위에 Q타입이 생성됨. 
    • IntelliJ IDEA를 이용해서 생성
      • 아래 세 가지 방법 중 하나만 하면 Q 타입이 생성됨. 
        • Build -> Build Project 
        • Build -> Rebuild
        • main(), 또는 테스트를 실행
      • 인텔리제이로 생성할 경우 src/main/generated 하위에 Q 타입이 생성됨.

     


    QueryDSL 사용 방법

    QueryDSL은 JPQL을 QType 클래스로 한번 감싸는 작업을 한다. 이 작업은 JpaQueryFactory를 이용해서 한다. 그런데 JPQL은 EntityManager를 이용해서 생성되기 때문에 JpaQueryFactor는 EntityManager가 필요하다. 따라서 QueryDSL을 사용하기 위해서 우선 아래 코드를 작성해야한다. 

    private final EntityManager em;
    private final JPAQueryFactory queryFactory;
    
    public JpaItemRepositoryV3(EntityManager em) {
        this.em = em;
        this.queryFactory = new JPAQueryFactory(em); // QueryDSL은 JPQL을 만들어주는 역할을 한다. JPA는 Entity Manager를 가지고 동작한다.
    }

     

     


    QueryDSL을 이용한 동적 쿼리 생성

    QueryDSL은 동적 쿼리를 손쉽게 생성할 수 있도록 아래 두 가지 방법을 지원해준다.

    • BooleanBuilder
    • BooleanExpression

    개인적으로는 코드 재활용이 더 용이한 BooleanExpression을 메서드로 뺀 후에 사용하는 것이 좋다고 생각한다. (https://ojt90902.tistory.com/695) 코드는 아래와 같이 작성할 수 있다. 

    • where 절에는 booleanExpression이 들어갈 수 있다. 만약 booleanExpression이 null이면 무시된다.
    • where 절에 booleanExpression을 나열하면 and 조건으로 들어간다.
    • where 절에 or 조건을 사용하고 싶다면, booleanExpression을 or() 메서드를 이용해서 연결한다. 
    public List<Item> findAll2(ItemSearchCond cond) {
        return queryFactory.select(item)
                .from(item)
                .where(
                        maxPrice2(cond.getMaxPrice()),
                        itemNameLike2(cond.getItemName()))
                .fetch();
    }
    
    private BooleanExpression maxPrice2(Integer price) {
        return price != null ? item.price.loe(price) : null; 
    }
    
    private BooleanExpression itemNameLike2(String name) {
        return StringUtils.hasText(name) ? item.itemName.like("%" + name + "%") : null;
    }

    QueryDSL의 예외변환

    QueryDSL은 JPQL을 작성해주는 역할을 한다. 따라서 JPA의 PersistenceException이 발생하는데, 이 Exception을 QueryDSL이 변환해주지는 않는다. @Repository 어노테이션이 있는 곳에서 에러가 발생하면 예외변환 AOP를 이용해서 PersistenceException은 스프링 예외로 변환된다. 


    정리

    • QueryDSL은 JPA의 동적 쿼리 기능을 손쉽게 사용할 수 있도록 해준다.
    • QueryDSL은 자바 코드로 쿼리를 작성할 수 있게 해준다. 컴파일 시점에 문제를 파악할 수 있다.
    • QueryDSL은 Q 클래스를 생성한다. Q 클래스를 통해서 도메인을 외우지 않아도 필요한 필드를 추론해서 사용할 수 있게 된다. 

    댓글

    Designed by JB FACTORY