조인과 즉시 로딩, 지연 로딩

PPakSSam·2022년 1월 20일
0
post-thumbnail

JPA - 조인 순서


즉시 로딩과 지연로딩에 대한 개념이 부족하면 즉시 로딩과 지연 로딩을 먼저 읽는 것을 권장한다.

조인과 즉시 로딩

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "AGE")
    private int age;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;    
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    @Column(name = "NAME")
    private String name;
}

회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인

위의 경우에 다음과 같이 코드를 작성할 것이다.

Team teamA = new Team("A");
Team teamB = new Team("B");
teamRepository.saveAll(Arrays.asList(teamA, teamB));

Member member1 = new Member("member1", 20, teamA);
Member member2 = new Member("member2", 30, teamB);
memberRepository.saveAll(Arrays.asList(member1, member2));

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

String query = "select m from Member m join m.team t on t.name = 'A'";
List<Member> resultList = em.createQuery(query, Member.class).getResultList();

그리고 코드를 실행시키면 다음과 같은 쿼리가 나간다.

select
    member0_.member_id as member_i1_7_,
    member0_.age as age2_7_,
    member0_.team_id as team_id4_7_,
    member0_.username as username3_7_ 
from
    member member0_ 
inner join
    team team1_ 
        on member0_.team_id=team1_.team_id 
        and (
            team1_.name='A'
        )
----------------------------------------------------
select
    team0_.team_id as team_id1_14_0_,
    team0_.name as name2_14_0_ 
from
    team team0_ 
where
    team0_.team_id=?

쿼리를 보면 알 수 있듯이 N+1 문제가 발생하는 것을 알 수 있다.
즉시 로딩과 지연 로딩에서 즉시 로딩의 주의점으로 N+1문제가 발생할 수 있다고 했는데,
쿼리에 join이 포함되어도 여전히 N+1 문제가 발생함을 알 수 있다.

조인과 지연 로딩

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "AGE")
    private int age;

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

회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인

Team teamA = new Team("A");
Team teamB = new Team("B");
teamRepository.saveAll(Arrays.asList(teamA, teamB));

Member member1 = new Member("member1", 20, teamA);
Member member2 = new Member("member2", 30, teamB);
memberRepository.saveAll(Arrays.asList(member1, member2));

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

String query = "select m from Member m join m.team t on t.name = 'A'";
List<Member> resultList = em.createQuery(query, Member.class)
                            .getResultList(); // (1)

for (Member member : resultList) {
    System.out.println("teamName = " + member.getTeam().getName()); // (2)
}

위의 코드를 실행시키면 다음과 같은 쿼리가 나온다.

// 첫번째 쿼리
select
    member0_.member_id as member_i1_7_,
    member0_.age as age2_7_,
    member0_.team_id as team_id4_7_,
    member0_.username as username3_7_ 
from
    member member0_ 
inner join
    team team1_ 
        on member0_.team_id=team1_.team_id 
        and (
            team1_.name='A'
        )
-------------------------------------------------------
// 2번째 쿼리
select
    team0_.team_id as team_id1_14_0_,
    team0_.name as name2_14_0_ 
from
    team team0_ 
where
    team0_.team_id=?

위의 코드의 (1) 코드까지만 실행시키면 첫번째 쿼리만 나간다.
즉시 로딩의 경우 (1) 코드까지만 실행시켜도 N+1문제가 발생하는데 지연로딩은 그렇지 않다.
그러나 (2) 코드까지 실행시키면 프록시를 초기화 시켜야 하므로 2번째 쿼리가 나가게 되고,
지연 로딩 역시 N+1 문제가 발생함을 알 수 있다.

왜 이런문제가 발생하는 걸까?

아니 Member와 Team을 조인했는데 왜 Team에 대한 정보를 얻으려 또 쿼리를 날리지?

나는 위와 같이 생각했었다. 분명 조인을 하는 이유는 두 테이블의 정보를 가져오기 위해 하는건데 왜 팀의 정보를 또 얻으려 쿼리를 날리는 것인가 궁금했다.

String query = "select m from Member m join m.team t on t.name = 'A'";
List<Member> resultList = em.createQuery(query, Member.class)
                            .getResultList();

다음과 같이 코드를 작성했을 때 조인을 했으므로 Member와 Team의 정보를 모두 가져올거라 기대할 수도 있다고 생각한다.

그러나 반환타입이 Member이므로 Member에 대한 정보만 가져온다.
이는 쿼리를 보면 명확하게 확인할 수 있다.

select
    member0_.member_id as member_i1_7_,
    member0_.age as age2_7_,
    member0_.team_id as team_id4_7_,
    member0_.username as username3_7_ 
from
    member member0_ 
inner join
    team team1_ 
        on member0_.team_id=team1_.team_id 
        and (
            team1_.name='A'
        )

즉 join을 사용하였어도 Member에 대한 정보만 가져왔기 때문에 Team에 대한 정보는 없다.
따라서 Team의 정보를 요구할 때 Team의 정보를 얻는 select문이 또 나가게 되는 것이다.

N+1 문제 해결법 - fetch join의 사용

Team teamA = new Team("A");
Team teamB = new Team("B");
teamRepository.saveAll(Arrays.asList(teamA, teamB));

Member member1 = new Member("member1", 20, teamA);
Member member2 = new Member("member2", 30, teamB);
memberRepository.saveAll(Arrays.asList(member1, member2));

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

String query = "select m from Member m join fetch m.team t";
List<Member> resultList = em.createQuery(query, Member.class)
                            .getResultList(); // (1)

for (Member member : resultList) {
    System.out.println("teamName = " + member.getTeam().getName()); // (2)
}

select m from Member m join fetch m.team t 이 쿼리를 주의 깊게 보길 바란다.
join 대신에 join fetch를 사용하였다.

그리고 위의 코드를 실행하면 다음과 같은 쿼리가 나간다.

select
    member0_.member_id as member_i1_7_0_,
    team1_.team_id as team_id1_14_1_,
    member0_.age as age2_7_0_,
    member0_.team_id as team_id4_7_0_,
    member0_.username as username3_7_0_,
    team1_.name as name2_14_1_ 
from
    member member0_ 
inner join
    team team1_ 
        on member0_.team_id=team1_.team_id

쿼리가 1개만 나가게 되고 필자가 원했던 Member와 Team의 정보를 모두 가져온다!
따라서 (2) 코드를 실행할 때 이미 Team의 정보가 있으므로 쿼리가 또 나가지 않게 된다.
이렇게 fetch 조인을 사용하면 N+1문제가 해결되게 된다.

라고 할줄 알았지??

아니 이게 무슨말인가 해결된 것이 아니었던 말인가??

우리는 select m from Member m join fetch m.team t 를 원한 것이 아니었다.
팀이름이 A인 member를 조회하고 싶은 거였다. 그래서 다음과 같이 코드를 작성하였다.

String query = "select m from Member m join fetch m.team t on m.team.name = 'A'";
List<Member> resultList = em.createQuery(query, Member.class)
                            .getResultList();

그리고 실행시키면 다음과 같은 오류가 발생한다.

org.hibernate.hql.internal.ast.QuerySyntaxException: 
with-clause not allowed on fetched associations; 
use filters 
[select m from Member m join fetch m.team t on m.team.name = 'A']

이에 대한 내용은 fetch 조인 사용시 주의점에서 다루겠다.

profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글