[JPA] JPQL 기본 문법

벼랑 끝 코딩·2025년 4월 14일

JPA

목록 보기
3/3
post-thumbnail

지난 포스팅에는 JPA의 기본 동작과 다양한 애노테이션에 대해 알아봤다.

이번에는 JPA에서 사용하는 SQL인 JPQL에 대해 알아보자.

JPQL 사용 이유

JPA를 사용하여 객체를 조회하면 SQL을 자동으로 생성하고
생성한 SQL을 토대로 테이블을 만든 뒤 객체로 변환하는데,
객체를 조회할 때마다 테이블의 모든 데이터를 객체로 변환하는 것은 성능 저하로 이어진다.
결국 조건문을 사용하는 SQL이 필요하다.

하지만 사용하는 DB마다 SQL 문법에 차이가 있기 때문에
DB를 변경하는 경우 SQL을 변경해야 하는 번거로움이 발생한다.
결국 SQL의 추상화가 필요하다.

JPQL이 바로 SQL을 추상화하고 테이블이 아닌 객체를 대상으로 작성하는 객체 지향 SQL이다.

실무에서는 JPQL로 해결할 수 없는 경우가 발생하는데
그땐 아래의 다양한 라이브러리 또는 기능을 고려해보자.

QueryDSL

JPQL을 직접 문자열로 작성하는 경우 오타 등의 이유로 오류가 발생할 수 있다.
QueryDSLJPQL을 Java 코드로 작성하게 해주는 라이브러리로,
QueryDSL을 사용하면 컴파일 시점에 문법 오류를 탐색할 수 있다.

동적 쿼리 문제도 QueryDSL로 간단하게 해결할 수 있다.

NativeSQL

JPA가 제공하는 SQL을 직접 사용하는 기능이다.
JPQL로 해결할 수 없는 DB에 사용한다.

JDBC

JDBC는 영속성 컨텍스트를 거치지 않고 바로 DB에 적용하기 때문에 JPA와 함께 사용 시,
JDBC 기능 사용 전 flush() 메서드를 호출하여 DB와 동기화한 이후 JDBC를 사용해야 한다.

JPQL 문법

JPQL을 사용하기 위한 다양한 문법을 알아보자.

기본 문법

  • JPQL은 Entity와 필드의 대소문자를 구분한다.
  • JPQL 키워드는 대소문자를 구분하지 않는다.
  • JPQL에서는 별칭을 필수로 작성해야 하며, AS는 생략 가능하다.
// 두 JPQL은 동일한 JPQL
select m from Model m where m.entity.id = :entityId
select m from Model m where m.entity = :entity

JPA는 Entity를 식별자로 구분하는 방식이다.
Entity를 사용하는 것은 Entity의 primary Key를 사용하는 것과 동일하다.

경로 표현식

object.field  // 객체 상태 필드 탐색
object.entity  // 객체 내 객체 필드 탐색

JPQL에서는 점(.)을 통해 객체 그래프 탐색이 가능하다.
객체 내 객체 필드를 DB에서 조회하기 위해서는 반드시 연관관계가 있어야 한다.
연관 관계가 없는 경우 예외가 발생한다.

객체 내 객체 필드를 조회 시 묵시적 내부 조인이 발생하여 내부 필드까지 조회할 수 있다.
객체 필드가 컬렉션인 경우 묵시적 내부 조인이 발생하지만 탐색은 불가능하다.
묵시적 내부 조인은 사용하지 않는 객체까지 조회하기 위한 SQL을 생성하여
예상치 못한 상황이 발생할 수 있으므로 명시적 조인 JPQL을 작성해서 사용해야 한다.

타입 표현

JPQL 내에서 타입 별로 표현하는 방식에는 차이가 있다.

  • 문자 : 'string'
  • 숫자 : Long=1L, Double=1.0D, Float=1.0F
  • Boolean : true, false
  • ENUM : package.path.EnumClassName.TYPE
  • Entity Type : 상속 관계에서 사용, type(e) = ClassName

treat

treat(e as ClassName)

treat(e as ClassName)는 type(e) = ClassName과 유사하지만
type(e) = ClassName은 주로 where절에 사용되고,
treat는 select, where, join 등 다양하게 사용되면서
다운캐스팅하여 자식 필드에 접근할 수 있다는 점에서 차이가 있다.

조건식

