📌 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됨.
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가지 기능 제공
- SQL에 DISTINCT를 추가하여 DB에 쿼리를 보내 중복을 제거.
- 애플리케이션에서 엔티티 중복 제거
- 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 |