beadss / jpa-study

jpa슽터디입니다
1 stars 2 forks source link

10장 정리-1 #29

Open joont92 opened 5 years ago

joont92 commented 5 years ago

JPA에서 현재까지 사용했던 검색은 아래와 같다.

하지만 현실적으로 이 기능만으로 어플리케이션을 개발하기에는 무리이다.
그렇다고 모든 엔티티를 메모리에 올려두고 어플리케이션 내에서 필터하는 것은 현실성이 없는 소리이다.
즉, 데이터베이스에서 필터해서 조회해올 무언가가 필요하고, 그게 객체지향 쿼리 언어(JPQL)이다.

조회 방식에는 JPQL말고 몇가지 더 있는데, 아래는 JPA에서 공식 지원하는 검색 방법이다.

그리고 아래는 JPA에서 공식 지원하지는 않지만 알아둘 가치가 있는 애들이다.

JPQL

엔티티 객체를 조회하는 객체지향 쿼리 언어이다.
문법은 SQL과 비슷한데, 실제론 SQL을 추상화 한것이기 때문에 특정 데이터베이스에 의존하지 않는 특징이 있다.

SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다.
(참고로 엔티티 저장은 그냥 entityManager.persist를 사용하면 되므로 INSERT 문은 없다.)
JPQL에서 UPDATE, DELETE 문은 벌크 연산이라고 해서 뒤에서 따로 설명할 것이므로, SELECT 문만 작성하겠다.

기본 문법

기본 형태는 아래와 같다.

SELECT m FROM Member AS m WHERE m.username = 'Hello'
  1. 대소문자 구문
    • 엔티티와 속성은 대소문자를 구분한다. Member와 member는 다르고 username과 USERNAME은 다르다.
    • SELECT, FROM 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
  2. 엔티티 이름
    • FROM 이후에 오는 대상은 테이블 이름이 아니라 엔티티 이름이다.
    • 기본값인 클래스명을 엔티티명으로 사용하는 것을 추천한다.
  3. 별칭은 필수
    • JPQL은 별칭을 필수로 사용해야 한다. AS 뒤에 m이 Member의 별칭이다.
    • AS는 생략 가능하다.

TypedQuery, Query

작성한 JPQL을 실행시키기 위해 만드는 쿼리 객체이다.
JPQL이 반환할 타입을 명확하게 지정할 수 있으면 TypedQuery를 사용하고, 명확하게 지정할 수 없으면 Query를 사용하면 된다.

// 조회대상이 정확히 Member 엔티티이므로 TypedQuery 사용 가능
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

// 조회대상이 String, Integer로 명확하지 않으므로 Query 사용
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");

TypedQuery로 실행된 쿼리는 두번쨰 인자로 주어진 클래스를 반환하고,
Query의 경우 예제처럼 조회 컬럼이 1개 이상일 경우 Object[], 1개일 경우 Object를 반환한다.

결과 조회

쿼리 객체에서 아래의 메서드들을 사용해 JPQL을 실행한다.

파라미터 바인딩

아래와 같은 이름 기준 파라미터 바인딩을 지원한다.

TypedQuery<Member> query = 
    em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class)
    .setParameter("username", "joont1"); // JPQL은 대부분 메서드 체인 방식으로 되어있어서 이렇게 연속해서 작성하는 것이 가능하다

List<Member> result = query.getResultLst();

username은 Member 클래스에 정의된 프로퍼티 이름이다. 앞에 :를 붙여서 바인딩한다.
username 에 joont1 이 바인딩 될 것이다.

참고로 아래와 같이 위치 기준 파라미터 바인딩도 지원하기는 한다.

TypedQuery<Member> query = 
    em.createQuery("SELECT m FROM Member m WHERE m.username = ?1", Member.class)
    .setParameter(1, "joont1");

이것보다는 전자가 더 명확하다.

파라미터 바인딩 방식은 선택이 아닌 필수이다

  • JPQL에 직접 문자를 더하면 SQL Injection을 당할 수 있다
  • JPA에서 파라미터만 다를 뿐 같은 쿼리로 인식하므로, JPQL을 SQL로 파싱한 결과를 재사용할 수 있다
  • SQL 내에서도 같은 쿼리는 결과를 재사용한다

