본문 바로가기
JPA/스프링 DATA JPA

Data JPA 08 @EntityGraph

by 킹차니 2022. 1. 7.

@EntityGraph

연관된 엔티티들을 SQL한번에 조회하는 방법

 

현재 Member와 Team엔티티는 다대일 연관관계를 가지고 있는데, fetch=FetchType.LAZY로 되어 있어 지연 로딩 전략을 하고있다.

 

Member엔티티

@Entity
public class Member {

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

    @ToString.Exclude//연관관계 필드는 toString을 안하는 것이 좋다.
    @JoinColumn(name="team_id")
    @ManyToOne(fetch=FetchType.LAZY)
    private Team team; /* Team과 다대일 연관관계 */
}

하여 JPA로 Member를 조회하면 Team은 프록시 객체로 가져온 뒤, Team관련 데이터를 조회할 때 쿼리가 나가는 N+1문제가 발생한다.

( 이해가 안간다면 페치 조인 포스트 참고 )

 

아래와 같이 이를 테스트해보자.

@Test
void findMemberLazy(){
    
    //given
    Team teamA = new Team("teamA");
    Team teamB = new Team("teamB");
    teamJPARepository.save(teamA);
    teamJPARepository.save(teamB);

    Member member1 = new Member("member1", 10, teamA);
    Member member2 = new Member("member2", 10, teamB);
    memberRepository.save(member1);
    memberRepository.save(member2);

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

    //when
    List<Member> members = memberRepository.findAll();
    for (Member member : members){
        System.out.println("member = " + member.getUsername());
        /* 여기서 team 쿼리가 나간다! */
        System.out.println("member.team = " + member.getTeam().getName());
    }
}

이를 실행하면 한번에 Team까지 join하지 않고 아래와 같이 각 member1, member2의 Team의 name정보를 얻기 위해 주석 아래의 코드에서 team의 name을 select하기 위한 쿼리가 나가게 되는 것이다. 이와 같은 일은 현재 Member가 Team을 Lazy로 설정했기 때문이다.

select
        member0_.member_id as member_i1_0_,
        member0_.age as age2_0_,
        member0_.team_id as team_id4_0_,
        member0_.username as username3_0_ 
    from
        member member0_

// member1의 team.name 조회 쿼리
    select
        team0_.team_id as team_id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?

// member2의 team.name 조회 쿼리
    select
        team0_.team_id as team_id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?

위와 같이 세번의 쿼리가 나간다. 하여 이를 순수 JPA를 사용할때는 페치 조인을 사용하여 해결했다.

 

 

data jpa 리퍼지토리에서는 아래와 같이 할 수 있을 것이다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Member m left join fetch m.team")
    List<Member> findMemberFetchJoin();
}

하지만 이렇게 일일히 fetch join 쿼리를 작성하는 것은 꽤나 귀찮다. 하여 훨씬 쉬운 방법을 제공하는데, 그것이 @EntityGraph이다.

 

 

@EntityGraph

 

아래와 같이 하면 된다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @EntityGraph(attributePaths = {"team"})
    List<Member> findAll();
}

위처럼 @EntityGraph(attributePaths ={})의 대괄호 안에 fetch join하고 싶은 엔티티를 넣어준다. 현재 Memer클래스의 필드에 Team team이라 되어 있으므로, "team"을 적어준 것이다. 이를 테스트 해보자.

@Test
void findMemberEntityGraph(){

    //given
    Team teamA = new Team("teamA");
    Team teamB = new Team("teamB");
    teamJPARepository.save(teamA);
    teamJPARepository.save(teamB);

    Member member1 = new Member("member1", 10, teamA);
    Member member2 = new Member("member2", 10, teamB);
    memberRepository.save(member1);
    memberRepository.save(member2);

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

    //when
    List<Member> members = memberRepository.findAll();
    for (Member member : members){
        System.out.println("member = " + member.getUsername());
        System.out.println("member.team = " + member.getTeam().getName());
    }
}

실행결과 아래처럼 fetch join이 적용된 하나의 쿼리만 나갔다.

    select
        member0_.member_id as member_i1_0_0_,
        team1_.team_id as team_id1_1_1_,
        member0_.age as age2_0_0_,
        member0_.team_id as team_id4_0_0_,
        member0_.username as username3_0_0_,
        team1_.name as name2_1_1_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.team_id=team1_.team_id

 

또한 아래와 같이 Member를 select하는 쿼리만 짠 뒤, @EntityGraph로 페치 조인만 추가하는 것도 가능하다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @EntityGraph(attributePaths = {"team"})
    @Query("select m from Member m")
    List<Member> findAll();
}

 

또한 아래와 같이 메서드 이름으로 쿼리를 정의하는 방법으로도 사용가능하다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @EntityGraph(attributePaths = {"team"})
    List<Member> findEntityGraphByUsername(@Param("username") String username);
    //find ... by 의 ...에는 아무거나 적어도 된다.
}

 

 

 

 

 

 

김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.