JPQL (Java Persistence Query Language)
JPQL란?
테이블이 아닌 객체를 검색하는 객체지향 쿼리
| 1
2
3
4
5
 | --SQL: 테이블 대상
SELECT * FROM MEMBER WHERE USERNAME = 'kim';
--JPQL: 엔티티 객체 대상
SELECT m FROM Member m WHERE m.username = 'kim';
 | 
  - SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
- 엔티티와 속성은 대소문자를 구분함 (Member, username)
- JPQL 키워드는 대소문자를 구분하지 않음 (SELECT, FROM, WHERE)
- 엔티티 이름을 사용 (테이블 이름이 아님)
- 별칭(alias)은 필수 (member m)
단점
동적 쿼리를 만들기 어렵다.
| 1
2
3
4
5
6
7
8
 | // 동적 쿼리 만들기 어려움
String jpql = "select m from Member m where";
if (username != null) {
    jpql += " m.username = :username";
}
if (age != null) {
    jpql += " and m.age = :age";
}
 | 
객체지향 쿼리 언어 종류
JPQL
| 1
2
3
4
 | String jpql = "select m from Member m where m.username = :username";
List<Member> result = em.createQuery(jpql, Member.class)
    .setParameter("username", "kim")
    .getResultList();
 | 
Criteria
자바 코드로 JPQL을 작성할 수 있지만 복잡하고 실용성이 없음
QueryDSL - 오픈소스 라이브러리
자바 코드로 JPQL을 작성, 동적 쿼리 작성이 편리
| 1
2
3
4
5
6
7
8
 | // QueryDSL 사용 (간결하고 직관적!)
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = QMember.member;
List<Member> result = queryFactory
    .selectFrom(m)
    .where(m.username.eq("kim"))
    .fetch();
 | 
  - 문자가 아닌 자바 코드로 작성해 컴파일 시점에 문법 오류 발견
- 동적 쿼리 작성이 편리
- 단순하고 쉬워 실무에서 가장 많이 사용
- JPQL을 알면 쉽게 사용 가능
JDBC
JDBC 커넥션 직접 사용 또는 Spring JdbcTemplate, MyBatis 등과 함께 사용 가능
| 1
2
 | em.flush();  // 플러시로 DB 동기화
// 네이티브 SQL 실행
 | 
  - 영속성 컨텍스트를 적절한 시점에 수동으로 플러시(flush) 필요
경로 표현식
. 을 찍어 그래프를 탐색
상태 필드 (State Field)
  - 값을 저장하기 위한 필드
- 경로 탐색의 끝 더이상 탐색 불가
연관 필드 (Association Field)
단일 값 연관 필드
| 1
2
3
4
5
 | select m.team from Member m
// m.team은 단일 값 연관 필드
// 묵시적 조인 발생: SELECT * FROM MEMBER M INNER JOIN TEAM T ...
select m.team.name from Member m  // 계속 탐색 가능!
 | 
  - @ManyToOne,- OneToOne
- 대상이 엔티티
- 묵시적 내부 조인 발생
- 탐색 O
컬렉션 값 연관 필드
| 1
2
3
4
5
 | select t.members from Team t
// t.members는 컬렉션 값 연관 필드
// 묵시적 조인 발생
select t.members.username from Team t  // 오류 컬렉션에서 탐색 불가
 | 
컬렉션에서 탐색하려면 명시적 조인 사용
| 1
2
 | select m.username from Team t
join t.members m  // 명시적 조인으로 별칭 부여
 | 
  - @OneToMany,- @ManyToMany
- 대상이 컬렉션
- 묵시적 내부 조인 발생
- 탐색 X (더 이상 탐색 불가)
명시적 조인 VS 묵시적 조인
명시적 조인
JOIN 키워드를 직접 사용
| 1
 | select m from Member m join m.team t
 | 
묵시적 조인
경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 가능)
| 1
 | select m.team from Member m  // team 접근 시 자동으로 조인 발생실무에서 묵시적 조인은 가급적 사용하지 말자 명시적 조인만 사용하자
 | 
  실무 권장사항
  묵시적 조인은 가급적 사용하지 말고, 명시적 조인만 사용하자
  
    - 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움