프로젝션

조회할 대상을 지정하는 것을 프로젝션이라고 한다.
SELECT [프로젝션 대상] FROM 으로 대상을 지정한다.
대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다.

엔티티 프로젝션

SELECT m FROM Member m // member

SELECT m.team FROM Memher m // team

둘 다 엔티티를 프로젝션 대상으로 사용했다.
참고로 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

JPQL로 조회할 때는 쿼리 캐시가 되어있지 않은 이상 영속성 컨텍스트를 뒤지지 않고 그냥 조회 -> 영속성 컨텍스트에 저장 하지 않을까?
JPQL을 실행하면서 영속성 컨텍스트에 있는 애들은 조회하지 않고 하는건 말이 안되는 행위인듯

임베디드 타입 프로젝션

엔티티를 통해서 조회한다.

Address address = 
    em.createQuery("SELECT m.address FROM Member m", Address.class)
    .getSingleResult();

임베디드 타입은 엔티티 타입이 아닌 값 타입이므로
이렇게 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.

스칼라 타입 프로젝션

// 이름조회
TypedQuery<String> query = em.createQuery("SELECT m.username FROM Member m", String.class);
List<String> resultList = query.getResultList();

// 이름조회(중복제거)
TypedQuery<String> query = em.createQuery("SELECT DISTINCT m.username FROM Member m", String.class);
List<String> resultList = query.getResultList();

// 통계 쿼리
TypedQuery<Double> query = em.createQuery("SELECT AVG(o.orderAmount) FROM Order o", Double.class);
List<Double> resultList = query.getResultList();

조회되는 컬럼이 1건이라 TypedQuery를 사용하였다. 보다시피 통계 쿼리도 스칼라 타입으로 조회할 수 있다.

여러 값 조회

아래와 같이 여러값으로 조회했을 때는 TypedQuery를 사용할 수 없고, Query만 사용할 수 있다.

Query query = em.createQuery("SELECT m.username, m.age, m.team FROM Member m");
List<Object[]> resultList = query.getResultList();

for(Object[] row : resultList){
    String username = (String)row[0];
    Integer age = (Integer)row[1];
    Team team = (Team)row[2];
}

물론 아때도 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

NEW 명령어

NEW 명령어를 사용하면 Object[] 대신 바로 객체로 생성해서 받아볼 수 있다.

TypedQuery<UserDTO> query = 
    em.createQuery("SELECT NEW com.joont.dto.UserDTO(m.username, m.age, m.team) FROM Member m", UserDTO.class);

List<UserDTO> resultList = query.getResultList();

기존이라면 하나하나 번거롭게 변환했어야 했을 작업을 NEW 명령어를 사용해서 간단하게 처리했다.
NEW 명령어를 사용하려면 아래 2가지를 주의해야 한다.

페이징 API

JPA는 데이터베이스들의 페이징들을 아래의 두 API로 추상화했다.
(페이징은 데이터베이스마다 문법이 다 다르다)

TypedQuery<Member> query = 
    em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);

query.setFirstResult(10);
query.setMaxResult(20);
query.getResultList();

11번쨰 데이터부터 시작해서 20개를 조회한다. 즉 11~30번 데이터를 조회하게 된다.
지원하는 모든 데이터베이스를 추상화했기 때문에 데이터베이스가 바껴도 방언만 바꿔주면 된다.

집합과 정렬

집합 함수

함수 설명 리턴타입
COUNT 결과 수를 구한다 Long
MAX, MIN 최대, 최소값을 구한다 대상에 따라 다름
AVG 평균값을 구한다. 숫자타입만 사용할 수 있다. 숫자가 아니면 0을 리턴한다. Double
SUM 합을 구한다. 숫자타입만 사용할 수 있다. 정수합 : Long
소수합 : Double

집합 함수 사용 시 참고사항

  • 통계를 계산할 때 NULL값은 무시된다.
  • 값이 없을 때 SUM, AVG, MAX, MIN를 사용하면 NULL을 리턴한다. COUNT는 0을 리턴한다.
  • DISTINCT를 집합 함수안에 사용하면 중복된 값을 제거하고 집합을 구한다.

