Programming/Java

[JPA] JPQL 기본 문법 정리

kmindev 2025. 1. 8. 16:33

1. 개요

김영한 님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편' 을 들으면서 정리하는 포스팅입니다.

 

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

 

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

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

www.inflearn.com

 

2. JPQL이란?

실제 애플리케이션을 개발할 때에는 필요한 데이터를 가져오기 위해서는 다양한 검색조건과 복잡한 조인쿼리를 작성해야할 때가 많다.

 

JPA에서는 검색조건과 조인쿼리를 해결하기 위해 SQL을 추상화 한 JPQL이라는 객체 지향 쿼리를 제공하며, JPQL은 SQL과 매우 유사한 구조를 가진다.

 

SQL과 JPQL의 가장 큰 차이점은 SQL은 테이블을 대상으로 쿼리를 하는 반면, JPQL은 엔티티(객체)를 대상으로 쿼리한다는 점이다.

 

SQL Vs. JPQL

// SQL
SELECT id, username, age FROM Member

// JPQL
SELECT m FROM Member m

 

 JPA 환경에서 복잡한 쿼리를 작성하기 위해 사용할 수 있는 4가지 방법

  • JPQL: 객체지향 쿼리를 작성할 수 있는 기본적인 방식
  • Criteria: 동적쿼리 작성에 유리하지만 가독성이 떨어짐. 
  • QueryDSL: 동적 쿼리를 직관적으로 작성할 수 있어, 실무에서 가장 많이 사용.
  • Native Query: 복잡한 SQL을 직접 작성할 수 있음.
  • JDBC, Mybatis 등 : 전통적인 방식으로 SQL 처리

 

JPA 환경에서 복잡한 쿼리를 작성하기 위해서는 JPQL, Criteria, QueryDSL, Native 쿼리, JDBC  등을 사용할 수 있지만, JPQL과 QueryDSL만 사용하면 대부분의 문제는 해결할 수 있다. 실무에서는 대부분 QueryDSL을 사용하며, JPQL을 이해하면 QueryDSL은 금방 배워서 활용할 수 있다. 

 

이번 포스팅에서는 JPQL 위주로 설명을 이어가겠다.

 

3. JPQL과 TypeQuery

A. JPQL

위에서 언급했듯이, JPQL은 엔티티를 대상으로 하는 객체지향 쿼리 언어이다.

JPA 내부적으로 JPQL을 SQL로 변환하여 실행한다.

 

JPQL 기본 문법

--- select 문법
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]

--- update 문법
update_절
[where_절]

-- delete 문법
delete_절
[where_절]

 

B. TypeQuery

TypeQuery는 결과를 특정 타입의 객체로 반환할 수 있도록 도와준다.

이를 사용하면 쿼리 결과를 타입 안전하게 처리할 수 있다.

TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> result = query.getResultList();

 

4. 프로젝션(SELECT)

프로젝션은 SELECT 절에서 조회할 대상을 지정하여 반환하는 작업을 뜻한다.

엔티티, 임베디드 타입, 스칼라 타입, DTO를 프로젝션으로 지정할 수 있다.

// 엔티티 프로젝션
SELECT m FROM Member m
SELECT m.team FROM Member m

// 임베디드 타입 프로젝션(Member는 Address라는 임베디드 타입을 가지는 경우)
SELECT m.address FROM Member m

// 스칼라 타입
SELECT m.username, m.age FROM Member m

// DTO 프로젝션
SELECT new com.example.dto.MemberDTO(m.username, m.age) FROM Member m

 

5. 페이징 API

페이징 API를 사용하면 복잡한 쿼리를 작성할 필요없이 아주 편리하게 페이징 기능을 구현할 수 있다. 

List<Member> resultList1 = em.createQuery("select m from Member m order by m.age desc", Member.class)
                    .setFirstResult(0) // 시작 위치(0번째부터)
                    .setMaxResults(10) // 최대 결과 수(10개)
                    .getResultList();

 

