N+1에 대해서 바로 정의를 해보자면,
1개의 query가 수행되길 기대했는데, N개의 추가 쿼리가 발생한 현상을 의미한다.
이렇게 말하면 와닿지 않을 것 같다.🫠
이해를 돕기 위해서 Post와 Comment의 연관관계를 통해서 N+1에 대해서 살펴보고자 한다.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Setter
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Lob
private String content;
private String title;
// @OneToMany의 경우 기본 fetch type은 지연 로딩이지만, N+1문제를 바로 테스트 해보기 위해서 즉시 로딩으로 설정했다.
@OneToMany(mappedBy = "post", fetch = FetchType.EAGER)
private List<Comment> comments = new ArrayList<>();
public Post(String content, String title) {
this.content = content;
this.title = title;
}
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Setter
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Lob
private String content;
public Comment(String content) {
this.content = content;
}
@ManyToOne
@JoinColumn(name = "post_id")
private Post post;
@Override
public String toString() {
return "Comment{" +
"id=" + id +
", content='" + content + '\'' +
'}';
}
}
하나의 게시글(Post)은 여러 개의 댓글(Comment)을 가질 수 있다.
테스트를 위해서 게시글은 총 3개, 각 게시글마다 1개의 댓글을 준비했다.
N+1을 확인하기 위해 모든 게시글을 가져오는 상황을 생각해 보자.🧐
테스트 코드는 아래와 같다.
@SpringBootTest
@Transactional
@Rollback(value = false)
class PostTest {
@Autowired
EntityManager em;
@Autowired
private PostRepository postRepository;
@Autowired
private CommentRepository commentRepository;
@Test
void checkNPlusOneProblem() throws Exception {
// 게시글 총 3개
for (int i = 0; i < 3; i++) {
Post createdPost = postRepository.save(new Post("content_" + i, "title_" + i));
// 각 게시글 당 1개의 댓글 생성
commentRepository.save(new Comment("content_" + i, createdPost));
}
em.flush();
em.clear();
System.out.println("=== START ===");
// 모든 post 조회
List<Post> posts = postRepository.findAll();
System.out.println("=== END ===");
}
}
테스트 코드를 실행했을 때, 결과는 아래와 같다.

Post 전체 개수는 3개이고 각 Post마다 Comment가 1개씩 존재하는 상황에서 출력이 총 4번 발생한다.
Post 전체 개수 조회 쿼리(1번) + Post 전체 개수만큼 Comment 조회 쿼리 발생(N번)
조금 더 구체적으로 상황을 드려다 보자.
즉시로딩으로 설정했기 때문이라고 생각할 수 있지만 지연로딩일 경우에도 발생할 수 있다.
상단의 Post를 지연로딩으로 변경하자.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Setter
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Lob
private String content;
private String title;
// 지연 로딩 적용
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
private List<Comment> comments = new ArrayList<>();
public Post(String content, String title) {
this.content = content;
this.title = title;
}
}
변경한 이후에 동일한 테스트 코드를 실행시켜보자.
결과는 아래와 같다.

지연로딩을 설정해서 해결된거 아닌가? 연관관계인 Comment에 접근하지 않는다면 발생하지 않는다.
(정확히는 getComments() method까지는 발생하지 않는다. getComments() method 이후에 각 comment의 필드나, comments의 size() 등에 접근할 경우 N+1이 발생한다.)
아래의 테스트 코드로 수정하면 동일한 N+1이 발생한다.
@SpringBootTest
@Transactional
@Rollback(value = false)
class PostTest {
@Autowired
EntityManager em;
@Autowired
private PostRepository postRepository;
@Autowired
private CommentRepository commentRepository;
@Test
void checkNPlusOneProblem() throws Exception {
// 게시글 총 3개
for (int i = 0; i < 3; i++) {
Post createdPost = postRepository.save(new Post("content_" + i, "title_" + i));
// 각 게시글 당 1개의 댓글 생성
commentRepository.save(new Comment("content_" + i, createdPost));
}
em.flush();
em.clear();
System.out.println("=== START ===");
List<Post> posts = postRepository.findAll();
for (Post post : posts) {
// post의 연관관계인 comment의 size() method에 접근
post.getComments().size();
}
System.out.println("=== END ===");
}
}

정의를 다시 한 번 체크해보자.
1개의 Query로 수행되길 기대했는데 N개의 추가 쿼리가 발생한 현상
사실 전체 게시글을 조회해 달라는 요구 사항에 한 번의 쿼리만 수행한다고 예상하고 전체 게시글을 조회했을 것이다. 하지만 연관관계로 매핑이 되어 있던 Comment가
전체 Post 개수만큼 호출 되었다. 이와 같은 문제를 N+1이라고 한다.
그렇다면 N+1은 왜 발생할까?
기본적으로 JPA에서는 엔터티 조회 시 엔터티 자체만 가져오기 때문이다.
(하위 엔터티에 대해서 한 번에 가져오지 않는다.)
하위 엔터티(Comment)에 대해서 데이터를 어떻게 가져올지에 대한 방식은 2가지 존재한다.
1. 즉시 로딩(FetchType.EAGER)
2. 지연 로딩(FetchType.LAZY)
즉시 로딩이라고 하더라도 데이터를 하위 엔터티까지 한 번에 가져오는 것이 아니라, 상위 엔터티 조회 이후에, 즉시 로딩임을 인지하고 추가 쿼리가 발생하게 된다.
지연 로딩은 하위 엔터티의 필드 혹은 하위 엔터티의 size() method 같이 직접 접근할 경우에 추가 쿼리가 발생하게 된다.
Post와 Comment의 연관 관계를 통해서 N+1이 무엇이고 왜 발생하는 지에 대해서 살펴봤다.
연관관계가 없는 엔터티를 조회한다면 N+1문제는 절대 발생하지 않는다.
연관 관계를 가진 Entity를 조회할 때 N+1이 발생할 수 있고, 엔터티 조회 시 기본적으로 엔터티 자체만 가져오기 때문에 발생한다는 사실을 인지할 필요가 있다.
다음 포스팅에서는 N+1 해결책에 대해서 상세히 알아보자.