본문 바로가기
Study/JPA

객체지향 쿼리 언어) JPQL - 페치 조인(fetch join)

by _비니_ 2024. 7. 30.

📌 JPQL - 페치 조인(fetch join) 🌟🌟🌟

 

🌟🌟🌟실무에서 정말 정말 정말 중요 !!! 🌟🌟🌟

  • SQL 조인 종류X
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • join fetch 명령어 사용해 [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로를 나타냄

 

📌 엔티티 페치 조인

  • 회원을 조회하면서 연관된 팀도 함께 조회(SQL 쿼리 한 번에 가능)
  • SQL을 보면 회원 뿐만 아니라 팀(T.)도 함께 SELECT

[JPQL]

select m from Member m join fetch m.team

 

>> m만 select 함

 

 

[SQL]

SELECT M., T.* FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID

>> SQL문에서는 Member와 Team 둘 다 select됨.

 

MEMBER 테이블과 TEAM 테이블이 ID와 TEAM_ID를 통해 연결되어 있음. FK(Foreign Key) -> PK(Primary Key) 관계를 통해 데이터가 연결됨

 

Inner Join 결과
페치 조인(fetch join)을 통해 얻은 데이터

 

try {
            
    Team teamA = new Team();
    teamA.setName("팀A");
    em.persist(teamA);

    Team teamB = new Team();
    teamB.setName("팀B");
    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> result = em.createQuery(query, Member.class)
        .getResultList();

for(Member member : result) {
    System.out.println("member = " + member.getUsername + ", " + member.getTeam().getName());
}

 

 

현재 Member와 Team의 연관관계

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
@OneToMany
private List<Member> members = new ArrayList<>();

>> fetch = FetchType.LAZY 이므로 실제 member.getTeam().getName() 를 호출할 때 DB에 쿼리를 날림 >> 영속성 컨텍스트에 조회함.

 

  • 회원1과 회원2는 팀A 소속
  • 팀A는 영속성 컨텍스트에 없기에 SQL 쿼리로 가져옴
    • 위 결과에서 SQL 문이 나간 것을 확인할 수 있음
  • 회원2를 호출할 시점에는 팀A가 영속성 컨텍스트(1차 캐시)에 있으므로 캐시에서 가져옴
    • 즉 위 결과에서 회원 2는 SQL 쿼리가 나가지 않고 바로 결과가 조회되는 것을 확인할 수 있음

 

  • 회원3은 팀B 소속.
  • 팀B는 영속성 컨텍스트에 없기에 SQL 쿼리가 나감.

 

⇒ 연관된 데이터를 한 번에 불러와 성능을 개선하지만, 사용 시 주의가 필요

⇒ 위에서 쿼리가 2번 나감 !

⇒ 만약 회원이 100명이라면 ..? ⇒ N+1문제 🚨

 

🚨 N+1 문제

만약 회원(Member) 100명을 가져오고, 각각의 회원이 속한 팀(Team) 정보를 조회해야 한다면, 기본적으로 100번의 추가 쿼리가 발생할 수 있음.

이를 N+1 문제라고 하며, 성능 저하의 원인이 됨 !!!

 

💡  해결 방법

  • Fetch Join을 사용하여 연관된 엔터티를 한 번에 가져오도록 해야 함
String query = "select m From Member m join fetch m.team";

 

  • member와 team이 한 번에 조인
  • 이 때 member.getTeam().getName() 여기에서의 Team은 프록시가 아님!!🚨
    • member와 team의 데이터를 join해서 이미 다 들고 왔기 때문.
    • result에 담기는 시점에 team은 프록시가 아닌 실제 엔티티!!! ‘

지연 로딩 설정이 되어있어도 Fetch Join이 우선

 

 

📌 컬렉션 페치 조인

  • 일대다 관계나 컬렉션 페치 조인일 때

[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'

 

 

 

String query = "select t From Team t join fetch t.members";

List<Team> result = em.createQuery(query, Team.class)
        .getResultList();

for(Team team : result) {
    System.out.println("team = " + team.getName + "|" + team.getMembers().size();
}

  • 🚨 주의: 팀 A가 중복으로 출력되고 있음
    • JPA에서 일대다 조인을 할 때, 다수의 멤버와 팀이 조인되면서 중복된 데이터가 생성될 수 있기 때문 !!!
    • DB 입장에서는 일대다 조인을 할 때 데이터가 뻥튀기 됨 !!

 

  • 팀 A에 회원이 2명이면, 위에와 같이 두 개의 행으로 나타나짐
  • JPA 입장에서는 2개의 행으로 나타나졌는지 모름

 

  • 팀A 둘 다 ID가 1로 영속성 컨텍스트에 올리고 그것을 똑같이 사용
  • ⇒ 컬렉션에는 같은 주소값을 가진 결과가 2개가 나옴
  • fetch join을 했기에 teamA 입장에서는 회원1, 회원2를 가짐

JPA는 데이터베이스에서 결과가 나온 수만큼 컬렉션의 개수를 돌려주도록 설계가 돼있음.

⇒ 수를 줄일지 말지는 사용자한테 맡김

 

 

📌 페치 조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령
  • JPQL의 DISTINCT 2가지 기능 제공
    1. SQL에 DISTINCT를 추가하여 DB에 쿼리를 보내 중복을 제거.
    2. 애플리케이션에서 엔티티 중복 제거
      • DB가 아닌 SQL에 DISTINCT 추가해서 DB에 쿼리를 날린 후 그 결과가 애플리케이션에 올라왔을 때 중복된 것이 있으면 제거함.
String query = "select t From Team t join fetch t.members";
    •  

여기에서 join fetch t.members 가 없다면 ?? size ⇒ 2

있으면 join하면서 데이터가 뻥튀기 되므로 size ⇒ 3

 

 

위와 같은 중복을 제거하고 싶다면?? ⇒ DISTINCT

select distinct t
from Team t join fetch t.members 
where t.name = ‘팀A’
  • 위 쿼리는 Team과 연관된 Member를 한 번에 가져오되, Team이 중복되지 않게함

🚨 주의: SQL에서 DISTINCT를 사용해도 데이터가 다른 경우 SQL 결과에서 중복 제거가 실패할 수 있음. 테이블의 모든 필드가 완전히 동일해야만 DISTINCT가 제대로 작동하기 때문 !!

  • SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과 에서 중복제거 실패
  • (ID, NAME, ID, TEAM_ID, NAME가 완전히 동일해야 DISTINCT이 됨🌟)

 

  • DISTINCT가 추가로 애플리케이션에서 중복 제거시도
  • 🌟같은 식별자를 가진 Team 엔티티 제거🌟

 

[DISTINCT 추가시 결과]

teamname = 팀A, team = Team@0x100

-> username = 회원1, member = Member@0x200

-> username = 회원2, member = Member@0x300

 

일대다 ⇒ 뻥튀기가 됨. / 다대일 ⇒ 뻥튀기 안됨

 

하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용됩니다.

 

 

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

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

[JPQL]

select t
from Team t join t.members m
where t.name = ‘팀A'

Team과 연관된 Member를 조인하나, 실제로는 Team만 조회

 

[SQL]

SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

일반 조인은 연관된 엔터티를 조회하지 않고, 해당 엔터티만 조회

 

 

 

📌 일반 조인

  • 조인을 해서 모두 가져오지만 SELECT는 team만 조회
  • 회원 엔티티는 조회X

 

 

📌 페치 조인

 

[JPQL]

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

Team과 연관된 Member를 한 번에 조회하여 엔터티 그래프를 로드

 

 

[SQL]

SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = '팀A'
  • 지연로딩 같은 거 없음
  • Fetch Join은 일반 조인과 달리 연관된 엔터티도 함께 조회하는 기능을 제공 ⇒ 사실상 즉시로딩이 사용되는 것
  • 이 과정에서 발생하는 데이터 중복 문제를 해결하기 위해 DISTINCT를 사용할 수 있음
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

반응형

'Study > JPA' 카테고리의 다른 글

객체지향 쿼리 언어) Named 쿼리, 벌크 연산  (0) 2024.07.30
객체지향 쿼리 언어) JPQL - 경로 표현식  (0) 2024.07.30
JPQL  (0) 2024.07.27
조인과 서브쿼리  (0) 2024.07.27
프로젝션과 페이징  (0) 2024.07.27