SpringDataJPA

페치조인 정리

lby132 2022. 11. 30. 00:46
@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
@Test
void findMemberLazy() {
    final Team teamA = new Team("teamA");
    final Team teamB = new Team("teamB");
    teamRepository.save(teamA);
    teamRepository.save(teamB);
    final Member member1 = new Member("member1", 10, teamA);
    final Member member2 = new Member("member1", 10, teamA);
    memberRepository.save(member1);
    memberRepository.save(member2);

    em.flush();
    em.clear();

    //Member만 가져오는 쿼리가 나감
    //final List<Member> members = memberRepository.findAll();
    //final List<Member> members = memberRepository.findMemberFetchJoin();
    final List<Member> members = memberRepository.findEntityGraphByUsername("member1");

    for (Member member : members) {
        System.out.println("member = " + member.getUsername());
        System.out.println("member.getTeam = " + member.getTeam().getClass());
        System.out.println("member.team.name = " + member.getTeam().getName());
    }

현재 이상황을 두고 설명하자면 시스템아웃을 보면 member.getUsername()을 호출했을때

Member엔티티에서 실제 DB에 셀렉트쿼리를 날려서 가져왔을것이다. 그래서 username이 찍혔고,

그 밑에 라인에 시스템아웃을 보면 Member엔티티에 참조되어있는 Team을 출력하고 있다. 더 자세히 말하면 현재 Team엔티티가 어떤 클래스인지 알려주고 있다. 결과는 어쩌고저쩌고 하이버네이트 프록시라고 나온다. 이유는 현재 team이 Member엔티티에서 Lazy로 설정되어 있기 때문이다. Lazy로 되어있으면 Eager 와 다르게 메인 엔티티가 호출될때 연관된 애들을 다가져오는게 아니라 일단 Team을 프록시로 가져와서 Select쿼리를 날려준다. 가짜 객체인것이다. 현재로서는 Team에 데이터가 채워지지 않은 상태이다. 그런데 그 바로 밑에 getTeam()에 getName()까지 호출하면 그때서야 Team에 진짜 DB에서 name값을 가져와서 넣어준다. 여기서 문제는 Member가 값이 두개있으니까 이 행위를 두번하는것이다. Member의 결과 값이 많아지면? 

이 문제를 N + 1 문제라고 하는데 Member의 결과값 만큼 Team도 똑같이 쿼리가 나간다. 결과 값이 N이고 1은 Member를 가져오기 위해 날린 쿼리이다. 예를 들면 회원이 100명이면 회원을 가져오기 위해 한번 날린 쿼리의 결과만큼(100번) 돈다는 뜻이다. Member에 연관된 Team까지 쿼리가 나가니깐. 지연로딩(Lazy)이던 즉시로딩(Eager)이던 똑같이 발생하는 문제이다. 

이 문제를 발생시키지 않기 위해 한방에 가져오는 Fetch Join을 쓴다.

@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();

// 페치조인 다른 방법 1
@Override //JpaRepository에 있는 findAll()을 오버라이딩함
@EntityGraph(attributePaths = {"team"}) // jpql로 select m from Member m left join fetch m.team 이렇게 페치조인을 걸어주기
                                        // 귀찮을때 써도되는 방법 내부적으로는 fetch join을 쓴다고 함.
List<Member> findAll();

// 페치조인 다른 방법 2 jpql이랑 같이 쓰고 싶을때
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

// 페치조인 다른 방법 3 메소드이름이랑 같이 쓰고 싶을때
//@EntityGraph("Member.all")  Member에서 설정한 NamedEntityGraph에 설정한 이름을 넣어주면 된다. 이 방법은 잘 안씀.
@EntityGraph(attributePaths = {"team"}) // 간단할때 사용. 아니면 jpql 로 페치조인 사용.
List<Member> findEntityGraphByUsername(@Param("username") String username);

이건 페치조인 쓰는 방법이다.

페치조인은 한번에 다 조인해서 가져온다. 프록시를 들고 오는게 아니라 아예 Member쿼리가 호출될때 와 Team을 left join을해서 한번에 다 가져온다. 참고로 페치조인은 left join을 기본적으로 한다.

 

'SpringDataJPA' 카테고리의 다른 글

auditing 적용할때 공통으로 @EntityListeners(AuditingEntityListener.class) 적용하기  (0) 2023.06.09
Auditing  (1) 2022.12.02
페이징처리 간편기능  (0) 2022.11.29
@Query  (0) 2022.11.29
NamedQuery  (0) 2022.11.29