그룹핑

GROUP BY, HAVING도 사용할 수 있다.

SELECT t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
FROM Member m LEFT JOIN m.team t
GROUP BY t.name  
HAVING AVG(m.age) >= 10

(team의 이름으로 그룹화한 뒤 나이의 평균이 10살 이상인 그룹에 대해서 집합을 구했다.)
문법은 아래와 같다.

group by절 : GROUP BY {단일값 경로 | 별칭} having절 : HAVING 조건식

이런식의 통계 쿼리는 보통 전체 데이터를 기준으로 사용하므로 실시간으로 사용하기에는 부담이 많다.

정렬

ORDER BY도 사용할 수 있다.

SELECT t.name, COUNT(m.age) AS cnt  
FROM Member m LEFT JOIN m.team t  
GROUP BY t.name  
ORDER BY t.name ASC, cnt DESC

문법은 아래와 같다.

order by절 : ORDER BY {상태필드 경로 | 결과변수 [ASC | DESC]}

상태필드는 m.name 같이 객체의 상태를 나타내는 필드를 말하고,
결과변수는 SELECT 절에 나타나는 값을 말한다. 위의 예제에서는 cnt가 결과변수이다.

조인

내부 조인

SELECT m FROM Member m INNER JOIN m.team t

보다시피 일반적인 SQL 조인과 조금 다르다.
가장 큰 특징은 연관 필드를 사용해서 조인한다는 점이다.
즉, 조인을 사용하려면 엔티티에 연관관계 명시는 필수적으로 되어있어야 한다.
(위와 같이 작성하면 Member의 team 필드에서 관계의 정보를 얻은 뒤 조인할 것이다)

아래는 잘못 작성된 JPQL 조인이다.

SELECT m FROM Member m INNER JOIN Team t // 잘못된 조인

만약 조인한 두 개의 엔티티를 조회하려면 다음과 같이 작성하면 된다.

SELECT m,t
FROM Member m INNER JOIN m.team t

서로 다른 타입의 두 엔티티를 조회했으므로 TypedQuery는 사용할 수 없다.

List<Object[]> list = em.createQuery(jpql).getResultList(); // 위에서 작성한 쿼리

for(Object[] o : list){
    Member m = (Member)o[0];
    Team t = (Team)o[1];
}

외부 조인

키워드만 바꿔주면 된다.

SELECT m FROM Member m LEFT JOIN m.team t

컬렉션 조인

일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 말한다.
아래와 같이 컬렉션 값 연관 필드를 사용하면 된다.

SELECT t, m FROM Team t LEFT JOIN t.members m

세타 조인

CROSS JOIN을 말한다. 세타 조인은 내부 조인만 지원한다는게 무슨말.. 인지?

ON절

JPA 2.1부터 조인할 떄 ON 절을 지원한다.

SELECT m, t 
FROM Member m LEFT JOIN m.team t
ON t.name = 'A'

실행 결과는 아래와 같다.

SELECT m.*, t.*
FROM Member m
LEFT JOIN Team t ON m.team_id = t.id AND t.name = 'A'

ON 절을 사용하면 조인 대상을 필터링 하고 사용할 수 있다.

패치 조인

패치조인은 SQL에 있는 개념은 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
연관된 엔티티나 컬렉션을 한번에 같이 조회하는 기능이고, 문법은 아래와 같다.

fetch join : [ LEFT [ OUTER ] | INNER ] JOIN FETCH 조인경로

엔티티 패치 조인

SELECT m 
FROM Member m INNER JOIN FETCH m.team

실행해보면 SELECT 절에 m 만 명시했음에도 불구하고, M.*, T.*의 형태로 연관된 팀까지 함께 조회한다.
그리고 기존의 INNER JOIN 에서 Object[] 로 받아야 했던것 과는 달리, Member의 team 변수에 값이 다 채워진 상태로 리턴된다.
즉, 객체 그래프를 그대로 유지하면서 받을 수 있는 방법이다. 그러므로 성능 최적화를 위해 제공화는 기능이라고 하는 것이다.

