TIL-46. N+1 문제 및 ORM 최적화

solarrrrr·2022년 1월 17일
0

Today I Learned

목록 보기
46/74
post-thumbnail

ORM이란?

ORM은 Object Relational Mapping의 약자로
객체와 관계형 데이터베이스를 자동으로 매핑해 주는 기술을 말한다.
즉 객체를 통해 데이터베이스를 다룰 수 있게 해 주는 것을 ORM이라고 부른다.

ORM의 장단점을 살펴보면 아래와 같다.

장점

  • 객체지향적인 코드를 통해 SQL문과는 달리 프로그래밍적 접근이 가능하다.
  • 재사용 및 유지보수의 편리성이 증가한다.
  • DBMS에 대한 종속성이 줄어든다.
    (DB를 바꾸더라도 ORM 코드가 그대로 이용 가능하므로 다른 DB로의 이전이 수월하다)

단점

  • 복잡한 SQL문을 구성하는 데 있어 어려움이 있다.
  • 잘못 사용하면 N+1 문제가 발생할 수 있다.

N+1 문제란?

ORM에서는 명령을 실행할 때마다 데이터베이스에서 데이터를 가져오는 것이 아니라
모든 명령 처리가 끝난 후 실제로 데이터를 불러와야 할 시점이 되었을 때
쿼리를 실행하게 된다.

쿼리 1번으로 N건의 데이터를 가져왔다고 할 때
여기에서 원하는 데이터를 얻기 위해 N건의 데이터를 데이터의 수만큼 반복해 2차적인 쿼리가 발생하는 상황을 N+1 문제라고 한다.

Django의 예를 들어보면,
Django 웹 프레임워크는 ORM을 사용하는데 Django ORM의 특징 중 하나는 Queryset을 이용한다는 것이다.

Queryset의 특징으로는 Lazy-Loading(지연 호출)이 있는데
이 특징 때문에 N+1 문제가 발생하고 그로 인해 성능 저하 이슈가 나타나게 된다.

Lazy Loading의 특징

Lazy Loading이 없는 웹페이지에 접속하면 그 안에 있는 모든 내용이 다운로드가 된다. 그래서 첫 로딩이 좀 느릴 수 있다.
다만 사용자가 모든 콘텐츠를 실제로 이용한다면 문제가 크지 않은데
최상단 이미지만 확인하고 나가버린다면 낭비가 발생하게 된다.
이런 문제를 해결하기 위해 Lazy Loading 기법이 나오게 되었다.

  • 필요한 시점에 SQL을 호출함.
  • 필요해야만 SQL을 호출함.
  • 필요한 만큼만 호출함.

한마디로 정의하면 Lazy Loding은
페이지를 불러오는 시점에 당장 필요하지 않은 리소스들은 추후에 로딩하는 기술이다.

아래 코드를 보자.

user_list = User.objects.all()

유저의 전체 리스트를 user_list에 담는 선언이다.
이때 쿼리셋에서는 SQL을 호출하지 않는다.

list(user_list)

이렇게 해야 SQL 호출이 일어나게 된다.
실제 데이터를 얻어와야 하는 시점에 호출이 일어난다.

users = User.objects.all()
first_user = users[0]
user_list = list(users)

이렇게 코드를 작성했을 때 SQL 호출은 몇 번 일어났을까?
정답은 두 번이다.

첫 번째 줄은 위에서 말한 것과 같이 쿼리셋으로 만드는 과정이고
실제 호출은 일어나지 않는다.

두 번째 줄에서야 첫 번째 인덱스의 유저 정보를 가져오므로
이때 호출이 발생되며

마지막 줄에서는 유저 정보를 리스트로 담아 user_list에 담기 때문에
이때 불필요한 호출이 한 번 더 발생하게 되는 것이다.

이러한 불필요한 호출을 줄이는 방법은 의외로 간단한데
바로 순서를 바꿔주는 것이다.

users = User.objects.all()
user_list = list(users)
first_user = users[0]

두 번째 줄에서 모든 유저의 정보를 가져오는 실제 호출이 일어났고
쿼리셋은 이때 가져온 데이터를 캐시에 저장하게 된다.
두 번째 줄에 의해 users는 모든 유저 정보를 담고 있는 쿼리셋이 된 것이다.

그래서 첫 번째 인덱스의 유저 정보를 가져올 때
user_list[0]으로 하지 않고 users[0]으로 가져오는 것이 가능해진다.

이처럼 쿼리셋을 호출하는 순서가 바뀌는 것만으로도 불필요한 호출을 막을 수가 있다.

N+1 문제의 해결법

돌아가서, Lazy Loading의 특징으로 인해 발생하는 N+1 문제는
어떻게 해결하면 될까?

바로 Eager Loading(즉시 호출)을 사용하는 것이다.
Eager Loading은 쿼리를 날릴 때 사용할 데이터까지 포함해 쿼리를 날리기 때문에 비효율적으로 늘어나는 쿼리를 방지할 수 있다.

Django에서는 Eager Loading 방식으로 select_related와 prefetch_related 메서드를 제공하고 있다.

select_related는 원래의 쿼리에 JOIN을 통해서 즉시 로딩하는 방식을 말하는데,
foreign-key, one-to-one처럼
single valued relationships에서만 사용이 가능하며 보통 정참조할 때 많이 사용된다.
(SQL의 JOIN을 사용하는 방식)

prefetch_related는 추가 쿼리를 수행해서 데이터를 즉시 로딩하는 방식을 말하는데,
foreign-key, one-to-one뿐만 아니라 many-to-many, many-to-one 등
모든 relationships에서 사용 가능하다.
(SQL의 WHERE ... IN 구문을 사용하는 방식)

select_related는 조건절이 필요하다면 filter를 달면 되고
prefetch_related의 경우 filter 외에 추가적인 조건절을 달고 싶다면
prefetch 함수를 이용하면 된다.

쿼리셋 작성 순서
Model > annotate > select_related > filter > prefetch_related

profile
몰입

0개의 댓글