Spring/SpringDataJPA

@EntityGraph

느리지만 꾸준하게 2022. 4. 17. 23:04

JPA의 fetchjoin 기술(지연로딩개념 : LAZY)을 명확히 이해한 다음에

@EntityGraph로 넘어오자.

 

먼저 MemberRepositoryTest를 작성하면 member1은 teamA를 참조하고

member2는 teamB를 참조한다.

 

@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);
    Member member2 = new Member("member1", 10, teamB);
    memberRepository.save(member1);
    memberRepository.save(member2);
    
    
    // 영속성 컨텍스트의 있는 캐시 정보들을 db에 완전히 반영을 해서 INSERT를 정확하게 다하고 db에 다 반영을 시킨 다음에
    em.flush();
    // clear를 다한 후에 영속성 컨텍스트를 다 날려버리는 것이다.
    em.clear();
}

Member와 team의 관계는 ManyToOne이고 fetch = LAZY로 설정해놓았다.

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "tema_id")
private Team team;

 

MemberRepositoryTest를 실행시키면

@Test
    public void bulkUpdate() {
        //given
        memberRepository.save(new Member("member1", 10));
        memberRepository.save(new Member("member2", 19));
        memberRepository.save(new Member("member3", 20));
        memberRepository.save(new Member("member4", 21));
        memberRepository.save(new Member("member5", 40));

        // when
        int resultCount = memberRepository.bulkAgePlus(20);
//        em.clear();

        List<Member> result = memberRepository.findByUsername("member5");
        Member member5 = result.get(0);
        System.out.println("member5 = " + member5);

        // then
        assertThat(resultCount).isEqualTo(3);
    }


    @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);
        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());
        }

    }

 

한번의 쿼리로 결과 두개를 가져왔다.

select
        member0_.member_id as member_i1_0_,
        member0_.age as age2_0_,
        member0_.tema_id as tema_id4_0_,
        member0_.username as username3_0_ 
    from
        member member0_

member = member1
member = member2

 

 

그리고 getTeam().getClass를 해서 team의 class를 찍어보면 순서가

member query가 나가고 member1 찍고

teamClass를 찍은 다음에 team id를 가지고 query를 날려서 데이터를 가지고 온다.

그래서 teamA가 찍혀진다.

@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);
        Member member2 = new Member("member2", 10, teamB);
        memberRepository.save(member1);
        memberRepository.save(member2);

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

        // when
        // 멤버객체만 쭉 뽑아오기 위해
//        select Member!
        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());
        }

    }

그리고 두번째 루프에서 member2가 찍혀지고 proxy찍고

member2와 연관된 teamB가 어플리케이션 로딩과 연관이 안되어있어서 db쿼리를 날린다. 그래서 team에 대한 쿼리가 두번 날라가게 된다. 그리고 teamB가 조회된다. 

N + 1 문제라고 볼 수 있는데  query를 한번 날렸는데 member에 대한 결과는 2번이 나와서 n + 1 문제라고 할 수 있다.

select
        member0_.member_id as member_i1_0_,
        member0_.age as age2_0_,
        member0_.tema_id as tema_id4_0_,
        member0_.username as username3_0_ 
    from
        member member0_


member = member1


member.teamClass = class study.datajpa.entity.Team$HibernateProxy$Zsh4bA5f



select
        team0_.team_id as team_id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?


member.team = teamA
member = member2
member.teamClass = class study.datajpa.entity.Team$HibernateProxy$Zsh4bA5f

select
        team0_.team_id as team_id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
        
        
member.team = teamB

 

JPA에서는 이걸 fetchjoin이라는 걸로 해결을 해주는데 Member를 조회할 때 연관된 team을 같이 한번에 다 끌고온다.

// MemberRepository

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

테스트를 돌려보면

// MemberRepositoryTest

@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);
        Member member2 = new Member("member2", 10, teamB);
        memberRepository.save(member1);
        memberRepository.save(member2);

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

        // when
        // 멤버객체만 쭉 뽑아오기 위해
//        select Member! N + 1 문제
        List<Member> members = memberRepository.findMemberFetchJoin();

        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());
        }

    }

 

아래 부분에서 정확하게 join을 해서 member에 대한 데이터와 team에 대한 데이터를 다 select한다.

한번에 다 끌고 오기 때문에 

List<Member> members = memberRepository.findMemberFetchJoin();

 

select
        member0_.member_id as member_i1_0_0_,
        team1_.team_id as team_id1_1_1_,
        member0_.age as age2_0_0_,
        member0_.tema_id as tema_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_.tema_id=team1_.team_id

team 엔티티까지 같이 생성해서 Team team에다가 넣어놓는다.

// Member class

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

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "tema_id")
private Team team;

즉 아래 부분에서 proxy라는 것이 붙지않고

System.out.println("member = " + member.getUsername());
System.out.println("member.teamClass = " + member.getTeam().getClass());

class명이 확실하게 붙는다. 즉 순수하게 team 객체가 잘 되었다고 보면 된다.(proxy가 아니구나)

그리고 바로 teamA가 출력이 된다.

member = member1
member.teamClass = class study.datajpa.entity.Team
member.team = teamA

 

 