List<Member> list = em.createQuery(jpql, Member.class).getResultList(); // 위에서 작성한 쿼리

for(Member m : list){
    System.out.println(m.getTeam().getName());
}

(Member의 Team은 fetch가 LAZY라고 가정한다)
패치 조인을 통해 이미 연관된 팀을 같이 조히했으므로 위와 같이 수행해도 LAZY 로딩이 발생하지 않는다.

컬렉션 패치 조인

일대다 관계에서도 패치 조인을 사용할 수 있다.

SELECT t
FROM Team t INNER JOIN FETCH t.members

이것 또한 SELECT 절에 t 만 명시했음에도 불구하고, T.*, M.*의 형태로 연관된 회원까지 함께 조회된다.
근데 여기서 주의할 점이 있는데, 쿼리의 결과가 증가해서 그런지 위 jpql의 결과를 리스트로 받아보면 Team의 개수가 Member의 개수와 동일함을 볼 수 있다.

List<Team> list = em.createQuery(jpql, Team.class).getResultList(); // 위에서 작성한 쿼리

for(Team t : list){
    System.out.println(t);

    for(Member m : t.getMembers()){
        System.out.println("-> " + m);
    }
}

Team@0x100 -> Member@0x200 -> Member@0x300 Team@0x100 -> Member@0x200 -> Member@0x300

이렇듯 일대다 조인은 결과가 증가할 수 있음에 주의해야 한다.

DISTINCT

JPQL의 DISTINCT는 SQL에 DISTINCT를 추가하는 것은 물론이고, 어플리케이션에서 한번 더 중복을 제거한다.
이 특징을 이용해서 위의 컬렉션 패치 조인에서 리스트가 중복되서 나오는 문제를 해결할 수 있다.

SELECT DISTINCT t
FROM Team t INNER JOIN FETCH t.members

이렇게 작성하면 먼저 SQL에 DISTINCT가 적용된다.
하지면 지금은 로우의 데이터가 다르므로 DISTINCT는 효과가 없다.

다음으로 어플리케이션에서 DISTINCT 명령을 보고 중복된 데이터를 걸러낸다.
SELECT DISTINCT t는 Team 엔티티의 중복을 제거하라는 의미이므로, 여기에서 중복이 제거되고, 예상했던 결과를 받아볼 수 있게된다.

패치조인과 일반조인의 차이

위에서도 언급했지만, 일반조인의 경우 결과를 반환할 때 연관관계까지 고려하지 않는다.
단지 SELECT 절에 지정한 엔티티만을 조회하고, 연관된 엔티티에 대해서는 프록시나 컬렉션 래퍼를 반환한다.

List<Team> list = em.createQuery(jpql, Team.class).getResultList(); // 일반 조인 쿼리

for(Team t : list){
    System.out.println(t.getMembers.get(0)); // ?
}

그러므로 위와 같이 조회하면
fetchType이 LAZY일 경우 ? 부분에서 LAZY 로딩이 발생할 것이고,
fetchType이 EAGER일 경우 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한번 더 실핼하게 된다.

패치조인의 특징과 한계

패치조인은 글로벌 전략보다 우선한다. (글로벌 전략 : 엔티티에 직접 적용하는 로딩 전략 e.g. fetch=FetchType.LAZY)
그러므로 최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하기 보다는 글로벌 전략은 지연 로딩으로 설정하고 최적화가 필요한 곳에서 패치조인을 사용하는 것이 전체적으로 봤을때 훨씬 효과적이다.

물론 이런 좋은 패치조인에도 한계가 있다.

이렇듯 패치조인으로 모든것을 해결할수는 없다. 필요할 때만 사용하여 성능 최적화를 꾀하는 것이 좋다.

경로 표현식

경로 표현식이란 .(점)을 찍어 그래프를 탐색하는 것을 말한다.

SELECT m.username
FROM Member m
    INNER JOIN m.team t
    INNER JOIN m.orders o
WHERE t.name = 'TeamA';

여기서 m.username, m.team, m.orders, t.name 모두 경로 표현식이다.

아래는 경로 표현식의 종류와 특징들이다.