- 성능 튜닝이 어려움
- 명시적 조인을 사용하면 쿼리를 예측하기 쉬움
페치 조인 (Fetch Join)
JPQL에서 성능 최적화를 위해 제공하는 기능
연관된 엔티티나 컬렉션을 SQL 한번에 조회하는 기능
| 1
2
3
4
5
 | // 일반 조인
select m from Member m join m.team t
// 페치 조인
select m from Member m join fetch m.team
 | 
엔티티 페치 조인
회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)
| 1
2
3
4
5
6
7
8
9
 | String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
    .getResultList();
for (Member member : members) {
    // 페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 X
    System.out.println("username = " + member.getUsername() 
        + ", teamname = " + member.getTeam().getName());
}
 | 
| 1
2
3
 | SELECT M.*, T.* 
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID = T.ID
 | 
  - 페치 조인으로 Member와 Team을 함께 조회
- 지연 로딩 설정이 있어도 페치 조인이 우선 (LAZY 무시)
- 실제 엔티티를 조회(프록시 아님)
컬렉션 패치 조인
일대다 관계에서 컬렉션 페치 조인
| 1
2
3
4
5
6
7
8
9
10
11
12
 | String jpql = "select t from Team t join fetch t.members";
List<Team> teams = em.createQuery(jpql, Team.class)
    .getResultList();
for (Team team : teams) {
    System.out.println("team = " + team.getName() 
        + " | members = " + team.getMembers().size());
    
    for (Member member : team.getMembers()) {
        System.out.println("-> member = " + member);
    }
}
 | 
페치 조인과 DISTINT
JPQL의 DISTINCT는 2가지 기능 제공
  - SQL에 DISTINCT 추가
- 애플레케이션에서 엔티티 중복제거
| 1
2
3
 | String jpql = "select distinct t from Team t join fetch t.members";
List<Team> teams = em.createQuery(jpql, Team.class)
    .getResultList();
 | 
| 1
2
3
 | SELECT DISTINCT T.*, M.* 
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
 | 
  - SQL의 DISTINCT는 데이터가 완전히 같아야 중복 제거
- JPQL의 DISTINCT는 SQL + 애플리케이션에서 같은 식별자를 가진 엔티티 중복 제거
페치조인과 일반조인의 차이
일반조인
| 1
 | select t from Team t join t.members m
 | 
  - 일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않음
- SELECT 절에 지정한 엔티티만 조회
| 1
2
 | SELECT T.* FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
 | 
  - Team만 조회, Member는 조회안됨 (지연 로딩)
페치 조인
| 1
 | select t from Team t join fetch t.members
 | 
  - 연관된 엔티티를 함께 조회(즉시로딩)
- 페치조인은 객체 그래프를 SQL 한번에 조회하는 개념
| 1
2
 | SELECT T.*, M.* FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
 | 
→ Team과 Member 모두 조회
페치조인의 한계
페치 조인 대상에는 별칭을 줄 수 없다.
| 1
2
3
4
5
 | --안됨
select t from Team t join fetch t.members m where m.age > 10
--이렇게 해야 함
select m from Member m where m.age > 10 and m.team = :team
 | 
둘 이상의 컬렉션은 페치 조인할 수 없다
| 1
2
3
4
 | --안됨
select t from Team t 
join fetch t.members 
join fetch t.orders
 | 
  - 데이터 정합성 문제 발생 가능
- 카테시안 곱으로 데이터 폭발
컬렉션을 패치조인하면 페이징 API를 사용할 수 없다
| 1
2
3
4
5
6
 | --경고 발생 (메모리에서 페이징)
String jpql = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(jpql, Team.class)
    .setFirstResult(0)
    .setMaxResults(10)
    .getResultList();
 | 
  - 일대다 페치 조인에서 페이징하면 메모리에서 페이징 처리
- 데이터가 많으면 메모리 초과 위험
해결방법 →
| 1
2
3
4
5
6
 | --단일 값 연관 필드는 페이징 가능 (1:1, N:1)
