N+1 쿼리 문제

이지·2025년 7월 11일

matchpoint_refactoring

목록 보기
2/2

리팩토링을 위해 모델 필드부터 차근차근 살펴보던 중, 우리 프로젝트가 ForeignKey(FK)로 정말 많이 엮여 있음을 실감했다. ApplicantInfo(대회 신청자 정보)에서 Applicant, 그리고 다시 User로 이어지는 구조처럼, 여러 모델이 FK로 깊게 연결되어 있었다.
이 과정에서 “N+1 쿼리 문제”가 반드시 발생할 수밖에 없다는 점을 뒤늦게 떠올렸다.

N+1 쿼리 문제란?

N+1 쿼리 문제란, 쿼리문을 날릴 때 FK로 엮여 있는 객체들을 반복적으로 불러오느라, 데이터베이스에 과도한 쿼리문이 실행되는 현상이다. 이는 대규모 데이터 환경에서 DB 성능 저하의 주요 원인이 될 수 있지만, Django ORM의 간단한 옵션만으로도 충분히 예방이 가능하다.

실제로 우리 프로젝트의 기존 코드를 살펴보면, ApplicantInfo를 조회할 때 many=True 옵션만 덜렁 있는 구조였고, CompetitionApplicantSerializer에서는 user_name, user_phone에 접근하기 위해 obj.user.username, obj.user.phone으로 각 row에 접근하게 설계되어 있었다.
이런 구조에서는 Applicant가 많아질수록 user 정보를 가져오기 위해 매번 추가 쿼리가 발생하게 된다.

사실, 이 조회 API는 로그인한 유저가 본인의 신청 대회만 조회하는 API이기 때문에 성능 저하를 일으킬만한 쿼리 문제가 발생하지는 않는다!

하지만, 관리자 페이지 API를 구현할 때 ㅡ모든 대회 참가자 조회 등ㅡ 대량 데이터를 한 번에 불러와야하는 상황이 올 때는 반드시 신경써야할 부분이다.
좋은 코드를 작성하는 습관을 들이기 위해서, 그리고 미래의 확장성을 위해서,
예방 가능한 문제는 미리 알아두고 방어하는 것이 중요하다고 생각했다.

대표적인 해결방법 2가지

한 객체가 다른 하나의 객체를 참조하는 OneToOneField 관계에서 사용한다.
SQL JOIN을 사용하여 관련된 객체를 한 번의 쿼리로 함께 가져온다.
우리 프로젝트의 예시로는, Applicant를 조회하면 각 Applicant의 User 정보를 반복적으로 접근하는 문제가 발생하는데, select_related('user')를 사용하면 모든 Applicant와 연결된 user 정보를 한 번에 가져오기 때문에 이후 user.username, user.phone에 접근할 때 추가 쿼리가 발생하지 않는다.

한 객체가 여러 객체를 참조하거나, 여러 객체가 한 객체를 참조하는 ManyToManyField에서 사용한다. 우리 프로젝트에서는 Competition -> Applicant -> User 의 관계에서 첫 번째 쿼리로 메인 객체인 Competition을 가져오고, 두 번째 쿼리로 관련된 객체들(Applicant, User)를 한 번에 가져온 뒤, 파이썬에서 객체를 매칭하여 연결한다. 이로써 추가 쿼리 없이 모든 데이터를 효율적으로 조회할 수 있다.

이러한 경험을 통해, 단순히 기능 구현에 그치지 않고,
실제 서비스 환경에서의 효율성과 확장성까지 고려하는 개발자로 한 단계 성장할 수 있었다고 생각한다.
특히, 지금 당장은 문제가 없더라도, 미래의 확장성과 좋은 습관을 위해 미리 최적화하는 것의 중요성을 실감할 수 있었다.

profile
이지하게 살자

0개의 댓글