development/spring

JPA - JPQL과 QueryDSL 활용

bokshiri 2023. 1. 31. 23:51

안녕하세요 복실복실 개발자입니다.

지난 포스팅에 이어 이번 글에서도 JPA를 주제로 이야기를 해보려 합니다.

앞선 글에서 언급하였던 것처럼 JPA-Hibernate 기반의 ORM 기술을 활용한 개발 방식에서는 .xml 파일에 직접 쿼리문을 작성하지 않고 객체를 바탕으로 데이터를 제어할 수 있습니다. 주로 사용되는 두 가지 방식에 대해서 살펴보도록 하겠습니다.

  • 1. @Query 어노테이션을 활용한 JPQL - 객체지향 쿼리언어
  • 2. queryDSL - JPA에서 기본으로 제공하는 JPQL을 정적인 형태의 코드로 작성할 수 있도록 기능을 제공하는 프레임워크

1. @Query 어노테이션을 활용한 JPQL - 객체지향 쿼리언어

JPQL은 JPA의 일부로서, 테이블이 아닌 객체(엔티티 - @Entity)를 대상으로 한 객체 지향 쿼리 언어입니다. queryDSL을 다루면서 이야기하겠지만 queryDSL의 경우 자바 메서드로 데이터를 제어하기 때문에 컴파일 시점에 syntax error 등을 잡아낼 수 있는 반면, JPQL은 @Query 어노테이션의 value로 query String을 개발자가 직접 입력하기 때문에 컴파일 시점에서 에러를 잡아낼 수 없다는 한계가 있습니다. 만일 개발자가 작성한 문법에 에러가 발생하였다면 런타임 시점 혹은 쿼리 실행 시점에 가서야 문제가 있다는 것을 알아차릴 수 있습니다.

공부를 하며 활용한 테이블은 2개 남짓으로, 보기에도 아주 간단한 쿼리를 활용하여 실습을 진행하였습니다.

Spring Data Jpa에서는 @Query 어노테이션을 기본으로 제공하고 있기 때문에 별도의 라이브러리를 추가하지 않아도 곧바로 사용할 수 있습니다.

JpaRepository를 상속받은 MemberRepository 예시

공부를 진행하기 전에는 JPQL이 테이블과 매핑된 객체만을 제어하는 언어라 생각하였는데, 한 가지 방식이 더 있었습니다. Spring Data JPA에서는 이미지에서도 보이듯이 데이터베이스의 테이블을 직접 제어할 수 있는 nativeQuery 기능을 제공하고 있습니다. 상세한 설명은 모두 개발단계에서 주석에 달아놓았으므로 핵심적인 부분만 요약하여 정리해보도록 하겠습니다.

1. JPQL

  • 객체를 제어하는 언어이기 때문에 테이블이 아닌 객체와 객체의 속성을 대상으로 쿼리를 작성하여야 합니다.(객체 지향 쿼리 언어)
  • 일반 SQL과는 달리 테이블에 반드시 alias를 지정해주어야 합니다.
  • 엔티티와 속성(컬럼)은 대소문자를 구분해야하며, sql 예약어(Select, Update, From, Where, etc...)는 대소문자를 구분하지 않아도 됩니다.
  • 외부의 인자를 파라미터로 활용할 경우 메서드에서 @Param 어노테이션을 사용하여 쿼리에 값을 전달할 수 있습니다.

2. 일반 SQL

  • 데이터베이스의 특정 스키마의 테이블을 대상으로 쿼리를 작성하여야 합니다.
  • 실습을 하며 직면했던 문제: JPQL과 JPA에서 제공하는 기본 CRUD는 문제없이 동작하였으나, application.yml에 jpa의 default_schema를 분명 명시해주었음에도 일반 sql문을 실행하였을 때 member 릴레이션을 찾지 못하는 문제가 발생하였습니다. 때문에 위의 이미지에서는 member 테이블 앞에 스키마를 명시해준 형태로 사용하였습니다. 혹시 위 현상에 대해 아시는 분이 있다면, 지식을 나눠주시면 감사드리겠습니다.
  • 위 문제에 대한 해답을 찾았습니다. application.yml의 datasource url 선언시 parameter에 currentSchema를 추가하여 접속하였더니 sql에서 스키마 명시 없이도 성공적으로 릴레이션에 연결할 수 있었습니다. 추측하건데 jpa-hibernate-default_schema 항목은 객체를 제어할 때에만 사용되고, @Query 어노테이션의 nativeQuery를 사용할 땐 데이터베이스의 릴레이션에 직접 접근하기 때문에 datasource 접속 시에 스키마를 명시해줘야 하는 것 같습니다.

 

