docs(keycloak-session-store): remake all 28 diagrams through the techviz pipeline
The originating repository's SVGs were drawn by hand and every one of them
put a title, a subtitle and an explanation band inside the canvas. This
repository forbids both, so they could not be carried over — the whole set
was rebuilt through the skill's pipeline instead.
Each diagram went through prepare, references, prompt, a VizSpec 1.1 citing
document line ranges, lint, and render. All 28 pass lint and produce the
same eight formats the existing keycloak project has. Sentences moved out of
the canvas into <desc> and the paragraph beside each figure; the drawings
carry names only.
Two lint rules did real work rather than formatting work:
edge-through-node caught arrows crossing an unrelated
node and implying an adjacency that
does not exist — four diagrams had to
be restructured, not just relaid out
evidence-outside-prepared-context caught a diagram citing another
section; its anchor moved from B-0 to
B-1 so all three sections it draws on
are inside the prepared context
lab-topology also had to change profile: its context offers a different
candidate set, and query-fanout with shard roles is what the section
actually shows — one entry point spreading to two Keycloak nodes.
The document now carries all 28 inline, one per claim that needed one, and
the section recording what was still missing is updated: the diagram gap is
closed, Studio records remain.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@@ -0,0 +1,22 @@
|
||||
# 발견과 전송은 다른 경로다
|
||||
|
||||
## Alternative text
|
||||
|
||||
두 노드가 데이터베이스로는 이어져 있고 TCP 7800 으로는 끊긴 구성. 세션 조회는 살아 있고 무효화 통지는 막힌다.
|
||||
|
||||
## Long description
|
||||
|
||||
노드가 서로를 찾는 경로와 실제로 이야기하는 경로가 다르다. 발견은 PostgreSQL 의 JGROUPS_PING 테이블을 쓰고 전송은 TCP 7800 을 쓴다. 7800 만 막으면 둘 다 데이터베이스에 등록된 채로 남아 서로 존재한다고 믿지만 메시지는 오가지 않는다. 세션은 데이터베이스에 있으므로 교차 노드 refresh 는 200 을 유지하고, 로그아웃 무효화 통지는 7800 을 타므로 400 이어야 할 것이 200 이 된다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **keycloak-0** (service): 로그아웃을 처리한 노드. Evidence: L183–L189.
|
||||
- **TCP 7800** (channel): 무효화 통지가 지나는 길. NetworkPolicy 로 막았다. Evidence: L191–L196.
|
||||
- **keycloak-1** (service): 통지를 못 받아 세션이 죽은 것을 모른다. Evidence: L183–L189.
|
||||
- **PostgreSQL** (datastore): 세션이 여기 있어 7800 과 무관하다. Evidence: L186–L190.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **keycloak-0 → TCP 7800:** 무효화 통지. Evidence: L186–L190.
|
||||
- **keycloak-1 → PostgreSQL:** 세션은 여기서 읽는다 — 7800 과 무관. Evidence: L186–L189.
|
||||
- **TCP 7800 → keycloak-1:** 막혀서 닿지 않는다. Evidence: L186–L190.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 발견과 전송은 다른 경로다
|
||||
# Question: TCP 7800 을 끊으면 무엇이 깨지고 무엇이 남는가
|
||||
direction: down
|
||||
n0: "keycloak-0" {
|
||||
shape: rectangle
|
||||
}
|
||||
n1: "TCP 7800" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "keycloak-1" {
|
||||
shape: rectangle
|
||||
}
|
||||
n3: "PostgreSQL" {
|
||||
shape: cylinder
|
||||
}
|
||||
n0 -> n1: "무효화 통지"
|
||||
n1 -> n2: "막혀서 닿지 않는다"
|
||||
n2 -> n3: "세션은 여기서 읽는다 — 7800 과 무관"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="keycloak-0", shape=box, style="rounded,filled"];
|
||||
n1 [label="TCP 7800", shape=box, style="rounded,filled"];
|
||||
n2 [label="keycloak-1", shape=box, style="rounded,filled"];
|
||||
n3 [label="PostgreSQL", shape=cylinder, style="rounded,filled"];
|
||||
n0 -> n1 [label="무효화 통지", style=solid];
|
||||
n1 -> n2 [label="막혀서 닿지 않는다", style=solid];
|
||||
n2 -> n3 [label="세션은 여기서 읽는다 — 7800 과 무관", style=solid];
|
||||
}
|
||||
@@ -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="a1-transport-vs-discovery" name="발견과 전송은 다른 경로다">
|
||||
<mxGraphModel dx="680" dy="696" 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_k0" value="keycloak-0" tooltip="로그아웃을 처리한 노드. | Evidence: L183-L189" 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="92.5" y="60.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_transport" value="TCP 7800<br/>8080·9000 만 열고 누락시킨다" tooltip="무효화 통지가 지나는 길. NetworkPolicy 로 막았다. | Evidence: L191-L196" 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="80.5" y="220.0" width="174.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_k1" value="keycloak-1<br/>400 이어야 할 refresh 가 200" tooltip="통지를 못 받아 세션이 죽은 것을 모른다. | Evidence: L183-L189" 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="70.0" y="387.0" width="195.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_db" value="PostgreSQL<br/>교차 노드 refresh 200" tooltip="세션이 여기 있어 7800 과 무관하다. | Evidence: L186-L190" 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;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="91.0" y="554.0" width="153.0" height="97.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_k0-t" value="무효화 통지" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_k0" target="n_transport">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="191.5" y="172.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_k1-db" value="세션은 여기서 읽는다 — 7800 과 무관" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_k1" target="n_db">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="191.5" y="506.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_t-k1" value="막혀서 닿지 않는다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_transport" target="n_k1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="191.5" y="339.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-k0-t",
|
||||
"type": "arrow",
|
||||
"x": 167.5,
|
||||
"y": 124.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 563171480,
|
||||
"version": 1,
|
||||
"versionNonce": 1937314071,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-k0",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-transport",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-k0-t",
|
||||
"type": "text",
|
||||
"x": 146.5,
|
||||
"y": 160.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1595638930,
|
||||
"version": 1,
|
||||
"versionNonce": 1546274523,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "무효화 통지",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "무효화 통지",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-k1-db",
|
||||
"type": "arrow",
|
||||
"x": 167.5,
|
||||
"y": 458.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1363167760,
|
||||
"version": 1,
|
||||
"versionNonce": 1170603998,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-k1",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-db",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-k1-db",
|
||||
"type": "text",
|
||||
"x": 99.5,
|
||||
"y": 494.0,
|
||||
"width": 184,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 107988814,
|
||||
"version": 1,
|
||||
"versionNonce": 1427420655,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "세션은 여기서 읽는다 — 7800 과 무관",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "세션은 여기서 읽는다 — 7800 과 무관",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-t-k1",
|
||||
"type": "arrow",
|
||||
"x": 167.5,
|
||||
"y": 291.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 681804025,
|
||||
"version": 1,
|
||||
"versionNonce": 1772524548,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-transport",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-k1",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-t-k1",
|
||||
"type": "text",
|
||||
"x": 146.5,
|
||||
"y": 327.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 174704529,
|
||||
"version": 1,
|
||||
"versionNonce": 1007619210,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "막혀서 닿지 않는다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "막혀서 닿지 않는다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-k0",
|
||||
"type": "rectangle",
|
||||
"x": 92.5,
|
||||
"y": 60.0,
|
||||
"width": 150.0,
|
||||
"height": 64.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 772945349,
|
||||
"version": 1,
|
||||
"versionNonce": 30235372,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-k0",
|
||||
"type": "text",
|
||||
"x": 102.5,
|
||||
"y": 70.0,
|
||||
"width": 130.0,
|
||||
"height": 44.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1107535317,
|
||||
"version": 1,
|
||||
"versionNonce": 630670490,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "keycloak-0",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "keycloak-0",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-transport",
|
||||
"type": "rectangle",
|
||||
"x": 80.5,
|
||||
"y": 220.0,
|
||||
"width": 174.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 24015876,
|
||||
"version": 1,
|
||||
"versionNonce": 215653166,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-transport",
|
||||
"type": "text",
|
||||
"x": 90.5,
|
||||
"y": 230.0,
|
||||
"width": 154.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1471007992,
|
||||
"version": 1,
|
||||
"versionNonce": 896858137,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "TCP 7800\n8080·9000 만 열고 누락시킨다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "TCP 7800\n8080·9000 만 열고 누락시킨다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-k1",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 387.0,
|
||||
"width": 195.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1338591829,
|
||||
"version": 1,
|
||||
"versionNonce": 367320954,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-k1",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 397.0,
|
||||
"width": 175.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1741943564,
|
||||
"version": 1,
|
||||
"versionNonce": 368770159,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "keycloak-1\n400 이어야 할 refresh 가 200",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "keycloak-1\n400 이어야 할 refresh 가 200",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-db",
|
||||
"type": "rectangle",
|
||||
"x": 91.0,
|
||||
"y": 554.0,
|
||||
"width": 153.0,
|
||||
"height": 97.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 285058125,
|
||||
"version": 1,
|
||||
"versionNonce": 1824840415,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-db",
|
||||
"type": "text",
|
||||
"x": 101.0,
|
||||
"y": 564.0,
|
||||
"width": 133.0,
|
||||
"height": 77.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1231096363,
|
||||
"version": 1,
|
||||
"versionNonce": 313576462,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "PostgreSQL\n교차 노드 refresh 200",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "PostgreSQL\n교차 노드 refresh 200",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "a1-transport-vs-discovery",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "af42cd905290687548b620849f1e143752f9ab346de4de352701256cc6657e32",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "A-1 · JGroups 전송(TCP 7800) 차단",
|
||||
"line": 185
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"a1-transport-vs-discovery.svg",
|
||||
"a1-transport-vs-discovery.mmd",
|
||||
"a1-transport-vs-discovery.d2",
|
||||
"a1-transport-vs-discovery.dot",
|
||||
"a1-transport-vs-discovery.drawio",
|
||||
"a1-transport-vs-discovery.excalidraw",
|
||||
"a1-transport-vs-discovery.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
%% 발견과 전송은 다른 경로다
|
||||
%% question: TCP 7800 을 끊으면 무엇이 깨지고 무엇이 남는가
|
||||
flowchart TB
|
||||
n0["keycloak-0"]
|
||||
n1["TCP 7800"]
|
||||
n2["keycloak-1"]
|
||||
n3[("PostgreSQL")]
|
||||
n0 -->|"무효화 통지"| n1
|
||||
n1 -->|"막혀서 닿지 않는다"| n2
|
||||
n2 -->|"세션은 여기서 읽는다 — 7800 과 무관"| n3
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="696" viewBox="0 0 680 696" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">발견과 전송은 다른 경로다</title>
|
||||
<desc id="diagram-description">노드가 서로를 찾는 경로와 실제로 이야기하는 경로가 다르다. 발견은 PostgreSQL 의 JGROUPS_PING 테이블을 쓰고 전송은 TCP 7800 을 쓴다. 7800 만 막으면 둘 다 데이터베이스에 등록된 채로 남아 서로 존재한다고 믿지만 메시지는 오가지 않는다. 세션은 데이터베이스에 있으므로 교차 노드 refresh 는 200 을 유지하고, 로그아웃 무효화 통지는 7800 을 타므로 400 이어야 할 것이 200 이 된다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"a1-transport-vs-discovery","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"A-1 · JGroups 전송(TCP 7800) 차단","line":185}},"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="696" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="167.5,124.0 167.5,172.0 167.5,172.0 167.5,220.0" data-evidence="186-190" />
|
||||
<rect class="edge-label-bg" x="162.4" y="158.0" width="58.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="191.5" y="173.0">무효화 통지</text>
|
||||
<polyline class="edge kind-read style-solid emphasis-normal" points="167.5,458.0 167.5,506.0 167.5,506.0 167.5,554.0" data-evidence="186-189" />
|
||||
<rect class="edge-label-bg" x="105.5" y="492.0" width="172.1" height="22" rx="3" />
|
||||
<text class="edge-label" x="191.5" y="507.0">세션은 여기서 읽는다 — 7800 과 무관</text>
|
||||
<polyline class="edge kind-blocked style-solid emphasis-normal" points="167.5,291.0 167.5,339.0 167.5,339.0 167.5,387.0" data-evidence="186-190" />
|
||||
<rect class="edge-label-bg" x="149.0" y="325.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="191.5" y="340.0">막혀서 닿지 않는다</text>
|
||||
<g id="node-k0">
|
||||
<rect class="node-shape kind-service emphasis-primary role-source" data-evidence="183-189" x="92.5" y="60.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="167.5" y="90.0">keycloak-0</text>
|
||||
</g>
|
||||
<g id="node-transport">
|
||||
<rect class="node-shape kind-channel emphasis-warning role-control" data-evidence="191-196" x="80.5" y="220.0" width="174.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="167.5" y="247.0">TCP 7800</text>
|
||||
<line class="node-detail-divider" x1="94.5" y1="268.0" x2="240.5" y2="268.0" />
|
||||
<text class="node-detail" x="96.5" y="285.0">8080·9000 만 열고 누락시킨다</text>
|
||||
</g>
|
||||
<g id="node-k1">
|
||||
<rect class="node-shape kind-service emphasis-primary role-target" data-evidence="183-189" x="70.0" y="387.0" width="195.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="167.5" y="414.0">keycloak-1</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="435.0" x2="251.0" y2="435.0" />
|
||||
<text class="node-detail" x="86.0" y="452.0">400 이어야 할 refresh 가 200</text>
|
||||
</g>
|
||||
<g id="node-db">
|
||||
<rect class="node-shape kind-datastore emphasis-primary role-store" data-evidence="186-190" x="91.0" y="567.0" width="153.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-primary role-store" cx="167.5" cy="567.0" rx="76.5" ry="13.0" /><path class="storage-bottom" d="M 91.0 638.0 A 76.5 13.0 0 0 0 244.0 638.0" />
|
||||
<text class="node-label" x="167.5" y="594.0">PostgreSQL</text>
|
||||
<line class="node-detail-divider" x1="105.0" y1="615.0" x2="230.0" y2="615.0" />
|
||||
<text class="node-detail" x="107.0" y="632.0">교차 노드 refresh 200</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,22 @@
|
||||
# 200 과 디스크 사이의 빈 구간
|
||||
|
||||
## Alternative text
|
||||
|
||||
클라이언트가 200 을 받은 뒤에도 WAL 이 아직 디스크에 닿지 않은 구간이 남아 있는 구성.
|
||||
|
||||
## Long description
|
||||
|
||||
Keycloak 은 트랜잭션마다 SET LOCAL synchronous_commit TO OFF 를 건다. PostgreSQL 은 COMMIT 을 WAL 디스크 기록을 기다리지 않고 즉시 반환하고, 클라이언트는 200 과 토큰을 받는다. 실측된 wal_writer_delay 는 200ms 이며 그 사이에 프로세스가 죽으면 그만큼이 사라진다. 153건 중 4건이 그렇게 유실됐다. 버그가 아니라 의도된 설계이고 그 대가를 숫자로 확인한 것이다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **클라이언트** (actor): 200 과 토큰을 받는다. Evidence: L207–L213.
|
||||
- **Keycloak** (service): 트랜잭션마다 synchronous_commit 을 끈다. Evidence: L211–L215.
|
||||
- **PostgreSQL** (datastore): COMMIT 을 즉시 반환한다. WAL 은 아직 메모리에 있다. Evidence: L211–L215.
|
||||
- **디스크의 WAL** (datastore): 여기 닿아야 살아남는다. Evidence: L207–L215.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **클라이언트 → Keycloak:** 로그인 요청. Evidence: L207–L211.
|
||||
- **Keycloak → PostgreSQL:** 세션 INSERT · COMMIT. Evidence: L211–L215.
|
||||
- **PostgreSQL → 디스크의 WAL:** 지연된 WAL 기록. Evidence: L211–L215.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 200 과 디스크 사이의 빈 구간
|
||||
# Question: 로그인이 성공했는데 세션이 없을 수 있는 이유는 무엇인가
|
||||
direction: down
|
||||
n0: "클라이언트" {
|
||||
shape: person
|
||||
}
|
||||
n1: "Keycloak" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "PostgreSQL" {
|
||||
shape: cylinder
|
||||
}
|
||||
n3: "디스크의 WAL" {
|
||||
shape: cylinder
|
||||
}
|
||||
n0 -> n1: "로그인 요청"
|
||||
n1 -> n2: "세션 INSERT · COMMIT"
|
||||
n2 -> n3: "지연된 WAL 기록"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="클라이언트", shape=box, style="rounded,dashed,filled"];
|
||||
n1 [label="Keycloak", shape=box, style="rounded,filled"];
|
||||
n2 [label="PostgreSQL", shape=cylinder, style="rounded,filled"];
|
||||
n3 [label="디스크의 WAL", shape=cylinder, style="rounded,filled"];
|
||||
n0 -> n1 [label="로그인 요청", style=solid];
|
||||
n1 -> n2 [label="세션 INSERT · COMMIT", style=solid];
|
||||
n2 -> n3 [label="지연된 WAL 기록", style=solid];
|
||||
}
|
||||
@@ -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="a3-commit-to-disk-gap" name="200 과 디스크 사이의 빈 구간">
|
||||
<mxGraphModel dx="680" dy="742" 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_client" value="클라이언트<br/>로그인 153건 성공" tooltip="200 과 토큰을 받는다. | Evidence: L207-L213" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="134.5" y="60.0" width="150.0" height="84.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_keycloak" value="Keycloak<br/>SET LOCAL synchronous_commit TO OFF" tooltip="트랜잭션마다 synchronous_commit 을 끈다. | Evidence: L211-L215" 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="240.0" width="279.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_pg" value="PostgreSQL<br/>wal_writer_delay 200ms" tooltip="COMMIT 을 즉시 반환한다. WAL 은 아직 메모리에 있다. | Evidence: L211-L215" 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;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
|
||||
<mxGeometry x="115.5" y="407.0" width="188.0" height="97.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_disk" value="디스크의 WAL<br/>DB 에 실제로 존재 149건" tooltip="여기 닿아야 살아남는다. | Evidence: L207-L215" 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;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="134.5" y="600.0" width="150.0" height="97.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_c-k" value="로그인 요청" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_client" target="n_keycloak">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="233.5" y="192.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_k-pg" value="세션 INSERT · COMMIT" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_keycloak" target="n_pg">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="233.5" y="359.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_pg-disk" value="지연된 WAL 기록" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_pg" target="n_disk">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="233.5" y="552.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-c-k",
|
||||
"type": "arrow",
|
||||
"x": 209.5,
|
||||
"y": 144.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 637304825,
|
||||
"version": 1,
|
||||
"versionNonce": 1285963241,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-client",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-keycloak",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-c-k",
|
||||
"type": "text",
|
||||
"x": 188.5,
|
||||
"y": 180.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1672464756,
|
||||
"version": 1,
|
||||
"versionNonce": 775794122,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "로그인 요청",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "로그인 요청",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-k-pg",
|
||||
"type": "arrow",
|
||||
"x": 209.5,
|
||||
"y": 311.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1058895576,
|
||||
"version": 1,
|
||||
"versionNonce": 290419384,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-keycloak",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-pg",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-k-pg",
|
||||
"type": "text",
|
||||
"x": 161.5,
|
||||
"y": 347.0,
|
||||
"width": 144,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1504141155,
|
||||
"version": 1,
|
||||
"versionNonce": 265883063,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "세션 INSERT · COMMIT",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "세션 INSERT · COMMIT",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-pg-disk",
|
||||
"type": "arrow",
|
||||
"x": 209.5,
|
||||
"y": 504.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 375875096,
|
||||
"version": 1,
|
||||
"versionNonce": 124607208,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-pg",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-disk",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-pg-disk",
|
||||
"type": "text",
|
||||
"x": 188.5,
|
||||
"y": 540.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 326014364,
|
||||
"version": 1,
|
||||
"versionNonce": 1099775321,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "지연된 WAL 기록",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "지연된 WAL 기록",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-client",
|
||||
"type": "rectangle",
|
||||
"x": 134.5,
|
||||
"y": 60.0,
|
||||
"width": 150.0,
|
||||
"height": 84.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1893748721,
|
||||
"version": 1,
|
||||
"versionNonce": 1322839316,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-client",
|
||||
"type": "text",
|
||||
"x": 144.5,
|
||||
"y": 70.0,
|
||||
"width": 130.0,
|
||||
"height": 64.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 817706832,
|
||||
"version": 1,
|
||||
"versionNonce": 1332899567,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "클라이언트\n로그인 153건 성공",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "클라이언트\n로그인 153건 성공",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-keycloak",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 240.0,
|
||||
"width": 279.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 819278378,
|
||||
"version": 1,
|
||||
"versionNonce": 1725278844,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-keycloak",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 250.0,
|
||||
"width": 259.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1265230567,
|
||||
"version": 1,
|
||||
"versionNonce": 1115669911,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "Keycloak\nSET LOCAL synchronous_commit TO OFF",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "Keycloak\nSET LOCAL synchronous_commit TO OFF",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-pg",
|
||||
"type": "rectangle",
|
||||
"x": 115.5,
|
||||
"y": 407.0,
|
||||
"width": 188.0,
|
||||
"height": 97.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 495847287,
|
||||
"version": 1,
|
||||
"versionNonce": 1955484919,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-pg",
|
||||
"type": "text",
|
||||
"x": 125.5,
|
||||
"y": 417.0,
|
||||
"width": 168.0,
|
||||
"height": 77.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1900836085,
|
||||
"version": 1,
|
||||
"versionNonce": 160871788,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "PostgreSQL\nwal_writer_delay 200ms",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "PostgreSQL\nwal_writer_delay 200ms",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-disk",
|
||||
"type": "rectangle",
|
||||
"x": 134.5,
|
||||
"y": 600.0,
|
||||
"width": 150.0,
|
||||
"height": 97.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 492098394,
|
||||
"version": 1,
|
||||
"versionNonce": 862350580,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-disk",
|
||||
"type": "text",
|
||||
"x": 144.5,
|
||||
"y": 610.0,
|
||||
"width": 130.0,
|
||||
"height": 77.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1696521973,
|
||||
"version": 1,
|
||||
"versionNonce": 731803796,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "디스크의 WAL\nDB 에 실제로 존재 149건",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "디스크의 WAL\nDB 에 실제로 존재 149건",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "a3-commit-to-disk-gap",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "29e279f39e07ba519e4e0685e5719d5d63b3ecf8b61a18816cbd5a1529baf854",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "A-2 · A-3 — DB 가 멈출 때와 죽을 때",
|
||||
"line": 201
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"a3-commit-to-disk-gap.svg",
|
||||
"a3-commit-to-disk-gap.mmd",
|
||||
"a3-commit-to-disk-gap.d2",
|
||||
"a3-commit-to-disk-gap.dot",
|
||||
"a3-commit-to-disk-gap.drawio",
|
||||
"a3-commit-to-disk-gap.excalidraw",
|
||||
"a3-commit-to-disk-gap.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
%% 200 과 디스크 사이의 빈 구간
|
||||
%% question: 로그인이 성공했는데 세션이 없을 수 있는 이유는 무엇인가
|
||||
flowchart TB
|
||||
n0(["클라이언트"])
|
||||
n1["Keycloak"]
|
||||
n2[("PostgreSQL")]
|
||||
n3[("디스크의 WAL")]
|
||||
n0 -->|"로그인 요청"| n1
|
||||
n1 -->|"세션 INSERT · COMMIT"| n2
|
||||
n2 -->|"지연된 WAL 기록"| n3
|
||||
classDef external stroke-dasharray: 6 4
|
||||
class n0 external
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="742" viewBox="0 0 680 742" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">200 과 디스크 사이의 빈 구간</title>
|
||||
<desc id="diagram-description">Keycloak 은 트랜잭션마다 SET LOCAL synchronous_commit TO OFF 를 건다. PostgreSQL 은 COMMIT 을 WAL 디스크 기록을 기다리지 않고 즉시 반환하고, 클라이언트는 200 과 토큰을 받는다. 실측된 wal_writer_delay 는 200ms 이며 그 사이에 프로세스가 죽으면 그만큼이 사라진다. 153건 중 4건이 그렇게 유실됐다. 버그가 아니라 의도된 설계이고 그 대가를 숫자로 확인한 것이다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"a3-commit-to-disk-gap","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"A-2 · A-3 — DB 가 멈출 때와 죽을 때","line":201}},"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="742" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="209.5,144.0 209.5,192.0 209.5,192.0 209.5,240.0" data-evidence="207-211" />
|
||||
<rect class="edge-label-bg" x="204.4" y="178.0" width="58.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="233.5" y="193.0">로그인 요청</text>
|
||||
<polyline class="edge kind-write style-solid emphasis-normal" points="209.5,311.0 209.5,359.0 209.5,359.0 209.5,407.0" data-evidence="211-215" />
|
||||
<rect class="edge-label-bg" x="164.2" y="345.0" width="138.6" height="22" rx="3" />
|
||||
<text class="edge-label" x="233.5" y="360.0">세션 INSERT · COMMIT</text>
|
||||
<polyline class="edge kind-write style-solid emphasis-normal" points="209.5,504.0 209.5,552.0 209.5,552.0 209.5,600.0" data-evidence="211-215" />
|
||||
<rect class="edge-label-bg" x="191.0" y="538.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="233.5" y="553.0">지연된 WAL 기록</text>
|
||||
<g id="node-client">
|
||||
<g class="actor-symbol emphasis-primary" data-evidence="207-213"><circle cx="209.5" cy="80.0" r="11.0" /><line x1="209.5" y1="96.0" x2="209.5" y2="105.0" /><line x1="191.5" y1="106.0" x2="227.5" y2="106.0" /><line x1="209.5" y1="105.0" x2="194.5" y2="122.0" /><line x1="209.5" y1="105.0" x2="224.5" y2="122.0" /></g>
|
||||
<text class="node-label" x="209.5" y="137.0">클라이언트</text>
|
||||
</g>
|
||||
<g id="node-keycloak">
|
||||
<rect class="node-shape kind-service emphasis-normal role-control" data-evidence="211-215" x="70.0" y="240.0" width="279.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="209.5" y="267.0">Keycloak</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="288.0" x2="335.0" y2="288.0" />
|
||||
<text class="node-detail" x="86.0" y="305.0">SET LOCAL synchronous_commit TO OFF</text>
|
||||
</g>
|
||||
<g id="node-pg">
|
||||
<rect class="node-shape kind-datastore emphasis-warning role-control" data-evidence="211-215" x="115.5" y="420.0" width="188.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-warning role-control" cx="209.5" cy="420.0" rx="94.0" ry="13.0" /><path class="storage-bottom" d="M 115.5 491.0 A 94.0 13.0 0 0 0 303.5 491.0" />
|
||||
<text class="node-label" x="209.5" y="447.0">PostgreSQL</text>
|
||||
<line class="node-detail-divider" x1="129.5" y1="468.0" x2="289.5" y2="468.0" />
|
||||
<text class="node-detail" x="131.5" y="485.0">wal_writer_delay 200ms</text>
|
||||
</g>
|
||||
<g id="node-disk">
|
||||
<rect class="node-shape kind-datastore emphasis-primary role-target" data-evidence="207-215" x="134.5" y="613.0" width="150.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-primary role-target" cx="209.5" cy="613.0" rx="75.0" ry="13.0" /><path class="storage-bottom" d="M 134.5 684.0 A 75.0 13.0 0 0 0 284.5 684.0" />
|
||||
<text class="node-label" x="209.5" y="640.0">디스크의 WAL</text>
|
||||
<line class="node-detail-divider" x1="148.5" y1="661.0" x2="270.5" y2="661.0" />
|
||||
<text class="node-detail" x="150.5" y="678.0">DB 에 실제로 존재 149건</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
@@ -0,0 +1,22 @@
|
||||
# 노드를 잃는 두 가지
|
||||
|
||||
## Alternative text
|
||||
|
||||
kc-lab-2 를 잃으면 데이터베이스가 함께 사라지고, kc-lab-1 을 잃으면 진입 경로가 사라지는 구성.
|
||||
|
||||
## Long description
|
||||
|
||||
kc-lab-2 를 잃으면 keycloak-1 은 살아 있지만 PostgreSQL 이 같이 죽어 외부 응답이 503 이 된다. kc-lab-1 을 잃으면 keycloak-0 은 계속 돌고 있는데 traefik 과 API 서버가 사라져 연결 자체가 안 된다. 워크로드가 멀쩡한데 도달할 수 없어 장애다. 진입점이 단일 노드에 있으면 워크로드 이중화는 의미가 없다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **외부 요청** (actor): 사용자. Evidence: L221–L228.
|
||||
- **진입 경로 (kc-lab-1)** (gateway): traefik 과 API 서버. 잃으면 000 이다. Evidence: L221–L231.
|
||||
- **keycloak-0 (kc-lab-2)** (service): 4b 에서는 계속 돌고 있었다. Evidence: L221–L231.
|
||||
- **PostgreSQL (kc-lab-2)** (datastore): 4a 에서 함께 죽는다. 잃으면 503 이다. Evidence: L221–L233.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **외부 요청 → 진입 경로 (kc-lab-1):** 요청. Evidence: L221–L228.
|
||||
- **진입 경로 (kc-lab-1) → keycloak-0 (kc-lab-2):** 라우팅. Evidence: L221–L228.
|
||||
- **keycloak-0 (kc-lab-2) → PostgreSQL (kc-lab-2):** 세션 조회. Evidence: L221–L233.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 노드를 잃는 두 가지
|
||||
# Question: 워커를 잃는 것과 컨트롤 플레인을 잃는 것은 어떻게 다른가
|
||||
direction: down
|
||||
n0: "외부 요청" {
|
||||
shape: person
|
||||
}
|
||||
n1: "진입 경로 (kc-lab-1)" {
|
||||
shape: diamond
|
||||
}
|
||||
n2: "keycloak-0 (kc-lab-2)" {
|
||||
shape: rectangle
|
||||
}
|
||||
n3: "PostgreSQL (kc-lab-2)" {
|
||||
shape: cylinder
|
||||
}
|
||||
n0 -> n1: "요청"
|
||||
n1 -> n2: "라우팅"
|
||||
n2 -> n3: "세션 조회"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="외부 요청", shape=box, style="rounded,dashed,filled"];
|
||||
n1 [label="진입 경로 (kc-lab-1)", shape=diamond, style="rounded,filled"];
|
||||
n2 [label="keycloak-0 (kc-lab-2)", shape=box, style="rounded,filled"];
|
||||
n3 [label="PostgreSQL (kc-lab-2)", shape=cylinder, style="rounded,filled"];
|
||||
n0 -> n1 [label="요청", style=solid];
|
||||
n1 -> n2 [label="라우팅", style=solid];
|
||||
n2 -> n3 [label="세션 조회", style=solid];
|
||||
}
|
||||
@@ -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="a4-two-node-losses" name="노드를 잃는 두 가지">
|
||||
<mxGraphModel dx="680" dy="716" 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_client" value="외부 요청" tooltip="사용자. | Evidence: L221-L228" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="92.5" y="60.0" width="150.0" height="84.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_entry" value="진입 경로 (kc-lab-1)<br/>kubectl 도 불통" tooltip="traefik 과 API 서버. 잃으면 000 이다. | Evidence: L221-L231" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;rhombus;perimeter=rhombusPerimeter;fillColor=#fff7e8;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
|
||||
<mxGeometry x="92.5" y="240.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_workload" value="keycloak-0 (kc-lab-2)<br/>도달할 수 없을 뿐이다" tooltip="4b 에서는 계속 돌고 있었다. | Evidence: L221-L231" 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="77.0" y="407.0" width="181.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_store" value="PostgreSQL (kc-lab-2)<br/>local-path PVC 라 재배치 불가" tooltip="4a 에서 함께 죽는다. 잃으면 503 이다. | Evidence: L221-L233" 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;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="574.0" width="195.0" height="97.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_c-e" value="요청" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_client" target="n_entry">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="191.5" y="192.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_e-w" value="라우팅" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_entry" target="n_workload">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="191.5" y="359.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_w-s" value="세션 조회" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_workload" target="n_store">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="191.5" y="526.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-c-e",
|
||||
"type": "arrow",
|
||||
"x": 167.5,
|
||||
"y": 144.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1513220568,
|
||||
"version": 1,
|
||||
"versionNonce": 255748168,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-client",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-entry",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-c-e",
|
||||
"type": "text",
|
||||
"x": 146.5,
|
||||
"y": 180.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 809267409,
|
||||
"version": 1,
|
||||
"versionNonce": 1101078427,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "요청",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "요청",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-e-w",
|
||||
"type": "arrow",
|
||||
"x": 167.5,
|
||||
"y": 311.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 547479275,
|
||||
"version": 1,
|
||||
"versionNonce": 1997988007,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-entry",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-workload",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-e-w",
|
||||
"type": "text",
|
||||
"x": 146.5,
|
||||
"y": 347.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1950725582,
|
||||
"version": 1,
|
||||
"versionNonce": 1719353100,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "라우팅",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "라우팅",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-w-s",
|
||||
"type": "arrow",
|
||||
"x": 167.5,
|
||||
"y": 478.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 635791852,
|
||||
"version": 1,
|
||||
"versionNonce": 232690295,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-workload",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-store",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-w-s",
|
||||
"type": "text",
|
||||
"x": 146.5,
|
||||
"y": 514.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 919243678,
|
||||
"version": 1,
|
||||
"versionNonce": 1666844084,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "세션 조회",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "세션 조회",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-client",
|
||||
"type": "rectangle",
|
||||
"x": 92.5,
|
||||
"y": 60.0,
|
||||
"width": 150.0,
|
||||
"height": 84.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1893748721,
|
||||
"version": 1,
|
||||
"versionNonce": 1322839316,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-client",
|
||||
"type": "text",
|
||||
"x": 102.5,
|
||||
"y": 70.0,
|
||||
"width": 130.0,
|
||||
"height": 64.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 817706832,
|
||||
"version": 1,
|
||||
"versionNonce": 1332899567,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "외부 요청",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "외부 요청",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-entry",
|
||||
"type": "rectangle",
|
||||
"x": 92.5,
|
||||
"y": 240.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#fff4e6",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1075845919,
|
||||
"version": 1,
|
||||
"versionNonce": 559732662,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-entry",
|
||||
"type": "text",
|
||||
"x": 102.5,
|
||||
"y": 250.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 910566709,
|
||||
"version": 1,
|
||||
"versionNonce": 1052126635,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "진입 경로 (kc-lab-1)\nkubectl 도 불통",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "진입 경로 (kc-lab-1)\nkubectl 도 불통",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-workload",
|
||||
"type": "rectangle",
|
||||
"x": 77.0,
|
||||
"y": 407.0,
|
||||
"width": 181.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 74091781,
|
||||
"version": 1,
|
||||
"versionNonce": 1219291209,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-workload",
|
||||
"type": "text",
|
||||
"x": 87.0,
|
||||
"y": 417.0,
|
||||
"width": 161.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 971306354,
|
||||
"version": 1,
|
||||
"versionNonce": 366449134,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "keycloak-0 (kc-lab-2)\n도달할 수 없을 뿐이다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "keycloak-0 (kc-lab-2)\n도달할 수 없을 뿐이다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-store",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 574.0,
|
||||
"width": 195.0,
|
||||
"height": 97.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1496341391,
|
||||
"version": 1,
|
||||
"versionNonce": 1595424416,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-store",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 584.0,
|
||||
"width": 175.0,
|
||||
"height": 77.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 156936309,
|
||||
"version": 1,
|
||||
"versionNonce": 52632335,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "PostgreSQL (kc-lab-2)\nlocal-path PVC 라 재배치 불가",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "PostgreSQL (kc-lab-2)\nlocal-path PVC 라 재배치 불가",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "a4-two-node-losses",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "788cb5ef3210d1bb16d41feb27bd35f4041e93fea3e9826e4463391b82ccee5e",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "A-4 · 노드 상실 — 둘 다 전면 장애지만 이유가 다르다",
|
||||
"line": 223
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"a4-two-node-losses.svg",
|
||||
"a4-two-node-losses.mmd",
|
||||
"a4-two-node-losses.d2",
|
||||
"a4-two-node-losses.dot",
|
||||
"a4-two-node-losses.drawio",
|
||||
"a4-two-node-losses.excalidraw",
|
||||
"a4-two-node-losses.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
%% 노드를 잃는 두 가지
|
||||
%% question: 워커를 잃는 것과 컨트롤 플레인을 잃는 것은 어떻게 다른가
|
||||
flowchart TB
|
||||
n0(["외부 요청"])
|
||||
n1{"진입 경로 (kc-lab-1)"}
|
||||
n2["keycloak-0 (kc-lab-2)"]
|
||||
n3[("PostgreSQL (kc-lab-2)")]
|
||||
n0 -->|"요청"| n1
|
||||
n1 -->|"라우팅"| n2
|
||||
n2 -->|"세션 조회"| n3
|
||||
classDef external stroke-dasharray: 6 4
|
||||
class n0 external
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="716" viewBox="0 0 680 716" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">노드를 잃는 두 가지</title>
|
||||
<desc id="diagram-description">kc-lab-2 를 잃으면 keycloak-1 은 살아 있지만 PostgreSQL 이 같이 죽어 외부 응답이 503 이 된다. kc-lab-1 을 잃으면 keycloak-0 은 계속 돌고 있는데 traefik 과 API 서버가 사라져 연결 자체가 안 된다. 워크로드가 멀쩡한데 도달할 수 없어 장애다. 진입점이 단일 노드에 있으면 워크로드 이중화는 의미가 없다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"a4-two-node-losses","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"A-4 · 노드 상실 — 둘 다 전면 장애지만 이유가 다르다","line":223}},"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="716" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="167.5,144.0 167.5,192.0 167.5,192.0 167.5,240.0" data-evidence="221-228" />
|
||||
<rect class="edge-label-bg" x="169.5" y="178.0" width="44.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="191.5" y="193.0">요청</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="167.5,311.0 167.5,359.0 167.5,359.0 167.5,407.0" data-evidence="221-228" />
|
||||
<rect class="edge-label-bg" x="169.5" y="345.0" width="44.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="191.5" y="360.0">라우팅</text>
|
||||
<polyline class="edge kind-read style-solid emphasis-normal" points="167.5,478.0 167.5,526.0 167.5,526.0 167.5,574.0" data-evidence="221-233" />
|
||||
<rect class="edge-label-bg" x="165.8" y="512.0" width="51.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="191.5" y="527.0">세션 조회</text>
|
||||
<g id="node-client">
|
||||
<g class="actor-symbol emphasis-primary" data-evidence="221-228"><circle cx="167.5" cy="80.0" r="11.0" /><line x1="167.5" y1="96.0" x2="167.5" y2="105.0" /><line x1="149.5" y1="106.0" x2="185.5" y2="106.0" /><line x1="167.5" y1="105.0" x2="152.5" y2="122.0" /><line x1="167.5" y1="105.0" x2="182.5" y2="122.0" /></g>
|
||||
<text class="node-label" x="167.5" y="137.0">외부 요청</text>
|
||||
</g>
|
||||
<g id="node-entry">
|
||||
<polygon class="node-shape kind-gateway emphasis-warning role-control" data-evidence="221-231" points="167.5,240.0 242.5,275.5 167.5,311.0 92.5,275.5" />
|
||||
<text class="node-label" x="167.5" y="267.0">진입 경로 (kc-lab-1)</text>
|
||||
<line class="node-detail-divider" x1="106.5" y1="288.0" x2="228.5" y2="288.0" />
|
||||
<text class="node-detail" x="108.5" y="305.0">kubectl 도 불통</text>
|
||||
</g>
|
||||
<g id="node-workload">
|
||||
<rect class="node-shape kind-service emphasis-normal role-control" data-evidence="221-231" x="77.0" y="407.0" width="181.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="167.5" y="434.0">keycloak-0 (kc-lab-2)</text>
|
||||
<line class="node-detail-divider" x1="91.0" y1="455.0" x2="244.0" y2="455.0" />
|
||||
<text class="node-detail" x="93.0" y="472.0">도달할 수 없을 뿐이다</text>
|
||||
</g>
|
||||
<g id="node-store">
|
||||
<rect class="node-shape kind-datastore emphasis-warning role-target" data-evidence="221-233" x="70.0" y="587.0" width="195.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-warning role-target" cx="167.5" cy="587.0" rx="97.5" ry="13.0" /><path class="storage-bottom" d="M 70.0 658.0 A 97.5 13.0 0 0 0 265.0 658.0" />
|
||||
<text class="node-label" x="167.5" y="614.0">PostgreSQL (kc-lab-2)</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="635.0" x2="251.0" y2="635.0" />
|
||||
<text class="node-detail" x="86.0" y="652.0">local-path PVC 라 재배치 불가</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.1 KiB |
@@ -0,0 +1,22 @@
|
||||
# 비대칭 차단은 가르지 못한다
|
||||
|
||||
## Alternative text
|
||||
|
||||
한 방향이 막혀도 반대 방향으로 연결이 성립하는 구성. 양방향을 다 막아야 두 멤버가 분리된다.
|
||||
|
||||
## Long description
|
||||
|
||||
JGroups 는 한 방향이 막혀도 열린 방향으로 재연결한다. 그래서 비대칭 차단으로는 cluster_size 가 2 로 유지된다. 양방향을 다 막으면 양쪽 모두 멤버 1개가 되지만 한쪽만 DOWN 이 된다. 코디네이터 쪽이 살아남고 분단된 쪽은 스스로 readiness 를 떨어뜨려 로드밸런서에서 빠지므로 외부 응답은 200 을 유지한다. 전면 장애 경로가 없다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **keycloak-0** (service): 코디네이터. 분단돼도 살아남는다. Evidence: L226–L236.
|
||||
- **막은 방향** (channel): raw 테이블 PREROUTING 에서 끊는다. Evidence: L226–L236.
|
||||
- **keycloak-1** (service): 분단되면 스스로 로드밸런서에서 빠진다. Evidence: L226–L236.
|
||||
- **열린 반대 방향** (channel): 비대칭 차단에서는 여기로 재연결한다. Evidence: L226–L232.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **막은 방향 → keycloak-1:** 막혀서 닿지 않는다. Evidence: L226–L232.
|
||||
- **keycloak-0 → 막은 방향:** JGroups 메시지. Evidence: L226–L232.
|
||||
- **keycloak-1 → 열린 반대 방향:** 반대 방향으로 재연결. Evidence: L226–L232.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 비대칭 차단은 가르지 못한다
|
||||
# Question: 한 방향만 막으면 클러스터가 갈라지는가
|
||||
direction: down
|
||||
n0: "keycloak-0" {
|
||||
shape: rectangle
|
||||
}
|
||||
n1: "막은 방향" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "keycloak-1" {
|
||||
shape: rectangle
|
||||
}
|
||||
n3: "열린 반대 방향" {
|
||||
shape: rectangle
|
||||
}
|
||||
n0 -> n1: "JGroups 메시지"
|
||||
n1 -> n2: "막혀서 닿지 않는다"
|
||||
n2 -> n3: "반대 방향으로 재연결"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="keycloak-0", shape=box, style="rounded,filled"];
|
||||
n1 [label="막은 방향", shape=box, style="rounded,filled"];
|
||||
n2 [label="keycloak-1", shape=box, style="rounded,filled"];
|
||||
n3 [label="열린 반대 방향", shape=box, style="rounded,filled"];
|
||||
n0 -> n1 [label="JGroups 메시지", style=solid];
|
||||
n1 -> n2 [label="막혀서 닿지 않는다", style=solid];
|
||||
n2 -> n3 [label="반대 방향으로 재연결", style=solid];
|
||||
}
|
||||
@@ -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="a5-partition-asymmetry" name="비대칭 차단은 가르지 못한다">
|
||||
<mxGraphModel dx="680" dy="677" 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_k0" value="keycloak-0<br/>cluster_size 1" tooltip="코디네이터. 분단돼도 살아남는다. | Evidence: L226-L236" 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="70.0" y="60.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_fwd" value="막은 방향<br/>conntrack 보다 먼저" tooltip="raw 테이블 PREROUTING 에서 끊는다. | Evidence: L226-L236" 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="70.0" y="227.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_k1" value="keycloak-1<br/>readiness 실패" tooltip="분단되면 스스로 로드밸런서에서 빠진다. | Evidence: L226-L236" 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="394.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_rev" value="열린 반대 방향<br/>가르지 못한다" tooltip="비대칭 차단에서는 여기로 재연결한다. | Evidence: L226-L232" 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="561.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_f-k1" value="막혀서 닿지 않는다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_fwd" target="n_k1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="169.0" y="346.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_k0-f" value="JGroups 메시지" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_k0" target="n_fwd">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="169.0" y="179.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_k1-rev" value="반대 방향으로 재연결" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_k1" target="n_rev">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="169.0" y="513.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-f-k1",
|
||||
"type": "arrow",
|
||||
"x": 145.0,
|
||||
"y": 298.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 754052919,
|
||||
"version": 1,
|
||||
"versionNonce": 1393818205,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-fwd",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-k1",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-f-k1",
|
||||
"type": "text",
|
||||
"x": 124.0,
|
||||
"y": 334.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1611436970,
|
||||
"version": 1,
|
||||
"versionNonce": 1646108018,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "막혀서 닿지 않는다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "막혀서 닿지 않는다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-k0-f",
|
||||
"type": "arrow",
|
||||
"x": 145.0,
|
||||
"y": 131.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 251939064,
|
||||
"version": 1,
|
||||
"versionNonce": 443740378,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-k0",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-fwd",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-k0-f",
|
||||
"type": "text",
|
||||
"x": 124.0,
|
||||
"y": 167.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1066092082,
|
||||
"version": 1,
|
||||
"versionNonce": 522159602,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "JGroups 메시지",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "JGroups 메시지",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-k1-rev",
|
||||
"type": "arrow",
|
||||
"x": 145.0,
|
||||
"y": 465.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 978841079,
|
||||
"version": 1,
|
||||
"versionNonce": 1381835331,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-k1",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-rev",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-k1-rev",
|
||||
"type": "text",
|
||||
"x": 124.0,
|
||||
"y": 501.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 635184409,
|
||||
"version": 1,
|
||||
"versionNonce": 253961448,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "반대 방향으로 재연결",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "반대 방향으로 재연결",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-k0",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 60.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 772945349,
|
||||
"version": 1,
|
||||
"versionNonce": 30235372,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-k0",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 70.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1107535317,
|
||||
"version": 1,
|
||||
"versionNonce": 630670490,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "keycloak-0\ncluster_size 1",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "keycloak-0\ncluster_size 1",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-fwd",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 227.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 535996259,
|
||||
"version": 1,
|
||||
"versionNonce": 1320060582,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-fwd",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 237.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1947049900,
|
||||
"version": 1,
|
||||
"versionNonce": 493507574,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "막은 방향\nconntrack 보다 먼저",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "막은 방향\nconntrack 보다 먼저",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-k1",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 394.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1338591829,
|
||||
"version": 1,
|
||||
"versionNonce": 367320954,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-k1",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 404.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1741943564,
|
||||
"version": 1,
|
||||
"versionNonce": 368770159,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "keycloak-1\nreadiness 실패",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "keycloak-1\nreadiness 실패",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-rev",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 561.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1979585501,
|
||||
"version": 1,
|
||||
"versionNonce": 1354430280,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-rev",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 571.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 697822121,
|
||||
"version": 1,
|
||||
"versionNonce": 1254339695,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "열린 반대 방향\n가르지 못한다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "열린 반대 방향\n가르지 못한다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "a5-partition-asymmetry",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "26b2dc620b94c979befdef3808475899361f52b7f622fc06a78b5c6c59cd72a7",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "A-5 · 비대칭 분단 — 전면 장애 경로가 없다",
|
||||
"line": 246
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"a5-partition-asymmetry.svg",
|
||||
"a5-partition-asymmetry.mmd",
|
||||
"a5-partition-asymmetry.d2",
|
||||
"a5-partition-asymmetry.dot",
|
||||
"a5-partition-asymmetry.drawio",
|
||||
"a5-partition-asymmetry.excalidraw",
|
||||
"a5-partition-asymmetry.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
%% 비대칭 차단은 가르지 못한다
|
||||
%% question: 한 방향만 막으면 클러스터가 갈라지는가
|
||||
flowchart TB
|
||||
n0["keycloak-0"]
|
||||
n1["막은 방향"]
|
||||
n2["keycloak-1"]
|
||||
n3["열린 반대 방향"]
|
||||
n0 -->|"JGroups 메시지"| n1
|
||||
n1 -->|"막혀서 닿지 않는다"| n2
|
||||
n2 -->|"반대 방향으로 재연결"| n3
|
||||
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="677" viewBox="0 0 680 677" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">비대칭 차단은 가르지 못한다</title>
|
||||
<desc id="diagram-description">JGroups 는 한 방향이 막혀도 열린 방향으로 재연결한다. 그래서 비대칭 차단으로는 cluster_size 가 2 로 유지된다. 양방향을 다 막으면 양쪽 모두 멤버 1개가 되지만 한쪽만 DOWN 이 된다. 코디네이터 쪽이 살아남고 분단된 쪽은 스스로 readiness 를 떨어뜨려 로드밸런서에서 빠지므로 외부 응답은 200 을 유지한다. 전면 장애 경로가 없다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"a5-partition-asymmetry","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"A-5 · 비대칭 분단 — 전면 장애 경로가 없다","line":246}},"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="677" />
|
||||
<polyline class="edge kind-blocked style-solid emphasis-normal" points="145.0,298.0 145.0,346.0 145.0,346.0 145.0,394.0" data-evidence="226-232" />
|
||||
<rect class="edge-label-bg" x="126.5" y="332.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="169.0" y="347.0">막혀서 닿지 않는다</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="145.0,131.0 145.0,179.0 145.0,179.0 145.0,227.0" data-evidence="226-232" />
|
||||
<rect class="edge-label-bg" x="123.2" y="165.0" width="91.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="169.0" y="180.0">JGroups 메시지</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="145.0,465.0 145.0,513.0 145.0,513.0 145.0,561.0" data-evidence="226-232" />
|
||||
<rect class="edge-label-bg" x="123.2" y="499.0" width="91.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="169.0" y="514.0">반대 방향으로 재연결</text>
|
||||
<g id="node-k0">
|
||||
<rect class="node-shape kind-service emphasis-primary role-source" data-evidence="226-236" x="70.0" y="60.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="87.0">keycloak-0</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="108.0" x2="206.0" y2="108.0" />
|
||||
<text class="node-detail" x="86.0" y="125.0">cluster_size 1</text>
|
||||
</g>
|
||||
<g id="node-fwd">
|
||||
<rect class="node-shape kind-channel emphasis-warning role-control" data-evidence="226-236" x="70.0" y="227.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="254.0">막은 방향</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="275.0" x2="206.0" y2="275.0" />
|
||||
<text class="node-detail" x="86.0" y="292.0">conntrack 보다 먼저</text>
|
||||
</g>
|
||||
<g id="node-k1">
|
||||
<rect class="node-shape kind-service emphasis-normal role-target" data-evidence="226-236" x="70.0" y="394.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="421.0">keycloak-1</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="442.0" x2="206.0" y2="442.0" />
|
||||
<text class="node-detail" x="86.0" y="459.0">readiness 실패</text>
|
||||
</g>
|
||||
<g id="node-rev">
|
||||
<rect class="node-shape kind-channel emphasis-normal role-support" data-evidence="226-232" x="70.0" y="561.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="588.0">열린 반대 방향</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="609.0" x2="206.0" y2="609.0" />
|
||||
<text class="node-detail" x="86.0" y="626.0">가르지 못한다</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,22 @@
|
||||
# 지연이 곱해지는 두 단계
|
||||
|
||||
## Alternative text
|
||||
|
||||
네트워크 지연이 왕복 횟수만큼 누적되고 커넥션 풀 대기에서 다시 증폭되며 마지막에 readiness 실패로 이어지는 구성.
|
||||
|
||||
## Long description
|
||||
|
||||
주입한 지연은 200ms 인데 로그인 응답이 1,872ms 가 됐다. 로그인 한 번이 데이터베이스 왕복을 여러 번 하므로 왕복 횟수만큼 더해진다. 동시 20건에서는 22.2초가 됐다. 요청이 커넥션을 오래 붙들어 뒤의 요청이 풀에서 대기하기 때문이며 커넥션 획득 대기 최대가 20,000ms 였다. 마지막으로 readiness 프로브가 타임아웃으로 실패해 느린 노드가 로드밸런서에서 빠진다. 느림이 장애로 승격된다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **주입한 지연** (process): tc netem 으로 넣는다. Evidence: L253–L258.
|
||||
- **왕복 횟수만큼 누적** (process): 로그인 한 번이 DB 를 여러 번 왕복한다. Evidence: L259–L263.
|
||||
- **커넥션 풀 대기** (component): 앞 요청이 커넥션을 붙들어 뒤가 밀린다. Evidence: L264–L268.
|
||||
- **readiness 실패** (process): 느린 노드가 로드밸런서에서 빠진다. Evidence: L269–L273.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **주입한 지연 → 왕복 횟수만큼 누적:** 왕복마다 더해진다. Evidence: L253–L263.
|
||||
- **커넥션 풀 대기 → readiness 실패:** 프로브도 타임아웃. Evidence: L264–L273.
|
||||
- **왕복 횟수만큼 누적 → 커넥션 풀 대기:** 커넥션 점유가 길어진다. Evidence: L259–L268.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 지연이 곱해지는 두 단계
|
||||
# Question: 200밀리초가 어떻게 22초가 되는가
|
||||
direction: down
|
||||
n0: "주입한 지연" {
|
||||
shape: rectangle
|
||||
}
|
||||
n1: "왕복 횟수만큼 누적" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "커넥션 풀 대기" {
|
||||
shape: rectangle
|
||||
}
|
||||
n3: "readiness 실패" {
|
||||
shape: rectangle
|
||||
}
|
||||
n0 -> n1: "왕복마다 더해진다"
|
||||
n1 -> n2: "커넥션 점유가 길어진다"
|
||||
n2 -> n3: "프로브도 타임아웃"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="주입한 지연", shape=box, style="rounded,filled"];
|
||||
n1 [label="왕복 횟수만큼 누적", shape=box, style="rounded,filled"];
|
||||
n2 [label="커넥션 풀 대기", shape=box, style="rounded,filled"];
|
||||
n3 [label="readiness 실패", shape=box, style="rounded,filled"];
|
||||
n0 -> n1 [label="왕복마다 더해진다", style=solid];
|
||||
n1 -> n2 [label="커넥션 점유가 길어진다", style=solid];
|
||||
n2 -> n3 [label="프로브도 타임아웃", style=solid];
|
||||
}
|
||||
@@ -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="a6-latency-multiplication" name="지연이 곱해지는 두 단계">
|
||||
<mxGraphModel dx="680" dy="677" 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_inject" value="주입한 지연<br/>200 ms" tooltip="tc netem 으로 넣는다. | Evidence: L253-L258" 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="71.5" y="60.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_roundtrips" value="왕복 횟수만큼 누적<br/>66ms → 1,872ms" tooltip="로그인 한 번이 DB 를 여러 번 왕복한다. | Evidence: L259-L263" 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="71.5" y="227.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_pool" value="커넥션 풀 대기<br/>획득 대기 최대 20,000ms" tooltip="앞 요청이 커넥션을 붙들어 뒤가 밀린다. | Evidence: L264-L268" 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="70.0" y="394.0" width="153.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_probe" value="readiness 실패<br/>동시 20건 최대 22.2초" tooltip="느린 노드가 로드밸런서에서 빠진다. | Evidence: L269-L273" 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="71.5" y="561.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_i-r" value="왕복마다 더해진다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_inject" target="n_roundtrips">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="170.5" y="179.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_p-pr" value="프로브도 타임아웃" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_pool" target="n_probe">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="170.5" y="513.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_r-p" value="커넥션 점유가 길어진다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_roundtrips" target="n_pool">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="170.5" y="346.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-i-r",
|
||||
"type": "arrow",
|
||||
"x": 146.5,
|
||||
"y": 131.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 590595326,
|
||||
"version": 1,
|
||||
"versionNonce": 144296589,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-inject",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-roundtrips",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-i-r",
|
||||
"type": "text",
|
||||
"x": 125.5,
|
||||
"y": 167.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1795354243,
|
||||
"version": 1,
|
||||
"versionNonce": 270464775,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "왕복마다 더해진다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "왕복마다 더해진다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-p-pr",
|
||||
"type": "arrow",
|
||||
"x": 146.5,
|
||||
"y": 465.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1415151372,
|
||||
"version": 1,
|
||||
"versionNonce": 369564785,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-pool",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-probe",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-p-pr",
|
||||
"type": "text",
|
||||
"x": 125.5,
|
||||
"y": 501.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 800431591,
|
||||
"version": 1,
|
||||
"versionNonce": 1649790496,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "프로브도 타임아웃",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "프로브도 타임아웃",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-r-p",
|
||||
"type": "arrow",
|
||||
"x": 146.5,
|
||||
"y": 298.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1208893182,
|
||||
"version": 1,
|
||||
"versionNonce": 958848636,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-roundtrips",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-pool",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-r-p",
|
||||
"type": "text",
|
||||
"x": 122.5,
|
||||
"y": 334.0,
|
||||
"width": 96,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 194269574,
|
||||
"version": 1,
|
||||
"versionNonce": 208108007,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "커넥션 점유가 길어진다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "커넥션 점유가 길어진다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-inject",
|
||||
"type": "rectangle",
|
||||
"x": 71.5,
|
||||
"y": 60.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 939343234,
|
||||
"version": 1,
|
||||
"versionNonce": 1584547150,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-inject",
|
||||
"type": "text",
|
||||
"x": 81.5,
|
||||
"y": 70.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 379587616,
|
||||
"version": 1,
|
||||
"versionNonce": 1009589520,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "주입한 지연\n200 ms",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "주입한 지연\n200 ms",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-roundtrips",
|
||||
"type": "rectangle",
|
||||
"x": 71.5,
|
||||
"y": 227.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 708691311,
|
||||
"version": 1,
|
||||
"versionNonce": 1798580033,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-roundtrips",
|
||||
"type": "text",
|
||||
"x": 81.5,
|
||||
"y": 237.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 162889864,
|
||||
"version": 1,
|
||||
"versionNonce": 1688811133,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "왕복 횟수만큼 누적\n66ms → 1,872ms",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "왕복 횟수만큼 누적\n66ms → 1,872ms",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-pool",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 394.0,
|
||||
"width": 153.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 23264887,
|
||||
"version": 1,
|
||||
"versionNonce": 1346443202,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-pool",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 404.0,
|
||||
"width": 133.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 864087689,
|
||||
"version": 1,
|
||||
"versionNonce": 1004607352,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "커넥션 풀 대기\n획득 대기 최대 20,000ms",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "커넥션 풀 대기\n획득 대기 최대 20,000ms",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-probe",
|
||||
"type": "rectangle",
|
||||
"x": 71.5,
|
||||
"y": 561.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1262508266,
|
||||
"version": 1,
|
||||
"versionNonce": 1472248480,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-probe",
|
||||
"type": "text",
|
||||
"x": 81.5,
|
||||
"y": 571.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 461284557,
|
||||
"version": 1,
|
||||
"versionNonce": 88654975,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "readiness 실패\n동시 20건 최대 22.2초",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "readiness 실패\n동시 20건 최대 22.2초",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "a6-latency-multiplication",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "c9d669f597dc9b26d68636bb52e7239c266a8c5985c73d51c3fe0b7ebd7ff231",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "A-6 · 지연 주입 — 200밀리초가 22초가 된다",
|
||||
"line": 255
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"a6-latency-multiplication.svg",
|
||||
"a6-latency-multiplication.mmd",
|
||||
"a6-latency-multiplication.d2",
|
||||
"a6-latency-multiplication.dot",
|
||||
"a6-latency-multiplication.drawio",
|
||||
"a6-latency-multiplication.excalidraw",
|
||||
"a6-latency-multiplication.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
%% 지연이 곱해지는 두 단계
|
||||
%% question: 200밀리초가 어떻게 22초가 되는가
|
||||
flowchart TB
|
||||
n0["주입한 지연"]
|
||||
n1["왕복 횟수만큼 누적"]
|
||||
n2["커넥션 풀 대기"]
|
||||
n3["readiness 실패"]
|
||||
n0 -->|"왕복마다 더해진다"| n1
|
||||
n1 -->|"커넥션 점유가 길어진다"| n2
|
||||
n2 -->|"프로브도 타임아웃"| n3
|
||||
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="677" viewBox="0 0 680 677" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">지연이 곱해지는 두 단계</title>
|
||||
<desc id="diagram-description">주입한 지연은 200ms 인데 로그인 응답이 1,872ms 가 됐다. 로그인 한 번이 데이터베이스 왕복을 여러 번 하므로 왕복 횟수만큼 더해진다. 동시 20건에서는 22.2초가 됐다. 요청이 커넥션을 오래 붙들어 뒤의 요청이 풀에서 대기하기 때문이며 커넥션 획득 대기 최대가 20,000ms 였다. 마지막으로 readiness 프로브가 타임아웃으로 실패해 느린 노드가 로드밸런서에서 빠진다. 느림이 장애로 승격된다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"a6-latency-multiplication","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"A-6 · 지연 주입 — 200밀리초가 22초가 된다","line":255}},"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="677" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="146.5,131.0 146.5,179.0 146.5,179.0 146.5,227.0" data-evidence="253-263" />
|
||||
<rect class="edge-label-bg" x="131.3" y="165.0" width="78.3" height="22" rx="3" />
|
||||
<text class="edge-label" x="170.5" y="180.0">왕복마다 더해진다</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="146.5,465.0 146.5,513.0 146.5,513.0 146.5,561.0" data-evidence="264-273" />
|
||||
<rect class="edge-label-bg" x="131.3" y="499.0" width="78.3" height="22" rx="3" />
|
||||
<text class="edge-label" x="170.5" y="514.0">프로브도 타임아웃</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="146.5,298.0 146.5,346.0 146.5,346.0 146.5,394.0" data-evidence="259-268" />
|
||||
<rect class="edge-label-bg" x="121.3" y="332.0" width="98.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="170.5" y="347.0">커넥션 점유가 길어진다</text>
|
||||
<g id="node-inject">
|
||||
<rect class="node-shape kind-process emphasis-warning role-source" data-evidence="253-258" x="71.5" y="60.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="146.5" y="87.0">주입한 지연</text>
|
||||
<line class="node-detail-divider" x1="85.5" y1="108.0" x2="207.5" y2="108.0" />
|
||||
<text class="node-detail" x="87.5" y="125.0">200 ms</text>
|
||||
</g>
|
||||
<g id="node-roundtrips">
|
||||
<rect class="node-shape kind-process emphasis-normal role-control" data-evidence="259-263" x="71.5" y="227.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="146.5" y="254.0">왕복 횟수만큼 누적</text>
|
||||
<line class="node-detail-divider" x1="85.5" y1="275.0" x2="207.5" y2="275.0" />
|
||||
<text class="node-detail" x="87.5" y="292.0">66ms → 1,872ms</text>
|
||||
</g>
|
||||
<g id="node-pool">
|
||||
<rect class="node-shape kind-component emphasis-warning role-control" data-evidence="264-268" x="70.0" y="394.0" width="153.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="146.5" y="421.0">커넥션 풀 대기</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="442.0" x2="209.0" y2="442.0" />
|
||||
<text class="node-detail" x="86.0" y="459.0">획득 대기 최대 20,000ms</text>
|
||||
</g>
|
||||
<g id="node-probe">
|
||||
<rect class="node-shape kind-process emphasis-warning role-target" data-evidence="269-273" x="71.5" y="561.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="146.5" y="588.0">readiness 실패</text>
|
||||
<line class="node-detail-divider" x1="85.5" y1="609.0" x2="207.5" y2="609.0" />
|
||||
<text class="node-detail" x="87.5" y="626.0">동시 20건 최대 22.2초</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,22 @@
|
||||
# 재시작이 지우는 것과 남기는 것
|
||||
|
||||
## Alternative text
|
||||
|
||||
재시작으로 Infinispan 캐시가 0 이 되지만 PostgreSQL 의 세션 행은 그대로 남아 refresh 가 계속 통하는 구성.
|
||||
|
||||
## Long description
|
||||
|
||||
롤링 재시작은 한 번에 한 파드씩 바꾼다. 파드가 죽으면 그 노드의 Infinispan sessions 캐시는 0 으로 초기화된다. 그러나 PostgreSQL 의 세션 행은 151개가 그대로 남았고 재시작 전에 발급한 refresh token 이 여전히 200 을 받는다. 전 구간 서비스 중단이 없었다. volatile 모드였다면 같은 재시작에서 400 Session not active 가 되어 전원 로그아웃이 된다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **롤링 재시작** (process): 한 번에 한 파드씩 교체한다. Evidence: L270–L274.
|
||||
- **Infinispan sessions 캐시** (component): 파드와 함께 사라진다. Evidence: L270–L278.
|
||||
- **PostgreSQL 세션 행** (datastore): 재시작과 무관하다. Evidence: L270–L278.
|
||||
- **재시작 전 발급한 refresh token** (component): 여전히 통한다. Evidence: L270–L278.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **PostgreSQL 세션 행 → 재시작 전 발급한 refresh token:** 세션이 남아 있어 통한다. Evidence: L270–L278.
|
||||
- **롤링 재시작 → Infinispan sessions 캐시:** 비운다. Evidence: L270–L278.
|
||||
- **롤링 재시작 → PostgreSQL 세션 행:** 건드리지 않는다. Evidence: L270–L278.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 재시작이 지우는 것과 남기는 것
|
||||
# Question: 롤링 재시작 뒤에도 로그인이 유지되는 이유는 무엇인가
|
||||
direction: down
|
||||
n0: "롤링 재시작" {
|
||||
shape: rectangle
|
||||
}
|
||||
n1: "Infinispan sessions 캐시" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "PostgreSQL 세션 행" {
|
||||
shape: cylinder
|
||||
}
|
||||
n3: "재시작 전 발급한 refresh token" {
|
||||
shape: rectangle
|
||||
}
|
||||
n0 -> n1: "비운다"
|
||||
n0 -> n2: "건드리지 않는다"
|
||||
n2 -> n3: "세션이 남아 있어 통한다"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="롤링 재시작", shape=box, style="rounded,filled"];
|
||||
n1 [label="Infinispan sessions 캐시", shape=box, style="rounded,filled"];
|
||||
n2 [label="PostgreSQL 세션 행", shape=cylinder, style="rounded,filled"];
|
||||
n3 [label="재시작 전 발급한 refresh token", shape=box, style="rounded,filled"];
|
||||
n0 -> n1 [label="비운다", style=solid];
|
||||
n0 -> n2 [label="건드리지 않는다", style=solid];
|
||||
n2 -> n3 [label="세션이 남아 있어 통한다", style=solid];
|
||||
}
|
||||
@@ -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="a8-cache-vs-session" name="재시작이 지우는 것과 남기는 것">
|
||||
<mxGraphModel dx="680" dy="556" 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_restart" value="롤링 재시작<br/>kubectl rollout restart" tooltip="한 번에 한 파드씩 교체한다. | Evidence: L270-L274" 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="171.5" y="60.0" width="195.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_cache" value="Infinispan sessions 캐시<br/>entries_unique → 0" tooltip="파드와 함께 사라진다. | Evidence: L270-L278" 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="70.0" y="240.0" width="188.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_db" value="PostgreSQL 세션 행<br/>151 → 151" tooltip="재시작과 무관하다. | Evidence: L270-L278" 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;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="318.0" y="227.0" width="150.0" height="97.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_refresh" value="재시작 전 발급한 refresh token<br/>200" tooltip="여전히 통한다. | Evidence: L270-L278" 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="192.5" y="420.0" width="153.0" height="91.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_db-rt" value="세션이 남아 있어 통한다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_db" target="n_refresh">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="331.0" y="344.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_r-c" value="비운다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_restart" target="n_cache">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="212.0" y="157.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_r-db" value="건드리지 않는다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_restart" target="n_db">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="335.5" y="151.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-db-rt",
|
||||
"type": "arrow",
|
||||
"x": 269.0,
|
||||
"y": 324.0,
|
||||
"width": 124.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 214320726,
|
||||
"version": 1,
|
||||
"versionNonce": 1768987917,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
124.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
124.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-db",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-refresh",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-db-rt",
|
||||
"type": "text",
|
||||
"x": 279.0,
|
||||
"y": 332.0,
|
||||
"width": 104,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 273713751,
|
||||
"version": 1,
|
||||
"versionNonce": 286370415,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "세션이 남아 있어 통한다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "세션이 남아 있어 통한다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-r-c",
|
||||
"type": "arrow",
|
||||
"x": 164.0,
|
||||
"y": 131.0,
|
||||
"width": 96.0,
|
||||
"height": 109.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 421159370,
|
||||
"version": 1,
|
||||
"versionNonce": 1082378856,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
96.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
96.0,
|
||||
54.5
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
54.5
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
109.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-restart",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-cache",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-r-c",
|
||||
"type": "text",
|
||||
"x": 167.0,
|
||||
"y": 145.5,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 738095813,
|
||||
"version": 1,
|
||||
"versionNonce": 1982565255,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "비운다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "비운다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-r-db",
|
||||
"type": "arrow",
|
||||
"x": 278.0,
|
||||
"y": 131.0,
|
||||
"width": 115.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1542068113,
|
||||
"version": 1,
|
||||
"versionNonce": 611269416,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
115.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
115.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-restart",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-db",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-r-db",
|
||||
"type": "text",
|
||||
"x": 290.5,
|
||||
"y": 139.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 953082516,
|
||||
"version": 1,
|
||||
"versionNonce": 1040618264,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "건드리지 않는다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "건드리지 않는다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-restart",
|
||||
"type": "rectangle",
|
||||
"x": 171.5,
|
||||
"y": 60.0,
|
||||
"width": 195.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 541751545,
|
||||
"version": 1,
|
||||
"versionNonce": 112214280,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-restart",
|
||||
"type": "text",
|
||||
"x": 181.5,
|
||||
"y": 70.0,
|
||||
"width": 175.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 902264258,
|
||||
"version": 1,
|
||||
"versionNonce": 194970368,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "롤링 재시작\nkubectl rollout restart",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "롤링 재시작\nkubectl rollout restart",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-cache",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 240.0,
|
||||
"width": 188.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1479106285,
|
||||
"version": 1,
|
||||
"versionNonce": 377767731,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-cache",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 250.0,
|
||||
"width": 168.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1704427303,
|
||||
"version": 1,
|
||||
"versionNonce": 762747802,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "Infinispan sessions 캐시\nentries_unique → 0",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "Infinispan sessions 캐시\nentries_unique → 0",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-db",
|
||||
"type": "rectangle",
|
||||
"x": 318.0,
|
||||
"y": 227.0,
|
||||
"width": 150.0,
|
||||
"height": 97.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 285058125,
|
||||
"version": 1,
|
||||
"versionNonce": 1824840415,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-db",
|
||||
"type": "text",
|
||||
"x": 328.0,
|
||||
"y": 237.0,
|
||||
"width": 130.0,
|
||||
"height": 77.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1231096363,
|
||||
"version": 1,
|
||||
"versionNonce": 313576462,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "PostgreSQL 세션 행\n151 → 151",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "PostgreSQL 세션 행\n151 → 151",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-refresh",
|
||||
"type": "rectangle",
|
||||
"x": 192.5,
|
||||
"y": 420.0,
|
||||
"width": 153.0,
|
||||
"height": 91.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1598484681,
|
||||
"version": 1,
|
||||
"versionNonce": 1621202109,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-refresh",
|
||||
"type": "text",
|
||||
"x": 202.5,
|
||||
"y": 430.0,
|
||||
"width": 133.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 794432888,
|
||||
"version": 1,
|
||||
"versionNonce": 204506970,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "재시작 전 발급한 refresh token\n200",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "재시작 전 발급한 refresh token\n200",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "a8-cache-vs-session",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "60ae0f0551684a3d013de2a0eca5a41d5edf858f99511c29441388c02f7310c2",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "A-8 · 롤링 재시작 — 세션은 살아남고 캐시만 사라진다",
|
||||
"line": 272
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"a8-cache-vs-session.svg",
|
||||
"a8-cache-vs-session.mmd",
|
||||
"a8-cache-vs-session.d2",
|
||||
"a8-cache-vs-session.dot",
|
||||
"a8-cache-vs-session.drawio",
|
||||
"a8-cache-vs-session.excalidraw",
|
||||
"a8-cache-vs-session.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
%% 재시작이 지우는 것과 남기는 것
|
||||
%% question: 롤링 재시작 뒤에도 로그인이 유지되는 이유는 무엇인가
|
||||
flowchart TB
|
||||
n0["롤링 재시작"]
|
||||
n1["Infinispan sessions 캐시"]
|
||||
n2[("PostgreSQL 세션 행")]
|
||||
n3["재시작 전 발급한 refresh token"]
|
||||
n0 -->|"비운다"| n1
|
||||
n0 -->|"건드리지 않는다"| n2
|
||||
n2 -->|"세션이 남아 있어 통한다"| n3
|
||||
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="556" viewBox="0 0 680 556" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">재시작이 지우는 것과 남기는 것</title>
|
||||
<desc id="diagram-description">롤링 재시작은 한 번에 한 파드씩 바꾼다. 파드가 죽으면 그 노드의 Infinispan sessions 캐시는 0 으로 초기화된다. 그러나 PostgreSQL 의 세션 행은 151개가 그대로 남았고 재시작 전에 발급한 refresh token 이 여전히 200 을 받는다. 전 구간 서비스 중단이 없었다. volatile 모드였다면 같은 재시작에서 400 Session not active 가 되어 전원 로그아웃이 된다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"a8-cache-vs-session","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"A-8 · 롤링 재시작 — 세션은 살아남고 캐시만 사라진다","line":272}},"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="556" />
|
||||
<polyline class="edge kind-read style-solid emphasis-normal" points="393.0,324.0 393.0,372.0 269.0,372.0 269.0,420.0" data-evidence="270-278" />
|
||||
<rect class="edge-label-bg" x="278.4" y="330.0" width="105.1" height="22" rx="3" />
|
||||
<text class="edge-label" x="331.0" y="345.0">세션이 남아 있어 통한다</text>
|
||||
<polyline class="edge kind-write style-solid emphasis-normal" points="260.0,131.0 260.0,185.5 164.0,185.5 164.0,240.0" data-evidence="270-278" />
|
||||
<rect class="edge-label-bg" x="190.0" y="143.5" width="44.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="212.0" y="158.5">비운다</text>
|
||||
<polyline class="edge kind-read style-solid emphasis-normal" points="278.0,131.0 278.0,179.0 393.0,179.0 393.0,227.0" data-evidence="270-278" />
|
||||
<rect class="edge-label-bg" x="299.7" y="137.0" width="71.6" height="22" rx="3" />
|
||||
<text class="edge-label" x="335.5" y="152.0">건드리지 않는다</text>
|
||||
<g id="node-restart">
|
||||
<rect class="node-shape kind-process emphasis-primary role-source" data-evidence="270-274" x="171.5" y="60.0" width="195.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="269.0" y="87.0">롤링 재시작</text>
|
||||
<line class="node-detail-divider" x1="185.5" y1="108.0" x2="352.5" y2="108.0" />
|
||||
<text class="node-detail" x="187.5" y="125.0">kubectl rollout restart</text>
|
||||
</g>
|
||||
<g id="node-cache">
|
||||
<rect class="node-shape kind-component emphasis-warning role-target" data-evidence="270-278" x="70.0" y="240.0" width="188.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="164.0" y="267.0">Infinispan sessions 캐시</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="288.0" x2="244.0" y2="288.0" />
|
||||
<text class="node-detail" x="86.0" y="305.0">entries_unique → 0</text>
|
||||
</g>
|
||||
<g id="node-db">
|
||||
<rect class="node-shape kind-datastore emphasis-primary role-target" data-evidence="270-278" x="318.0" y="240.0" width="150.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-primary role-target" cx="393.0" cy="240.0" rx="75.0" ry="13.0" /><path class="storage-bottom" d="M 318.0 311.0 A 75.0 13.0 0 0 0 468.0 311.0" />
|
||||
<text class="node-label" x="393.0" y="267.0">PostgreSQL 세션 행</text>
|
||||
<line class="node-detail-divider" x1="332.0" y1="288.0" x2="454.0" y2="288.0" />
|
||||
<text class="node-detail" x="334.0" y="305.0">151 → 151</text>
|
||||
</g>
|
||||
<g id="node-refresh">
|
||||
<rect class="node-shape kind-component emphasis-primary role-target" data-evidence="270-278" x="192.5" y="420.0" width="153.0" height="91.0" rx="7" />
|
||||
<text class="node-label" x="269.0" y="447.0">재시작 전 발급한 refresh</text>
|
||||
<text class="node-label" x="269.0" y="465.0">token</text>
|
||||
<line class="node-detail-divider" x1="206.5" y1="486.0" x2="331.5" y2="486.0" />
|
||||
<text class="node-detail" x="208.5" y="503.0">200</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,22 @@
|
||||
# 덮어쓰기를 만드는 기본키
|
||||
|
||||
## Alternative text
|
||||
|
||||
두 브라우저 세션이 서로 다른 세션 행을 갖지만 토큰 테이블에서는 같은 행을 가리키는 구성.
|
||||
|
||||
## Long description
|
||||
|
||||
토큰을 PostgreSQL 로 옮겨 다중 인스턴스 문제는 풀렸다. 그러나 기본키가 client_registration_id 와 principal_name 의 조합이고 세션 id 가 들어 있지 않다. 같은 사용자의 두 세션이 같은 행을 쓰므로 나중 로그인이 앞의 토큰을 덮어쓴다. 그리고 로그아웃하면 Redis 세션은 0 키로 정리되지만 PostgreSQL 에는 평문 refresh token 이 한 행 그대로 남는다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **브라우저 A 세션** (component): 세션 id 가 다르다. Evidence: L360–L366.
|
||||
- **브라우저 B 세션** (component): 세션 id 가 다르다. Evidence: L360–L366.
|
||||
- **토큰 행** (datastore): 기본키에 세션 id 가 없어 둘이 같은 행을 쓴다. Evidence: L370–L377.
|
||||
- **로그아웃 후 남는 것** (datastore): Redis 세션은 0 키인데 여기는 1 행이 남는다. Evidence: L378–L384.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **브라우저 A 세션 → 토큰 행:** principal 이름으로 쓴다. Evidence: L370–L377.
|
||||
- **브라우저 B 세션 → 토큰 행:** 같은 키로 덮어쓴다. Evidence: L370–L377.
|
||||
- **토큰 행 → 로그아웃 후 남는 것:** 로그아웃이 정리하지 않는다. Evidence: L378–L384.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 덮어쓰기를 만드는 기본키
|
||||
# Question: 같은 사용자의 두 브라우저가 서로의 토큰을 지우는 이유는 무엇인가
|
||||
direction: down
|
||||
n0: "브라우저 A 세션" {
|
||||
shape: rectangle
|
||||
}
|
||||
n1: "브라우저 B 세션" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "토큰 행" {
|
||||
shape: cylinder
|
||||
}
|
||||
n3: "로그아웃 후 남는 것" {
|
||||
shape: cylinder
|
||||
}
|
||||
n0 -> n2: "principal 이름으로 쓴다"
|
||||
n1 -> n2: "같은 키로 덮어쓴다"
|
||||
n2 -> n3: "로그아웃이 정리하지 않는다"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="브라우저 A 세션", shape=box, style="rounded,filled"];
|
||||
n1 [label="브라우저 B 세션", shape=box, style="rounded,filled"];
|
||||
n2 [label="토큰 행", shape=cylinder, style="rounded,filled"];
|
||||
n3 [label="로그아웃 후 남는 것", shape=cylinder, style="rounded,filled"];
|
||||
n0 -> n2 [label="principal 이름으로 쓴다", style=solid];
|
||||
n1 -> n2 [label="같은 키로 덮어쓴다", style=solid];
|
||||
n2 -> n3 [label="로그아웃이 정리하지 않는다", style=solid];
|
||||
}
|
||||
@@ -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="b2-primary-key-overwrite" name="덮어쓰기를 만드는 기본키">
|
||||
<mxGraphModel dx="680" dy="555" 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_s1" value="브라우저 A 세션" tooltip="세션 id 가 다르다. | Evidence: L360-L366" 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="70.0" y="60.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_s2" value="브라우저 B 세션" tooltip="세션 id 가 다르다. | Evidence: L360-L366" 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="280.0" y="60.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_token-row" value="토큰 행<br/>PRIMARY KEY (client_registration_id, principal_name)" tooltip="기본키에 세션 id 가 없어 둘이 같은 행을 쓴다. | Evidence: L370-L377" 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;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
|
||||
<mxGeometry x="100.0" y="220.0" width="300.0" height="97.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_leftover" value="로그아웃 후 남는 것<br/>평문 refresh token" tooltip="Redis 세션은 0 키인데 여기는 1 행이 남는다. | Evidence: L378-L384" 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;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
|
||||
<mxGeometry x="175.0" y="413.0" width="150.0" height="97.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_s1-t" value="principal 이름으로 쓴다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_s1" target="n_token-row">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="193.0" y="144.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_s2-t" value="같은 키로 덮어쓴다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_s2" target="n_token-row">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="307.0" y="144.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_t-l" value="로그아웃이 정리하지 않는다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_token-row" target="n_leftover">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="274.0" y="365.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-s1-t",
|
||||
"type": "arrow",
|
||||
"x": 145.0,
|
||||
"y": 124.0,
|
||||
"width": 96.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 188196126,
|
||||
"version": 1,
|
||||
"versionNonce": 660062316,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
96.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
96.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-s1",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-token-row",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-s1-t",
|
||||
"type": "text",
|
||||
"x": 125.0,
|
||||
"y": 132.0,
|
||||
"width": 136,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1249885615,
|
||||
"version": 1,
|
||||
"versionNonce": 1560347761,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "principal 이름으로 쓴다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "principal 이름으로 쓴다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-s2-t",
|
||||
"type": "arrow",
|
||||
"x": 259.0,
|
||||
"y": 124.0,
|
||||
"width": 96.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 372831280,
|
||||
"version": 1,
|
||||
"versionNonce": 594516541,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
96.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
96.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-s2",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-token-row",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-s2-t",
|
||||
"type": "text",
|
||||
"x": 262.0,
|
||||
"y": 132.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1554494580,
|
||||
"version": 1,
|
||||
"versionNonce": 120007628,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "같은 키로 덮어쓴다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "같은 키로 덮어쓴다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-t-l",
|
||||
"type": "arrow",
|
||||
"x": 250.0,
|
||||
"y": 317.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1868737350,
|
||||
"version": 1,
|
||||
"versionNonce": 1318954268,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-token-row",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-leftover",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-t-l",
|
||||
"type": "text",
|
||||
"x": 218.0,
|
||||
"y": 353.0,
|
||||
"width": 112,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1742074407,
|
||||
"version": 1,
|
||||
"versionNonce": 318513966,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "로그아웃이 정리하지 않는다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "로그아웃이 정리하지 않는다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-s1",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 60.0,
|
||||
"width": 150.0,
|
||||
"height": 64.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1674580432,
|
||||
"version": 1,
|
||||
"versionNonce": 1226920683,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-s1",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 70.0,
|
||||
"width": 130.0,
|
||||
"height": 44.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 31564725,
|
||||
"version": 1,
|
||||
"versionNonce": 636086059,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "브라우저 A 세션",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "브라우저 A 세션",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-s2",
|
||||
"type": "rectangle",
|
||||
"x": 280.0,
|
||||
"y": 60.0,
|
||||
"width": 150.0,
|
||||
"height": 64.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1579564630,
|
||||
"version": 1,
|
||||
"versionNonce": 1520235427,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-s2",
|
||||
"type": "text",
|
||||
"x": 290.0,
|
||||
"y": 70.0,
|
||||
"width": 130.0,
|
||||
"height": 44.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1199224271,
|
||||
"version": 1,
|
||||
"versionNonce": 829864800,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "브라우저 B 세션",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "브라우저 B 세션",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-token-row",
|
||||
"type": "rectangle",
|
||||
"x": 100.0,
|
||||
"y": 220.0,
|
||||
"width": 300.0,
|
||||
"height": 97.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 231634717,
|
||||
"version": 1,
|
||||
"versionNonce": 1632260043,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-token-row",
|
||||
"type": "text",
|
||||
"x": 110.0,
|
||||
"y": 230.0,
|
||||
"width": 280.0,
|
||||
"height": 77.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1219720359,
|
||||
"version": 1,
|
||||
"versionNonce": 1419335304,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "토큰 행\nPRIMARY KEY (client_registration_id, principal_name)",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "토큰 행\nPRIMARY KEY (client_registration_id, principal_name)",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-leftover",
|
||||
"type": "rectangle",
|
||||
"x": 175.0,
|
||||
"y": 413.0,
|
||||
"width": 150.0,
|
||||
"height": 97.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1735543634,
|
||||
"version": 1,
|
||||
"versionNonce": 743210434,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-leftover",
|
||||
"type": "text",
|
||||
"x": 185.0,
|
||||
"y": 423.0,
|
||||
"width": 130.0,
|
||||
"height": 77.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1765460472,
|
||||
"version": 1,
|
||||
"versionNonce": 786800441,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "로그아웃 후 남는 것\n평문 refresh token",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "로그아웃 후 남는 것\n평문 refresh token",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "b2-primary-key-overwrite",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "653f795b5dd9a627e79a865316e22d8d7ca35f86cbdf6e72dea25c631687cadf",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "B-2 · 저장소를 나눠 풀자 다른 두 문제가 남았다",
|
||||
"line": 365
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"b2-primary-key-overwrite.svg",
|
||||
"b2-primary-key-overwrite.mmd",
|
||||
"b2-primary-key-overwrite.d2",
|
||||
"b2-primary-key-overwrite.dot",
|
||||
"b2-primary-key-overwrite.drawio",
|
||||
"b2-primary-key-overwrite.excalidraw",
|
||||
"b2-primary-key-overwrite.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
%% 덮어쓰기를 만드는 기본키
|
||||
%% question: 같은 사용자의 두 브라우저가 서로의 토큰을 지우는 이유는 무엇인가
|
||||
flowchart TB
|
||||
n0["브라우저 A 세션"]
|
||||
n1["브라우저 B 세션"]
|
||||
n2[("토큰 행")]
|
||||
n3[("로그아웃 후 남는 것")]
|
||||
n0 -->|"principal 이름으로 쓴다"| n2
|
||||
n1 -->|"같은 키로 덮어쓴다"| n2
|
||||
n2 -->|"로그아웃이 정리하지 않는다"| n3
|
||||
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="555" viewBox="0 0 680 555" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">덮어쓰기를 만드는 기본키</title>
|
||||
<desc id="diagram-description">토큰을 PostgreSQL 로 옮겨 다중 인스턴스 문제는 풀렸다. 그러나 기본키가 client_registration_id 와 principal_name 의 조합이고 세션 id 가 들어 있지 않다. 같은 사용자의 두 세션이 같은 행을 쓰므로 나중 로그인이 앞의 토큰을 덮어쓴다. 그리고 로그아웃하면 Redis 세션은 0 키로 정리되지만 PostgreSQL 에는 평문 refresh token 이 한 행 그대로 남는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"b2-primary-key-overwrite","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"B-2 · 저장소를 나눠 풀자 다른 두 문제가 남았다","line":365}},"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="555" />
|
||||
<polyline class="edge kind-write style-solid emphasis-normal" points="145.0,124.0 145.0,172.0 241.0,172.0 241.0,220.0" data-evidence="370-377" />
|
||||
<rect class="edge-label-bg" x="127.0" y="130.0" width="131.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="193.0" y="145.0">principal 이름으로 쓴다</text>
|
||||
<polyline class="edge kind-write style-solid emphasis-normal" points="355.0,124.0 355.0,172.0 259.0,172.0 259.0,220.0" data-evidence="370-377" />
|
||||
<rect class="edge-label-bg" x="264.5" y="130.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="307.0" y="145.0">같은 키로 덮어쓴다</text>
|
||||
<polyline class="edge kind-blocked style-solid emphasis-normal" points="250.0,317.0 250.0,365.0 250.0,365.0 250.0,413.0" data-evidence="378-384" />
|
||||
<rect class="edge-label-bg" x="218.1" y="351.0" width="111.8" height="22" rx="3" />
|
||||
<text class="edge-label" x="274.0" y="366.0">로그아웃이 정리하지 않는다</text>
|
||||
<g id="node-s1">
|
||||
<rect class="node-shape kind-component emphasis-primary role-source" data-evidence="360-366" x="70.0" y="60.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="90.0">브라우저 A 세션</text>
|
||||
</g>
|
||||
<g id="node-s2">
|
||||
<rect class="node-shape kind-component emphasis-primary role-source" data-evidence="360-366" x="280.0" y="60.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="355.0" y="90.0">브라우저 B 세션</text>
|
||||
</g>
|
||||
<g id="node-token-row">
|
||||
<rect class="node-shape kind-datastore emphasis-warning role-target" data-evidence="370-377" x="100.0" y="233.0" width="300.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-warning role-target" cx="250.0" cy="233.0" rx="150.0" ry="13.0" /><path class="storage-bottom" d="M 100.0 304.0 A 150.0 13.0 0 0 0 400.0 304.0" />
|
||||
<text class="node-label" x="250.0" y="260.0">토큰 행</text>
|
||||
<line class="node-detail-divider" x1="114.0" y1="281.0" x2="386.0" y2="281.0" />
|
||||
<text class="node-detail" x="116.0" y="298.0">PRIMARY KEY (client_registration_id, principal_name)</text>
|
||||
</g>
|
||||
<g id="node-leftover">
|
||||
<rect class="node-shape kind-datastore emphasis-warning role-target" data-evidence="378-384" x="175.0" y="426.0" width="150.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-warning role-target" cx="250.0" cy="426.0" rx="75.0" ry="13.0" /><path class="storage-bottom" d="M 175.0 497.0 A 75.0 13.0 0 0 0 325.0 497.0" />
|
||||
<text class="node-label" x="250.0" y="453.0">로그아웃 후 남는 것</text>
|
||||
<line class="node-detail-divider" x1="189.0" y1="474.0" x2="311.0" y2="474.0" />
|
||||
<text class="node-detail" x="191.0" y="491.0">평문 refresh token</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.1 KiB |
@@ -0,0 +1,22 @@
|
||||
# 회전 경쟁에서 이긴 요청도 진다
|
||||
|
||||
## Alternative text
|
||||
|
||||
동시에 도착한 refresh 요청들이 경쟁을 일으키고, 그 결과 client session 자체가 지워지는 구성.
|
||||
|
||||
## Long description
|
||||
|
||||
revokeRefreshToken 을 켜고 refreshTokenMaxReuse 를 0 으로 둔 상태에서 같은 refresh token 으로 동시에 5건을 보냈다. 순차로 돌리면 재현되지 않으며 백그라운드로 띄우고 wait 해야 경합이 생긴다. 이긴 요청이 받은 새 토큰조차 쓸 수 없었다. Keycloak 이 경쟁을 감지하면 client session 을 지우기 때문이다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **동시 refresh 5건** (actor): 같은 refresh token 을 쓴다. Evidence: L369–L375.
|
||||
- **회전 검사** (process): 이미 쓴 토큰인지 본다. Evidence: L369–L377.
|
||||
- **client session** (component): 경쟁이 감지되면 지워진다. Evidence: L378–L384.
|
||||
- **이긴 요청의 새 토큰** (component): 세션이 지워져 쓸 수 없다. Evidence: L378–L384.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **동시 refresh 5건 → 회전 검사:** 동시 도착. Evidence: L369–L377.
|
||||
- **client session → 이긴 요청의 새 토큰:** 세션이 없으니 못 쓴다. Evidence: L378–L384.
|
||||
- **회전 검사 → client session:** 경쟁 감지 시 삭제. Evidence: L378–L384.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 회전 경쟁에서 이긴 요청도 진다
|
||||
# Question: 같은 refresh token 으로 동시에 여러 요청이 오면 어떻게 되는가
|
||||
direction: down
|
||||
n0: "동시 refresh 5건" {
|
||||
shape: person
|
||||
}
|
||||
n1: "회전 검사" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "client session" {
|
||||
shape: rectangle
|
||||
}
|
||||
n3: "이긴 요청의 새 토큰" {
|
||||
shape: rectangle
|
||||
}
|
||||
n0 -> n1: "동시 도착"
|
||||
n1 -> n2: "경쟁 감지 시 삭제"
|
||||
n2 -> n3: "세션이 없으니 못 쓴다"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="동시 refresh 5건", shape=box, style="rounded,dashed,filled"];
|
||||
n1 [label="회전 검사", shape=box, style="rounded,filled"];
|
||||
n2 [label="client session", shape=box, style="rounded,filled"];
|
||||
n3 [label="이긴 요청의 새 토큰", shape=box, style="rounded,filled"];
|
||||
n0 -> n1 [label="동시 도착", style=solid];
|
||||
n1 -> n2 [label="경쟁 감지 시 삭제", style=solid];
|
||||
n2 -> n3 [label="세션이 없으니 못 쓴다", style=solid];
|
||||
}
|
||||
@@ -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="b3-rotation-contention" name="회전 경쟁에서 이긴 요청도 진다">
|
||||
<mxGraphModel dx="680" dy="676" 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_concurrent" value="동시 refresh 5건<br/>& 와 wait 이 없으면 재현되지 않는다" tooltip="같은 refresh token 을 쓴다. | Evidence: L369-L375" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="135.0" y="60.0" width="170.0" height="84.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_rotation" value="회전 검사<br/>revokeRefreshToken=true · refreshTokenMaxReuse=0" tooltip="이미 쓴 토큰인지 본다. | Evidence: L369-L377" 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="70.0" y="240.0" width="300.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_client-session" value="client session" tooltip="경쟁이 감지되면 지워진다. | Evidence: L378-L384" 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="145.0" y="407.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_new-token" value="이긴 요청의 새 토큰" tooltip="세션이 지워져 쓸 수 없다. | Evidence: L378-L384" 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="145.0" y="567.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_c-r" value="동시 도착" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_concurrent" target="n_rotation">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="244.0" y="192.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_cs-nt" value="세션이 없으니 못 쓴다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_client-session" target="n_new-token">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="244.0" y="519.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_r-cs" value="경쟁 감지 시 삭제" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_rotation" target="n_client-session">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="244.0" y="359.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-c-r",
|
||||
"type": "arrow",
|
||||
"x": 220.0,
|
||||
"y": 144.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1593849987,
|
||||
"version": 1,
|
||||
"versionNonce": 148545859,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-concurrent",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-rotation",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-c-r",
|
||||
"type": "text",
|
||||
"x": 199.0,
|
||||
"y": 180.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1032385081,
|
||||
"version": 1,
|
||||
"versionNonce": 839461234,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "동시 도착",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "동시 도착",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-cs-nt",
|
||||
"type": "arrow",
|
||||
"x": 220.0,
|
||||
"y": 471.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 99440628,
|
||||
"version": 1,
|
||||
"versionNonce": 1024407807,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-client-session",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-new-token",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-cs-nt",
|
||||
"type": "text",
|
||||
"x": 196.0,
|
||||
"y": 507.0,
|
||||
"width": 96,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 58947181,
|
||||
"version": 1,
|
||||
"versionNonce": 1723624389,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "세션이 없으니 못 쓴다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "세션이 없으니 못 쓴다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-r-cs",
|
||||
"type": "arrow",
|
||||
"x": 220.0,
|
||||
"y": 311.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1168095643,
|
||||
"version": 1,
|
||||
"versionNonce": 453468426,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-rotation",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-client-session",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-r-cs",
|
||||
"type": "text",
|
||||
"x": 199.0,
|
||||
"y": 347.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 214571209,
|
||||
"version": 1,
|
||||
"versionNonce": 965394081,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "경쟁 감지 시 삭제",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "경쟁 감지 시 삭제",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-concurrent",
|
||||
"type": "rectangle",
|
||||
"x": 135.0,
|
||||
"y": 60.0,
|
||||
"width": 170.0,
|
||||
"height": 84.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 116525699,
|
||||
"version": 1,
|
||||
"versionNonce": 47604175,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-concurrent",
|
||||
"type": "text",
|
||||
"x": 145.0,
|
||||
"y": 70.0,
|
||||
"width": 150.0,
|
||||
"height": 64.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 328133387,
|
||||
"version": 1,
|
||||
"versionNonce": 230764220,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "동시 refresh 5건\n& 와 wait 이 없으면 재현되지 않는다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "동시 refresh 5건\n& 와 wait 이 없으면 재현되지 않는다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-rotation",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 240.0,
|
||||
"width": 300.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1792227895,
|
||||
"version": 1,
|
||||
"versionNonce": 1813018350,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-rotation",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 250.0,
|
||||
"width": 280.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1123092473,
|
||||
"version": 1,
|
||||
"versionNonce": 125702461,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "회전 검사\nrevokeRefreshToken=true · refreshTokenMaxReuse=0",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "회전 검사\nrevokeRefreshToken=true · refreshTokenMaxReuse=0",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-client-session",
|
||||
"type": "rectangle",
|
||||
"x": 145.0,
|
||||
"y": 407.0,
|
||||
"width": 150.0,
|
||||
"height": 64.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1317434994,
|
||||
"version": 1,
|
||||
"versionNonce": 1603889408,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-client-session",
|
||||
"type": "text",
|
||||
"x": 155.0,
|
||||
"y": 417.0,
|
||||
"width": 130.0,
|
||||
"height": 44.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 848098034,
|
||||
"version": 1,
|
||||
"versionNonce": 1648263378,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "client session",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "client session",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-new-token",
|
||||
"type": "rectangle",
|
||||
"x": 145.0,
|
||||
"y": 567.0,
|
||||
"width": 150.0,
|
||||
"height": 64.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 46113166,
|
||||
"version": 1,
|
||||
"versionNonce": 1651668549,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-new-token",
|
||||
"type": "text",
|
||||
"x": 155.0,
|
||||
"y": 577.0,
|
||||
"width": 130.0,
|
||||
"height": 44.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 629939483,
|
||||
"version": 1,
|
||||
"versionNonce": 1848829025,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "이긴 요청의 새 토큰",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "이긴 요청의 새 토큰",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "b3-rotation-contention",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "3fbbe8b3528bbcc2ad23e48a820e9ddc6fe6072857bfbe2fa3df0e566fe99162",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "B-3 · Refresh Token Rotation 경쟁 (Q2)",
|
||||
"line": 391
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"b3-rotation-contention.svg",
|
||||
"b3-rotation-contention.mmd",
|
||||
"b3-rotation-contention.d2",
|
||||
"b3-rotation-contention.dot",
|
||||
"b3-rotation-contention.drawio",
|
||||
"b3-rotation-contention.excalidraw",
|
||||
"b3-rotation-contention.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
%% 회전 경쟁에서 이긴 요청도 진다
|
||||
%% question: 같은 refresh token 으로 동시에 여러 요청이 오면 어떻게 되는가
|
||||
flowchart TB
|
||||
n0(["동시 refresh 5건"])
|
||||
n1["회전 검사"]
|
||||
n2["client session"]
|
||||
n3["이긴 요청의 새 토큰"]
|
||||
n0 -->|"동시 도착"| n1
|
||||
n1 -->|"경쟁 감지 시 삭제"| n2
|
||||
n2 -->|"세션이 없으니 못 쓴다"| n3
|
||||
classDef external stroke-dasharray: 6 4
|
||||
class n0 external
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="676" viewBox="0 0 680 676" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">회전 경쟁에서 이긴 요청도 진다</title>
|
||||
<desc id="diagram-description">revokeRefreshToken 을 켜고 refreshTokenMaxReuse 를 0 으로 둔 상태에서 같은 refresh token 으로 동시에 5건을 보냈다. 순차로 돌리면 재현되지 않으며 백그라운드로 띄우고 wait 해야 경합이 생긴다. 이긴 요청이 받은 새 토큰조차 쓸 수 없었다. Keycloak 이 경쟁을 감지하면 client session 을 지우기 때문이다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"b3-rotation-contention","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"B-3 · Refresh Token Rotation 경쟁 (Q2)","line":391}},"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="676" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="220.0,144.0 220.0,192.0 220.0,192.0 220.0,240.0" data-evidence="369-377" />
|
||||
<rect class="edge-label-bg" x="218.2" y="178.0" width="51.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="244.0" y="193.0">동시 도착</text>
|
||||
<polyline class="edge kind-blocked style-solid emphasis-normal" points="220.0,471.0 220.0,519.0 220.0,519.0 220.0,567.0" data-evidence="378-384" />
|
||||
<rect class="edge-label-bg" x="194.8" y="505.0" width="98.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="244.0" y="520.0">세션이 없으니 못 쓴다</text>
|
||||
<polyline class="edge kind-blocked style-solid emphasis-normal" points="220.0,311.0 220.0,359.0 220.0,359.0 220.0,407.0" data-evidence="378-384" />
|
||||
<rect class="edge-label-bg" x="201.5" y="345.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="244.0" y="360.0">경쟁 감지 시 삭제</text>
|
||||
<g id="node-concurrent">
|
||||
<g class="actor-symbol emphasis-primary" data-evidence="369-375"><circle cx="220.0" cy="80.0" r="11.0" /><line x1="220.0" y1="96.0" x2="220.0" y2="105.0" /><line x1="202.0" y1="106.0" x2="238.0" y2="106.0" /><line x1="220.0" y1="105.0" x2="205.0" y2="122.0" /><line x1="220.0" y1="105.0" x2="235.0" y2="122.0" /></g>
|
||||
<text class="node-label" x="220.0" y="137.0">동시 refresh 5건</text>
|
||||
</g>
|
||||
<g id="node-rotation">
|
||||
<rect class="node-shape kind-process emphasis-warning role-control" data-evidence="369-377" x="70.0" y="240.0" width="300.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="220.0" y="267.0">회전 검사</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="288.0" x2="356.0" y2="288.0" />
|
||||
<text class="node-detail" x="86.0" y="305.0">revokeRefreshToken=true · refreshTokenMaxReuse=0</text>
|
||||
</g>
|
||||
<g id="node-client-session">
|
||||
<rect class="node-shape kind-component emphasis-warning role-target" data-evidence="378-384" x="145.0" y="407.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="220.0" y="437.0">client session</text>
|
||||
</g>
|
||||
<g id="node-new-token">
|
||||
<rect class="node-shape kind-component emphasis-warning role-target" data-evidence="378-384" x="145.0" y="567.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="220.0" y="597.0">이긴 요청의 새 토큰</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,22 @@
|
||||
# 지우지 않으면 통과한다
|
||||
|
||||
## Alternative text
|
||||
|
||||
밖에서 들어온 위조 헤더가 프록시를 그대로 통과해 앱에 닿는 구성. 프록시가 그 이름을 설정할 때만 덮어쓴다.
|
||||
|
||||
## Long description
|
||||
|
||||
nginx 는 자기가 proxy_set_header 로 설정한 헤더만 덮어쓴다. 설정하지 않은 이름은 클라이언트가 보낸 값이 그대로 지나간다. 그래서 X-Auth-Request-Roles 같은 헤더를 앱이 믿으려면 프록시가 그 이름을 먼저 빈 값으로 지워야 한다. 그리고 IdP 에서 클레임을 바꿔도 반영되지 않는다. 12회 요청 6초 동안 옛 값이 갔고 세션을 지워 재인증한 뒤에야 새 값이 왔다. 세션은 로그인 시점의 스냅샷이다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **밖에서 보낸 위조 헤더** (actor): 앱이 믿는 이름을 그대로 쓴다. Evidence: L393–L400.
|
||||
- **nginx** (gateway): 설정하지 않은 이름은 덮어쓰지 않는다. Evidence: L393–L400.
|
||||
- **oauth2-proxy** (gateway): 인증 결과를 헤더로 넣는다. Evidence: L393–L400.
|
||||
- **앱** (service): 헤더를 믿고 인가한다. Evidence: L401–L408.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **밖에서 보낸 위조 헤더 → nginx:** 위조 헤더. Evidence: L393–L400.
|
||||
- **nginx → oauth2-proxy:** 지우지 않으면 그대로 지난다. Evidence: L393–L400.
|
||||
- **oauth2-proxy → 앱:** 인가 헤더. Evidence: L393–L408.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 지우지 않으면 통과한다
|
||||
# Question: Edge 가 넣어주는 인가 헤더를 앱이 믿어도 되는가
|
||||
direction: down
|
||||
n0: "밖에서 보낸 위조 헤더" {
|
||||
shape: person
|
||||
}
|
||||
n1: "nginx" {
|
||||
shape: diamond
|
||||
}
|
||||
n2: "oauth2-proxy" {
|
||||
shape: diamond
|
||||
}
|
||||
n3: "앱" {
|
||||
shape: rectangle
|
||||
}
|
||||
n0 -> n1: "위조 헤더"
|
||||
n1 -> n2: "지우지 않으면 그대로 지난다"
|
||||
n2 -> n3: "인가 헤더"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="밖에서 보낸 위조 헤더", shape=box, style="rounded,dashed,filled"];
|
||||
n1 [label="nginx", shape=diamond, style="rounded,filled"];
|
||||
n2 [label="oauth2-proxy", shape=diamond, style="rounded,filled"];
|
||||
n3 [label="앱", shape=box, style="rounded,filled"];
|
||||
n0 -> n1 [label="위조 헤더", style=solid];
|
||||
n1 -> n2 [label="지우지 않으면 그대로 지난다", style=solid];
|
||||
n2 -> n3 [label="인가 헤더", style=solid];
|
||||
}
|
||||
@@ -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="b4-header-trust-boundary" name="지우지 않으면 통과한다">
|
||||
<mxGraphModel dx="680" dy="690" 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_attacker" value="밖에서 보낸 위조 헤더<br/>X-Auth-Request-Roles" tooltip="앱이 믿는 이름을 그대로 쓴다. | Evidence: L393-L400" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
|
||||
<mxGeometry x="124.5" y="60.0" width="170.0" height="84.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_nginx" value="nginx<br/>proxy_set_header ... "" 로 먼저 지워야 한다" tooltip="설정하지 않은 이름은 덮어쓰지 않는다. | Evidence: L393-L400" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;rhombus;perimeter=rhombusPerimeter;fillColor=#fff7e8;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="240.0" width="279.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_proxy" value="oauth2-proxy<br/>--set-xauthrequest" tooltip="인증 결과를 헤더로 넣는다. | Evidence: L393-L400" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;rhombus;perimeter=rhombusPerimeter;fillColor=#fff7e8;" vertex="1" parent="1">
|
||||
<mxGeometry x="129.5" y="407.0" width="160.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_app" value="앱<br/>세션은 로그인 시점의 스냅샷" tooltip="헤더를 믿고 인가한다. | Evidence: L401-L408" 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="134.5" y="574.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_a-n" value="위조 헤더" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_attacker" target="n_nginx">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="233.5" y="192.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_n-p" value="지우지 않으면 그대로 지난다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_nginx" target="n_proxy">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="233.5" y="359.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_p-a" value="인가 헤더" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_proxy" target="n_app">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="233.5" y="526.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-a-n",
|
||||
"type": "arrow",
|
||||
"x": 209.5,
|
||||
"y": 144.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1392959887,
|
||||
"version": 1,
|
||||
"versionNonce": 960034335,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-attacker",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-nginx",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-a-n",
|
||||
"type": "text",
|
||||
"x": 188.5,
|
||||
"y": 180.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 683347805,
|
||||
"version": 1,
|
||||
"versionNonce": 1056915663,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "위조 헤더",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "위조 헤더",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-n-p",
|
||||
"type": "arrow",
|
||||
"x": 209.5,
|
||||
"y": 311.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1877236961,
|
||||
"version": 1,
|
||||
"versionNonce": 266888902,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-nginx",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-proxy",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-n-p",
|
||||
"type": "text",
|
||||
"x": 173.5,
|
||||
"y": 347.0,
|
||||
"width": 120,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1431696520,
|
||||
"version": 1,
|
||||
"versionNonce": 51503328,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "지우지 않으면 그대로 지난다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "지우지 않으면 그대로 지난다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-p-a",
|
||||
"type": "arrow",
|
||||
"x": 209.5,
|
||||
"y": 478.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 579930019,
|
||||
"version": 1,
|
||||
"versionNonce": 141204879,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-proxy",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-app",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-p-a",
|
||||
"type": "text",
|
||||
"x": 188.5,
|
||||
"y": 514.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1931736220,
|
||||
"version": 1,
|
||||
"versionNonce": 15055194,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "인가 헤더",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "인가 헤더",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-attacker",
|
||||
"type": "rectangle",
|
||||
"x": 124.5,
|
||||
"y": 60.0,
|
||||
"width": 170.0,
|
||||
"height": 84.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 145023296,
|
||||
"version": 1,
|
||||
"versionNonce": 365379687,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-attacker",
|
||||
"type": "text",
|
||||
"x": 134.5,
|
||||
"y": 70.0,
|
||||
"width": 150.0,
|
||||
"height": 64.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 539786699,
|
||||
"version": 1,
|
||||
"versionNonce": 180744341,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "밖에서 보낸 위조 헤더\nX-Auth-Request-Roles",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "밖에서 보낸 위조 헤더\nX-Auth-Request-Roles",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-nginx",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 240.0,
|
||||
"width": 279.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#fff4e6",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 721013300,
|
||||
"version": 1,
|
||||
"versionNonce": 1520646945,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-nginx",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 250.0,
|
||||
"width": 259.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 117243520,
|
||||
"version": 1,
|
||||
"versionNonce": 936748041,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "nginx\nproxy_set_header ... \"\" 로 먼저 지워야 한다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "nginx\nproxy_set_header ... \"\" 로 먼저 지워야 한다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-proxy",
|
||||
"type": "rectangle",
|
||||
"x": 129.5,
|
||||
"y": 407.0,
|
||||
"width": 160.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#fff4e6",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 254112210,
|
||||
"version": 1,
|
||||
"versionNonce": 42167050,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-proxy",
|
||||
"type": "text",
|
||||
"x": 139.5,
|
||||
"y": 417.0,
|
||||
"width": 140.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 418528669,
|
||||
"version": 1,
|
||||
"versionNonce": 1299542283,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "oauth2-proxy\n--set-xauthrequest",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "oauth2-proxy\n--set-xauthrequest",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-app",
|
||||
"type": "rectangle",
|
||||
"x": 134.5,
|
||||
"y": 574.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 594277328,
|
||||
"version": 1,
|
||||
"versionNonce": 293358427,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-app",
|
||||
"type": "text",
|
||||
"x": 144.5,
|
||||
"y": 584.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1055843055,
|
||||
"version": 1,
|
||||
"versionNonce": 1025494149,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "앱\n세션은 로그인 시점의 스냅샷",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "앱\n세션은 로그인 시점의 스냅샷",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "b4-header-trust-boundary",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "9ae5cf39dfc20e31d81b892ae7483d2b9ccf94fa2c91a03837200b463bfc1103",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "B-4 · Edge 인가의 범위 (Q4)",
|
||||
"line": 401
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"b4-header-trust-boundary.svg",
|
||||
"b4-header-trust-boundary.mmd",
|
||||
"b4-header-trust-boundary.d2",
|
||||
"b4-header-trust-boundary.dot",
|
||||
"b4-header-trust-boundary.drawio",
|
||||
"b4-header-trust-boundary.excalidraw",
|
||||
"b4-header-trust-boundary.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
%% 지우지 않으면 통과한다
|
||||
%% question: Edge 가 넣어주는 인가 헤더를 앱이 믿어도 되는가
|
||||
flowchart TB
|
||||
n0(["밖에서 보낸 위조 헤더"])
|
||||
n1{"nginx"}
|
||||
n2{"oauth2-proxy"}
|
||||
n3["앱"]
|
||||
n0 -->|"위조 헤더"| n1
|
||||
n1 -->|"지우지 않으면 그대로 지난다"| n2
|
||||
n2 -->|"인가 헤더"| n3
|
||||
classDef external stroke-dasharray: 6 4
|
||||
class n0 external
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="690" viewBox="0 0 680 690" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">지우지 않으면 통과한다</title>
|
||||
<desc id="diagram-description">nginx 는 자기가 proxy_set_header 로 설정한 헤더만 덮어쓴다. 설정하지 않은 이름은 클라이언트가 보낸 값이 그대로 지나간다. 그래서 X-Auth-Request-Roles 같은 헤더를 앱이 믿으려면 프록시가 그 이름을 먼저 빈 값으로 지워야 한다. 그리고 IdP 에서 클레임을 바꿔도 반영되지 않는다. 12회 요청 6초 동안 옛 값이 갔고 세션을 지워 재인증한 뒤에야 새 값이 왔다. 세션은 로그인 시점의 스냅샷이다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"b4-header-trust-boundary","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"B-4 · Edge 인가의 범위 (Q4)","line":401}},"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="690" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="209.5,144.0 209.5,192.0 209.5,192.0 209.5,240.0" data-evidence="393-400" />
|
||||
<rect class="edge-label-bg" x="207.8" y="178.0" width="51.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="233.5" y="193.0">위조 헤더</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="209.5,311.0 209.5,359.0 209.5,359.0 209.5,407.0" data-evidence="393-400" />
|
||||
<rect class="edge-label-bg" x="174.2" y="345.0" width="118.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="233.5" y="360.0">지우지 않으면 그대로 지난다</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="209.5,478.0 209.5,526.0 209.5,526.0 209.5,574.0" data-evidence="393-408" />
|
||||
<rect class="edge-label-bg" x="207.8" y="512.0" width="51.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="233.5" y="527.0">인가 헤더</text>
|
||||
<g id="node-attacker">
|
||||
<g class="actor-symbol emphasis-warning" data-evidence="393-400"><circle cx="209.5" cy="80.0" r="11.0" /><line x1="209.5" y1="96.0" x2="209.5" y2="105.0" /><line x1="191.5" y1="106.0" x2="227.5" y2="106.0" /><line x1="209.5" y1="105.0" x2="194.5" y2="122.0" /><line x1="209.5" y1="105.0" x2="224.5" y2="122.0" /></g>
|
||||
<text class="node-label" x="209.5" y="137.0">밖에서 보낸 위조 헤더</text>
|
||||
</g>
|
||||
<g id="node-nginx">
|
||||
<polygon class="node-shape kind-gateway emphasis-warning role-control" data-evidence="393-400" points="209.5,240.0 349.0,275.5 209.5,311.0 70.0,275.5" />
|
||||
<text class="node-label" x="209.5" y="267.0">nginx</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="288.0" x2="335.0" y2="288.0" />
|
||||
<text class="node-detail" x="86.0" y="305.0">proxy_set_header ... "" 로 먼저 지워야 한다</text>
|
||||
</g>
|
||||
<g id="node-proxy">
|
||||
<polygon class="node-shape kind-gateway emphasis-normal role-control" data-evidence="393-400" points="209.5,407.0 289.5,442.5 209.5,478.0 129.5,442.5" />
|
||||
<text class="node-label" x="209.5" y="434.0">oauth2-proxy</text>
|
||||
<line class="node-detail-divider" x1="143.5" y1="455.0" x2="275.5" y2="455.0" />
|
||||
<text class="node-detail" x="145.5" y="472.0">--set-xauthrequest</text>
|
||||
</g>
|
||||
<g id="node-app">
|
||||
<rect class="node-shape kind-service emphasis-primary role-target" data-evidence="401-408" x="134.5" y="574.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="209.5" y="601.0">앱</text>
|
||||
<line class="node-detail-divider" x1="148.5" y1="622.0" x2="270.5" y2="622.0" />
|
||||
<text class="node-detail" x="150.5" y="639.0">세션은 로그인 시점의 스냅샷</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,22 @@
|
||||
# 볼륨 없는 영속화와 유예 없는 회전
|
||||
|
||||
## Alternative text
|
||||
|
||||
Redis 의 데이터 디렉터리가 컨테이너 파일시스템일 때 영속화 설정이 무의미해지는 구성과, 새 kid 를 만난 검증기가 JWKS 를 재조회하는 구성.
|
||||
|
||||
## Long description
|
||||
|
||||
redis-cli config set appendonly yes 를 켜도 아무것도 달라지지 않았다. /data 가 컨테이너 파일시스템이라 컨테이너와 함께 죽기 때문이다. appendonlydir 이 만들어졌다가 그대로 버려진다. 볼륨 없는 영속화 설정은 장식이다. realm 키를 회전했을 때는 JWKS 캐시의 유예 구간을 기대했는데 없었다. NimbusJwtDecoder 는 모르는 kid 를 만나면 JWKS 를 다시 가져온다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **appendonly yes** (process): 설정은 적용된다. Evidence: L406–L412.
|
||||
- **/data** (datastore): 컨테이너 파일시스템이다. Evidence: L406–L412.
|
||||
- **컨테이너** (component): 죽으면 /data 도 같이 사라진다. Evidence: L406–L412.
|
||||
- **PersistentVolume** (datastore): 여기 있어야 설정이 뜻을 갖는다. Evidence: L406–L412.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **appendonly yes → /data:** AOF 파일을 쓴다. Evidence: L406–L412.
|
||||
- **/data → 컨테이너:** 컨테이너와 함께 죽는다. Evidence: L406–L412.
|
||||
- **/data → PersistentVolume:** 볼륨을 붙여야 남는다. Evidence: L406–L412.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 볼륨 없는 영속화와 유예 없는 회전
|
||||
# Question: 설정만으로 영속화와 무중단 키 교체가 되는가
|
||||
direction: down
|
||||
n0: "appendonly yes" {
|
||||
shape: rectangle
|
||||
}
|
||||
n1: "/data" {
|
||||
shape: cylinder
|
||||
}
|
||||
n2: "컨테이너" {
|
||||
shape: rectangle
|
||||
}
|
||||
n3: "PersistentVolume" {
|
||||
shape: cylinder
|
||||
}
|
||||
n0 -> n1: "AOF 파일을 쓴다"
|
||||
n1 -> n2: "컨테이너와 함께 죽는다"
|
||||
n1 -> n3: "볼륨을 붙여야 남는다"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="appendonly yes", shape=box, style="rounded,filled"];
|
||||
n1 [label="/data", shape=cylinder, style="rounded,filled"];
|
||||
n2 [label="컨테이너", shape=box, style="rounded,filled"];
|
||||
n3 [label="PersistentVolume", shape=cylinder, style="rounded,filled"];
|
||||
n0 -> n1 [label="AOF 파일을 쓴다", style=solid];
|
||||
n1 -> n2 [label="컨테이너와 함께 죽는다", style=solid];
|
||||
n1 -> n3 [label="볼륨을 붙여야 남는다", style=solid];
|
||||
}
|
||||
@@ -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="b5-b6-storage-and-keys" name="볼륨 없는 영속화와 유예 없는 회전">
|
||||
<mxGraphModel dx="680" dy="555" 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_config" value="appendonly yes<br/>redis-cli config set" tooltip="설정은 적용된다. | Evidence: L406-L412" 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="163.0" y="60.0" width="174.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_datadir" value="/data<br/>appendonlydir 이 만들어졌다 버려진다" tooltip="컨테이너 파일시스템이다. | Evidence: L406-L412" 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;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
|
||||
<mxGeometry x="142.0" y="227.0" width="216.0" height="97.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_container" value="컨테이너<br/>볼륨이 없으면 여기까지다" tooltip="죽으면 /data 도 같이 사라진다. | Evidence: L406-L412" 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="70.0" y="429.5" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_volume" value="PersistentVolume" tooltip="여기 있어야 설정이 뜻을 갖는다. | Evidence: L406-L412" 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;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="280.0" y="420.0" width="150.0" height="90.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_c-d" value="AOF 파일을 쓴다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_config" target="n_datadir">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="274.0" y="179.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_d-c" value="컨테이너와 함께 죽는다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_datadir" target="n_container">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="193.0" y="348.8" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_d-v" value="볼륨을 붙여야 남는다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_datadir" target="n_volume">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="307.0" y="344.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-c-d",
|
||||
"type": "arrow",
|
||||
"x": 250.0,
|
||||
"y": 131.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1041793716,
|
||||
"version": 1,
|
||||
"versionNonce": 152807890,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-config",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-datadir",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-c-d",
|
||||
"type": "text",
|
||||
"x": 229.0,
|
||||
"y": 167.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1623889644,
|
||||
"version": 1,
|
||||
"versionNonce": 941350353,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "AOF 파일을 쓴다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "AOF 파일을 쓴다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-d-c",
|
||||
"type": "arrow",
|
||||
"x": 145.0,
|
||||
"y": 324.0,
|
||||
"width": 96.0,
|
||||
"height": 105.5,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1710772655,
|
||||
"version": 1,
|
||||
"versionNonce": 825890023,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
96.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
96.0,
|
||||
52.75
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
52.75
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
105.5
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-datadir",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-container",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-d-c",
|
||||
"type": "text",
|
||||
"x": 145.0,
|
||||
"y": 336.75,
|
||||
"width": 96,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1795748316,
|
||||
"version": 1,
|
||||
"versionNonce": 1772431002,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "컨테이너와 함께 죽는다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "컨테이너와 함께 죽는다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-d-v",
|
||||
"type": "arrow",
|
||||
"x": 259.0,
|
||||
"y": 324.0,
|
||||
"width": 96.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1778945409,
|
||||
"version": 1,
|
||||
"versionNonce": 128234071,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
96.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
96.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-datadir",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-volume",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-d-v",
|
||||
"type": "text",
|
||||
"x": 262.0,
|
||||
"y": 332.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1825891785,
|
||||
"version": 1,
|
||||
"versionNonce": 120899807,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "볼륨을 붙여야 남는다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "볼륨을 붙여야 남는다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-config",
|
||||
"type": "rectangle",
|
||||
"x": 163.0,
|
||||
"y": 60.0,
|
||||
"width": 174.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 736776605,
|
||||
"version": 1,
|
||||
"versionNonce": 1424542118,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-config",
|
||||
"type": "text",
|
||||
"x": 173.0,
|
||||
"y": 70.0,
|
||||
"width": 154.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 802906507,
|
||||
"version": 1,
|
||||
"versionNonce": 72227637,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "appendonly yes\nredis-cli config set",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "appendonly yes\nredis-cli config set",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-datadir",
|
||||
"type": "rectangle",
|
||||
"x": 142.0,
|
||||
"y": 227.0,
|
||||
"width": 216.0,
|
||||
"height": 97.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 253670220,
|
||||
"version": 1,
|
||||
"versionNonce": 694258328,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-datadir",
|
||||
"type": "text",
|
||||
"x": 152.0,
|
||||
"y": 237.0,
|
||||
"width": 196.0,
|
||||
"height": 77.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1101224859,
|
||||
"version": 1,
|
||||
"versionNonce": 1801986568,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "/data\nappendonlydir 이 만들어졌다 버려진다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "/data\nappendonlydir 이 만들어졌다 버려진다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-container",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 429.5,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 899340409,
|
||||
"version": 1,
|
||||
"versionNonce": 719103033,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-container",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 439.5,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 625860718,
|
||||
"version": 1,
|
||||
"versionNonce": 292100911,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "컨테이너\n볼륨이 없으면 여기까지다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "컨테이너\n볼륨이 없으면 여기까지다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-volume",
|
||||
"type": "rectangle",
|
||||
"x": 280.0,
|
||||
"y": 420.0,
|
||||
"width": 150.0,
|
||||
"height": 90.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 690472711,
|
||||
"version": 1,
|
||||
"versionNonce": 62320937,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-volume",
|
||||
"type": "text",
|
||||
"x": 290.0,
|
||||
"y": 430.0,
|
||||
"width": 130.0,
|
||||
"height": 70.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 964975471,
|
||||
"version": 1,
|
||||
"versionNonce": 313361807,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "PersistentVolume",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "PersistentVolume",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "b5-b6-storage-and-keys",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "51b618881bf3ef23a734b2beb319e756c12a9a7ff1ae578b7b5ef78a1461b0e9",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "B-5 · B-6 — 저장소 상실과 키 회전",
|
||||
"line": 415
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"b5-b6-storage-and-keys.svg",
|
||||
"b5-b6-storage-and-keys.mmd",
|
||||
"b5-b6-storage-and-keys.d2",
|
||||
"b5-b6-storage-and-keys.dot",
|
||||
"b5-b6-storage-and-keys.drawio",
|
||||
"b5-b6-storage-and-keys.excalidraw",
|
||||
"b5-b6-storage-and-keys.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
%% 볼륨 없는 영속화와 유예 없는 회전
|
||||
%% question: 설정만으로 영속화와 무중단 키 교체가 되는가
|
||||
flowchart TB
|
||||
n0["appendonly yes"]
|
||||
n1[("/data")]
|
||||
n2["컨테이너"]
|
||||
n3[("PersistentVolume")]
|
||||
n0 -->|"AOF 파일을 쓴다"| n1
|
||||
n1 -->|"컨테이너와 함께 죽는다"| n2
|
||||
n1 -->|"볼륨을 붙여야 남는다"| n3
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="555" viewBox="0 0 680 555" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">볼륨 없는 영속화와 유예 없는 회전</title>
|
||||
<desc id="diagram-description">redis-cli config set appendonly yes 를 켜도 아무것도 달라지지 않았다. /data 가 컨테이너 파일시스템이라 컨테이너와 함께 죽기 때문이다. appendonlydir 이 만들어졌다가 그대로 버려진다. 볼륨 없는 영속화 설정은 장식이다. realm 키를 회전했을 때는 JWKS 캐시의 유예 구간을 기대했는데 없었다. NimbusJwtDecoder 는 모르는 kid 를 만나면 JWKS 를 다시 가져온다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"b5-b6-storage-and-keys","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"B-5 · B-6 — 저장소 상실과 키 회전","line":415}},"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="555" />
|
||||
<polyline class="edge kind-write style-solid emphasis-normal" points="250.0,131.0 250.0,179.0 250.0,179.0 250.0,227.0" data-evidence="406-412" />
|
||||
<rect class="edge-label-bg" x="231.5" y="165.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="274.0" y="180.0">AOF 파일을 쓴다</text>
|
||||
<polyline class="edge kind-blocked style-solid emphasis-normal" points="241.0,324.0 241.0,376.8 145.0,376.8 145.0,429.5" data-evidence="406-412" />
|
||||
<rect class="edge-label-bg" x="143.8" y="334.8" width="98.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="193.0" y="349.8">컨테이너와 함께 죽는다</text>
|
||||
<polyline class="edge kind-write style-solid emphasis-normal" points="259.0,324.0 259.0,372.0 355.0,372.0 355.0,420.0" data-evidence="406-412" />
|
||||
<rect class="edge-label-bg" x="261.1" y="330.0" width="91.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="307.0" y="345.0">볼륨을 붙여야 남는다</text>
|
||||
<g id="node-config">
|
||||
<rect class="node-shape kind-process emphasis-warning role-source" data-evidence="406-412" x="163.0" y="60.0" width="174.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="250.0" y="87.0">appendonly yes</text>
|
||||
<line class="node-detail-divider" x1="177.0" y1="108.0" x2="323.0" y2="108.0" />
|
||||
<text class="node-detail" x="179.0" y="125.0">redis-cli config set</text>
|
||||
</g>
|
||||
<g id="node-datadir">
|
||||
<rect class="node-shape kind-datastore emphasis-warning role-control" data-evidence="406-412" x="142.0" y="240.0" width="216.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-warning role-control" cx="250.0" cy="240.0" rx="108.0" ry="13.0" /><path class="storage-bottom" d="M 142.0 311.0 A 108.0 13.0 0 0 0 358.0 311.0" />
|
||||
<text class="node-label" x="250.0" y="267.0">/data</text>
|
||||
<line class="node-detail-divider" x1="156.0" y1="288.0" x2="344.0" y2="288.0" />
|
||||
<text class="node-detail" x="158.0" y="305.0">appendonlydir 이 만들어졌다 버려진다</text>
|
||||
</g>
|
||||
<g id="node-container">
|
||||
<rect class="node-shape kind-component emphasis-warning role-target" data-evidence="406-412" x="70.0" y="429.5" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="456.5">컨테이너</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="477.5" x2="206.0" y2="477.5" />
|
||||
<text class="node-detail" x="86.0" y="494.5">볼륨이 없으면 여기까지다</text>
|
||||
</g>
|
||||
<g id="node-volume">
|
||||
<rect class="node-shape kind-datastore emphasis-primary role-target" data-evidence="406-412" x="280.0" y="433.0" width="150.0" height="64.0" /><ellipse class="node-shape kind-datastore emphasis-primary role-target" cx="355.0" cy="433.0" rx="75.0" ry="13.0" /><path class="storage-bottom" d="M 280.0 497.0 A 75.0 13.0 0 0 0 430.0 497.0" />
|
||||
<text class="node-label" x="355.0" y="463.0">PersistentVolume</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,22 @@
|
||||
# 쿠키에 담으면 공유할 것이 없다
|
||||
|
||||
## Alternative text
|
||||
|
||||
세션이 쿠키에 담기고 replica 는 같은 Secret 만 읽는 구성. Redis 저장소를 켜면 쿠키에 티켓만 남고 서버에 세션이 생긴다.
|
||||
|
||||
## Long description
|
||||
|
||||
oauth2-proxy 는 세션 전체를 쿠키에 담고 replica 는 같은 k8s Secret 을 읽을 뿐이다. 공유할 서버 상태가 없으니 콜백이 다른 replica 로 가도 된다. 대신 --cookie-secret 이 단수라 옛 secret 도 당분간 받아준다가 불가능하고 교체하는 순간 모든 쿠키가 한꺼번에 무효가 된다. Redis 저장소를 켜면 쿠키에는 티켓만 남는데, 티켓 안에 세션 id 가 있으므로 secret 을 바꾸면 어느 Redis 키를 지울지도 모르게 된다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **k8s Secret** (datastore): 모든 replica 가 같은 값을 읽는다. Evidence: L419–L428.
|
||||
- **쿠키의 티켓** (component): 세션 id 와 복호화 키가 함께 암호화되어 있다. Evidence: L429–L438.
|
||||
- **Redis 의 세션** (datastore): 티켓을 못 풀면 어느 키인지도 모른다. Evidence: L429–L440.
|
||||
- **TTL** (component): 요청으로 갱신되지 않아 생성 시각의 함수다. Evidence: L441–L452.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **Redis 의 세션 → TTL:** 생성 시각이 여기 남는다. Evidence: L441–L452.
|
||||
- **k8s Secret → 쿠키의 티켓:** 티켓을 암호화한다. Evidence: L419–L432.
|
||||
- **쿠키의 티켓 → Redis 의 세션:** 키 이름을 만든다. Evidence: L429–L438.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 쿠키에 담으면 공유할 것이 없다
|
||||
# Question: oauth2-proxy 의 secret 회전은 무엇을 남기는가
|
||||
direction: down
|
||||
n0: "k8s Secret" {
|
||||
shape: cylinder
|
||||
}
|
||||
n1: "쿠키의 티켓" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "Redis 의 세션" {
|
||||
shape: cylinder
|
||||
}
|
||||
n3: "TTL" {
|
||||
shape: rectangle
|
||||
}
|
||||
n0 -> n1: "티켓을 암호화한다"
|
||||
n1 -> n2: "키 이름을 만든다"
|
||||
n2 -> n3: "생성 시각이 여기 남는다"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="k8s Secret", shape=cylinder, style="rounded,filled"];
|
||||
n1 [label="쿠키의 티켓", shape=box, style="rounded,filled"];
|
||||
n2 [label="Redis 의 세션", shape=cylinder, style="rounded,filled"];
|
||||
n3 [label="TTL", shape=box, style="rounded,filled"];
|
||||
n0 -> n1 [label="티켓을 암호화한다", style=solid];
|
||||
n1 -> n2 [label="키 이름을 만든다", style=solid];
|
||||
n2 -> n3 [label="생성 시각이 여기 남는다", style=solid];
|
||||
}
|
||||
@@ -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="b7-cookie-session-tradeoff" name="쿠키에 담으면 공유할 것이 없다">
|
||||
<mxGraphModel dx="680" dy="729" 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_secret" value="k8s Secret<br/>--cookie-secret 은 단수" tooltip="모든 replica 가 같은 값을 읽는다. | Evidence: L419-L428" 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;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="60.0" width="174.0" height="97.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_ticket" value="쿠키의 티켓<br/>secret 을 바꾸면 못 푼다" tooltip="세션 id 와 복호화 키가 함께 암호화되어 있다. | Evidence: L429-L438" 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="80.5" y="253.0" width="153.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_redis-session" value="Redis 의 세션<br/>고아로 남는다" tooltip="티켓을 못 풀면 어느 키인지도 모른다. | Evidence: L429-L440" 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;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
|
||||
<mxGeometry x="82.0" y="420.0" width="150.0" height="97.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_ttl" value="TTL<br/>역산으로 고아를 고른다" tooltip="요청으로 갱신되지 않아 생성 시각의 함수다. | Evidence: L441-L452" 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="82.0" y="613.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_r-ttl" value="생성 시각이 여기 남는다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_redis-session" target="n_ttl">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="181.0" y="565.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_s-t" value="티켓을 암호화한다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_secret" target="n_ticket">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="181.0" y="205.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_t-r" value="키 이름을 만든다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_ticket" target="n_redis-session">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="181.0" y="372.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-r-ttl",
|
||||
"type": "arrow",
|
||||
"x": 157.0,
|
||||
"y": 517.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1135934964,
|
||||
"version": 1,
|
||||
"versionNonce": 1808446587,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-redis-session",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-ttl",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-r-ttl",
|
||||
"type": "text",
|
||||
"x": 129.0,
|
||||
"y": 553.0,
|
||||
"width": 104,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1479126096,
|
||||
"version": 1,
|
||||
"versionNonce": 701602824,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "생성 시각이 여기 남는다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "생성 시각이 여기 남는다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-s-t",
|
||||
"type": "arrow",
|
||||
"x": 157.0,
|
||||
"y": 157.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1862434854,
|
||||
"version": 1,
|
||||
"versionNonce": 618488197,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-secret",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-ticket",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-s-t",
|
||||
"type": "text",
|
||||
"x": 136.0,
|
||||
"y": 193.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1484782167,
|
||||
"version": 1,
|
||||
"versionNonce": 493222229,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "티켓을 암호화한다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "티켓을 암호화한다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-t-r",
|
||||
"type": "arrow",
|
||||
"x": 157.0,
|
||||
"y": 324.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 927026216,
|
||||
"version": 1,
|
||||
"versionNonce": 260065228,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-ticket",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-redis-session",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-t-r",
|
||||
"type": "text",
|
||||
"x": 136.0,
|
||||
"y": 360.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 576053733,
|
||||
"version": 1,
|
||||
"versionNonce": 1463594537,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "키 이름을 만든다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "키 이름을 만든다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-secret",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 60.0,
|
||||
"width": 174.0,
|
||||
"height": 97.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1236896219,
|
||||
"version": 1,
|
||||
"versionNonce": 1269619969,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-secret",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 70.0,
|
||||
"width": 154.0,
|
||||
"height": 77.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 809034914,
|
||||
"version": 1,
|
||||
"versionNonce": 1728073668,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "k8s Secret\n--cookie-secret 은 단수",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "k8s Secret\n--cookie-secret 은 단수",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-ticket",
|
||||
"type": "rectangle",
|
||||
"x": 80.5,
|
||||
"y": 253.0,
|
||||
"width": 153.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 36669833,
|
||||
"version": 1,
|
||||
"versionNonce": 185547631,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-ticket",
|
||||
"type": "text",
|
||||
"x": 90.5,
|
||||
"y": 263.0,
|
||||
"width": 133.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1697363880,
|
||||
"version": 1,
|
||||
"versionNonce": 82751645,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "쿠키의 티켓\nsecret 을 바꾸면 못 푼다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "쿠키의 티켓\nsecret 을 바꾸면 못 푼다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-redis-session",
|
||||
"type": "rectangle",
|
||||
"x": 82.0,
|
||||
"y": 420.0,
|
||||
"width": 150.0,
|
||||
"height": 97.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 23377508,
|
||||
"version": 1,
|
||||
"versionNonce": 61209674,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-redis-session",
|
||||
"type": "text",
|
||||
"x": 92.0,
|
||||
"y": 430.0,
|
||||
"width": 130.0,
|
||||
"height": 77.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 162518557,
|
||||
"version": 1,
|
||||
"versionNonce": 332434866,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "Redis 의 세션\n고아로 남는다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "Redis 의 세션\n고아로 남는다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-ttl",
|
||||
"type": "rectangle",
|
||||
"x": 82.0,
|
||||
"y": 613.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1465514607,
|
||||
"version": 1,
|
||||
"versionNonce": 1144908308,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-ttl",
|
||||
"type": "text",
|
||||
"x": 92.0,
|
||||
"y": 623.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 56475114,
|
||||
"version": 1,
|
||||
"versionNonce": 531370675,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "TTL\n역산으로 고아를 고른다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "TTL\n역산으로 고아를 고른다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "b7-cookie-session-tradeoff",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "ae4774e0d8dfda387902c00a5d3c933672d4adf799cd6774760a74b8a1ad376a",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "B-7 · B-7a — 쿠키에 담는 세션, 그리고 그 대가",
|
||||
"line": 424
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"b7-cookie-session-tradeoff.svg",
|
||||
"b7-cookie-session-tradeoff.mmd",
|
||||
"b7-cookie-session-tradeoff.d2",
|
||||
"b7-cookie-session-tradeoff.dot",
|
||||
"b7-cookie-session-tradeoff.drawio",
|
||||
"b7-cookie-session-tradeoff.excalidraw",
|
||||
"b7-cookie-session-tradeoff.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
%% 쿠키에 담으면 공유할 것이 없다
|
||||
%% question: oauth2-proxy 의 secret 회전은 무엇을 남기는가
|
||||
flowchart TB
|
||||
n0[("k8s Secret")]
|
||||
n1["쿠키의 티켓"]
|
||||
n2[("Redis 의 세션")]
|
||||
n3["TTL"]
|
||||
n0 -->|"티켓을 암호화한다"| n1
|
||||
n1 -->|"키 이름을 만든다"| n2
|
||||
n2 -->|"생성 시각이 여기 남는다"| n3
|
||||
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="729" viewBox="0 0 680 729" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">쿠키에 담으면 공유할 것이 없다</title>
|
||||
<desc id="diagram-description">oauth2-proxy 는 세션 전체를 쿠키에 담고 replica 는 같은 k8s Secret 을 읽을 뿐이다. 공유할 서버 상태가 없으니 콜백이 다른 replica 로 가도 된다. 대신 --cookie-secret 이 단수라 옛 secret 도 당분간 받아준다가 불가능하고 교체하는 순간 모든 쿠키가 한꺼번에 무효가 된다. Redis 저장소를 켜면 쿠키에는 티켓만 남는데, 티켓 안에 세션 id 가 있으므로 secret 을 바꾸면 어느 Redis 키를 지울지도 모르게 된다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"b7-cookie-session-tradeoff","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"B-7 · B-7a — 쿠키에 담는 세션, 그리고 그 대가","line":424}},"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="729" />
|
||||
<polyline class="edge kind-read style-solid emphasis-normal" points="157.0,517.0 157.0,565.0 157.0,565.0 157.0,613.0" data-evidence="441-452" />
|
||||
<rect class="edge-label-bg" x="128.4" y="551.0" width="105.1" height="22" rx="3" />
|
||||
<text class="edge-label" x="181.0" y="566.0">생성 시각이 여기 남는다</text>
|
||||
<polyline class="edge kind-write style-solid emphasis-normal" points="157.0,157.0 157.0,205.0 157.0,205.0 157.0,253.0" data-evidence="419-432" />
|
||||
<rect class="edge-label-bg" x="141.8" y="191.0" width="78.3" height="22" rx="3" />
|
||||
<text class="edge-label" x="181.0" y="206.0">티켓을 암호화한다</text>
|
||||
<polyline class="edge kind-read style-solid emphasis-normal" points="157.0,324.0 157.0,372.0 157.0,372.0 157.0,420.0" data-evidence="429-438" />
|
||||
<rect class="edge-label-bg" x="141.8" y="358.0" width="78.3" height="22" rx="3" />
|
||||
<text class="edge-label" x="181.0" y="373.0">키 이름을 만든다</text>
|
||||
<g id="node-secret">
|
||||
<rect class="node-shape kind-datastore emphasis-primary role-source" data-evidence="419-428" x="70.0" y="73.0" width="174.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-primary role-source" cx="157.0" cy="73.0" rx="87.0" ry="13.0" /><path class="storage-bottom" d="M 70.0 144.0 A 87.0 13.0 0 0 0 244.0 144.0" />
|
||||
<text class="node-label" x="157.0" y="100.0">k8s Secret</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="121.0" x2="230.0" y2="121.0" />
|
||||
<text class="node-detail" x="86.0" y="138.0">--cookie-secret 은 단수</text>
|
||||
</g>
|
||||
<g id="node-ticket">
|
||||
<rect class="node-shape kind-component emphasis-warning role-control" data-evidence="429-438" x="80.5" y="253.0" width="153.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="157.0" y="280.0">쿠키의 티켓</text>
|
||||
<line class="node-detail-divider" x1="94.5" y1="301.0" x2="219.5" y2="301.0" />
|
||||
<text class="node-detail" x="96.5" y="318.0">secret 을 바꾸면 못 푼다</text>
|
||||
</g>
|
||||
<g id="node-redis-session">
|
||||
<rect class="node-shape kind-datastore emphasis-warning role-target" data-evidence="429-440" x="82.0" y="433.0" width="150.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-warning role-target" cx="157.0" cy="433.0" rx="75.0" ry="13.0" /><path class="storage-bottom" d="M 82.0 504.0 A 75.0 13.0 0 0 0 232.0 504.0" />
|
||||
<text class="node-label" x="157.0" y="460.0">Redis 의 세션</text>
|
||||
<line class="node-detail-divider" x1="96.0" y1="481.0" x2="218.0" y2="481.0" />
|
||||
<text class="node-detail" x="98.0" y="498.0">고아로 남는다</text>
|
||||
</g>
|
||||
<g id="node-ttl">
|
||||
<rect class="node-shape kind-component emphasis-primary role-target" data-evidence="441-452" x="82.0" y="613.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="157.0" y="640.0">TTL</text>
|
||||
<line class="node-detail-divider" x1="96.0" y1="661.0" x2="218.0" y2="661.0" />
|
||||
<text class="node-detail" x="98.0" y="678.0">역산으로 고아를 고른다</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,22 @@
|
||||
# 백채널 로그아웃은 양쪽이 있어야 한다
|
||||
|
||||
## Alternative text
|
||||
|
||||
Keycloak 이 부를 주소와 앱이 받을 엔드포인트가 각각 비어 있어 로그아웃 통지가 어느 쪽에서도 성립하지 않는 구성.
|
||||
|
||||
## Long description
|
||||
|
||||
C-1 에서 로그아웃이 다른 앱으로 퍼지지 않는 것을 관측했고 원인은 단순했다. 두 클라이언트 모두 backchannelLogoutUrl 이 비어 있어 Keycloak 은 부를 주소를 몰랐고, BFF 소스에는 oidcLogout 설정이 없어 주소를 알려줘도 받을 곳이 없었다. Keycloak 파드에서 앱 URL 로 요청하면 200 이 돌아오므로 네트워크 문제도 아니다. 설정이 빠졌다와 기능이 없다는 다르게 고쳐야 하는데 여기는 둘 다였다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **한 앱에서 로그아웃** (actor): Keycloak 세션이 끝난다. Evidence: L458–L465.
|
||||
- **backchannelLogoutUrl** (component): 클라이언트 설정. 비어 있었다. Evidence: L458–L466.
|
||||
- **앱의 수신 엔드포인트** (component): 소스에 없었다. Evidence: L458–L466.
|
||||
- **다른 앱의 세션** (component): 그대로 남는다. Evidence: L458–L466.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **backchannelLogoutUrl → 앱의 수신 엔드포인트:** 주소가 있어야 부른다. Evidence: L458–L466.
|
||||
- **앱의 수신 엔드포인트 → 다른 앱의 세션:** 받을 곳이 있어야 지운다. Evidence: L458–L466.
|
||||
- **한 앱에서 로그아웃 → backchannelLogoutUrl:** 통지할 주소를 찾는다. Evidence: L458–L466.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 백채널 로그아웃은 양쪽이 있어야 한다
|
||||
# Question: IdP 쪽만 설정하면 로그아웃이 퍼지는가
|
||||
direction: down
|
||||
n0: "한 앱에서 로그아웃" {
|
||||
shape: person
|
||||
}
|
||||
n1: "backchannelLogoutUrl" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "앱의 수신 엔드포인트" {
|
||||
shape: rectangle
|
||||
}
|
||||
n3: "다른 앱의 세션" {
|
||||
shape: rectangle
|
||||
}
|
||||
n0 -> n1: "통지할 주소를 찾는다"
|
||||
n1 -> n2: "주소가 있어야 부른다"
|
||||
n2 -> n3: "받을 곳이 있어야 지운다"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="한 앱에서 로그아웃", shape=box, style="rounded,dashed,filled"];
|
||||
n1 [label="backchannelLogoutUrl", shape=box, style="rounded,filled"];
|
||||
n2 [label="앱의 수신 엔드포인트", shape=box, style="rounded,filled"];
|
||||
n3 [label="다른 앱의 세션", shape=box, style="rounded,filled"];
|
||||
n0 -> n1 [label="통지할 주소를 찾는다", style=solid];
|
||||
n1 -> n2 [label="주소가 있어야 부른다", style=solid];
|
||||
n2 -> n3 [label="받을 곳이 있어야 지운다", style=solid];
|
||||
}
|
||||
@@ -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="c2-backchannel-both-sides" name="백채널 로그아웃은 양쪽이 있어야 한다">
|
||||
<mxGraphModel dx="680" dy="690" 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_logout" value="한 앱에서 로그아웃" tooltip="Keycloak 세션이 끝난다. | Evidence: L458-L465" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="85.5" y="60.0" width="150.0" height="84.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_kc-config" value="backchannelLogoutUrl<br/>Keycloak 은 부를 주소를 모른다" tooltip="클라이언트 설정. 비어 있었다. | Evidence: L458-L466" 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="70.0" y="240.0" width="181.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_app-endpoint" value="앱의 수신 엔드포인트<br/>oidcLogout 설정 없음" tooltip="소스에 없었다. | Evidence: L458-L466" 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="85.5" y="407.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_app-session" value="다른 앱의 세션<br/>네트워크는 200 으로 닿는다" tooltip="그대로 남는다. | Evidence: L458-L466" 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="85.5" y="574.0" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_c-e" value="주소가 있어야 부른다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_kc-config" target="n_app-endpoint">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="184.5" y="359.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_e-s" value="받을 곳이 있어야 지운다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_app-endpoint" target="n_app-session">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="184.5" y="526.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_l-c" value="통지할 주소를 찾는다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_logout" target="n_kc-config">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="184.5" y="192.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "techviz-harness",
|
||||
"elements": [
|
||||
{
|
||||
"id": "edge-c-e",
|
||||
"type": "arrow",
|
||||
"x": 160.5,
|
||||
"y": 311.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1513220568,
|
||||
"version": 1,
|
||||
"versionNonce": 255748168,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-kc-config",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-app-endpoint",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-c-e",
|
||||
"type": "text",
|
||||
"x": 139.5,
|
||||
"y": 347.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 809267409,
|
||||
"version": 1,
|
||||
"versionNonce": 1101078427,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "주소가 있어야 부른다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "주소가 있어야 부른다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-e-s",
|
||||
"type": "arrow",
|
||||
"x": 160.5,
|
||||
"y": 478.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 1793841938,
|
||||
"version": 1,
|
||||
"versionNonce": 1078828844,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-app-endpoint",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-app-session",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-e-s",
|
||||
"type": "text",
|
||||
"x": 132.5,
|
||||
"y": 514.0,
|
||||
"width": 104,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 588860991,
|
||||
"version": 1,
|
||||
"versionNonce": 220268688,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "받을 곳이 있어야 지운다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "받을 곳이 있어야 지운다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-l-c",
|
||||
"type": "arrow",
|
||||
"x": 160.5,
|
||||
"y": 144.0,
|
||||
"width": 0.0,
|
||||
"height": 96.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": null,
|
||||
"seed": 281015540,
|
||||
"version": 1,
|
||||
"versionNonce": 30036555,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
96.0
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "node-logout",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "node-kc-config",
|
||||
"focus": 0,
|
||||
"gap": 4
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "edge-label-l-c",
|
||||
"type": "text",
|
||||
"x": 139.5,
|
||||
"y": 180.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1891696490,
|
||||
"version": 1,
|
||||
"versionNonce": 410528482,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 13,
|
||||
"fontFamily": 5,
|
||||
"text": "통지할 주소를 찾는다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "통지할 주소를 찾는다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-logout",
|
||||
"type": "rectangle",
|
||||
"x": 85.5,
|
||||
"y": 60.0,
|
||||
"width": 150.0,
|
||||
"height": 84.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 135793232,
|
||||
"version": 1,
|
||||
"versionNonce": 1679436838,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-logout",
|
||||
"type": "text",
|
||||
"x": 95.5,
|
||||
"y": 70.0,
|
||||
"width": 130.0,
|
||||
"height": 64.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1938124211,
|
||||
"version": 1,
|
||||
"versionNonce": 1421660240,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "한 앱에서 로그아웃",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "한 앱에서 로그아웃",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-kc-config",
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 240.0,
|
||||
"width": 181.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 514639084,
|
||||
"version": 1,
|
||||
"versionNonce": 1749573177,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-kc-config",
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 250.0,
|
||||
"width": 161.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 230474799,
|
||||
"version": 1,
|
||||
"versionNonce": 1719931632,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "backchannelLogoutUrl\nKeycloak 은 부를 주소를 모른다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "backchannelLogoutUrl\nKeycloak 은 부를 주소를 모른다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-app-endpoint",
|
||||
"type": "rectangle",
|
||||
"x": 85.5,
|
||||
"y": 407.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 153582198,
|
||||
"version": 1,
|
||||
"versionNonce": 1995435187,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-app-endpoint",
|
||||
"type": "text",
|
||||
"x": 95.5,
|
||||
"y": 417.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 268772163,
|
||||
"version": 1,
|
||||
"versionNonce": 304002942,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "앱의 수신 엔드포인트\noidcLogout 설정 없음",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "앱의 수신 엔드포인트\noidcLogout 설정 없음",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-app-session",
|
||||
"type": "rectangle",
|
||||
"x": 85.5,
|
||||
"y": 574.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1710553737,
|
||||
"version": 1,
|
||||
"versionNonce": 1839332537,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "node-label-app-session",
|
||||
"type": "text",
|
||||
"x": 95.5,
|
||||
"y": 584.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": null,
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 573778378,
|
||||
"version": 1,
|
||||
"versionNonce": 1579160282,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 0,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "다른 앱의 세션\n네트워크는 200 으로 닿는다",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "다른 앱의 세션\n네트워크는 200 으로 닿는다",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 10,
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"currentItemFontFamily": 5
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "c2-backchannel-both-sides",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "43396622cbf01d243efac1f48396163cc64738ef902bddda0ded0d9fb6467eb1",
|
||||
"source_context": {
|
||||
"document": "docs/keycloak-session-store/final/document.md",
|
||||
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
|
||||
"anchor": {
|
||||
"kind": "heading",
|
||||
"value": "C층 — SSO 와 로그아웃 전파",
|
||||
"line": 464
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
"c2-backchannel-both-sides.svg",
|
||||
"c2-backchannel-both-sides.mmd",
|
||||
"c2-backchannel-both-sides.d2",
|
||||
"c2-backchannel-both-sides.dot",
|
||||
"c2-backchannel-both-sides.drawio",
|
||||
"c2-backchannel-both-sides.excalidraw",
|
||||
"c2-backchannel-both-sides.alt.md"
|
||||
],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": false,
|
||||
"composition_profile": "component-flow",
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"diagram_only": true
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
%% 백채널 로그아웃은 양쪽이 있어야 한다
|
||||
%% question: IdP 쪽만 설정하면 로그아웃이 퍼지는가
|
||||
flowchart TB
|
||||
n0(["한 앱에서 로그아웃"])
|
||||
n1["backchannelLogoutUrl"]
|
||||
n2["앱의 수신 엔드포인트"]
|
||||
n3["다른 앱의 세션"]
|
||||
n0 -->|"통지할 주소를 찾는다"| n1
|
||||
n1 -->|"주소가 있어야 부른다"| n2
|
||||
n2 -->|"받을 곳이 있어야 지운다"| n3
|
||||
classDef external stroke-dasharray: 6 4
|
||||
class n0 external
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="690" viewBox="0 0 680 690" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">백채널 로그아웃은 양쪽이 있어야 한다</title>
|
||||
<desc id="diagram-description">C-1 에서 로그아웃이 다른 앱으로 퍼지지 않는 것을 관측했고 원인은 단순했다. 두 클라이언트 모두 backchannelLogoutUrl 이 비어 있어 Keycloak 은 부를 주소를 몰랐고, BFF 소스에는 oidcLogout 설정이 없어 주소를 알려줘도 받을 곳이 없었다. Keycloak 파드에서 앱 URL 로 요청하면 200 이 돌아오므로 네트워크 문제도 아니다. 설정이 빠졌다와 기능이 없다는 다르게 고쳐야 하는데 여기는 둘 다였다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"c2-backchannel-both-sides","profile":"component-flow"},"source_context":{"document":"docs/keycloak-session-store/final/document.md","document_sha256":"1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe","anchor":{"kind":"heading","value":"C층 — SSO 와 로그아웃 전파","line":464}},"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="690" />
|
||||
<polyline class="edge kind-blocked style-solid emphasis-normal" points="160.5,311.0 160.5,359.0 160.5,359.0 160.5,407.0" data-evidence="458-466" />
|
||||
<rect class="edge-label-bg" x="138.7" y="345.0" width="91.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="184.5" y="360.0">주소가 있어야 부른다</text>
|
||||
<polyline class="edge kind-blocked style-solid emphasis-normal" points="160.5,478.0 160.5,526.0 160.5,526.0 160.5,574.0" data-evidence="458-466" />
|
||||
<rect class="edge-label-bg" x="131.9" y="512.0" width="105.1" height="22" rx="3" />
|
||||
<text class="edge-label" x="184.5" y="527.0">받을 곳이 있어야 지운다</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="160.5,144.0 160.5,192.0 160.5,192.0 160.5,240.0" data-evidence="458-466" />
|
||||
<rect class="edge-label-bg" x="138.7" y="178.0" width="91.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="184.5" y="193.0">통지할 주소를 찾는다</text>
|
||||
<g id="node-logout">
|
||||
<g class="actor-symbol emphasis-primary" data-evidence="458-465"><circle cx="160.5" cy="80.0" r="11.0" /><line x1="160.5" y1="96.0" x2="160.5" y2="105.0" /><line x1="142.5" y1="106.0" x2="178.5" y2="106.0" /><line x1="160.5" y1="105.0" x2="145.5" y2="122.0" /><line x1="160.5" y1="105.0" x2="175.5" y2="122.0" /></g>
|
||||
<text class="node-label" x="160.5" y="137.0">한 앱에서 로그아웃</text>
|
||||
</g>
|
||||
<g id="node-kc-config">
|
||||
<rect class="node-shape kind-component emphasis-warning role-control" data-evidence="458-466" x="70.0" y="240.0" width="181.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="160.5" y="267.0">backchannelLogoutUrl</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="288.0" x2="237.0" y2="288.0" />
|
||||
<text class="node-detail" x="86.0" y="305.0">Keycloak 은 부를 주소를 모른다</text>
|
||||
</g>
|
||||
<g id="node-app-endpoint">
|
||||
<rect class="node-shape kind-component emphasis-warning role-control" data-evidence="458-466" x="85.5" y="407.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="160.5" y="434.0">앱의 수신 엔드포인트</text>
|
||||
<line class="node-detail-divider" x1="99.5" y1="455.0" x2="221.5" y2="455.0" />
|
||||
<text class="node-detail" x="101.5" y="472.0">oidcLogout 설정 없음</text>
|
||||
</g>
|
||||
<g id="node-app-session">
|
||||
<rect class="node-shape kind-component emphasis-warning role-target" data-evidence="458-466" x="85.5" y="574.0" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="160.5" y="601.0">다른 앱의 세션</text>
|
||||
<line class="node-detail-divider" x1="99.5" y1="622.0" x2="221.5" y2="622.0" />
|
||||
<text class="node-detail" x="101.5" y="639.0">네트워크는 200 으로 닿는다</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.1 KiB |
@@ -0,0 +1,22 @@
|
||||
# 캐시 온도가 결과를 가른다
|
||||
|
||||
## Alternative text
|
||||
|
||||
냉시동에서는 클라이언트 조회가, 반쯤 더운 상태에서는 스코프 조회가 데이터베이스에 닿아 실패하고, 완전히 더운 상태에서는 어느 쪽도 닿지 않는 구성.
|
||||
|
||||
## Long description
|
||||
|
||||
volatile 모드에서 로그인은 SQL 을 0개 쏜다. refresh 는 딱 한 문장을 쏘는데 CLIENT_SCOPE_CLIENT 의 선택적 스코프 조회이며, 그것도 첫 번째만 쏘고 이후 캐시된다. 그래서 DB 를 세웠을 때 완전 냉시동이면 클라이언트 조회부터 실패해 로그인이 400 이고, CLIENT 캐시만 더우면 refresh 가 500 이며, 완전히 더우면 둘 다 200 이다. A-7 이 표에 적은 것은 그 사이의 한 상태였다.
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **로그인 · refresh 요청** (actor): 같은 명령이 캐시 상태에 따라 다른 답을 받는다. Evidence: L284–L296.
|
||||
- **CLIENT 조회** (process): 냉시동에서 여기서 실패한다. Evidence: L306–L312.
|
||||
- **CLIENT_SCOPE_CLIENT 조회** (process): refresh 만 쏘고 첫 번째만 쏜다. Evidence: L284–L292.
|
||||
- **PostgreSQL** (datastore): 세운 상태다. 여기 닿는 조회만 실패한다. Evidence: L296–L302.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **CLIENT 조회 → CLIENT_SCOPE_CLIENT 조회:** refresh 는 스코프도 다시 계산한다. Evidence: L284–L292.
|
||||
- **로그인 · refresh 요청 → CLIENT 조회:** 클라이언트 확인. Evidence: L306–L312.
|
||||
- **CLIENT_SCOPE_CLIENT 조회 → PostgreSQL:** 캐시에 없으면 여기까지 간다. Evidence: L296–L302.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 캐시 온도가 결과를 가른다
|
||||
# Question: volatile 에서 DB 를 세우면 로그인과 refresh 는 어떻게 되는가
|
||||
direction: down
|
||||
n0: "로그인 · refresh 요청" {
|
||||
shape: person
|
||||
}
|
||||
n1: "CLIENT 조회" {
|
||||
shape: rectangle
|
||||
}
|
||||
n2: "CLIENT_SCOPE_CLIENT 조회" {
|
||||
shape: rectangle
|
||||
}
|
||||
n3: "PostgreSQL" {
|
||||
shape: cylinder
|
||||
}
|
||||
n0 -> n1: "클라이언트 확인"
|
||||
n1 -> n2: "refresh 는 스코프도 다시 계산한다"
|
||||
n2 -> n3: "캐시에 없으면 여기까지 간다"
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph techviz {
|
||||
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
|
||||
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
|
||||
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
|
||||
n0 [label="로그인 · refresh 요청", shape=box, style="rounded,dashed,filled"];
|
||||
n1 [label="CLIENT 조회", shape=box, style="rounded,filled"];
|
||||
n2 [label="CLIENT_SCOPE_CLIENT 조회", shape=box, style="rounded,filled"];
|
||||
n3 [label="PostgreSQL", shape=cylinder, style="rounded,filled"];
|
||||
n0 -> n1 [label="클라이언트 확인", style=solid];
|
||||
n1 -> n2 [label="refresh 는 스코프도 다시 계산한다", style=solid];
|
||||
n2 -> n3 [label="캐시에 없으면 여기까지 간다", style=solid];
|
||||
}
|
||||
@@ -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="cache-temperature-outcomes" name="캐시 온도가 결과를 가른다">
|
||||
<mxGraphModel dx="680" dy="716" 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_request" value="로그인 · refresh 요청" tooltip="같은 명령이 캐시 상태에 따라 다른 답을 받는다. | Evidence: L284-L296" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="106.5" y="60.0" width="150.0" height="84.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_client-lookup" value="CLIENT 조회<br/>select ce1_0.ID from CLIENT" tooltip="냉시동에서 여기서 실패한다. | Evidence: L306-L312" 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="70.0" y="240.0" width="223.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_scope-lookup" value="CLIENT_SCOPE_CLIENT 조회<br/>DEFAULT_SCOPE='f' — 선택적 스코프" tooltip="refresh 만 쏘고 첫 번째만 쏜다. | Evidence: L284-L292" 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="70.0" y="407.0" width="223.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_db" value="PostgreSQL<br/>캐시에 있으면 닿지 않는다" tooltip="세운 상태다. 여기 닿는 조회만 실패한다. | Evidence: L296-L302" 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;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="106.5" y="574.0" width="150.0" height="97.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_c-s" value="refresh 는 스코프도 다시 계산한다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_client-lookup" target="n_scope-lookup">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="205.5" y="359.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_r-c" value="클라이언트 확인" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_request" target="n_client-lookup">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="205.5" y="192.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_s-db" value="캐시에 없으면 여기까지 간다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_scope-lookup" target="n_db">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="205.5" y="526.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||