SQL N+1 문제, 해결방법

박진배·2022년 2월 24일
0

개발을 하다 보면 다른 테이블을 참조해야 하는 경우가 필히 발생합니다. 이 때 저지르기 쉬운 실수 중 하나가 바로 SQL N+1 문제입니다.

1. SQL N+1 문제란?

SQL은 이미 들어보셨을 것이라고 생각합니다. 요약해보자면, 현재 업계에서 제일 많이 통용되는 관계형 DB에서 데이터를 처리하기 위해 설계된 언어입니다. 그런데, 저희는 SQL 문법을 자세하게 알 필요는 없습니다. (물론 자세하게 알면 더 좋긴 하겠죠..?)

이유는 ORM이라는 것 때문인데요, ORM이란 사용하시는 컴퓨터 언어로 작성된 코드를 SQL 문법으로 자동으로 바꿔주어 DB가 인지하고 실행할 수 있게 만들어주는 것을 뜻합니다.

Ruby on Rails로 코드를 작성하시더라도 역시 이 ORM을 통해 간접적으로 DB를 조작할 수 있는데요, 이 때 DB에 쿼리를 한번만 날리는 것이 아니라 추가적으로 N번을 날리는 것을 SQL N+1 문제라고 합니다. 원래대로라면 1번에 끝내야 할 것을 N번을 더 실행해서 끝내기 때문에 굉장히 비효율적인 상황이라고 생각할 수 있을 것 같습니다. 특히나 실제로 배포되어 유저 수가 점점 늘어가는 서비스의 경우에는 굉장히 치명적인 문제가 되는 것입니다.

2. SQL N+1 예시

이제 SQL N+1문제는 발생시키면 안 된다는 것을 아셨을텐데요, 실제 제가 실수했던 상황을 공유해보겠습니다. 토이프로젝트로 지니나 멜론 뮤직과 같은 서비스를 만들어보던 중 했던 실수입니다.

사용자 모델은 User, 음악 모델은 Music, 이 둘을 연결하는 중간 모델은 UserMusic(재생목록 구현 목적)으로 설정했습니다.

개인의 재생목록 리스트를 보여주기 위해서 UserMusic records들을 loop를 통해서 보여주려고 했는데요, 이때 보여주려고 했던 정보는 개인의 재생목록에 추가되어 있는 음악의 제목, 음악의 아티스트명, 음악의 앨범명 등이었습니다. 따라서 저는 아래와 같이

music = user_music.music

이런 코드를 통해 그 음악의 정보를 보여주려고 했습니다. 이 때 쿼리를 보시면

UserMusic이 로드 된 이후 Music Load가 총 5번 로드되는 것을 볼 수 있는데요, 재생목록에 5개의 음악이 있었기 때문에 5번의 쿼리가 추가적으로 날라가게 되는 것입니다.

3. SQL N+1 쉽게 알아차리는 방법

물론 익숙해지면 코드를 작성하던 중 바로 문제를 인지하겠지만, 이런 것은 개발자 본인을 믿지 말고 아예 시스템적으로 해결하는 것도 좋은 방법인 것 같습니다.

bullet이란 gem을 소개해 드릴텐데요, gem을 통해 rails에서 코딩 없이 설치만으로 필요한 기능을 구현할 수 있습니다. gem에 대해 자세히 설명이 돼있는 게시물이 있어 밑에 참조링크 남겨두겠습니다.

해당 gem을 설치하게 되면 SQL N+1문제를 발생시킬 수 있는 코드가 포함된 경우, 아래와 같은 팝업을 띄우게 됩니다. 그리고 어떻게 해결하면 좋을지 해결방법까지 알려줍니다.. 갓 짱짱맨

즉, includes라는 문법을 사용하면 이 문제를 해결할 수 있다는 뜻입니다.

4. SQL N+1 해결 방법

팝업창이 알려준대로 해당 문법을 추가해주면

<% user_musics.includes(:music).each do |user_music| %>
        <% music = user_music.music %>

해당 팝업이 사라지게 됩니다. 그리고 쿼리를 확인해보았는데요,

이전과 다르게 단 1번의 쿼리만으로 재생목록 음악을 전부 불러오는 것을 확인할 수 있습니다. 그렇다면 include method는 무엇이길래 SQL N + 1문제를 해결할 수 있을까요?

5. SQL N+1 문제를 해결하는 ruby method

include처럼 이 문제를 해결할 수 있는 메서드는 2개가 더 있습니다.

5-1. preload
데이터 탐색 전에 미리 테이블을 참조해 데이터를 로드해 놓는 방식.

5-2. eager_load
user_music과 music 테이블을 조인시킴으로써 필요한 데이터를 로드하는 방식.

5-3. include
default는 preload로 작동하지만, whereorder 메서드를 사용해 다른 테이블을 참조하는 경우에는 eager_load로 작동하는 메서드.

즉, include는 2가지 속성을 모두 가진 메서드이며 상황에 따라 자동으로 용도를 변형시킵니다. 일단 지금까지 공부한 바로는 그냥 include 메서드로 N+1 문제를 해결하면 될 것 같다는 생각인데, preloadeager_load를 각각 특정해서 쓰는 것이 보다 더 효율적인 상황이 있다면 댓글로 알려주시면 감사하겠습니다:)

이 부분은 저도 좀 더 공부해서 명확한 답을 알아낸다면 다른 포스팅에서 다뤄보겠습니다.

6. 참조링크

https://ideveloper2.tistory.com/80 (gem에 대해서)
https://velog.io/@khy226/Rails-SQL-N1-%EB%AC%B8%EC%A0%9C-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-feat.-includes-joins-preload (include, preload, eager_load에 대해서)

profile
창업을 경험한 백엔드 개발자입니다. Rails와 NestJS를 쓰고 있습니다.

0개의 댓글