스프링/Spring-boot

A-Z Query dsl 학습하기 그리고 이해하기

지팡구 2023. 1. 31. 10:32
본 내용은 infrearn 김영한 님의 query dsl 강의를 수강 후 정리한 글입니다. 코드 위주의 설명보다는 해당 강의를 보고 얻게된 지식이나 인사이트와 관련된 글입니다.
또한 Query dsl에 관한 저의 느낀점과 학습한 점을 정리한 것이라 Query dsl 사용 관련 내용을 담고 있지는 않습니다..

목차


    Query dsl 들어가기

     

    네? query dsl이요? 그게 뭔가요?

     

    querydsl을 알게 된 것은 작년 이맘 때쯤이였다..  

    졸업작품 프로젝트를 시작하고, 어떻게 데이터를 검색할까에 관한 물음의 답이 바로 Query dsl이였다.

     

    기술적 의사결정을 내린 후 본격적으로 query dsl을 프로젝트에 적용하기로 했었고, 팀원중 한명이 도입을 했다. 

    그 당시의 나는 query dsl에 관련된 지식이 없었기에 먼저 나서서 적용하겠다 라고 할 수가 없었다. 

     

    그래서 어찌 저찌 팀원이 적용한 코드를 기반으로 내가 맡은 부분에도 활용하게 되었고, 아 꼭 공부를 해야겠다고 생각했는데, 1년이 지난 지금 학습을 하게 되어 참 유감이다. 그래서 Query dsl을 학습하면서 알게된 내용과, 느낀점, 간단한 코드 기반? 등의 내용을 정리하고자 이렇게 학습 내용을 기록한다. 

     


    Query dsl의 이야기

     

    Query dsl은?

    JPA 진영에서 SQL의 쿼리 작성을 도와주는 프레임 워크중 하나이다. 그럼 Jpa를 이용하면 되는거 아니냐? 할 수있는데,  직접 Query를 작성하다보면 아.. 이는 답이 없음을 느끼게 된다. ( 완전 소규모의 프로젝트 같은 경우엔 그냥 혼자 뚝딱하고 짜면 되는데, 만약에 연관된 테이블이냐 여러 테이블의 데이터를 긁어와야할 땐?.. 그냥 join문 남발할꺼냐?)

     

    이런 경우가 생기면 수 많은 쿼리를 직접 작성해야하고, 컴파일 단계에서 오류를 잡아내기도 하지만, 직접 데이터를 확인하지 않는 시점까지 가야만 정확하게 쿼리를 짰는지 확인이 가능하다.

     

    Query dsl 은 이러한 상황을 query를 자동으로 생성해줌으로써, 조금더 안전하고 정확한 작성이 가능하고, 또한 개발자의 수고를 덜어준다는 점이 장점이 있다. 

     

     

     

    JPA 진영의 여러 데이터 관련한 친구들의 이야기를 해보면서 더 나아가보자... 

     

     

    JPA

    앞서 이야기 한 것처럼 Query dsl은 JPA 진영에 속해있다. Query dsl 외에도 뭐 JPQL, Native Query, Mybatis, JDBC API.. 등등 많은 라이브러리들이 존재하는데, 각자의 장 단점이 존재한다.  많이 사용하는 것이 JPQL이라는 친구이다.

     

     

    여기서 핵심은 JPA는  Entity 중심의 개발이라는 것이다. ( 객체 중심 )

     

    여기서 궁금했던 점 발생!! ( 해결 )

    아니 Entity 중심의 개발 ( JPA ) ?  vs Table 중심의 개발 ( SQL ) 

    엔티티 ( 객체 ) 는 테이블의 한 행이다. 이 행을 중심으로 개발을 한다는 점. - JPA

    SQL은 테이블 전체를 대상으로 Query 문을 날린다. 

    전체를 대상으로 query를 날릴것이냐, 혹은 객체 하나를 대상으로 query를 날릴 것이냐? 

     

     

    JPA의 특징인 Entity 중심의 개발로 인해서 데이터를 검색해야하는 조건이 생기면 SQL을 사용해야하며, 이 때 테이블 대상이 아닌 Entity를 대상으로 검색을 해야한다. 결국 JPA에서 원하는 데이터를 불러오려면 검색 조건이 포함된 SQL을 날려야 하는데, JPA에서는 이를 JPQL을 통해 해결한다. 

    (일반적인 SQL 같은 경우엔 Table 중심으로 query문을 날리지만, 앞서 설명한 JPQL은 Entity 즉 객체를 대상으로 query를 날린다.) 

     

    문법으로 봤을땐 SQL과 비슷하지만, 사용에 있어 다른점이 조금씩 존재한다. 

     

    경험한 다른 바로는 일단 우선 JPQL에서는 fetch join (  페 치 조 인) 이라는 것이 존재한다. 이는 여러 연관된 데이터를 이용해서 query를 튜닝할 때 많이 사용한다. ( 나중에 fetch join 관련해서도 다뤄 보겠다. )

     

     

    아니 이야기가 산으로 갔는데, 다시 Query dsl로 돌아와서, 쩄든 JPA 진영에서 다양한 ORM들과 라이브러리를 제공해준다.  Query dsl는 이런 JPA 진영의 SQL, JPQL을 코드로 작성할 수있도록 도와주는 빌더 Api 역할을 하고, 실용적이다. 

     

    우선 이 Query dsl의 장점은 문자가 아닌 코드로 작성을 통해서 가독성이 좋고, 간편하다.

    • 컴파일 시점에 문법 오류 발견
    • 코드의 자동 완성으로 인한 생산성 향상
    • 단순, 명확, 그리고 쉽다
    • 동적쿼리

    그렇다고 해서 무조건 Query dsl에 의존하냐 ? 그건 아니다. 

    복잡한 검색은 query dsl을 통해 성능을 끌어올려서 사용하며, 간단한 검색이나 혹은 간단한 JPA을 이용한다. 

     

     

    Query dsl

    Query dsl을 간단히  정리해보면 ??!!  다음과 같다.

    ( 제 학습 스펙트럼에서의 총 입니다..)

    • query dsl은 JPA 진영에서 SQL 관련 작업을 도운다
    • 현업에서는 query dsl, JPA를 적절히 섞어서 쓴다는 점
    • Query dsl은 데이터를 튜닝할 때, 주로 사용한다는 점 ( 데이터 튜닝 = 성능 개선)

     

    또한 query dsl을 위해서 Repository를 Custom 해서 사용 가능하다. (  사용자 정의 레포지토리 )

     

    출처 : 인프런 - 김영한님의 query dsl

     

    MemberRepository는 JpaRepository와 MemberRepositoryCustom을 상속받아서 사용하고있다.

    MemberRepositoryImpl은 MemberRepositoryCustom의 구현체 

     

    이렇게 사용함으로써 확장성이 더욱 증가했다는 점, 내 입맛에 맞출 수 있다는 점

     


    또한 Query dsl을 사용하기 위해선 Entity 객체를 Q Type으로 만들어 줘야한다. ( 아래 의존성 등록 부분 참고)

    근데 Entity만 Q 타입으로 등록할 수 있냐? 그렇지 않다 -> DTO도 Q 타입으로 만들수있다.

    하지만 이렇게 DTO의 생성자에 해당 어노테이션을 작성해서 사용하게 되면 순수 Dto가 아닌 Query dsl에 의존성을 담고 있는 단점이 발생한다. 

     

     

    그럼 DTO를 Q type으로 만들면 어떤 장점이 발생하냐?  -> 반환 타입을 DTO로 할 수있다는 점, 컴파일러로 타입을 체크가능하다.

     


    검색 조건을 직접 만들 수 있고, 메소드를 분리할 수있다. 

     

    검색 조건이 단건이면 괜찮은데 여러 조건이면 and()를 이용하게 된다. 

     

    이러한 조건이 있다고 가정해보자

    사용자를 검색하는데, 사용자 이름 데이터가 null이 아니라면 builder에 and 조건절이 붙는다

    ( 밑에 나이 파라미터도 마찬가지)

     

    이러한 검색 조건을 

     

    삼항 연산자 사용

     

    윗 코드처럼 메소드로 만들어서 사용하게 되면 아무래도 재사용성에 유리한 이점이 있고,

    해당 로직의 가독성이 증가한다.

     


     서브 쿼리를 사용할 때는 JPAExpressions를 사용하면 된다

     

    여기서 주의할 점. 서브 쿼리절의 Alias가 중복되면 안 됌.

    ( 추가적으로 학습하다가 서브쿼리에서 IN조건절을 사용할 시 성능이 떨어진다는 것을 알게 되었다. )

    http://jason-heo.github.io/mysql/2014/05/22/avoid-mysql-in.html

     

    select, where 절에는 sub query를 적용할 수 있고, from 절에는 JPA JPQL의 한계점으로 서브쿼리를 지원하지 않는다.

     

    from 절의 서브쿼리 해결방안

    • 1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
    • 2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
    • 3. nativeSQL을 사용한다.

    프로젝션 결과 반환 관련

     

    대상 하나를 조회하게 되면 타입을 명확하게 지정할 수 있다.

    그러나 대상이 둘 이상일 경우 타입은 Tuple로 바뀌게 된다. 


    DTO를 조회할 때?

     

    순수 JPA에서는 DTO를 조회할 때, new 명령어를 사용해야하며, dto 패키지의 이름을 다 적어야해서 지저분하다.

    또한 생성자 방식만 지원한다는 점!

     

    이러한 문제? 불편한? 부분들을 @QueryProjection 을 이용해 DTO를 Q 타입으로 만들 수있어 가독성이 증가한다.

    그러나 Query dsl에 DTO가 의존적으로 변한다는 점 !  ( DTO를 Q 파일로 생성해야하니까 )

     

    3가지 방식이 존재

     

    • 프로퍼티 접근
    • 필드 직접 접근
    • 생성자 사용

    BooleanBuilder를 이용한 동적 쿼리

     

    순수 JPA에서 동적 쿼리를 해결하는 2가지 방식을 제공한다

    1. BooeleanBuilder

    2.  where 다중 파라미터

     

     

    먼저 1번 BooleanBuilder이다. 

        // 들어온 파라미터의 조건에 따라서 값이 바뀌어야한다 .
        BooleanBuilder builder = new BooleanBuilder();
    
        // and 조건
        if (nameParam != null) {
          builder.and(member.username.eq(nameParam));
        }
        if (ageParam != null) {
          builder.and(member.age.eq(ageParam));
        }
        return queryFactory
            .selectFrom(member)
            .where(builder)
            .fetch();

     

    윗 코드처럼 BooleanBuilder 를 이용해 유연하게 and 조건 절을 만들어 낼 수 있다.

     

    다음은 2번 Where 다중 파라미터이다

    private BooleanExpression usernameEqual(String nameParam) {
      return nameParam != null ? member.username.eq(nameParam) : null;
    }
    
    private BooleanExpression ageEqual(Integer ageParam) {
      return ageParam != null ? member.age.eq(ageParam) : null;
    }
    
    // 이렇게 메소드를 분리하면 아래와 같은 장점이 생김 ( 조립이 가능하다는 점. )  + ( 재사용성에서도 유리한 이점이 있다 ) + ( query의 가독성이 증가한다 )
    private BooleanExpression allEqual(String nameParam, Integer ageParam) {
      return usernameEqual(nameParam).and(ageEqual(ageParam));
    }

    아무래도 2번 방식이 더욱 쿼리 자체의 가독성도 높아지고 재사용하기에 유리하다. ( 1번 같은 경우엔 해당 메서드 속에서 생성 주기를 같이 하기 때문에)

     

    여기서 궁금한 점 또 발생!! ( BooleanExpression과 Predicate의 차이점)

    우선 윗 코드에서 반환 타입을 BooleanExpression말고 Predicate를 사용해도 똑같은 결과를 얻을 수 있다. ( 동적쿼리 )

    그럼 둘의 차이가 무엇인데 왜 사람들이 BooleanExpression을 선호할까? 

     

    우선 이 BooleanExpression은 Predicate를 상속받아서 사용하고 있다 ( 자식입장 )

     

     

    위에서 코드를 보면 BooleanExpression타입의 2개의 메소드를 조합해서 다른 메소드를 만들 수있는데, 이러한 장점이 있다 ( 재사용성 ) 또한 만약 동적 쿼리에서 null을 반환할 시 where절에서 조건이 무시되기에 안전하다. 

     


    벌크연산 ( 수정 & 삭제)

     

    벌크 연산은 말 그대로 여러 개의 데이터를 한번에 수정하거나, 삭제하는 등의 행위를 말하는데, 여기서 주의할 점이 있따. 바로 벌크 연산 수행 시 영속성 컨텍스트에 있는 Entity를 무시하고 실행되기에 배치 쿼리를 실행하면 영속성 컨텍스트를 초기화 해서 다시 불러아야한다. ( DB의 데이터 = 최신, 영속성 컨택스트 = 이전 데이터)

     

    갑자기 문득 궁금했다

    아니 왜 스프링에서 영속성 컨텍스트라는 개념을 만들었을까? ( 기억이 날 듯 ,말 듯 해서 다시 찾아봄,)

    정답 : DB와 관련된 작업 수행 시 발생하는 많은 리소스를 줄이기 위해서 영속성 컨택스트 내부에 1차 캐시를 두고, 1차 캐시에서 데이터를 우선으로 찾고, 없다면 데이터를 불러온다. 그 후 다시 캐시 내부에 저장함으로써 효율적인 작업이 가능하다는 점!

     

    1차 캐시 : 영속성 컨택스트 내부의 엔티티 보관 저장소

    (트랜잭션의 생명주기랑 같이함, 같은 엔티티가 있으면 객체의 동일성 보장)

     

    2차 캐시 : 애플리케이션 종료시까지 생명주기를 같이 함. 

    ( 동시성을 극대화 하기위해 캐시 한 객체를 직접반환하지 않고 복사본을 만들어서 반환 = 동시성 이슈로 인해서 )

     


    < 김영한님의 실무 경험을 정리 >

     
    • 수 조 단위의 정산, 크리티컬한 결제 시스템도 JPA로 다 처리한다. ( SpringBoot + JPA + QueryDSL 기본 )
    • JPA로 실무를 하다 보면, 테이블 중심에서 객체 중심으로 개발 패러다임이 변화된다.
    • 유연한 데이터베이스 변경의 장점과 테스트
      • Junit 통합 테스트시에 H2 DB 메모리 모드로 돌려서 사용한다.
      • 로컬 PC에는 H2 DB 서버 모드로 실행한다.
      • 개발 운영은 MySQL, Oracle로 한다.
      • 데이터베이스 방언을 설정만 바꾸면 가능한 일이다.
    • 데이터베이스 변경 경험(개발 도중 MySQL -> Oracle로 바뀐적도 있다.)
    • 테스트, 통합 테스트시에 CRUD를 믿고 간다.(내가 짠 쿼리는 그것 마저 테스트를 거쳐 가야 한다.)
      • 이런거 테스트 할 시간에 CRUD 믿고, 핵심 비즈니스 테스트 코드를 열심히 짜자.
    • 빠른 에러 발견
      • 쿼리 때문에 문제가 발생한적이 한번도 없다.
      • 컴파일 시점에 대부분 오류를 발견할 수 있다.
      • 늦어도 애플리케이션 로딩 시점에 발견한다.
      • 최소한 뭐리 문법 실수나 오류는 거의 발생하지 않는다.
    • 대부분이 비즈니스 로직의 오류이다.

    성능

    • JPA 자체로 인한 성능 저하 이슈는 거의 없다.
    • 성능 이슈 대부분은 JPA를 잘 이해하지 못해서 발생한다.
      • 즉시로딩: 쿼리가 튄다. -> 지연 로딩을 변경한다.
        • 어노테이션 수정으로 바로 적용 된다.
      • N + 1 문제 -> 대부분 페치 조인으로 문제를 해결한다.
        • 이 부분, 이부분 페치 조인으로 바꾸자. SQL 베이스로 프로젝트를 짰다면 갈아 엎어야 한다.
      • 하나의 쿼리에 집중해서 성능을 고민하는게 아니라, JPA를 사용해서 빠르게 개발한 다음에
      • 실제 성능 테스트를 진행하고, 병목이 발생하는 구간들을 찾아서 설정을 빠르게 변경하고 다시 반복해서 성능 테스트 할 수 있다.

    생산성

    • 단순 코딩 시간이 줄어든다. -> 개발 생산성이 향상 된다. -> 잉여 시간이 발생한다.
    • 비즈니스 로직 작성시 흐름이 끊기지 않는다.
      • SQL 중심으로 개발하다 보면, 비즈니스 로직 자바로 짜다가 머리의 컨텍스트가 SQL로 스위칭 된다.
      • 다시 자바로 온다, 다시 SQL로 간다. 병목 발생하고 커피마시러 간다. 야근한다.
    • 남는 시간에 더 많은 테스트를 작성하고
      • 순수 자바 코드가 많아져서 테스트 코드 만들기에 용이하다.
    • 남는 시간에 기술 공부를 하고
    • 남는 시간에 코드에 금칠을 하고
    • 팀원 대부분은 다시는 과거로 돌아가고 싶어하지 않는다.
    • 이 얘기는 JPA를 잘 공부하고, 잘 알고 프로젝트를 진행 했을 경우 이야기이다. 대부분 잘 모르고 진행하면 JPA를 원망한다.

    많이 하는 질문

    • ORM 프레임워크를 사용하면 SQL과 데이터베이스는 잘 몰라도 되나요?
      • 더 잘 알아야 한다. SQL과 DB를 모르고 ORM을 사용하는건 말이 안된다. 객체와 DB를 잘 알고 그것을 더 편하게 사용하려고 ORM을 사용하는 것이다. 결국 둘 다 잘해야 된다.
    • 성능이 느리지 않나요?
      • 잘 쓰면 최적화 할 수 있는 포인트가 더 많다.
    • 통계 쿼리처럼 매우 복잡한 SQL은 어떻게 하나요?
      • 거의 다 QueryDSL로 처리하고, DTO로 뽑아낸다.
      • 정말 안될 경우 네이티브 쿼리 사용한다.
    • MyBatis와 어떤 차이가 있나요?
      • JPA를 사용하면서 같이 써도 된다. 다만, flush 같은 이슈 처리들을 잘 고려 해야 한다.
      • MyBatis는 쿼리를 직접 다 짜야 하지 않나.
    • 하이버네이트 프레임워크를 신뢰할 수 있나요?
      • 쿠팡, 배민 거래량이 조단위로 들어가는 회사들에서 기본으로 JPA 깔고 간다.
    • 제 주위에는 MyBatis(iBatis, myBatis)만 사용하는데요?
      • 주변에 JPA 할 줄 아는 사람이 있어야 같이 적용하지, 사실 쿼리중심으로 개발하는 지금의 SI에서는 힘들다.
    • 학습 곡선이 높다고 하던데요?
      • 일주일 공부하고 평생 시간 아끼자.
      • 어려운 포인트는 찾아서 딥하게 공부하자.
      • 시간이 절약되는 만큼 비즈니스 로직을 고민할 수 있는 시간이 늘어난다.

    하지만 우리는 query dsl을 사용하려면 많은 작업들이 필요하다..

    1. 먼저 build.gradle 파일 설정 

    buildscript { // query dsl 추가 
        ext {
            queryDslVersion = "5.0.0"
        }
    }
    
    
    plugins {
        id 'java'
        id 'org.springframework.boot' version '2.7.7'
        id 'io.spring.dependency-management' version '1.0.15.RELEASE'
        //querydsl 추가
        id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    
    }
    
    group = 'study'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '11'
    
    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        compileOnly 'org.projectlombok:lombok'
        runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
    
        // query dsl
        implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
        annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}"
    
    }
    
    tasks.named('test') {
        useJUnitPlatform()
    }
    
    //querydsl 추가 시작
    def querydslDir = "$buildDir/generated/querydsl"
    
    querydsl {
        jpa = true
        querydslSourcesDir = querydslDir
    }
    sourceSets {
        main.java.srcDir querydslDir
    }
    compileQuerydsl {
        options.annotationProcessorPath = configurations.querydsl
    }
    
    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
        querydsl.extendsFrom compileClasspath
    }
    //querydsl 추가 끝

     

    2.  Q 클래스 생성

     

    3. 마지막으로 EntityManager를 주입

    이렇게 하면 기본적으로 사용이 가능하고, 세부적인 문법이나 사용법은 다른 블로그를 참고하는 것이 좋다.

    ( 이 블로그에선 자세하게 다루진 않을 것이다 ) 

     

     

     

     


    Query dsl 마무으리

     

     

    깃 주소 첨부 :  https://github.com/jipang9/querydsl.git


     

    < 이번 쳅터 참고  레퍼런스 >

    https://ict-nroo.tistory.com/117

     

    [JPA] Spring Data JPA와 QueryDSL 이해, 실무 경험 공유

    Spring Data JPA와 QueryDSLJPA 기반 프로젝트Spring Data JPAQueryDSLSpring Data JPA지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결개발자는 인터페이스만 작성한다스프링 데이터 JPA가 구현 객체를 동적으

    ict-nroo.tistory.com

    https://www.inflearn.com/course/ORM-JPA-Basic

     

    자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

    JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

    www.inflearn.com

    https://www.youtube.com/watch?v=WfrSN9Z7MiA&list=PL9mhQYIlKEhfpMVndI23RwWTL9-VL-B7U 

     

    https://velog.io/@youngerjesus/%EC%9A%B0%EC%95%84%ED%95%9C-%ED%98%95%EC%A0%9C%EB%93%A4%EC%9D%98-Querydsl-%ED%99%9C%EC%9A%A9%EB%B2%95

     

    우아한 형제들의 Querydsl 사용법

    이 글은 "우아한테크콘서트2020 수십억건에서 Querydsl 사용하기" 와 발표자이신 이동욱님의 기술 블로그를 보고 작성한 글입니다. 모든 예제와 추가로 Querydsl 사용 문법은 https://github.com/Youngerjesus/Q

    velog.io


     

     

    < 다음 학습 레퍼런스 >

    https://ict-nroo.tistory.com/116

    패치조인, 엔티티 객체 그래프, N+1 문제, @QueryProjection