6. 조인

A. 명시적 조인

A-1. 내부 조인

  • 내부 조인은 두 엔티티 간에 매칭되는 데이터만 조회할 때 사용한다.
  • 즉, 조인 대상이 존재하지 않는 경우, 해당 데이터는 조회되지 않는다.
--- JPQL
SELECT m FROM Member m JOIN m.team t WHERE t.name = 'TeamA'

--- SQL
SELECT m.* 
FROM Member m  
INNER JOIN Team t ON m.team_id = t.id  
WHERE t.name = 'TeamA'

 

A-2. 외부조인

  • 조인 대상이 존재하지 않더라도 기준 엔티티의 데이터를 유지하며, 조인 대상이 없으면 NULL을 반환한다.
--- JPQL
SELECT m FROM Member m LEFT JOIN m.team t

--- SQL
SELECT m.*, t.* 
FROM Member m  
LEFT JOIN Team t ON m.team_id = t.id

 

A-3. 세타조인

  • 엔티티를 특정 조건으로 조인하는 방식이다.
  • ON 절 대신 사용하여 연관관계가 없는 엔티티를 조인할 때 사용이 가능하다.
  • Cross Join + Where 조건 필터링으로 데이터를 조회한다.
--- JPQL
SELECT m, p FROM Member m, Product p WHERE m.age > p.price

--- SQL
SELECT m.*, p.* 
FROM Member m, Product p  
WHERE m.age > p.price

 

A-4 ON 절

  • ON 절을 활용하면 조인 대상을 필터링하거나 연관관계가 없는 엔티티 외부 조인이 가능하다.
--- 조건 필터링
SELECT m, t FROM Member m LEFT JOIN m.team t ON t.name = 'TeamA'

--- 연관관계가 없는 엔티티 외부 조인
SELECT m, o FROM Member m LEFT JOIN Order o ON m.username = o.customerName

 

B. 묵시적 조인(경로 탐색)

  • 객체 그래프 탐색을 통해 묵시적으로 조인을 수행할 수 있다.
  • 하지만 예상치 못한 SQL 쿼리가 발생하는 것을 방지하기 이해 명시적 조인을 사용하는 것을 권장한다.
--- Member 엔티티에서 Team 엔티티의 이름을 조회하는 경우
SELECT m.team.name FROM Member m

 

7. 패치 조인

  • SQL에서 제공하는 JOIN 종류가 아니다.
  • 연관된 엔티티나 컬렉션을 한번에 조회할 때 사용한다.
  • JPQL에서 성능을 최적화할 때 사용할 수 있다.
  • N + 1 문제를 부분적으로 해결할 수 있다.
--- JPQL
SELECT m FROM Member m JOIN FETCH m.team

--- SQL
SELECT m.*, t.* 
FROM Member m  
INNER JOIN Team t ON m.team_id = t.id

 

N + 1 문제와 해결 방법에 대해서 궁금하다면 아래 게시글을 참고바란다.

https://soonmin.tistory.com/126

 

8. 서브 쿼리

A. 서브 쿼리란?

  • 쿼리 안에 또 다른 쿼리가 포함된 형태로, 특정 조건을 만족하는 데이터를 조회할 때 사용한다.
  • JPA에서는 WHERE, HAVING, SELECT 절에서만 서브 쿼리를 사용할 수 있다.
--- 특정 팀에서 가장 나이가 많은 회원 조회
SELECT m FROM Member m 
WHERE m.age = (SELECT MAX(m2.age) FROM Member m2)

B. 서브 쿼리 지원 함수

서브쿼리 지원함수

함수 설명
EXISTS 서브 쿼리에 결과가 존재하면 참
IN 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참
ALL 서브 쿼리의 모든 결과와 비교하여 모두 만족하면 참
ANY, SOME 서브 쿼리의 하나라도 만족하면 참

 

서브쿼리 지원함수 사용예제

