feat: 문서 구조 변경 및 tech-visual 스킬 추가
@@ -0,0 +1,12 @@
|
||||
# assets — 발표 슬라이드용 스크린샷
|
||||
|
||||
[../document.md](../document.md)는 실측 수치·실행계획을 텍스트로 담아 그대로 렌더된다. 슬라이드에서 화면 캡처로 보여주고 싶을 때 아래를 여기에 저장한다.
|
||||
|
||||
| 파일명 | 캡처 대상 |
|
||||
|---|---|
|
||||
| `feed-nplus1-sql-log.png` | 피드 조회 시 highlights 조회가 아이템마다 반복되는 SQL 로그 |
|
||||
| `curve-console.png` | `=== L1 N=10/100/1000 ... collectionFetches=10/100/1000 ...` 곡선 콘솔 |
|
||||
| `explain-highlights-index-scan.png` | 반복되는 하이라이트 조회 EXPLAIN(`Index Scan ... Execution Time 0.173ms`) |
|
||||
| `explain-feed-items-seqscan.png` | 목록 쿼리 EXPLAIN(정렬키 인덱스 없어 Seq Scan + Sort) |
|
||||
|
||||
콘솔·실행계획 원문은 `./gradlew :app-bootstrap:test --tests '*FeedPersistenceIT*'` 실행 후 `src/app-bootstrap/build/test-results/test/TEST-*FeedPersistenceIT*.xml`의 system-out에서 뽑을 수 있다.
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="baseline-schema" name="기준선 스키마의 관계">
|
||||
<mxGraphModel dx="885" dy="305" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="885" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_users" value="users" tooltip="entity | Evidence: L35-L35" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="60.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_pages" value="pages" tooltip="entity | Evidence: L36-L36" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="196.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-items" value="feed_items" tooltip="entity | Evidence: L35-L35" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="380.0" y="128.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_highlights" value="highlights" tooltip="entity | Evidence: L37-L37" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="690.0" y="128.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_feed-items-have-highlights" value="여러 highlights를 가짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-items" target="n_highlights">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="610.0" y="132.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_pages-have-feed-items" value="여러 feed_item이 딸림" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_pages" target="n_feed-items">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="324.0" y="198.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_users-have-feed-items" value="여러 feed_item을 가짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_users" target="n_feed-items">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="324.0" y="121.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="885" height="305" viewBox="0 0 885 305" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">기준선 스키마의 관계</title>
|
||||
<desc id="diagram-description">왼쪽의 users와 pages가 각각 중앙의 feed_items에 연결된다. feed_items는 오른쪽의 highlights로 이어진다. 간선은 user와 page 각각에 여러 feed_item이 연결되고, 한 feed_item에 여러 highlight가 연결되는 관계를 나타낸다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"baseline-schema","profile":"component-flow"},"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}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="885" height="305" />
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="530.0,160.0 610.0,160.0 610.0,160.0 690.0,160.0" data-evidence="37-37" />
|
||||
<rect class="edge-label-bg" x="544.0" y="118.0" width="131.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="610.0" y="133.0">여러 highlights를 가짐</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="220.0,228.0 300.0,228.0 300.0,169.0 380.0,169.0" data-evidence="36-36" />
|
||||
<rect class="edge-label-bg" x="261.4" y="184.5" width="125.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="324.0" y="199.5">여러 feed_item이 딸림</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="220.0,92.0 300.0,92.0 300.0,151.0 380.0,151.0" data-evidence="35-35" />
|
||||
<rect class="edge-label-bg" x="261.4" y="107.5" width="125.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="324.0" y="122.5">여러 feed_item을 가짐</text>
|
||||
<g id="node-users">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-source" data-evidence="35-35" x="70.0" y="60.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="90.0">users</text>
|
||||
</g>
|
||||
<g id="node-pages">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-source" data-evidence="36-36" x="70.0" y="196.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="226.0">pages</text>
|
||||
</g>
|
||||
<g id="node-feed-items">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-store" data-evidence="35-35" x="380.0" y="128.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="455.0" y="158.0">feed_items</text>
|
||||
</g>
|
||||
<g id="node-highlights">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-sink" data-evidence="37-37" x="690.0" y="128.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="765.0" y="158.0">highlights</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="eager-lazy-query-sequence" name="EAGER 2차 조회는 반환 전에, LAZY highlights 조회는 매핑 접근 뒤에 실행된다">
|
||||
<mxGraphModel dx="680" dy="542" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_load-feed-mapping" value="loadFeed DTO 매핑" tooltip="FeedItem을 순회하며 응답 DTO를 조립하고 highlights 게터에 접근하는 호출자. | Evidence: L322-L322, L449-L451" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_hibernate" value="Hibernate" tooltip="파생 쿼리의 루트 조회와 EAGER 2차 SELECT, LAZY 컬렉션 초기화를 수행하는 JPA provider. | Evidence: L320-L322" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="255.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_postgresql" value="PostgreSQL" tooltip="Hibernate가 루트 및 연관 SELECT를 실행하는 데이터베이스. | Evidence: L417-L422, L447-L447" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=cylinder3;boundedLbl=1;backgroundOutline=1;fillColor=#eef6fb;" vertex="1" parent="1">
|
||||
<mxGeometry x="465.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_m1-find-all" value="findAllBy(...)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_load-feed-mapping" target="n_hibernate">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="225.0" y="128.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_m2-root-select" value="SELECT feed_items" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_hibernate" target="n_postgresql">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="435.0" y="190.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_m3-eager-secondary-selects" value="SELECT user / page · EAGER 2차" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_hibernate" target="n_postgresql">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="435.0" y="252.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_m4-return-eager-loaded-items" value="EAGER 연관이 채워진 FeedItem 반환" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_hibernate" target="n_load-feed-mapping">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="225.0" y="314.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_m5-access-highlights" value="매핑 중 getHighlights() 접근" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_load-feed-mapping" target="n_hibernate">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="225.0" y="376.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_m6-lazy-highlights-select" value="SELECT highlights WHERE feed_item_id = ?" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_hibernate" target="n_postgresql">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="435.0" y="438.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="542" viewBox="0 0 680 542" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">EAGER 2차 조회는 반환 전에, LAZY highlights 조회는 매핑 접근 뒤에 실행된다</title>
|
||||
<desc id="diagram-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를 실행한다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"eager-lazy-query-sequence","profile":"sequence"},"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}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="680" height="542" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="322-322,449-451" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="120.0" y="65.0">loadFeed DTO 매핑</text>
|
||||
<line class="lifeline" x1="120.0" y1="99.0" x2="120.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-primary role-participant" data-evidence="320-322" x="255.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="330.0" y="65.0">Hibernate</text>
|
||||
<line class="lifeline" x1="330.0" y1="99.0" x2="330.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="417-422,447-447" x="465.0" y="45.7" width="150.0" height="42.7" /><ellipse class="node-shape kind-participant emphasis-normal role-participant" cx="540.0" cy="45.7" rx="75.0" ry="10.7" /><path class="storage-bottom" d="M 465.0 88.3 A 75.0 10.7 0 0 0 615.0 88.3" />
|
||||
<text class="node-label" x="540.0" y="65.0">PostgreSQL</text>
|
||||
<line class="lifeline" x1="540.0" y1="99.0" x2="540.0" y2="512.0" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 330.0,140.0" data-evidence="320-320" />
|
||||
<rect class="edge-label-bg" x="159.1" y="114.0" width="131.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="129.0">1. findAllBy(...)</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-primary" points="330.0,202.0 540.0,202.0" data-evidence="320-320" />
|
||||
<rect class="edge-label-bg" x="359.0" y="176.0" width="152.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="191.0">2. SELECT feed_items</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="330.0,264.0 540.0,264.0" data-evidence="318-321" />
|
||||
<rect class="edge-label-bg" x="318.8" y="238.0" width="232.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="253.0">3. SELECT user / page · EAGER 2차</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,326.0 120.0,326.0" data-evidence="318-320" />
|
||||
<rect class="edge-label-bg" x="122.2" y="300.0" width="205.6" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="315.0">4. EAGER 연관이 채워진 FeedItem 반환</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,388.0 330.0,388.0" data-evidence="322-322,449-451" />
|
||||
<rect class="edge-label-bg" x="128.9" y="362.0" width="192.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="377.0">5. 매핑 중 getHighlights() 접근</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-primary" points="330.0,450.0 540.0,450.0" data-evidence="322-322,429-430" />
|
||||
<rect class="edge-label-bg" x="281.9" y="424.0" width="306.1" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="439.0">6. SELECT highlights WHERE feed_item_id = ?</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="nplus1-query-fanout" name="반환 부모 수 N이 컬렉션 초기화와 자식 SELECT 횟수를 결정한다">
|
||||
<mxGraphModel dx="964" dy="300" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="964" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_feed-request" value="loadFeed(0, N) → FeedItem N개<br/>page size = 반환 부모 수 N" tooltip="한 요청에서 반환한 부모 수 N이 N+1 증가 계수가 되는 피드 조회. | Evidence: L341-L341, L398-L398" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="60.0" width="181.0" height="91.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_collection-initializations" value="Highlight 컬렉션 초기화 N회<br/>collectionFetches = N" tooltip="각 FeedItem의 지연 컬렉션 접근이 부모마다 한 번의 초기화를 만든 결과. | Evidence: L337-L337, L378-L384" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="411.0" y="70.0" width="181.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_highlight-selects" value="Highlight SELECT N회<br/>부모별 자식 행 전부 조회" tooltip="배치와 서브셀렉트가 없는 기준선에서 컬렉션 초기화마다 실행되는 자식 SELECT. | Evidence: L337-L337, L384-L385" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="752.0" y="70.0" width="167.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_initialization-runs-select" value="초기화마다 SELECT 1회" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_collection-initializations" target="n_highlight-selects">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="672.0" y="77.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_parents-trigger-initialization" value="아이템마다 컬렉션 접근" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-request" target="n_collection-initializations">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="331.0" y="77.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="964" height="300" viewBox="0 0 964 300" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">반환 부모 수 N이 컬렉션 초기화와 자식 SELECT 횟수를 결정한다</title>
|
||||
<desc id="diagram-description">왼쪽의 loadFeed 요청은 한 페이지에서 N개의 FeedItem을 반환한다. 매핑이 각 부모의 Highlight 컬렉션에 접근하면 컬렉션 초기화가 부모마다 한 번씩 일어나 총 N회가 된다. 현재 기준선에서는 배치나 서브셀렉트가 없어 초기화 한 번마다 Highlight SELECT도 한 번 실행되므로 추가 조회가 N회 발생한다. 각 SELECT는 해당 부모의 Highlight 자식 행을 전부 읽는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"nplus1-query-fanout","profile":"component-flow"},"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}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="964" height="300" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="592.0,105.5 672.0,105.5 672.0,105.5 752.0,105.5" data-evidence="337-337,384-384" />
|
||||
<rect class="edge-label-bg" x="612.8" y="63.5" width="118.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="672.0" y="78.5">초기화마다 SELECT 1회</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="251.0,105.5 331.0,105.5 331.0,105.5 411.0,105.5" data-evidence="384-384" />
|
||||
<rect class="edge-label-bg" x="281.8" y="63.5" width="98.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="331.0" y="78.5">아이템마다 컬렉션 접근</text>
|
||||
<g id="node-feed-request">
|
||||
<rect class="node-shape kind-request emphasis-normal role-source" data-evidence="341-341,398-398" x="70.0" y="60.0" width="181.0" height="91.0" rx="7" />
|
||||
<text class="node-label" x="160.5" y="87.0">loadFeed(0, N) →</text>
|
||||
<text class="node-label" x="160.5" y="105.0">FeedItem N개</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="126.0" x2="237.0" y2="126.0" />
|
||||
<text class="node-detail" x="86.0" y="143.0">page size = 반환 부모 수 N</text>
|
||||
</g>
|
||||
<g id="node-collection-initializations">
|
||||
<rect class="node-shape kind-operation emphasis-primary role-service" data-evidence="337-337,378-384" x="411.0" y="70.0" width="181.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="501.5" y="97.0">Highlight 컬렉션 초기화 N회</text>
|
||||
<line class="node-detail-divider" x1="425.0" y1="118.0" x2="578.0" y2="118.0" />
|
||||
<text class="node-detail" x="427.0" y="135.0">collectionFetches = N</text>
|
||||
</g>
|
||||
<g id="node-highlight-selects">
|
||||
<rect class="node-shape kind-query emphasis-normal role-sink" data-evidence="337-337,384-385" x="752.0" y="70.0" width="167.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="835.5" y="97.0">Highlight SELECT N회</text>
|
||||
<line class="node-detail-divider" x1="766.0" y1="118.0" x2="905.0" y2="118.0" />
|
||||
<text class="node-detail" x="768.0" y="135.0">부모별 자식 행 전부 조회</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="query-port-boundary" name="조회 전략은 FeedQueryPort 뒤의 퍼시스턴스 어댑터에 격리된다">
|
||||
<mxGraphModel dx="1090" dy="360" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1090" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_get-feed-use-case" value="GetFeedUseCase" tooltip="조회 사용자, 페이지 크기, FeedSummary 계약만 아는 애플리케이션 계층. | Evidence: L295-L295" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=hexagon;perimeter=hexagonPerimeter2;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="420.0" y="185.0" width="230.0" height="130.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-controller" value="FeedController<br/>GET /feed" tooltip="조회 입력과 FeedSummary 반환 형태만 아는 웹 계층. | Evidence: L295-L295" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="85.0" width="210.0" height="94.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-query-adapter" value="FeedQueryAdapter<br/>PostgreSQL 조회<br/>Fetch Join · Batch Fetch<br/>DTO Projection · 윈도우 함수" tooltip="FeedQueryPort를 구현하며 구체 조회 전략을 책임지는 퍼시스턴스 어댑터. | Evidence: L295-L295, L299-L299" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="825.0" y="85.0" width="220.0" height="126.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-query-port" value="FeedQueryPort" tooltip="상위 계층과 구체 조회 전략을 분리하는 조회 포트. | Evidence: L295-L295, L299-L299" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;rounded=1;arcSize=50;fillColor=#f6f1fb;" vertex="1" parent="1">
|
||||
<mxGeometry x="670.0" y="155.0" width="125.0" height="58.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_adapter-implements-port" value="implements" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-query-adapter" target="n_feed-query-port">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="834.8" y="231.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_controller-to-use-case" value="GET /feed 조회 위임" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-controller" target="n_get-feed-use-case">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="361.5" y="191.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_use-case-to-port" value="조회 의존" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_get-feed-use-case" target="n_feed-query-port">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="704.0" y="132.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1090" height="360" viewBox="0 0 1090 360" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">조회 전략은 FeedQueryPort 뒤의 퍼시스턴스 어댑터에 격리된다</title>
|
||||
<desc id="diagram-description">왼쪽의 FeedController가 GET /feed 요청을 받아 중앙의 GetFeedUseCase에 조회를 위임한다. 유스케이스는 오른쪽의 FeedQueryPort에 조회를 의존한다. FeedQueryAdapter는 FeedQueryPort를 구현하는 아웃바운드 어댑터이며 PostgreSQL 조회를 수행한다. Fetch Join, Batch Fetch, DTO Projection, 윈도우 함수 같은 구체 전략은 이 어댑터의 책임이므로 상위 계층은 전략 교체의 영향을 받지 않는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"query-port-boundary","profile":"ports-adapters"},"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}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="1090" height="360" />
|
||||
<polyline class="edge kind-dependency style-solid emphasis-normal" points="935.0,211.0 935.0,259.0 732.5,259.0 732.5,213.0" data-evidence="295-295" />
|
||||
<rect class="edge-label-bg" x="792.2" y="217.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="834.8" y="232.0">implements</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="255.0,132.0 337.5,132.0 337.5,250.0 420.0,250.0" data-evidence="295-295" />
|
||||
<rect class="edge-label-bg" x="302.2" y="177.0" width="118.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="361.5" y="192.0">GET /feed 조회 위임</text>
|
||||
<polyline class="edge kind-dependency style-solid emphasis-normal" points="650.0,250.0 680.0,250.0 680.0,119.0 640.0,119.0 640.0,184.0 670.0,184.0" data-evidence="295-295" />
|
||||
<rect class="edge-label-bg" x="678.2" y="118.0" width="51.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="704.0" y="133.0">조회 의존</text>
|
||||
<g id="node-get-feed-use-case">
|
||||
<polygon class="node-shape kind-application emphasis-primary role-core" data-evidence="295-295" points="454.0,185.0 616.0,185.0 650.0,250.0 616.0,315.0 454.0,315.0 420.0,250.0" />
|
||||
<text class="node-role" x="535.0" y="204.0">«core»</text>
|
||||
<text class="node-label" x="535.0" y="248.0">GetFeedUseCase</text>
|
||||
</g>
|
||||
<g id="node-feed-controller">
|
||||
<rect class="node-shape kind-adapter emphasis-normal role-inbound-adapter" data-evidence="295-295" x="45.0" y="85.0" width="210.0" height="94.0" rx="7" />
|
||||
<text class="node-role" x="150.0" y="104.0">«inbound-adapter»</text>
|
||||
<text class="node-label" x="150.0" y="131.0">FeedController</text>
|
||||
<line class="node-detail-divider" x1="59.0" y1="152.0" x2="241.0" y2="152.0" />
|
||||
<text class="node-detail" x="61.0" y="169.0">GET /feed</text>
|
||||
</g>
|
||||
<g id="node-feed-query-adapter">
|
||||
<rect class="node-shape kind-adapter emphasis-normal role-outbound-adapter" data-evidence="295-295,299-299" x="825.0" y="85.0" width="220.0" height="126.0" rx="7" />
|
||||
<text class="node-role" x="935.0" y="104.0">«outbound-adapter»</text>
|
||||
<text class="node-label" x="935.0" y="131.0">FeedQueryAdapter</text>
|
||||
<line class="node-detail-divider" x1="839.0" y1="152.0" x2="1031.0" y2="152.0" />
|
||||
<text class="node-detail" x="841.0" y="169.0">PostgreSQL 조회</text>
|
||||
<text class="node-detail" x="841.0" y="185.0">Fetch Join · Batch Fetch</text>
|
||||
<text class="node-detail" x="841.0" y="201.0">DTO Projection · 윈도우 함수</text>
|
||||
</g>
|
||||
<g id="node-feed-query-port">
|
||||
<rect class="node-shape kind-interface emphasis-normal role-port" data-evidence="295-295,299-299" x="670.0" y="155.0" width="125.0" height="58.0" rx="22" />
|
||||
<text class="node-role" x="732.5" y="174.0">«port»</text>
|
||||
<text class="node-label" x="732.5" y="201.0">FeedQueryPort</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="skew-profile" name="Zipf-like 분포만 무거운 머리와 긴 꼬리를 함께 재현한다">
|
||||
<mxGraphModel dx="854" dy="300" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="854" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_uniform" value="균일분포<br/>분포 형태: 모두 3개<br/>극단적 소수: 없음<br/>스트레스 조건: 재현 못함<br/>선택 결과: 제외" tooltip="모든 아이템에 하이라이트 3개를 주는 분포로, 매우 많은 하이라이트 조건을 재현하지 못한다. | Evidence: L177-L177, L195-L195" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#9ca3af;fillColor=#f9fafb;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="45.0" width="220.0" height="122.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_normal" value="정규분포<br/>분포 형태: 평균 근처 집중<br/>극단적 소수: 없음<br/>스트레스 조건: 재현 못함<br/>선택 결과: 제외" tooltip="평균 근처에 몰려 극단적으로 많은 소수와 무거운 머리를 만들지 못하는 분포다. | Evidence: L177-L177, L196-L196" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#9ca3af;fillColor=#f9fafb;" vertex="1" parent="1">
|
||||
<mxGeometry x="317.0" y="45.0" width="220.0" height="122.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_zipf-like" value="Zipf-like 합성 분포<br/>분포 형태: 무거운 머리 + 긴 꼬리<br/>극단적 소수: 있음 · 1위 500개<br/>스트레스 조건: 재현<br/>선택 결과: s=1.15 합성 분포" tooltip="순위 기반 지수로 편중 강도를 조절하며 무거운 머리와 긴 꼬리를 재현하는 합성 스트레스 분포다. | Evidence: L177-L177, L181-L181, L184-L184, L188-L190, L197-L197, L203-L203" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="589.0" y="45.0" width="220.0" height="122.0" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="854" height="300" viewBox="0 0 854 300" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">Zipf-like 분포만 무거운 머리와 긴 꼬리를 함께 재현한다</title>
|
||||
<desc id="diagram-description">왼쪽부터 균일분포, 정규분포, Zipf-like 합성 분포를 같은 네 기준으로 비교한다. 균일분포는 모든 아이템이 3개이고, 정규분포는 평균 근처에 몰려 둘 다 극단적으로 많은 소수를 만들지 못하므로 제외된다. Zipf-like 분포는 소수의 인기 아이템이 압도적인 무거운 머리와 나머지의 긴 꼬리를 만들며, 지수 s=1.15와 상한 500·하한 1을 사용해 매우 많은 하이라이트 조건과 Top-N 필요성을 재현하는 합성 스트레스 분포로 선택된다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"skew-profile","profile":"comparison"},"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}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="854" height="300" />
|
||||
<g id="node-uniform">
|
||||
<rect class="node-shape kind-distribution emphasis-muted role-option" data-evidence="177-177,195-195" x="45.0" y="45.0" width="220.0" height="122.0" rx="7" />
|
||||
<text class="node-label" x="155.0" y="72.0">균일분포</text>
|
||||
<line class="node-detail-divider" x1="59.0" y1="93.0" x2="251.0" y2="93.0" />
|
||||
<text class="node-detail" x="61.0" y="110.0">분포 형태: 모두 3개</text>
|
||||
<text class="node-detail" x="61.0" y="126.0">극단적 소수: 없음</text>
|
||||
<text class="node-detail" x="61.0" y="142.0">스트레스 조건: 재현 못함</text>
|
||||
<text class="node-detail" x="61.0" y="158.0">선택 결과: 제외</text>
|
||||
</g>
|
||||
<g id="node-normal">
|
||||
<rect class="node-shape kind-distribution emphasis-muted role-option" data-evidence="177-177,196-196" x="317.0" y="45.0" width="220.0" height="122.0" rx="7" />
|
||||
<text class="node-label" x="427.0" y="72.0">정규분포</text>
|
||||
<line class="node-detail-divider" x1="331.0" y1="93.0" x2="523.0" y2="93.0" />
|
||||
<text class="node-detail" x="333.0" y="110.0">분포 형태: 평균 근처 집중</text>
|
||||
<text class="node-detail" x="333.0" y="126.0">극단적 소수: 없음</text>
|
||||
<text class="node-detail" x="333.0" y="142.0">스트레스 조건: 재현 못함</text>
|
||||
<text class="node-detail" x="333.0" y="158.0">선택 결과: 제외</text>
|
||||
</g>
|
||||
<g id="node-zipf-like">
|
||||
<rect class="node-shape kind-distribution emphasis-primary role-option" data-evidence="177-177,181-181,184-184,188-190,197-197,203-203" x="589.0" y="45.0" width="220.0" height="122.0" rx="7" />
|
||||
<text class="node-label" x="699.0" y="72.0">Zipf-like 합성 분포</text>
|
||||
<line class="node-detail-divider" x1="603.0" y1="93.0" x2="795.0" y2="93.0" />
|
||||
<text class="node-detail" x="605.0" y="110.0">분포 형태: 무거운 머리 + 긴 꼬리</text>
|
||||
<text class="node-detail" x="605.0" y="126.0">극단적 소수: 있음 · 1위 500개</text>
|
||||
<text class="node-detail" x="605.0" y="142.0">스트레스 조건: 재현</text>
|
||||
<text class="node-detail" x="605.0" y="158.0">선택 결과: s=1.15 합성 분포</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="strategy-journey" name="두 형제 문제는 Fetch Join에서 합류한 뒤 단계별 해법으로 최종 구조가 된다">
|
||||
<mxGraphModel dx="1236" dy="421" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1236" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_requirements-model-baseline" value="요구사항·모델·기준선<br/>1 과제 요구사항<br/>2 도메인·데이터 모델<br/>3 최초 피드 조회" tooltip="전체 여정은 과제 요구사항에서 도메인·데이터 모델을 거쳐 최초 피드 조회 기준선으로 시작한다. | Evidence: L25-L25" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="165.5" width="150.0" height="105.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_collection-n-plus-one" value="컬렉션 N+1 (N1)<br/>유형: 컬렉션 N+1<br/>발생: 기준선과 동시에<br/>관계: N2와 형제 문제<br/>진행: Fetch Join으로 합류" tooltip="기준선에서 N2와 동시에 나타나는 컬렉션 조회 문제다. | Evidence: L25-L25" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="383.5" y="60.0" width="167.0" height="122.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_hidden-to-one-queries" value="User·Page 숨은 쿼리 (N2)<br/>유형: User·Page 숨은 쿼리<br/>발생: 기준선과 동시에<br/>관계: N1과 형제 문제<br/>진행: Fetch Join으로 합류" tooltip="기준선에서 N1과 동시에 나타나는 User·Page 연관의 숨은 쿼리 문제다. | Evidence: L25-L25" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="380.0" y="254.0" width="174.0" height="122.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_fetch-join-failure" value="Fetch Join → 실패<br/>1 N1·N2 합류<br/>2 다중 컬렉션·페이징 실패" tooltip="N1과 N2가 Fetch Join 시도에서 합류한 뒤 다중 컬렉션·페이징 실패로 이어진다. | Evidence: L25-L25" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
|
||||
<mxGeometry x="714.0" y="174.0" width="150.0" height="88.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_improvement-chain" value="후속 개선 → 최종 구조<br/>1 Batch Fetch<br/>2 DTO Projection<br/>3 아이템별 Top-3<br/>4 Keyset Pagination<br/>5 가시성 조건 인덱싱<br/>6 최종 피드 조회 구조" tooltip="실패 뒤 다섯 조회 전략 단계가 명시된 순서로 발전해 최종 피드 조회 구조를 만든다. | Evidence: L25-L25" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="1024.0" y="140.0" width="167.0" height="156.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_baseline-reveals-n1" value="기준선에서 갈라짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_requirements-model-baseline" target="n_collection-n-plus-one">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="325.8" y="165.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_baseline-reveals-n2" value="기준선에서 갈라짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_requirements-model-baseline" target="n_hidden-to-one-queries">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="324.0" y="271.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_failure-to-improvements" value="실패 뒤 단계별 개선" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_fetch-join-failure" target="n_improvement-chain">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="944.0" y="190.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_n1-joins-fetch-join" value="Fetch Join으로 합류" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_collection-n-plus-one" target="n_fetch-join-failure">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="656.2" y="165.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_n2-joins-fetch-join" value="Fetch Join으로 합류" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_hidden-to-one-queries" target="n_fetch-join-failure">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="658.0" y="271.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1236" height="421" viewBox="0 0 1236 421" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">두 형제 문제는 Fetch Join에서 합류한 뒤 단계별 해법으로 최종 구조가 된다</title>
|
||||
<desc id="diagram-description">왼쪽에서 과제 요구사항, 도메인·데이터 모델, 최초 피드 조회 기준선 순으로 시작한다. 기준선에서 컬렉션 N+1(N1)과 User·Page 연관의 숨은 쿼리(N2)가 서로 앞뒤가 아닌 형제 문제로 동시에 갈라지고, 두 경로는 Fetch Join 시도에서 합류한다. 이 시도는 다중 컬렉션·페이징 실패로 이어진다. 마지막 노드는 Batch Fetch, DTO Projection, 아이템별 Top-3, Keyset Pagination, 가시성 조건 인덱싱을 거쳐 최종 피드 조회 구조에 도달하는 순서를 담는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"strategy-journey","profile":"component-flow"},"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}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="1236" height="421" />
|
||||
<polyline class="edge kind-problem style-solid emphasis-normal" points="220.0,209.0 301.8,209.0 301.8,121.0 383.5,121.0" data-evidence="25-25" />
|
||||
<rect class="edge-label-bg" x="286.6" y="151.0" width="78.3" height="22" rx="3" />
|
||||
<text class="edge-label" x="325.8" y="166.0">기준선에서 갈라짐</text>
|
||||
<polyline class="edge kind-problem style-solid emphasis-normal" points="220.0,227.0 300.0,227.0 300.0,315.0 380.0,315.0" data-evidence="25-25" />
|
||||
<rect class="edge-label-bg" x="284.9" y="257.0" width="78.3" height="22" rx="3" />
|
||||
<text class="edge-label" x="324.0" y="272.0">기준선에서 갈라짐</text>
|
||||
<polyline class="edge kind-improvement style-solid emphasis-normal" points="864.0,218.0 944.0,218.0 944.0,218.0 1024.0,218.0" data-evidence="25-25" />
|
||||
<rect class="edge-label-bg" x="898.1" y="176.0" width="91.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="944.0" y="191.0">실패 뒤 단계별 개선</text>
|
||||
<polyline class="edge kind-flow style-solid emphasis-normal" points="550.5,121.0 632.2,121.0 632.2,209.0 714.0,209.0" data-evidence="25-25" />
|
||||
<rect class="edge-label-bg" x="597.0" y="151.0" width="118.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="656.2" y="166.0">Fetch Join으로 합류</text>
|
||||
<polyline class="edge kind-flow style-solid emphasis-normal" points="554.0,315.0 634.0,315.0 634.0,227.0 714.0,227.0" data-evidence="25-25" />
|
||||
<rect class="edge-label-bg" x="598.8" y="257.0" width="118.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="658.0" y="272.0">Fetch Join으로 합류</text>
|
||||
<g id="node-requirements-model-baseline">
|
||||
<rect class="node-shape kind-journey-stage emphasis-normal role-source" data-evidence="25-25" x="70.0" y="165.5" width="150.0" height="105.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="192.5">요구사항·모델·기준선</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="213.5" x2="206.0" y2="213.5" />
|
||||
<text class="node-detail" x="86.0" y="230.5">1 과제 요구사항</text>
|
||||
<text class="node-detail" x="86.0" y="246.5">2 도메인·데이터 모델</text>
|
||||
<text class="node-detail" x="86.0" y="262.5">3 최초 피드 조회</text>
|
||||
</g>
|
||||
<g id="node-collection-n-plus-one">
|
||||
<rect class="node-shape kind-problem emphasis-normal role-service" data-evidence="25-25" x="383.5" y="60.0" width="167.0" height="122.0" rx="7" />
|
||||
<text class="node-label" x="467.0" y="87.0">컬렉션 N+1 (N1)</text>
|
||||
<line class="node-detail-divider" x1="397.5" y1="108.0" x2="536.5" y2="108.0" />
|
||||
<text class="node-detail" x="399.5" y="125.0">유형: 컬렉션 N+1</text>
|
||||
<text class="node-detail" x="399.5" y="141.0">발생: 기준선과 동시에</text>
|
||||
<text class="node-detail" x="399.5" y="157.0">관계: N2와 형제 문제</text>
|
||||
<text class="node-detail" x="399.5" y="173.0">진행: Fetch Join으로 합류</text>
|
||||
</g>
|
||||
<g id="node-hidden-to-one-queries">
|
||||
<rect class="node-shape kind-problem emphasis-normal role-service" data-evidence="25-25" x="380.0" y="254.0" width="174.0" height="122.0" rx="7" />
|
||||
<text class="node-label" x="467.0" y="281.0">User·Page 숨은 쿼리 (N2)</text>
|
||||
<line class="node-detail-divider" x1="394.0" y1="302.0" x2="540.0" y2="302.0" />
|
||||
<text class="node-detail" x="396.0" y="319.0">유형: User·Page 숨은 쿼리</text>
|
||||
<text class="node-detail" x="396.0" y="335.0">발생: 기준선과 동시에</text>
|
||||
<text class="node-detail" x="396.0" y="351.0">관계: N1과 형제 문제</text>
|
||||
<text class="node-detail" x="396.0" y="367.0">진행: Fetch Join으로 합류</text>
|
||||
</g>
|
||||
<g id="node-fetch-join-failure">
|
||||
<rect class="node-shape kind-query-strategy emphasis-warning role-service" data-evidence="25-25" x="714.0" y="174.0" width="150.0" height="88.0" rx="7" />
|
||||
<text class="node-label" x="789.0" y="201.0">Fetch Join → 실패</text>
|
||||
<line class="node-detail-divider" x1="728.0" y1="222.0" x2="850.0" y2="222.0" />
|
||||
<text class="node-detail" x="730.0" y="239.0">1 N1·N2 합류</text>
|
||||
<text class="node-detail" x="730.0" y="255.0">2 다중 컬렉션·페이징 실패</text>
|
||||
</g>
|
||||
<g id="node-improvement-chain">
|
||||
<rect class="node-shape kind-query-strategy emphasis-primary role-sink" data-evidence="25-25" x="1024.0" y="140.0" width="167.0" height="156.0" rx="7" />
|
||||
<text class="node-label" x="1107.5" y="168.0">후속 개선 → 최종 구조</text>
|
||||
<line class="node-detail-divider" x1="1038.0" y1="189.0" x2="1177.0" y2="189.0" />
|
||||
<text class="node-detail" x="1040.0" y="206.0">1 Batch Fetch</text>
|
||||
<text class="node-detail" x="1040.0" y="222.0">2 DTO Projection</text>
|
||||
<text class="node-detail" x="1040.0" y="238.0">3 아이템별 Top-3</text>
|
||||
<text class="node-detail" x="1040.0" y="254.0">4 Keyset Pagination</text>
|
||||
<text class="node-detail" x="1040.0" y="270.0">5 가시성 조건 인덱싱</text>
|
||||
<text class="node-detail" x="1040.0" y="286.0">6 최종 피드 조회 구조</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="target-schema" name="목표 스키마에 추가되는 mention 관계">
|
||||
<mxGraphModel dx="895" dy="305" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="895" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_users" value="users" tooltip="entity | Evidence: L35-L35" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="60.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_pages" value="pages" tooltip="entity | Evidence: L36-L36" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="196.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-items" value="feed_items" tooltip="entity | Evidence: L35-L35" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="380.0" y="128.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-item-mentions" value="feed_item_mentions" tooltip="entity | Evidence: L43-L43" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="690.0" y="60.0" width="160.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_highlights" value="highlights" tooltip="entity | Evidence: L37-L37" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="695.0" y="196.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_feed-items-have-highlights" value="여러 highlights를 가짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-items" target="n_highlights">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="636.5" y="198.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_feed-items-to-mentions" value="피드 아이템을 연결" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-items" target="n_feed-item-mentions">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="634.0" y="126.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_pages-have-feed-items" value="여러 feed_item이 딸림" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_pages" target="n_feed-items">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="324.0" y="198.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_users-have-feed-items" value="여러 feed_item을 가짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_users" target="n_feed-items">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="324.0" y="126.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_users-to-feed-item-mentions" value="mentioned 사용자로 연결" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_users" target="n_feed-item-mentions">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="455.0" y="55.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="895" height="305" viewBox="0 0 895 305" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">목표 스키마에 추가되는 mention 관계</title>
|
||||
<desc id="diagram-description">왼쪽의 users와 pages가 중앙의 feed_items에 연결된다. 오른쪽에는 highlights와 feed_item_mentions가 놓인다. feed_items는 두 엔티티에 각각 연결되고, 기존 users도 mentioned 사용자 역할로 feed_item_mentions에 연결된다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"target-schema","profile":"component-flow"},"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}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="895" height="305" />
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="530.0,169.0 612.5,169.0 612.5,228.0 695.0,228.0" data-evidence="37-37" />
|
||||
<rect class="edge-label-bg" x="570.5" y="184.5" width="131.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="636.5" y="199.5">여러 highlights를 가짐</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="530.0,151.0 610.0,151.0 610.0,101.0 690.0,101.0" data-evidence="43-43" />
|
||||
<rect class="edge-label-bg" x="591.5" y="112.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="634.0" y="127.0">피드 아이템을 연결</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="220.0,228.0 300.0,228.0 300.0,169.0 380.0,169.0" data-evidence="36-36" />
|
||||
<rect class="edge-label-bg" x="261.4" y="184.5" width="125.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="324.0" y="199.5">여러 feed_item이 딸림</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="220.0,101.0 300.0,101.0 300.0,151.0 380.0,151.0" data-evidence="35-35" />
|
||||
<rect class="edge-label-bg" x="261.4" y="112.0" width="125.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="324.0" y="127.0">여러 feed_item을 가짐</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="220.0,83.0 455.0,83.0 455.0,83.0 690.0,83.0" data-evidence="43-43" />
|
||||
<rect class="edge-label-bg" x="389.1" y="41.0" width="131.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="455.0" y="56.0">mentioned 사용자로 연결</text>
|
||||
<g id="node-users">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-source" data-evidence="35-35" x="70.0" y="60.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="90.0">users</text>
|
||||
</g>
|
||||
<g id="node-pages">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-source" data-evidence="36-36" x="70.0" y="196.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="226.0">pages</text>
|
||||
</g>
|
||||
<g id="node-feed-items">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-store" data-evidence="35-35" x="380.0" y="128.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="455.0" y="158.0">feed_items</text>
|
||||
</g>
|
||||
<g id="node-feed-item-mentions">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-sink" data-evidence="43-43" x="690.0" y="60.0" width="160.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="770.0" y="90.0">feed_item_mentions</text>
|
||||
</g>
|
||||
<g id="node-highlights">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-sink" data-evidence="37-37" x="695.0" y="196.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="770.0" y="226.0">highlights</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="700" height="230" viewBox="0 0 700 230" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">배치 페치는 부모를 먼저 페이징하고 자식을 IN 으로 묶는다</title>
|
||||
<desc id="diagram-description">왼쪽에서 엔티티만 페이징해 DB Limit이 정상 발행되고 부모 페이지가 정해진다. 가운데에서 그 페이지의 부모 키를 모아 자식을 IN 조건 한 번으로 가져온다. 오른쪽 실행계획은 준조인이라 부모와 자식을 곱하지 않고 자식 행만 반환한다. 왕복 수는 부모 수를 배치 크기로 나눈 올림값이 된다.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<pattern id="hatch" width="7" height="7" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="7" stroke="#9ca3af" stroke-width="3" />
|
||||
</pattern>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.dashed { stroke-dasharray: 7 5; }
|
||||
.edge.primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.muted { stroke: #9ca3af; }
|
||||
.node { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node.primary { stroke: #2563eb; stroke-width: 2.4; }
|
||||
.node.muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node.marked { stroke: #4b5563; stroke-width: 2.4; stroke-dasharray: 5 4; }
|
||||
.label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.fill-kept { fill: #2563eb; stroke: none; }
|
||||
.fill-drop { fill: url(#hatch); stroke: #9ca3af; stroke-width: 1; }
|
||||
.fill-none { fill: #ffffff; stroke: #d1d5db; stroke-width: 1; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="700" height="230" />
|
||||
<rect class="node primary" x="40" y="70" width="160" height="80" rx="7" />
|
||||
<text class="label" x="120.0" y="115.0">부모 페이징</text>
|
||||
<rect class="node primary" x="265" y="60" width="170" height="100" rx="7" />
|
||||
<text class="label" x="350.0" y="115.0">부모 키 IN</text>
|
||||
<rect class="node " x="500" y="70" width="160" height="80" rx="7" />
|
||||
<text class="label" x="580.0" y="115.0">준조인</text>
|
||||
<polyline class="edge primary" points="200,110 265,110" />
|
||||
<polyline class="edge primary" points="435,110 500,110" />
|
||||
<rect class="group-box" x="20" y="178" width="660" height="40" rx="9" />
|
||||
<rect class="group-label-bg" x="34" y="169" width="97.6" height="18" />
|
||||
<text class="group-label" x="40" y="182">자식은 따로 가져온다</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="700" height="250" viewBox="0 0 700 250" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">컬렉션 fetch join은 부모 행을 자식 수만큼 늘린다</title>
|
||||
<desc id="diagram-description">왼쪽 부모 테이블에서 출발한 조인이 가운데를 지나면서 부모 한 행을 자식 수만큼 반복한 행 묶음으로 만든다. 그 묶음이 오른쪽 전송 단계로 그대로 나간다. 아래쪽에서 Hibernate 6 이상이 루트 엔티티를 중복 제거해 결과 리스트를 부모 수로 되돌리지만, 늘어난 행은 SQL과 전송 단계에 남아 있다. 그래서 리스트 크기로는 이 증가가 보이지 않고 조인 카디널리티나 실행계획의 실제 행수로 확인해야 한다.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<pattern id="hatch" width="7" height="7" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="7" stroke="#9ca3af" stroke-width="3" />
|
||||
</pattern>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.dashed { stroke-dasharray: 7 5; }
|
||||
.edge.primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.muted { stroke: #9ca3af; }
|
||||
.node { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node.primary { stroke: #2563eb; stroke-width: 2.4; }
|
||||
.node.muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node.marked { stroke: #4b5563; stroke-width: 2.4; stroke-dasharray: 5 4; }
|
||||
.label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.fill-kept { fill: #2563eb; stroke: none; }
|
||||
.fill-drop { fill: url(#hatch); stroke: #9ca3af; stroke-width: 1; }
|
||||
.fill-none { fill: #ffffff; stroke: #d1d5db; stroke-width: 1; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="700" height="250" />
|
||||
<rect class="node " x="40" y="70" width="150" height="80" rx="7" />
|
||||
<text class="label" x="115.0" y="115.0">부모 테이블</text>
|
||||
<rect class="node primary" x="255" y="60" width="170" height="100" rx="7" />
|
||||
<text class="label" x="340.0" y="115.0">조인</text>
|
||||
<rect class="node " x="490" y="70" width="170" height="80" rx="7" />
|
||||
<text class="label" x="575.0" y="115.0">전송 행</text>
|
||||
<rect class="node muted" x="490" y="175" width="170" height="60" rx="7" />
|
||||
<text class="label" x="575.0" y="210.0">결과 리스트</text>
|
||||
<polyline class="edge " points="190,110 255,110" />
|
||||
<polyline class="edge primary" points="425,110 490,110" />
|
||||
<polyline class="edge dashed muted" points="575,150 575,175" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="542" viewBox="0 0 680 542" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">EAGER 2차 조회는 반환 전에, LAZY highlights 조회는 매핑 접근 뒤에 실행된다</title>
|
||||
<desc id="diagram-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를 실행한다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"eager-lazy-query-sequence","profile":"sequence"},"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}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="680" height="542" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="322-322,449-451" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="120.0" y="65.0">loadFeed DTO 매핑</text>
|
||||
<line class="lifeline" x1="120.0" y1="99.0" x2="120.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-primary role-participant" data-evidence="320-322" x="255.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="330.0" y="65.0">Hibernate</text>
|
||||
<line class="lifeline" x1="330.0" y1="99.0" x2="330.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="417-422,447-447" x="465.0" y="45.7" width="150.0" height="42.7" /><ellipse class="node-shape kind-participant emphasis-normal role-participant" cx="540.0" cy="45.7" rx="75.0" ry="10.7" /><path class="storage-bottom" d="M 465.0 88.3 A 75.0 10.7 0 0 0 615.0 88.3" />
|
||||
<text class="node-label" x="540.0" y="65.0">PostgreSQL</text>
|
||||
<line class="lifeline" x1="540.0" y1="99.0" x2="540.0" y2="512.0" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 330.0,140.0" data-evidence="320-320" />
|
||||
<rect class="edge-label-bg" x="159.1" y="114.0" width="131.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="129.0">1. findAllBy(...)</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-primary" points="330.0,202.0 540.0,202.0" data-evidence="320-320" />
|
||||
<rect class="edge-label-bg" x="359.0" y="176.0" width="152.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="191.0">2. SELECT feed_items</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="330.0,264.0 540.0,264.0" data-evidence="318-321" />
|
||||
<rect class="edge-label-bg" x="318.8" y="238.0" width="232.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="253.0">3. SELECT user / page · EAGER 2차</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,326.0 120.0,326.0" data-evidence="318-320" />
|
||||
<rect class="edge-label-bg" x="122.2" y="300.0" width="205.6" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="315.0">4. EAGER 연관이 채워진 FeedItem 반환</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,388.0 330.0,388.0" data-evidence="322-322,449-451" />
|
||||
<rect class="edge-label-bg" x="128.9" y="362.0" width="192.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="377.0">5. 매핑 중 getHighlights() 접근</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-primary" points="330.0,450.0 540.0,450.0" data-evidence="322-322,429-430" />
|
||||
<rect class="edge-label-bg" x="281.9" y="424.0" width="306.1" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="439.0">6. SELECT highlights WHERE feed_item_id = ?</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="700" height="290" viewBox="0 0 700 290" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">컬렉션 fetch join에 페이징을 걸면 자르는 곳이 DB에서 메모리로 옮겨진다</title>
|
||||
<desc id="diagram-description">위쪽은 컬렉션 fetch join에 페이징을 건 경로다. 발행 SQL에 Limit 노드가 없어 조인 결과 전체가 애플리케이션으로 넘어오고, Hibernate가 메모리에서 부모 페이지만 남기고 나머지를 버린다. 아래쪽은 엔티티만 페이징한 경로다. Limit 노드가 붙어 DB가 페이지만 돌려주므로 애플리케이션은 버릴 것을 받지 않는다. 두 경로의 반환 건수는 같지만 위쪽은 부모 전체를 하이드레이트한다.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<pattern id="hatch" width="7" height="7" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="7" stroke="#9ca3af" stroke-width="3" />
|
||||
</pattern>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.dashed { stroke-dasharray: 7 5; }
|
||||
.edge.primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.muted { stroke: #9ca3af; }
|
||||
.node { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node.primary { stroke: #2563eb; stroke-width: 2.4; }
|
||||
.node.muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node.marked { stroke: #4b5563; stroke-width: 2.4; stroke-dasharray: 5 4; }
|
||||
.label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.fill-kept { fill: #2563eb; stroke: none; }
|
||||
.fill-drop { fill: url(#hatch); stroke: #9ca3af; stroke-width: 1; }
|
||||
.fill-none { fill: #ffffff; stroke: #d1d5db; stroke-width: 1; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="700" height="290" />
|
||||
<rect class="group-box" x="30" y="50" width="640" height="96" rx="9" />
|
||||
<rect class="group-label-bg" x="44" y="41" width="166.0" height="18" />
|
||||
<text class="group-label" x="50" y="54">컬렉션 fetch join + 페이징</text>
|
||||
<rect class="node " x="55" y="74" width="165" height="56" rx="7" />
|
||||
<text class="label" x="137.5" y="107.0">조인 결과 전량</text>
|
||||
<rect class="node primary" x="275" y="74" width="165" height="56" rx="7" />
|
||||
<text class="label" x="357.5" y="107.0">메모리에서 자름</text>
|
||||
<rect class="node " x="495" y="74" width="150" height="56" rx="7" />
|
||||
<text class="label" x="570.0" y="107.0">페이지</text>
|
||||
<polyline class="edge " points="220,102 275,102" />
|
||||
<polyline class="edge " points="440,102 495,102" />
|
||||
<rect class="group-box" x="30" y="186" width="640" height="96" rx="9" />
|
||||
<rect class="group-label-bg" x="44" y="177" width="74.8" height="18" />
|
||||
<text class="group-label" x="50" y="190">엔티티만 페이징</text>
|
||||
<rect class="node primary" x="55" y="210" width="165" height="56" rx="7" />
|
||||
<text class="label" x="137.5" y="243.0">정렬 + Limit</text>
|
||||
<rect class="node primary" x="275" y="210" width="165" height="56" rx="7" />
|
||||
<text class="label" x="357.5" y="243.0">DB가 자름</text>
|
||||
<rect class="node " x="495" y="210" width="150" height="56" rx="7" />
|
||||
<text class="label" x="570.0" y="243.0">페이지</text>
|
||||
<polyline class="edge primary" points="220,238 275,238" />
|
||||
<polyline class="edge primary" points="440,238 495,238" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="280" viewBox="0 0 720 280" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">OFFSET은 건너뛸 행까지 읽고 버리지만 keyset은 커서 이후만 읽는다</title>
|
||||
<desc id="diagram-description">위쪽 OFFSET 막대에서 빗금 구간은 정렬 순서상 앞에 있어 만들어졌다가 버려지는 범위이고, 오른쪽 진한 구간이 실제로 반환되는 페이지다. 아래쪽 keyset 막대에서 빈 구간은 아예 읽지 않는 범위이고, 커서 표시 뒤부터 페이지만 읽는다. 페이지가 깊어질수록 위쪽 빗금 구간만 길어지고 아래쪽은 그대로다. 이 이점은 커서와 같은 순서의 정렬키 인덱스가 있을 때만 성립한다.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<pattern id="hatch" width="7" height="7" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="7" stroke="#9ca3af" stroke-width="3" />
|
||||
</pattern>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.dashed { stroke-dasharray: 7 5; }
|
||||
.edge.primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.muted { stroke: #9ca3af; }
|
||||
.node { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node.primary { stroke: #2563eb; stroke-width: 2.4; }
|
||||
.node.muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node.marked { stroke: #4b5563; stroke-width: 2.4; stroke-dasharray: 5 4; }
|
||||
.label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.fill-kept { fill: #2563eb; stroke: none; }
|
||||
.fill-drop { fill: url(#hatch); stroke: #9ca3af; stroke-width: 1; }
|
||||
.fill-none { fill: #ffffff; stroke: #d1d5db; stroke-width: 1; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="720" height="280" />
|
||||
<rect class="group-box" x="30" y="46" width="660" height="84" rx="9" />
|
||||
<rect class="group-label-bg" x="44" y="37" width="59.599999999999994" height="18" />
|
||||
<text class="group-label" x="50" y="50">OFFSET</text>
|
||||
<rect class="fill-drop" x="55" y="76" width="500" height="34" rx="3" />
|
||||
<rect class="fill-kept" x="555" y="76" width="110" height="34" rx="3" />
|
||||
<text class="group-label" x="305" y="98" text-anchor="middle">버려지는 범위</text>
|
||||
<rect class="group-box" x="30" y="170" width="660" height="84" rx="9" />
|
||||
<rect class="group-label-bg" x="44" y="161" width="59.599999999999994" height="18" />
|
||||
<text class="group-label" x="50" y="174">keyset</text>
|
||||
<rect class="fill-none" x="55" y="200" width="500" height="34" rx="3" />
|
||||
<rect class="fill-kept" x="555" y="200" width="110" height="34" rx="3" />
|
||||
<text class="group-label" x="305" y="222" text-anchor="middle">읽지 않는 범위</text>
|
||||
<line x1="555" y1="188" x2="555" y2="246" stroke="#2563eb" stroke-width="2.4" />
|
||||
<text class="group-label" x="555" y="182" text-anchor="middle" fill="#2563eb">커서</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="660" height="230" viewBox="0 0 660 230" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">프로젝션은 엔티티 적재를 없애지만 자식 행수는 그대로 남는다</title>
|
||||
<desc id="diagram-description">왼쪽 배치 방식은 페이지를 조회하면서 엔티티를 대량으로 하이드레이트한다. 가운데 프로젝션은 필요한 스칼라 값만 캐리어로 받아 엔티티를 만들지 않고 발행 쿼리도 상수로 고정한다. 다만 오른쪽처럼 자식 조회는 페이지 부모의 자식을 전부 가져오므로 행수는 줄지 않는다. 화면이 필요로 하는 것은 부모당 상위 몇 개뿐이다.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<pattern id="hatch" width="7" height="7" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="7" stroke="#9ca3af" stroke-width="3" />
|
||||
</pattern>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.dashed { stroke-dasharray: 7 5; }
|
||||
.edge.primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.muted { stroke: #9ca3af; }
|
||||
.node { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node.primary { stroke: #2563eb; stroke-width: 2.4; }
|
||||
.node.muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node.marked { stroke: #4b5563; stroke-width: 2.4; stroke-dasharray: 5 4; }
|
||||
.label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.fill-kept { fill: #2563eb; stroke: none; }
|
||||
.fill-drop { fill: url(#hatch); stroke: #9ca3af; stroke-width: 1; }
|
||||
.fill-none { fill: #ffffff; stroke: #d1d5db; stroke-width: 1; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="660" height="230" />
|
||||
<rect class="node " x="30" y="70" width="170" height="80" rx="7" />
|
||||
<text class="label" x="115.0" y="115.0">엔티티 적재</text>
|
||||
<rect class="node primary" x="245" y="60" width="170" height="100" rx="7" />
|
||||
<text class="label" x="330.0" y="115.0">스칼라 프로젝션</text>
|
||||
<rect class="node marked" x="460" y="70" width="170" height="80" rx="7" />
|
||||
<text class="label" x="545.0" y="115.0">자식 행 전량</text>
|
||||
<polyline class="edge primary" points="200,110 245,110" />
|
||||
<polyline class="edge muted" points="415,110 460,110" />
|
||||
<rect class="group-box" x="20" y="178" width="620" height="40" rx="9" />
|
||||
<rect class="group-label-bg" x="34" y="169" width="135.6" height="18" />
|
||||
<text class="group-label" x="40" y="182">적재는 사라지고 행수는 남는다</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="300" viewBox="0 0 720 300" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">그룹당 상위 몇 개를 만드는 세 방식은 읽는 범위가 다르다</title>
|
||||
<desc id="diagram-description">세 방식 모두 부모마다 상위 몇 개씩 같은 결과를 만든다. 윈도우 함수는 파티션 전체를 읽어 순번을 매긴 뒤 상위만 남긴다. LATERAL은 부모마다 정렬 인덱스에서 필요한 개수만 읽고 멈춘다. 애플리케이션 그룹핑은 자식 전량을 애플리케이션으로 보낸 뒤 코드에서 자른다. 읽는 범위가 다르므로 읽은 블록 수도 갈린다. 그룹이 크고 필요한 개수가 작을수록 LATERAL이 읽지 않는 범위가 넓어진다.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<pattern id="hatch" width="7" height="7" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="7" stroke="#9ca3af" stroke-width="3" />
|
||||
</pattern>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.dashed { stroke-dasharray: 7 5; }
|
||||
.edge.primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.muted { stroke: #9ca3af; }
|
||||
.node { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node.primary { stroke: #2563eb; stroke-width: 2.4; }
|
||||
.node.muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node.marked { stroke: #4b5563; stroke-width: 2.4; stroke-dasharray: 5 4; }
|
||||
.label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.fill-kept { fill: #2563eb; stroke: none; }
|
||||
.fill-drop { fill: url(#hatch); stroke: #9ca3af; stroke-width: 1; }
|
||||
.fill-none { fill: #ffffff; stroke: #d1d5db; stroke-width: 1; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="720" height="300" />
|
||||
<rect class="node " x="40" y="56" width="190" height="62" rx="7" />
|
||||
<text class="label" x="135.0" y="92.0">윈도우 함수</text>
|
||||
<rect class="node " x="330" y="56" width="350" height="62" rx="7" />
|
||||
<text class="label" x="505.0" y="92.0">파티션 전체</text>
|
||||
<polyline class="edge " points="230,87 330,87" />
|
||||
<rect class="node primary" x="40" y="146" width="190" height="62" rx="7" />
|
||||
<text class="label" x="135.0" y="182.0">LATERAL</text>
|
||||
<rect class="node primary" x="330" y="146" width="350" height="62" rx="7" />
|
||||
<text class="label" x="505.0" y="182.0">부모별 인덱스 seek</text>
|
||||
<polyline class="edge primary" points="230,177 330,177" />
|
||||
<rect class="node " x="40" y="236" width="190" height="62" rx="7" />
|
||||
<text class="label" x="135.0" y="272.0">애플리케이션 그룹핑</text>
|
||||
<rect class="node muted" x="330" y="236" width="350" height="62" rx="7" />
|
||||
<text class="label" x="505.0" y="272.0">자식 전량</text>
|
||||
<polyline class="edge " points="230,267 330,267" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |