- Today
- Total
개발하는 고라니
[JPA] 페치 조인 (FETCH JOIN) 본문
진행하기 앞서..
JPA를 한다면 JPQL(Java Persistence Query Language)의 사용은 필수적이다. JPQL에는 경로 표현식이라는 것이 존재하는데, 경로 표현식이란 점(.)을 찍어 객체 그래프를 탐색하는 것이라고 설명할 수 있다.
SELECT M.name FROM Member M #상태필드
SELECT M.team FROM Member M #단일 값 연관 경로
SELECT T.memberList FROM Team T #컬렉션 값 연관 경로
위 3가지에 따라 결과가 달라지고, 내부적으로 동작하는게 달라지므로 꼭 주의해서 사용해야한다.
- 상태필드 : 단순하게 값을 저장하는 필드
- 연관필드 : 연관 관계를 위한 필드
- 단일 값 연관 필드 : @ManyToOne, @OneToOne 처럼 xxxToOne (x대일) 관계, 대상이 엔티티
- 컬렉션 값 연관 필드 : @ManyToMany, @OneToMany 처럼 xxxToMany(x대다) 관계, 대상이 컬렉션
경로 표현식 특징
※ 경로 표현식의 특징을 이해하면 JPQL에 따라 SQL이 변하는 것을 더 명확히 알 수 있으므로 잘 알아두자.
- 상태 필드(state field) - 경로 탐색의 끝으로, 더이상 탐색이 불가하다.
- 단일 값 연관 경로 - 묵시적 내부 조인이 발생하며 추가적인 경로 표현식으로 탐색이 가능하다.
- 컬렉션 값 연관 경로 - 묵시적 내부 조인이 발생하며, 더이상 탐색이 불가하다.
- But, From 절에서 명시적 조인을 통해 별칭(alias)을 얻으면 별칭을 통해 탐색이 가능하다.
※ 되도록 '묵시적 내부 조인'이 되도록 JPQL을 작성하지 말 것. 묵시적 내부 조인이 발생하면 의도치 않은 조인 쿼리가 나갈 수 있으며 발견하기 어렵다. 이는 곧 성능과도 연관이 있어 되도록 가독성 있게 '명시적 조인'을 사용한다.
(JPQL == SQL 처럼 짜도록 한다.)
상태 필드 경로 탐색
List<Integer> members = em.createQuery("select m.age from Member m", Integer.class)
.getResultList();
* Hibernate
select m.age from Member m
* SQL
select member0_.age as col_0_0_ from Member member0_
- 상태 필드는 추가적인 조인이 없다.
단일 값 연관 경로 탐색
List<Team> teamList = em.createQuery("select m.team from Member m", Team.class).getResultList();
* Hibernate
select m.team from Member m
* SQL
select
team1_.team_id as team_id1_3_, team1_.name as name2_3_
from
Member member0_
inner join
Team team1_ on member0_.team_id=team1_.team_id
- 단일 값 연관 경로는 탐색 시 묵시적 내부 조인이 발생한다.
컬렉션 값 연관 경로 탐색
List<Collection> resultList = em.createQuery("select t.members from Team t", Collection.class)
.getResultList();
/* 컬렉션 값을 조회 시, TypedQuery 제네릭을 Collection으로 지정해야한다. */
* Hibernate
select t.members from Team t
* SQL
select
members1_.member_id as member_i1_0_, members1_.age as age2_0_, members1_.team_id as team_id4_0_, members1_.username as username3_0_
from
Team team0_
inner join
Member members1_ on team0_.team_id=members1_.team_id
- 컬렉션 값 연관 경로는 탐색 시 묵시적 내부 조인이 발생하며 값을 받는 클래스 타입을 'Collection'으로 지정해야한다.
※ 묵시적 조인 시 주의사항
- 항상 내부조인이 일어난다.
- 컬렉션은 경로 탐색의 끝으로, 더이상 점을 찍어 탐색이 불가하며, alias를 얻어서 사용하면 가능하다.
- 경로탐색은 주로 Select, Where 절에서 사용되나, 묵시적 조인으로 인해 SQL의 From (Join)에 영향을 준다.
페치 조인
※ 실무에서 매우 중요한 조인이며, SQL이 지원하는 Join의 종류가 아니다. JPQL에서 지원하는 기능으로 '성능 최적화'를 위해 사용한다. (SQL이 2번 나갈 것을 1번만 나가게 할 수 있다.)
※ 연관된 엔티티나 컬렉션을 SQL로 한번에 조회하는 기능이다.
※ 사용법 : SELECT M FROM Member M JOIN FETCH M.team
( [ LEFT [OUTER] | INNER ] JOIN FETCH )
설명을 보면 마치 fetch 전략 중에 EAGER(즉시 로딩) 과 같다. 실제로 동작은 즉시 로딩처럼 동작한다.
< --- 참고 --- >
페치조인과 일반조인의 차이...
Member : Team = N : 1(다대일) 관계일 때, fetch 전략을 EAGER (즉시 로딩)으로 설정했다 하자.
1. Member member = em.find(Member.class, 7L);
member.getTeam().getName();
(1) SQL
select
member0_.member_id as member_i1_0_0_, member0_.age as age2_0_0_, member0_.team_id as
team_id4_0_0_, member0_.username as username3_0_0_, team1_.team_id as
team_id1_3_1_, team1_.name as name2_3_1_
from
Member member0_
left outer join Team team1_
on member0_.team_id=team1_.team_id where member0_.member_id=?
-> 즉시 로딩으로 설정 했기에 em.find() 시 Member에 연관된 Team까지 땡겨와 영속성 컨텍스트에 저장한다.
2. Member member = em.createQuery("select m from Member m join m.team where m.id = :id", Member.class)
.setParameter("id", 7L)
.getSingleResult();
member.getTeam().getName();
(1) Hibernate
select m from Member m join m.team where m.id = :id
(2) SQL
select
member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
Member member0_
inner join
Team team1_ on member0_.team_id=team1_.team_id
where
member0_.member_id=?
<------------------------->
select
team0_.team_id as team_id1_3_0_, team0_.name as name2_3_0_
from
Team team0_
where
team0_.team_id=?
Q. 분명히 JPQL로 명시적 조인을 써서 Member와 Team을 한번에 끌어왔다. 그리고 Member와 Team은 즉시로딩으로 설정되어있다. 하지만 왜 SQL이 2번 출력되는가?
A. JPQL은 'query에 쓴대로만 나간다' 즉, 위에서 Member만 select하고 Team은 select 하지 않았기 때문에 Member엔티티만 값이 채워져 있고 Team 엔티티는 빈 깡통인 상태이다. 따라서 team을 구하고, 실제로 값을 쓸 때 DB에 select 쿼리를 날려 Team 엔티티를 가져오는 과정을 거친다. (쿼리가 2번 나간다 --> N + 1문제)
Solution>
JPQL을 다음과 같이 작성한다.
1) select m, t from Member m join m.team t where m.id = :id (명시적 조인)
2) select m from Member m join fetch m.team where m.id = :id (페치 조인)
결과>
- 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다.
- 페치 조인 실행시 연관된 엔티티를 함께 조회한다.
페치조인 사용하는 이유
위에서 언급한 것으로 충분한 설명이 됬으리라 생각되지만, 이유를 더 들어보자면 다음과 같다.
- 대부분 엔티티 간의 연관관계는 지연로딩 (LAZY)로 설정이 되어있다. 하지만 상황에 따라 연관된 엔티티를 한번에 함께 끌어와야할 상황이 존재할 수 있고, 그럴 때 Fetch Join을 사용해서 즉시 로딩 (EAGER)처럼 동작하게 한다.
컬렉션 페치 조인
※ 컬렉션 페치 조인은 일대다 관계에서 사용할 수 있으며, 데이터가 증폭될 수 있다. (데이터 뻥튀기)
team_id | team_name | member_id | member_name |
2 | team_b | 1 | user_a |
3 | team_c | 2 | user_b |
3 | team_c | 3 | user_c |
이처럼 team과 member 는 일대다 관계이고, team을 fetch join할 때, 3번 팀은 2명의 멤버를 갖고 있는데, 이들을 모두 가져와야하므로 어쩔 수 없이 3번 팀의 행(Row)가 2개가 출력이 된다. 이는 우리가 원했던 형태의 결과가 아닐 것이다.
그리고, 위의 결과에서 3번 팀의 행이 2개가 출력되었는데, 사실 저 2개의 엔티티는 동일하다. 따라서 DISTINCT로 중복제거가 가능하다.
SQL의 DISTINCT
- Row가 '완벽하게' 일치해야 중복 제거
JPQL의 DISTINCT
- SQL의 DISTINCT 기능에 추가하여, 동일한 엔티티면 중복 제거 (Application Level에서 엔티티 중복을 제거한다)
- 즉, 같은 식별자를 가진 Entity는 중복제거가 가능하다.
페치 조인의 특징과 한계
- 페치 조인의 대상에는 '원칙상' 별칭을 줄 수 없다.
- Hibernate는 가능하지만, 가급적 사용을 지양한다.
- 둘 이상의 컬렉션은 페치 조인을 할 수 없다
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다. (가장 큰 맹점이다)
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치조인해도 페이징이 가능하다. (데이터 증폭이 안되기 때문)
- Hibernate는 경고로그를 남기며, 메모리에서 페이징을 한다.
- 일단 전체 데이터를 가져와 메모리에 올려놓고, 메모리에서 N개씩 데이터를 결과로 보여준다. 자칫 OutOfMemory가 발생할 수 있고, 성능에 지대한 영향을 준다.
※ Solution
1) @BatchSize(100)
- (N+1) 문제를 (테이블 수 + 1)로 줄인다. SQL의 IN 쿼리를 사용한다. 이를 사용하면 한번에 100개의 데이터를 가져온다.
2) JPQL에서 DTO를 조회한다. ("select new com.gorany.dto.TestDTO(...) from Test t...")
마치며
- 모든 것을 페치 조인으로 해결할 수는 없다. 추후 QueryDSL을 사용하는 것이 바람직한 경우가 많다.
- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
- 여러 테이블을 조인해 엔티티가 가진 모양이 아닌 다른 결과를 내야한다면, 페치 조인보다 일반 조인이 효과적이고, 필요한 데이터만 조회해 DTO로 반환하는 것이 좋다.
- fetch join으로 Entity를 조회
- Entity를 조회해 DTO로
- JPQL에서 바로 DTO를 조회
# References
인프런 - 김영한님의 JPA 기본편 강의 중 경로 표현식과 페치 조인
'Framework > JPA (Hibernate)' 카테고리의 다른 글
[JPA] OSIV (Open Session In View) (2) | 2021.09.19 |
---|---|
[JPA] 벌크 연산 (Bulk Operation) (0) | 2021.08.05 |
[JPA] Cascade / OrphanRemoval (0) | 2021.07.25 |
[JPA] Proxy (0) | 2021.07.23 |
[JPA] 상속관계 매핑 (0) | 2021.07.23 |