--- EXISTS 예제 
--- 특정 팀에 소속된 회원이 존재하는 경우만 조회
SELECT t FROM Team t 
WHERE EXISTS (SELECT m FROM Member m WHERE m.team = t)

--- IN 사용 예제
--- 30세 이상인 회원이 속한 모든 팀 조회
SELECT t FROM Team t 
WHERE t IN (SELECT m.team FROM Member m WHERE m.age >= 30)

---  ALL 사용 예제
--- 모든 회원의 나이보다 나이가 많은 회원 조회
SELECT m FROM Member m 
WHERE m.age > ALL (SELECT m2.age FROM Member m2)

--- ANY, SOME 사용예제
--- 같은 팀내에서 가장 어린 회원 조회
SELECT m FROM Member m 
WHERE m.age <= ANY (SELECT m2.age FROM Member m2 WHERE m2.team = m.team)

 

9. JPQL 타입표현과 기타표현식

  • JPQL에서 기본적인 타입 표현식을 지원한다.
표현식 설명
>, <, <=, >=, <>, =, !=,  비교 연산자
AND, OR, NOT 논리 연산자
BETWEEN 범위 탐색
IN 여러 값 검색
LIKE 패턴 검색
EXISTS, NOT EXISTS 존재 여부 확인
IS NULL, IS NOT NULL NULL 체크

 

10. 조건식(CASE, COALESCE, NULLIF)

  • JPQL에서 조건식에 따라 데이터를 조정할 수 있다.
--- CASE: 특정 조건에 따라 값을 반환
SELECT 
  CASE 
    WHEN m.age >= 18 THEN '성인' 
    ELSE '미성년자' 
  END 
FROM Member m

--- COALESCE: NULL일 경우 대체값을 반환
SELECT COALESCE(m.name, '이름 없음') FROM Member m

--- NULLIF: 두 값이 같으면 NULL 반환, 다르면 첫 번째 값 반환
SELECT NULLIF(m.name, 'admin') FROM Member m

 

11. JPQL 함수

함수 설명
CONCAT(a, b) 문자열 합치기
SUBSTRING(str, start, length) 부분 문자열 추출
LOWER(str), UPPER(str) 소문자, 대문자 변환
LENGTH(str) 문자열 길이 반환
ABS(x) 절댓값
SQRT(x) 제곱근
MOD(x, y) 나머지 연산
CURRENT_DATE 현재 날짜
CURRENT_TIME 현재 시간
CURRENT_TIMESTAMP 현재 날짜 + 시간

 

12. Named 쿼리

  • Named 쿼리를 사용하면, 정적인 쿼리를 미리 정의하여 이름을 통해 재사용할 수 있다.
  • 애플리케이션 로딩 시점에 초기화 후 재사용한다.
  • 애플리케이션 로딩 시점에 쿼리를 검증할 수 있다.
@NamedQuery(
  name = "Member.findByUsername",
  query = "SELECT m FROM Member m WHERE m.username = :username"
)
public class Member { ... }

List<Member> members = em.createNamedQuery("Member.findByUsername", Member.class)
                .setParameter("username", "kim");

 

 

13. 벌크 연산 처리

  • 대량의 데이터를 일일이 Update 또는 Delete 하는 방법보다 한번에 모두 쿼리하는 것이 더 효과적인 방법이다.
  • 벌크 연산은 영속성 컨텍스트를 무시하고 쿼리를 실행한다.
    • 벌크 연산 전 flush()가 내부적으로 호출되어, 쓰기 지연(Flush Queue)에 있던 쿼리가 실행된다. 
    • 벌크 연산 후에 영속성 컨텍스트와 DB 데이터 불일치 문제가 발생하므로 반드시 영속성 컨텍스트를 초기화 해주도록 하자.
// 모든 회원 나이를 +1 증가
UPDATE Member m SET m.age = m.age + 1

// 특정 조건을 만족하는 회원 삭제
DELETE FROM Member m WHERE m.age < 18