본문 바로가기

JPA

[Spring Data JPA] @EntityGraph

엔티티 그래프(Entity Graph)

JPA(EntityManager)나 Hibernate에서 제공하는 기능으로, 엔티티를 조회할 때 연관된 엔티티를 어떤 방식으로 가져올지 정의하는 방법이다. 이를 통해 명시적으로 패치 전략(Fetch Strategy)을 설정하고, 필요에 따라 지연 로딩(Lazy Loading)과 즉시 로딩(Eager Loading)을 제어할 수 있다.

 

엔티티 그래프를 사용하는가?

  • N+1 문제 해결:
    • JPA의 기본 설정에서 연관 엔티티는 지연 로딩(FetchType.LAZY)으로 설정한다.
    • 필요 시 연관된 데이터를 쿼리하는데, 다수의 추가 쿼리가 발생해 성능 문제가 생길 수 있다.
    • 이를 엔티티 그래프로 해결할 수 있다.
  • JPQL 대체:
    • 복잡한 JPQL을 사용하지 않고, 엔티티 그래프를 활용해 간결하고 명시적으로 원하는 연관 데이터를 가져올 수 있다.
  • 선택적 로딩:
    • 필요에 따라 연관된 특정 데이터만 로딩할 수 있어 성능 최적화가 가능하다.

예시 코드) member → team은 지연로딩 관계 

@Test
public void findMemberLazy() {
    // given
    // member1 -> teamA
    // member2 -> teamB
    Team teamA = new Team("teamA");
    Team teamB = new Team("teamB");
    teamRepository.save(teamA);
    teamRepository.save(teamB);

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

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

    // when N + 1
    // select Member 1
    List<Member> members = memberRepository.findAll();
    for (Member member : members) {
        System.out.println("member = " + member.getUsername());
        System.out.println("member.teamClass = " + member.getTeam().getClass());
        System.out.println("member.team = " + member.getTeam().getName());
    }
}
  • 따라서 다음과 같이 team의 데이터를 조회할 때 마다 쿼리가 실행된다.(N+1문제)

실행 결과

    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0
       
member = member1
member.getTeam().getClass() = class study.data_jpa.entity.Team$HibernateProxy$yqMTZgrE

    // 팀A가 필요한 시점에 팀 조회
    select
        t1_0.team_id,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?
member.getTeam().getName() = teamA
=========================================================================================
member = member2
member.getTeam().getClass() = class study.data_jpa.entity.Team$HibernateProxy$yqMTZgrE

    // 팀B가 필요한 시점에 팀 조회    
    select
        t1_0.team_id,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?
member.getTeam().getName() = teamB
  • 만약 select Member(1)의 결과가 10개가 나왔다면 관련된 팀의 프록시를 초기화해야 하기 때문에 10(n)번의 쿼리가 추가로 나감.

 참고: 다음과 같이 지연 로딩 여부를 확인할 수 있다.

//Hibernate 기능으로 확인 Hibernate.isInitialized(member.getTeam())

//JPA 표준 방법으로 확인
 PersistenceUnitUtil util =
 em.getEntityManagerFactory().getPersistenceUnitUtil();
util.isLoaded(member.getTeam());

JPQL 페치 조인

연관된 엔티티를 한번에 조회하려면 페치 조인이 필요하다.

@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
  • 스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하게 도와준다.
  • 이 기능을 사용하면 JPQL 없이 페치 조인을 사용할 수 있다. (JPQL + 엔티티 그래프도 가능)

실행 결과

select
        m1_0.member_id,
        m1_0.age,
        t1_0.team_id,
        t1_0.name,
        m1_0.username 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id
member = member1
member.teamClass = class study.data_jpa.entity.Team
member.team = teamA
member = member2
member.teamClass = class study.data_jpa.entity.Team
member.team = teamB
  • 테스트는 위와 메서드 이름만 바뀌고 동일하다. (findAll() → findMemberFetchJoin())
  • member.teamClass를 보면 프록시가 아니고 진짜 데이터가 다 채워진 진짜 엔티티가 나온것을 확인할 수 있다.
  • 이를 통해, 페치 조인은 연관관계가 있는 객체를 데이터베이스의 조인을 활용해 한 번에 모두 조인하면서 select 절에 데이터들을 넣어주는 것을 확인할 수 있다. 

그런데 매번 이렇게 Query로 JPQL을 적어줘야 할까?

  • 예를 들면 findByUserName()처럼 메서드 이름으로 쿼리를 만들고 싶은데 페치 조인까지 하고 싶은 경우가 있을 것이다.
  • Spring Data JPA 는 엔티티 그래프로 이런 문제를 해결해준다.

엔티티 그래프

//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"}) 
List<Member> findAll();

//JPQL + 엔티티 그래프 
@EntityGraph(attributePaths = {"team"}) 
@Query("select m from Member m") 
List<Member> findMemberEntityGraph();

//메서드 이름으로 쿼리에서 특히 편리하다. 
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)
  • 스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하게 도와준다.
  • 이 기능을 사용하면 JPQL 없이 페치 조인을 사용할 수 있다. (JPQL + 엔티티 그래프도 가능)
  • 사실상 페치 조인(FETCH JOIN)의 간편 버전

  • LEFT OUTER JOIN 사용

NamedEntityGraph 사용 방법

@NamedEntityGraph(name = "Member.all", attributeNodes =
@NamedAttributeNode("team"))
@Entity
public class Member {...}
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
  • 엔티티 그래프를 또 다른 방법으로 사용할 수 있다.
  • @NamedQuery와 사용 방법이 비슷하다.