개발
home
🎁

Spring JPA에서 N+1 문제 해결 방법 (fetch join, batch size, entity graph, querydsl)

Created
2023/04/19
Tags
JPA
N+1 문제
fetch join
batch size
entity graph
QueryDSL
2023-04-19 @이영훈

N+1 문제란

N+1 문제는 데이터베이스와 연관된 애플리케이션에서 발생하는 성능 문제 중 하나입니다.
이 문제는 한 개의 쿼리로 가져올 수 있는 데이터를 여러 번의 쿼리로 가져오게 되면서 발생합니다.
특히 객체 관계 매핑(Object-Relational Mapping, ORM) 기술을 사용하는 경우 더 자주 발생할 수 있습니다.

N+1 문제가 발생하는 상황

예를 들어, 게시글(Post)과 댓글(Comment)을 조회하는 웹 애플리케이션이 있다고 가정합니다.
Post와 Comment의 Entity입니다.
@Entity class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null var title: String? = null @OneToMany(mappedBy = "post", fetch = FetchType.LAZY) var comments: MutableList<Comment> = ArrayList() } @Entity class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null var content: String? = null @ManyToOne(fetch = FetchType.LAZY) var post: Post? = null }
Kotlin
복사
Reository와 Service 로직입니다.
@Repository interface PostRepository : JpaRepository<Post, Long> @Service class PostService(private val postRepository: PostRepository) { @Transactional(readOnly = true) fun findAllPostsWithComments(): List<Post> { val posts = postRepository.findAll() // N+1 문제 발생 posts.forEach { post -> val comments = post.comments println("게시글: ${post.title}") comments.forEach { comment -> println("댓글: ${comment.content}") } } return posts } }
Kotlin
복사
위 코드에서 N+1 문제가 발생할 수 있습니다. 예를 들어, 100개의 게시글이 있다고 가정하면, PostRepository의 findAll() 메서드가 호출될 때 모든 게시글을 조회합니다.
이때, 각 게시글의 댓글을 조회하기 위해 추가로 100번의 쿼리가 실행됩니다. 이는 총 101번의 쿼리 실행으로 이어지며, 매우 비효율적인 방법입니다.
즉, 다음과 같이 SQL이 실행되면서 N+1 문제가 발생한 것을 알 수 있습니다.
SELECT * FROM post;
SQL
복사
SELECT * FROM comment WHERE post_id = 1; SELECT * FROM comment WHERE post_id = 2; SELECT * FROM comment WHERE post_id = 3; ... SELECT * FROM comment WHERE post_id = 99; SELECT * FROM comment WHERE post_id = 100;
SQL
복사

N+1 문제를 해결하는 방법

1.
Fetch Join: 연관된 엔티티를 쿼리 시 함께 조회하는 방법입니다. JPQL(Java Persistence Query Language)이나 Criteria API를 사용하여 작성할 수 있습니다.
예를 들어, JPQL에서는 다음과 같이 작성할 수 있습니다.
val jpql = "SELECT p FROM Post p JOIN FETCH p.comments" // ✅ fetch join 사용 val postsWithComments = entityManager.createQuery(jpql, Post::class.java).resultList
Kotlin
복사
2.
Batch Size: @BatchSize 어노테이션을 사용하여 한 번의 쿼리로 여러 개의 연관된 엔티티를 조회하는 방법입니다. 이를 통해 쿼리 수를 줄일 수 있습니다.
예를 들어, Post 엔티티에서 comments에 @BatchSize를 설정하면 다음과 같습니다.
@Entity class Post( // ... 생략 ... @BatchSize(size = 10) @OneToMany(mappedBy = "post") val comments: MutableList<Comment> = ArrayList() )
Kotlin
복사
@BatchSize 는 지정된 사이즈만큼 데이터를 한번에 조회합니다. 예를 들어, @BatchSize(10) 이 지정된 경우, 10개의 데이터를 한번에 조회하여 성능 향상을 기대할 수 있습니다.
다음과 같이 IN 절로 한 번에 여러 게시글의 댓글들을 가져오는 것을 확인할 수 있습니다.
SELECT * FROM post; SELECT * FROM comment WHERE post_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
SQL
복사
3.
Entity Graph: 그래프 기능을 사용하여 필요한 연관된 엔티티를 한 번의 쿼리로 조회하는 방법입니다.
예를 들어, @NamedEntityGraph를 사용하여 Post와 Comment를 함께 조회하는 방법은 다음과 같습니다.
@Entity @NamedEntityGraph( name = "post-with-comments", attributeNodes = [NamedAttributeNode("comments")] ) class Post( // ... 생략 ... @OneToMany(mappedBy = "post") val comments: MutableList<Comment> = ArrayList() )
Kotlin
복사
그런 다음, post-with-comments를 사용하여 게시글과 댓글을 함께 조회할 수 있습니다.
val entityGraph = entityManager.getEntityGraph("post-with-comments") val postsWithComments = entityManager.createQuery("SELECT p FROM Post p", Post::class.java) .setHint("javax.persistence.fetchgraph", entityGraph) .resultList
Kotlin
복사
4.
Querydsl: Querydsl은 타입 세이프한 쿼리를 작성할 수 있는 프레임워크로, JPA와 함께 사용할 수 있습니다. 이를 이용해 조인을 사용하여 연관된 엔티티를 함께 조회하는 방법입니다.
예를 들어, 게시글과 댓글을 함께 조회하는 Querydsl 쿼리를 작성하면 다음과 같습니다.
import com.querydsl.jpa.impl.JPAQueryFactory @Service class PostService( private val queryFactory: JPAQueryFactory ) { @Transactional(readOnly = true) fun findAllPostsWithComments(): List<Post> { val QPost = QPost.post val QComment = QComment.comment val postsWithComments = queryFactory .selectFrom(QPost) .leftJoin(QPost.comments, QComment).fetchJoin() // ✅ fetch join 사용 .fetch() // ... 생략 ... return postsWithComments } }
Kotlin
복사