본문 바로가기
Programming/삽질일지

[8/25 TIS] N+1 문제 찾아나서기

by JKROH 2023. 8. 25.
반응형

 메인 프로젝트를 고도화 및 리팩토링하는 과정에서, 프로젝트 진행 당시에는 N+1 문제를 신경쓰지 못했기 때문에, N+1 문제를 찾아나서기 위해 전체적으로 서비스를 직접 이용해보며 실행되는 쿼리들을 살펴보고자 하였다(사실 N+1문제가 발생하기를 내심 기대했다, 나도 N+1문제를 해결해보는 과정을 좀 겪으면서 왜 생기는지, 어떻게 해결하는지를 스스로 체득하고 싶었다).

 

 모든 테스트의 시발점인 칵테일 레시피 조회 동작을 수행하기 위해 전체 카테고리에 들어갔을 때부터, 실행되는 쿼리의 상태가 조금 이상해보였다.

전체 카테고리 페이지

 카테고리 페이지에 들어가면 여러 개의 칵테일을 동시에 조회한다. 그런데 실행되는 쿼리문은 동시에 사용자 정보를 무수하게 조회하고 있었다.

Hibernate: select cocktail0_.cocktail_id as cocktail1_4_, cocktail0_.category as category2_4_, cocktail0_.created_at as created_3_4_, cocktail0_.image_url as image_ur4_4_, cocktail0_.liquor as liquor5_4_, cocktail0_.last_modified_at as last_mod6_4_, cocktail0_.name as name7_4_, cocktail0_.rate as rate8_4_, cocktail0_.rated_count as rated_co9_4_, cocktail0_.total_rate_sum as total_r10_4_, cocktail0_.user_user_id as user_us12_4_, cocktail0_.view_count as view_co11_4_ from cocktails cocktail0_ order by cocktail0_.view_count desc
Hibernate: select user0_.user_id as user_id1_11_0_, user0_.age as age2_11_0_, user0_.email as email3_11_0_, user0_.gender as gender4_11_0_, user0_.is_active_user as is_activ5_11_0_, user0_.name as name6_11_0_, user0_.password as password7_11_0_, user0_.profile_image_url as profile_8_11_0_, user0_.subscriber_count as subscrib9_11_0_, roles1_.users_user_id as users_us1_14_1_, roles1_.roles as roles2_14_1_ from users user0_ left outer join users_roles roles1_ on user0_.user_id=roles1_.users_user_id where user0_.user_id=?
Hibernate: select user0_.user_id as user_id1_11_0_, user0_.age as age2_11_0_, user0_.email as email3_11_0_, user0_.gender as gender4_11_0_, user0_.is_active_user as is_activ5_11_0_, user0_.name as name6_11_0_, user0_.password as password7_11_0_, user0_.profile_image_url as profile_8_11_0_, user0_.subscriber_count as subscrib9_11_0_, roles1_.users_user_id as users_us1_14_1_, roles1_.roles as roles2_14_1_ from users user0_ left outer join users_roles roles1_ on user0_.user_id=roles1_.users_user_id where user0_.user_id=?

...

