개발기록
N+1 문제와 해결 방법: Kotlin과 Spring Data JPA 본문
N+1 문제란?
N+1 문제는 ORM(Object-Relational Mapping)을 사용할 때 자주 발생하는 성능 이슈입니다. 이 문제는 데이터베이스에서 데이터를 조회할 때, 의도치 않게 많은 수의 추가 쿼리가 발생하는 현상을 말합니다.
- N+1 이름의 유래:
- 1: 부모 엔티티를 조회하는 첫 번째 쿼리
- N: 각 부모 엔티티와 연관된 자식 엔티티를 조회하는 N번의 추가 쿼리
이 문제는 애플리케이션의 성능을 크게 저하시키고, 데이터베이스에 불필요한 부하를 줄 수 있습니다.
N+1 문제 예시
게시글(Post)과 댓글(Comment) 엔티티가 있는 블로그 시스템을 예로 들어보겠습니다.
@Entity
class Post(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
val title: String,
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
val comments: List<Comment> = emptyList()
)
@Entity
class Comment(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
val content: String,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
val post: Post
)
interface PostRepository : JpaRepository<Post, Long>
@Service
class PostService(private val postRepository: PostRepository) {
fun findAllPostsWithComments(): List<Post> {
val posts = postRepository.findAll()
posts.forEach { post ->
post.comments.size // 이 부분에서 N+1 문제 발생
}
return posts
}
}
이 코드에서 findAllPostsWithComments() 메소드를 호출하면 N+1 문제가 발생합니다:
postRepository.findAll()로 모든 게시글을 가져옵니다. (1번의 쿼리)- 각 게시글에 대해
post.comments.size를 호출할 때, 해당 게시글의 댓글을 로딩하기 위해 추가 쿼리가 발생합니다. (N번의 쿼리)
N+1 문제 해결 방법
Fetch Join 사용
Fetch Join을 사용하면 연관 엔티티를 함께 조회할 수 있습니다.
interface PostRepository : JpaRepository<Post, Long> {
@Query("SELECT p FROM Post p LEFT JOIN FETCH p.comments")
fun findAllWithComments(): List<Post>
}
@Service
class PostService(private val postRepository: PostRepository) {
fun findAllPostsWithComments(): List<Post> {
return postRepository.findAllWithComments()
}
}
이 방법은 한 번의 쿼리로 모든 게시글과 그에 연관된 댓글을 함께 가져옵니다.
@EntityGraph 사용
@EntityGraph를 사용하여 연관 엔티티를 함께 로딩하도록 JPA에 지시할 수 있습니다.
interface PostRepository : JpaRepository<Post, Long> {
@EntityGraph(attributePaths = ["comments"])
override fun findAll(): List<Post>
}
@Service
class PostService(private val postRepository: PostRepository) {
fun findAllPostsWithComments(): List<Post> {
return postRepository.findAll()
}
}
배치 사이즈 설정
@BatchSize를 사용하여 지정된 크기만큼의 연관 엔티티를 한 번에 로딩할 수 있습니다.
import org.hibernate.annotations.BatchSize
@Entity
class Post(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
val title: String,
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
@BatchSize(size = 20)
val comments: List<Comment> = emptyList()
)
@Service
class PostService(private val postRepository: PostRepository) {
fun findAllPostsWithComments(): List<Post> {
val posts = postRepository.findAll()
posts.forEach { post ->
post.comments.size // 이제 배치 크기만큼 한 번에 로딩됩니다.
}
return posts
}
}
이 방법은 N+1 문제를 완전히 해결하지는 않지만, 쿼리 수를 크게 줄일 수 있습니다.
결론
N+1 문제는 ORM을 사용할 때 흔히 발생하는 성능 이슈입니다. Fetch Join, @EntityGraph, 배치 사이즈 설정 등의 방법으로 이 문제를 해결하거나 완화할 수 있습니다. 각 방법에는 장단점이 있으므로, 상황에 따라 적절한 방법을 선택해야 합니다.
성능 최적화 시 항상 실제 성능 향상을 측정하고, 애플리케이션의 요구사항에 맞는지 확인하는 것이 중요합니다. 또한, 데이터베이스 인덱싱, 쿼리 최적화 등 다른 성능 개선 기법들과 함께 사용하면 더 나은 결과를 얻을 수 있습니다.
'Spring' 카테고리의 다른 글
| @Transactional(readOnly = true)의 모든 것 (1) | 2024.09.15 |
|---|---|
| @Transactional: REQUIRES_NEW vs NESTED 전파 속성 비교 (0) | 2024.09.13 |
| @Configuration은 어떻게 싱글톤 빈을 보장하는가? (0) | 2024.09.10 |
| Spring AOP 내부 호출 문제 해결 방법 (0) | 2024.09.10 |
| Spring의 프록시 기반 트랜잭션 관리 (0) | 2024.09.09 |