일반 JOIN vs Fetch JOIN

일상 회고록·2024년 1월 8일
0
post-thumbnail
post-custom-banner

N+1을 공부하다보니 Fetch Join을 알게 되었는데, 어딘가 들어본 듯한 용어인 듯 머리 속을 찾아보니 아무것도 나오지 않았습니다..

N+1을 공부하며 파생적으로 알아야 하는 주제들이 많은 것 같습니다 ^^*

각설하고, 일반 Join Fetch Join 에 대해 알아보도록 하겠습니다!

1. FETCH JOIN

JPA는 2가지 Fetch 전략이 존재하는데 일반적으로 지연 로딩 전략(Lazy)를 사용합니다.

하지만 이러한 전략으로 인해 N+1 문제가 발생하는 것은 아닙니다.

Fetch Join은 처음 Entity를 조회할 때 Join하여 모든 데이터를 가져올 수 있도록 등장하게 되었습니다.

오잉? 그럼 그냥 일반 JOIN 사용하면 되는 것 아닌가요?

이제부터 차이를 설명해드리겠습니다!

2. 일반 JOIN 과 FETCH JOIN 차이점

먼저 두 개념의 차이점을 간략하게 설명 후 예시를 통해 심층적으로 알아보도록 하겠습니다.

기본적인 JOIN 자체 개념은 안다고 가정하겠습니다!

  • 일반 JOIN
    • 연관 Entity에 JOIN을 걸어도, 실제 쿼리에서 SELECT 하는 Entity는 오직 JPQL에서 조회하는 주체가 되는 Entity만 조회하여 영속화한다
    • 따라서 조회의 주체가 되는 Entity만 SELECT 해서 영속화하기 때문에 연관 Entity의 데이터는 필요하지 않지만, 검색 조건에는 필요한 경우 주로 사용한다
  • FETCH JOIN
    • 조회의 주체가 되는 Entity 이외에 FETCH JOIN하는 연관 Entity도 함께 SELECT 하여 모두 영속화한다
    • 즉, FETCH JOIN 하는 Entity가 모두 영속화 되기 때문에 FetchType.Lazy인 Entity를 참조하더라도 별도의 쿼리 진행 없이 참조가 가능하다. (N+1문제 해결)

글로 주루룩 적어 놓으니 무슨 말인지 잘 와닿지 않습니다,,

핵심은 Fetch 여부에 따라 Join 시 연관 Entity의 영속화 여부가 결정되는 것 같습니다.

예시를 통해 좀 더 알아볼까요?

3. 차이점 예시

테스트할 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;
        }
    }

일반 JOIN 실행하는 경우

저는 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을 사용하면 무엇이 달라지는지 확인해보겠습니다.

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이 좋은 선택일까요?

일반 JOIN은 언제 사용?

JPA는 객체과 데이터베이스간의 일관성을 잘 고려하여 사용해야 하기에 로직에 꼭 필요한 Entity만 영속성 컨텍스트에 담아 사용하는 것이 좋습니다.

설명한 것과 같이 일반 Join의 경우는 연관 Entity를 영속성 컨텍스트에 올리지 않습니다.

따라서 쿼리 검색 조건에는 필요하지만, 조회하는 Entity의 정보만 필요한 경우라면 굳이 연관 Entity를 영속성 컨텍스트에 담지 않아도 됩니다. (위의 예시에서 member의 정보가 필요하지 않은 경우)

이러한 경우는 Fetch Join을 사용하기 보단 일반 Join을 사용하는 것이 더 효과적입니다.

모든 경우에 그렇지만, 무지성 사용보단 상황을 적절하게 판단 후 알맞는 방법을 사용하는 자세가 필요합니다.

FETCH JOIN 단점

  • Fetch Join 대상에는 별칭을 줄 수 없음

    • 별칭을 부여하게 되면 연관된 데이터 수가 달라져서 데이터 무결성이 깨질 수 있음
  • 둘 이상의 컬렉션을 Fetch 할 수 없음

  • 컬렉션은 Fetch Join 하면 페이징 API를 사용할 수 없음

    • Fetch Join 후 페이징을 하게되면 메모리에 모든 데이터를 올린 후 메모리에서 페이징이 이루어짐

4.정리

마지막으로 다시 정리 해보겠습니다.

  • 일반 JOIN

    • Join 되는 연관 Entity는 영속성 컨텍스트에 올라가지 않는다
    • 검색 조건에만 필요하고, 연관 Entity의 정보는 필요하지 않은 경우 적합한 방법이다
  • FETCH JOIN

    • Join 되는 연관 Entity 또한 영속성 컨텍스트에 올라간다

차이점이 잘 이해 되셨나요?

잘 이해가 되지 않는다면 직접 본인만의 예시를 작성해보는 것도 좋을 것 같습니다 :)

읽어주셔서 감사합니다!



References

profile
하고 싶은 것들이 많은 개발자입니다
post-custom-banner

0개의 댓글