네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

Fetch Join 


JPQL(Java Persistence Query Language)에는 경로 표현식이 있다.

경로 표현식이란 점(.)으로 객체 그래프를 탐색하는 것이다.

SELECT m.name FROM Member m  // 상태 필드
SELECT m.team FROM Member t  // 단일 값 연관 경로
SELECT t.members FROM Team t  // 컬렉션 값 연관 경로

* 상태 필드(state field): 단순하게 값을 저장하는 필드

* 연관 필드: 연관 관계를 위한 필드

    - 단일 값 연관 필드: @ManyToOne, @OneToOne 처럼 xxxToOne 관계, 대상이 엔티티

    - 컬렉션 값 연관 필드: @ManyToMany, @OneToMany 처럼 xxxToMany 관계, 대상이 컬렉션

 

[경로 표현식 특징]

* 상태 필드: 경로 탐색의 끝으로, 더이상 탐색이 불가능하다.

* 단일 값 연관 경로: 묵시적 내부 조인이 발생하며 추가적인 경로 표현식으로 탐색이 가능

* 컬렉션 값 연관 경로: 묵시적 내부 조인이 발생하며 더이상 탐색이 불가하다.

    - From 절에서 명시적 조인을 통해 별칭(alias)을 얻으면 별칭을 통해 탐색이 가능하다.

 

** 되도록 묵시적 내부 조인이 되도록 JPQL을 작성하지 않도록 JPQL을 작성하지 말 것.

묵시적 내부 조인이 발생하면 의도치 않은 조인 쿼리가 발생할 수 있으며 발견하기 어렵다. 성능과 연관이 있을 수 있기 때문에 명시적 조인을 사용한다.

 

[상태 필드 경로 탐색]

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> teams = 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

 

- 단일 값 연관 경로는 탐색 시 묵시적 내부 조인이 발생한다.

 

[컬렉션 값 연관 경로 탐색]

// 컬렉션 값을 조회 시, TypedQuery 제네릭을 Collection으로 지정해야 한다.
List<Collection> resultList = em.createQuery("SELECT t.members FROM Team t", Collection.class)
                                .getResultList();

* 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)에 영향을 준다.

 

 


Fetch Join (페치 조인)

JPQL에서 지원하는 기능으로 '성능 최적화'를 위해 사용한다.

연관된 엔티티나 컬렉션을 SQL로 한번에 조회하는 기능이다.

JOIN FETCH 명령어를 사용한다.

SELECT m FROM Member m JOIN FETCH m.team

 

- 즉시 로딩처럼 동작한다.

 

[페치 조인과 일반 조인 차이점]

 

 가정  Member:Team = N:1 다대일 관계일 때, fetch 전략을 EAGER (즉시 로딩)으로 설정했다

Member member = em.find(Member.class, 5L);
membe.getTeam().getName();

* 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까지 조회해서 영속성 컨텍스트에 저장한다.

 

 문제  일반 JOIN

Member member = em.createQuery("SELECT m FROM Member m JOIN m.team WHERE m.id = :id", Member.class)
                  .setParameter("id", 5L)
                  .getSingleResult();
                  
member.getTeam().getName();

* Hibernate

SELECT m FROM Member m JOIN m.team WHERE m.id = :id

 

* 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_

left outer 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 = ?

 

JPQL로 명시적 조인을 사용하여 Member 와 Team을 한번에 가져왔다. Member와 Team은 즉시로딩으로 설정되어 있다.

하지만, SQL이 두 번 출력되는 문제가 발생한다.

 

JPQL에서 Member만 select 하고 Team은 select 하지 않았기 때문에 Member 엔티티에만 값이 채워져 있고 Team 엔티티는 비어있는 상태다.

team을 구하고 실제로 값을 사용할 때(위 코드에서 member.getTeam().getName()에서)

select 쿼리로 Team 엔티티를 가져온다.

=> 쿼리가 2번이 나간다 => N+1 문제가 발생한다.

 

 해결  페치 조인 사용 또는 명시적 조인

페치 조인

SELECT m from Member m JOIN FETCH m.team WHERE m.id = :id

명시적 조인

SELECT m, t FROM Member m JOIN m.team t WHERE m.id = :id

 

 결론 

일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않는다.

페치 조인 실행 시 연관된 엔티티를 함께 조회한다.

 

[페치 조인을 사용하는 이유]

- 대부분 엔티티 간의 연관관계는 지연로딩(LAZY)로 되어있다. 하지만 상황에 따라 연관된 엔티티를 한번에 함께 끌어외야 할 상황이 존재하기 때문에, 그럴 때 Fetch Join을 사용하여 즉시 로딩(EAGER)처럼 동작하게 한다. (*LAZY를 적용해도 페치 조인이 우선시 된다.)

 

[컬렉션 페치 조인]

- 컬렉션 페치 조인은 일대다 관계에서 사용할 수 있으며 데이터가 많아질 수 있다.

=> DISTINCT로 중복 제거가 가능하다.

 

* SQL의 DISTINCT

- ROW가 완벽히 일치해야 중복 제거

* JPQL의 DISTINCT

- SQL의 DISTINCT 기능 뿐만 아니라, 동일한 엔티티면 중복 제거(Application단에서 엔티티 중복을 제거한다.)

즉, 같은 식별자를 가진 Entity는 중복 제거가 가능하다.

 

[페치 조인의 특징과 한계]

 문제 

- 페치 조인의 대상에는 원칙상 별칭을 줄 수 없다.

    * Hibernate는 가능하지만, 가급적 사용을 지양한다.

- 둘 이상의 컬렉션은 페치 조인을 할 수 없다.

- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.

    * 일대일, 다대일같은 단일 값 연관 필드들은 페치 조인을 해도 페이징이 가능하다. (데이터 증복이 없음)

    * Hibernate는 경고로그를 남기며 메모리에서 페이징을 한다.

      일단, 전체 데이터를 가져와 메모리에 올려놓고, 메모리에서 N개씩 데이터를 결과로 보여준다. 자칫 OutOfMemory가 발생할 수 있고, 성능에 영향을 준다.

 

 해결 

1) @BatchSize

N+1 문제를 테이블 수 + 1로 줄인다. SQL의 IN 쿼리를 사용한다.

2) JPQL에서 DTO로 조회한다. SELECT new com.jiyoon.dto.TestDTO(...) from Test t ...

 

 결론 

- 모든 것을 페치 조인으로 해결할 수는 없다. QueryDSL을 사용하는 것이 바람직한 경우가 많다.

- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.

- 여러 테이블을 조인해 엔티티가 가진 모양이 다른 결과를 내야한다면, 페치 조인보다 일반 조인이 효과적이고, 필요한 데이터만 조회해 DTO로 반환하는 것이 좋다.

    * FETCH JOIN으로 엔티티 조회

    * Entity를 조회해 DTO로 변환

    * JPQL에서 바로 DTO를 조회

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !