N + 1 문제를 해결하는 과정에서 발생하는 성능 차이
개요
프로젝트 도중 N + 1 문제를 해결하는 과정에서 예상하지 못한 성능 문제가 발생하였습니다.
N + 1 문제를 해결하고 성능을 개선하면서 1 + 1 쿼리로 바꿨음에도 불구하고 성능이 떨어지는 이슈가 발생한 사례를 소개하도록 하겠습니다.
N + 1 문제란
N + 1 문제를 해결하는 과정을 살펴보기 전에 N + 1 문제가 무엇인지 먼저 간단하게 알아보도록 하겠습니다.
N + 1 문제는 연관관계가 설정된 엔티티 사이에서 한 엔티티를 조회하였을 때, 조회된 엔티티의 개수(N) 만큼 연관된 엔티티를 조회하기 위해 추가적인 쿼리가 발생하는 문제입니다.
N + 1 문제의 해결 방법
N + 1 문제를 해결하기 위한 방법으로는 상황에 따라 FetchJoin, EntityGraph, BatchSize 등 다양한 방법을 선택 할 수 있습니다.
제가 진행하던 프로젝트에서는 페이징이 발생하는 상황에서의 N + 1 문제가 발생하는 상황이 발생했었습니다.
페이징 상황에서의 N + 1 문제 이슈
페이징을 처리하는 경우에 만약 엔티티간의 연관관계가 있고 해당 엔티티를 바로 조회하는 경우라면 일대다에 해당하는 관계에 대해서 FetchType을 Lazy로 설정하고 BatchSize로 해결하는 방법을 사용하면 되지만 여러가지 얽혀있는 쿼리에 대한 조회가 필요하여 DTO를 따로 만들어서 사용한다면 곧바로 엔티티를 조회해서 사용하지 못하기 때문에, 위와 같은 방식을 사용 할 수가 없습니다.
페이징이 필요한 조회에서의 문제점을 알아보고 어떤 개선 과정이 있었는지 살펴보도록 하겠습니다.
문제 상황
도메인을 설계를 하면서 도메인간의 책임을 확실히 하고 의존성을 최대한 줄이기 위해 객체간의 연관관계를 설정하지 않고 Primary Key를 가지도록 도메인을 다음과 같이 설계했습니다.

그리고 이를 조회하기 위한 코드를 다음과 같이 작성하였습니다.

하지만, 해당 코드의 stream문에서 image를 가지고 오는 부분에서 조회 된 원소의 개수만큼 쿼리가 호출 되었습니다. 즉 N + 1 문제가 발생하게 된 것입니다. 해당 문제를 해결하기 위해 코드를 다음과 같이 수정하였습니다.

