{ "schemaVersion": 4, "project": "n+1liner", "ssot": "final/document.md", "sourceRepository": { "path": "/home/donghyeon/workspace/github-project/ca-tmpl", "revision": "761384d7b317469f8b4bb9c243a73419c00ea3a1", "verified": "이 커밋에 문서가 인용한 IT 7종(FeedPersistenceIT·FeedBatchFetchIT·FeedProjectionIT·FeedTopNIT·FeedKeysetIT·FeedVisibilityIT·FeedCrownIT)이 모두 있다. 저장소 HEAD 는 nplus1-replay-l3 브랜치라 이 커밋이 아니다" }, "ssotSha256": "a32d9e8ba07129fc26deeef2ce07e3624ce6f34888d6cf503d0b7f226bbb4b6b", "sourceRevision": "n+1liner-lab@2026-08", "generatedAt": "2026-09-07", "candidateScope": { "document": "final/document.md", "sections": [ "§3", "§4", "§5", "§6", "§7", "§8", "§9", "§10", "§11", "§12", "§13", "§14", "§15", "§16", "§17", "§18" ], "excluded": [ "§1 해결할 문제", "§2 조회 전략의 전체 여정", "부록. 측정 재현과 provenance, 함정" ], "note": "접어 넣은 분석이 아니라 처음부터 한 편으로 쓴 글이라 제2부가 없다. §1·§2 는 문제 진술과 요약이고 부록은 재현 절차라 후보 자리가 아니다" }, "note": "이 프로젝트의 글감 전부다. 분해 계약이자 색인이고, 이 파일이 정본이다. 노드의 칸은 사람이 적고 file·publication·status 는 기록 파일에서 읽어 채운다 — python3 scripts/build-tech-log-tree.py n+1liner", "contract": { "decomposition": [ "글감을 찾는 입력은 final/document.md 하나다. 거기에 없는 근거는 먼저 SSOT 에 넣는다.", "후보 전부는 candidates 에 처분과 함께 남고 PROMOTE 만 topics 로 올라간다.", "없애고 관련 Case 나 Concept 의 한 절로 넣어도 이해·결정·재사용성이 그대로라면 독립 기록으로 만들지 않는다.", "Topic 은 독자 질문 하나다. 그 물음에 답하지 않는 글감은 다른 Topic 으로 옮긴다.", "Concept 은 Case·Decision·Question 을 먼저 고른 뒤 그것을 이해하는 데 필요한 것만 거꾸로 더한다." ], "readinessValues": [ "READY", "OPEN", "NEEDS_EVIDENCE", "NEEDS_DECISION", "BLOCKED" ], "dispositionValues": { "PROMOTE": "독립 Tech Log 로 쓴다", "MERGE_INTO": "다른 기록의 한 절로 흡수한다", "KEEP_IN_SSOT": "분석에는 남기고 독립 기록으로 만들지 않는다 — 정상적인 성공 결과다", "NEEDS_EVIDENCE": "주장에 아직 검증이 없다", "NEEDS_DECISION": "방향이 그럴듯하지만 프로젝트가 정하지 않았다", "BLOCKED": "원본이 불완전하거나 서로 어긋난다" } }, "topics": { "jpa-feed-query-performance": { "topic": "jpa-feed-query-performance", "title": "JPA 피드 조회 성능 — N+1 진단과 조회 전략의 진화", "readerQuestion": "피드 한 화면을 그리는 데 쿼리가 몇 번 나가고, 조회 전략을 바꿀 때 무엇이 대신 늘어나는가?", "kinds": { "case": [ { "title": "Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1", "slug": "eager-toone-nplus1-without-access", "readiness": "READY", "source": [ "final/document.md#7-2", "final/document.md#7-3", "final/document.md#7-4" ], "code": [ "FeedPersistenceIT.l2ToOneEagerHiddenNPlusOneCurve", "FeedPersistenceIT.l2EagerToOneFiresEvenWithZeroFieldAccess" ], "evidence": [ "evidence/raw/metrics/l2-toone-split.csv", "evidence/raw/explain/toone-pages-plan.txt", "evidence/raw/explain/toone-users-plan.txt" ], "classification": "같은 EAGER 애너테이션이 정반대 곡선을 그린 것을 엔티티별 통계로 확정했고 접근 0 테스트로 원인을 닫았다", "missing-verification": "Hibernate 다른 버전에서 같은 2차 SELECT 형태가 나오는지는 재지 않았다", "relations": [ "reference:fetch-type-vs-fetch-strategy", "decision:measure-plan-on-real-postgresql" ], "kind": "case", "publication": "게시됨", "file": "jpa-feed-query-performance/case/case-eager-toone-nplus1-without-access.md", "status": "게시 전", "studioId": "32d0be7d-d88e-4760-8d91-35d3a233a99a", "assets": [ "eager-lazy-query-sequence" ], "evidenceFiles": [ "../../../final/evidence/raw/explain/highlights-child-plan-A.txt", "../../../final/evidence/raw/explain/toone-pages-plan.txt", "../../../final/evidence/raw/explain/toone-users-plan.txt" ] }, { "title": "Fetch Join으로 N+1을 해결하다 만난 MultiBag과 행 폭증", "slug": "fetch-join-multibag-and-row-explosion", "readiness": "READY", "source": [ "final/document.md#9-2", "final/document.md#9-3", "final/document.md#9-5" ], "code": [ "FeedPersistenceIT.l3TwoBagFetchJoinThrowsMultipleBagFetchException", "FeedPersistenceIT.l3SingleCollectionFetchJoinExplodesTransferredRows" ], "evidence": [ "evidence/raw/metrics/l3-cartesian.csv", "evidence/raw/explain/l3-cartesian-join-plan.txt" ], "classification": "두 실패를 각각 재현하고 조인 카디널리티로 확정했다. 리스트 크기로는 보이지 않던 것을 실행계획이 드러냈다", "missing-verification": "Hibernate 6+ 의 루트 중복 제거가 다른 버전에서도 같은지는 재지 않았다", "relations": [ "case:collection-fetch-join-in-memory-paging", "reference:fetch-strategy-selection", "decision:no-collection-fetch-join-with-pagination" ], "kind": "case", "publication": "게시됨", "file": "jpa-feed-query-performance/case/case-fetch-join-multibag-and-row-explosion.md", "status": "게시 전", "studioId": "7ed75172-fd56-42bf-956a-8f9fc1cca235", "assets": [ "cartesian-row-multiplication" ], "evidenceFiles": [ "../../../final/evidence/raw/explain/l3-cartesian-join-plan.txt" ] }, { "title": "Collection Fetch Join Pagination의 In-memory Paging", "slug": "collection-fetch-join-in-memory-paging", "readiness": "READY", "source": [ "final/document.md#10-2", "final/document.md#10-4" ], "code": [ "FeedPersistenceIT.l4CollectionFetchJoinPagingLoadsWholeDatasetInMemory" ], "evidence": [ "evidence/raw/metrics/l4-inmemory-paging.csv", "evidence/raw/explain/l4-collection-join-no-limit.txt", "evidence/raw/explain/l4-entity-paging-limit.txt" ], "classification": "반환 크기가 정상인데 적재량이 N 이라는 어긋남을 통계로 잡고 Limit 노드 부재로 원인을 확정했다", "missing-verification": "기준선의 할당량을 재지 않아 두 구조의 메모리를 직접 비교한 값은 없다", "relations": [ "case:fetch-join-multibag-and-row-explosion", "decision:no-collection-fetch-join-with-pagination" ], "kind": "case", "publication": "게시됨", "file": "jpa-feed-query-performance/case/case-collection-fetch-join-in-memory-paging.md", "status": "게시 전", "studioId": "c1158754-e3d2-47b8-bb41-81787c0ca84b", "assets": [ "in-memory-paging" ], "evidenceFiles": [ "../../../final/evidence/raw/explain/l4-entity-paging-limit.txt", "../../../final/evidence/raw/explain/l5-entity-paging-limit.txt" ] }, { "title": "Projection 이후에도 1,509행을 읽은 Row Over-fetch", "slug": "projection-row-over-fetch", "readiness": "READY", "source": [ "final/document.md#12-2", "final/document.md#12-4", "final/document.md#12-6" ], "code": [ "FeedProjectionIT.l6ProjectionHydratesZeroEntities", "FeedProjectionIT.l6ProbeProjectionStillFetchesAllHighlightsNotTopN" ], "evidence": [ "evidence/raw/metrics/l6-projection-resolution.csv", "evidence/raw/metrics/l6-explain-width.csv", "evidence/raw/explain/l6-parent-projection.txt" ], "classification": "엔티티 적재가 0 이 된 것과 자식 행수가 그대로인 것을 같은 실행에서 확인해 무엇이 풀리고 무엇이 남았는지 닫았다", "missing-verification": "width 가 좁아지지 않은 이유를 추정으로만 적었고 전송 바이트를 직접 재지 않았다", "relations": [ "decision:read-projection-for-screen-query" ], "kind": "case", "publication": "게시됨", "file": "jpa-feed-query-performance/case/case-projection-row-over-fetch.md", "status": "게시 전", "studioId": "4c9c3b90-bc89-4300-9334-088ea95d37d8", "assets": [ "projection-row-over-fetch" ], "evidenceFiles": [ "../../../final/evidence/raw/explain/l6-parent-projection.txt" ] }, { "title": "Visibility OR이 Keyset Index를 깨뜨린 문제", "slug": "visibility-or-breaks-keyset-index", "readiness": "READY", "source": [ "final/document.md#14-2", "final/document.md#14-3", "final/document.md#14-5" ], "code": [ "FeedKeysetIT.l15DeepPageOffsetOverScansButKeysetStaysFlat", "FeedKeysetIT.l15ProbeVisibilityOrBreaksKeysetIndex" ], "evidence": [ "evidence/raw/metrics/l15-depth-curve.csv", "evidence/raw/explain/l15-keyset-index-seek.txt", "evidence/raw/explain/l15-visibility-or-probe.txt" ], "classification": "깊이 곡선으로 keyset 이득을 확정한 뒤 같은 쿼리에 가시성을 얹어 그 이득이 사라지는 조건까지 재고 닫았다", "missing-verification": "인덱스를 테스트 안에서 만들고 지웠으므로 운영 마이그레이션에 반영한 상태로는 재지 않았다", "relations": [ "reference:keyset-pagination-design", "decision:keyset-for-feed-pagination" ], "kind": "case", "publication": "게시됨", "file": "jpa-feed-query-performance/case/case-visibility-or-breaks-keyset-index.md", "status": "게시 전", "studioId": "e6715e81-6dbd-4287-8e19-946c334f38fb", "assets": [ "keyset-vs-offset" ], "evidenceFiles": [ "../../../final/evidence/raw/explain/l15-keyset-no-index.txt", "../../../final/evidence/raw/explain/l15-offset-deep-page.txt" ] } ], "concept": [], "reference": [ { "title": "JPA N+1 정량 진단 기준", "slug": "nplus1-quantitative-diagnosis", "readiness": "READY", "source": [ "final/document.md#6-1", "final/document.md#7-1" ], "classification": "지표를 갈라 읽고 항등식으로 검산하는 절차라 다음 프로젝트의 어느 연관에도 그대로 적용된다", "scope": "ORM 이 연관을 채우는 조회 경로. 총 쿼리 수 하나로 원인을 말하려는 자리", "exceptions": "배치나 서브셀렉트를 켜면 초기화 컬렉션 수와 SELECT 수의 등식이 깨지므로 두 지표를 함께 읽는다", "relations": [ "case:eager-toone-nplus1-without-access", "reference:postgresql-query-plan-measurement" ], "kind": "reference", "publication": "게시됨", "file": "jpa-feed-query-performance/reference/reference-nplus1-quantitative-diagnosis.md", "status": "게시 전", "studioId": "b0b55ac9-c0a3-4c01-ba84-0aa478923ace", "assets": [], "evidenceFiles": [] }, { "title": "Fetch Type과 Fetch Strategy 구분", "slug": "fetch-type-vs-fetch-strategy", "readiness": "READY", "source": [ "final/document.md#5-4", "final/document.md#7-5" ], "classification": "언제 나가는가와 몇 번 나가는가를 가르는 구분이라 ORM 을 쓰는 다음 프로젝트에도 적용된다", "scope": "연관을 가진 엔티티를 목록으로 조회하는 모든 경로", "exceptions": "단건 조회는 provider 가 JOIN 으로 합칠 수 있어 이 구분이 같은 형태로 나타나지 않는다", "relations": [ "case:eager-toone-nplus1-without-access" ], "kind": "reference", "publication": "게시됨", "file": "jpa-feed-query-performance/reference/reference-fetch-type-vs-fetch-strategy.md", "status": "게시 전", "studioId": "51095f6e-2cc8-439c-8648-065033614215", "assets": [], "evidenceFiles": [] }, { "title": "Fetch Join · Batch · Projection 선택 기준", "slug": "fetch-strategy-selection", "readiness": "READY", "source": [ "final/document.md#9-4", "final/document.md#11-5", "final/document.md#12-5" ], "classification": "무엇을 줄이려는지 먼저 정하고 그 지표로 전후를 재는 기준이라 조회 전략을 바꾸는 어느 작업에도 적용된다", "scope": "조회 전략을 바꾸면서 개선을 주장해야 하는 자리", "exceptions": "ToOne 만 있는 조회에서는 fetch join 이 행을 곱하지 않으므로 전송 행수 축을 따로 재지 않아도 된다", "relations": [ "case:fetch-join-multibag-and-row-explosion", "decision:batch-fetch-for-entity-graph" ], "kind": "reference", "publication": "게시됨", "file": "jpa-feed-query-performance/reference/reference-fetch-strategy-selection.md", "status": "게시 전", "studioId": "db99cbc5-9123-4599-b368-39ff3170e81d", "assets": [], "evidenceFiles": [] }, { "title": "Top-N-per-group 선택 기준", "slug": "top-n-per-group-selection", "readiness": "READY", "source": [ "final/document.md#13-1", "final/document.md#13-6" ], "classification": "그룹당 상한을 SQL 로 표현하는 세 방법과 그 사이의 선택 축이라 다른 목록 화면에도 적용된다", "scope": "부모마다 자식 상위 몇 개만 필요한 조회", "exceptions": "K 가 그룹 크기에 가까우면 인덱스 seek 이득이 사라져 더 단순한 윈도우 함수를 고른다", "relations": [ "reference:postgresql-query-plan-measurement" ], "kind": "reference", "publication": "게시됨", "file": "jpa-feed-query-performance/reference/reference-top-n-per-group-selection.md", "status": "게시 전", "studioId": "bf5f2462-0e94-4723-bdc8-f7dd709b2dbb", "assets": [], "evidenceFiles": [] }, { "title": "Keyset Pagination 설계 기준", "slug": "keyset-pagination-design", "readiness": "READY", "source": [ "final/document.md#14-1", "final/document.md#14-4" ], "classification": "커서와 정렬키 인덱스의 컬럼·방향을 맞추는 설계 기준이라 무한 스크롤을 쓰는 다음 화면에도 적용된다", "scope": "페이지 깊이가 커지는 목록 조회", "exceptions": "임의 페이지로 건너뛰어야 하는 화면은 커서로 표현되지 않으므로 이 기준이 걸리지 않는다", "relations": [ "case:visibility-or-breaks-keyset-index", "decision:keyset-for-feed-pagination" ], "kind": "reference", "publication": "게시됨", "file": "jpa-feed-query-performance/reference/reference-keyset-pagination-design.md", "status": "게시 전", "studioId": "06788903-3dfa-4f70-b159-f1224384fd0b", "assets": [], "evidenceFiles": [] }, { "title": "Feed Visibility Query Pattern", "slug": "feed-visibility-query-pattern", "readiness": "READY", "source": [ "final/document.md#15-1", "final/document.md#15-4" ], "classification": "술어가 여럿인 필터를 정렬과 함께 쓸 때의 선택 축이라 권한이 걸린 다른 목록에도 적용된다", "scope": "서로 다른 조건의 분기를 OR 로 묶고 정렬·페이징까지 요구하는 조회", "exceptions": "분기가 하나뿐이거나 정렬을 요구하지 않으면 인덱스 순서를 잃는 문제가 생기지 않는다", "relations": [ "question:promote-feed-visible-to-cqrs" ], "kind": "reference", "publication": "게시됨", "file": "jpa-feed-query-performance/reference/reference-feed-visibility-query-pattern.md", "status": "게시 전", "studioId": "635fcedd-d402-4297-bcf3-9fcdf4200d28", "assets": [], "evidenceFiles": [] }, { "title": "PostgreSQL Query Plan 측정 기준", "slug": "postgresql-query-plan-measurement", "readiness": "READY", "source": [ "final/document.md#4-1", "final/document.md#4-6", "final/document.md#4-7", "final/document.md#6-4" ], "classification": "쿼리 수로는 보이지 않는 것을 계획에서 읽는 절차이고 도구 선택 기준까지 포함해 다음 측정에도 적용된다", "scope": "실행계획·인덱스 동작이 결론에 들어가는 모든 측정", "exceptions": "주장이 조회 횟수의 증가 형태에만 걸려 있으면 내장 카운터로 충분하고 전용 도구를 들이지 않는다", "relations": [ "decision:measure-plan-on-real-postgresql" ], "kind": "reference", "publication": "게시됨", "file": "jpa-feed-query-performance/reference/reference-postgresql-query-plan-measurement.md", "status": "게시 전", "studioId": "e8c2e9ea-cd87-46f8-9469-849dbd433d86", "assets": [], "evidenceFiles": [] } ], "question": [ { "title": "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가", "slug": "cardinality-estimate-after-analyze", "readiness": "OPEN", "source": [ "final/document.md#6-4", "final/document.md#4-6" ], "known": "대량 시드 직후 계획의 추정 행수는 1 이고 실제는 500 이다. 조회는 ix_highlights_feed_items_created 를 Index Scan 으로 탔고 실행시간은 0.173 ms 다", "unknown": "ANALYZE highlights 뒤 추정 행수가 얼마로 바뀌는지, 그리고 스캔 방식이 뒤집히는지", "next-verification": "같은 조회를 ANALYZE 전후로 EXPLAIN 해 Plan A 와 Plan B 를 같은 표에 채운다", "decision-criterion": "Plan B 의 추정이 실제의 2 배 안에 들어오고 스캔 방식이 유지되면 닫는다. 방식이 뒤집히면 시드 뒤 ANALYZE 를 측정 절차에 넣는 Decision 으로 넘긴다", "relations": [ "reference:postgresql-query-plan-measurement" ], "kind": "question", "publication": "게시됨", "file": "jpa-feed-query-performance/question/question-cardinality-estimate-after-analyze.md", "status": "게시 전", "studioId": "e1e0e2a0-c6b6-45bf-be42-f697ba5e2fff", "assets": [], "evidenceFiles": [] }, { "title": "Round Trip과 Row Volume을 독립 측정할 것인가", "slug": "isolate-round-trip-and-row-volume", "readiness": "OPEN", "source": [ "final/document.md#4-5", "final/document.md#6-2" ], "known": "현재 데이터셋은 N 을 키우면 반환 아이템 수·자식 행수·엔티티 생성량·왕복이 동시에 늘어난다. 하이라이트 총량은 N 에 정비례하지 않는다", "unknown": "왕복만 바꾼 데이터셋에서 지연이 왕복 수에 선형으로 붙는지, 그리고 격리 데이터셋을 유지할 값어치가 있는지", "next-verification": "FeedItem 당 Highlight 1 개인 데이터셋 A 와 FeedItem 20 고정에 Highlight 를 바꾸는 데이터셋 B 를 만들어 각각 잰다", "decision-criterion": "두 데이터셋에서 지연 곡선이 갈리면 격리 데이터셋을 시더에 남기고, 갈리지 않으면 현재 편중 분포 하나로 유지한다", "relations": [ "reference:nplus1-quantitative-diagnosis" ], "kind": "question", "publication": "게시됨", "file": "jpa-feed-query-performance/question/question-isolate-round-trip-and-row-volume.md", "status": "게시 전", "studioId": "5159c415-232d-424a-970a-b0db52746767", "assets": [], "evidenceFiles": [] }, { "title": "Highlight 없는 FeedItem을 허용할 것인가", "slug": "nullable-first-highlighted-at", "readiness": "OPEN", "source": [ "final/document.md#3-1", "final/document.md#14-4" ], "known": "first_highlighted_at 은 null 을 허용한다. 시더는 하이라이트가 만든 FeedItem 만 만들어 이 값을 항상 채우므로 지금까지 null 이 나타난 적이 없다", "unknown": "하이라이트 없는 FeedItem 을 만드는 경로가 도메인에 있는지, 그리고 NOT NULL 로 좁힐지 null 정렬 위치를 정할지", "next-verification": "FeedItem 을 만드는 도메인 경로를 훑어 하이라이트 없이 생성되는 곳이 있는지 확인한다", "decision-criterion": "그런 경로가 없으면 NOT NULL 로 좁히는 Decision 으로, 있으면 커서의 null 처리와 부분 인덱스 predicate 를 정하는 Decision 으로 넘긴다", "relations": [ "decision:keyset-for-feed-pagination", "reference:keyset-pagination-design" ], "kind": "question", "publication": "게시됨", "file": "jpa-feed-query-performance/question/question-nullable-first-highlighted-at.md", "status": "게시 전", "studioId": "b099ca65-bf9f-4d61-814c-74722453fa3c", "assets": [], "evidenceFiles": [] }, { "title": "feed_visible을 Production CQRS로 승격할 것인가", "slug": "promote-feed-visible-to-cqrs", "readiness": "OPEN", "source": [ "final/document.md#15-5", "final/document.md#17-3", "final/document.md#18" ], "known": "사전계산 조회는 커버링 인덱스 하나만 읽어 buffers 1 이었고 단일 OR 은 122, UNION 분해는 200 이었다. 현재 구현은 같은 저장소 위의 CQRS-lite 다", "unknown": "지금 트래픽을 단일 OR 이나 UNION 으로 감당할 수 있는지, 그리고 쓰기 변경을 투영으로 옮기는 경로를 도메인 이벤트로 할지 아웃박스로 할지", "next-verification": "목표 처리량과 허용 지연을 정하고 부하 테스트로 현재 조회 방식이 그것을 견디는지 확인한다", "decision-criterion": "현재 방식이 목표를 못 견디면 별도 물리 읽기 저장소와 동기화 경로를 넣는 Decision 으로 넘기고, 견디면 CQRS-lite 유지 결정을 확정한다", "relations": [ "decision:keep-read-model-as-cqrs-lite", "reference:feed-visibility-query-pattern" ], "kind": "question", "publication": "게시됨", "file": "jpa-feed-query-performance/question/question-promote-feed-visible-to-cqrs.md", "status": "게시 전", "studioId": "5088ce14-b096-41d3-abba-64b7afb48bb9", "assets": [], "evidenceFiles": [] }, { "title": "실제 동시 트래픽에서도 이 구조가 안정적인가", "slug": "concurrency-stability", "readiness": "OPEN", "source": [ "final/document.md#4-5", "final/document.md#18" ], "known": "모든 측정은 단일 스레드로 이미 열린 트랜잭션 안의 어댑터 호출만 쟀고 HTTP 종단을 거치지 않았다. 지연은 warm 캐시에서 잰 5 개 표본의 중앙값과 최댓값이다", "unknown": "목표 처리량에서 커넥션 풀이 포화되는지, 포화를 피하려면 풀을 몇 개로 잡아야 하는지, 꼬리 지연이 어디까지 벌어지는지", "next-verification": "목표 처리량과 허용 지연을 먼저 정하고 부하 테스트와 APM 으로 종단 지연·처리량·풀 상태를 함께 잰다", "decision-criterion": "목표 처리량에서 풀이 포화되지 않고 꼬리 지연이 허용치 안이면 닫는다. 벗어나면 풀 크기와 조회 전략을 다시 정하는 Decision 으로 넘긴다", "relations": [ "question:promote-feed-visible-to-cqrs" ], "kind": "question", "publication": "게시됨", "file": "jpa-feed-query-performance/question/question-concurrency-stability.md", "status": "게시 전", "studioId": "6cbe963f-86f8-4df6-be5a-900712970d01", "assets": [], "evidenceFiles": [] } ], "decision": [ { "title": "Query Plan은 실제 PostgreSQL에서 측정한다", "slug": "measure-plan-on-real-postgresql", "readiness": "READY", "decision-status": "ADOPTED", "source": [ "final/document.md#4-1", "final/document.md#4-6" ], "decision-evidence": [ "case:eager-toone-nplus1-without-access", "reference:postgresql-query-plan-measurement" ], "grounds": "비용 모델·통계·저장 방식·인덱스 종류 네 축이 엔진마다 달라 인메모리로 잰 계획을 운영 계획으로 읽으면 체계적으로 틀린 결론에 이른다", "classification": "인메모리 대체라는 대안을 두고 프로젝트가 실제 PostgreSQL 을 골랐고 감수한 비용이 기록돼 있다", "relations": [ "reference:postgresql-query-plan-measurement" ], "kind": "decision", "publication": "게시됨", "file": "jpa-feed-query-performance/decision/decision-measure-plan-on-real-postgresql.md", "status": "게시 전", "studioId": "ae6c9bea-d3a3-46e1-bbd4-8d580d336394", "assets": [], "evidenceFiles": [] }, { "title": "Query Strategy는 FeedQueryPort 뒤에서 소유한다", "slug": "query-strategy-behind-port", "readiness": "READY", "decision-status": "ADOPTED", "source": [ "final/document.md#5-2" ], "decision-evidence": [ "reference:fetch-strategy-selection" ], "grounds": "조회 전략을 여섯 번 바꾸는 동안 컨트롤러와 유스케이스가 한 번도 바뀌지 않았다", "classification": "전략을 상위 계층에 노출하는 대안을 두고 포트 뒤에 가두기로 정했다", "relations": [ "reference:fetch-strategy-selection", "decision:read-projection-for-screen-query" ], "kind": "decision", "publication": "게시됨", "file": "jpa-feed-query-performance/decision/decision-query-strategy-behind-port.md", "status": "게시 전", "studioId": "4e3200c8-eff5-4442-ae84-ae7b7fa92c8b", "assets": [], "evidenceFiles": [] }, { "title": "Collection Fetch Join과 Pagination을 같이 사용하지 않는다", "slug": "no-collection-fetch-join-with-pagination", "readiness": "READY", "decision-status": "ADOPTED", "source": [ "final/document.md#10-5", "final/document.md#9-6" ], "decision-evidence": [ "case:collection-fetch-join-in-memory-paging", "case:fetch-join-multibag-and-row-explosion" ], "grounds": "컬렉션 fetch join 에 페이징을 걸면 DB LIMIT 이 빠지고 부모 전체를 메모리에 올린 뒤 자른다", "classification": "fetch join 으로 한 번에 가져오는 대안을 실제로 시도했다가 계획과 통계로 확인하고 버렸다", "relations": [ "case:collection-fetch-join-in-memory-paging", "decision:batch-fetch-for-entity-graph" ], "kind": "decision", "publication": "게시됨", "file": "jpa-feed-query-performance/decision/decision-no-collection-fetch-join-with-pagination.md", "status": "게시 전", "studioId": "5e4d033c-d6fe-4257-a4dc-1ade44473c72", "assets": [], "evidenceFiles": [] }, { "title": "Entity Graph 조회에는 Batch Fetch를 사용한다", "slug": "batch-fetch-for-entity-graph", "readiness": "READY", "decision-status": "ADOPTED", "source": [ "final/document.md#11-1", "final/document.md#11-5" ], "decision-evidence": [ "reference:fetch-strategy-selection" ], "grounds": "부모만 페이징하고 자식을 IN 으로 채우면 왕복이 2,022 개에서 23 개로 줄고 부모 적재가 페이지 크기에 머문다", "classification": "fetch join 과 배치를 실제로 견주고 배치를 골랐으며 세션 범위라는 비용을 함께 적었다", "relations": [ "decision:no-collection-fetch-join-with-pagination" ], "kind": "decision", "publication": "게시됨", "file": "jpa-feed-query-performance/decision/decision-batch-fetch-for-entity-graph.md", "status": "게시 전", "studioId": "08a74b35-10c3-4874-8fbc-209b0b6e942e", "assets": [], "evidenceFiles": [] }, { "title": "화면 조회는 Read Projection을 사용한다", "slug": "read-projection-for-screen-query", "readiness": "READY", "decision-status": "ADOPTED", "source": [ "final/document.md#12-1", "final/document.md#12-5" ], "decision-evidence": [ "case:projection-row-over-fetch", "reference:fetch-strategy-selection" ], "grounds": "캐리어 record 로 받으면 영속 엔티티가 만들어지지 않아 적재가 1,569 개에서 0 개가 되고 쿼리는 N 과 무관하게 2 개다", "classification": "엔티티 조회를 유지하는 대안을 두고 프로젝션을 골랐고 조립을 애플리케이션이 진다는 비용을 적었다", "relations": [ "case:projection-row-over-fetch", "decision:query-strategy-behind-port" ], "kind": "decision", "publication": "게시됨", "file": "jpa-feed-query-performance/decision/decision-read-projection-for-screen-query.md", "status": "게시 전", "studioId": "30a37f34-b406-4061-b924-e22e0be0c3bf", "assets": [], "evidenceFiles": [] }, { "title": "Feed Pagination은 Keyset을 사용한다", "slug": "keyset-for-feed-pagination", "readiness": "READY", "decision-status": "ADOPTED", "source": [ "final/document.md#14-1", "final/document.md#14-4" ], "decision-evidence": [ "case:visibility-or-breaks-keyset-index", "reference:keyset-pagination-design" ], "grounds": "100 번째 페이지에서 OFFSET 은 2,000 행을 훑고 keyset 은 20 행을 읽었다", "classification": "OFFSET 유지라는 대안을 깊이 곡선으로 견주고 keyset 을 골랐으며 임의 페이지 이동을 잃는다는 비용을 적었다", "relations": [ "case:visibility-or-breaks-keyset-index", "question:nullable-first-highlighted-at" ], "kind": "decision", "publication": "게시됨", "file": "jpa-feed-query-performance/decision/decision-keyset-for-feed-pagination.md", "status": "게시 전", "studioId": "1dbce381-f0dc-4d49-ad68-bd31d205677e", "assets": [], "evidenceFiles": [] }, { "title": "현재 Read Model은 CQRS-lite로 유지한다", "slug": "keep-read-model-as-cqrs-lite", "readiness": "READY", "decision-status": "ADOPTED", "source": [ "final/document.md#17-1", "final/document.md#17-2" ], "decision-evidence": [ "case:projection-row-over-fetch", "reference:feed-visibility-query-pattern" ], "grounds": "읽기 모델만 분리해도 엔티티 적재 0 과 상수 쿼리 2 개를 얻었고, 사전계산을 상시 유지하면 뷰어별 투영을 동기화하는 비용이 새로 생긴다", "classification": "풀 CQRS 를 대안으로 두고 저장소는 분리하지 않기로 정했으며 승격 조건을 열린 질문으로 남겼다", "relations": [ "case:visibility-or-breaks-keyset-index", "question:promote-feed-visible-to-cqrs", "decision:read-projection-for-screen-query" ], "kind": "decision", "publication": "게시됨", "file": "jpa-feed-query-performance/decision/decision-keep-read-model-as-cqrs-lite.md", "status": "게시 전", "studioId": "7f248f68-ce2b-43ec-94ce-82324d0bd1a7", "assets": [], "evidenceFiles": [] } ] } } }, "candidates": [ { "id": "SSOT-collection-nplus1-grows-linearly-with-n", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#6-2", "final/document.md#6-4" ], "summary": "컬렉션 조회 수가 반환 아이템 수에 정확히 비례했다", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "reference:nplus1-quantitative-diagnosis", "reason": "이 측정의 분해와 검산이 그 Reference 의 규칙 3·4·5 에 이미 들어 있다. 독립 Case 로 빼도 이해가 달라지지 않는다" }, { "id": "SSOT-eager-toone-nplus1-without-access", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#7-2", "final/document.md#7-3", "final/document.md#7-4" ], "summary": "Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "case:eager-toone-nplus1-without-access", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-fetch-join-multibag-and-row-explosion", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#9-2", "final/document.md#9-3", "final/document.md#9-5" ], "summary": "Fetch Join으로 N+1을 해결하다 만난 MultiBag과 행 폭증", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "case:fetch-join-multibag-and-row-explosion", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-collection-fetch-join-in-memory-paging", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#10-2", "final/document.md#10-4" ], "summary": "Collection Fetch Join Pagination의 In-memory Paging", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "case:collection-fetch-join-in-memory-paging", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-batch-fetch-collapses-query-count", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#11-2", "final/document.md#11-3", "final/document.md#11-4", "final/document.md#11-6" ], "summary": "배치 페치가 왕복을 87.9배 줄이고 페이징을 되살렸다", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "decision:batch-fetch-for-entity-graph", "reason": "2,022 → 23 과 페이지 크기 적재는 그 Decision 의 근거 절과 fetch-strategy-selection 이 이미 담는다" }, { "id": "SSOT-projection-row-over-fetch", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#12-2", "final/document.md#12-4", "final/document.md#12-6" ], "summary": "Projection 이후에도 1,509행을 읽은 Row Over-fetch", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "case:projection-row-over-fetch", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-top-n-per-group-three-plans", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#13-2", "final/document.md#13-3", "final/document.md#13-4", "final/document.md#13-5" ], "summary": "같은 top-3을 만드는 세 방법이 읽는 행수는 달랐다", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "reference:top-n-per-group-selection", "reason": "세 방식의 buffers 204·430, 인덱스 토글 168 → 4,446, K 곡선 60·695·1,509 가 그 Reference 의 규칙 2~6 에 전부 있다" }, { "id": "SSOT-visibility-or-breaks-keyset-index", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#14-2", "final/document.md#14-3", "final/document.md#14-5" ], "summary": "Visibility OR이 Keyset Index를 깨뜨린 문제", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "case:visibility-or-breaks-keyset-index", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-visibility-three-plans-compared", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#15-2", "final/document.md#15-3", "final/document.md#15-4" ], "summary": "가시성 세 분기를 처리하는 세 방식의 실행계획을 갈랐다", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "reference:feed-visibility-query-pattern", "reason": "후보 1,500 과 buffers 122·200·1 이 그 Reference 와 promote-feed-visible-to-cqrs 의 사실 절에 이미 있다" }, { "id": "SSOT-unified-query-stacks-three-techniques", "kindCandidate": "CASE", "sourceRefs": [ "final/document.md#16-2", "final/document.md#16-3" ], "summary": "세 기법을 한 쿼리에 겹쳐도 Sort 노드가 생기지 않았다", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "question:promote-feed-visible-to-cqrs", "reason": "가장 깊은 페이지의 19 행 대 200 행이 그 Question 의 사실 절에 있다. §16 의 나머지는 어느 기록도 인용하지 않는다" }, { "id": "SSOT-eager-toone-secondary-select", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#5-4", "final/document.md#7-5" ], "summary": "EAGER는 로딩 시점 계약이지 JOIN 보장이 아니다", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "reference:fetch-type-vs-fetch-strategy", "reason": "EAGER 가 2차 SELECT 로 채워진다는 설명이 그 Reference 의 목적·규칙에 이미 들어 있다" }, { "id": "SSOT-nplus1-quantitative-diagnosis", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#6-1", "final/document.md#7-1" ], "summary": "JPA N+1 정량 진단 기준", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:nplus1-quantitative-diagnosis", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-fetch-type-vs-fetch-strategy", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#5-4", "final/document.md#7-5" ], "summary": "Fetch Type과 Fetch Strategy 구분", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:fetch-type-vs-fetch-strategy", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-fetch-strategy-selection", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#9-4", "final/document.md#11-5", "final/document.md#12-5" ], "summary": "Fetch Join · Batch · Projection 선택 기준", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:fetch-strategy-selection", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-top-n-per-group-selection", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#13-1", "final/document.md#13-6" ], "summary": "Top-N-per-group 선택 기준", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:top-n-per-group-selection", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-keyset-pagination-design", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#14-1", "final/document.md#14-4" ], "summary": "Keyset Pagination 설계 기준", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:keyset-pagination-design", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-feed-visibility-query-pattern", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#15-1", "final/document.md#15-4" ], "summary": "Feed Visibility Query Pattern", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:feed-visibility-query-pattern", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-postgresql-query-plan-measurement", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#4-1", "final/document.md#4-6", "final/document.md#4-7", "final/document.md#6-4" ], "summary": "PostgreSQL Query Plan 측정 기준", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "reference:postgresql-query-plan-measurement", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-cardinality-estimate-after-analyze", "kindCandidate": "QUESTION", "sourceRefs": [ "final/document.md#6-4", "final/document.md#4-6" ], "summary": "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "question:cardinality-estimate-after-analyze", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-isolate-round-trip-and-row-volume", "kindCandidate": "QUESTION", "sourceRefs": [ "final/document.md#4-5", "final/document.md#6-2" ], "summary": "Round Trip과 Row Volume을 독립 측정할 것인가", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "question:isolate-round-trip-and-row-volume", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-nullable-first-highlighted-at", "kindCandidate": "QUESTION", "sourceRefs": [ "final/document.md#3-1", "final/document.md#14-4" ], "summary": "Highlight 없는 FeedItem을 허용할 것인가", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "question:nullable-first-highlighted-at", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-promote-feed-visible-to-cqrs", "kindCandidate": "QUESTION", "sourceRefs": [ "final/document.md#15-5", "final/document.md#17-3", "final/document.md#18" ], "summary": "feed_visible을 Production CQRS로 승격할 것인가", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "question:promote-feed-visible-to-cqrs", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-concurrency-stability", "kindCandidate": "QUESTION", "sourceRefs": [ "final/document.md#4-5", "final/document.md#18" ], "summary": "실제 동시 트래픽에서도 이 구조가 안정적인가", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "question:concurrency-stability", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-measure-plan-on-real-postgresql", "kindCandidate": "DECISION", "sourceRefs": [ "final/document.md#4-1", "final/document.md#4-6" ], "summary": "Query Plan은 실제 PostgreSQL에서 측정한다", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "decision:measure-plan-on-real-postgresql", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-query-strategy-behind-port", "kindCandidate": "DECISION", "sourceRefs": [ "final/document.md#5-2" ], "summary": "Query Strategy는 FeedQueryPort 뒤에서 소유한다", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "decision:query-strategy-behind-port", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-no-collection-fetch-join-with-pagination", "kindCandidate": "DECISION", "sourceRefs": [ "final/document.md#10-5", "final/document.md#9-6" ], "summary": "Collection Fetch Join과 Pagination을 같이 사용하지 않는다", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "decision:no-collection-fetch-join-with-pagination", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-batch-fetch-for-entity-graph", "kindCandidate": "DECISION", "sourceRefs": [ "final/document.md#11-1", "final/document.md#11-5" ], "summary": "Entity Graph 조회에는 Batch Fetch를 사용한다", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "decision:batch-fetch-for-entity-graph", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-read-projection-for-screen-query", "kindCandidate": "DECISION", "sourceRefs": [ "final/document.md#12-1", "final/document.md#12-5" ], "summary": "화면 조회는 Read Projection을 사용한다", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "decision:read-projection-for-screen-query", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-keyset-for-feed-pagination", "kindCandidate": "DECISION", "sourceRefs": [ "final/document.md#14-1", "final/document.md#14-4" ], "summary": "Feed Pagination은 Keyset을 사용한다", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "decision:keyset-for-feed-pagination", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-keep-read-model-as-cqrs-lite", "kindCandidate": "DECISION", "sourceRefs": [ "final/document.md#17-1", "final/document.md#17-2" ], "summary": "현재 Read Model은 CQRS-lite로 유지한다", "disposition": "PROMOTE", "dispositionReview": "CONFIRMED", "target": "decision:keep-read-model-as-cqrs-lite", "reason": "SSOT 가 이 주장을 독립된 관측·규칙·결정·질문으로 담고 있고 독립성 검사를 통과한다" }, { "id": "SSOT-4-6-engine-plan-differences", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#4-6" ], "summary": "DB 엔진마다 실행계획과 인덱스가 다른 네 축", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "decision:measure-plan-on-real-postgresql", "reason": "실제 PostgreSQL 을 고른 결정의 근거 그 자체다. 그 Decision 의 근거 절로 들어간다" }, { "id": "SSOT-4-7-tool-selection", "kindCandidate": "REFERENCE", "sourceRefs": [ "final/document.md#4-7" ], "summary": "전용 측정 도구 대신 내장 3종을 고른 기준", "disposition": "MERGE_INTO", "dispositionReview": "CONFIRMED", "target": "reference:postgresql-query-plan-measurement", "reason": "정밀도를 주장 강도에 맞춘다는 같은 규칙이다. 그 Reference 의 적용 조건 절로 들어간다" }, { "id": "SSOT-3-2-resource-id-value-object", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#3-2" ], "summary": "식별자를 ResourceId 값 객체로 만든 네 가지 이유", "disposition": "KEEP_IN_SSOT", "dispositionReview": "CONFIRMED", "target": null, "reason": "이 Topic 의 독자 질문은 조회 비용이다. 식별자 모델링은 그 물음에 답하지 않는다" }, { "id": "SSOT-3-3-narrow-association-getters", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#3-3" ], "summary": "퍼시스턴스 엔티티의 연관 게터를 package-private 로 좁힌 이유", "disposition": "KEEP_IN_SSOT", "dispositionReview": "CONFIRMED", "target": null, "reason": "캡슐화 관례이고 조회 비용을 가르지 않는다. 분석에는 남기고 독립 기록으로 만들지 않는다" }, { "id": "SSOT-4-2-4-3-seed-distribution", "kindCandidate": "CONCEPT", "sourceRefs": [ "final/document.md#4-2", "final/document.md#4-3" ], "summary": "시더가 네 엔티티를 서로 다른 규칙으로 만들고 하이라이트를 편중 분포로 두는 이유", "disposition": "KEEP_IN_SSOT", "dispositionReview": "CONFIRMED", "target": null, "reason": "측정 환경의 구성이다. 필요한 만큼은 각 Case 의 검증 환경과 격리 질문이 이미 담는다" }, { "id": "SSOT-18-deferred-topics", "kindCandidate": "OPEN_QUESTION", "sourceRefs": [ "final/document.md#18" ], "summary": "OSIV·커넥션 풀·쓰기 N+1·리액티브·탐지기·NoSQL 임베드", "disposition": "KEEP_IN_SSOT", "dispositionReview": "CONFIRMED", "target": null, "reason": "SSOT 가 이번 범위 밖이라고 스스로 적은 목록이다. 미지수도 다음 검증도 없어 Question 이 되지 않는다" } ], "unlisted": [], "history": {}, "counts": { "topics": 1, "nodes": 24, "written": 24, "unwritten": 0, "unlisted": 0, "candidates": 36 } }