feat: 문서 구조 변경 및 tech-visual 스킬 추가
This commit is contained in:
+135
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "baseline-schema",
|
||||
"title": "기준선 스키마의 관계",
|
||||
"question": "현재 기준선에서 users, pages, feed_items, highlights는 어떻게 연결되는가?",
|
||||
"type": "erd",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"백엔드 개발자"
|
||||
],
|
||||
"summary": "users와 pages가 feed_items에 연결되고, 각 feed_item은 여러 highlights를 가진다.",
|
||||
"alt": "users와 pages에서 feed_items로 모이고 highlights로 이어지는 기준선 관계도.",
|
||||
"long_description": "왼쪽의 users와 pages가 각각 중앙의 feed_items에 연결된다. feed_items는 오른쪽의 highlights로 이어진다. 간선은 user와 page 각각에 여러 feed_item이 연결되고, 한 feed_item에 여러 highlight가 연결되는 관계를 나타낸다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "baseline-schema",
|
||||
"line": 39
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "component-flow",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"rationale": "두 시작 엔티티가 feed_items로 모이고 highlights로 이어지는 명시 관계를 왼쪽에서 오른쪽으로 읽는 연결 구조가 가장 직접적이다.",
|
||||
"focus_node": "feed-items"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "users",
|
||||
"label": "users",
|
||||
"kind": "entity",
|
||||
"role": "source",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "pages",
|
||||
"label": "pages",
|
||||
"kind": "entity",
|
||||
"role": "source",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 36,
|
||||
"end_line": 36
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-items",
|
||||
"label": "feed_items",
|
||||
"kind": "entity",
|
||||
"role": "store",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "highlights",
|
||||
"label": "highlights",
|
||||
"kind": "entity",
|
||||
"role": "sink",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 37,
|
||||
"end_line": 37
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "users-have-feed-items",
|
||||
"from": "users",
|
||||
"to": "feed-items",
|
||||
"label": "여러 feed_item을 가짐",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "pages-have-feed-items",
|
||||
"from": "pages",
|
||||
"to": "feed-items",
|
||||
"label": "여러 feed_item이 딸림",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 36,
|
||||
"end_line": 36
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-items-have-highlights",
|
||||
"from": "feed-items",
|
||||
"to": "highlights",
|
||||
"label": "여러 highlights를 가짐",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 37,
|
||||
"end_line": 37
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "기준선에 명시된 세 관계만 같은 엔티티 추상화 수준에서 표현했다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "eager-lazy-query-sequence",
|
||||
"title": "EAGER 2차 조회는 반환 전에, LAZY highlights 조회는 매핑 접근 뒤에 실행된다",
|
||||
"question": "루트 피드 조회부터 EAGER ToOne과 LAZY highlights 조회까지 SQL은 어떤 순서로 발생하는가?",
|
||||
"type": "sequence",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"JPA·Hibernate를 사용하는 백엔드 개발자",
|
||||
"쿼리 성능 분석자"
|
||||
],
|
||||
"summary": "Hibernate는 feed_items를 먼저 조회하고 EAGER user·page를 2차 SELECT로 채운 뒤 반환하며, 매핑 중 getHighlights() 접근이 생긴 다음 LAZY highlights SELECT를 실행한다.",
|
||||
"alt": "loadFeed 매핑, Hibernate, PostgreSQL 사이에서 루트 SELECT, EAGER user·page 2차 SELECT, getHighlights 접근, LAZY highlights SELECT가 차례로 일어나는 시퀀스.",
|
||||
"long_description": "세 참가자를 왼쪽부터 loadFeed DTO 매핑, Hibernate, PostgreSQL 순으로 읽는다. loadFeed가 findAllBy 파생 쿼리를 호출하면 Hibernate가 PostgreSQL에서 feed_items를 먼저 조회한다. 이어 fetch join되지 않은 EAGER user와 page를 별도의 2차 SELECT로 채우고, 반환 시점까지 로딩된 FeedItem을 loadFeed에 돌려준다. 이후 DTO 매핑이 getHighlights()에 접근하면 Hibernate가 해당 아이템의 highlights 컬렉션 SELECT를 실행한다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "eager-lazy-query-sequence",
|
||||
"line": 324
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "sequence",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"payment-approval-sequence"
|
||||
],
|
||||
"rationale": "문서가 루트 조회, EAGER 2차 SELECT, 반환, 매핑 접근, LAZY SELECT의 시간 순서를 명시하므로 참가자별 메시지를 위에서 아래로 배열하는 sequence 구성이 적합하다.",
|
||||
"focus_node": "hibernate"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "load-feed-mapping",
|
||||
"label": "loadFeed DTO 매핑",
|
||||
"kind": "participant",
|
||||
"role": "participant",
|
||||
"description": "FeedItem을 순회하며 응답 DTO를 조립하고 highlights 게터에 접근하는 호출자.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 322,
|
||||
"end_line": 322
|
||||
},
|
||||
{
|
||||
"start_line": 449,
|
||||
"end_line": 451
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "hibernate",
|
||||
"label": "Hibernate",
|
||||
"kind": "participant",
|
||||
"role": "participant",
|
||||
"emphasis": "primary",
|
||||
"description": "파생 쿼리의 루트 조회와 EAGER 2차 SELECT, LAZY 컬렉션 초기화를 수행하는 JPA provider.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 320,
|
||||
"end_line": 322
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "postgresql",
|
||||
"label": "PostgreSQL",
|
||||
"kind": "participant",
|
||||
"role": "participant",
|
||||
"shape": "database",
|
||||
"description": "Hibernate가 루트 및 연관 SELECT를 실행하는 데이터베이스.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 417,
|
||||
"end_line": 422
|
||||
},
|
||||
{
|
||||
"start_line": 447,
|
||||
"end_line": 447
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "m1-find-all",
|
||||
"from": "load-feed-mapping",
|
||||
"to": "hibernate",
|
||||
"label": "findAllBy(...)",
|
||||
"kind": "request",
|
||||
"order": 1,
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 320,
|
||||
"end_line": 320
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "m2-root-select",
|
||||
"from": "hibernate",
|
||||
"to": "postgresql",
|
||||
"label": "SELECT feed_items",
|
||||
"kind": "data",
|
||||
"order": 2,
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 320,
|
||||
"end_line": 320
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "m3-eager-secondary-selects",
|
||||
"from": "hibernate",
|
||||
"to": "postgresql",
|
||||
"label": "SELECT user / page · EAGER 2차",
|
||||
"kind": "data",
|
||||
"order": 3,
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 318,
|
||||
"end_line": 321
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "m4-return-eager-loaded-items",
|
||||
"from": "hibernate",
|
||||
"to": "load-feed-mapping",
|
||||
"label": "EAGER 연관이 채워진 FeedItem 반환",
|
||||
"kind": "response",
|
||||
"style": "dashed",
|
||||
"order": 4,
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 318,
|
||||
"end_line": 320
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "m5-access-highlights",
|
||||
"from": "load-feed-mapping",
|
||||
"to": "hibernate",
|
||||
"label": "매핑 중 getHighlights() 접근",
|
||||
"kind": "request",
|
||||
"order": 5,
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 322,
|
||||
"end_line": 322
|
||||
},
|
||||
{
|
||||
"start_line": 449,
|
||||
"end_line": 451
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "m6-lazy-highlights-select",
|
||||
"from": "hibernate",
|
||||
"to": "postgresql",
|
||||
"label": "SELECT highlights WHERE feed_item_id = ?",
|
||||
"kind": "data",
|
||||
"order": 6,
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 322,
|
||||
"end_line": 322
|
||||
},
|
||||
{
|
||||
"start_line": 429,
|
||||
"end_line": 430
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "EAGER와 LAZY의 차이를 정적 관계가 아니라 실제 SQL 발생 순서와 접근 시점으로 보여준다."
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "nplus1-query-fanout",
|
||||
"title": "반환 부모 수 N이 컬렉션 초기화와 자식 SELECT 횟수를 결정한다",
|
||||
"question": "왜 한 번의 피드 요청에서 반환한 FeedItem 수 N이 Highlight 추가 조회 N회로 이어지는가?",
|
||||
"type": "data-flow",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"JPA 기반 피드 조회의 N+1 원인을 진단하는 개발자"
|
||||
],
|
||||
"summary": "한 페이지에서 N개의 FeedItem을 반환하면 각 부모의 Highlight 컬렉션을 한 번씩 초기화해 추가 SELECT도 N회 발생한다.",
|
||||
"alt": "FeedItem N개를 반환하는 loadFeed 요청이 컬렉션 초기화 N회와 Highlight SELECT N회로 이어지는 인과 흐름도.",
|
||||
"long_description": "왼쪽의 loadFeed 요청은 한 페이지에서 N개의 FeedItem을 반환한다. 매핑이 각 부모의 Highlight 컬렉션에 접근하면 컬렉션 초기화가 부모마다 한 번씩 일어나 총 N회가 된다. 현재 기준선에서는 배치나 서브셀렉트가 없어 초기화 한 번마다 Highlight SELECT도 한 번 실행되므로 추가 조회가 N회 발생한다. 각 SELECT는 해당 부모의 Highlight 자식 행을 전부 읽는다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "nplus1-query-fanout",
|
||||
"line": 380
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "component-flow",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"rationale": "원문은 서로 다른 저장소로 분산되는 라우팅이 아니라 반환 부모 수가 컬렉션 초기화와 반복 SELECT를 차례로 유발하는 인과 경로를 설명하므로 component-flow가 가장 정확하다.",
|
||||
"focus_node": "collection-initializations"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "feed-request",
|
||||
"label": "loadFeed(0, N) → FeedItem N개",
|
||||
"kind": "request",
|
||||
"role": "source",
|
||||
"shape": "box",
|
||||
"details": [
|
||||
"page size = 반환 부모 수 N"
|
||||
],
|
||||
"description": "한 요청에서 반환한 부모 수 N이 N+1 증가 계수가 되는 피드 조회.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 341,
|
||||
"end_line": 341
|
||||
},
|
||||
{
|
||||
"start_line": 398,
|
||||
"end_line": 398
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "collection-initializations",
|
||||
"label": "Highlight 컬렉션 초기화 N회",
|
||||
"kind": "operation",
|
||||
"role": "service",
|
||||
"shape": "box",
|
||||
"details": [
|
||||
"collectionFetches = N"
|
||||
],
|
||||
"emphasis": "primary",
|
||||
"description": "각 FeedItem의 지연 컬렉션 접근이 부모마다 한 번의 초기화를 만든 결과.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 337,
|
||||
"end_line": 337
|
||||
},
|
||||
{
|
||||
"start_line": 378,
|
||||
"end_line": 384
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "highlight-selects",
|
||||
"label": "Highlight SELECT N회",
|
||||
"kind": "query",
|
||||
"role": "sink",
|
||||
"shape": "box",
|
||||
"details": [
|
||||
"부모별 자식 행 전부 조회"
|
||||
],
|
||||
"description": "배치와 서브셀렉트가 없는 기준선에서 컬렉션 초기화마다 실행되는 자식 SELECT.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 337,
|
||||
"end_line": 337
|
||||
},
|
||||
{
|
||||
"start_line": 384,
|
||||
"end_line": 385
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "parents-trigger-initialization",
|
||||
"from": "feed-request",
|
||||
"to": "collection-initializations",
|
||||
"label": "아이템마다 컬렉션 접근",
|
||||
"kind": "request",
|
||||
"style": "solid",
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 384,
|
||||
"end_line": 384
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "initialization-runs-select",
|
||||
"from": "collection-initializations",
|
||||
"to": "highlight-selects",
|
||||
"label": "초기화마다 SELECT 1회",
|
||||
"kind": "request",
|
||||
"style": "solid",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 337,
|
||||
"end_line": 337
|
||||
},
|
||||
{
|
||||
"start_line": 384,
|
||||
"end_line": 384
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "정량 표는 본문에 남기고, 그림은 부모 수 N이 초기화와 SELECT 횟수 N을 만드는 단일 인과 관계에 집중한다."
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "query-port-boundary",
|
||||
"title": "조회 전략은 FeedQueryPort 뒤의 퍼시스턴스 어댑터에 격리된다",
|
||||
"question": "GET /feed 조회는 어떤 상위 계층을 거쳐 포트에 도달하며, 실제 조회 전략은 어디에 격리되는가?",
|
||||
"type": "architecture",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"백엔드 개발자",
|
||||
"아키텍처 검토자"
|
||||
],
|
||||
"summary": "FeedController는 조회 유스케이스를 호출하고, 유스케이스는 FeedQueryPort에 의존하며, FeedQueryAdapter가 포트를 구현해 PostgreSQL 조회 전략을 맡는다.",
|
||||
"alt": "GET /feed를 받는 FeedController에서 GetFeedUseCase와 FeedQueryPort로 이어지고 FeedQueryAdapter가 포트를 구현하는 포트·어댑터 구조.",
|
||||
"long_description": "왼쪽의 FeedController가 GET /feed 요청을 받아 중앙의 GetFeedUseCase에 조회를 위임한다. 유스케이스는 오른쪽의 FeedQueryPort에 조회를 의존한다. FeedQueryAdapter는 FeedQueryPort를 구현하는 아웃바운드 어댑터이며 PostgreSQL 조회를 수행한다. Fetch Join, Batch Fetch, DTO Projection, 윈도우 함수 같은 구체 전략은 이 어댑터의 책임이므로 상위 계층은 전략 교체의 영향을 받지 않는다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "query-port-boundary",
|
||||
"line": 297
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "ports-adapters",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"order-ports-adapters"
|
||||
],
|
||||
"rationale": "문서의 핵심은 상위 웹·애플리케이션 계층과 교체 가능한 조회 전략 사이의 포트 의존 및 어댑터 구현 방향이므로 ports-adapters 구성이 직접 답한다.",
|
||||
"focus_node": "get-feed-use-case"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "feed-controller",
|
||||
"label": "FeedController",
|
||||
"kind": "adapter",
|
||||
"role": "inbound-adapter",
|
||||
"details": [
|
||||
"GET /feed"
|
||||
],
|
||||
"description": "조회 입력과 FeedSummary 반환 형태만 아는 웹 계층.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "get-feed-use-case",
|
||||
"label": "GetFeedUseCase",
|
||||
"kind": "application",
|
||||
"role": "core",
|
||||
"shape": "hexagon",
|
||||
"emphasis": "primary",
|
||||
"description": "조회 사용자, 페이지 크기, FeedSummary 계약만 아는 애플리케이션 계층.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-query-port",
|
||||
"label": "FeedQueryPort",
|
||||
"kind": "interface",
|
||||
"role": "port",
|
||||
"shape": "port",
|
||||
"description": "상위 계층과 구체 조회 전략을 분리하는 조회 포트.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
},
|
||||
{
|
||||
"start_line": 299,
|
||||
"end_line": 299
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-query-adapter",
|
||||
"label": "FeedQueryAdapter",
|
||||
"kind": "adapter",
|
||||
"role": "outbound-adapter",
|
||||
"description": "FeedQueryPort를 구현하며 구체 조회 전략을 책임지는 퍼시스턴스 어댑터.",
|
||||
"details": [
|
||||
"PostgreSQL 조회",
|
||||
"Fetch Join · Batch Fetch",
|
||||
"DTO Projection · 윈도우 함수"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
},
|
||||
{
|
||||
"start_line": 299,
|
||||
"end_line": 299
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "controller-to-use-case",
|
||||
"from": "feed-controller",
|
||||
"to": "get-feed-use-case",
|
||||
"label": "GET /feed 조회 위임",
|
||||
"kind": "request",
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "use-case-to-port",
|
||||
"from": "get-feed-use-case",
|
||||
"to": "feed-query-port",
|
||||
"label": "조회 의존",
|
||||
"kind": "dependency",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "adapter-implements-port",
|
||||
"from": "feed-query-adapter",
|
||||
"to": "feed-query-port",
|
||||
"label": "implements",
|
||||
"kind": "dependency",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "구체 조회 기법보다 웹·애플리케이션 계층, FeedQueryPort, 퍼시스턴스 어댑터 사이의 의존 경계를 한 수준에서 보여준다."
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "skew-profile",
|
||||
"title": "Zipf-like 분포만 무거운 머리와 긴 꼬리를 함께 재현한다",
|
||||
"question": "균일·정규분포와 비교할 때 왜 Zipf-like 분포가 하이라이트 조회의 스트레스 데이터에 적합한가?",
|
||||
"type": "concept",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"백엔드 엔지니어",
|
||||
"성능 실험 설계를 검토하는 독자"
|
||||
],
|
||||
"summary": "균일분포와 정규분포는 극단적으로 많은 소수를 없애지만, 선택한 Zipf-like 합성 분포는 무거운 머리와 긴 꼬리를 만들어 대량 하이라이트와 Top-N 필요성을 재현한다.",
|
||||
"alt": "균일분포, 정규분포, Zipf-like 합성 분포를 분포 형태와 극단적 소수, 스트레스 조건 재현 여부, 선택 결과로 나란히 비교한 도표.",
|
||||
"long_description": "왼쪽부터 균일분포, 정규분포, Zipf-like 합성 분포를 같은 네 기준으로 비교한다. 균일분포는 모든 아이템이 3개이고, 정규분포는 평균 근처에 몰려 둘 다 극단적으로 많은 소수를 만들지 못하므로 제외된다. Zipf-like 분포는 소수의 인기 아이템이 압도적인 무거운 머리와 나머지의 긴 꼬리를 만들며, 지수 s=1.15와 상한 500·하한 1을 사용해 매우 많은 하이라이트 조건과 Top-N 필요성을 재현하는 합성 스트레스 분포로 선택된다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "skew-profile",
|
||||
"line": 192
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "comparison",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"contract-comparison"
|
||||
],
|
||||
"rationale": "문서가 균일·정규분포를 제외하고 Zipf-like 분포를 선택한 근거를 같은 비교 기준으로 직접 대조하므로, 호출 관계를 만들지 않는 정렬된 comparison 구성이 핵심 주장에 가장 적합하다.",
|
||||
"focus_node": "zipf-like"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "uniform",
|
||||
"label": "균일분포",
|
||||
"kind": "distribution",
|
||||
"role": "option",
|
||||
"description": "모든 아이템에 하이라이트 3개를 주는 분포로, 매우 많은 하이라이트 조건을 재현하지 못한다.",
|
||||
"details": [
|
||||
"분포 형태: 모두 3개",
|
||||
"극단적 소수: 없음",
|
||||
"스트레스 조건: 재현 못함",
|
||||
"선택 결과: 제외"
|
||||
],
|
||||
"emphasis": "muted",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 177,
|
||||
"end_line": 177
|
||||
},
|
||||
{
|
||||
"start_line": 195,
|
||||
"end_line": 195
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "normal",
|
||||
"label": "정규분포",
|
||||
"kind": "distribution",
|
||||
"role": "option",
|
||||
"description": "평균 근처에 몰려 극단적으로 많은 소수와 무거운 머리를 만들지 못하는 분포다.",
|
||||
"details": [
|
||||
"분포 형태: 평균 근처 집중",
|
||||
"극단적 소수: 없음",
|
||||
"스트레스 조건: 재현 못함",
|
||||
"선택 결과: 제외"
|
||||
],
|
||||
"emphasis": "muted",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 177,
|
||||
"end_line": 177
|
||||
},
|
||||
{
|
||||
"start_line": 196,
|
||||
"end_line": 196
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "zipf-like",
|
||||
"label": "Zipf-like 합성 분포",
|
||||
"kind": "distribution",
|
||||
"role": "option",
|
||||
"description": "순위 기반 지수로 편중 강도를 조절하며 무거운 머리와 긴 꼬리를 재현하는 합성 스트레스 분포다.",
|
||||
"details": [
|
||||
"분포 형태: 무거운 머리 + 긴 꼬리",
|
||||
"극단적 소수: 있음 · 1위 500개",
|
||||
"스트레스 조건: 재현",
|
||||
"선택 결과: s=1.15 합성 분포"
|
||||
],
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 177,
|
||||
"end_line": 177
|
||||
},
|
||||
{
|
||||
"start_line": 181,
|
||||
"end_line": 181
|
||||
},
|
||||
{
|
||||
"start_line": 184,
|
||||
"end_line": 184
|
||||
},
|
||||
{
|
||||
"start_line": 188,
|
||||
"end_line": 190
|
||||
},
|
||||
{
|
||||
"start_line": 197,
|
||||
"end_line": 197
|
||||
},
|
||||
{
|
||||
"start_line": 203,
|
||||
"end_line": 203
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "세 분포를 동일한 네 항목으로 맞춰 비교하고, 문서가 직접 제시한 제외·선택 이유만 포함했다."
|
||||
}
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "strategy-journey",
|
||||
"title": "두 형제 문제는 Fetch Join에서 합류한 뒤 단계별 해법으로 최종 구조가 된다",
|
||||
"question": "과제 요구사항에서 시작해 기준선의 N1·N2 분기와 실패·개선을 거쳐 최종 피드 조회 구조는 어떻게 발전하는가?",
|
||||
"type": "flow",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"백엔드 엔지니어",
|
||||
"성능 개선 과정을 검토하는 독자"
|
||||
],
|
||||
"summary": "요구사항과 모델을 거친 기준선에서 N1·N2가 동시에 갈라져 Fetch Join으로 합류하고, 실패 뒤 Batch Fetch·DTO Projection·Top-3·Keyset·가시성 인덱싱을 차례로 거쳐 최종 피드 조회 구조에 도달한다.",
|
||||
"alt": "요구사항과 모델에서 기준선으로 진행한 뒤 N1과 N2로 분기하고 Fetch Join에서 합류해, 실패와 다섯 개선 단계를 거쳐 최종 피드 조회 구조에 이르는 흐름도.",
|
||||
"long_description": "왼쪽에서 과제 요구사항, 도메인·데이터 모델, 최초 피드 조회 기준선 순으로 시작한다. 기준선에서 컬렉션 N+1(N1)과 User·Page 연관의 숨은 쿼리(N2)가 서로 앞뒤가 아닌 형제 문제로 동시에 갈라지고, 두 경로는 Fetch Join 시도에서 합류한다. 이 시도는 다중 컬렉션·페이징 실패로 이어진다. 마지막 노드는 Batch Fetch, DTO Projection, 아이템별 Top-3, Keyset Pagination, 가시성 조건 인덱싱을 거쳐 최종 피드 조회 구조에 도달하는 순서를 담는다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "strategy-journey",
|
||||
"line": 27
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "component-flow",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"rationale": "요구사항에서 최종 조회 구조까지 한 방향으로 발전하면서 기준선의 두 형제 문제가 분기했다가 Fetch Join 시도에서 합류하므로, 좌측 출발점·중앙의 분기와 합류·우측 종착점을 갖는 component-flow가 전체 여정을 가장 직접적으로 드러낸다.",
|
||||
"focus_node": "fetch-join-failure"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "requirements-model-baseline",
|
||||
"label": "요구사항·모델·기준선",
|
||||
"kind": "journey-stage",
|
||||
"role": "source",
|
||||
"description": "전체 여정은 과제 요구사항에서 도메인·데이터 모델을 거쳐 최초 피드 조회 기준선으로 시작한다.",
|
||||
"details": [
|
||||
"1 과제 요구사항",
|
||||
"2 도메인·데이터 모델",
|
||||
"3 최초 피드 조회"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "collection-n-plus-one",
|
||||
"label": "컬렉션 N+1 (N1)",
|
||||
"kind": "problem",
|
||||
"role": "service",
|
||||
"description": "기준선에서 N2와 동시에 나타나는 컬렉션 조회 문제다.",
|
||||
"details": [
|
||||
"유형: 컬렉션 N+1",
|
||||
"발생: 기준선과 동시에",
|
||||
"관계: N2와 형제 문제",
|
||||
"진행: Fetch Join으로 합류"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "hidden-to-one-queries",
|
||||
"label": "User·Page 숨은 쿼리 (N2)",
|
||||
"kind": "problem",
|
||||
"role": "service",
|
||||
"description": "기준선에서 N1과 동시에 나타나는 User·Page 연관의 숨은 쿼리 문제다.",
|
||||
"details": [
|
||||
"유형: User·Page 숨은 쿼리",
|
||||
"발생: 기준선과 동시에",
|
||||
"관계: N1과 형제 문제",
|
||||
"진행: Fetch Join으로 합류"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "fetch-join-failure",
|
||||
"label": "Fetch Join → 실패",
|
||||
"kind": "query-strategy",
|
||||
"role": "service",
|
||||
"description": "N1과 N2가 Fetch Join 시도에서 합류한 뒤 다중 컬렉션·페이징 실패로 이어진다.",
|
||||
"details": [
|
||||
"1 N1·N2 합류",
|
||||
"2 다중 컬렉션·페이징 실패"
|
||||
],
|
||||
"emphasis": "warning",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "improvement-chain",
|
||||
"label": "후속 개선 → 최종 구조",
|
||||
"kind": "query-strategy",
|
||||
"role": "sink",
|
||||
"description": "실패 뒤 다섯 조회 전략 단계가 명시된 순서로 발전해 최종 피드 조회 구조를 만든다.",
|
||||
"details": [
|
||||
"1 Batch Fetch",
|
||||
"2 DTO Projection",
|
||||
"3 아이템별 Top-3",
|
||||
"4 Keyset Pagination",
|
||||
"5 가시성 조건 인덱싱",
|
||||
"6 최종 피드 조회 구조"
|
||||
],
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "baseline-reveals-n1",
|
||||
"from": "requirements-model-baseline",
|
||||
"to": "collection-n-plus-one",
|
||||
"label": "기준선에서 갈라짐",
|
||||
"kind": "problem",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "baseline-reveals-n2",
|
||||
"from": "requirements-model-baseline",
|
||||
"to": "hidden-to-one-queries",
|
||||
"label": "기준선에서 갈라짐",
|
||||
"kind": "problem",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "n1-joins-fetch-join",
|
||||
"from": "collection-n-plus-one",
|
||||
"to": "fetch-join-failure",
|
||||
"label": "Fetch Join으로 합류",
|
||||
"kind": "flow",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "n2-joins-fetch-join",
|
||||
"from": "hidden-to-one-queries",
|
||||
"to": "fetch-join-failure",
|
||||
"label": "Fetch Join으로 합류",
|
||||
"kind": "flow",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "failure-to-improvements",
|
||||
"from": "fetch-join-failure",
|
||||
"to": "improvement-chain",
|
||||
"label": "실패 뒤 단계별 개선",
|
||||
"kind": "improvement",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "line 25의 전체 순서를 유지하면서 문서 폭에 맞추기 위해 연속된 출발 세 단계, Fetch Join과 그 실패, 실패 뒤 다섯 개선 단계를 각각 하나의 단계 노드 안에 번호로 묶었다."
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "target-schema",
|
||||
"title": "목표 스키마에 추가되는 mention 관계",
|
||||
"question": "목표 모델에서 기존 users는 mentioned 사용자 역할로 feed_item_mentions에 어떻게 연결되는가?",
|
||||
"type": "erd",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"백엔드 개발자"
|
||||
],
|
||||
"summary": "기준선 관계에 feed_items와 기존 users를 잇는 feed_item_mentions 관계가 추가된다.",
|
||||
"alt": "기존 users를 mentioned 사용자 역할로 재사용해 feed_item_mentions와 연결한 5노드 목표 관계도.",
|
||||
"long_description": "왼쪽의 users와 pages가 중앙의 feed_items에 연결된다. 오른쪽에는 highlights와 feed_item_mentions가 놓인다. feed_items는 두 엔티티에 각각 연결되고, 기존 users도 mentioned 사용자 역할로 feed_item_mentions에 연결된다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "target-schema",
|
||||
"line": 45
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "component-flow",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"rationale": "기준선의 연결 경로와 feed_item_mentions를 통한 추가 연결을 하나의 방향성 있는 관계망으로 읽게 하는 구조가 목표 모델의 차이를 직접 드러낸다.",
|
||||
"focus_node": "feed-items"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "users",
|
||||
"label": "users",
|
||||
"kind": "entity",
|
||||
"role": "source",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "pages",
|
||||
"label": "pages",
|
||||
"kind": "entity",
|
||||
"role": "source",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 36,
|
||||
"end_line": 36
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-items",
|
||||
"label": "feed_items",
|
||||
"kind": "entity",
|
||||
"role": "store",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "highlights",
|
||||
"label": "highlights",
|
||||
"kind": "entity",
|
||||
"role": "sink",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 37,
|
||||
"end_line": 37
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-item-mentions",
|
||||
"label": "feed_item_mentions",
|
||||
"kind": "entity",
|
||||
"role": "sink",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 43,
|
||||
"end_line": 43
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "users-have-feed-items",
|
||||
"from": "users",
|
||||
"to": "feed-items",
|
||||
"label": "여러 feed_item을 가짐",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "pages-have-feed-items",
|
||||
"from": "pages",
|
||||
"to": "feed-items",
|
||||
"label": "여러 feed_item이 딸림",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 36,
|
||||
"end_line": 36
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-items-have-highlights",
|
||||
"from": "feed-items",
|
||||
"to": "highlights",
|
||||
"label": "여러 highlights를 가짐",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 37,
|
||||
"end_line": 37
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-items-to-mentions",
|
||||
"from": "feed-items",
|
||||
"to": "feed-item-mentions",
|
||||
"label": "피드 아이템을 연결",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 43,
|
||||
"end_line": 43
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "users-to-feed-item-mentions",
|
||||
"from": "users",
|
||||
"to": "feed-item-mentions",
|
||||
"label": "mentioned 사용자로 연결",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 43,
|
||||
"end_line": 43
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "기준선 관계를 유지하면서 line 43의 feed_item_mentions가 기존 users를 mentioned 사용자 역할로 참조하는 관계만 추가했다."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user