
현재 프로젝트에는 사람과 부두에 대한 테이블이 있다. 각 사람에게 출입가능한 부두를 나타내기 위해 Person과 Wharf는 다대다 관계이고 이를 연결 하기 위해 PersonWharf 테이블을 구축한 상태이다.
※ 필요한 필드값만 표시
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long personId;
private String name;
@OneToMany(mappedBy = "person", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Permission> permissionList = new ArrayList<>();
}
@Entity
public class Wharf {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long wharfId;
private String name;
@OneToMany(mappedBy = "wharf", cascade = CascadeType.ALL)
private List<Permission> permissionList = new ArrayList<>();
}
@Entity
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long permissionId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "personId")
private Person person;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "wharfId")
private Wharf wharf;
}
우선 fetch = FetchType.LAZY의 의미 부터 알아보자.
fetch = FetchType.LAZY은 이 값을 지연로딩을 통해 가져온다는 것을 의미한다. 반대의 개념은 fetch = FetchType.Eager 즉시로딩이 있다.
지연로딩으로 설정을 하면 해당 필드값이 호출되기 전까지는 객체를 가져오지 않는다.
예를 들어 person에 값을 가져오기 위해 personRepository.findById()를 호출했다고 하자.
그러면 select 쿼리문이 나가면서 person값을 가져올 것 이다. 하지만 DB 연관이 없는 personId 값은 가져오지만 FetchType.LAZY로 설정되어있는 permissionList 가져오지 않고 우선 proxy객체로 대체하고 추후에 이 객체값이 필요할때 가져온다.
지연로딩을 하면 필요없는 값을 가져오는것을 사전에 방지하는 효과가 있다.
우리가 데이터를 가져올때 항상 그와 연관된 데이터 모두가 필요한것은 아니다.
예를들어 person 객체를 가져올떄 그 사람이 name값만 필요할 수도 있다. 이런 상황에서 person과 연관된 모든 데이터를 한번에 가져오면 성능이 매우 떨어지게된다.
하지만 person에 연관된 데이터도 가져오고 싶을때 문제가 생긴다.
만약 우리가 person을 통해 person과 연관있는 wharf의 name까지 알고 싶다고 하자. 이 때 JPA의 순서를 나타내면 이렇다.
person 객체를 가져올떄 select 쿼리문 한번 호출
permission를 가져올때 permissionList의 길이만 큼 select 쿼리문 N번 호출
->지연 로딩이니 1번에선 permissionList는 proxy 객체 였다.
wharf를 가져올때 permissionList의 길이만 큼 select 쿼리문 N번 호출
->지연 로딩이니 2번에선 wharf는 proxy 객체 였다.
이렇게 쿼리문이 많이 나가는 현상이 발생한다. 이 문제를 N+1 문제 라고 한다.
1번의 명령만 내렸는데 연쇄적으로 N번의 쿼리가 나가는 현상이다.
현재 데이터는
person1 -> wharf2,wharf3
person2 -> wharf1
으로 되어있다.
모든 person을 조회하고 해당 person에 연결된 wharf에 name값을 가져오는 코드를 실행한다.
//모든 Person 조회
List<Person> personList = personResoitory.findAll();
//person 연결된 Permission 객체를 가져오고 Permission객체와 연관된 Wharf의 name을 가져온다.
personList.stream().map(Permission::getWharf).map(Wharf::getName).collect(Collectors.toList());
실행 쿼리
//person 조회
2023-12-16T17:32:26.788+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL :
select
w1_0.person_id,
w1_1.birth,
w1_1.name,
w1_1.nationality,
w1_1.person_id,
w1_1.phone,
w1_1.sex,
w1_0.account_id,
w1_0.face_url,
w1_0.position
from
worker w1_0
join
person w1_1
on w1_0.person_id=w1_1.person_id
//Permission 조회
2023-12-16T17:32:26.796+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL
select
p1_0.person_id,
p1_0.permission_id,
p1_0.wharf_id
from
permission p1_0
where
p1_0.person_id=?
//Wharf 조회
2023-12-16T17:32:26.811+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL :
select
w1_0.wharf_id,
w1_0.name
from
wharf w1_0
where
w1_0.wharf_id=?
2023-12-16T17:32:26.815+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL :
select
w1_0.wharf_id,
w1_0.name
from
wharf w1_0
where
w1_0.wharf_id=?
//Permission 조회
2023-12-16T17:32:26.818+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL :
select
p1_0.person_id,
p1_0.permission_id,
p1_0.wharf_id
from
permission p1_0
where
p1_0.person_id=?
//Wharf 조회
2023-12-16T17:32:26.821+09:00 DEBUG 21428 --- [nio-8080-exec-1] org.hibernate.SQL :
select
w1_0.wharf_id,
w1_0.name
from
wharf w1_0
where
w1_0.wharf_id=?
select person 1번
person 객체 2개 생성
select Permission 2번
Permission 객체 3개 생성
select Wharf 3번
1+2+3 총 6번의 쿼리가 발생
//모든 Person 조회
@Query("SELECT p FROM Person p JOIN FETCH p.permissionList per JOIN FETCH per.wharf")
List<Person> findAllPersonWithWharf();
//모든 person 조회
List<Person> personList = personRepository.findAllPersonWithWharf();
//위와 동일
personList.stream().map(Permission::getWharf).map(Wharf::getName).collect(Collectors.toList());
//한번의 쿼리만 실행
2023-12-16T21:30:55.176+09:00 DEBUG 20324 --- [nio-8080-exec-2] org.hibernate.SQL :
select
w1_0.person_id,
w1_1.birth,
w1_1.name,
w1_1.nationality,
w1_1.person_id,
p1_0.person_id,
p1_0.permission_id,
w2_0.wharf_id,
w2_0.name,
w1_1.phone,
w1_1.sex,
w1_0.account_id,
w1_0.face_url,
w1_0.position
from
worker w1_0
join
person w1_1
on w1_0.person_id=w1_1.person_id
join
permission p1_0
on w1_1.person_id=p1_0.person_id
join
wharf w2_0
on w2_0.wharf_id=p1_0.wharf_id
join을 이용하여 쿼리를 작성하면 데이터는 가져오지만 영속성 컨텍스트에 들어가진 않는다.