contents
JPQL(Jakarta Persistence Query Language) 은 관계형 데이터베이스에 저장된 데이터를 검색하고 조작하는 데 사용되는 강력한 객체 지향 쿼리 언어입니다. 이는 자바의 객체-관계 매핑(ORM) 표준 명세인 JPA(Jakarta Persistence API) 의 핵심 부분입니다.
JPQL을 원시 데이터베이스 테이블 대신 자바 객체(엔티티)를 대상으로 작동하는, 이식성 높고 데이터베이스에 독립적인 SQL 버전이라고 생각할 수 있습니다.
JPQL이 해결하는 문제: SQL의 한계
왜 그냥 일반 SQL을 사용하지 않을까요? JPQL은 자바 개발자를 위해 두 가지 주요 문제를 해결하기 위해 만들어졌습니다.
-
데이터베이스 독립성 (이식성) 🐘: PostgreSQL, MySQL, Oracle과 같은 다른 데이터베이스들은 약간씩 다른 SQL 방언(dialect)과 함수를 가지고 있습니다. 만약 쿼리를 네이티브 SQL로 작성하면, 애플리케이션은 특정 데이터베이스에 종속됩니다. JPQL은 추상화 계층을 제공합니다. 표준 JPQL로 쿼리를 작성하면, JPA 프로바이더(Hibernate나 EclipseLink 등)가 설정된 데이터베이스에 맞는 정확한 SQL 방언으로 자동 번역해 줍니다. 이는 데이터 접근 계층의 이식성을 높여줍니다.
-
객체-관계 불일치: 개발자들은 객체, 클래스, 필드(예:
firstName필드를 가진Customer객체)의 관점에서 생각합니다. 데이터베이스는 테이블, 행, 열(예:first_name열을 가진customers테이블)의 관점에서 생각합니다. JPQL은 객체 지향 도메인 모델을 사용하여 쿼리를 작성하게 함으로써 이러한 격차를 해소합니다. 이는 더 직관적이고 오류 발생 가능성을 줄여줍니다.
JPQL vs. SQL: 주요 차이점
| 특징 | JPQL | 네이티브 SQL |
|---|---|---|
| 쿼리 대상 | 엔티티와 그 영속 필드. | 테이블과 그 컬럼. |
| 문법 | 클래스 이름과 필드 이름을 사용 (대소문자 구분). | 테이블 이름과 컬럼 이름을 사용 (종종 대소문자 무시). |
| 이식성 | 높음 (데이터베이스 독립적). | 낮음 (특정 데이터베이스 방언에 따라 다를 수 있음). |
| 객체 모델 | 엔티티 관계(@OneToMany 등)를 이해함. |
객체 관계를 이해하지 못함. |
| 예시 | SELECT c FROM Customer c WHERE c.lastName = 'Doe' |
SELECT * FROM customers WHERE last_name = 'Doe' |
핵심 문법 및 구조 📜
JPQL 쿼리는 SQL과 매우 유사하게 여러 절(clause)로 구성되지만, 객체 지향적인 초점을 맞춥니다.
SELECT 절
SELECT 절은 검색하려는 대상을 식별합니다.
-
전체 엔티티 선택: 가장 일반적인 사용 사례입니다. 쿼리는 완전히 관리되는 엔티티 객체를 반환합니다.
-- 모든 Customer 객체를 선택 SELECT c FROM Customer c -
특정 필드 선택: 엔티티에서 특정 필드만 검색할 수도 있습니다. 전체 객체가 필요하지 않은 경우 더 효율적입니다.
-- 모든 고객의 이름과 이메일을 선택 SELECT c.name, c.email FROM Customer c
FROM 절
FROM 절은 쿼리할 엔티티를 지정합니다. 중요한 점은 데이터베이스 테이블 이름이 아닌 엔티티 클래스 이름을 사용한다는 것입니다. 또한 쿼리 전체에서 사용될 별칭(alias)을 할당합니다.
-- "Customer"는 자바 클래스 이름이고, "c"는 별칭입니다.
FROM Customer c
WHERE 절
WHERE 절은 SQL에서와 마찬가지로 조건을 기반으로 결과를 필터링합니다. 별칭과 엔티티의 필드 이름을 사용합니다. 값을 안전하게 전달하기 위해 이름 기반 파라미터(콜론 :으로 시작)를 사용하는 것이 일반적입니다.
-- 특정 도시에 있는 고객 찾기
SELECT c FROM Customer c WHERE c.city = :cityParam
JOIN 절 🔗
JPQL은 엔티티에 정의된 관계(@OneToMany, @ManyToOne 등)를 활용하여 조인을 직관적으로 만듭니다. 객체 그래프를 직접 탐색할 수 있습니다.
-
묵시적 조인: 점 표기법을 사용하여 관련 엔티티로 이동할 수 있습니다.
-- 특정 제품 이름을 가진 주문 찾기 -- Order 엔티티에 'product' 필드가 있다고 가정 SELECT o FROM Order o WHERE o.product.name = 'Laptop' -
명시적 조인: 더 많은 제어가 필요할 때, 특히 컬렉션의 경우
JOIN키워드를 사용합니다.-- 하나 이상의 주문을 한 모든 고객 찾기 -- Customer 엔티티에 'orders' 컬렉션이 있다고 가정 SELECT DISTINCT c FROM Customer c JOIN c.orders oJPQL은 일치하는 관계가 없을 수 있는 엔티티를 포함하기 위해
LEFT JOIN도 지원합니다.
기타 일반적인 절
-
ORDER BY: 결과를 정렬합니다 (ORDER BY c.lastName ASC). -
GROUP BY: 같은 값을 가진 행들을 요약 행으로 그룹화합니다 (GROUP BY p.category). -
HAVING:GROUP BY절이 적용된 후 결과를 필터링합니다.
코드에서 JPQL 실행 방법 ⚙️
자바 애플리케이션에서 JPA의 EntityManager를 사용하여 JPQL 쿼리를 실행합니다.
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import java.util.List;
// 'em'이 주입된 EntityManager 인스턴스라고 가정
public List<Customer> findCustomersByCity(String city) {
// 1. JPQL 쿼리 문자열 정의
String jpql = "SELECT c FROM Customer c WHERE c.city = :cityParam";
// 2. Query 객체 생성
TypedQuery<Customer> query = em.createQuery(jpql, Customer.class);
// 3. 이름 기반 파라미터 설정
query.setParameter("cityParam", city);
// 4. 쿼리 실행 및 결과 가져오기
List<Customer> customers = query.getResultList();
return customers;
}
고급 개념
-
네임드 쿼리 (Named Queries): 자주 사용되는 쿼리의 경우,
@NamedQuery애노테이션을 사용하여 엔티티에 직접 한 번만 정의할 수 있습니다. 이는 쿼리를 체계적으로 관리하고 JPA 프로바이더가 시작 시점에 쿼리를 파싱하여 성능을 향상시킬 수 있게 합니다. -
생성자 표현식 (Constructor Expressions): 전체 엔티티를 가져오는 대신 쿼리 결과를 DTO(Data Transfer Object)에 직접 매핑할 수 있습니다.
SELECT NEW com.example.dto.CustomerInfo(c.name, c.email) FROM Customer c -
Criteria API: JPQL을 문자열로 작성하는 대신, JPA는 Criteria API를 제공합니다. 이를 통해 유창한(fluent) 자바 API를 사용하여 프로그래밍 방식으로 쿼리를 구축할 수 있습니다. 더 장황하지만 타입 안정성을 제공하여 컴파일 시점에 구문 오류를 방지할 수 있습니다.
references