단일 값 연관 경로 탐색 예제

SELECT o.member from Order o

위 JPQL은 아래와 같이 변환된다.

SELECT m.*
FROM Order_ o
    INNER JOIN Member m ON o.member_id = m.id

위처럼 JPQL에 JOIN을 적어주지 않았는데 JOIN이 발생하는 것을 묵시적 조인이라고 하고, JOIN을 직접 적어주는 것을 명시적 조인이라고 한다.
묵시적 조인은 내부 조인만 가능 하다. 외부 조인을 하고 싶으면 명시적 조인을 사용해야 한다.

컬렉션 값 연관 경로 탐색

컬렉션 값에서는 경로 탐색이 불가능하다(가장 많이 하는 실수)

SELECT t.members.username FROM Team t // 실패

만약 경로 탐색을 하고 싶으면 명시적 조인을 사용해서 외부 별칭을 획득해야 한다.

SELECT m.username
FROM Team t  
INNER JOIN t.members m

참고로 컬렉션을 컬렉션의 크기를 구할 수 있는 size라는 특별한 기능을 제공한다.

SELECT t.member.size FROM Team t

는 COUNT 함수를 사용하는 함수로 적절히 변환된다.

기본적으로 쿼리에서 조인이 성능상 차지하는 부분은 아주 크다.
단순하면 별로 문제될 것 없으나, 복잡하고 성능이 중요하면 분석이 용이하도록 명시적 조인을 사용하는 것이 좋다.

서브쿼리

JPQL에서는 서브쿼리를 WHERE, HAVING 절에서만 사용할 수 있다. SELECT, FROM 절에서는 사용할 수 없다.
아래는 간단한 서브쿼리 예시이다.

// 회원들의 평균 나이를 넘는 회원 조회
SELECT m
FROM Member m
WHERE m.age > (SELECT AVG(m2.age) FROM Member m2)

서브쿼리 함수

// teamA에 소속인 회원
SELECT m 
FROM Memner m 
WHERE EXISTS (
    SELECT t
    FROM m.team t
    WHERE t.name = 'teamA'
)
// 전체 상품 각각의 재고보다 주문량이 많은 주문들  
SELECT o
FROM Order o
WHERE o.orderAmoun > ALL(
    SELECT p.stockAmoun from Product p // o.p가 아니고?
)

// 어떤 팀이든 팀에 소속된 회원  
SELECT m
FROM Member m
WHERE m.team = ANY(
    SELECT t
    FROM Team t // 이것도 좀 이상한데...
)

조건식

타입 표현

