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문제를 먼저 풀어볼 수도 있어요.