경로 표현식과 페치 조인에 대해 자세히 알아보자
: .(점)을 찍어 객체 그래프를 탐색하는 것
select m.username // 상태 필드
from Member m
join m.team t // 단일 값 연관 필드
join m.orders o // 컬렉션 값 연관 필드
where t.name = '팀A'
@ManyToOne, @OneToOne, 대상이 엔티티 (ex: m.team)@OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)※ 명시적 조인: join 키워드 직접 사용
select m from Member m join m.team t
※ 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)
select m.team from Member m
select o.member.team from Order o - 성공, 묵시적 조인 2회 발생select t.members from Team -> 성공, 컬렉션이 경로 탐색의 끝select t.members.username from Team t -> 실패, 컬렉션이 경로 탐색의 끝이기 때문select m.username from Team t join t.members m -> 성공, 명시적 조인으로 컬렉션을 가져옴❗️ 가급적 묵시적 조인 대신에 명시적 조인 사용
조인은 SQL 튜닝에 중요 포인트!
묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움 😭
join fetch 명령어 사용 - [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로select m from Member m join fetch m.teamSELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.IDTeam teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("member3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "select m from Member m";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
for (Member member : result) {
System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}
실행 결과
Hibernate:
/* select
m
from
Member m */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id4_0_,
member0_.username as username3_0_
from
Member member0_
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
member = member1, teamA // SQL 조회
member = member2, teamA // 1차 캐시
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
member = member3, teamB // SQL 조회
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for (Member member : members) {
// 페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 X
System.out.println("username = " + member.getUsername() + ", " +
"teamName = " + member.getTeam().name());
}
실행 결과
Hibernate:
/* select
m
from
Member m
join
fetch m.team */ select
member0_.id as id1_0_0_,
team1_.id as id1_3_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_3_1_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
member = member1, teamA
member = member2, teamA
member = member3, teamB

select t from Team t join fetch t.members where t.name = ‘팀A’SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = ‘팀A’String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
for(Team team : teams) {
System.out.println("teamname = " + team.getName() + ", team = " + team);
for (Member member : team.getMembers()) {
// 페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
System.out.println(“-> username = " + member.getUsername()+ ", member = " + member);
}
}
실행 결과
Hibernate:
/* select
m
from
Member m
join
fetch m.team */ select
member0_.id as id1_0_0_,
team1_.id as id1_3_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_3_1_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
❗️ 중복되는 결과 발생
조인하면서 데이터가 뻥튀기 됨 (일대다 조인 시 발생)



DISTINCT는 중복된 결과를 제거하는 명령select distinct t from Team t join fetch t.members where t.name = ‘팀A’DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과에서 중복제거 실패

DISTINCT 적용 후 실행 결과
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
select t from Team t join t.members m where t.name = ‘팀A’SELECT T.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = ‘팀A’select t from Team t join fetch t.members where t.name = ‘팀A’SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = ‘팀A’객체 그래프 사상에 맞지 않음
객체 그래프는 기본적으로 데이터를 모두 조회함. 그런데 컬렉션의 일부만 조회될 시 객체 관점에서 문제가 발생할 수 있음
-> fetch join을 사용하지 않고, 일부만 조회할 엔티티를 대상으로 쿼리를 날려야 함!
@BatchSize 사용 또는 batch size 글로벌 세팅 변경 (컬렉션 페치조인은 페이징 해결이 안되니까)@BatchSize는 N+1 문제의 또 다른 해결 방안이기도 함!
@OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략