Files
document-haness/docs/n+1liner/tech-log-studio/tech-log-tree.json
T

1299 lines
58 KiB
JSON

{
"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": "bb33f443aaffe90be9e848fa49864b299a2ab628c300e2662ef32ac1c4d6964a",
"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": "원본이 불완전하거나 서로 어긋난다"
}
},
"assetLedger": {
"note": "final/assets/diagrams/ 의 그림 10장 가운데 글감에 배정한 것과, 배정하지 않은 것의 이유를 적는다",
"assigned": [
"cartesian-row-multiplication",
"projection-row-over-fetch",
"eager-lazy-query-sequence",
"batch-fetch-in-clause"
],
"unassigned": [
{
"asset": [
"nplus1-query-fanout"
],
"reason": "컬렉션(Highlight) N+1 의 팬아웃을 그린 그림인데, 그 주장을 담은 Case 가 없다. case:eager-toone-nplus1-without-access 는 ToOne(user·page) 을 다루고 case:collection-fetch-join-in-memory-paging 은 fetch join 과 페이징을 다룬다"
},
{
"asset": [
"baseline-schema",
"target-schema",
"query-port-boundary",
"strategy-journey"
],
"reason": "데이터 모델·포트 경계·전체 여정을 그린 그림이라 글감 하나에 속하지 않는다. final/document.md 가 제자리에서 싣는다"
},
{
"asset": [
"skew-profile"
],
"reason": "표로 되는 그림이다 — 분포 셋을 네 속성으로 늘어놓았을 뿐 관계선이 없다. §4.3 에 순위별 하이라이트 수 표가 이미 있다"
}
],
"removed": [
{
"asset": [
"in-memory-paging",
"keyset-vs-offset",
"top-n-per-group"
],
"reason": "표로 되는 그림이라 지웠다. 같은 비교가 기록의 표에 더 자세히 있었다"
}
]
},
"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"
],
"ssot-assets": [
"eager-lazy-query-sequence"
],
"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"
],
"assetFiles": [
"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"
],
"ssot-assets": [
"cartesian-row-multiplication"
],
"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"
],
"assetFiles": [
"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",
"ssot-assets": [
"batch-fetch-in-clause"
],
"publication": "게시됨",
"file": "jpa-feed-query-performance/case/case-collection-fetch-join-in-memory-paging.md",
"status": "게시 전",
"studioId": "c1158754-e3d2-47b8-bb41-81787c0ca84b",
"assets": [
"batch-fetch-in-clause"
],
"assetFiles": [
"batch-fetch-in-clause"
],
"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"
],
"ssot-assets": [
"projection-row-over-fetch"
],
"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"
],
"assetFiles": [
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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": [],
"assetFiles": [],
"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
}
}