객체지향 쿼리 언어 (JPQL)

JPQL(Java Persistence Query Language) 이란?

지난 [JPA] 스프링 데이터 JPA에서 Specification으로 동적 WHERE절 구현하기 (1) 글에서 'JPA에서 사용하는 SQL과 비슷한 언어라고 만 생각하면 될 것 같다.' 라고 남겨 놨었는데, 사실 Specification을 제대로 쓰기 위해선 JPQL을 먼저 알아야 한다.

JPA는 기존의 SQL 중심적인 개발을 타파하고 엔티티라는 객체를 중심으로 객체 지향적인 개발을 위해서 탄생했다. 그러나 결국 데이터라는 것이 데이터베이스 저장소에 저장되는 이상, 데이터를 효율적으로 다루기 위해서는 SQL이 필요하다. 다음과 같은 경우를 생각해 보자.

어떤 도서관에 존재하는 모든 책의 정보가 담긴 데이터베이스에서 'JPA' 라는 단어가 제목으로 포함된 책들을 찾는다.

위의 명제를 JPA의 기본 기능만 사용해 구현하고자 한다면 다음과 같을 것이다.

  1. 애플리케이션에서 findAll() 함수를 이용해 모든 데이터를 불러온다.
  2. 불러온 데이터를 모두 메모리에 적재한다.
  3. 책 엔티티에서 제목이란 요소에 'JPA'가 포함된 모든 객체를 찾는다.
  4. 제목 요소에 'JPA'란 제목이 포함된 책 엔티티를 새로운 List 객체에 add 한다.

그러나 이런 방법은 현실적이지 않다. 우선 도서관에 책이 몇 권이나 있을지는 모르겠지만, 적어도 양손으로 편히 셀 수 있는 숫자는 아닐 것이다. 이렇게 많은 데이터를 모두 메모리에 적재해 놓고 작업하는 것은 메모리 낭비일 뿐이고, 운이 나쁘다면 메모리 초과 오류를 보게 될 수도 있을 것이다. 즉, SQL은 대용량의 데이터를 효율적으로 다루기 위한 필수 불가결의 존재다.

JPA에서 특정 데이터베이스에 대한 종속성을 피하는 방법

물론 알고 있겠지만, 데이터베이스 벤더에 따라 SQL 문법은 상이하다. 동일한 기능을 지원 하지만 벤더 별로 문법이 다른 것을 JPA 에서는 Dialet(이하 방언) 이라고 표현한다. 이 방언의 존재 때문에, SQL을 사용하게 되면 좋든 싫든 간에 특정 데이터베이스에 대한 종속성을 피할 수 없다.

JPA 에서는 이 종속성 문제를 해결하기 위해 SQL 언어를 추상화 시킨 별도의 쿼리 언어를 쓰는데, 이 쿼리 언어가 바로 JPQL이다. JPQL은 다음과 같은 특징을 가진다.

  1. SQL을 추상화 했기 때문에, 특정 데이터베이스 SQL에 종속적이지 않다.
  2. 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 쿼리 한다. (이름이 '객체지향 쿼리 언어'인 이유다.)
  3. JPQL이 번역되면 SQL로 변환된다.

위와 같은 특성 때문에 우리는 하나의 기능에 하나의 JPQL문만 작성하면 되고, 설령 데이터베이스가 바뀌더라도 방언의 종류만 변경해 주면 JPA가 알아서 해당 방언에 맞춰 번역 해준다.

JPQL의 문법

JPQL의 문법은 기본적으로 ANSI SQL 문법과 상당히 유사하다. JPQL은 SELECT, UPDATE, DELETE문을 사용할 수 있다. INSERT문의 경우에는 EntityManager의 persist() 함수가 실행되면 자동으로 INSERT문이 실행되기 때문에 별도로 지원하지 않는다. 지난 글에서 다룬 Book 엔티티를 바탕으로 몇 가지 JPQL문을 작성해 보겠다.

@Getter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "BOOK")
public class Book {
    
    @Id
    @Column(name = "BOOK_ID")
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @Column(name = "BOOK_TITLE", nullable = false)
    private String title;

    // 그 외, 기타 컬럼들...
}
Book 엔티티 코드의 일부

SELECT문

SELECT b FROM Book AS b WHERE b.title LIKE CONCAT('%',:title,'%')
Book 엔티티에서 제목에 특정 문자열이 포함된 책을 모두 찾는 쿼리

UPDATE문

UPDATE Book AS b SET b.title = :title WHERE b.id = :id
id 값을 이용해 특정 Book 엔티티를 찾고 제목을 업데이트 하는 쿼리

DELETE문

DELETE FROM Book AS b WHERE b.id = :id
id 값을 이용해 특정 Book 엔티티를 삭제하는 쿼리

보다시피 위에서 얘기한 대로 JPQL의 문법이 ANSI SQL과 상당히 유사하다는 것을 알 수 있다.

JPQL 작성 규칙

모든 프로그래밍 언어가 엄격하든 느슨하든 작성 규칙이 있듯이 JPQL에도 당연히 작성 규칙이 존재한다. JPQL의 작성 규칙은 다음과 같다.

  • JPQL의 FROM절에는 엔티티 명을 사용한다.

