N+1을 공부하다보니 Fetch Join을 알게 되었는데, 어딘가 들어본 듯한 용어인 듯 머리 속을 찾아보니 아무것도 나오지 않았습니다..
N+1을 공부하며 파생적으로 알아야 하는 주제들이 많은 것 같습니다 ^^*
각설하고, 일반 Join 과 Fetch Join 에 대해 알아보도록 하겠습니다!
JPA는 2가지 Fetch 전략이 존재하는데 일반적으로 지연 로딩 전략(Lazy)를 사용합니다.
하지만 이러한 전략으로 인해 N+1 문제가 발생하는 것은 아닙니다.
Fetch Join은 처음 Entity를 조회할 때 Join하여 모든 데이터를 가져올 수 있도록 등장하게 되었습니다.
오잉? 그럼 그냥 일반 JOIN 사용하면 되는 것 아닌가요?
이제부터 차이를 설명해드리겠습니다!
먼저 두 개념의 차이점을 간략하게 설명 후 예시를 통해 심층적으로 알아보도록 하겠습니다.
기본적인 JOIN 자체 개념은 안다고 가정하겠습니다!
글로 주루룩 적어 놓으니 무슨 말인지 잘 와닿지 않습니다,,
핵심은 Fetch 여부에 따라 Join 시 연관 Entity의 영속화 여부가 결정되는 것 같습니다.
예시를 통해 좀 더 알아볼까요?
테스트할 Entity들의 구조는 아래와 같습니다
Team
```java
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private List<Member> members = new ArrayList<>();
public void addMember(Member member){
member.setTeam(this);
members.add(member);
}
}
```
Member
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
public String name;
public int age;
@ManyToOne(fetch = FetchType.LAZY)
public Team team;
public Member(String name, int age, Team team) {
this.name = name;
this.age = age;
this.team = team;
}
public setTeam(Team team) {
this.team = team;
}
}
저는 Member와의 Join을 통해 모든 Team을 조회하려 합니다.
//TeamRepository.java
@Query("SELECT distinct t FROM Team AS t join t.members")
public List<Team> findAllWithMemberUsingJoin();
따라서 위와 같은 메소드를 실행하게 되면
이러한 결과가 떨어집니다.
일반적인 개념의 Join이 이루어진 형태의 쿼리 결과입니다 (일반 Join을 사용했으니,,)
주의 깊게 봐야할 부분은 SELECT 후 가져오는 컬럼들입니다.
Team의 컬럼인 id 와 name만 가져오고 있습니다.
더 자세히 파악하기 위해 결과를 출력해보도록 하겠습니다.
//TeamService.java
public List<Team> findAllTeam() {
return teamRepository.findAllWithMemberUsingJoin();
}
//Test.java
@Test
public void joinTest() {
List<Team> teamUsingJoin = teamService.findAllTeam();
System.out.println(teamUsingJoin);
}
쿼리는 동일하게 실행되었지만, 갑자기 LazyInitializationException 이 발생합니다 ???
LazyInitializationException은 Session(Transaction)없이 Lazy Entity를 사용하는 경우가 주된 원인입니다
즉, Join 후 Team의 Lazy Entity인 members가 초기화 되지 않은 상태이기에 위와 같은 예외가 발생합니다.
일반 Join은 실제 쿼리에 Join을 수행하지만, Join 대상(연관 Entity)에 대한 영속성까지는 관여하지 않는다는 걸 알 수 있습니다.
Join만 수행 한 뒤 영속성 컨텍스트에는 SELECT 대상만 담아두는 것이죠.
정리해보자면,
1. 일반 Join으로 Team Entity 초기화 완료
2. 하지만 일반 Join은 연관 Entity까지 초기화하지 않기 때문에 Member는 초기화되지 않음
3. 아직 초기화되지 않은 members에 접근하면서 LazyInitializationException 발생
이해가 조금 되셨나요?
그렇다면 Fetch Join을 사용하면 무엇이 달라지는지 확인해보겠습니다.
//TeamRepository.java
@Query("SELECT distinct t FROM Team AS t join fetch t.members")
public List<Team> findAllWithMemberUsingFetchJoin();
위와 같은 메소드를 실행하게 되면
이러한 쿼리 결과가 떨어집니다.
일반 Join을 수행하는 경우와 큰 틀에선 차이가 없어보이지만, 마찬가지로 SELECT 후 가져오는 컬럼들을 주의깊게 봐야합니다.
일반 Join과 달리 Fetch Join 하는 연관 Entity의 컬럼 또한 가져오고 있는 것을 알 수 있습니다.
더 자세히 확인하기 위해 이번에도 결과를 출력해보도록 하겠습니다.
// TeamService.java
public List<Team> findAllTeam(){
return teamRepository.findAllWithMemberUsingFetchJoin();
}
//Test.java
@Test
public void fetchJoinTest() {
List<Team> teamUsingFetchJoin = teamService.findAllWithMemberUsingFetchJoin();
System.out.println(teamUsingFetchJoin);
}
//출력 결과
[
Team(
id=1,
name=team1,
members=[
Member(
id=1,
name=team1member1,
age=1
),
Member(
id=2,
name=team2member2,
age=2
),
Member(
id=3,
name=team3member3,
age=3
)
]
),
Team(
id=2,
name=team2,
members=[
Member(
id=4,
name=team2member4,
age=4
),
Member(
id=5,
name=team2member5,
age=5
)
]
)
]
LazyInitializationException이 발생하지 않고, Team의 Members 까지 무사히 가져온 걸 확인 할 수 있습니다.
Fetch Join을 사용함으로써 연관 Entity 또한 영속성 컨텍스트에 올라갔기 때문에 SELECT 시 참조가 가능해졌습니다.
그렇다면 무조건 Fetch Join이 좋은 선택일까요?
JPA는 객체과 데이터베이스간의 일관성을 잘 고려하여 사용해야 하기에 로직에 꼭 필요한 Entity만 영속성 컨텍스트에 담아 사용하는 것이 좋습니다.
설명한 것과 같이 일반 Join의 경우는 연관 Entity를 영속성 컨텍스트에 올리지 않습니다.
따라서 쿼리 검색 조건에는 필요하지만, 조회하는 Entity의 정보만 필요한 경우라면 굳이 연관 Entity를 영속성 컨텍스트에 담지 않아도 됩니다. (위의 예시에서 member의 정보가 필요하지 않은 경우)
이러한 경우는 Fetch Join을 사용하기 보단 일반 Join을 사용하는 것이 더 효과적입니다.
모든 경우에 그렇지만, 무지성 사용보단 상황을 적절하게 판단 후 알맞는 방법을 사용하는 자세가 필요합니다.
Fetch Join 대상에는 별칭을 줄 수 없음
둘 이상의 컬렉션을 Fetch 할 수 없음
컬렉션은 Fetch Join 하면 페이징 API를 사용할 수 없음
마지막으로 다시 정리 해보겠습니다.
일반 JOIN
FETCH JOIN
차이점이 잘 이해 되셨나요?
잘 이해가 되지 않는다면 직접 본인만의 예시를 작성해보는 것도 좋을 것 같습니다 :)
읽어주셔서 감사합니다!
References