반응형
페치 조인(Fetch Join)
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회한다.
- 페치 조인은 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면 페치 조인 보다는 일반 조인을 사용해서 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
- 페치 조인을 사용하면 명시적으로 즉시로딩할 수 있다.
- 지연로딩 설정이 되어있어도 페치 조인이 우선으로 적용된다.
N+1
문제를 해결할 수 있다.
- 페치 조인 문법
[LEFT [OUTER] | INNER] JOIN FETCH 조인경로
- 예) N:1 페치조인. 회원을 조회하면서 연관된 팀도 함께 조회(회원과 팀은 N : 1 관계)
//JPQL SELECT m FROM Member m JOIN FETCH m.team //실제 실행되는 SQL SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID
- 예) 1 : N 관계, 컬렉션 페치 조인
//JPQL SELECT t FROM Team t JOIN FETCH t.members //실제 실행되는 SQL SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
- 팀과 회원(1 : N) 관계에서는 중복된 데이터가 발생한다.
- 회원1, 회원2 모두 팀A에 소속되어있는 경우, 조인 결과로 팀A가 여러 행으로 나타난다.
- 팀A 중복 데이터 발생
- 조회 결과인 팀A는 1차 캐시에 저장되고 Team 인스턴스들은 이를 참조한다.
- JPA는 동일한 객체에 대해서 유일성을 보장한다. 따라서 팀A의 인스턴스가 여러 개 있어도 이는 동일한 주소를 가진다.
- 팀A는 자신에게 소속된 회원 정보를 가지고 있다.
- 회원1, 회원2 모두 팀A에 소속되어있는 경우, 조인 결과로 팀A가 여러 행으로 나타난다.
- 중복된 데이터는 DISTINCT 명령어를 통해서 제거할 수 있다.
- SQL의 DISTINCT는 모든 컬럼의 데이터가 완전히 동일해야 중복으로 인정하기 때문에 중복된 데이터를 제거하는데 한계가 있다.
- 이를 극복하고자 JPQL의 DISTINCT는 2가지 기능을 제공한다.
SQL에 DISTINCT를 추가하는 기능
애플리케이션 레벨에서 엔티티의 중복을 제거하는 기능
- 같은 식별자를 가진 엔티티를 제거한다.
- 팀과 회원(1 : N) 관계에서는 중복된 데이터가 발생한다.