Hibernate: select user0_.user_id as user_id1_11_, user0_.age as age2_11_, user0_.email as email3_11_, user0_.gender as gender4_11_, user0_.is_active_user as is_activ5_11_, user0_.name as name6_11_, user0_.password as password7_11_, user0_.profile_image_url as profile_8_11_, user0_.subscriber_count as subscrib9_11_ from users user0_ where user0_.email=?
Hibernate: select bookmarks0_.users_user_id as users_us1_12_0_, bookmarks0_.bookmarks_bookmark_id as bookmark2_12_0_, bookmark1_.bookmark_id as bookmark1_0_1_, bookmark1_.cocktail_cocktail_id as cocktail5_0_1_, bookmark1_.age as age2_0_1_, bookmark1_.gender as gender3_0_1_, bookmark1_.user_id as user_id4_0_1_, cocktail2_.cocktail_id as cocktail1_4_2_, cocktail2_.category as category2_4_2_, cocktail2_.created_at as created_3_4_2_, cocktail2_.image_url as image_ur4_4_2_, cocktail2_.liquor as liquor5_4_2_, cocktail2_.last_modified_at as last_mod6_4_2_, cocktail2_.name as name7_4_2_, cocktail2_.rate as rate8_4_2_, cocktail2_.rated_count as rated_co9_4_2_, cocktail2_.total_rate_sum as total_r10_4_2_, cocktail2_.user_user_id as user_us12_4_2_, cocktail2_.view_count as view_co11_4_2_, user3_.user_id as user_id1_11_3_, user3_.age as age2_11_3_, user3_.email as email3_11_3_, user3_.gender as gender4_11_3_, user3_.is_active_user as is_activ5_11_3_, user3_.name as name6_11_3_, user3_.password as password7_11_3_, user3_.profile_image_url as profile_8_11_3_, user3_.subscriber_count as subscrib9_11_3_ from users_bookmarks bookmarks0_ inner join bookmarks bookmark1_ on bookmarks0_.bookmarks_bookmark_id=bookmark1_.bookmark_id left outer join cocktails cocktail2_ on bookmark1_.cocktail_cocktail_id=cocktail2_.cocktail_id left outer join users user3_ on cocktail2_.user_user_id=user3_.user_id where bookmarks0_.users_user_id=?

 

정황상 의심되는 경우는 크게 2가지가 있다.

  • 칵테일 정보를 조회하며 회원 정보도 함께 조회하는 경우
  • 북마크 여부를 조회하기 위해 회원 정보를 조회하는 경우

 아마도 첫 번째 경우에서 문제가 발생한다고 예상이 되는데, 그렇게 생각하는 이유는 다음과 같다.

    private MultiResponseDto<CocktailDto.SimpleResponse> createCocktailsSimpleMultiResponseDtos(String email, List<Cocktail> cocktails) {
        User user = userService.findUserByEmail(email);
        List<CocktailDto.SimpleResponse> responses = cocktails.stream()
                .map(cocktail -> cocktailConverter.convertEntityToSimpleResponseDto(user.isBookmarked(cocktail.getCocktailId()), cocktail))
                .collect(Collectors.toList());
        return new MultiResponseDto<>(responses);
    }

 여러 개의 칵테일 목록을 보여주는 로직은 위 코드대로 실행되는데, 그 과정에서 데이터 베이스에서 User정보를 건드리는 일은 단 한 번밖에 발생하지 않는다. 물론 user.isBookmarked() 에서 문제가 발생할 수도 있지만, 그건 User를 Select하지는 않을 것이다.

 

 결국, 카테고리 페이지에 접속하자마자 N+1문제를 만났구나! 라고 판단을 내렸다. 칵테일 - 사용자는 N - 1의 관계이고, 이를 조회하는 과정에서 조회되는 칵테일 마다 사용자 정보를 추가로 조회한 것이라고 생각했기 때문이다.

 

 지금 당장은 해당 문제를 해결하지는 않을 것이다. 조금 더 내부 코드들을 살펴보고, N+1 문제를 해결하기 위한 방법들을 살펴보는 것이 우선시 되어야한다. N+1 문제를 찾고자 테스트를 시작하자 발견한 것은 꽤 기분이 좋다. N+1 문제에 대해 전혀 신경쓰지 않고 개발을 진행했었기 때문에 이를 찾아나서기 위한 여정이 얼마나 걸릴 지 몰랐는데, 뭐 시작하자마자 발견했으니 말이다. 테스팅 공부와 JPA공부를 병행하고 있는 지금, 또다시 눈 앞에 나타난 공부해볼만한 문제를 앞으로의 학습 과정을 조금 더 능동적으로 만들어 줄 수 있는 좋은 자극제로 삼아야겠다.

반응형

댓글