JPA를 활용하면서 테이블 간의 연관관계(1:N, N:1, 1:1)가 맺어져 있는 경우, 한 번 조회가 발생할 때 연관되어 있는 테이블에서 데이터들을 또 조회해 연관정보를 채우게 된다.
즉, 1개의 쿼리를 조회하기 위해 연관된 데이터를 위한 N개의 쿼리가 추가로 발생하는 문제로 정의할 수 있다.
이 때, 연관관계를 맺으면서 데이터를 바로가져와 매핑하는가? 나중에 해당 정보를 사용하는 경우 쿼리를 발생시켜 매핑하는가? 에 따라 전략을 정할 수 있는데 이를 fetchType
이라고 한다.
fetchType에는 2가지가 존재하는데 fetchType.EAGER
, fetchType.LAZY
가 존재한다.
JPA에서는 기본적으로 fetchType.LAZY
의 전략이 기본으로 설정되어 있다.
(N+1 문제가 발생할 것을 우려해서)
Member Class - 유저
// 어노테이션 생략
Public Class Member{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
Post Class - 게시글
// 어노테이션 생략
Public Class Member{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@Lob
private String content;
@ManyToOne(fetch = fetchType.EAGER)
@JoinColumn(name = "userId")
private Member member;
}
게시글(Post)와 유저(Member)는 N:1의 연관관계를 맺고 있고 현재는 예시를 위해 fetchType.EAGER
의 전략을 사용하고 있다.
이 때 게시글을 조회하는 경우, 유저의 정보를 매핑하기 위해 유저를 조회하는 쿼리가 추가로 발생해 게시글 조회 + 유저 조회 의 두 번의 조회가 발생한다.
즉, 게시글을 조회하기 위한 의도에서 추가적인 유저 쿼리가 발생하는 것이다.
따라서 필요한 경우 데이터를 조회하는 전략인 fetchType.LAZY
를 기본으로 사용하면 예기치 못한 N+1 문제를 피할 수 있다.
Member Class - 유저
// 어노테이션 생략
Public Class Member{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
Post Class - 게시글
// 어노테이션 생략
Public Class Member{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@Lob
private String content;
@ManyToOne(fetch = fetchType.LAZY)
@JoinColumn(name = "userId")
private Member member;
}
그렇다면 과연 fetchType.LAZY
의 경우는 N+1을 막을 수 있는 것인가?
생각해보면 그렇지도 않다.
예를 들어 [제목]-[작성자] 와 같이 게시글 목록을 뿌리면서 작성자의 이름을 적는 즉 유저의 정보가 필요한 경우를 생각해보면 쉽게 알 수 있다.
모든 게시글 N개를 조회해와 목록을 뿌리면서 결과적으로 유저의 정보를 N번 사용하게 된다. 이 때 추가적인 N개의 쿼리가 발생하게 된다.
이렇게 되면 결국 N+1을 피할 수 없게 된다.
JPA에서는 N+1의 문제로 인해 기본 전략을 fetchType.LAZY
로 사용하지만 이 마저도 N+1 문제를 피할 수 없다.
그렇다면 이를 해결할 수 있는 방법은 무엇인가?
기본전략인 fetchType.LAZY
를 기본으로 사용하되 연관관계의 데이터가 바로 필요한 경우 JPQL의 fetch join 키워드를 활용해 조회 한번에 필요한 데이터를 함께 불러오는 것이다.
하지만 fetch join을 사용할 때에도 고려사항이 몇 가지 있기에 fetch join에 대한 공부는 추가로 더 필요할 것 같다.