조회된 레코드들의 미션 아이디들을 리스트로 묶고, 이미지를 In 절로 가지고 온 뒤 스트림의 GroupingBy를 통하여 애플리케이션 내부에서 쿼리를 여러 번 호출하지 않고 각 미션에 맞게 묶어주도록 개선을 하였습니다.
위에서는 N + 1 의 쿼리가 발생하지만, 위와 같이 코드를 수정하여 1 + 1 의 쿼리가 발생하도록 수정 할 수 있었습니다.
성능이 개선 됐을 것으로 예상을 하고 과연 얼마만큼의 성능이 개선되었는지 궁금하여 JMeter로 부하테스트를 진행해보았습니다.
부하 테스트
테스트는 다음과 같이 진행하였습니다.
쓰레드 개수 : 1000 반복 횟수 : 10
그리고 테스트에 대한 결과는 다음과 같이 나왔습니다.
1 + N
평균 처리 시간 : 306.2ms 처리량 : 300.2/s
1 + 1
평균 처리 시간 : 468.6ms 처리량 : 195.0/s
예상했던 결과와 달리 오히려 1 + 1 쿼리를 사용한 API 호출이 좀 더 오래걸리는 것을 확인 할 수 있었습니다.
페이징 처리 시 limit를 6개로 설정을 해두었었는데, limit의 개수가 너무 적어서 생긴 문제일까 싶어. limit를 하나씩 늘려나가면서 언제 성능이 역전되는지 지속적으로 테스트를 해봤습니다.
limit를 9개로 했을 때 테스트에서 1 + 1 쿼리를 발생시키는 API가 더 속도가 빨라지는 것을 볼 수 있었습니다.
결론
페이징 처리에 필요한 N + 1 쿼리의 성능을 개선 시킬 때에는 API 테스트를 해보고 성능을 개선을 하는 방향도 중요하지만, 혹시 나중에 limit의 개수가 늘어날 가능성이 있는 부분을 미리 대비하기 위하여 1 + 1 로 확장 가능성을 열어두는 것 또한 한 가지 방법이 될 수 있다는 결론을 내렸습니다.
N + 1 문제를 해결하는 과정에서 발생하는 성능 차이
개요
프로젝트 도중 N + 1 문제를 해결하는 과정에서 예상하지 못한 성능 문제가 발생하였습니다.
N + 1 문제를 해결하고 성능을 개선하면서 1 + 1 쿼리로 바꿨음에도 불구하고 성능이 떨어지는 이슈가 발생한 사례를 소개하도록 하겠습니다.
N + 1 문제란
N + 1 문제를 해결하는 과정을 살펴보기 전에 N + 1 문제가 무엇인지 먼저 간단하게 알아보도록 하겠습니다.
N + 1 문제는 연관관계가 설정된 엔티티 사이에서 한 엔티티를 조회하였을 때, 조회된 엔티티의 개수(N) 만큼 연관된 엔티티를 조회하기 위해 추가적인 쿼리가 발생하는 문제입니다.
N + 1 문제의 해결 방법
N + 1 문제를 해결하기 위한 방법으로는 상황에 따라 FetchJoin, EntityGraph, BatchSize 등 다양한 방법을 선택 할 수 있습니다.
제가 진행하던 프로젝트에서는 페이징이 발생하는 상황에서의 N + 1 문제가 발생하는 상황이 발생했었습니다.
페이징 상황에서의 N + 1 문제 이슈
페이징을 처리하는 경우에 만약 엔티티간의 연관관계가 있고 해당 엔티티를 바로 조회하는 경우라면 일대다에 해당하는 관계에 대해서 FetchType을 Lazy로 설정하고 BatchSize로 해결하는 방법을 사용하면 되지만 여러가지 얽혀있는 쿼리에 대한 조회가 필요하여 DTO를 따로 만들어서 사용한다면 곧바로 엔티티를 조회해서 사용하지 못하기 때문에, 위와 같은 방식을 사용 할 수가 없습니다.
페이징이 필요한 조회에서의 문제점을 알아보고 어떤 개선 과정이 있었는지 살펴보도록 하겠습니다.
문제 상황
도메인을 설계를 하면서 도메인간의 책임을 확실히 하고 의존성을 최대한 줄이기 위해 객체간의 연관관계를 설정하지 않고 Primary Key를 가지도록 도메인을 다음과 같이 설계했습니다.

그리고 이를 조회하기 위한 코드를 다음과 같이 작성하였습니다.

하지만, 해당 코드의 stream문에서 image를 가지고 오는 부분에서 조회 된 원소의 개수만큼 쿼리가 호출 되었습니다. 즉 N + 1 문제가 발생하게 된 것입니다. 해당 문제를 해결하기 위해 코드를 다음과 같이 수정하였습니다.

조회된 레코드들의 미션 아이디들을 리스트로 묶고, 이미지를 In 절로 가지고 온 뒤 스트림의 GroupingBy를 통하여 애플리케이션 내부에서 쿼리를 여러 번 호출하지 않고 각 미션에 맞게 묶어주도록 개선을 하였습니다.
위에서는 N + 1 의 쿼리가 발생하지만, 위와 같이 코드를 수정하여 1 + 1 의 쿼리가 발생하도록 수정 할 수 있었습니다.
성능이 개선 됐을 것으로 예상을 하고 과연 얼마만큼의 성능이 개선되었는지 궁금하여 JMeter로 부하테스트를 진행해보았습니다.
부하 테스트
테스트는 다음과 같이 진행하였습니다.
쓰레드 개수 : 1000 반복 횟수 : 10
그리고 테스트에 대한 결과는 다음과 같이 나왔습니다.
1 + N
평균 처리 시간 : 306.2ms 처리량 : 300.2/s
1 + 1
평균 처리 시간 : 468.6ms 처리량 : 195.0/s
예상했던 결과와 달리 오히려 1 + 1 쿼리를 사용한 API 호출이 좀 더 오래걸리는 것을 확인 할 수 있었습니다.
페이징 처리 시 limit를 6개로 설정을 해두었었는데, limit의 개수가 너무 적어서 생긴 문제일까 싶어. limit를 하나씩 늘려나가면서 언제 성능이 역전되는지 지속적으로 테스트를 해봤습니다.
limit를 9개로 했을 때 테스트에서 1 + 1 쿼리를 발생시키는 API가 더 속도가 빨라지는 것을 볼 수 있었습니다.
결론
페이징 처리에 필요한 N + 1 쿼리의 성능을 개선 시킬 때에는 API 테스트를 해보고 성능을 개선을 하는 방향도 중요하지만, 혹시 나중에 limit의 개수가 늘어날 가능성이 있는 부분을 미리 대비하기 위하여 1 + 1 로 확장 가능성을 열어두는 것 또한 한 가지 방법이 될 수 있다는 결론을 내렸습니다.