반응형
05-15 00:00
Today
Total
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
관리 메뉴

개발하는 고라니

[JPA] 페치 조인 (FETCH JOIN) 본문

Framework/JPA (Hibernate)

[JPA] 페치 조인 (FETCH JOIN)

조용한고라니 2021. 8. 5. 15:17
반응형

진행하기 앞서..

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

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 본 강의는 자바 백엔

www.inflearn.com

인프런 - 김영한님의 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
Comments