[JPA]join 과 fetch join , @ToOne에서N+1 문제 해결

정태규·2023년 12월 21일
0

JPA

목록 보기
3/4

결론부터 말하면,

join : 연관된 객체를 select하지 않고 주체만 select한다.

fetch join: 연관된 객체까지 select 한다.

따라서, 검색 조건에만 필요하고 데이터가 필요 없다면 join
데이터까지 필요하다면 fetch join을 쓰면 된다!

자세하게 들어가 보겠다.

//Member entity
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;

    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }

    public Member(String username, int age) {
        this.username = username;
        this.age = age;
    }

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "Team_id")
    private Team team;

    public Member(String username) {
        this.username = username;
    }

    public void changeUserName(String username) {
        this.username = username;
    }

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }


}
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of={"id","name"})
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "Team_id")
    private Long id;
    private String name;
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}
//MemberRepository
@Query("select m from Member m join m.team t")
    List<Member> findMembersWithTeam();

이렇게 Member와 team이 다대일로 매핑 되어 있다.

이때, Member 객체를 Team을 join해서 조회 해보자.

@Test
    public void findMembers() {
        Member member1 = new Member("member1", 10);
        Member member2 = new Member("member2", 10);
        Member member3 = new Member("member3", 10);

        memberRepository.saveAll(Arrays.asList(member1, member2, member3));

        Team team1 = new Team("team1");
        Team team2 = new Team("team2");
        Team team3 = new Team("team3");

        teamRepository.saveAll(Arrays.asList(team1, team2, team3));

        member1.changeTeam(team1);
        member2.changeTeam(team2);
        member3.changeTeam(team3);
        //영속성 컨텍스트를 비워준다.
        em.flush();
        em.clear();

        List<Member> members = memberRepository.findMembersWithTeam();
				
        for (Member member : members) {
          System.out.println("member.getTeam() = " + member.getTeam());
        }

    }

결과
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
inner join
team team1

on member0.team_id=team1.team_id

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=?

select
team0.team_id as team_id1_1_0,
team0.name as name2_1_0
from
team team0
where
team0
.team_id=?

//MemberRepository
@Query("select m from Member m join m.team t")
    List<Member> findMembersWithTeam();

왜 join으로 조회했을 때 쿼리가 4번이 나갈까?

(select member 1번, select team 3번)

1번째 쿼리에서는 member들을 조회,
2번째는 member중 1번째의 team을 조회,
3번째는 member중 2번째의 team을 조회,
4번째는 member중 3번째의 team을 조회하게된다.
그리고, 중요한 점은 join은 조회 했을때, join의 주체만 영속성 컨텍스트에 영속되고 join당한 엔티티는 영속되지 않는다. 이 때문에 쿼리를 다시 날리는 것이다.

여기서 fetchtype 'eager' 와 'lazy'의 차이점을 살펴보자면
lazy는 Member를 조회할때, 매핑되어 있는 Team은 proxy객체로 조회하기 때문에 쿼리를 날리지 않고 있는다. 그러다 team객체를 조회하려고 하면 그때서야 쿼리를 날려서 정보를 가져온다.
eager는 Member를 조회할때, team도 조회하지만, 쿼리를 따로 날린다. member를 조회하고 나서 조회하다 보니 team도 있기 때문에 team에 대한 쿼리를 또 날린다.

N+1 문제라는 건 내가 날리고 싶었던 쿼리 1과 예상치 못한 쿼리 N개를 의미한다.
위에서 예를 들면, 1번째 쿼리가 1이고, 나머지 2,3,4번째 쿼리가 N인 것이다.

그렇다면 이 문제는 어떻게 해결 할 수 있을까??

이때 나오는 것이 join fetch이다.

@Test
    public void findMembers() {
        Member member1 = new Member("member1", 10);
        Member member2 = new Member("member2", 10);
        Member member3 = new Member("member3", 10);

        memberRepository.saveAll(Arrays.asList(member1, member2, member3));

        Team team1 = new Team("team1");
        Team team2 = new Team("team2");
        Team team3 = new Team("team3");

        teamRepository.saveAll(Arrays.asList(team1, team2, team3));

        member1.changeTeam(team1);
        member2.changeTeam(team2);
        member3.changeTeam(team3);
        //영속성 컨텍스트를 비워준다.
        em.flush();
        em.clear();

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

        for (Member member : members) {
            System.out.println("member.getTeam() = " + member.getTeam());
        }

    }

결과
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
inner join
team team1

on member0.team_id=team1.team_id

join fetch를 하게되면, member를 영속화 할때 team의 정보까지도 같이 영속화 된다. 따라서 쿼리를 한번만 보내게 되고, n+1 문제가 해결되는 것이다.

0개의 댓글