페치 조인과 일반 조인의 차이
- JPQL은 결과를 반환할 때 연관관계를 고려하지 않는다.
- SELECT 절에 지정한 엔티티만 조회한다.
- 페치 조인 사용 시, 연관된 엔티티를 함께 조회한다.(즉시로딩)
- 연관된 엔티티에 대한 정보를 얻어오는 동작을 수행해도 조회 SQL이 발생되지 않는다.
- 일반 조인 사용 시, 연관된 엔티티를 함께 조회하지 않는다.
- 연관된 엔티티에 대한 정보를 얻어오는 동작을 수행 시, 조회 SQL 발생된다.
//[페치 조인] Team 엔티티와 Member 엔티티 모두 조회한다.
//JPQL
SELECT t FROM Team t JOIN FETCH t.members
//실제 실행되는 SQL
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M
ON T.ID = M.TEAM_ID
//[일반 조인] Team 엔티티만 조회한다.
//JPQL
SELECT t FROM Team t JOIN t.members
//실제 실행되는 SQL
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M
ON T.ID = M.TEAM_ID
페치 조인의 특징과 한계
페치 조인 대상에는 별칭을 줄 수 없다.
- 하이버네이트는 가능하지만 페치 조인을 여러 개 사용하는 경우를 제외하고는 별칭은 사용하지 않는 것이 좋다.
- 별칭을 통해서 특정 조건의 데이터만 가져와서 조작하는 것은 위험하다.
- 특정 조건의 데이터만 조작하는 경우에도 해당되지 않는 나머지 데이터들이 변경되거나 제거될 수 있다. 데이터 정합성 문제 발생
- JPA의 객체 그래프 탐색의 의도는 연관된 모든 데이터를 가지고 오는 것이다.
- 특정 조건의 데이터만 필요하다면 별도의 SQL 조회를 하는 것이 좋다.
- 하이버네이트는 가능하지만 페치 조인을 여러 개 사용하는 경우를 제외하고는 별칭은 사용하지 않는 것이 좋다.
페치 조인에서 컬렉션은 하나만 지정할 수 있다. 둘 이상의 컬렉션은 페치 조인 할 수 없다.
- 둘 이상의 컬렉션이 사용된다면 1 : N : N 관계가 되어서 중복된 데이터가 매우 많이 발생한다.
컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
1 : 1
,N : 1
같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다.- 중복 데이터가 발생하지 않는다.
1 : N
과 같은 경우, 메모리에서 페이징하는데 이는 매우 위험하다.- 메모리를 모두 사용하여 서버 장애 발생
1 : N의 경우 페이징 방법
N : 1
관계로 SQL을 뒤집어 작성하여 페이징 처리
- 페치 조인을 사용하지 않고 필요한 엔티티를 조인한 후,
@BatchSize
를 이용해서N+1 문제
를 방지하고 페이징 처리@BatchSize
는 지연 로딩 시, 연관된 엔티티를 배치사이즈 만큼 IN 쿼리가 생성되어 함께 조회한다.- IN 쿼리를 통해서 회원정보도 함께 가져오기 떄문에 추가적인 쿼리가 발생하지 않는다.
N+1
개의 쿼리가 아니라 사이즈만큼 쿼리 수를 맞출 수 있다.
- 배치사이즈는 전역 설정으로 설정할 수도 있고 필요한 부분에만 애노테이션으로 설정할 수도 있다.
- 애노테이션 설정
@BatchSize(size = 100)
- 전역 설정
- 1000 이하의 값으로 적절하게 설정
<property name="default_batch_fetch_size" value="100" />
- 애노테이션 설정
- 예) 페이징
@Entity public class Team { @Id @GeneratedValue private Long id; private String name; @BatchSize(size = 100) @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>(); } //1 : N 관계로 하이버네이트에서 경고 발생 //WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory! List<Team> result = em.createQuery("select t from Team t join fetch t.members", Team.class) .setFirstResult(0) .setMaxResults(3) .getResultList(); //실행된 SQL select team0_.id as id1_9_0_, members1_.id as id1_6_1_, team0_.name as name2_9_0_, members1_.age as age2_6_1_, members1_.USERNAME as USERNAME6_6_1_, members1_.TEAM_ID as TEAM_ID10_6_1_, members1_.TEAM_ID as TEAM_ID10_6_0__, members1_.id as id1_6_0__ from Team team0_ inner join Member members1_ on team0_.id=members1_.TEAM_ID //N : 1 관계로 SQL 작성하여 페이징 처리 List<Member> result = em.createQuery("select m from Member m join fetch m.team", Member.class) .setFirstResult(0) .setMaxResults(3) .getResultList(); //실행된 SQL select member0_.id as id1_6_0_, team1_.id as id1_9_1_, member0_.age as age2_6_0_, member0_.USERNAME as USERNAME6_6_0_, member0_.TEAM_ID as TEAM_ID10_6_0_, team1_.name as name2_9_1_ from Member member0_ inner join Team team1_ on member0_.TEAM_ID=team1_.id limit ? //@BatchSize를 이용해서 페이징 처리 List<Team> result = em.createQuery("select t from Team t", Team.class) .setFirstResult(0) .setMaxResults(3) // 3건 IN 쿼리 발생. (130건 까지 가져온다면 100건 IN 쿼리 발생.) .getResultList(); //실행된 SQL select members0_.TEAM_ID as TEAM_ID10_6_1_, members0_.id as id1_6_1_, members0_.id as id1_6_0_, members0_.age as age2_6_0_, members0_.USERNAME as USERNAME6_6_0_, members0_.TEAM_ID as TEAM_ID10_6_0_, from Member members0_ where members0_.TEAM_ID in ( // 3건 IN 쿼리 발생 ?, ?, ? )
[참고자료]
반응형
'Java > ORM' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 값 타입 (0) | 2022.04.24 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 영속성 전이 (0) | 2022.04.19 |
[자바 ORM 표준 JPA 프로그래밍] 프록시와 연관관계 관리 (0) | 2022.04.15 |
[자바 ORM 표준 JPA 프로그래밍] 상속관계 매핑 (0) | 2022.04.06 |
[자바 ORM 표준 JPA 프로그래밍] 연관관계 매핑 (0) | 2022.04.03 |