// 기본 CASE
select
	case when 조건식 then result1
    	 when 조건식 then result2
    	 else result3
	end
from entity e


// 단순 CASE
select
	case e.field
    	when value1 then result1
        when value2 then result2
        else result3
    end
fron entity e

JPQL 조건식은 case, when, end 구문으로 생성할 수 있다.

기본 CASE에서는 조건식을 만족하는 경우 결과를 반환하고,
단순 CASE에서는 필드가 값을 만족하는 경우 결과를 반환한다.

coalesce

select coalesce(e.field, 'NullResult') from entity e

coalesce(필드값, null일 때 값)은 null이 아니면 보유한 값을 반환하고,
null인 경우 null일 때 값을 설정하여 반환하는 키워드이다.

nullif

select nullif(e.field, 'NullValue') from entity e

nullif(필드값, null을 반환할 값)은 null을 반환할 값이면 null을 반환하고,
null을 반환할 값이 아니면 보유한 값을 반환하는 키워드이다.

반환

JPQL 쿼리를 지연 실행, 파라미터 설정 등을 위해 createQuery() 메서드를 호출 시
Model 객체를 즉시 반환하지 않고 TypedQuery, Query와 같은 준비 객체를 생성한다.
createQuery()는 준비 객체만을 생성하여 호출 시점에 DB에 직접 접근하지 않고,
query 값을 조회하는 메서드를 호출해야 실제 DB에 접근한다.

TypedQuery

TypedQuery<Model> query = entityManager.createQuery("jpql");

TypedQuery반환 타입이 명확한 경우에 사용하는 반환 값 포함 객체이다.

TypedQuery에 반환 타입을 지정하여 createQuery() 메서드의 값을 받을 수 있다.

Query

Query query = entityManager.createQuery("jpql");

Query반환 타입이 명확하지 않은 경우에 사용하는 반환 값 포함 객체이다.

getSingleResult()

Model model = query.getSingleResult();

createQuery() 메서드를 호출하여 반환 받은 준비 객체에서

쿼리 결과가 하나일 때에는 getSingleResult() 메서드를 호출하여 결과를 조회할 수 있다.

getSingleResult(), getResultList() 메서드를 호출해야 실제 DB에 접근한다.

getResultList()

createQuery() 메서드를 호출하여 반환 받은 준비 객체에서

쿼리 결과가 하나 이상일 때에는 getResultList() 메서드를 호출하면
리스트로 결과를 반환받을 수 있다.

Parameter Binding

String jpql = "select m from Model m where m.field = :field";

TypedQuery<Model> query = entityManager.createQuery(jpql);
query.setParameter("field", "value");

Model model = query.getSingleResult();

JPQL에서 파라미터를 전달받을 변수는 ':' + field 변수명을 입력하여 선언할 수 있다.

이후 createQuery() 메서드를 호출하여 준비 객체를 생성하고,
setParameter() 메서드를 호출하여 변수명을 입력하고 값을 바인딩할 수 있다.

String jpql = "select m from Model m where m.field = ?1";

TypedQuery<Model> query = entityManager.createQuery(jpql);
query.setParameter(1, "value");

Model model = query.getSingleResult();

'?' + 번호를 입력하여 파라미터 변수를 번호로 선언할 수도 있다.

프로젝션

프로젝션이란 select 절에서 조회 대상을 지정하는 것을 의미한다.

  • entity : entity 조회
  • entity 내 entity : entity의 entity 필드 조회
  • 스칼라 : entity의 field 조회
  • 특정 객체(주로 DTO) : 여러 field를 조회하여 특정 객체로 변환

특정 객체 조회

String jpql = "select new package.path.modelDTO(m.field) from Model m";

MemberDto memberDto = entityManager.createQuery(jpql, MemberDto.class)
						.getSingleResult();

객체의 여러 field를 조회한 값을 특정 객체로 변환하려면 new 키워드를 사용해야 한다.
new 키워드와 함께 변환할 객체의 패키지 경로와 클래스 이름을 명시하고
변환할 객체 생성자의 필드 순서와 타입이 일치하도록 조회한 field의 값을 전달하면,
특정 객체로 자동 변환할 수 있다.

테이블을 조인하여 특정 객체 결과를 원한다면 new 키워드 사용을 고려하면 된다.

DTO

