이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › JPA
JPQL
JPQL은 무엇이고, SQL과 무엇이 다른가
em.find는 기본 키 하나로 엔티티 하나만 찾습니다. 나이가 20 이상인 회원 모두처럼 조건으로 여럿을 찾으려면 쿼리(DB에 묻는 글)를 써야 하고, JPA에서는 그 쿼리를 JPQL이라는 글로 적습니다. JPQL은 SQL과 모양이 비슷하지만 테이블과 열이 아니라 엔티티 클래스와 필드의 이름으로 씁니다. 화면의 Member 클래스는 @Table로 member_tbl 테이블과, @Column으로 name 필드와 user_name 열과 짝이 되어 있어서 두 이름이 다릅니다. 코드의 jpql 변수에 담은 글을 em.createQuery가 JPA에 넘기면 JPA가 그 글을 읽고, query.getResultList()를 부르면 JPA가 JPQL을 SQL로 바꿔 DB에 보내고, 돌아온 행을 Member 객체로 만들어 list에 담습니다. 이 실습은 JPQL에 적는 이름을 엔티티 이름에서 테이블 이름, 열 이름으로 바꿔 가며 같은 코드를 실행해, DB에 나간 SQL과 list에 돌아온 것, 그리고 오류를 봅니다.
실행으로 대조 · Hibernate 7.4.5와 H2 2.4.240에서 네 가지 조합을 화면의 코드째 컴파일해 실행하고, 나간 SQL과 돌아온 객체와 오류가 난 줄을 대조
이 실습에 나오는 개념
- JPQL
- 테이블이 아니라 엔티티와 그 필드를 대상으로 작성하는 JPA의 쿼리 언어다. SQL과 문법이 비슷하며, 실행할 때 JPA 구현체가 사용 중인 데이터베이스에 맞는 SQL로 변환한다. 특정 데이터베이스의 문법에 의존하지 않는다는 장점이 있다.
- em.createQuery와 getResultList
- em.createQuery(글, 결과 클래스)는 JPQL로 쓴 글을 JPA에 넘겨 쿼리 객체(TypedQuery)를 만들어 돌려주는 메서드이고, 그 쿼리 객체의 getResultList()는 SELECT 쿼리를 실행해 결과를 목록(List)으로 돌려주는 메서드이다. 글이 잘못되었거나 결과 클래스가 맞지 않으면 createQuery가 IllegalArgumentException을 던진다고 API 문서가 정한다. Hibernate 7.4.5에서 관찰하면 글 속의 엔티티 이름과 필드 이름이 틀린 오류는 getResultList가 아니라 createQuery 줄에서 났고, DB로 SQL이 나가는 것은 getResultList 줄이었다. 반대로 em.find(클래스, 기본 키)는 기본 키 값 하나로 엔티티 하나를 찾는 메서드여서, 조건으로 여러 개를 찾을 때는 쿼리를 쓴다.
- 엔티티 이름과 필드 이름
- JPQL은 테이블과 열이 아니라 엔티티와 그 필드를 대상으로 쓰는 쿼리여서, from 뒤에는 엔티티 이름을, 조건에는 그 엔티티의 필드 이름을 적는다. Jakarta Persistence 명세 4.3.1절은 쿼리 문자열에서 엔티티를 엔티티 이름으로 가리키며 이름은 @Entity의 name이고 정하지 않으면 엔티티 클래스의 비한정 이름(패키지를 뺀 클래스 이름)이 된다고 정한다. Hibernate 안내서는 HQL과 JPQL에서 데이터베이스의 테이블과 열을 직접 가리킬 수 없다고 적는다. 그래서 클래스가 @Table로 member_tbl 테이블에, 필드가 @Column으로 user_name 열에 짝지어져 있어도 JPQL에는 클래스 이름 Member와 필드 이름 name을 쓰고, 테이블 이름이나 열 이름을 적으면 오류가 난다.
- @Table
- @Table은 엔티티 클래스 위에 붙여, 그 엔티티가 짝이 되는 테이블(주 테이블)을 정하는 애너테이션이다. name에 테이블 이름을 적고, 적지 않으면 엔티티 이름이 쓰인다고 API 문서가 정하며, @Table 자체를 붙이지 않아도 이 기본값이 적용된다. 이 이름은 SQL에 나가는 테이블 이름일 뿐이어서, JPQL의 from 뒤에는 쓸 수 없고 클래스의 엔티티 이름을 적어야 한다. 필드에 대응하는 열 이름을 정하는 것은 @Column의 name이다.
이렇게 바꿔 보세요
- 처음 설정으로 — Member와 age를 쓴 JPQL. JPA가 SQL로 바꿔 DB에 보내고, 돌아온 행을 Member 객체 2개로 만들어 줍니다.
- 테이블 이름을 적으면 — JPA가 member_tbl이라는 엔티티를 찾지 못해, createQuery 줄에서 오류가 납니다. DB에는 아무것도 나가지 않습니다.
- 열 이름을 적으면 — Member에 user_name이라는 필드가 없어서, createQuery 줄에서 오류가 납니다.
- 필드 이름을 적으면 — JPA가 name 필드를 user_name 열로 바꿔 SQL을 만듭니다. 조건에 맞는 kim 한 명이 Member 객체로 돌아옵니다.
처음 설정으로 실행하면
- DB의 준비: 코드가 실행되기 전에 DB에는 member_tbl 테이블이 있고, 행이 세 개 들어 있습니다. (id 1, user_name kim, age 25), (id 2, user_name lee, age 17), (id 3, user_name park, age 30)입니다. 화면의 Member 클래스는 이 테이블과 짝이 됩니다. @Table이 테이블 이름 member_tbl을 정하고, @Column이 name 필드의 열 이름 user_name을 정합니다. 코드의 em은 EntityManager이고 JPA에 일을 시키는 객체입니다. em.find 메서드는 기본 키(id) 하나로 행 하나만 찾아 주므로, 나이가 20 이상인 회원 모두처럼 조건으로 여럿을 찾으려면 쿼리를 써야 합니다.
- String jpql = "select m from Member m where m.age >= 20": JPQL로 쓴 쿼리를 글자로 적어 jpql 변수에 담습니다. 아직 글자일 뿐이라서 JPA도 DB도 이 글을 읽지 않았고, SQL은 나가지 않았습니다. 이 글에서 Member는 우리가 쓴 클래스의 이름이고 m.age는 그 클래스의 age 필드입니다.
- TypedQuery<Member> query = em.createQuery(jpql, Member.class): em이 jpql 글을 JPA에 넘기고, JPA가 글을 읽으면서 적힌 이름을 확인합니다. Member는 JPA가 아는 엔티티의 이름이고(엔티티의 이름은 기본으로 클래스 이름입니다), m.age는 그 엔티티의 필드입니다. 이름이 모두 맞으므로 JPA가 쿼리 객체를 만들어 query에 담습니다. 이 줄까지 나간 SQL은 0번입니다. 쿼리 객체를 만든 것이지 아직 DB에 묻지 않았습니다.
- List<Member> list = query.getResultList(): 이제 JPA가 JPQL을 SQL로 바꿔 DB에 보냅니다. 이 실행에서 Hibernate 7.4.5가 만들어 보낸 SQL은 "select m1_0.id,m1_0.age,m1_0.user_name from member_tbl m1_0 where m1_0.age>=20"입니다. JPQL의 Member가 테이블 이름 member_tbl로, 조건의 m.age가 열 이름 age로 바뀌어 있습니다. 우리가 쓴 코드에는 이 SQL이 없고, JPQL을 SQL로 옮기는 일은 JPA가 했습니다. SQL의 글자는 Hibernate 버전에 따라 다를 수 있습니다.
- DB가 조건에 맞는 행 2개(kim, park)를 돌려주고, JPA가 행마다 Member 객체를 만들어 열의 값을 필드에 채웁니다. user_name 열의 값은 name 필드로 들어갑니다. 이 객체들이 list에 담겨 돌아옵니다. 돌아온 것은 행이 아니라 Member 클래스의 객체 2개이고, 이 실행에서 list에 담긴 객체의 클래스는 모두 Member였습니다. 나이가 17인 lee는 조건에 맞지 않아 돌아오지 않았습니다.
실행 결과
- JPQL은 SQL과 모양이 비슷하지만 테이블과 열이 아니라 엔티티 클래스(Member)와 그 필드(age)의 이름으로 쓰는 쿼리입니다. JPA가 이 글을 SQL로 바꿔 DB에 보냈고, 돌아온 행 2개는 Member 객체 2개가 되어 list에 담겼습니다.
- em.find와 달리 조건으로 여러 개를 찾았습니다. 쿼리에 적은 Member는 SQL에서 member_tbl이 되었습니다. 쿼리에 적는 이름과 DB의 이름이 다를 수 있고, 둘을 이어 주는 것이 @Table과 @Column입니다.
코드
@Entity
// 테이블: Member 클래스 하나가 member_tbl 테이블 하나와 짝이 된다
@Table(name = "member_tbl")
public class Member {
@Id
private Long id;
// 열: name 필드는 user_name 열과 짝이 된다
@Column(name = "user_name")
private String name;
private int age;
}
// 조회하는 쪽
// JPQL: JPA가 읽는 쿼리 글
String jpql = "select m from Member m where m.age >= 20";
TypedQuery<Member> query = em.createQuery(jpql, Member.class);
List<Member> list = query.getResultList();
이 모형의 전제
Hibernate 7.4.5와 H2 2.4.240(메모리 DB)에서 JDK 21로 실행해 대조했습니다. 스프링은 쓰지 않고 Hibernate를 코드로 직접 시작했습니다(Configuration에 클래스를 등록, hbm2ddl.auto=create-drop으로 테이블을 만듦). 네 가지 조합을 모두, 화면의 코드를 글자 그대로 컴파일해 실행했습니다. 화면의 Member 클래스는 import 줄만 더해 Member.java로, "조회하는 쪽" 네 줄은 EntityManager를 받는 메서드의 본문으로 넣었습니다. 화면에 보이지 않는 것은 import 줄, DB 연결 설정, EntityManager를 얻고 닫는 코드이고, 이 실습은 읽기만 하므로 트랜잭션은 시작하지 않았습니다. 조합마다 DB를 새로 만들고, 실행 전에 JDBC로 member_tbl에 세 행((1, kim, 25), (2, lee, 17), (3, park, 30))을 미리 넣었습니다. 이 준비의 SQL은 세지 않았습니다. 대조한 것은 오류가 났는지와 어느 줄에서 났는지(코드 줄을 스택 트레이스로 찾음), 예외의 종류와 메시지, Hibernate의 StatementInspector로 잡은 SQL의 글자와 수와 그 SQL이 나간 줄, 돌아온 list의 크기와 원소의 클래스와 값입니다. 단계 문장이 기대는 사실도 코드 줄마다 바로 아래에 측정 줄을 끼운 두 번째 실행에서 쟀습니다. jpql 변수와 createQuery 줄을 지난 시점에 나간 SQL은 0번이고 getResultList 줄을 지난 뒤 1번이라는 것, 오류가 나는 두 조합에서는 오류가 createQuery 줄에서 나고 다음 줄은 실행되지 않으며 SQL이 한 번도 나가지 않는다는 것, 돌아온 객체의 클래스가 Member 그대로라는 것입니다. 문서가 정한 것은 Jakarta Persistence 3.2 명세 4.1절(JPQL은 문자열로 쓰는 동적 쿼리의 언어), 4.3절(쿼리의 대상은 엔티티와 임베더블의 추상 스키마 타입), 4.3.1절(쿼리 문자열에서는 엔티티를 엔티티 이름으로 가리키며, 이름은 @Entity의 name이고 기본은 엔티티 클래스의 비한정 이름)과 EntityManager.createQuery, TypedQuery.getResultList, Table, EntityManager.find의 API 문서(createQuery는 질의 문자열이 잘못되면 IllegalArgumentException을 던지고, getResultList는 SELECT 질의를 실행해 목록을 돌려주며, @Table의 name은 기본이 엔티티 이름, find는 기본 키로 하나를 찾는다는 것), Hibernate Query Language 안내서 1.1절(HQL과 JPQL에서는 테이블과 열을 직접 가리킬 수 없고 엔티티 클래스와 그 필드를 가리킨다는 것)입니다. 명세 페이지는 요약으로만 돌아와 해당 절의 문장만 확인했고 절 전체를 읽지는 못했습니다. 문서가 정하지 않고 이 버전에서 관찰한 것은 JPQL을 읽어 이름을 확인하는 일이 createQuery 줄에서 일어난다는 것(오류가 그 줄에서 난 것)과, SQL이 getResultList 줄에서 나간다는 것, 오류의 예외 종류와 메시지, 나간 SQL의 글자(별칭 m1_0 등)입니다. 이 글자들은 Hibernate 버전에 따라 다를 수 있습니다. MySQL에서의 실행 대조는 하지 않았습니다. 이 실습은 조인과 페치 조인, UPDATE와 DELETE를 하는 벌크 쿼리, 조건이 바뀌는 동적 쿼리, 파라미터 바인딩, 결과를 받는 여러 방법은 다루지 않으며, 공부 탭의 "JPQL과 페치 조인", "벌크 연산", "동적 쿼리와 Querydsl" 실습에서 다룹니다.
앞 실습: 지연 로딩 · 다음 실습: 테이블, 행, 열