티스토리 뷰

Back-end/JPA

자바 ORM 표준 JPA fetch join

이안_ian 2023. 5. 29. 21:11




반응형

페치 조인

@Entity
public class Member {
  중략..
  
  @ManyToOne(fetch = FetchType.LAZY) //Member, Team 관계를 나타내는 다:1(N:1)
  @JoinColumn(name = "TEAM_ID")
  private Team team;
}

Member 조회 때 여러 연관관계가 걸릴 수 있는데 그냥 매핑을 할 경우 모든 테이블에 join이 걸리므로

성능이 떨어지는걸 막고자 연관관계 매핑 시 Lazy 로딩을 사용하게 된다.

그러면 Team을 바로 join하지 않고 프록시 상태로 갖고 있다가 값을 참조하는 순간에 select 쿼리가 발생된다.

 

그런데 아래와 같이 사용할 경우 최악의 성능이 생길 수 있다.

Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

em.flush();
em.clear();

String query = "select m from Member m";
List<Member> rs = em.createQuery(query, Member.class).getResultList();

for(Member member : rs){
  sout("member = " + member.getUsername()+", " + member.getTeam().getName());
}

이렇게 할 경우 team의 값은 프록시로 null인 상태다. 값 조회를 위해 그때 조회 쿼리를 날린다.

select * from team where team_id = ?

회원1일 때 teamA를 조회해서 가져올 것이고, 회원2일 땐 회원1에서 조회한 1차 캐시에서 데이터를 불러올 것이다.

하지만 회원3은 다른팀인 teamB를 가져와야 하기에 조회 쿼리가 새롭게 발생될 것이다.

 

만약 회원 100명을 조회하고 team이 모두 다르다면 team을 조회하는 쿼리가 100번 발생될 것이다.

이게 바로 N+1 문제다.

 

그래서 이럴 경우엔 fetch join을 사용해서 해당 문제를 해결한다.

//기본은 inner join
String query = "select m from member m join fetch m.team";

//outer join을 원하면 아래로
String query = "select m from member m left join fetch m.team";

아래의 쿼리로 실행된다.

select *
from membr m
inner team t
  on m.team_id = t.id

처음부터 team에 대한 정보는 다 들고오기 때문에 프록시 상태도 아니며 원하는 값을 바로 가져왔기에 위 문제가 발생되지 않는 것이다.

컬렉션 페치 조인과 distinct

일대다 조인에서는 데이터가 뻥튀기 될 수 있다.

Team에서 Member로 join을 걸 경우 가운데처럼 2개의 데이터가 생성되는데,

논리적으로 보면 팀A에 회원1, 회원2가 있다고 끝나야 하지만

fetch join의 결과 리스트는 2 row 전달 받아 팀A > 회원1,회원2 이라는 같은 데이터가 2개 들어가게 된다.

 

이럴 때 distinct을 사용하면 된다.

String query = "select distinct t from team join fetch t.members"

실제 쿼리에도 distinct가 붙어서 실행되지만 추가로 애플리케이션에서도 중복 제거를 시도한다.

같은 식별자를 가진 Team 엔티티 제거.

페치 조인과 일반 조인의 차이

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

단지 select 절에 지정한 엔티티만 조회할 뿐.

[JPQL]
select t
from Team t join t.members m
where t.name= '팀A'

[SQL]
select T.*
from Team T
inner join member m on T.id = m.team_id
where T.name = '팀A'

 

페치 조인을 사용할 때만 연관된 엔티티를 함께 조회함(즉시 로딩).

[JPQL]
select t
from Team t
join fetch t.members
where t.name = '팀A'

[SQL]
select t.*, m.*
from team t
inner join member m on t.id = m.team_id
where t.name = '팀A'

페치 조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
    하이버네이트는 가능, 하지만 가급적 사용X
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
    일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능. 일대다는 뻥튀기 때문에 불가능
  • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
    @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
  • 최적화가 필요한 곳은 페치 조인 적용

페치 조인 정리

  • 모든 것을 페치 조인으로 해결할 수는 없음
  • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야하면, 페치 조인보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적

 

@BatchSize

 

출처 : https://www.inflearn.com/course/ORM-JPA-Basic

 

반응형

'Back-end > JPA' 카테고리의 다른 글

자바 ORM 표준 JPA 프록시  (0) 2023.05.31
자바 ORM 표준 JPA 벌크 연산  (0) 2023.05.30
JPA @MappedSuperclass  (0) 2023.05.28
자바 ORM 표준 JPA 연관관계  (0) 2023.05.27
자바 ORM 표준 JPA DDL, 기본키 매핑  (0) 2023.05.25
댓글
반응형
최근에 달린 댓글
글 보관함
Total
Today
Yesterday
최근에 올라온 글
«   2024/04   »
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