조회 결과로 Entity를 그대로 반환하는 것은 보안, 양방향 연관 관계에서 무한 루프,
무엇보다 Entity 변경 시 API 응답이 변경되어 유지보수 측면에서 큰 문제를 발생시킨다.
따라서 Entity는 특정 객체인 DTO로 변환해서 반환해야 한다.

DTO는 View, API 통신에서 사용하고
Entity는 DB에 데이터를 저장할 때 사용한다.

정렬

  • GROUP BY : 데이터를 그룹화할 때 사용
    HAVING : GROUP BY를 필터링할 때 WHERE 대신 사용
  • ORDER BY : 데이터를 ASC 또는 DESC 정렬 시 사용, 맨 마지막에 위치

집합

  • COUNT(entity)
  • AVG(entity.field)
  • SUM(entity.field)
  • MAX(entity.field)
  • MIN(entity.field)

집합과 관련된 함수 외에도 다양한 JPQL 함수가 존재하고
사용자 정의 함수도 설정할 수 있다.

조인

  • (INNER) JOIN, 내부 조인 : 두 객체의 조건에 일치하는 데이터만 조회
    JPQL은 연관관계 매핑에 따라 INNER JOIN 시 ON 절 자동 생성
  • LEFT/RIGHT (OUTER) JOIN, 외부 조인 : 조건에 일치하지 않아도
    기준(LEFT 또는 RIGHT) 테이블 데이터 모두 조회
  • 세타 조인 : 연관 관계가 없는 두 테이블을 조건으로 조인
  • fetch join : : 객체 필드 즉시 조회
  • ON : JOIN 절에서 WHERE 대신 사용

JPQL은 연관관계 매핑이 선행되어야 ON 절을 사용할 수 있다.
연관관계가 없다면 기본적으로 세타 조인 또는 WHERE 절을 사용하지만
예외적으로 외부 조인은 전혀 관계가 없는 필드끼리의 ON 절을 추가할 수 있다.

페이징

String jpql = "select m from Model m";
List<Model> modelList = entityManager.createQuery(jpql, Model.class)
							.setFirstResult(1)
        					.setMaxResults(10)
       						.getResultList();

페이징이란 성능 최적화를 위해 많은 양의 데이터 중 일부만 조회하는 방식이다.

setFirstResult() 메서드를 호출하여 시작 위치를 설정하고,
setMaxResults() 메서드를 호출하여 총 조회 데이터 수를 설정할 수 있다.

서브 쿼리

select m from Model m where m.number > (select avg(m2.number) from Model m2)

서브 쿼리란 쿼리 내부에 또 다른 쿼리를 작성하는 것을 의미한다.

JPA는 서브 쿼리를 WHERE, HAVING 절에만 사용 가능하고
Hibernate는 SELECT, FROM 절까지 사용 가능하다.

서브 쿼리 관련 키워드

  • EXISTS : 하나라도 존재하는 경우 true
  • ALL : 반환된 모든 값이 조건을 만족하는 경우 true
  • ANY, SOME : 하나라도 만족하는 경우 true
  • IN : 반환 값이 목록에 포함되는 경우 true

@NamedQuery

자주 작성하는 쿼리를 xml 또는 애노테이션에 정의해서 사용할 수 있다.
xml, 애노테이션에 모두 작성된 경우 xml이 우선권을 가진다.
xml에 작성하면 운영 환경에 따라 적절히 배포 가능하여 xml 작성이 권장된다.

@NamedQuery(name = "QueryName", query = "jpql")
class Model {
	// 코드
}

// 사용
entityManager.createQuery("QueryName", Model.class)
		.setParameter("parameter", value)
        .getResultList();

애노테이션으로 정의할 경우 jpql과 연관된 클래스에 @NamedQuery를 선언하여 사용한다.
name 속성에 코드에 작성할 쿼리 이름을 입력하고, query에는 자주 사용하는 쿼리를 작성한다.

마무리

JPA가 기본적으로 SQL을 자동 생성하여 편리하게 개발할 수 있지만,
성능 최적화를 위해 결국 객체 지향 SQL인 JPQL을 사용해야만 한다.
JPQL로 쿼리를 어떻게 작성하느냐에 따라 성능에 크게 영향을 미칠 수 있으니
JPQL 문법을 숙지하고 JPQL을 능숙하게 작성할 수 있도록 노력하자.

profile
복습에 대한 비판과 지적을 부탁드립니다

0개의 댓글