N+1 문제는 JPA를 사용할 때 발생하는 대표적인 성능 문제입니다.
가장 흔한 예시로 게시글 목록과 조회수 데이터를 가져올 때 발생하는 상황을 예로 들어 설명하겠습니다.
게시판 시스템을 만든다고 가정합니다.
Post
엔티티: 게시글 정보를 저장합니다.View
엔티티: 각 게시글의 조회수를 저장합니다.관계: Post
와 View
는 1대1 관계입니다.
@Entity
public class Post {
@Id
@GeneratedValue
private Long id;
private String title;
private String content;
@OneToOne(mappedBy = "post", fetch = FetchType.LAZY)
private View view;
}
@Entity
public class View {
@Id
@GeneratedValue
private Long id;
private int viewCount;
@OneToOne
@JoinColumn(name = "post_id")
private Post post;
}
2. N+1 문제 발생 코드
게시글 목록을 가져오고 각 게시글의 조회수(View)를 함께 출력하려고 합니다.
List<Post> posts = em.createQuery("SELECT p FROM Post p", Post.class)
.getResultList();
for (Post post : posts) {
System.out.println("Title: " + post.getTitle() + ", 조회수: " + post.getView().getViewCount());
}
쿼리 실행 과정
첫 번째 쿼리
SELECT p FROM Post p
→ 게시글(Post) 목록을 조회합니다. (1개의 쿼리 실행)
지연 로딩(Lazy Loading)
각 게시글의 조회수를 가져오기 위해 N번의 추가 쿼리가 실행됩니다.
→ SELECT v.* FROM View v WHERE v.post_id = ?
sql
코드 복사
-- 첫 번째 쿼리: 게시글(Post) 조회
SELECT * FROM Post;
-- N번의 추가 쿼리: 각 게시글의 조회수(View)를 가져옴
SELECT * FROM View WHERE post_id = 1;
SELECT * FROM View WHERE post_id = 2;
SELECT * FROM View WHERE post_id = 3;
...
결과:
총 1 + N개의 쿼리가 실행됩니다.
게시글이 많아질수록 쿼리 횟수가 급격히 증가해 성능이 저하됩니다.
JOIN FETCH
를 사용해 게시글과 조회수를 한 번에 가져옵니다.
List<Post> posts = em.createQuery(
"SELECT p FROM Post p JOIN FETCH p.view", Post.class)
.getResultList();
for (Post post : posts) {
System.out.println("Title: " + post.getTitle() + ", 조회수: " + post.getView().getViewCount());
}
실행되는 SQL:
-- Fetch Join을 사용해 Post와 View를 한 번에 조회
SELECT p.*, v.*
FROM Post p
JOIN View v ON p.id = v.post_id;
결과:
쿼리 1번만 실행되며 N+1 문제가 해결됩니다.
@EntityGraph
사용@EntityGraph
를 설정해 Fetch Join과 같은 효과를 줍니다.
EntityGraph 설정:
@Entity
@NamedEntityGraph(name = "Post.view", attributeNodes = @NamedAttributeNode("view"))
public class Post {
@Id
@GeneratedValue
private Long id;
private String title;
private String content;
@OneToOne(mappedBy = "post", fetch = FetchType.LAZY)
private View view;
}
EntityGraph 사용:
List<Post> posts = em.createQuery("SELECT p FROM Post p", Post.class)
.setHint("javax.persistence.fetchgraph", em.getEntityGraph("Post.view"))
.getResultList();
for (Post post : posts) {
System.out.println("Title: " + post.getTitle() + ", 조회수: " + post.getView().getViewCount());
}
실행되는 SQL:
-- Fetch Join을 사용해 Post와 View를 한 번에 조회
SELECT p.*, v.*
FROM Post p
JOIN View v ON p.id = v.post_id;
결과:
쿼리 1번만 실행되며 N+1 문제가 해결됩니다.
방법 | 설명 | 장점 |
---|---|---|
Fetch Join | JPQL에 JOIN FETCH 를 추가 | 간단한 쿼리 최적화 |
@EntityGraph | 애너테이션 기반으로 Fetch Join 설정 | 코드 재사용 및 유지보수 용이 |
N+1 문제는 게시판에서 게시글 조회와 조회수 조회와 같은 상황에서 자주 발생합니다.
@EntityGraph
를 사용하면 1번의 쿼리로 모든 데이터를 효율적으로 가져올 수 있습니다.1:1 관계에서 별도의 테이블을 사용하는 것이 직관적으로 이해되지 않을 수 있습니다. Post 테이블에 조회수 칼럼을 추가하는 것이 더 간단해 보이기 때문입니다. 하지만 이렇게 별도의 테이블을 사용하는 데에는 다음과 같은 이유와 장점이 있습니다.
결론적으로, 1:1 관계에서 별도의 테이블을 사용하는 것은 단순히 데이터를 분리하는 것을 넘어, 데이터 모델의 유연성, 확장성, 성능 향상 등 다양한 이점을 제공합니다. 물론, 모든 경우에 별도의 테이블이 항상 최선의 선택은 아닙니다. 시스템의 요구사항과 특성을 고려하여 적절한 데이터 모델을 설계해야 합니다.
다음과 같은 경우 별도의 테이블을 사용하는 것이 유리합니다.
반대로, 다음과 같은 경우 Post 테이블에 조회수 칼럼을 추가하는 것이 간단할 수 있습니다.