취미로 운영하는 서비스의 사용자 목록 조회 API에서 응답 시간이 급격하게 증가하는 문제가 발생했습니다. JPA를 기본으로 사용하고 있었고, 조회 시에는 Querydsl을 적용하여 N+1 문제가 발생하지 않을 것이라고 예상했지만, 실제로는 사용자 수가 증가함에 따라 응답 시간이 눈에 띄게 느려지는 현상이 있었습니다
원인
서비스가 성정하면서 사용자 수 많큼 N+1 발생하여급격히 느려지고 있었습니다.

위 이미지는 Sentry에서 해당 API의 응답 시간을 보여주는 그래프입니다. 개선 전에는 응답 시간이 20초까지 치솟았지만, N+1 문제를 해결한 후에는 420ms 수준으로 크게 개선된 것을 확인할 수 있습니다.
초창기 QueryDsl 코드는 아래와 같다. 취미삼아 만든 서비스다보니 간단하게 작성하였습니다.
var query = getQuerydsl().createQuery().select(user).from(user);
var users = query.fetch().stream().map(UserView::toDto).toList();
해결
N+1 문제를 해결하기 위해 다음과 같은 두 가지 주요 방법을 고려했습니다.
이번 문제에서는 Projection을 선택하여 해결했습니다. 사용자 목록 조회 API는 연관된 엔티티의 데이터를 추가적으로 필요로 하지 않았기 때문에, Fetch Join보다는 필요한 필드만 선택하여 조회하는 Projection이 더 간단하고 효율적인 해결책이라고 판단했습니다.
var query = getQuerydsl().createQuery().select(
Projections.constructor(
UserView.class,
user.id,
user.email,
user.nickname,
user.email,
user.createdAt
)
)
.from(user);
결론
Projection을 통해 불필요한 연관 관계 조회 및 추가 쿼리 생성을 방지함으로써 사용자 목록 조회 API의 응답 시간을 20000ms (20초)에서 420ms로 획기적으로 개선할 수 있었습니다. 서비스 초기 단계에는 간과하기 쉬운 N+1 문제가 서비스 규모가 확장됨에 따라 심각한 성능 저하를 야기할 수 있다는 것을 다시 한번 확인했습니다. 앞으로 개발 시 데이터베이스 쿼리 성능에 대한 주의를 기울이고, 필요에 따라 Fetch Join, Projection 등 다양한 최적화 기법을 적극적으로 활용해야겠습니다.