목록을 한 번 조회한 뒤 각 행의 연관을 건마다 다시 조회하는 문제입니다. 지연 로딩 이 기본값이라 코드 한 줄이 쿼리 한 번이 되어 생깁니다.
N+1 문제
N+1이란?
1번의 목록 쿼리 + N번의 상세 쿼리
예시
목록을 한 번 조회한 뒤 항목마다 다시 조회하면 조회 수가 항목 수만큼 늘어난다
글 20개 목록을 보여줄 때
목록 조회 1번
글쓴이 조회 20번
화면 하나에 조회가 21번 나간다
한 번에 모아 가져오면
2번으로 끝난다
글쓴이 스무 명을 한 번에 묻거나 처음부터 이어서 가져온다
목록이 길어질수록 차이가 커진다. 100개면 101번과 2번이다
-- 1번: 게시글 10개 조회
SELECT * FROM posts LIMIT 10;
-- N번: 각 게시글 작성자 (10번 반복!)
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
...
SELECT * FROM users WHERE id = 10;
-- 총 11번! (1 + 10)
해결 방법
방법 SQL 쿼리 수 JOIN posts JOIN users1번 Eager Load ORM include 2번 Batch WHERE id IN (...)2번
ORM별 해결법
# Django
Post.objects.select_related('author')
# JPA/Hibernate
@EntityGraph(attributePaths={"author"})
# Prisma
prisma.post.findMany({
include: { author: true }
})
왜 눈에 안 보이나
한 번 도는 코드는 정상으로 보입니다. 개수가 늘 때만 드러납니다.
언제 무엇을 보나 개발 중 데이터가 적어 빠르다. 못 본다 시험 목록이 한두 건이면 못 본다 운영 목록이 길어지고 나서 느려진다
그래서 시간이 아니라 질의 횟수로 잡습니다. 요청 하나가 몇 번 질의했는지를 남기고, 그 수가
목록 길이에 따라 늘어나면 이 문제입니다. 시간 기준으로는 데이터가 쌓인 뒤에 압니다.
한꺼번에 가져오기의 함정
붙여서 한 번에 가져오면 질의 수는 줄지만 다른 문제가 생깁니다.
여럿을 동시에 붙이면 행이 곱해져 결과가 폭발한다
그 상태로 개수를 세면 틀린 수가 나온다
페이지를 잘라도 원하는 개수가 안 나온다
그래서 붙이기 대신 두 번 나눠 부르는 쪽이 나을 때가 많습니다. 목록을 먼저 가져오고,
그 번호들로 관련된 것을 한 번에 가져와 코드에서 맞춥니다. 질의 두 번으로 끝납니다.
실무 포인트
ORM 사용 시 쿼리 로그 확인 필수
목록 10건: 11번 vs 1번 = 10배 차이
Q. N+1 문제란? 어떻게 해결하나요? 목록을 한 번 조회한 뒤, 각 항목마다 추가 쿼리가 나가는 현상입니다.
1번 글 목록 100건 조회
100번 각 글의 작성자를 하나씩 조회
= 101번
해결은 한 번에 모아 가져오는 것입니다.
방법 내용 특징 조인 한 쿼리로 함께 가져온다 왕복 1회. 같은 작성자 정보가 반복 전송된다 배치 조회 작성자 id 를 모아 IN 으로 한 번 더 왕복 2회. 중복 전송이 없다
어느 쪽이 나은지는 중복 정도 로 갈립니다. 글 100건이 실제로는 12명이 쓴 것이라면, 조인은 작성자 정보를 100번 실어 오고 배치는 12번만 가져옵니다.
흔한 실수: N+1 만 없애면 끝이라고 보는 것. 조인으로 바꿨는데 전송량이 늘어 더 느려지는 경우가 있습니다. 왕복 횟수와 전송량을 함께 봐야 합니다.
Q. JPA에서 N+1 문제 해결 방법은?
방법 내용 주의 페치 조인 조인으로 한 번에 가져온다 컬렉션을 여러 개 조인하면 행이 곱해진다 배치 크기 설정 지연 로딩을 IN 으로 묶어 가져온다 왕복이 몇 번으로 줄어든다 엔티티 그래프 어떤 연관을 함께 가져올지 선언한다 조회마다 다르게 지정할 수 있다 DTO 로 직접 조회 필요한 컬럼만 프로젝션 엔티티가 아니라 변경 감지가 없다
컬렉션 페치 조인에는 함정이 있습니다. 글 100건에 댓글이 각 10개면 결과가 1,000행이 되고, 페이징을 함께 쓰면 DB 에서 자를 수 없어 메모리에서 처리합니다. 그래서 컬렉션은 배치 크기로 푸는 편이 안전합니다.
흔한 실수: 모든 연관을 즉시 로딩으로 바꾸는 것. N+1 은 사라지지만 쓰지도 않는 연관까지 매번 가져와 더 느려집니다.
Q. Eager vs Lazy Loading 차이는? 연관 데이터를 언제 가져오는지가 다릅니다.
항목 즉시 로딩 지연 로딩 시점 본체를 읽을 때 함께 실제로 접근할 때 장점 추가 쿼리가 없다 안 쓰면 안 가져온다 단점 안 쓰는 것도 가져온다. 조인이 커진다 접근 시점에 N+1 이 나기 쉽다
기본은 지연 로딩 입니다. 필요한 것만 가져오고, 함께 써야 하는 곳에서만 페치 조인이나 배치로 명시하는 방식이 예측 가능합니다.
즉시 로딩을 기본으로 두면 어떤 쿼리가 나갈지 코드에서 읽히지 않고, 연관이 연쇄되면 한 번의 조회가 테이블 여러 개를 끌고 옵니다.
흔한 실수: 지연 로딩을 쓰면 N+1 이 없다고 보는 것. 오히려 지연 로딩에서 반복문 안 접근으로 N+1 이 생깁니다. 지연을 기본으로 두되 필요한 곳에서 명시적으로 함께 가져오는 것 이 답입니다.
Q. N+1 문제를 어떻게 탐지하나요?
방법 내용 쿼리 로그 확인 한 요청에서 비슷한 쿼리가 반복되는지 본다 요청당 쿼리 수 계측 임계치를 넘으면 경고를 남긴다 APM 의 트레이스 한 요청 안의 DB 호출 횟수와 시간을 본다 테스트에서 검증 특정 API 호출 시 쿼리 수를 단언한다 개발 환경 도구 프레임워크별 N+1 감지 플러그인
가장 확실한 것은 요청당 쿼리 수를 지표로 만드는 것 입니다. 목록 API 가 항상 3번 이하로 끝나야 한다면 그것을 테스트로 고정합니다. 그러면 나중에 누군가 연관을 추가해도 바로 드러납니다.
운영에서는 목록 응답 시간이 데이터 양에 비례해 늘어나는 패턴이 신호입니다. 10건일 때 50ms, 100건일 때 500ms 면 건당 쿼리가 나가고 있을 가능성이 큽니다.
흔한 실수: 개발 환경에서 데이터가 적어 못 잡는 것. 10건으로는 11번 쿼리가 빨라 보입니다. 운영 규모의 데이터로 확인해야 합니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
데이터베이스 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.