
Spring Data JPA 게시글은 대부분 인프런의 김영한님의 강의인 '실전! 스프링 데이터 JPA' 기반으로 내용을 정리했습니다.
@EntityGraph
연관된 엔티티들을SQL 한 번에 조회하는 방법이다. EntityGraph를 제대로 이해하려면 fetch join에 대해 자세히 알고 있어야 한다. 그래서 본론에 들어가기 앞서 fetch join에 대해 먼저 알아보자.
fetch join
- MemberRepositoryTest.class
// Member.class
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="team_id")
private Team team;
지금 Member 기준에서 Member와 Team의 연관관계는 다대일이다. 그리고 연관 관계 매핑 속성에 fetch = FetchType.LAZY가 걸려있다. 지연 로딩이라는 건데 무조건 모든 연관 관계에서는 지연 로딩 설정을 해주어야 한다. (연관 관계 종류에 따라 자동으로 LAZY가 걸려있는 것도 있지만 아닌 것도 있음)
@Test
public void findMemberLazy() {
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
teamRepository.save(teamA);
teamRepository.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();
List<Member> members = memberRepository.findAll();
for (Member member : members) {
System.out.println("member = " + member.getUsername());
}
}
Member와 Team을 적절하게 설정해주고 Member에 관해 findAll() 후 데이터를 출력해보면 신기한 점을 찾아볼 수 있다.
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_
member = member1
member = member2
쿼리를 보면 team에 대한 것은 싹 사라져있다. 원래대로라면 Member와 team이 join 해야 하지 않나?라고 생각할 수 있는데 지연 로딩을 걸면 team 객체의 값을 직접적으로 호출하기 전까지는 team은 가짜 객체(프록시)로 존재해 쓸데없는 성능 낭비를 줄일 수 있다. 그럼 team 데이터를 호출해보자.
프록시를 잘 모른다면 아래 포스팅을 참고하자.
JPA 프록시와 연관관계 - 프록시
JPA 게시글은 대부분 인프런의 김영한님의 강의인 '자바 ORM 표준 JPA 프로그래밍' 기반으로 내용을 정리했습니다. 프록시 나는 둘 중에 하나만 조회하고 싶어 Member와 Team이 연관관계로 매핑되어
devhan.tistory.com
for (Member member : members) {
System.out.println("member = " + member.getUsername());
System.out.println("member.getTeam().getName() = " + member.getTeam().getName()); // 추가
}
team까지 호출하면 select 쿼리가 총 몇 번 나갈까? 원래 생각대로라면 두 번이 나가야 정상인데 출력 로그를 보면 select 쿼리가 세 번이 나간 걸 확인할 수 있다.
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_
member = member1
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
member.getTeam().getName() = teamA
member = member2
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
member.getTeam().getName() = teamB
이런 문제를 N + 1 문제라고 한다. 쿼리를 잘 보면 첫 번째 쿼리는 memberRepository.findAll()에 관한 쿼리이고, 두 번째와 세 번째 쿼리는 member.getTeam().getName()에 대한 쿼리이다. member.getTeam()까지만 해도 Team 객체에 직접적인 접근이 아니기 때문에 두세 번째 쿼리가 나가지 않겠지만 getName()을 한 순간 Team에서 직접 접근하여 데이터를 가져와야 하기 때문에 프록시 객체로 해결할 수 없어 DB에 데이터를 가져오기 위해 추가적인 쿼리를 두 번 날리는 것이다. fetch join을 잘 모른다면 아래 포스팅을 참고하자.
JPA 객체지향 쿼리 언어(JPQL) - 패치 조인(기본)
JPA 게시글은 대부분 인프런의 김영한님의 강의인 '자바 ORM 표준 JPA 프로그래밍' 기반으로 내용을 정리했습니다. 패치(fetch) 조인 1 - 기본 정말 중요한 부분이므로 집중 필수! SQL 조인 종류가 아
devhan.tistory.com
위에 지연 로딩에 관한 문제점을 해결하기 위해 JPA에서는 fetch join이란 것을 지원한다.
- MemberRepository.interface
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
- MemberRepositoryTest.class
위에서 작성했던 test 코드에 아래 부분을 수정 후 돌려보자.
List<Member> members = memberRepository.findMemberFetchJoin(); // 수정
for (Member member : members) {
System.out.println("member = " + member.getUsername());
System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
}
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 = member1
member.getTeam().getName() = teamA
member = member2
member.getTeam().getName() = teamB
join으로 team에 대한 데이터를 한 번에 가져와서 select가 한 번만 일어난다. 언뜻보면 즉시 로딩(EAGER)과 별 차이점이 없어 보이기도 하다. 과연 그럴까? 즉시 로딩으로 설정 후 쿼리를 구경해보자.
즉시 로딩과 fetch join 차이점
- Member.class
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name="team_id")
private Team team;
- MemberRepositoryTest.class
//List<Member> members = memberRepository.findMemberFetchJoin();
List<Member> members = memberRepository.findAll();
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_
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
member = member1
member.getTeam().getName() = teamA
member = member2
member.getTeam().getName() = teamB
즉시 로딩의 경우에도 N + 1 문제가 발생한다. 지연 로딩과 다른 점은 프록시를 만들지 않고 애초에 member를 조회할 때 team까지 싹 다 조회해버린다. 그렇기 때문에 지연 로딩처럼 member.getTeam().getName() 출력문 사이사이에 select 문이 있지 않고 member 다음에 주르륵 딸려오는 것이다. 간단하게 정리하자면 즉시 로딩은 N + 1 문제가 발생하고, fetch join은 N + 1 문제가 발생하지 않는다.
이제 본론으로 들어가보자면 스프링 데이터 JPA에서 fetch join을 사용하려면 매번 JPQL을 써줘야 하는 귀찮음이 발생한다. 그래서 스프링 데이터 JPA에서 @EntityGraph를 지원하며 이 기능을 사용하면 추가적인 JPQL 없이 fetch join을 사용할 수 있다.
@EntityGraph
- MemberRepository.interface
@EntityGraph를 사용하기 위해 일단 JpaRepository 인터페이스에 있는 findAll()을 오버라이딩해야 한다.
// JpaRepository.interface
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#findAll()
*/
@Override
List<T> findAll();
// MemberRepository.interface
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll()
- MemberRepositoryTest.class
List<Member> members = memberRepository.findAll();
테스트 코드를 위와 같이 수정하고 실행해보자.
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 = member1
member.getTeam().getName() = teamA
member = member2
member.getTeam().getName() = teamB
그러면 위에서 JPQL을 이용해 fetch join을 한 쿼리와 똑같이 출력된다.
참고로 JPQL과 @EntityGraph는 같이 사용해도 된다.
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
메서드 이름으로 쿼리 생성 기능에도 @EntityGraph를 사용할 수 있다.
메서드 이름으로 쿼리 생성 기능을 잘 모른다면 아래 포스팅을 참고하자.
Spring Data JPA - 메서드 이름으로 쿼리 생성
Spring Data JPA 게시글은 대부분 인프런의 김영한님의 강의인 '실전! 스프링 데이터 JPA' 기반으로 내용을 정리했습니다. 메서드 이름으로 쿼리 생성 쿼리 메서드 기능 3가지 메서드 이름으로 쿼리
devhan.tistory.com
@EntityGraph(attributePaths = {"team"})
List<Member> findEntityGraphByUsername(@Param("username") String username);
NamedEntityGraph
JPA 표준 스펙 2.2부터 들어간 기능이다.
- Member.class
@NamedEntityGraph(name = "Member.all" , attributeNodes = @NamedAttributeNode("team"))
public class Member {
- MemberRepository.interface
@EntityGraph("Member.all")
List<Member> findEntityGraphByUsername(@Param("username") String username);
Member class 상단에 @NamedEntityGraph() 애노테이션을 추가하고, 사용할 때는 name을 이용해서 호출하면 된다.
! 간단한 기능의 메서드의 경우 @EntityGraph를 사용해도 되지만 복잡한 기능의 경우 꼭 JPQL로 풀어야 할 때가 있기도 하다. 그럴 경우엔 그냥 JPQL을 사용하는 게 맞다.