fetch join을 객체그래프로 표현된 (member.team) 연관된 것을 db에 join을 해서 한번에 select 포함시켜서 다 끌고 오는것이다.

db의 join은 join만 하는 것인데 select절에 데이터를 다 넣어준다.

 

즉 원하는 member를 조회할 때 한방에 쿼리로 끝내는 것이다.

 

 

 

 

스프링 데이터 JPA에서는 EntityGraph라는 것으로 findByUsername처럼 메서드 이름으로 해결을 해야하는데 

fetchjoin까지 같이 하고싶은 거까지 깔끔하게 해결해준다. 

// MemberRepository 

    @Override
    @EntityGraph(attributePaths = {"team"})
    List<Member> findAll();

테스트를 돌려주면 lazy 로딩 되는 것이 없고

//

@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);
        Member member2 = new Member("member2", 10, teamB);
        memberRepository.save(member1);
        memberRepository.save(member2);

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

        // when
        // 멤버객체만 쭉 뽑아오기 위해
//        select Member! N + 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());
        }

    }

member랑 team이랑 다가지고 온다.

select
        member0_.member_id as member_i1_0_0_,
        team1_.team_id as team_id1_1_1_,
        member0_.age as age2_0_0_,
        member0_.tema_id as tema_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_.tema_id=team1_.team_id
2022-04-17 22:46:49.540  INFO 40888 --- [           main] p6spy                                    : #1650203209540 | took 0ms | statement | connection 3| url jdbc:h2:tcp://localhost/~/datajpa
select member0_.member_id as member_i1_0_0_, team1_.team_id as team_id1_1_1_, member0_.age as age2_0_0_, member0_.tema_id as tema_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_.tema_id=team1_.team_id
select member0_.member_id as member_i1_0_0_, team1_.team_id as team_id1_1_1_, member0_.age as age2_0_0_, member0_.tema_id as tema_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_.tema_id=team1_.team_id;
member = member1
member.teamClass = class study.datajpa.entity.Team
member.team = teamA
member = member2
member.teamClass = class study.datajpa.entity.Team
member.team = teamB

 

즉 JPQL없이도 객체그래프를 한번에 엮어서 성능 최적화를 가져오는게 가능하다.

그리고 JPQL도 짜고 FetchJOIN도 가능하다.(JPQL에다가 entity graph를 넣는 것이다.)

@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityhGraph();

 

 

또 회원데이터를 쓸 때 팀 데이터를 쓸 일이 많이 생길수 있으므로 아래코드와 같이 엔티티 그래프를 뽑을 수 있다.

@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();


// 엔티티 그래프를 뽑을 수 있다.
@EntityGraph(attributePaths = {"team"})
List<Member> findEntityGraphByUsername(@Param("username") String username);

fetchjoin의 기본형인 left outer join이 나가고 select문이 아래와 같이 나온다. 그리고 데이터를 똑같이 쭉 출력한다.

=> entityGraph를 사용하면 fetchjoin을 굉장히 편리하게 쓸 수 있다.

select
        member0_.member_id as member_i1_0_0_,
        team1_.team_id as team_id1_1_1_,
        member0_.age as age2_0_0_,
        member0_.tema_id as tema_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_.tema_id=team1_.team_id 
    where
        member0_.username=?
        
        
member = member1
member.teamClass = class study.datajpa.entity.Team
member.team = teamA
member = member1
member.teamClass = class study.datajpa.entity.Team
member.team = teamB

 

이번에는 member class로 가서 JPA의 표준스펙인 NamedEntityGraph를 쓸 수가 있다.

@NamedQuery(
        name="Member.findByUsername",
        query="select m from Member m where m.username = :username"
)
@NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))

 

MemberRepository에서는 아래와 같이 설정해주고 실행시키면 결과가 똑같이 나온다.

//    @EntityGraph(attributePaths = {"team"})
    @EntityGraph("Member.all")
    List<Member> findEntityGraphByUsername(@Param("username") String username);
select
        member0_.member_id as member_i1_0_0_,
        team1_.team_id as team_id1_1_1_,
        member0_.age as age2_0_0_,
        member0_.tema_id as tema_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_.tema_id=team1_.team_id 
    where
        member0_.username=?

 

 

아래와 같이 두가지 방법이 있는데 왠만하면 attributePaths나 JPQL예서 Fetchjoin을 쓰는 것이 효율적이다.

간단할때는 EntityGraph / 복잡한 쿼리가 반영되면 JPQL Fetchjoin을 쓴다.

    @EntityGraph(attributePaths = {"team"})
//    @EntityGraph("Member.all")

EntityGraph의 패키지 경로는 아래와 같다.

package org.springframework.data.jpa.repository;

 

 

 

 

 

 

 

 

<출처 김영한: 실전! 스프링 데이터 JPA >

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard

 

실전! 스프링 데이터 JPA - 인프런 | 강의

스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.

www.inflearn.com

 

'Spring > SpringDataJPA' 카테고리의 다른 글

사용자 정의 리포지토리 구현  (0) 2022.04.18
JPA Hint & Lock  (0) 2022.04.17
벌크성 수정 쿼리(error 해결중)  (0) 2022.04.17
스프링 데이터 JPA 페이징과 정렬  (0) 2022.04.17
순수 JPA 페이징과 정렬  (0) 2022.04.17