Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@

## [Unreleased]

### Changed

- **전역 N+1 검사가 보고하지만 N+1 이 아닌 세 종류를 README 에 표로 적었습니다.** 긍정·부정
두 경우를 한 테스트에서 확인하는 것, 페이징이나 커서를 다음 장으로 넘기는 것, 같은 연산을 순차로
여러 번 부르는 것입니다. 공개 스위트 넷에 켜서 잰 비율도 함께 적었습니다. 스위트가 어떻게 쓰였는지에
따라 46건 중 4건에서 25건 중 25건까지 갈려, 켠 사람이 무엇을 보게 되는지 먼저 알려야 했습니다

## [0.6.0] - 2026-08-27

### Fixed
Expand Down
20 changes: 17 additions & 3 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,23 @@ class GlobalNPlusOneExampleTest {
| 모든 행이 같은 연관을 가리키는 경우 | 영속성 컨텍스트가 한 번 읽고 나머지는 메모리에서 주므로 DB 까지 반복이 가지 않습니다 |
| 다른 스레드에서 실행된 쿼리 | `query-counter.other-threads.enabled` 를 켜지 않으면 기록되지 않습니다. 이 검사가 아니라 카운팅 자체의 성질입니다 |

반대 방향도 있습니다. 일부러 반복문에서 조회하는 테스트, 파라미터화 테스트에서 값만 바꿔 같은
쿼리를 날리는 경우, 결과를 페이지로 나눠 읽는 경우가 모두 다른 것과 똑같이 보고됩니다. 목록을
정리하는 동안 `fail: false` 로 두는 이유가 그것입니다.
반대 방향도 있습니다. 판정 기준이 "같은 SELECT, 다른 파라미터 값" 이라, 코드에 N+1 이 없는데도
정확히 그 모양을 만드는 테스트가 세 종류 있습니다. 다른 것과 똑같이 보고되며, 목록을 정리하는
동안 `fail: false` 로 두는 이유가 그것입니다.

| 보고되지만 N+1 이 아닌 것 | 테스트가 하는 일 | 보고가 어떻게 보이나 |
|---|---|---|
| 긍정과 부정 두 경우를 한 테스트에서 확인 | `findByLastName("Davis")` 뒤에 `findByLastName("Daviss")` 를 불러 두 건과 0건을 각각 단언 | 같은 SELECT 2회, 파라미터는 테스트에 적힌 리터럴만 다르다 |
| 페이징이나 커서를 다음 장으로 넘김 | 첫 장을 읽고 거기서 받은 커서로 다음 장을 읽는다 | 같은 SELECT, 오프셋이나 커서 값만 다르다 |
| 같은 연산을 순차로 여러 번 호출 (라이프사이클) | 같은 레코드를 생성, 수정, 삭제하며 단계마다 id 로 다시 읽는다 | 같은 id 조회 SELECT 가 단계마다 한 번, 사이에 쓰기가 낀다 |

얼마나 자주 나는지는 프로젝트가 아니라 스위트가 어떻게 쓰였는지에 달렸습니다. 이 검사를 켜고
공개 스위트 넷에서 재 보니, 컬렉션을 읽고 연관을 만지는 저장소·서비스 테스트 스위트는 46건 중
4건이 이런 경우였고(나머지는 진짜 N+1), 서비스 시나리오 테스트 스위트는 9건 중 8건, 긍정·부정
두 경우와 페이징을 확인하는 저장소 테스트 스위트는 25건 중 25건이었습니다. 실행 횟수로도, 반복이
테스트의 첫 쿼리인지로도 진짜와 갈리지 않아 이 검사는 가르려 하지 않습니다. 보고를 테스트와
대조해 읽으십시오. N+1 은 앞선 쿼리가 돌려준 행마다 한 번씩 반복되고, 파라미터 값이 그 행들의
id 입니다.

정확한 숫자가 중요한 자리에는 이 검사보다 손으로 적은 어서션이 여전히 낫습니다.

Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,9 +540,24 @@ It is a net, not a full sweep. Turning it on does not mean every N+1 in the suit
| An association shared by every row | The persistence context reads it once and serves the rest from memory, so no repeat reaches the database |
| Queries executed on another thread | Not recorded unless `query-counter.other-threads.enabled` is on. A property of the counting itself, not of this check |

The other direction happens too. A test that reads in a loop on purpose, a parameterized test running
the same query with different values, and paging through results are all reported like any other
finding. That is what `fail: false` is for while you work through the list.
The other direction happens too. The rule is "same SELECT, different parameter values", and three
kinds of test produce exactly that without any N+1 in the code. They are reported like any other
finding, which is what `fail: false` is for while you work through the list.

| Reported, but not an N+1 | What the test does | What the finding looks like |
|---|---|---|
| Positive and negative case in one test | `findByLastName("Davis")` then `findByLastName("Daviss")`, asserting two rows and then none | The same SELECT, 2 executions, parameters differ only by the literal the test typed |
| Paging or cursor walk | Reads the first page, then the next page with the cursor from the first | The same SELECT with the page offset or cursor value as the only difference |
| The same operation called several times (lifecycle) | Create, update, then delete the same record, each step re-reading it by id | The same SELECT by id, once per step, with writes in between |

How often this happens depends on how the suite is written, not on the project. Measured on four
public suites with this check turned on: a suite of repository and service tests that read collections
and touch associations had 4 such findings out of 46 (the rest were real N+1s); a suite of service
scenario tests had 8 of 9; a suite of repository tests that check the positive and negative case and
page through results had 25 of 25. Neither the execution count nor whether the repeat is the first
query of the test separates these from the real ones, so the check does not try to. Read the
finding against the test: an N+1 repeats a query once per row that an earlier query returned, and
the parameter values are those rows' ids.

Where an exact number matters, an assertion written by hand still says it better than this check
does.
Expand Down
Loading