QueryDSL 사용 시, N+1 문제 해결하기

런던행·2025년 5월 6일

스프링부트

목록 보기
6/7

취미로 운영하는 서비스의 사용자 목록 조회 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 문제를 해결하기 위해 다음과 같은 두 가지 주요 방법을 고려했습니다.

  • Fetch Join: 연관된 엔티티를 한 번의 쿼리로 함께 가져오는 방법입니다. 필요한 연관 관계를 미리 로딩하여 추가적인 쿼리 발생을 방지할 수 있습니다.
  • Projection: 필요한 데이터만 선택적으로 조회하는 방법입니다. 연관된 엔티티의 모든 데이터를 가져오는 대신, DTO에 필요한 필드만 직접 조회하여 추가적인 쿼리 발생 가능성을 줄입니다.

이번 문제에서는 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 등 다양한 최적화 기법을 적극적으로 활용해야겠습니다.

profile
unit test, tdd, bdd, laravel, django, android native, vuejs, react, embedded linux, typescript

0개의 댓글