마지막으로, JPQL과는 관련이 없는 이야기지만 제가 개발을 진행할 때 인터페이스의 메서드에 public 접근제한자를 습관적으로 붙이고 있었습니다... 인터페이스는 기본적으로 default 접근제한자가 public이기 때문에 생략을 하여도 무방합니다. 또한 필드 역시 default값이 상수(static final)이기 때문에 생략을 하여도 상수로 선언한 것으로 인식됩니다. 앞으로는 개발을 할 때 이러한 지식을 활용하는 습관을 들여야 할 것 같습니다.

2. queryDSL - JPA에서 기본으로 제공하는 JPQL을 정적인 형태의 코드로 작성할 수 있도록 기능을 제공하는 프레임워크

다음으로 살펴볼 것은 바로 queryDSL입니다. 실습을 진행하며 가장 힘들었던 점은 queryDSL의 초기 설정이었습니다. 이 녀석은 IntelliJ, Spring Boot 등 외부 환경의 버전에 따라 설정 방법이 매우 상이합니다. 현재 제가 진행하고 있는 실습용 프로젝트의 환경은 스프링부트 3.0.2, 자바 17인데, 구글링을 통해 build.gradle 세팅을 동일하게 마쳤음에도 컴파일 시점에 지속적으로 아래와 같은 에러 메시지가 리턴되었습니다.

Execution failed for task ':compileQuerydsl'. > java.lang.NoClassDefFoundError: javax/persistence/Entity

이 문제로 인하여 무려 5시간 동안 gradle 스크립트 수정, 구글링, gradle refresh를 반복하였습니다... 그러다 문득 '현재 사용하고 있는 부트의 버전이랑 연관이 있는 것은 아닐까?' 라는 생각이 들었고 곧바로 구글을 찾아본 결과 해답을 얻을 수 있었습니다... 이 문제 덕에 하루가 참 빨리 지나갔습니다. ㅎㅎ

https://velog.io/@juhyeon1114/Spring-QueryDsl-gradle-%EC%84%A4%EC%A0%95-Spring-boot-3.0-%EC%9D%B4%EC%83%81 

 

해당 블로그에서 도움을 얻을 수 있었습니다. 제가 그 동안 gradle에 적용하였던 방식은 모두 스프링부트 2.대 버전 기준의 레퍼런스들이었습니다...(한숨) 이렇게 또 하나를 배워가는 거겠지요? 위의 블로그의 gradle script를 참조하여 최종적으로 정리한 build.gradle의 내용은 다음과 같습니다. 

plugins {
   id 'java'
   id 'org.springframework.boot' version '3.0.2'
   id 'io.spring.dependency-management' version '1.1.0'

   //querydsl 추가
   id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
}
// == 스프링 부트 3.0 이상 ==
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"

* 빌드시 컴파일 에러로 인해 하단의 스크립트를 추가하였습니다.

annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
def querydslDir = "$buildDir/generated/querydsl"

querydsl {
   jpa = true
   querydslSourcesDir = querydslDir
}

sourceSets {
   main.java.srcDir querydslDir
}

configurations {
   compileOnly {
      extendsFrom annotationProcessor
   }
   querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
   options.annotationProcessorPath = configurations.querydsl
}

clean {
   delete file('src/main/generated')
}

해당 스크립트 추가 후에 gradle task의 compileQuerydsl을 실행하면 $buildDir 경로에 @Entity로 등록하였던 엔티티가 Q클래스 형태로 생성되는 것을 확인할 수 있을 것입니다.

queryDSL은 기본적으로 컴파일 시점에 생성된 위와 같은 Q클래스를 이용하여 개발을 진행합니다. 왜 일반 클래스 파일이 아닌 Q클래스 형태를 이용하는지 궁금하신 분들은 https://hhhhhhhong.tistory.com/88 해당 블로그를 참고하시면 좋을 것 같습니다. 저도 의문점이 생겼었는데 해당 블로그에 잘 정리되어 있어, 많은 도움을 받을 수 있었습니다.

시간이 늦은 관계로 queryDSL 실습은 내일 이어서 포스팅하도록 하겠습니다..