SQL은 FROM절에 테이블 명을 사용하지만, JPQL은 객체지향 쿼리 언어기 때문에 엔티티 명을 사용한다. 여기서 한 가지 주의할 점은, 엔티티 명은 클래스 명에 의해 결정되는 것이 아니라 @Entity 어노테이션의 name 속성에 의해 결정된다는 점이다. 예를 들어...

@Entity(name = "BOOK")
public class Book {}

...라는 코드가 있다면, 클래스 명인 Book이 아닌 @Entity(name = "BOOK")의 BOOK을 엔티티 명으로 사용한다는 것이다. 만약 name 속성을 적지 않으면, 기본적으로 클래스 명과 동일한 이름을 엔티티 명을 설정한다. 일반적으로는 클래스 명과 동일하게 사용하는데, @Entity 어노테이션의 name 속성에 명시적으로 적어주는 게 좋다.

  • JPQL은 기본적으로 대소문자를 구분하지 않는다. 단, 엔티티와 속성 명은 대소문자를 구분한다.

JPQL에서 엔티티와 속성 명이 정확하지 않으면 런타임 예외가 발생한다. 따라서 항상 대소문자에 주의해야 한다. 참고로 속성 명은 엔티티 클래스 내부에 존재하는 필드 변수들의 이름을 의미한다. (EX. id, title 등...)

-- 엔티티 명: Book, 속성: id, title 일 때
-- BAD: 엔티티 명이 다름
SELECT b FROM book AS b WHERE b.id = :id AND b.title LIKE CONCAT('%',:title,'%')

-- BAD: 속성 명이 다름
SELECT b FROM Book AS b WHERE b.ID = :id AND b.TITLE LIKE CONCAT('%',:title,'%')

-- GOOD
SELECT b FROM Book AS b WHERE b.id = :id AND b.title LIKE CONCAT('%',:title,'%')
  • 테이블 Alias(이하 별칭)는 필수다.

일반적인 SQL에서는 테이블이 하나 뿐일 경우, 테이블 별칭을 설정할 필요가 없다. 하지만 JPQL에서는 테이블이 한 개여도 별칭이 필수다. 별칭을 설정할 때 쓰는 키워드인 AS는 생략이 가능하다.

-- GOOD
SELECT b FROM Book AS b WHERE b.id = :id AND b.title = :title

-- GOOD: AS는 생략되도 상관 없다
SELECT b FROM Book b WHERE b.id = :id AND b.title = :title

JPQL 작성을 위한 여러 방법들

JPQL은 훌륭한 도구지만, 몇 가지 문제가 있다. 일단 쿼리를 문자열로 직접 작성해야 한다는 점에서 IDE의 도움을 받을 수 없기 때문에 오타와 같은 문제에 취약하다. 더욱 성가신 점은 이런 문제가 발생해도 컴파일 단계에서 확인할 수 없다는 점이다. 이 때문에 JPA에서는 버전 2.0부터 JAVA 코드 기반으로 JPQL문을 작성할 수 있는 Criteria라는 JPQL 빌더 클래스를 추가했다. 스프링 데이터 JPA 에서는 Criteria를 기반으로 두는 Specification 이라는 인터페이스를 사용할 수 있다.

하지만 Criteria는 그 특유의 복잡함 때문에 오히려 코드의 가독성을 해친다는 문제가 있다. 이 때문에 오픈소스 프로젝트 중에는 간단한 방법으로 JPQL문을 생성할 수 있는 QueryDSL 이라는 비공식 프로젝트가 존재한다. 그 간단한 사용법 때문에 실제 현업에는 QueryDSL을 채용하는 곳도 많다고 한다.

JPA 에서 JAVA코드로 JPQL문을 작성하는 방법이 기본적으로 이 두 가지가 존재한다. 하지만 스프링 데이터 JPA 모듈에서는 Query Methods라고 하는, 함수 명을 기반으로 JPQL문을 작성해주는 특이한 기능이 존재한다. 이 주제까지 다루다가는 글이 너무 길어질 것 같으니, 해당 주제는 별도의 글로 다루도록 하고 여기 서는 스프링 데이터 JPA의 공식 문서에 대한 링크만 남겨두도록 하겠다.

Spring Data JPA - Reference Documentation

정리하며

이 글을 작성하기 전, 도서 장부 시스템을 만들며 그냥 무작정 Specification에 대해 검색하고 적당히 동적 조회 조건을 구현했던 지라 JPQL에 대해서 모르는 것이 많았다. 이번 기회에 JPQL에 대해 정리하면서 그동안 편하게 작성했던 코드들도 사실 뒤로는 그 복잡한 JPQL문을 작성하면서 데이터를 조회하고 있었다는 것을 알게 되니 이런 굉장한 기술을 굉장히 편하게 쓰게 되었구나 싶었다.

다음 글에서는 내가 도서 장부 시스템을 만들면서 새로운 저자, 출판사 엔티티를 추가해 책 엔티티로 부터 분리하고 이전과 같이 책을 조회 시 저자와 출판사 정보를 가져오기 위해 Specification으로 JOIN을 구현 하던 과정에 대해 다뤄 볼 예정이다.


참고한 문헌 및 블로그 글

  1. 자바 ORM 표준 JPA 프로그래밍 - 김명환 저