diff --git a/CHANGELOG.md b/CHANGELOG.md index 06bf0a9..3cd8802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ ## [Unreleased] +### Changed + +- **전역 N+1 검사가 보고하지만 N+1 이 아닌 세 종류를 README 에 표로 적었습니다.** 긍정·부정 + 두 경우를 한 테스트에서 확인하는 것, 페이징이나 커서를 다음 장으로 넘기는 것, 같은 연산을 순차로 + 여러 번 부르는 것입니다. 공개 스위트 넷에 켜서 잰 비율도 함께 적었습니다. 스위트가 어떻게 쓰였는지에 + 따라 46건 중 4건에서 25건 중 25건까지 갈려, 켠 사람이 무엇을 보게 되는지 먼저 알려야 했습니다 + ## [0.6.0] - 2026-08-27 ### Fixed diff --git a/README.ko.md b/README.ko.md index eb16204..146c98c 100644 --- a/README.ko.md +++ b/README.ko.md @@ -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 입니다. 정확한 숫자가 중요한 자리에는 이 검사보다 손으로 적은 어서션이 여전히 낫습니다. diff --git a/README.md b/README.md index 5cc0489..161f7a5 100644 --- a/README.md +++ b/README.md @@ -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.