https://velog.io/@hyeonjoonpark/Spring-Boot-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91
이 블로그에서 연관관계 매핑에 대해서 설명하다가 N+1문제가 잠깐 언급되었는데 이번 글에서는 N+1 문제에 관해 조금 더 자세히 설명해보고자 한다.
https://www.youtube.com/watch?v=ni92wUkAmQI
유튜브 우아한테크 수달님의 내용을 바탕으로 공부 한 내용을 정리해보았다
요청이 1개의 쿼리로 처리되길 기대했는데 N개의 추가 쿼리가 발생하는 현상
N+1 은 다양한 상황에서 발생되지만 @OneToMany
일대다 관계에서의 N+1에 대해서 알아보자
지연로딩
: 엔티티를 조회할 시 데이터를 사용할 때까지 로딩을 미루는 현상
@Getter
@Entity
@ToString
@Table(name = "crew")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Crew {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "crew_id")
private Long id;
@OneToMany(mappedBy = "crew")
private List<Todo> todoList;
}
@Getter
@ToString
@Entity
@Table(name = "todo")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "todo_id")
private Long id;
private String content; // 내용
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "crew_id")
private Crew crew;
}
다음과 같은 두개의 엔티티가 있다
하나의 크루에는 여러가지 할 일이 있고, 하나의 할 일은 하나의 크루 안에 속하기 때문에 이 두 엔티티의 연관관계는 1(크루):N(할 일)이다
그렇다면 CrewRepository.findAll
를 사용하여 데이터를 조회해보겠다
select * from crew
이렇게 요청을 보내면
private List<Todo> todoList;
이 할일 목록 리스트는 지연로딩으로 설정이 되어있기 때문에 proxy 객체로 가지고 있다.
-> 이때까지는 쿼리가 한번만 나감
크루 한명당 몇개의 할일이 있는지 조회를 할려고 하면
for(Crew crew : Crews) {
crew.todoList(proxy).getSize();
}
현재 지연로딩이기 때문에 proxy 객체이기 때문에 1차 캐시에서 crew_id가 1인 목록 있는지 조회를 한다
근데 없어서
select * from todo where todo_id = 1
다음과 같이 쿼리를 만들어서 데이터를 가져온다
두번째 데이터도
select * from todo where todo_id = 2
세번째 데이터도...
select * from todo where todo_id = 3
...
...
...
이렇게 해서 실행을 하면
이렇게 각각의 크루구성원의 할일 목록까지 쿼리를 만들어서 조회한다
만약 크루가 100만명이다...
select * from todo where todo_id = 1
--- (100만번 쿼리) ---
select * from todo where todo_id = 1000000
이렇게 하면 처음 크루만 전부조회하는 쿼리를 포함해서 N+1번의 쿼리가 만들어지는 것이다
엄청나게 비효율적이다
select crew.*, todo.* from crew join fetch todo
findAll() 을 할 때 이렇게 보내버리면 proxy 객체가 아닌 진짜 객체이기 때문에 DB를 거치지 않는다
JPA가 연관된 엔티티를 찾고 패치 전략이 뭔지 확인을 하면 데이터를 바로 조회해온다
조회해올 때 N번의 추가쿼리가 발생해온다
즉시로딩은 사용하지 않는 것을 권장한다
그냥 지연로딩 + Fetch Join
을 사용하자
대부분의 경우 이렇게 Fetch Join을 사용하여 해결하면 되지만 Fetch Join도 문제점이 있다
대표적인 상황이 페이징 처리 할 때이다
이 Fetch Join 문제점 해결방안으로
@ManyToOne
, @BatchSize()
가 있다
이 두가지에 대해서는 다음에 시간날 때 한번 작성해보도록 하겠다