종류 설명 예제
문자 작은 따옴표 사이에 표현.
작음 따옴표를 표현하고 싶으면 작은 따옴표 2개('') 사용
'HELLO'
'She''s '
숫자 L(Long 타입 지정)
D(Double 타입 지정)
F(Float 타입 지정)
10L
10D
10F
날짜 DATE {d 'yyyy-mm-dd'}
TIME {t 'hh:mm:ss'}
TIMESTAMP {ts 'yyyy-mm-dd hh:mm:ss.f}
m.createDate = {d '2012-03-24'}
Boolean TRUE, FALSE
Enum 패키지명을 포함한 전체 이름 com.joont.MemberType.Admin
엔티티 타입 엔티티의 타입을 표현함. 주로 상속과 관련해 사용. TYPE(m) = Member

연산자 우선 순위

  1. 경로 탐색 연산 : .
  2. 수학 연산 : +(단항 연산), -(단항 연산), *, /, +, -
  3. 비교 연산 : =, >, >=, <, <=, <>,
    [NOT] BETWEEN, [NOT] LIKE, [NOT] IN,
    IS [NOT] NULL, IS [NOT] EMPTY, [NOT] MEMBER [OF], [NOT] EXISTS
  4. 논리연산 : NOT, AND, OR

IS [NOT] EMPTY, [NOT] MEMBER [OF] 만 뺴면 사용법은 일반적인 SQL과 동일하다.
이 두개는 컬렉션 식으로써 JPA에서 제공하는, 컬렉션에만 사용가능환 특별 기능이다.

컬렉션 식

컬렉션에만 사용될 수 있음에 주의해야 한다. 컬렉션이 아닌 곳에 사용하면 오류가 발생한다.

SELECT m
FROM Member m
WHERE m.orders IS EMPTY

는 아래와 같이 실행된다.

SELECT m.*
FROM Member m
WHERE EXISTS (
    SELECT o.id
    FROM Order_ o
    WHERE o.member_id = m.id
)
// 전달된 멤버가 포함되어 있는 팀 조회  
SELECT t
FROM Team t
WHERE :memberParam MEMBER OF t.members

스칼라 식

숫자, 문자, 날짜, case, 엔티티 타입 같은 가장 기본적인 타입들을 스칼라 타입이라고 한다.

CASE 식

SELECT COALESCE(m.usernae, 'nobody') 
FROM Member m

다형성 쿼리

상속관계(@Inheritance)로 구성된 엔티티를 JPA에서 조회하면 그 자식 엔티티도 같이 조회한다.
이건 기존과 동일하다.

TYPE

상속 구조에서 조회 대상을 특정 타입으로 한정할 때 사용한다.

SELECT i
FROM Item i
WHERE TYPE(i) IN(Book, Movie)

는 아래와 같이 실행된다

SELECT i.*
FROM Item i
WHERE i.DTYPE IN('B', 'M')

TREAT

상속 구조에서 부모 타입을 특정 타입으로 다룰 때 사용한다.(자바의 타입 캐스팅과 비슷하다)
JPA 표준은 FROM, WHERE절에서만 사용 가능하고, 하이버네이트의 경우 SELECT에서도 가능하다.

SELECT i
FROM Item i
WHERE TREAT(i as Book).author = 'kim'

Item을 자식 타입인 Book으로 다뤘다. 그래서 Book의 필드인 author에 접근할 수 있다.

사용자 정의 함수 호출(since JPA2.1)

JPA 2.1부터 사용자 정의 함수를 지원한다.

문법 : FUNCTION(function_name {, function_arg}*)

SELECT FUNCTION('group_concat', i.name)
FROM Item i

하이버네이트를 사용할 경우 아래와 같이 방언 클래스를 상속해서 사용할 데이터베이스 함수를 미리 등록해야 한다.

public class MyH2Dialect extends H2Dialect{
    public MyH2Dialect(){
        registerFunction(
            "group_concat", 
            new StandardFunction("group_concat", StandardBasicTypes.STRING)
        );
    }
}

registerFunction의 두번째 인자로는 하이버네이트의 SQLFunction 구현체를 주면 된다.
지금은 기본 함수를 사용하겠다는 의미로 StandardFunction을 사용하였고,
첫번째 인자로 함수 이름, 두번째 인자로 리턴 타입을 주고 있는 모습이다.

상속한 Dialect는 아래와 같이 등록하면 되고,

<property name="hibernate.dialect" value="com.joont.dialect.MyH2Dialect" />

하이버네이트를 사용하면 기본 문법보다 축약해서 사용할 수 있다.

SELECT group_concat(i.name)
FROM Item i

엔티티 직접 사용

객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별하기 때문에
JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
몇 가지 예시를 보자.

SELECT COUNT(m)
FROM Member m

은 아래와 같이 변환된다.

SELECT COUNT(m.id)
FROM Member m

실행되는 sql은 아래와 같다.

SELECT m.*
FROM Member m 
WHERE m.team_id = ? -- team 파라미터 id 값

MEMBER 테이블은 이미 TEAM의 식별자 값을 가지고 있기 때문에 묵시적 조인은 일어나지 않는다.

Named 쿼리(정적 쿼리)

em.createQuery("select ... ") 처럼 JPQL을 직접 문자로 넘기는 것을 동적 쿼리라고 하고,
미리 정의한 쿼리에 이름을 부여해서 해당 이름으로 사용하는 것을 Named 쿼리(정적 쿼리)라고 한다.

Named 쿼리는 어플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해두므로 오류를 빨리 확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다.

Named 쿼리는 @NamedQuery 어노테이션을 사용해서 자바 코드에 작성하거나 XML 문서에 작성할 수 있다.

어노테이션에 정의

기타