즉시 로딩과 지연로딩에 대한 개념이 부족하면 즉시 로딩과 지연 로딩을 먼저 읽는 것을 권장한다.
@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문이 또 나가게 되는 것이다.
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 조인 사용시 주의점에서 다루겠다.