public class Team {
public Team(Long id, String name) {
this.id = id;
this.name = name;
}
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
여기 팀이 있다 팀은 일대다로 멤버와 연관되어 있다.
public class Member {
public Member (Long id, String username, Team team) {
this.id = id;
this.username = username;
this.team = team;
}
@Id private Long id;
private String username;
@ManyToOne @JoinColumn(name = "TEAM_ID")
private Team team;
}
멤버는 다대일로 팀과 연관되어있다.
public class TeamService {
private final TeamRepository teamRepository;
@Transactional
public Page<Team> getTeam (int offset, int limit) {
Pageable pageable = PageRequest.of(offset, limit);
return teamRepository.getTeam(pageable);
}
}
팀 서비스에서는 오프셋과 리미트가 있다.
public interface TeamRepository extends JpaRepository<Team, Long> {
@Query("SELECT t FROM Team t JOIN FETCH t.members")
Page<Team> getTeam(Pageable pageable);
}
팀 레포지터리에서는 팀과 팀의 구성원을 패치조인하여 페이징한다.
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(TeamService.class)
public class CollectionFetchJoin {
@Autowired
private TeamService teamService;
@Test
public void collectionFetchJoin() {
Page<Team> team = teamService.getTeam(1, 2);
team.getContent().forEach(t -> {
System.out.println("팀명: " + t.getName() + " | 멤버 수: " + t.getMembers().size());
t.getMembers().forEach(m -> System.out.println(" -> 멤버명: " + m.getUsername()));
});
}
}
DB에 데이터를 삽입하고 테스트를 시행한다.
페이징은 리턴타입을 기준으로한다. 오프셋1 리미트2면 팀1, 팀2가 지나간 팀3, 팀4가 반환될 것이다.
2026-02-27T17:44:25.847+09:00 WARN 7924 --- [jpatest] [ main]
org.hibernate.orm.query: HHH90003004: firstResult/maxResults specified with collection fetch;
applying in memory
경고 알림이 뜬다.
[Hibernate]
select
t1_0.id,
m1_0.team_id,
m1_0.id,
m1_0.username,
t1_0.name
from
team t1_0
join
member m1_0
on t1_0.id=m1_0.team_id
[Hibernate]
select
count(t1_0.id)
from
team t1_0
join
member m1_0
on t1_0.id=m1_0.team_id
패치조인이 발생했다. 그런데 쿼리를 잘보자 오프셋과 리미트가 없다. 팀과 회원을 조인한 모든 것을 메모리에 올리고 있다.
팀명: 팀3 | 멤버 수: 20
-> 멤버명: 회원41
-> 멤버명: 회원42
-> 멤버명: 회원43
-> 멤버명: 회원44
-> 멤버명: 회원45
-> 멤버명: 회원46
-> 멤버명: 회원47
-> 멤버명: 회원48
-> 멤버명: 회원49
-> 멤버명: 회원50
-> 멤버명: 회원51
-> 멤버명: 회원52
-> 멤버명: 회원53
-> 멤버명: 회원54
-> 멤버명: 회원55
-> 멤버명: 회원56
-> 멤버명: 회원57
-> 멤버명: 회원58
-> 멤버명: 회원59
-> 멤버명: 회원60
팀명: 팀4 | 멤버 수: 20
-> 멤버명: 회원61
-> 멤버명: 회원62
-> 멤버명: 회원63
-> 멤버명: 회원64
-> 멤버명: 회원65
-> 멤버명: 회원66
-> 멤버명: 회원67
-> 멤버명: 회원68
-> 멤버명: 회원69
-> 멤버명: 회원70
-> 멤버명: 회원71
-> 멤버명: 회원72
-> 멤버명: 회원73
-> 멤버명: 회원74
-> 멤버명: 회원75
-> 멤버명: 회원76
-> 멤버명: 회원77
-> 멤버명: 회원78
-> 멤버명: 회원79
-> 멤버명: 회원80
결과는 잘 나왔다 그런데 이것은 기존에 회원이 100,명 팀이 5개가 있는데 그것을 메모리에 올리고 자른 것이다.
오프셋과 리미트가 SQL에 있으면 디비에서 그것을 처리하지만 일대다 페이징을 페치조인으로 처리하는 경우에 그것을 메모리에서 처리한다.
이러한 이유는 일대다 같은 경우는 오프셋과 리미트가 몇개로 떨어질지 모르기 때문이다. 나는 팀마다 20명씩 넣어놨지만 사실 첫번째 팀은 5명이고 두번째 팀은 10명이고 이런것을 쿼리를 실행할때부터 알 수가 없다. 그래서 JPA는 전부 다 올려보고 자르는 방식을 쓰는 것이다.
나는 DB에 데이터를 100개만 넣어놨지만 수백만 수천만개가 한번에 메모리에 올라오게 되면 시스템이 붕괴될 것이다.
'데이터베이스 > JPA' 카테고리의 다른 글
| 지연로딩 vs 즉시로딩 vs 패치조인 vs JPQL일반조인 vs 일반조인 (0) | 2026.01.12 |
|---|---|
| 세션과 커넥션 (1) | 2026.01.09 |
| 스레드로컬과 세션 (0) | 2026.01.09 |