Blog

[CS]15 N+1 문제의 발생 이유와 해결 방법에 대해 설명해주실 수 있을까요?

Author
Summary
N+1문제란? 해결방법은?
Category
Study
Tags
CS
Favorite
Memory Date
2023/10/18
Cross Reference Study
Related Media
Related Thought
Related Lessons
tag
날짜
작성자
진행상황
진행 전
태그구분
6 more properties
N+1 문제의 발생 이유와 해결 방법에 대해 설명해주실 수 있을까요?
N+1 문제는 N+1 문제는 데이터베이스 액세스 시에 자주 발생하는 문제 중 하나이므로, 이를 고려한 쿼리 최적화 및 성능 향상 전략을 적용하는 것이 중요합니다. 특히 ORM(Object-Relational Mapping)을 사용하는 경우에 나타납니다.
실질적인 발생 이유는 N+1 문제는 관계형 데이터베이스에서 연관된 객체를 가져올 때, 연관된 객체를 가져올 때마다 추가적인 쿼리가 실행되면서 발생합니다.
예시로 유저와 게시글 엔티티가 일대다 관계일 때, 유저 엔티티에는 List타입의 게시글 배열이 게시글엔티티에 맵핑되어 있을 것입니다. 유저가 작성한 게시글을 조회 할 때, 실제 코드로는 for루프로 해당 배열을 순회하면서 시작할것인데, 이때 유저에 대한 조회가 +1, 나머지 10개의 게시글을 조회하게 될 것입니다.
정리하면 처음에는 사용자 정보만 가져오는 쿼리가 실행되고, for 루프에서 각 사용자의 게시물을 가져올 때마다 해당 사용자의 게시물에 대한 별도의 쿼리가 실행되기 때문에 실제로 게시글 10개를 가져오려는 쿼리에 +1 유저 정보를 가져오는 추가적인 데이터베이스 액세스가 발생하게 됩니다. 이로 인해 데이터베이스와의 통신 횟수가 증가하며, 이것이 N+1 문제로 알려진 현상입니다.
해결방법은 다음과 같습니다.
1.
즉시로딩(Eager Loading) 사용
Fetch타입을 설정하는 것은 애플리케이션의 특성, 데이터 양, 사용자 요구사항 등을 고려해서 설계 되지만, 기본적으로 N+1의 문제가 발생하면 성능을 향상시키기 위해 연관된 객체를 미리 로딩하여 N+1 문제를 해결할 수 있습니다. 이를 즉시로딩(Eager Loading)이라고 합니다. ORM에서는 이를 통해 한 번의 쿼리로 필요한 모든 데이터를 미리 로딩할 수 있습니다.
2.
배치 쿼리(Batch Query) 사용
여러 객체를 가져올 때 일일이 쿼리를 실행하는 대신, 배치 쿼리를 사용하여 한 번에 연관된 여러 객체를 가져올 수 있습니다. 이는 데이터베이스와의 통신 횟수를 줄여 성능을 향상시킬 수 있습니다.
3.
캐싱(Caching) 활용
빈번한 데이터베이스 쿼리를 방지하기 위해 결과를 캐싱하여 재사용할 수 있습니다. 이는 데이터를 메모리나 다른 저장 매체에 저장해두고, 동일한 쿼리가 여러 번 실행될 때 매번 데이터베이스에 접근하는 것을 방지하여 성능을 향상시킵니다.
추가질문 즉시로딩과 지연로딩의 장단점은 어떻게 될까요?
요약하면
일반적으로는 성능 문제와 메모리 관리를 고려하여 지연로딩을 기본으로 사용하고, 필요한 경우에만 즉시로딩을 적용하는 것이 좋습니다.
즉시로딩은 성능 향상에 도움이 되지만, 메모리 부담이 크고 불필요한 데이터 로딩 문제가 있습니다.
지연로딩은 메모리를 효율적으로 사용하고 불필요한 데이터 로딩을 피할 수 있지만, 성능 저하의 가능성이 있습니다.
각 부분의 장단점을 상세하게 말씀드리면
즉시로딩 (Eager Loading)
장점:
1.
성능 향상: 연관된 객체를 한 번에 모두 로딩하기 때문에, 데이터베이스에 대한 추가적인 쿼리를 줄일 수 있어 성능 향상에 도움이 됩니다.
2.
사용 편의성: 데이터베이스로부터 모든 필요한 정보를 한 번에 가져와서, 객체 사용 시에 따로 로딩할 필요가 없어 사용이 편리합니다.
단점:
1.
메모리 부담: 연관된 객체가 많은 경우, 즉시로딩은 모든 객체를 한 번에 로딩하기 때문에 메모리 부담이 크다는 단점이 있습니다.
2.
불필요한 데이터 로딩: 모든 연관된 객체를 가져오기 때문에, 실제로 사용되지 않는 데이터까지 로딩할 수 있습니다.
적절한 상황:
연관된 객체를 자주 사용하고, 객체 수가 적을 때 사용하면 좋습니다.
지연로딩 (Lazy Loading)
장점:
1.
메모리 절약: 필요한 순간에 연관된 객체를 로딩하기 때문에, 메모리를 효율적으로 사용할 수 있습니다.
2.
불필요한 데이터 로딩 회피: 연관된 객체를 사용할 때에만 로딩하기 때문에, 필요하지 않은 데이터를 로딩하지 않아 성능이 향상될 수 있습니다.
단점:
1.
성능 손실: 연관된 객체를 사용하는 시점에 쿼리를 실행해야 하므로, 지연로딩을 사용하는 경우 쿼리가 여러 번 발생할 수 있어 성능 저하의 가능성이 있습니다.
2.
복잡성: 객체 사용 시에 연관된 객체를 로딩하는 로직을 추가로 작성해야 하므로 코드가 복잡해질 수 있습니다.
적절한 상황:
연관된 객체를 자주 사용하지 않거나, 연관된 객체의 수가 많아서 모두 즉시로딩할 경우 메모리 부담이 큰 경우에 사용하면 좋습니다.
추가질문 배치쿼리로는 어떤방식으로 N+1이 해결되나요?
유저와 게시글이 일대다 연관관계라고하면 쿼리문 자체가 유저 테이블로부터 LEFT JOIN FETCH을 사용해서 유저의 게시글을 모두 가져오는 쿼리문을 작성하게 됩니다.
이렇게 하면 모든 사용자와 그에 따르는 모든 게시물을 포함하는 결과를 가져올 수 있습니다.
쿼리문 예시는 다음과 같습니다.
예전 쿼리문은 SELECT u FROM User u 이후 user로부터 게시글을 가져오게됩니다. 따라서 유저를 조회하는 이 쿼리문이 +1로 발생하게 됩니다.
하지만 SELECT u FROM User u LEFT JOIN FETCH u.posts로 쿼리문 자체에서 게시글까지 모두 가져오도록 한번의 쿼리로 작성하면 유저와 그에 속한 게시글까지 조회하게 됩니다.
추가질문 캐싱은 어떤방식으로 N+1이 해결되나요?
캐싱(Caching)을 활용하여 N+1 문제를 해결하는 아이디어는 데이터를 한 번 가져온 후에는 이를 캐시에 저장하여 필요할 때마다 데이터베이스에 접근하지 않도록 하는 것이 목표입니다.
이 경우에는 Repository의 Jpa구현체에 @Cacheable 어노테이션으로 해당 쿼리에 대한 캐시를 활성화해두면 됩니다.
첫 번째 호출 시에는 데이터베이스에서 데이터를 가져오는데 N+1의 문제가 발생 할 수 있지만, 이후의 호출에서는 캐시된 데이터를 사용하여 중복된 쿼리를 방지하고 성능을 향상시킵니다.
첫 번째 호출까지 N+1을 방지하고자 하면 @Query 어노테이션을 통해 배치쿼리를 작성해두고, @Cacheable까지 작성하면 첫 호출에서도 N+1이 방지되며, 그 이후 호출들은 캐시에서 사용하게 되어 최적화 할 수 있습니다.