String jpql = "select m from Member m join fetch m.team";
List<Member> result = em.createQuery(jpql, Member.class)
    .setFirstResult(0)
    .setMaxResults(10)
    .getResultList();
 | 
페치 조인 사용 전략
페치 조인 사용처
  - 객체 그래프를 유지할 때 효과적
- 최적화가 필요한 곳에 적용
일반 조인 사용처
  - 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 할 때
- 필요한 데이터만 조회해서 DTO로 반환하는 것이 효과적
| 1
2
3
 | --DTO로 직접 조회
select new com.example.MemberTeamDto(m.username, t.name)
from Member m join m.team t
 | 
Named 쿼리 - 정적쿼리
미리 정의해서 이름을 부여해두고 사용하는 JPQL
| 1
2
3
4
5
6
7
8
 | @Entity
@NamedQuery(
    name = "Member.findByUsername",
    query = "select m from Member m where m.username = :username"
)
public class Member {
    ...
}
 | 
| 1
2
3
4
 | // 사용
List<Member> result = em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username", "kim")
    .getResultList();
 | 
  - 정적 쿼리만 가능
- 애플리케이션 로딩 시점에 초기화 후 재사용 (성능 이점)
- 애플리케이션 로딩 시점에 쿼리 검증 (오류를 미리 잡음)
- 어노테이션 또는 XML에 정의 가능
장점→
  - 로딩 시점에 JPQL 문법을 체크하고 파싱해둠
- 오류를 빨리 확인 가능
- 미리 파싱해두므로 성능 이점
벌크 연산
쿼리 한 번으로 여러 테이블 로우를 변경하는 연산
| 1
2
3
4
5
6
7
8
 | // 변경 감지(Dirty Checking)로 하면?
List<Member> members = em.createQuery("select m from Member m")
    .getResultList();
for (Member member : members) {
    member.setAge(member.getAge() + 1);  // 각각 UPDATE 쿼리
}
// 100명이면 100번의 UPDATE 쿼리 실행
 | 
UPDATE 벌크 연산
| 1
2
3
4
5
6
 | String qlString = "update Member m set m.age = m.age + 1";
int resultCount = em.createQuery(qlString)
    .executeUpdate();
System.out.println("resultCount = " + resultCount);
// 한 번의 쿼리로 모든 Member의 age 증가!
 | 
DELETE 벌크 연산
| 1
2
3
 | String qlString = "delete from Member m where m.age < 18";
int resultCount = em.createQuery(qlString)
    .executeUpdate();
 | 
벌크 연산 주의 사항
영속성 컨텍스트를 무시하고 DB에 직접 쿼리
| 1
2
3
4
5
6
7
8
9
10
11
 | Member member = new Member();
member.setUsername("kim");
member.setAge(20);
em.persist(member);
// 벌크 연산 실행
em.createQuery("update Member m set m.age = 30")
    .executeUpdate();
System.out.println(member.getAge());  // 20 (영속성 컨텍스트 값)
// DB에는 30, 영속성 컨텍스트에는 20 → 불일치
 | 
해결 방법
벌크연산을 먼저 실행
| 1
2
3
4
5
6
7
 | // 벌크 연산 먼저
em.createQuery("update Member m set m.age = 30")
    .executeUpdate();
// 그 다음 조회
Member member = em.find(Member.class, 1L);
System.out.println(member.getAge());  // 30
 | 
벌크연산 수행 후(flush()) 영속성 컨텍스트 초기화(clear)
| 1
2
3
4
5
6
7
8
9
10
 | Member member = em.find(Member.class, 1L);
// 벌크 연산
em.createQuery("update Member m set m.age = 30")
    .executeUpdate();
em.clear();  // 영속성 컨텍스트 초기화
Member findMember = em.find(Member.class, 1L);  // DB에서 다시 조회
System.out.println(findMember.getAge());  // 30
 | 
Spring Data JPA @Modifying
| 1
2
3
 | @Modifying(clearAutomatically = true)  // 자동으로 clear
@Query("update Member m set m.age = :age where m.id = :id")
int bulkAgeUpdate(@Param("age") int age, @Param("id") Long id);
 |