ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • FetchType.Lazy 와 Fetch Join (N+1문제 해결!)
    Spring & JPA 2023. 11. 27. 01:17

     

    FetchType과 Fetch join까지 한번에 다 다루기 힘들고 이해해야할 부분도 많지만.. 최대한 쉽게 와닿을수 있게 글을 쓰려고 노력했다.

     

     

    Jpa 에서 Fetch에 대한 키워드가 FetchType.Lazy 와 Fetch Join  2개가 있었다. 

     

    둘다 데이터를 조회할 때 조금 더 효율적으로 작업하고자 사용한다는 공통점이 있다.

     

    하지만 FetchType은 엔티티 레벨에서 설정하는 값이고, Fetch Join은 레포지토리에서 조인 쿼리를 이용하는 것이다.

     

     

    간단하게 말하면 FetchType으로 글로벌 설정을 하고 Fetch Join으로 좀 더 세세하게 작업을 하는 것이다.

     

    처음에는 위 두개를 놓고 직접 비교하면서 공부하기보다 

     

    FetchType 값으로 이용 가능한 FetchType.Eager  vs. FetchType.Lazy   두 가지를 같이 공부하고 

    이후에 Fetch Join 을 공부하는 것이 더 이해하기 쉽다.

     

     


    FetchType.Eager  vs. FetchType.Lazy

     

    FetchType은 Entity에 설정하는 값이다.

     

    DB에서 값을 받아오기전부터 해당 Entity의 Lazy가 붙은 필드 값 불러오는것은 지연 시키겠다는 의미다.

     

    @Entity
    @NoArgsConstructor
    public class Team {
    
        @Id
        private Long id;
    
        @OneToMany(mappedBy = "team")
        private List<Member> members = new ArrayList<>();
    
    }

     

    @Entity
    @NoArgsConstructor
    public class Member {
    
        @Id
        private Long id;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "team_id")
        private Team team;
    
    }

     

    Team 클래스와 Team을 참조하는 FK를 가진 Member 클래스가 있다. 

     

    Team과 Member를 생성한 후 저장된 Member레포지토리에서 Member를 불러와보자.

     

    Team team1 = new Team();
    Team savedTeam1 = teamRepository.save(team1);
    
    Member member1 = new Member();
    member1.setTeam(savedTeam1);
    
    Member savedMember = memberRepository.save(member1);
    
    List<Member> all = memberRepository.findAll();
    
    System.out.printf(all.get(0).getTeam().getClass().toString());

     

    결과는 Member를 불러오고 Member가 참조하고있는 Team 의 클래스 정보를 까봤더니 프록시 객체가 나탄다.

     

     

     

    이게 바로 FetchType.Lazy 의 효과로, Team 정보 불러오는 것을 최대한 지연 시키겠다는 의미다. 

     


     

    왜 지연을 시켜?

     

    내가 필요한 정보는 member List 다.  member가 필요해서 불러오는 것이지, 당장 Member가 가진 Team은 필요한지 아닌지 모른다.

     

    그래서 Member에 있는 Team 정보는 Proxy라는 속이 빈 껍데기만 만들어놓고 Member의 다른 값들만 제대로 사용하겠다는 뜻이다.

     

    그래서 Member 리스트만 부르는 단 하나의 쿼리가 나간다. 

     

     

    이런 Lazy가 좋으면 얼마나 좋다고? 

     

    FetchType.Eager 도 사용해보자

     

     

    MemberList에 있는 Member들이 참조하는 각 Team에 대한 모든 쿼리가  나간다.

     

    지금 발생하는 것이 JPA에서 발생하는 N+1 문제다.

     

    하나의 쿼리를 날리는데(MemberList), 그 쿼리의 결과로 나타나는 N개의 케이스에 대해 N개만큼 쿼리가 다시 나가는 것이다.

     

    이러한 이유때문에 FetchType.Lazy를 사용해서 사용하기 전까지는 최대한 객체 정보를 지연시키고자 하는 것이다.


     

    Lazy로 인해 Member가 갖고있던 비어있던 Proxy Team 객체는 Team에 대한 정보를 get 메서드로  불러오면

     

    그제서야 DB에서 영속성 컨텍스트로 값을 가져와 해당 Team 객체 값들을 갱신한다. 

     

    물론 이때도 여전히 Proxy 객체긴 하지만 더이상 빈 껍데기가 아니다.


     

    Fetch Join  의 필요성

     

    물론 1차적으로 Team객체를 직접 사용하지 않는 경우도 있을테니 이러한 경우엔 Lazy의 확실한 이점이 있다.

     

    그런데 아이러니한 것은 Lazy로 지연로딩을 하더라도 결국에 Member의 Team정보를 얻어야한다면 N+1문제는 반드시 일어나게 되어있다.

     

    각 Member가 갖고있는 Team에 대해 그때 그때 조인을 해줘야하는데, 

    만약 List<Member> members 라는 리스트를 읽고싶다면  Member의 개수인 N개만큼 추가 쿼리가 나가기 때문이다.

     

    이 때 사용하는 것이 Fetch Join이다.

     

    참조하는 객체를 그때 그때 join해서 N+1 나타내는걸 막고자,  처음부터 두 개의 테이블을 통채로 join처리해서 불러온다.

     

     

    member와 team을 처음부터 합치는 fetch join문을 작성한다. 

     

     

    그 쿼리를 통해 처음부터 두 테이블을 조인을 하고있다. 

    (Fetch join도 결국엔 다른 특별한 조인이 아니라 inner join을 활용하는 것이다.)

    중요한 차이는 조인 하면서 관련된 모든 entity를 가져오는 것이다.

     

    이 때 Entity에 FetchType.Lazy가 걸려있더라도  fetch join문 확인 후 강제로 값이 채워질 것이다.

    team 객체도 애초에 proxy로 생성되지 않는다.

     

    현재 members내의 각team 정보를 찾더라도 또 다른 쿼리가 필요없이 기존에 가져온 값에서 모두 찾아낼 수 있다.

     

     

    **

     

    스프링 3.x~ 에서는 하이버네이트 6이상을 사용해서 다른 문제가 없지만

    하이버네이트 5 이하 버전을 사용한다면, 일대다 관계의 Fetch join , distinct 키워드에 대해 공부해야한다.

     

     

    그림이 좀 조잡하지만..

     

    일대다(team-member)에서 조인을 날린 결과를 보면 다음 PK가 중복된다. 

    이러면 영속성 컨텍스트에 가져올때 식별자(ID)가 중복된다. 이걸 쿼리문에 distinct를 사용해서 해결할 수 있다.

     

    **

     

     

     


     

     

    결론을 내보자면

     

    1차적으로 설정할 수 있는 Entity 레벨에서 FetchType 에서 Lazy는 사실상 반 강제로 해야한다고 보면되고

     

    Lazy가 걸려있는 객체 관계에서  자주 참조할 일이 있다면, 메서드 단위로 fetch join을 작성하면 될 것 같다.

     

    오늘 글에서는 fetchtype.lazy와 fetch join 의 필요성에 대해 공부했는데

     

    다음엔 fetch join의 한계와 해결 방법에 대해 포스팅 할 예정이다.

     


     

     

     

     

    Reference

     

    -  자바 ORM 표준 JPA 프로그래밍 | 김영한 저 

     

    - https://www.inflearn.com/course/lecture?courseSlug=스프링부트-JPA-API개발-성능최적화&unitId=24327&category=questionDetail&q=823816&tab=script

Designed by Tistory.