chore: 문서를 작성할 때 한국어의 표현 작성 스킬 추가 및 1인칭 관점의 글 작성 검증 테스트 추가
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"title": "`application-core`는 왜 Spring DI만 허용했을까",
|
||||
"document_type": "technical_blog",
|
||||
"language": "ko-KR",
|
||||
"audience": {
|
||||
"roles": [
|
||||
"Java 백엔드 개발자",
|
||||
"Clean Architecture를 적용하는 팀"
|
||||
],
|
||||
"prior_knowledge": [
|
||||
"Spring component scanning의 기본 개념",
|
||||
"Gradle multi-module의 기본 개념"
|
||||
],
|
||||
"needs": [
|
||||
"application layer의 framework 의존 경계를 판단할 기준",
|
||||
"선택 이유와 자동 검증 방법"
|
||||
]
|
||||
},
|
||||
"reader_goal": "`application-core`에서 Spring DI는 허용하면서 transaction, web, persistence 의존은 금지한 이유와 트레이드오프를 설명할 수 있다",
|
||||
"core_message": "framework-free라는 구호보다 의존 목적을 좁히고 자동 검증하는 편이 이 프로젝트의 문제에 맞았다. bean 등록을 위한 Spring DI는 허용하되 transaction, transport, persistence 정책은 application 경계 밖에 남겼다.",
|
||||
"scope": [
|
||||
"ca-tmpl의 `application-core` 의존성 결정",
|
||||
"Spring DI 허용 이유",
|
||||
"Gradle과 ArchUnit을 통한 경계 검증"
|
||||
],
|
||||
"non_scope": [
|
||||
"모든 Clean Architecture 프로젝트의 보편 규칙",
|
||||
"SLF4J 사용 이유",
|
||||
"운영 환경 성능 검증"
|
||||
],
|
||||
"prerequisites": [
|
||||
"Spring의 `@Service`, `@Component`, `@Configuration` 역할을 구분할 수 있음"
|
||||
],
|
||||
"required_topics": [
|
||||
"수동 bean 등록의 조립 코드 비용",
|
||||
"Spring DI 허용 범위",
|
||||
"`spring-tx`, Spring Web, JPA 금지",
|
||||
"`TransactionPort`",
|
||||
"Gradle dependency matrix",
|
||||
"ArchUnit rule과 정적 분석 한계"
|
||||
],
|
||||
"constraints": {
|
||||
"target_words": 1500,
|
||||
"tone": "프로젝트 문제와 선택 근거를 먼저 밝히는 직접적인 한국어 기술 블로그 문체",
|
||||
"version_context": "",
|
||||
"max_heading_depth": 3,
|
||||
"require_citations": true,
|
||||
"allow_external_knowledge": false,
|
||||
"citation_style": "hidden",
|
||||
"date_policy": "only_when_material",
|
||||
"style_profile": "woowahan_tech_blog_ko"
|
||||
},
|
||||
"forbidden_claims": [
|
||||
"application-core는 framework-free다",
|
||||
"SLF4J를 의도적으로 사용한다",
|
||||
"운영에서 검증했다"
|
||||
],
|
||||
"metadata": {
|
||||
"owner": "architecture",
|
||||
"risk": "medium",
|
||||
"example_kind": "golden-reader-facing"
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,13 @@
|
||||
"constraints": {
|
||||
"target_words": 1200,
|
||||
"tone": "운영 경험이 있는 엔지니어에게 설명하는 직접적이고 검증 가능한 문체",
|
||||
"version_context": "HTTP 의미론은 RFC 9110, 예시는 2026-07-23 기준",
|
||||
"version_context": "HTTP 메서드 의미론은 RFC 9110을 따른다.",
|
||||
"max_heading_depth": 3,
|
||||
"require_citations": true,
|
||||
"allow_external_knowledge": false
|
||||
"allow_external_knowledge": false,
|
||||
"citation_style": "hidden",
|
||||
"date_policy": "only_when_material",
|
||||
"style_profile": "woowahan_tech_blog_ko"
|
||||
},
|
||||
"forbidden_claims": [
|
||||
"재시도는 항상 안전하다"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: branch / feature-application-port-usecase-contract
|
||||
source_type: branch-note
|
||||
status: verified
|
||||
status_label: actually-implemented
|
||||
---
|
||||
|
||||
# branch: feature-application-port-usecase-contract
|
||||
|
||||
## 목표
|
||||
|
||||
application layer의 use case가 DTO, JPA entity, HTTP request, external client를 직접 다루지 않도록 port 계약과 command/query 모델을 고정한다.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
- D3: transaction boundary는 application use case 책임이지만 Spring `@Transactional` 직접 import는 금지하고 `TransactionPort` abstraction을 기본값으로 둔다.
|
||||
- D11: `TransactionPort`는 `Supplier<T>`와 `Runnable` 시그니처를 유지한다.
|
||||
- D13: `application-core`는 `org.springframework.stereotype.Service`와 `Component` 사용을 DI 등록 목적으로 허용한다. `spring-context`와 `spring-beans` 의존은 유지한다.
|
||||
- D13 이유: Spring DI까지 제거하면 use case bean마다 `@Configuration`에서 수동 등록해야 하므로 조립 코드가 급격히 늘어난다.
|
||||
- D13 경계: `spring-tx`, Spring Web, JPA annotation은 계속 금지한다. 편의 때문에 application layer의 책임을 transaction, transport, persistence까지 넓히지 않는다.
|
||||
|
||||
## 구현 및 검증
|
||||
|
||||
`application-core`의 `spring-tx` 의존성을 제거했다. `@Transactional`이 compile classpath에 없도록 했다. `application_does_not_use_spring_transactional_annotation`과 `application_does_not_depend_on_application_context` ArchUnit rule을 두고 negative fixture로 위반 검출을 확인했다.
|
||||
|
||||
## 선택의 비용
|
||||
|
||||
`application-core`가 Spring core DI 의존을 갖는다는 비용은 수용한다. 대신 허용 목적을 bean 등록으로 좁히고, transaction, transport, persistence 의존은 빌드 규칙과 ArchUnit으로 차단한다.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: branch / feature-log-management-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
status_label: implemented
|
||||
---
|
||||
|
||||
# branch: feature-log-management-contract
|
||||
|
||||
## 결정 사항
|
||||
|
||||
- 운영 로그는 structured JSON을 기본 포맷으로 둔다.
|
||||
- domain layer logger는 금지하고 domain invariant violation을 application layer에서 client-safe diagnostic log로 변환한다.
|
||||
|
||||
## 근거 경계
|
||||
|
||||
`domain layer logger 금지`는 외부 공식 문서가 직접 증명한 보편 원칙이 아니라 ca-tmpl 내부 정책이다. 외부 공개 글에서는 프로젝트 지역 결정으로만 표현한다.
|
||||
|
||||
이 문서는 `application-core`가 SLF4J를 사용하는 이유를 설명하지 않는다. 단어가 등장하거나 로거가 존재한다는 사실만으로 선택 이유를 만들어내지 않는다.
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Spring component stereotype and scanning notes
|
||||
source_type: official-doc
|
||||
status: reviewed
|
||||
---
|
||||
|
||||
# Spring component stereotype and scanning notes
|
||||
|
||||
## Supported behavior
|
||||
|
||||
Spring stereotype annotations such as `@Component` and `@Service` mark classes as candidates for component scanning and container registration.
|
||||
|
||||
## Evidence boundary
|
||||
|
||||
This vendor behavior explains what the annotations do. It does not prove why a particular project chose to use them, nor does it prove which other Spring dependencies the project allows. Project rationale must come from the project's own decision record.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: ca-tmpl - Clean Architecture 패키지 레이아웃 결정
|
||||
source_type: project
|
||||
status: verified
|
||||
confidence: high
|
||||
---
|
||||
|
||||
# ca-tmpl - Clean Architecture 패키지 레이아웃 결정
|
||||
|
||||
## 프로젝트 컨텍스트
|
||||
|
||||
ca-tmpl은 Java 21, Spring Boot 3.4, Gradle multi-module 기반 Clean Architecture template이다. `domain-core`, `application-core`, `adapter-*`, `shared-contract`, `app-bootstrap`, `sample-portfolio`를 물리적으로 분리한다.
|
||||
|
||||
## 실제 구현 내용
|
||||
|
||||
`domain-core`는 Spring, JPA, Servlet, Hibernate, Lombok, application, adapter, bootstrap 의존을 금지해 framework-neutral POJO 경계를 유지한다.
|
||||
|
||||
`application-core`는 adapter와 bootstrap, Spring Web, persistence, Hibernate에 의존하지 못한다. `@Transactional`과 `ApplicationContext` 직접 의존도 금지한다.
|
||||
|
||||
`shared-contract`는 response, request, error, operation, headers, logging, tracing, metrics, registry, annotation 같은 운영 계약 package만 허용한다. business common dumping ground로 사용하지 않는다.
|
||||
|
||||
## 경계 검증
|
||||
|
||||
Gradle의 `verifyCleanArchitectureDependencies`는 project dependency graph를 검사한다. ArchUnit의 `CleanArchitectureTest`는 source import graph를 검사한다. 두 검사는 서로 다른 그래프를 담당한다.
|
||||
|
||||
정적 분석은 모든 우회를 잡지 못한다. `getBean(String)`, `Class.forName(String)`, `BeanFactory#getBeansOfType` 같은 reflection-style bypass는 code review checklist로 보완한다.
|
||||
|
||||
## 검증 범위
|
||||
|
||||
module dependency matrix와 ArchUnit rule은 로컬에서 검증했다. 운영 배포와 운영 metric으로 검증한 결과는 없다.
|
||||
@@ -0,0 +1,712 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"document": "`application-core`는 왜 Spring DI만 허용했을까",
|
||||
"citation_style": "hidden",
|
||||
"reader_document_contains_internal_source_ids": false,
|
||||
"sections": [
|
||||
{
|
||||
"section_id": "01-problem-scene",
|
||||
"intent": "problem_scene",
|
||||
"title": "코드보다 먼저 드러난 문제",
|
||||
"reader_question": "독자가 공감할 수 있는 구체적인 상황에서 어떤 문제가 드러났는가?",
|
||||
"decision_requirements": [],
|
||||
"evidence": [
|
||||
{
|
||||
"id": "Lbe6cb7d8e8",
|
||||
"title": "branch / feature-application-port-usecase-contract — 결정 사항",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "결정 사항",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [
|
||||
"D11",
|
||||
"D13",
|
||||
"D3"
|
||||
],
|
||||
"priority": 103.785277
|
||||
},
|
||||
{
|
||||
"id": "Lf440ea562d",
|
||||
"title": "branch / feature-application-port-usecase-contract — 선택의 비용",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "선택의 비용",
|
||||
"line_start": 26,
|
||||
"line_end": 28,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 114.102502
|
||||
},
|
||||
{
|
||||
"id": "Ld4394f2f14",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "실제 구현 내용",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 49.973142
|
||||
},
|
||||
{
|
||||
"id": "L8db0ff5b86",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "검증 범위",
|
||||
"line_start": 28,
|
||||
"line_end": 30,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 52.655538
|
||||
}
|
||||
],
|
||||
"evidence_gap": false
|
||||
},
|
||||
{
|
||||
"section_id": "02-constraints",
|
||||
"intent": "constraints",
|
||||
"title": "문제를 어렵게 만든 제약",
|
||||
"reader_question": "단순한 해법을 막은 프로젝트 제약은 무엇이었는가?",
|
||||
"decision_requirements": [],
|
||||
"evidence": [
|
||||
{
|
||||
"id": "Lbe6cb7d8e8",
|
||||
"title": "branch / feature-application-port-usecase-contract — 결정 사항",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "결정 사항",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [
|
||||
"D11",
|
||||
"D13",
|
||||
"D3"
|
||||
],
|
||||
"priority": 103.785277
|
||||
},
|
||||
{
|
||||
"id": "Lf440ea562d",
|
||||
"title": "branch / feature-application-port-usecase-contract — 선택의 비용",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "선택의 비용",
|
||||
"line_start": 26,
|
||||
"line_end": 28,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 114.102502
|
||||
},
|
||||
{
|
||||
"id": "Ld4394f2f14",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "실제 구현 내용",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 49.973142
|
||||
},
|
||||
{
|
||||
"id": "L8db0ff5b86",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "검증 범위",
|
||||
"line_start": 28,
|
||||
"line_end": 30,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 52.655538
|
||||
}
|
||||
],
|
||||
"evidence_gap": false
|
||||
},
|
||||
{
|
||||
"section_id": "03-options",
|
||||
"intent": "options",
|
||||
"title": "검토한 선택지와 막힌 지점",
|
||||
"reader_question": "어떤 대안들을 검토했고 각각 어디에서 비용이 생겼는가?",
|
||||
"decision_requirements": [
|
||||
"상황·제약",
|
||||
"선택",
|
||||
"선택 이유",
|
||||
"검토한 대안",
|
||||
"수용한 비용",
|
||||
"보완 가드레일"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"id": "Lbe6cb7d8e8",
|
||||
"title": "branch / feature-application-port-usecase-contract — 결정 사항",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "결정 사항",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [
|
||||
"D11",
|
||||
"D13",
|
||||
"D3"
|
||||
],
|
||||
"priority": 103.785277
|
||||
},
|
||||
{
|
||||
"id": "Lf440ea562d",
|
||||
"title": "branch / feature-application-port-usecase-contract — 선택의 비용",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "선택의 비용",
|
||||
"line_start": 26,
|
||||
"line_end": 28,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 114.102502
|
||||
},
|
||||
{
|
||||
"id": "Ld4394f2f14",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "실제 구현 내용",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 49.973142
|
||||
},
|
||||
{
|
||||
"id": "L8db0ff5b86",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "검증 범위",
|
||||
"line_start": 28,
|
||||
"line_end": 30,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 52.655538
|
||||
}
|
||||
],
|
||||
"evidence_gap": false
|
||||
},
|
||||
{
|
||||
"section_id": "04-decision-rationale",
|
||||
"intent": "decision_rationale",
|
||||
"title": "선택의 이유와 지킨 경계",
|
||||
"reader_question": "왜 이 선택을 했으며 무엇을 일부러 포기하거나 금지했는가?",
|
||||
"decision_requirements": [
|
||||
"상황·제약",
|
||||
"선택",
|
||||
"선택 이유",
|
||||
"검토한 대안",
|
||||
"수용한 비용",
|
||||
"보완 가드레일"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"id": "Lbe6cb7d8e8",
|
||||
"title": "branch / feature-application-port-usecase-contract — 결정 사항",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "결정 사항",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [
|
||||
"D11",
|
||||
"D13",
|
||||
"D3"
|
||||
],
|
||||
"priority": 103.785277
|
||||
},
|
||||
{
|
||||
"id": "Lf440ea562d",
|
||||
"title": "branch / feature-application-port-usecase-contract — 선택의 비용",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "선택의 비용",
|
||||
"line_start": 26,
|
||||
"line_end": 28,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 114.102502
|
||||
},
|
||||
{
|
||||
"id": "Ld4394f2f14",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "실제 구현 내용",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 49.973142
|
||||
},
|
||||
{
|
||||
"id": "L8db0ff5b86",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "검증 범위",
|
||||
"line_start": 28,
|
||||
"line_end": 30,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 52.655538
|
||||
}
|
||||
],
|
||||
"evidence_gap": false
|
||||
},
|
||||
{
|
||||
"section_id": "05-mechanism",
|
||||
"intent": "mechanism",
|
||||
"title": "선택이 코드와 흐름에 반영되는 방식",
|
||||
"reader_question": "결정이 모듈, 인터페이스, 제어 흐름에 어떻게 반영되는가?",
|
||||
"decision_requirements": [],
|
||||
"evidence": [
|
||||
{
|
||||
"id": "Lbe6cb7d8e8",
|
||||
"title": "branch / feature-application-port-usecase-contract — 결정 사항",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "결정 사항",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [
|
||||
"D11",
|
||||
"D13",
|
||||
"D3"
|
||||
],
|
||||
"priority": 103.785277
|
||||
},
|
||||
{
|
||||
"id": "Lf440ea562d",
|
||||
"title": "branch / feature-application-port-usecase-contract — 선택의 비용",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "선택의 비용",
|
||||
"line_start": 26,
|
||||
"line_end": 28,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 114.102502
|
||||
},
|
||||
{
|
||||
"id": "Ld4394f2f14",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "실제 구현 내용",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 49.973142
|
||||
},
|
||||
{
|
||||
"id": "L8db0ff5b86",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "검증 범위",
|
||||
"line_start": 28,
|
||||
"line_end": 30,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 52.655538
|
||||
}
|
||||
],
|
||||
"evidence_gap": false
|
||||
},
|
||||
{
|
||||
"section_id": "06-evidence-verification",
|
||||
"intent": "evidence_verification",
|
||||
"title": "결정이 지켜지는지 확인하는 방법",
|
||||
"reader_question": "설명한 경계와 결과가 실제로 유지되는지 어떻게 확인하는가?",
|
||||
"decision_requirements": [],
|
||||
"evidence": [
|
||||
{
|
||||
"id": "Lbe6cb7d8e8",
|
||||
"title": "branch / feature-application-port-usecase-contract — 결정 사항",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "결정 사항",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [
|
||||
"D11",
|
||||
"D13",
|
||||
"D3"
|
||||
],
|
||||
"priority": 103.785277
|
||||
},
|
||||
{
|
||||
"id": "Lf440ea562d",
|
||||
"title": "branch / feature-application-port-usecase-contract — 선택의 비용",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "선택의 비용",
|
||||
"line_start": 26,
|
||||
"line_end": 28,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 114.102502
|
||||
},
|
||||
{
|
||||
"id": "L8db0ff5b86",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "검증 범위",
|
||||
"line_start": 28,
|
||||
"line_end": 30,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 52.655538
|
||||
},
|
||||
{
|
||||
"id": "Ld4394f2f14",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "실제 구현 내용",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 49.973142
|
||||
}
|
||||
],
|
||||
"evidence_gap": false
|
||||
},
|
||||
{
|
||||
"section_id": "07-tradeoffs",
|
||||
"intent": "tradeoffs",
|
||||
"title": "얻은 것, 잃은 것, 적용하지 않을 때",
|
||||
"reader_question": "이 선택의 비용과 한계는 무엇이며 언제 다른 선택이 나은가?",
|
||||
"decision_requirements": [
|
||||
"상황·제약",
|
||||
"선택",
|
||||
"선택 이유",
|
||||
"검토한 대안",
|
||||
"수용한 비용",
|
||||
"보완 가드레일"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"id": "Lbe6cb7d8e8",
|
||||
"title": "branch / feature-application-port-usecase-contract — 결정 사항",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "결정 사항",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [
|
||||
"D11",
|
||||
"D13",
|
||||
"D3"
|
||||
],
|
||||
"priority": 103.785277
|
||||
},
|
||||
{
|
||||
"id": "Lf440ea562d",
|
||||
"title": "branch / feature-application-port-usecase-contract — 선택의 비용",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "선택의 비용",
|
||||
"line_start": 26,
|
||||
"line_end": 28,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 114.102502
|
||||
},
|
||||
{
|
||||
"id": "Ld4394f2f14",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "실제 구현 내용",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 49.973142
|
||||
},
|
||||
{
|
||||
"id": "L54271e62b5",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 경계 검증",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "경계 검증",
|
||||
"line_start": 22,
|
||||
"line_end": 27,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 48.650226
|
||||
}
|
||||
],
|
||||
"evidence_gap": false
|
||||
},
|
||||
{
|
||||
"section_id": "08-conclusion",
|
||||
"intent": "conclusion",
|
||||
"title": "결국 지키려던 것은 무엇이었나",
|
||||
"reader_question": "세부 기술을 걷어냈을 때 남는 판단은 무엇인가?",
|
||||
"decision_requirements": [],
|
||||
"evidence": [],
|
||||
"evidence_gap": false
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"id": "Lf440ea562d",
|
||||
"title": "branch / feature-application-port-usecase-contract — 선택의 비용",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "선택의 비용",
|
||||
"line_start": 26,
|
||||
"line_end": 28,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 114.102502
|
||||
},
|
||||
{
|
||||
"id": "Lbe6cb7d8e8",
|
||||
"title": "branch / feature-application-port-usecase-contract — 결정 사항",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "결정 사항",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [
|
||||
"D11",
|
||||
"D13",
|
||||
"D3"
|
||||
],
|
||||
"priority": 103.785277
|
||||
},
|
||||
{
|
||||
"id": "L8db0ff5b86",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "검증 범위",
|
||||
"line_start": 28,
|
||||
"line_end": 30,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 52.655538
|
||||
},
|
||||
{
|
||||
"id": "Ld4394f2f14",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "실제 구현 내용",
|
||||
"line_start": 14,
|
||||
"line_end": 21,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 49.973142
|
||||
},
|
||||
{
|
||||
"id": "L54271e62b5",
|
||||
"title": "ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 경계 검증",
|
||||
"source_type": "canonical-project",
|
||||
"status": "verified",
|
||||
"path": "wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"heading": "경계 검증",
|
||||
"line_start": 22,
|
||||
"line_end": 27,
|
||||
"url": "repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 48.650226
|
||||
},
|
||||
{
|
||||
"id": "L6d3ebbb7a0",
|
||||
"title": "branch / feature-application-port-usecase-contract — 구현 및 검증",
|
||||
"source_type": "branch-note",
|
||||
"status": "verified",
|
||||
"path": "raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"heading": "구현 및 검증",
|
||||
"line_start": 22,
|
||||
"line_end": 25,
|
||||
"url": "repo:///raw/branch-notes/feature-application-port-usecase-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 37.568259
|
||||
},
|
||||
{
|
||||
"id": "L1259369d94",
|
||||
"title": "branch / feature-log-management-contract — 근거 경계",
|
||||
"source_type": "branch-note",
|
||||
"status": "raw",
|
||||
"path": "raw/branch-notes/feature-log-management-contract.md",
|
||||
"heading": "근거 경계",
|
||||
"line_start": 15,
|
||||
"line_end": 19,
|
||||
"url": "repo:///raw/branch-notes/feature-log-management-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 35.500648
|
||||
},
|
||||
{
|
||||
"id": "Lcb081a533b",
|
||||
"title": "Spring component stereotype and scanning notes — Evidence boundary",
|
||||
"source_type": "official-doc",
|
||||
"status": "reviewed",
|
||||
"path": "raw/official-docs/spring-component-scanning.md",
|
||||
"heading": "Evidence boundary",
|
||||
"line_start": 13,
|
||||
"line_end": 15,
|
||||
"url": "repo:///raw/official-docs/spring-component-scanning.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 22.580387
|
||||
},
|
||||
{
|
||||
"id": "L058b642200",
|
||||
"title": "Spring component stereotype and scanning notes — Supported behavior",
|
||||
"source_type": "official-doc",
|
||||
"status": "reviewed",
|
||||
"path": "raw/official-docs/spring-component-scanning.md",
|
||||
"heading": "Supported behavior",
|
||||
"line_start": 9,
|
||||
"line_end": 12,
|
||||
"url": "repo:///raw/official-docs/spring-component-scanning.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 15.066716
|
||||
},
|
||||
{
|
||||
"id": "L0ed1686206",
|
||||
"title": "branch / feature-log-management-contract — 결정 사항",
|
||||
"source_type": "branch-note",
|
||||
"status": "raw",
|
||||
"path": "raw/branch-notes/feature-log-management-contract.md",
|
||||
"heading": "결정 사항",
|
||||
"line_start": 10,
|
||||
"line_end": 14,
|
||||
"url": "repo:///raw/branch-notes/feature-log-management-contract.md",
|
||||
"accessed": "",
|
||||
"claim_ids": [],
|
||||
"decision_ids": [],
|
||||
"priority": 11.50868
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# `application-core`는 왜 Spring DI만 허용했을까
|
||||
|
||||
## 코드보다 먼저 드러난 문제
|
||||
|
||||
Clean Architecture를 적용하면 흔히 “코어에서 프레임워크를 제거해야 한다”는 문장부터 떠올린다. 이 원칙을 그대로 밀어붙이면 `application-core`의 use case도 Spring을 전혀 모르는 순수 Java 객체가 된다. 처음에는 경계가 가장 선명해 보인다.
|
||||
|
||||
문제는 조립 단계에서 드러났다. use case가 늘어날 때마다 `@Configuration`에 bean 등록 코드를 추가해야 했고, 생성자 의존성이 바뀔 때마다 조립 코드도 함께 수정해야 했다. 비즈니스 흐름과 무관한 등록 코드가 반복되면서 “Spring을 제거했다”는 이점보다 조립 비용이 더 빠르게 커졌다.
|
||||
|
||||
ca-tmpl이 풀려던 질문은 Spring을 쓰느냐 마느냐가 아니었다. `application-core`가 맡아야 할 책임은 지키면서, use case 등록에 필요한 반복 작업을 어디까지 줄일 것인가가 핵심이었다. 이 글은 그 결정을 다룬다. 모든 Clean Architecture 프로젝트에 같은 경계를 권하는 글은 아니며, 로깅 라이브러리 선택이나 운영 성능까지 설명하지 않는다.
|
||||
|
||||
## 문제를 어렵게 만든 제약
|
||||
|
||||
`application-core`는 application policy를 소유한다. command와 query, inbound port와 outbound port, transaction boundary의 의도는 이 계층에 있다. 반면 HTTP, JPA, Spring MVC, 구체적인 transaction 실행 방식은 adapter나 bootstrap 쪽 책임이다. DI 편의를 허용하더라도 이 구분이 무너지면 안 됐다.
|
||||
|
||||
그러나 의존성의 유무만으로 경계를 판단할 수는 없다. `spring-context`를 참조한다는 사실과 `@Transactional`로 transaction 정책을 표현한다는 사실은 같은 종류의 의존이 아니다. 전자는 객체를 컨테이너에 등록하는 조립 편의이고, 후자는 application policy를 Spring annotation으로 표현하는 설계 선택이다. 단순히 “Spring 있음/없음”으로 나누면 두 결정을 구분할 수 없다.
|
||||
|
||||
팀원이 규칙을 기억하는 데 의존하면 시간이 지날수록 예외가 쌓인다. 이를 막기 위해 허용과 금지의 경계는 문서에 적어 두는 데서 끝내지 않고, Gradle dependency graph와 source import graph에서 각각 위반을 검출할 수 있어야 했다.
|
||||
|
||||
## 검토한 선택지와 막힌 지점
|
||||
|
||||
가장 엄격한 선택은 `application-core`에서 Spring을 완전히 제거하는 것이다. use case는 순수 Java class로 두고 bootstrap module의 `@Configuration`에서 모두 수동 등록한다. framework 의존 경계는 가장 단순해지지만, use case 수와 생성자 의존성이 늘수록 조립 코드가 함께 증가한다. 프로젝트는 이 반복 비용을 실제 문제로 보았다.
|
||||
|
||||
반대쪽 선택은 Spring 편의를 application layer 전반에 허용하는 것이다. `@Service`뿐 아니라 `@Transactional`, Spring Web type, JPA annotation까지 사용할 수 있게 두면 구현 속도는 빨라질 수 있다. 그러나 transaction, transport, persistence 정책이 application code에 섞이면서 adapter를 교체하거나 경계를 검증하기 어려워진다. 편의를 허용하는 목적이 bean 등록을 넘어서는 순간이었다.
|
||||
|
||||
그래서 선택지를 “Spring을 제거할 것인가”와 “Spring을 사용할 것인가”로 나누지 않았다. 대신 의존 목적을 기준으로 잘랐다. 객체 등록에 필요한 DI stereotype은 허용하고, transaction 실행과 web·persistence 기술은 금지하는 중간 경계를 검토했다.
|
||||
|
||||
## 선택의 이유와 지킨 경계
|
||||
|
||||
ca-tmpl은 `application-core`에서 `@Service`와 `@Component`를 허용했다. use case를 component scanning으로 등록해, 각 use case마다 `@Configuration`에 bean을 수동 선언하는 반복을 피하기 위해서다. `spring-context`와 `spring-beans`를 compile dependency로 유지하는 비용도 함께 받아들였다.
|
||||
|
||||
다만 허용 목적을 DI 등록으로 한정했다. `spring-tx`, Spring Web, JPA annotation은 계속 금지한다. transaction boundary는 application use case가 결정하지만, 실행 방식은 `TransactionPort` 뒤로 숨긴다. application code는 `inWrite`, `inRead`, `inNew`처럼 필요한 transaction 의미를 요청하고, Spring의 `TransactionTemplate`을 사용하는 구현은 바깥에서 제공한다.
|
||||
|
||||
이 경계가 중요한 이유는 선택의 이점과 비용을 같은 위치에 묶어 두기 때문이다. 얻는 것은 use case 조립 코드의 감소다. 수용한 비용은 application module이 Spring core DI에 의존한다는 사실이다. 그 비용이 다른 프레임워크 의존으로 번지지 않도록 transaction, transport, persistence 의존을 명시적으로 금지했다.
|
||||
|
||||
따라서 “`application-core`는 framework-free다”라는 설명은 정확하지 않다. 더 정확한 설명은 “bean 등록을 위한 Spring DI는 허용하지만 application policy를 framework annotation과 adapter type으로 표현하지 않는다”이다.
|
||||
|
||||
## 선택이 코드와 흐름에 반영되는 방식
|
||||
|
||||
use case class는 application package에 놓이고 `@Service` 또는 `@Component`로 등록된다. 생성자에는 domain service나 outbound port 같은 application 경계의 dependency가 들어간다. controller DTO, JPA entity, Spring MVC type은 들어오지 않는다.
|
||||
|
||||
transaction이 필요한 write use case를 예로 들면 흐름은 다음과 같다.
|
||||
|
||||
```text
|
||||
HTTP adapter
|
||||
→ command 생성
|
||||
→ application use case 호출
|
||||
→ TransactionPort.inWrite(...) 요청
|
||||
→ SpringTransactionPort가 TransactionTemplate 실행
|
||||
→ outbound port 호출
|
||||
→ persistence adapter가 실제 저장 수행
|
||||
```
|
||||
|
||||
application use case가 알고 있는 것은 write transaction이 필요하다는 정책과 outbound port 계약이다. 어떤 transaction manager를 사용하고 어떤 persistence 기술이 저장을 수행하는지는 알지 못한다. DI stereotype은 use case를 찾고 연결하는 데만 쓰이며, transaction 구현을 application 안으로 가져오는 통로로 쓰이지 않는다.
|
||||
|
||||
이 구조의 불변조건은 세 가지다. application package는 adapter와 bootstrap에 의존하지 않는다. `@Transactional`을 직접 사용하지 않는다. `ApplicationContext`에서 bean을 런타임 조회하지 않는다. 이 조건이 지켜져야 DI 허용이 service locator나 framework policy 유입으로 확대되지 않는다.
|
||||
|
||||
## 결정이 지켜지는지 확인하는 방법
|
||||
|
||||
경계는 두 종류의 검사로 확인한다. Gradle의 dependency matrix는 module 간 `project()` 의존을 검사한다. 허용하지 않은 module dependency가 추가되면 build가 실패한다. 이 검사는 물리적인 build graph를 담당한다.
|
||||
|
||||
ArchUnit은 source와 bytecode의 의존 관계를 검사한다. application package가 adapter, bootstrap, Spring Web, persistence, Hibernate에 의존하지 않는지 확인한다. `@Transactional`과 `ApplicationContext` 직접 의존도 별도 rule로 차단한다. 의도된 위반 class를 test fixture에 두고 rule이 실제로 실패하는지도 검증한다.
|
||||
|
||||
검증 범위에는 한계가 있다. 정적 분석은 `getBean(String)`이나 `Class.forName(String)`처럼 문자열과 reflection을 이용한 우회를 모두 잡지 못한다. 따라서 빌드가 통과했다는 사실은 선언된 import와 dependency graph가 규칙을 지켰다는 뜻이지, 모든 런타임 우회가 불가능하다는 뜻은 아니다. 이 부분은 code review checklist로 보완한다.
|
||||
|
||||
또한 이 결정은 로컬 build와 architecture test로 확인됐다. 운영 배포와 운영 metric으로 검증된 선택이라고 확대해서 말할 수는 없다.
|
||||
|
||||
## 얻은 것, 잃은 것, 적용하지 않을 때
|
||||
|
||||
이 선택으로 use case 등록을 위한 반복적인 configuration code를 줄이면서도 transaction, web, persistence 경계를 유지할 수 있었다. “프레임워크 의존 0개”라는 단순한 규칙 대신, 허용 목적과 금지 범위를 더 세밀하게 표현하게 됐다.
|
||||
|
||||
반대로 규칙의 설명과 검증 비용은 늘었다. `spring-context`는 허용하지만 `spring-tx`는 금지한다는 차이를 팀원이 이해해야 하고, dependency matrix와 ArchUnit rule도 계속 관리해야 한다. 이 구분을 유지하는 이유는 bean 조립 편의가 transaction policy 유입의 근거로 확대되는 것을 막기 위해서다. Spring core DI 의존 자체를 제거해야 하는 library나 여러 DI container를 지원해야 하는 제품이라면 이 선택이 맞지 않을 수 있다. 그런 환경에서는 수동 조립이나 별도 composition module이 더 적합하다.
|
||||
|
||||
남은 위험은 허용된 stereotype이 점차 더 넓은 Spring 사용의 근거로 오해되는 것이다. 그래서 새 framework dependency를 추가할 때는 “application policy를 표현하기 위한가, 객체 조립을 위한가”를 먼저 묻는다. 전자라면 application 경계 밖으로 밀어내고, 후자라도 기존 허용 범위 안인지 build rule로 확인한다.
|
||||
|
||||
## 결국 지키려던 것은 무엇이었나
|
||||
|
||||
ca-tmpl이 지키려던 것은 framework-free라는 이름이 아니라 application 책임의 경계였다. bean 등록의 반복 비용을 줄이기 위해 Spring DI는 허용했지만, transaction·transport·persistence 정책이 application code로 들어오는 것은 막았다.
|
||||
|
||||
비슷한 결정을 내려야 한다면 의존성 개수부터 세지 않는 편이 낫다. 그 의존이 해결하는 구체적인 문제는 무엇인지, 제거했을 때 생기는 비용은 무엇인지, 허용 범위가 넓어지지 않도록 어떤 검사가 실패해야 하는지를 연속해서 답할 수 있어야 한다.
|
||||
@@ -0,0 +1,144 @@
|
||||
# Evidence and decision provenance
|
||||
|
||||
> This is an internal sidecar. It is not reader-facing article content.
|
||||
> Source IDs, repository paths, line ranges, status labels, and access dates belong here—not in `document.md`.
|
||||
|
||||
- Document: **`application-core`는 왜 Spring DI만 허용했을까**
|
||||
- Citation rendering: `hidden`
|
||||
- Evidence sources: **10**
|
||||
|
||||
## Section evidence map
|
||||
|
||||
| Section | Decision contract | Evidence | Status / location |
|
||||
|---|---|---|---|
|
||||
| 코드보다 먼저 드러난 문제 | — | `Lbe6cb7d8e8` branch / feature-application-port-usecase-contract — 결정 사항 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 결정 사항 (lines 14-21) |
|
||||
| ↳ | — | `Lf440ea562d` branch / feature-application-port-usecase-contract — 선택의 비용 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 선택의 비용 (lines 26-28) |
|
||||
| ↳ | — | `Ld4394f2f14` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 실제 구현 내용 (lines 14-21) |
|
||||
| ↳ | — | `L8db0ff5b86` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 검증 범위 (lines 28-30) |
|
||||
| 문제를 어렵게 만든 제약 | — | `Lbe6cb7d8e8` branch / feature-application-port-usecase-contract — 결정 사항 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 결정 사항 (lines 14-21) |
|
||||
| ↳ | — | `Lf440ea562d` branch / feature-application-port-usecase-contract — 선택의 비용 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 선택의 비용 (lines 26-28) |
|
||||
| ↳ | — | `Ld4394f2f14` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 실제 구현 내용 (lines 14-21) |
|
||||
| ↳ | — | `L8db0ff5b86` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 검증 범위 (lines 28-30) |
|
||||
| 검토한 선택지와 막힌 지점 | 상황·제약, 선택, 선택 이유, 검토한 대안, 수용한 비용, 보완 가드레일 | `Lbe6cb7d8e8` branch / feature-application-port-usecase-contract — 결정 사항 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 결정 사항 (lines 14-21) |
|
||||
| ↳ | — | `Lf440ea562d` branch / feature-application-port-usecase-contract — 선택의 비용 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 선택의 비용 (lines 26-28) |
|
||||
| ↳ | — | `Ld4394f2f14` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 실제 구현 내용 (lines 14-21) |
|
||||
| ↳ | — | `L8db0ff5b86` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 검증 범위 (lines 28-30) |
|
||||
| 선택의 이유와 지킨 경계 | 상황·제약, 선택, 선택 이유, 검토한 대안, 수용한 비용, 보완 가드레일 | `Lbe6cb7d8e8` branch / feature-application-port-usecase-contract — 결정 사항 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 결정 사항 (lines 14-21) |
|
||||
| ↳ | — | `Lf440ea562d` branch / feature-application-port-usecase-contract — 선택의 비용 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 선택의 비용 (lines 26-28) |
|
||||
| ↳ | — | `Ld4394f2f14` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 실제 구현 내용 (lines 14-21) |
|
||||
| ↳ | — | `L8db0ff5b86` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 검증 범위 (lines 28-30) |
|
||||
| 선택이 코드와 흐름에 반영되는 방식 | — | `Lbe6cb7d8e8` branch / feature-application-port-usecase-contract — 결정 사항 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 결정 사항 (lines 14-21) |
|
||||
| ↳ | — | `Lf440ea562d` branch / feature-application-port-usecase-contract — 선택의 비용 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 선택의 비용 (lines 26-28) |
|
||||
| ↳ | — | `Ld4394f2f14` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 실제 구현 내용 (lines 14-21) |
|
||||
| ↳ | — | `L8db0ff5b86` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 검증 범위 (lines 28-30) |
|
||||
| 결정이 지켜지는지 확인하는 방법 | — | `Lbe6cb7d8e8` branch / feature-application-port-usecase-contract — 결정 사항 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 결정 사항 (lines 14-21) |
|
||||
| ↳ | — | `Lf440ea562d` branch / feature-application-port-usecase-contract — 선택의 비용 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 선택의 비용 (lines 26-28) |
|
||||
| ↳ | — | `L8db0ff5b86` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 검증 범위 (lines 28-30) |
|
||||
| ↳ | — | `Ld4394f2f14` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 실제 구현 내용 (lines 14-21) |
|
||||
| 얻은 것, 잃은 것, 적용하지 않을 때 | 상황·제약, 선택, 선택 이유, 검토한 대안, 수용한 비용, 보완 가드레일 | `Lbe6cb7d8e8` branch / feature-application-port-usecase-contract — 결정 사항 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 결정 사항 (lines 14-21) |
|
||||
| ↳ | — | `Lf440ea562d` branch / feature-application-port-usecase-contract — 선택의 비용 | `verified` · raw/branch-notes/feature-application-port-usecase-contract.md — 선택의 비용 (lines 26-28) |
|
||||
| ↳ | — | `Ld4394f2f14` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 실제 구현 내용 (lines 14-21) |
|
||||
| ↳ | — | `L54271e62b5` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 경계 검증 | `verified` · wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 경계 검증 (lines 22-27) |
|
||||
| 결국 지키려던 것은 무엇이었나 | — | **GAP** | No allocated evidence |
|
||||
|
||||
## Source details
|
||||
|
||||
### `Lf440ea562d` branch / feature-application-port-usecase-contract — 선택의 비용
|
||||
|
||||
- Type: `branch-note`
|
||||
- Status: `verified`
|
||||
- Location: `raw/branch-notes/feature-application-port-usecase-contract.md — 선택의 비용 (lines 26-28)`
|
||||
- Public/reference URL: `repo:///raw/branch-notes/feature-application-port-usecase-contract.md`
|
||||
- Claim IDs: —
|
||||
- Decision IDs: —
|
||||
- Retrieval priority: `114.1025`
|
||||
|
||||
### `Lbe6cb7d8e8` branch / feature-application-port-usecase-contract — 결정 사항
|
||||
|
||||
- Type: `branch-note`
|
||||
- Status: `verified`
|
||||
- Location: `raw/branch-notes/feature-application-port-usecase-contract.md — 결정 사항 (lines 14-21)`
|
||||
- Public/reference URL: `repo:///raw/branch-notes/feature-application-port-usecase-contract.md`
|
||||
- Claim IDs: —
|
||||
- Decision IDs: `D11`, `D13`, `D3`
|
||||
- Retrieval priority: `103.7853`
|
||||
|
||||
### `L8db0ff5b86` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 검증 범위
|
||||
|
||||
- Type: `canonical-project`
|
||||
- Status: `verified`
|
||||
- Location: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 검증 범위 (lines 28-30)`
|
||||
- Public/reference URL: `repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md`
|
||||
- Claim IDs: —
|
||||
- Decision IDs: —
|
||||
- Retrieval priority: `52.6555`
|
||||
|
||||
### `Ld4394f2f14` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 실제 구현 내용
|
||||
|
||||
- Type: `canonical-project`
|
||||
- Status: `verified`
|
||||
- Location: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 실제 구현 내용 (lines 14-21)`
|
||||
- Public/reference URL: `repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md`
|
||||
- Claim IDs: —
|
||||
- Decision IDs: —
|
||||
- Retrieval priority: `49.9731`
|
||||
|
||||
### `L54271e62b5` ca-tmpl - Clean Architecture 패키지 레이아웃 결정 — 경계 검증
|
||||
|
||||
- Type: `canonical-project`
|
||||
- Status: `verified`
|
||||
- Location: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md — 경계 검증 (lines 22-27)`
|
||||
- Public/reference URL: `repo:///wiki/projects/ca-tmpl/clean-architecture-package-layout.md`
|
||||
- Claim IDs: —
|
||||
- Decision IDs: —
|
||||
- Retrieval priority: `48.6502`
|
||||
|
||||
### `L6d3ebbb7a0` branch / feature-application-port-usecase-contract — 구현 및 검증
|
||||
|
||||
- Type: `branch-note`
|
||||
- Status: `verified`
|
||||
- Location: `raw/branch-notes/feature-application-port-usecase-contract.md — 구현 및 검증 (lines 22-25)`
|
||||
- Public/reference URL: `repo:///raw/branch-notes/feature-application-port-usecase-contract.md`
|
||||
- Claim IDs: —
|
||||
- Decision IDs: —
|
||||
- Retrieval priority: `37.5683`
|
||||
|
||||
### `L1259369d94` branch / feature-log-management-contract — 근거 경계
|
||||
|
||||
- Type: `branch-note`
|
||||
- Status: `raw`
|
||||
- Location: `raw/branch-notes/feature-log-management-contract.md — 근거 경계 (lines 15-19)`
|
||||
- Public/reference URL: `repo:///raw/branch-notes/feature-log-management-contract.md`
|
||||
- Claim IDs: —
|
||||
- Decision IDs: —
|
||||
- Retrieval priority: `35.5006`
|
||||
|
||||
### `Lcb081a533b` Spring component stereotype and scanning notes — Evidence boundary
|
||||
|
||||
- Type: `official-doc`
|
||||
- Status: `reviewed`
|
||||
- Location: `raw/official-docs/spring-component-scanning.md — Evidence boundary (lines 13-15)`
|
||||
- Public/reference URL: `repo:///raw/official-docs/spring-component-scanning.md`
|
||||
- Claim IDs: —
|
||||
- Decision IDs: —
|
||||
- Retrieval priority: `22.5804`
|
||||
|
||||
### `L058b642200` Spring component stereotype and scanning notes — Supported behavior
|
||||
|
||||
- Type: `official-doc`
|
||||
- Status: `reviewed`
|
||||
- Location: `raw/official-docs/spring-component-scanning.md — Supported behavior (lines 9-12)`
|
||||
- Public/reference URL: `repo:///raw/official-docs/spring-component-scanning.md`
|
||||
- Claim IDs: —
|
||||
- Decision IDs: —
|
||||
- Retrieval priority: `15.0667`
|
||||
|
||||
### `L0ed1686206` branch / feature-log-management-contract — 결정 사항
|
||||
|
||||
- Type: `branch-note`
|
||||
- Status: `raw`
|
||||
- Location: `raw/branch-notes/feature-log-management-contract.md — 결정 사항 (lines 10-14)`
|
||||
- Public/reference URL: `repo:///raw/branch-notes/feature-log-management-contract.md`
|
||||
- Claim IDs: —
|
||||
- Decision IDs: —
|
||||
- Retrieval priority: `11.5087`
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="610" viewBox="0 0 1280 610" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Layered architecture boundary problem</title>
|
||||
<desc id="desc">Four technical layers depend downward; the business layer is consequently tied to database technology.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1280" height="610" fill="#FFFFFF"/>
|
||||
<rect x="300" y="55" width="600" height="82" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="600" y="90" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">Presentation</text>
|
||||
<text x="600" y="115" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">Controller · View</text>
|
||||
<rect x="300" y="175" width="600" height="82" fill="#EAF3FF" stroke="#0B5CC4" stroke-width="2" rx="8" />
|
||||
<text x="600" y="210" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">Business Logic</text>
|
||||
<text x="600" y="235" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">Service · 도메인 규칙</text>
|
||||
<rect x="300" y="295" width="600" height="82" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="600" y="330" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">Data Access</text>
|
||||
<text x="600" y="355" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">Repository · DAO</text>
|
||||
<rect x="470" y="435" width="260" height="85" fill="#F6F7F9" />
|
||||
<ellipse cx="600" cy="435" rx="130" ry="10" fill="#F6F7F9" stroke="#59616B" stroke-width="1.6" />
|
||||
<path d="M 470 435 L 470 520" fill="none" stroke="#59616B" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 730 435 L 730 520" fill="none" stroke="#59616B" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 470 520 C 522 532, 678 532, 730 520" fill="none" stroke="#59616B" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="600" y="479.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="700" fill="#25282D" text-anchor="middle">Database</text>
|
||||
<text x="600" y="500.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="450" fill="#667085" text-anchor="middle">기술 저장소</text>
|
||||
<path d="M 600 137 L 600 175" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="636.6" y="146" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="675" y="162" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">depends on</text>
|
||||
<path d="M 600 257 L 600 295" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="636.6" y="266" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="675" y="282" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">depends on</text>
|
||||
<path d="M 600 377 L 600 425" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="636.6" y="391" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="675" y="407" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">depends on</text>
|
||||
<rect x="975" y="182" width="250" height="150" fill="#FDEDEF" stroke="#D64550" stroke-width="1.6" rx="10" />
|
||||
<text x="1100" y="222" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="800" fill="#D64550" text-anchor="middle">문제</text>
|
||||
<text x="1100" y="255" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="650" fill="#25282D" text-anchor="middle">
|
||||
<tspan x="1100" dy="0">도메인이 기술에 묶인다</tspan>
|
||||
<tspan x="1100" dy="21">경계가 컴파일러에</tspan>
|
||||
<tspan x="1100" dy="21">보이지 않는다</tspan>
|
||||
</text>
|
||||
<path d="M 900 216 L 975 216" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-red)" />
|
||||
<rect x="888.6" y="183" width="98.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="938" y="199" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="middle">DB·기술에 종속</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="650" viewBox="0 0 1440 650" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Three architecture lenses</title>
|
||||
<desc id="desc">Layered, Hexagonal, and Clean are shown as three distinct views of the same inward dependency rule.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1440" height="650" fill="#FFFFFF"/>
|
||||
<rect x="40" y="45" width="420" height="455" fill="#FFFFFF" stroke="#8A919A" stroke-width="1.4" rx="10" />
|
||||
<text x="250" y="88" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="19" font-weight="750" fill="#25282D" text-anchor="middle">Layered</text>
|
||||
<text x="250" y="116" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="500" fill="#667085" text-anchor="middle">기술 책임을 층으로</text>
|
||||
<rect x="500" y="45" width="420" height="455" fill="#FFFFFF" stroke="#8A919A" stroke-width="1.4" rx="10" />
|
||||
<text x="710" y="88" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="19" font-weight="750" fill="#25282D" text-anchor="middle">Hexagonal</text>
|
||||
<text x="710" y="116" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="500" fill="#667085" text-anchor="middle">상호작용 경계를 포트로</text>
|
||||
<rect x="960" y="45" width="420" height="455" fill="#FFFFFF" stroke="#8A919A" stroke-width="1.4" rx="10" />
|
||||
<text x="1170" y="88" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="19" font-weight="750" fill="#25282D" text-anchor="middle">Clean</text>
|
||||
<text x="1170" y="116" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="500" fill="#667085" text-anchor="middle">정책 수준을 동심원으로</text>
|
||||
<rect x="105" y="160" width="290" height="60" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="250" y="195" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">Presentation</text>
|
||||
<path d="M 250 220 L 250 252" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="105" y="252" width="290" height="60" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="250" y="287" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">Application</text>
|
||||
<path d="M 250 312 L 250 344" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="105" y="344" width="290" height="60" fill="#EAF7F1" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="250" y="379" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">Domain</text>
|
||||
<polygon points="630,165 830,165 890,300 830,435 630,435 570,300" fill="#EAF3FF" stroke="#1677FF" stroke-width="2" />
|
||||
<text x="730" y="292" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="18" font-weight="750" fill="#0B5CC4" text-anchor="middle">Core</text>
|
||||
<circle cx="580" cy="300" r="8" fill="#FFFFFF" stroke="#1677FF" stroke-width="2" />
|
||||
<circle cx="880" cy="300" r="8" fill="#FFFFFF" stroke="#1677FF" stroke-width="2" />
|
||||
<path d="M 525 300 L 570 300" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<path d="M 890 300 L 935 300" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<text x="730" y="390" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#667085" text-anchor="middle">ports</text>
|
||||
<circle cx="1170" cy="300" r="150" fill="#F6F7F9" stroke="#858B94" stroke-width="1.6" />
|
||||
<circle cx="1170" cy="300" r="105" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.8" />
|
||||
<circle cx="1170" cy="300" r="55" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.8" />
|
||||
<text x="1170" y="305" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="750" fill="#168A5B" text-anchor="middle">Policy</text>
|
||||
<text x="1170" y="235" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#0B5CC4" text-anchor="middle">Use cases</text>
|
||||
<text x="1170" y="175" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#667085" text-anchor="middle">Frameworks</text>
|
||||
<path d="M 1310 300 L 1230 300" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="410" y="545" width="620" height="60" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.6" rx="30" />
|
||||
<text x="720" y="582" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="18" font-weight="750" fill="#0B5CC4" text-anchor="middle">한 규칙 · 의존은 안쪽으로만</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1480" height="760" viewBox="0 0 1480 760" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Executable clean architecture module picture</title>
|
||||
<desc id="desc">Inbound and outbound adapters point to application-core, which depends independently on domain-core and shared-contract; app-bootstrap wires the application.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1480" height="760" fill="#FFFFFF"/>
|
||||
<polygon points="535,190 895,190 975,365 895,540 535,540 455,365" fill="#EAF3FF" stroke="#1677FF" stroke-width="2.2" />
|
||||
<text x="715" y="265" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="22" font-weight="750" fill="#0B5CC4" text-anchor="middle">application-core</text>
|
||||
<text x="715" y="296" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="500" fill="#667085" text-anchor="middle">use cases · ports</text>
|
||||
<rect x="555" y="355" width="235" height="82" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="672.5" y="390" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">domain-core</text>
|
||||
<text x="672.5" y="415" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">main 의존 0</text>
|
||||
<rect x="805" y="355" width="235" height="82" fill="#FFFFFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="922.5" y="390" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">shared-contract</text>
|
||||
<text x="922.5" y="415" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">main 의존 0</text>
|
||||
<text x="797" y="477" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="600" fill="#667085" text-anchor="middle">서로 직접 의존하지 않음</text>
|
||||
<rect x="35" y="215" width="330" height="330" fill="#FBFCFE" stroke="#AEB5BF" stroke-width="1.3" rx="10" />
|
||||
<rect x="44" y="204" width="120.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="51" y="220" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="start">Inbound 어댑터 ×4</text>
|
||||
<rect x="75" y="270" width="250" height="44" fill="#FFFFFF" stroke="#69717B" stroke-width="1.5" rx="8" />
|
||||
<text x="200" y="297" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">web</text>
|
||||
<rect x="75" y="332" width="250" height="44" fill="#FFFFFF" stroke="#69717B" stroke-width="1.5" rx="8" />
|
||||
<text x="200" y="359" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">batch</text>
|
||||
<rect x="75" y="394" width="250" height="44" fill="#FFFFFF" stroke="#69717B" stroke-width="1.5" rx="8" />
|
||||
<text x="200" y="421" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">messaging-in</text>
|
||||
<rect x="75" y="456" width="250" height="44" fill="#FFFFFF" stroke="#69717B" stroke-width="1.5" rx="8" />
|
||||
<text x="200" y="483" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">scheduler</text>
|
||||
<rect x="1115" y="100" width="330" height="500" fill="#FBFCFE" stroke="#AEB5BF" stroke-width="1.3" rx="10" />
|
||||
<rect x="1124" y="89" width="133.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1131" y="105" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="start">Outbound 어댑터 ×10</text>
|
||||
<rect x="1155" y="155" width="250" height="46" fill="#FFFFFF" stroke="#69717B" stroke-width="1.5" rx="8" />
|
||||
<text x="1280" y="183" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">persistence</text>
|
||||
<rect x="1155" y="221" width="250" height="46" fill="#FFFFFF" stroke="#69717B" stroke-width="1.5" rx="8" />
|
||||
<text x="1280" y="249" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">object storage</text>
|
||||
<rect x="1155" y="287" width="250" height="46" fill="#FFFFFF" stroke="#69717B" stroke-width="1.5" rx="8" />
|
||||
<text x="1280" y="315" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">notification</text>
|
||||
<rect x="1155" y="353" width="250" height="46" fill="#FFFFFF" stroke="#69717B" stroke-width="1.5" rx="8" />
|
||||
<text x="1280" y="381" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">cache</text>
|
||||
<rect x="1155" y="419" width="250" height="46" fill="#FFFFFF" stroke="#69717B" stroke-width="1.5" rx="8" />
|
||||
<text x="1280" y="447" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">messaging</text>
|
||||
<rect x="1155" y="485" width="250" height="46" fill="#FFFFFF" stroke="#69717B" stroke-width="1.5" rx="8" />
|
||||
<text x="1280" y="513" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">http client</text>
|
||||
<path d="M 365 365 L 455 365" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="371.6" y="340" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="410" y="356" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Depends on</text>
|
||||
<path d="M 1115 365 L 975 365" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="1006.6" y="340" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1045" y="356" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Depends on</text>
|
||||
<path d="M 715 330 L 672 355" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-green)" />
|
||||
<rect x="599.6" y="318" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="638" y="334" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">Depends on</text>
|
||||
<path d="M 780 330 L 855 355" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<rect x="845.6" y="318" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="884" y="334" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Depends on</text>
|
||||
<rect x="630" y="35" width="290" height="105" fill="#FFFFFF" stroke="#59616B" stroke-width="1.6" rx="3" />
|
||||
<path d="M 630 70 L 920 70" fill="none" stroke="#59616B" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 630 105 L 920 105" fill="none" stroke="#59616B" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle cx="648" cy="52.5" r="4" fill="#1677FF" />
|
||||
<circle cx="648" cy="87.5" r="4" fill="#1677FF" />
|
||||
<circle cx="648" cy="122.5" r="4" fill="#1677FF" />
|
||||
<text x="775" y="92.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="700" fill="#25282D" text-anchor="middle">app-bootstrap</text>
|
||||
<text x="775" y="166" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="600" fill="#667085" text-anchor="middle">조립 루트</text>
|
||||
<path d="M 775 140 L 775 190" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="827.3" y="169" width="45.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="850" y="185" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">Wires</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1090" height="515" viewBox="0 0 1090 515" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">bootstrap이 선택한 어댑터를 연결하고 두 검증 게이트가 안쪽 의존을 지킨다</title>
|
||||
<desc id="diagram-description">가운데 Application Core를 기준으로 왼쪽에는 Inbound adapters와 Input port, 오른쪽에는 Output port와 Outbound adapters가 있다. 어댑터의 모듈 의존은 포트와 코어 쪽을 향한다. 아래의 app-bootstrap은 실제 사용할 양쪽 어댑터를 선택하고 application port에 연결한다. 별도의 두 검증 게이트 중 verifyCleanArchitectureDependencies는 모듈 간 프로젝트 의존을 검사하고 ArchUnit 규칙은 모듈 내부 코드의 금지된 프레임워크 타입 참조를 검사한다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"bootstrap-dependency-guards","profile":"ports-adapters"},"source_context":{"document":"/home/donghyeon/workspace/ai-tool/topic-arrange/executable-clean-architecture/claridoc-rewrite/document.md","document_sha256":"04fbab095d33d301746c34f7cca305730919bad3c341bcf63b8ad3ee3b396d31","anchor":{"kind":"marker","value":"bootstrap-dependency-guards","line":342}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="1090" height="515" />
|
||||
<polyline class="edge kind-validation style-dashed emphasis-normal" points="615.0,417.5 517.5,417.5 517.5,259.0 420.0,259.0" data-evidence="338-338,340-340" />
|
||||
<rect class="edge-label-bg" x="519.5" y="324.2" width="44.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="541.5" y="339.2">적용</text>
|
||||
<polyline class="edge kind-dependency style-solid emphasis-primary" points="255.0,117.0 337.5,117.0 337.5,241.0 420.0,241.0" data-evidence="322-328,340-340" />
|
||||
<rect class="edge-label-bg" x="335.8" y="165.0" width="51.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="361.5" y="180.0">입력 호출</text>
|
||||
<polyline class="edge kind-dependency style-solid emphasis-primary" points="935.0,149.0 935.0,361.0 535.0,361.0 535.0,315.0" data-evidence="311-311,340-340" />
|
||||
<rect class="edge-label-bg" x="762.1" y="319.0" width="111.8" height="22" rx="3" />
|
||||
<text class="edge-label" x="818.0" y="334.0">Output Port 구현</text>
|
||||
<g id="node-application-core">
|
||||
<polygon class="node-shape kind-domain emphasis-primary role-core" data-evidence="290-291,298-299,311-311" points="454.0,185.0 616.0,185.0 650.0,250.0 616.0,315.0 454.0,315.0 420.0,250.0" />
|
||||
<text class="node-role" x="535.0" y="204.0">«core»</text>
|
||||
<text class="node-label" x="535.0" y="240.0">Application Core</text>
|
||||
<line class="node-detail-divider" x1="434.0" y1="261.0" x2="636.0" y2="261.0" />
|
||||
<text class="node-detail" x="436.0" y="278.0">Use cases · Input / Output Ports</text>
|
||||
</g>
|
||||
<g id="node-inbound-adapters">
|
||||
<rect class="node-shape kind-adapter emphasis-normal role-inbound-adapter" data-evidence="293-293,315-322,331-333" x="45.0" y="85.0" width="210.0" height="64.0" rx="7" />
|
||||
<text class="node-role" x="150.0" y="104.0">«inbound-adapter»</text>
|
||||
<text class="node-label" x="150.0" y="131.0">Inbound adapters</text>
|
||||
</g>
|
||||
<g id="node-outbound-adapters">
|
||||
<rect class="node-shape kind-adapter emphasis-normal role-outbound-adapter" data-evidence="294-295,302-311,331-334" x="825.0" y="85.0" width="220.0" height="64.0" rx="7" />
|
||||
<text class="node-role" x="935.0" y="104.0">«outbound-adapter»</text>
|
||||
<text class="node-label" x="935.0" y="131.0">Outbound adapters</text>
|
||||
</g>
|
||||
<g id="node-assembly-enforcement">
|
||||
<rect class="node-shape kind-governance emphasis-normal" data-evidence="292-292,338-338,340-340" x="420.0" y="365.0" width="195.0" height="105.0" rx="7" />
|
||||
<text class="node-label" x="517.5" y="392.0">Assembly & guards</text>
|
||||
<line class="node-detail-divider" x1="434.0" y1="413.0" x2="601.0" y2="413.0" />
|
||||
<text class="node-detail" x="436.0" y="430.0">RUNTIME · adapter 선택·연결</text>
|
||||
<text class="node-detail" x="436.0" y="446.0">BUILD · 모듈 의존 검사</text>
|
||||
<text class="node-detail" x="436.0" y="462.0">TEST · 금지 타입 참조 검사</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1380" height="700" viewBox="0 0 1380 700" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Boundary enforcement comparison</title>
|
||||
<desc id="desc">A two-by-two parallel comparison of four structures and their available boundary enforcement mechanisms; it is not a progression.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1380" height="700" fill="#FFFFFF"/>
|
||||
<rect x="520" y="28" width="239.3" height="30" fill="#F6F7F9" stroke="#AEB5BF" stroke-width="1.2" rx="15" />
|
||||
<text x="639.6" y="48" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#667085" text-anchor="middle">2×2 병렬 비교 · 진행 단계 아님</text>
|
||||
<rect x="50" y="95" width="620" height="245" fill="#FFFFFF" stroke="#757C85" stroke-width="1.4" rx="10" />
|
||||
<rect x="50" y="95" width="9" height="245" fill="#D64550" rx="4" />
|
||||
<text x="84" y="138" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="19" font-weight="750" fill="#25282D" text-anchor="start">단일모듈 Layered</text>
|
||||
<text x="84" y="165" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="550" fill="#667085" text-anchor="start">설명용 검출 예시</text>
|
||||
<text x="105" y="213" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="500" fill="#25282D" text-anchor="start">
|
||||
<tspan x="105" dy="0">• 도메인 → JPA 타입</tspan>
|
||||
<tspan x="105" dy="21.7">• 공유 클래스패스</tspan>
|
||||
</text>
|
||||
<rect x="400" y="265" width="116.8" height="30" fill="#F6F7F9" stroke="#D64550" stroke-width="1.2" rx="15" />
|
||||
<text x="458.4" y="285" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#D64550" text-anchor="middle">경계 장치 없음</text>
|
||||
<rect x="710" y="95" width="620" height="245" fill="#FFFFFF" stroke="#757C85" stroke-width="1.4" rx="10" />
|
||||
<rect x="710" y="95" width="9" height="245" fill="#B7791F" rx="4" />
|
||||
<text x="744" y="138" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="19" font-weight="750" fill="#25282D" text-anchor="start">단일모듈 Clean</text>
|
||||
<text x="744" y="165" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="550" fill="#667085" text-anchor="start">별도 규칙 필요</text>
|
||||
<text x="765" y="213" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="500" fill="#25282D" text-anchor="start">
|
||||
<tspan x="765" dy="0">• 패키지 경계</tspan>
|
||||
<tspan x="765" dy="21.7">• 위반 타입은 여전히 보임</tspan>
|
||||
</text>
|
||||
<rect x="1060" y="265" width="116.7" height="30" fill="#FFFFFF" stroke="#B7791F" stroke-width="1.2" rx="15" />
|
||||
<text x="1118.3" y="285" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#B7791F" text-anchor="middle">ArchUnit 필요</text>
|
||||
<rect x="50" y="385" width="620" height="245" fill="#FFFFFF" stroke="#757C85" stroke-width="1.4" rx="10" />
|
||||
<rect x="50" y="385" width="9" height="245" fill="#1677FF" rx="4" />
|
||||
<text x="84" y="428" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="19" font-weight="750" fill="#25282D" text-anchor="start">멀티모듈 Clean</text>
|
||||
<text x="84" y="455" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="550" fill="#667085" text-anchor="start">클래스패스 격리 가능</text>
|
||||
<text x="105" y="503" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="500" fill="#25282D" text-anchor="start">
|
||||
<tspan x="105" dy="0">• 모듈별 classpath</tspan>
|
||||
<tspan x="105" dy="21.7">• 금지 타입 자체가 없음</tspan>
|
||||
</text>
|
||||
<rect x="400" y="555" width="94.5" height="30" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.2" rx="15" />
|
||||
<text x="447.2" y="575" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#1677FF" text-anchor="middle">javac 차단</text>
|
||||
<rect x="710" y="385" width="620" height="245" fill="#FFFFFF" stroke="#757C85" stroke-width="1.4" rx="10" />
|
||||
<rect x="710" y="385" width="9" height="245" fill="#168A5B" rx="4" />
|
||||
<text x="744" y="428" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="19" font-weight="750" fill="#25282D" text-anchor="start">정책·테스트 설치 Clean</text>
|
||||
<text x="744" y="455" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="550" fill="#667085" text-anchor="start">명시 규칙 강제</text>
|
||||
<text x="765" y="503" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="500" fill="#25282D" text-anchor="start">
|
||||
<tspan x="765" dy="0">• Gradle 의존 정책</tspan>
|
||||
<tspan x="765" dy="21.7">• ArchUnit · test-the-test</tspan>
|
||||
</text>
|
||||
<rect x="1060" y="555" width="96.4" height="30" fill="#FFFFFF" stroke="#168A5B" stroke-width="1.2" rx="15" />
|
||||
<text x="1108.2" y="575" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#168A5B" text-anchor="middle">복수 게이트</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1460" height="760" viewBox="0 0 1460 760" role="img" aria-labelledby="title desc">
|
||||
<title id="title">System boundary integrations and seams</title>
|
||||
<desc id="desc">Four implemented or configured external paths use solid arrows; three project-supplied extension seams use dashed arrows.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1460" height="760" fill="#FFFFFF"/>
|
||||
<rect x="35" y="45" width="1390" height="670" fill="#FBFCFE" stroke="#AEB5BF" stroke-width="1.3" rx="10" />
|
||||
<rect x="44" y="34" width="219.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="51" y="50" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="start">시스템 경계 · 구현 경로와 확장 seam</text>
|
||||
<rect x="90" y="82" width="313.6" height="30" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.2" rx="15" />
|
||||
<text x="246.8" y="102" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#0B5CC4" text-anchor="middle">실선 = 구현·설정 경로 존재 · 활성 런타임 아님</text>
|
||||
<rect x="780" y="82" width="236.4" height="30" fill="#FFFFFF" stroke="#7556D8" stroke-width="1.2" rx="15" />
|
||||
<text x="898.2" y="102" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#7556D8" text-anchor="middle">점선 = 프로젝트가 공급할 확장 seam</text>
|
||||
<rect x="95" y="170" width="250" height="72" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="220" y="211" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">persistence-jpa</text>
|
||||
<rect x="515" y="178" width="220" height="58" fill="#EAF3FF" />
|
||||
<ellipse cx="625" cy="178" rx="110" ry="10" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.6" />
|
||||
<path d="M 515 178 L 515 236" fill="none" stroke="#1677FF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 735 178 L 735 236" fill="none" stroke="#1677FF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 515 236 C 559 248, 691 248, 735 236" fill="none" stroke="#1677FF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="625" y="209" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="700" fill="#25282D" text-anchor="middle">PostgreSQL</text>
|
||||
<text x="625" y="230" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="450" fill="#667085" text-anchor="middle">드라이버 · dialect</text>
|
||||
<path d="M 345 206 L 515 206" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="395.5" y="181" width="69" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="430" y="197" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">구현·설정</text>
|
||||
<rect x="95" y="295" width="250" height="72" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="220" y="336" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">persistence-mongo</text>
|
||||
<rect x="515" y="303" width="220" height="58" fill="#EAF3FF" />
|
||||
<ellipse cx="625" cy="303" rx="110" ry="10" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.6" />
|
||||
<path d="M 515 303 L 515 361" fill="none" stroke="#1677FF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 735 303 L 735 361" fill="none" stroke="#1677FF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 515 361 C 559 373, 691 373, 735 361" fill="none" stroke="#1677FF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="625" y="334" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="700" fill="#25282D" text-anchor="middle">MongoDB</text>
|
||||
<text x="625" y="355" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="450" fill="#667085" text-anchor="middle">opt-in 스캐폴드</text>
|
||||
<path d="M 345 331 L 515 331" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="395.5" y="306" width="69" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="430" y="322" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">구현·설정</text>
|
||||
<rect x="95" y="420" width="250" height="72" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="220" y="461" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">objectstorage</text>
|
||||
<rect x="515" y="420" width="220" height="72" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="625" y="450" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">S3 / MinIO</text>
|
||||
<text x="625" y="475" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">선택형 백엔드</text>
|
||||
<path d="M 345 456 L 515 456" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="395.5" y="431" width="69" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="430" y="447" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">구현·설정</text>
|
||||
<rect x="95" y="545" width="250" height="72" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="220" y="586" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">fileserver</text>
|
||||
<polygon points="535,533 679,533 715,569 715,625 535,625" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.7" />
|
||||
<path d="M 679 533 L 679 569 L 715 569" fill="none" stroke="#1677FF" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="553" y="569" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="700" fill="#25282D" text-anchor="start">파일시스템</text>
|
||||
<text x="553" y="598" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="500" fill="#667085" text-anchor="start">
|
||||
<tspan x="553" dy="0">구현 경로</tspan>
|
||||
</text>
|
||||
<path d="M 345 581 L 515 581" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="395.5" y="556" width="69" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="430" y="572" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">구현·설정</text>
|
||||
<rect x="820" y="220" width="235" height="75" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="937.5" y="262.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">notification</text>
|
||||
<circle cx="1180" cy="257" r="10" fill="#FFFFFF" stroke="#7556D8" stroke-width="2" />
|
||||
<rect x="1210" y="220" width="170" height="75" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1295" y="262.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">SlackClient seam</text>
|
||||
<path d="M 1055 257 L 1170 257" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1093.0" y="232" width="39.1" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1112.5" y="248" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">seam</text>
|
||||
<rect x="820" y="370" width="235" height="75" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="937.5" y="412.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">cache-redis</text>
|
||||
<circle cx="1180" cy="407" r="10" fill="#FFFFFF" stroke="#7556D8" stroke-width="2" />
|
||||
<rect x="1210" y="370" width="170" height="75" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1295" y="412.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">RedisClient seam</text>
|
||||
<path d="M 1055 407 L 1170 407" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1093.0" y="382" width="39.1" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1112.5" y="398" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">seam</text>
|
||||
<rect x="820" y="520" width="235" height="75" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="937.5" y="562.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">messaging</text>
|
||||
<circle cx="1180" cy="557" r="10" fill="#FFFFFF" stroke="#7556D8" stroke-width="2" />
|
||||
<rect x="1210" y="520" width="170" height="75" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1295" y="562.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">KafkaSender seam</text>
|
||||
<path d="M 1055 557 L 1170 557" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1093.0" y="532" width="39.1" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1112.5" y="548" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">seam</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="530" viewBox="0 0 1440 530" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Package organization spectrum</title>
|
||||
<desc id="desc">A single axis places layer-first and feature-first at its ends and marks ca-tmpl as a hybrid supported by both feature and technical package evidence.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1440" height="530" fill="#FFFFFF"/>
|
||||
<path d="M 100 255 L 1340 255" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<circle cx="120" cy="255" r="9" fill="#FFFFFF" stroke="#667085" stroke-width="2" />
|
||||
<path d="M 120 180 L 120 330" fill="none" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="120" y="160" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="750" fill="#667085" text-anchor="middle">layer-first</text>
|
||||
<text x="120" y="358" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="550" fill="#667085" text-anchor="middle">기술 책임 중심</text>
|
||||
<circle cx="480" cy="255" r="9" fill="#FFFFFF" stroke="#25282D" stroke-width="2" />
|
||||
<path d="M 480 180 L 480 330" fill="none" stroke="#25282D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="480" y="160" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="750" fill="#25282D" text-anchor="middle">계층 소유 코어·어댑터</text>
|
||||
<text x="480" y="358" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="550" fill="#667085" text-anchor="middle">구조 경계</text>
|
||||
<circle cx="860" cy="255" r="13" fill="#1677FF" stroke="#1677FF" stroke-width="2" />
|
||||
<path d="M 860 180 L 860 330" fill="none" stroke="#1677FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="860" y="160" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="750" fill="#1677FF" text-anchor="middle">ca-tmpl · hybrid</text>
|
||||
<text x="860" y="358" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="550" fill="#667085" text-anchor="middle">현재 저장소 배치</text>
|
||||
<circle cx="1320" cy="255" r="9" fill="#FFFFFF" stroke="#667085" stroke-width="2" />
|
||||
<path d="M 1320 180 L 1320 330" fill="none" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="1320" y="160" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="750" fill="#667085" text-anchor="middle">feature-first</text>
|
||||
<text x="1320" y="358" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="550" fill="#667085" text-anchor="middle">기능 응집 중심</text>
|
||||
<rect x="590" y="380" width="250" height="70" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="715" y="409" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">application.worklog</text>
|
||||
<text x="715" y="434" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">기능 패키지</text>
|
||||
<rect x="880" y="380" width="250" height="70" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1005" y="409" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">web.controller</text>
|
||||
<text x="1005" y="434" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">기술 패키지</text>
|
||||
<path d="M 715 380 L 820 280" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-green)" />
|
||||
<rect x="666.0" y="299" width="108.0" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="720" y="315" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">supports hybrid</text>
|
||||
<path d="M 1005 380 L 900 280" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<rect x="956.0" y="299" width="108.0" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1010" y="315" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">supports hybrid</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1420" height="560" viewBox="0 0 1420 560" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Spring Modulith evidence and decision boundary</title>
|
||||
<desc id="desc">A fixed Gradle-script search establishes zero Spring Modulith dependency declarations; adoption remains a separate conditional evaluation.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1420" height="560" fill="#FFFFFF"/>
|
||||
<rect x="45" y="65" width="800" height="420" fill="#FBFCFE" stroke="#1677FF" stroke-width="1.3" rx="10" />
|
||||
<rect x="54" y="54" width="142.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="61" y="70" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="start">확정 가능한 저장소 사실</text>
|
||||
<polygon points="95,155 344,155 380,191 380,345 95,345" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.7" />
|
||||
<path d="M 344 155 L 344 191 L 380 191" fill="none" stroke="#1677FF" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="113" y="191" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="700" fill="#0B5CC4" text-anchor="start">검색 범위</text>
|
||||
<text x="113" y="220" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="500" fill="#667085" text-anchor="start">
|
||||
<tspan x="113" dy="0">고정된 Gradle</tspan>
|
||||
<tspan x="113" dy="17.4">빌드 스크립트 전체</tspan>
|
||||
</text>
|
||||
<circle cx="605" cy="250" r="92" fill="#EAF3FF" stroke="#1677FF" stroke-width="2" />
|
||||
<text x="605" y="240" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="32" font-weight="800" fill="#0B5CC4" text-anchor="middle">0건</text>
|
||||
<text x="605" y="275" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#0B5CC4" text-anchor="middle">Spring Modulith</text>
|
||||
<text x="605" y="300" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="600" fill="#667085" text-anchor="middle">의존 선언</text>
|
||||
<path d="M 380 250 L 513 250" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="414.4" y="225" width="64.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="446.5" y="241" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">전체 검색</text>
|
||||
<path d="M 915 75 L 915 485" fill="none" stroke="#AEB5BF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 6" />
|
||||
<rect x="880.6" y="88" width="68.8" height="21" fill="#FFFFFF" rx="2" />
|
||||
<text x="915" y="105" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#667085" text-anchor="middle">판단 경계</text>
|
||||
<rect x="985" y="115" width="390" height="320" fill="#FBFCFE" stroke="#7556D8" stroke-width="1.3" rx="10" stroke-dasharray="7 6" />
|
||||
<rect x="994" y="104" width="64.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1001" y="120" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="start">별도 평가</text>
|
||||
<rect x="1040" y="205" width="280" height="100" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1180" y="249" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">채택 여부</text>
|
||||
<text x="1180" y="273" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">
|
||||
<tspan x="1180" dy="0">별도 근거로</tspan>
|
||||
<tspan x="1180" dy="16.2">조건부 평가</tspan>
|
||||
</text>
|
||||
<path d="M 697 250 L 870 250 L 870 365 L 1040 365" fill="none" stroke="#7556D8" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" />
|
||||
<path d="M 901 351 L 929 379" fill="none" stroke="#7556D8" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 929 351 L 901 379" fill="none" stroke="#7556D8" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="1180" y="375" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#667085" text-anchor="middle">0건만으로 채택 결론을 내리지 않음</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="780" viewBox="0 0 1500 780" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Complementary enforcement scopes</title>
|
||||
<desc id="desc">Five partially overlapping enforcement scopes surround boundary violations without implying a fixed order or speed ranking.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1500" height="780" fill="#FFFFFF"/>
|
||||
<rect x="545" y="25" width="276.5" height="30" fill="#F6F7F9" stroke="#AEB5BF" stroke-width="1.2" rx="15" />
|
||||
<text x="683.2" y="45" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#667085" text-anchor="middle">독립·보완 범위 · 고정 실행 순서 없음</text>
|
||||
<circle cx="750" cy="420" r="105" fill="#FDEDEF" stroke="#D64550" stroke-width="1.8" />
|
||||
<text x="750" y="410" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="20" font-weight="800" fill="#D64550" text-anchor="middle">경계 위반</text>
|
||||
<text x="750" y="440" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="600" fill="#667085" text-anchor="middle">종류별 검출 표면</text>
|
||||
<ellipse cx="470" cy="275" rx="255" ry="150" fill="none" stroke="#1677FF" stroke-width="1.8" />
|
||||
<rect x="443.1" y="131" width="53.9" height="23" fill="#FFFFFF" stroke="#1677FF" stroke-width="1" rx="2" />
|
||||
<text x="470" y="150" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="650" fill="#1677FF" text-anchor="middle">javac</text>
|
||||
<text x="470" y="178" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="600" fill="#667085" text-anchor="middle">클래스패스 범위</text>
|
||||
<ellipse cx="745" cy="225" rx="285" ry="135" fill="none" stroke="#1677FF" stroke-width="1.8" />
|
||||
<rect x="714.1" y="96" width="61.9" height="23" fill="#FFFFFF" stroke="#1677FF" stroke-width="1" rx="2" />
|
||||
<text x="745" y="115" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="650" fill="#1677FF" text-anchor="middle">Gradle</text>
|
||||
<text x="745" y="143" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="600" fill="#667085" text-anchor="middle">project dependency 범위</text>
|
||||
<ellipse cx="950" cy="415" rx="285" ry="145" fill="none" stroke="#1677FF" stroke-width="1.8" />
|
||||
<rect x="1011.1" y="281" width="77.8" height="23" fill="#FFFFFF" stroke="#1677FF" stroke-width="1" rx="2" />
|
||||
<text x="1050" y="300" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="650" fill="#1677FF" text-anchor="middle">ArchUnit</text>
|
||||
<text x="1050" y="328" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="600" fill="#667085" text-anchor="middle">구조 규칙 범위</text>
|
||||
<ellipse cx="730" cy="590" rx="300" ry="120" fill="none" stroke="#1677FF" stroke-width="1.8" />
|
||||
<rect x="671.1" y="671" width="117.7" height="23" fill="#FFFFFF" stroke="#1677FF" stroke-width="1" rx="2" />
|
||||
<text x="730" y="690" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="650" fill="#1677FF" text-anchor="middle">test-the-test</text>
|
||||
<text x="730" y="718" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="600" fill="#667085" text-anchor="middle">비공허성 범위</text>
|
||||
<ellipse cx="320" cy="485" rx="310" ry="145" fill="none" stroke="#7556D8" stroke-width="1.8" />
|
||||
<rect x="193.0" y="351" width="114.0" height="23" fill="#FFFFFF" stroke="#7556D8" stroke-width="1" rx="2" />
|
||||
<text x="250" y="370" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="650" fill="#7556D8" text-anchor="middle">리뷰 · 런타임</text>
|
||||
<text x="250" y="398" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="600" fill="#667085" text-anchor="middle">정적 규칙 밖 범위</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="620" viewBox="0 0 1440 620" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Feed query ports and adapters</title>
|
||||
<desc id="desc">FeedController calls the concrete GetFeedUseCase; the application core owns FeedQueryPort, implemented by FeedQueryAdapter.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1440" height="620" fill="#FFFFFF"/>
|
||||
<polygon points="490,75 880,75 980,310 880,545 490,545 390,310" fill="#EAF3FF" stroke="#1677FF" stroke-width="2.3" />
|
||||
<text x="685" y="125" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#0B5CC4" text-anchor="middle" letter-spacing="0.8">application-core</text>
|
||||
<rect x="540" y="205" width="290" height="120" fill="#FFFFFF" stroke="#0B5CC4" stroke-width="1.8" rx="8" />
|
||||
<text x="685" y="259" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">GetFeedUseCase</text>
|
||||
<text x="685" y="283" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">
|
||||
<tspan x="685" dy="0">concrete service</tspan>
|
||||
<tspan x="685" dy="16.2">implements QueryUseCase<Q,R></tspan>
|
||||
</text>
|
||||
<circle cx="400" cy="310" r="10" fill="#FFFFFF" stroke="#1677FF" stroke-width="2" />
|
||||
<circle cx="970" cy="310" r="10" fill="#FFFFFF" stroke="#1677FF" stroke-width="2" />
|
||||
<text x="410" y="285" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="start">input boundary</text>
|
||||
<text x="960" y="285" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#0B5CC4" text-anchor="end">FeedQueryPort</text>
|
||||
<rect x="55" y="245" width="280" height="105" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="195" y="291.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">FeedController</text>
|
||||
<text x="195" y="316.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">driving adapter</text>
|
||||
<rect x="1060" y="245" width="300" height="105" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="1210" y="291.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">FeedQueryAdapter</text>
|
||||
<text x="1210" y="316.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">driven adapter · implements port</text>
|
||||
<path d="M 335 298 L 390 310" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="309.1" y="264" width="101.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="360" y="280" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Calls concrete</text>
|
||||
<path d="M 830 265 L 925 265 L 925 310 L 960 310" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="839.7" y="232" width="120.6" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="900" y="248" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Calls output port</text>
|
||||
<path d="M 1060 298 L 980 310" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" marker-end="url(#arrow-purple)" />
|
||||
<rect x="986.6" y="264" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1025" y="280" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Implements</text>
|
||||
<rect x="570" y="385" width="318.5" height="30" fill="#FFFFFF" stroke="#A8BCE0" stroke-width="1.2" rx="15" />
|
||||
<text x="729.2" y="405" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#667085" text-anchor="middle">QueryUseCase<Q,R>는 별도 객체가 아닌 구현 계약</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1640" height="1060" viewBox="0 0 1640 1060" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Idempotency execution branches</title>
|
||||
<desc id="desc">One deadline feeds two waiting entry points and four normal decisions; a separate claimed-execution lane shows RuntimeException cleanup outcomes.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1640" height="1060" fill="#FFFFFF"/>
|
||||
<rect x="25" y="35" width="1590" height="625" fill="#FBFCFE" stroke="#AEB5BF" stroke-width="1.3" rx="10" />
|
||||
<rect x="34" y="24" width="299.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="41" y="40" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="start">정상 결정 · 두 대기 진입점과 단일 200ms deadline</text>
|
||||
<rect x="55" y="95" width="260" height="72" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="185" y="136" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">execute(context, action, codec)</text>
|
||||
<rect x="360" y="115" width="178.0" height="30" fill="#FFF7E5" stroke="#B7791F" stroke-width="1.2" rx="15" />
|
||||
<text x="449.0" y="135" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#B7791F" text-anchor="middle">deadline = 시작 + 200ms</text>
|
||||
<rect x="625" y="95" width="210" height="72" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="730" y="136" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">store.find(scope, now)</text>
|
||||
<path d="M 315 131 L 360 131" fill="none" stroke="#B7791F" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-amber)" />
|
||||
<rect x="302.2" y="106" width="70.5" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="337.5" y="122" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#B7791F" text-anchor="middle">초기화 1회</text>
|
||||
<path d="M 560 131 L 625 131" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="566.7" y="106" width="51.6" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="592.5" y="122" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">lookup</text>
|
||||
<polygon points="930,85 1015,131 930,177 845,131" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.6" />
|
||||
<text x="930" y="124.4" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">
|
||||
<tspan x="930" dy="0">record</tspan>
|
||||
<tspan x="930" dy="15">존재?</tspan>
|
||||
</text>
|
||||
<path d="M 835 131 L 845 131" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<polygon points="1170,160 1260,210 1170,260 1080,210" fill="#FFFFFF" stroke="#7556D8" stroke-width="1.6" />
|
||||
<text x="1170" y="203.4" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">
|
||||
<tspan x="1170" dy="0">fingerprint</tspan>
|
||||
<tspan x="1170" dy="15">일치?</tspan>
|
||||
</text>
|
||||
<polygon points="930,270 1020,320 930,370 840,320" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.6" />
|
||||
<text x="930" y="313.4" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">
|
||||
<tspan x="930" dy="0">tryBegin</tspan>
|
||||
<tspan x="930" dy="15">성공?</tspan>
|
||||
</text>
|
||||
<path d="M 1015 145 L 1100 190" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1028.6" y="142.5" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1057.5" y="158.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Present</text>
|
||||
<path d="M 930 177 L 930 270" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="904.2" y="198.5" width="51.6" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="930" y="214.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Absent</text>
|
||||
<rect x="1375" y="120" width="205" height="70" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="1477.5" y="149" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">fingerprint-mismatch</text>
|
||||
<text x="1477.5" y="174" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">422</text>
|
||||
<path d="M 1260 190 L 1375 155" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-red)" />
|
||||
<rect x="1285.4" y="147.5" width="64.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1317.5" y="163.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="middle">Mismatch</text>
|
||||
<polygon points="1170,304 1255,350 1170,396 1085,350" fill="#FFFFFF" stroke="#7556D8" stroke-width="1.6" />
|
||||
<text x="1170" y="343.4" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">
|
||||
<tspan x="1170" dy="0">record</tspan>
|
||||
<tspan x="1170" dy="15">status</tspan>
|
||||
</text>
|
||||
<path d="M 1170 260 L 1170 304" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1147.3" y="257" width="45.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1170" y="273" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Match</text>
|
||||
<rect x="1375" y="300" width="205" height="70" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="1477.5" y="329" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">replay-hit</text>
|
||||
<text x="1477.5" y="354" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">action 0회</text>
|
||||
<path d="M 1255 335 L 1375 335" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-green)" />
|
||||
<rect x="1279.8" y="310" width="70.4" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1315" y="326" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">COMPLETED</text>
|
||||
<polygon points="1170,452 1257.5,500 1170,548 1082.5,500" fill="#FFF7E5" stroke="#B7791F" stroke-width="1.6" />
|
||||
<text x="1170" y="493.4" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#B7791F" text-anchor="middle">
|
||||
<tspan x="1170" dy="0">now <</tspan>
|
||||
<tspan x="1170" dy="15">deadline?</tspan>
|
||||
</text>
|
||||
<path d="M 1170 396 L 1170 452" fill="none" stroke="#B7791F" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-amber)" />
|
||||
<rect x="1134.8" y="399" width="70.4" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1170" y="415" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#B7791F" text-anchor="middle">IN_FLIGHT</text>
|
||||
<path d="M 1020 320 L 1082 480" fill="none" stroke="#B7791F" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-amber)" />
|
||||
<rect x="1012.6" y="375" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1051" y="391" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#B7791F" text-anchor="middle">Claim lost</text>
|
||||
<rect x="1375" y="465" width="205" height="70" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="1477.5" y="494" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">in-flight</text>
|
||||
<text x="1477.5" y="519" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">409</text>
|
||||
<path d="M 1258 500 L 1375 500" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-red)" />
|
||||
<rect x="1259.3" y="475" width="114.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1316.5" y="491" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="middle">Deadline reached</text>
|
||||
<rect x="1010" y="565" width="210" height="56" fill="#FFF7E5" stroke="#B7791F" stroke-width="1.5" rx="8" />
|
||||
<text x="1115" y="598" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">20ms 대기 후 재조회</text>
|
||||
<path d="M 1170 548 L 1115 565" fill="none" stroke="#B7791F" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-amber)" />
|
||||
<rect x="1088.5" y="531.5" width="108.0" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1142.5" y="547.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#B7791F" text-anchor="middle">Before deadline</text>
|
||||
<path d="M 1010 593 L 740 593 L 740 167" fill="none" stroke="#B7791F" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" marker-end="url(#arrow-ink)" />
|
||||
<rect x="805.4" y="567" width="89.2" height="20" fill="#FBFCFE" rx="2" />
|
||||
<text x="850" y="583" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#B7791F" text-anchor="middle">Retry lookup</text>
|
||||
<rect x="25" y="715" width="1590" height="300" fill="#FBFCFE" stroke="#1677FF" stroke-width="1.3" rx="10" />
|
||||
<rect x="34" y="704" width="305.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="41" y="720" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="start">클레임 후 실행 · RuntimeException과 discard 결과</text>
|
||||
<rect x="70" y="795" width="220" height="76" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="180" y="838" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">action.get()</text>
|
||||
<rect x="330" y="795" width="220" height="76" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="440" y="838" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">codec.serialize(result)</text>
|
||||
<rect x="630" y="795" width="220" height="76" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="740" y="838" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">store.complete(...)</text>
|
||||
<rect x="925" y="795" width="220" height="76" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="1035" y="827" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">new · action 1회</text>
|
||||
<text x="1035" y="852" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">응답 저장</text>
|
||||
<path d="M 290 833 L 330 833" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-green)" />
|
||||
<rect x="281.1" y="808" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="310" y="824" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">Success</text>
|
||||
<path d="M 550 833 L 630 833" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-green)" />
|
||||
<rect x="561.1" y="808" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="590" y="824" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">Success</text>
|
||||
<path d="M 850 833 L 925 833" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-green)" />
|
||||
<rect x="858.6" y="808" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="887.5" y="824" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">Success</text>
|
||||
<path d="M 930 370 L 930 700 L 180 700 L 180 795" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="504.8" y="674" width="70.4" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="540" y="690" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Claim won</text>
|
||||
<rect x="1210" y="760" width="180" height="70" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="1300" y="800" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">RuntimeException</text>
|
||||
<rect x="1210" y="900" width="180" height="70" fill="#FFFFFF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="1300" y="940" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">store.discard(scope)</text>
|
||||
<path d="M 180 871 L 180 935 L 1190 935 L 1190 795 L 1210 795" fill="none" stroke="#D64550" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" />
|
||||
<path d="M 440 871 L 440 935 L 1190 935 L 1190 795 L 1210 795" fill="none" stroke="#D64550" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" />
|
||||
<path d="M 740 871 L 740 935 L 1190 935 L 1190 795 L 1210 795" fill="none" stroke="#D64550" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" />
|
||||
<path d="M 1300 830 L 1300 900" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-red)" />
|
||||
<rect x="1271.1" y="840" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1300" y="856" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="middle">cleanup</text>
|
||||
<rect x="1430" y="835" width="170" height="65" fill="#FFFFFF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="1515" y="861.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">원래 예외 재전파</text>
|
||||
<text x="1515" y="886.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">discard 성공</text>
|
||||
<rect x="1430" y="930" width="170" height="65" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="1515" y="956.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="700" fill="#25282D" text-anchor="middle">discard 예외 대체 가능</text>
|
||||
<text x="1515" y="981.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">정리 불확실</text>
|
||||
<path d="M 1390 930 L 1430 867" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-red)" />
|
||||
<rect x="1381.1" y="873.5" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1410" y="889.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="middle">returns</text>
|
||||
<path d="M 1390 950 L 1430 962" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-red)" />
|
||||
<rect x="1384.2" y="931" width="51.6" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1410" y="947" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="middle">throws</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="978" height="673" viewBox="0 0 978 673" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">전송 타입은 inbound adapter에서 Command·Query로 수렴한다</title>
|
||||
<desc id="diagram-description">왼쪽에서 오른쪽으로 읽는다. web은 HTTP DTO, grpc는 protobuf message, graphql은 GraphQL request, websocket은 WebSocket message를 각 어댑터 경계에서 처리한다. 네 어댑터는 전송 기술 타입을 application-core로 넘기지 않고 Command 또는 Query로 변환한다. 변환된 입력만 Application use case를 호출한다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"inbound-transport-boundary","profile":"component-flow"},"source_context":{"document":"/home/donghyeon/workspace/ai-tool/topic-arrange/executable-clean-architecture/claridoc-rewrite/document.md","document_sha256":"04fbab095d33d301746c34f7cca305730919bad3c341bcf63b8ad3ee3b396d31","anchor":{"kind":"marker","value":"inbound-transport-boundary","line":336}},"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="978" height="673" />
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="599.0,344.0 679.0,344.0 679.0,344.0 759.0,344.0" data-evidence="322-328" />
|
||||
<rect class="edge-label-bg" x="643.2" y="302.0" width="71.6" height="22" rx="3" />
|
||||
<text class="edge-label" x="679.0" y="317.0">유스케이스 호출</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="253.0,424.0 349.5,424.0 349.5,351.3 446.0,351.3" data-evidence="322-327" />
|
||||
<rect class="edge-label-bg" x="341.1" y="373.7" width="64.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="373.5" y="388.7">GraphQL</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="253.0,264.0 349.5,264.0 349.5,336.7 446.0,336.7" data-evidence="322-327" />
|
||||
<rect class="edge-label-bg" x="337.7" y="286.3" width="71.6" height="22" rx="3" />
|
||||
<text class="edge-label" x="373.5" y="301.3">Protobuf</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="286.0,104.0 366.0,104.0 366.0,322.0 446.0,322.0" data-evidence="322-327" />
|
||||
<rect class="edge-label-bg" x="354.2" y="199.0" width="71.6" height="22" rx="3" />
|
||||
<text class="edge-label" x="390.0" y="214.0">HTTP DTO</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="254.5,584.0 350.2,584.0 350.2,366.0 446.0,366.0" data-evidence="322-328" />
|
||||
<rect class="edge-label-bg" x="335.1" y="461.0" width="78.3" height="22" rx="3" />
|
||||
<text class="edge-label" x="374.2" y="476.0">WebSocket</text>
|
||||
<g id="node-web">
|
||||
<rect class="node-shape kind-adapter emphasis-normal role-source" data-evidence="315-317" x="70.0" y="60.0" width="216.0" height="88.0" rx="7" />
|
||||
<text class="node-label" x="178.0" y="87.0">web</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="108.0" x2="272.0" y2="108.0" />
|
||||
<text class="node-detail" x="86.0" y="125.0">HTTP · JSON DTO</text>
|
||||
<text class="node-detail" x="86.0" y="141.0">validation · auth · errors</text>
|
||||
</g>
|
||||
<g id="node-grpc">
|
||||
<rect class="node-shape kind-adapter emphasis-normal role-source" data-evidence="315-318" x="103.0" y="220.0" width="150.0" height="88.0" rx="7" />
|
||||
<text class="node-label" x="178.0" y="247.0">grpc</text>
|
||||
<line class="node-detail-divider" x1="117.0" y1="268.0" x2="239.0" y2="268.0" />
|
||||
<text class="node-detail" x="119.0" y="285.0">Protobuf message</text>
|
||||
<text class="node-detail" x="119.0" y="301.0">server lifecycle</text>
|
||||
</g>
|
||||
<g id="node-graphql">
|
||||
<rect class="node-shape kind-adapter emphasis-normal role-source" data-evidence="315-319" x="103.0" y="380.0" width="150.0" height="88.0" rx="7" />
|
||||
<text class="node-label" x="178.0" y="407.0">graphql</text>
|
||||
<line class="node-detail-divider" x1="117.0" y1="428.0" x2="239.0" y2="428.0" />
|
||||
<text class="node-detail" x="119.0" y="445.0">GraphQL request</text>
|
||||
<text class="node-detail" x="119.0" y="461.0">query · mutation</text>
|
||||
</g>
|
||||
<g id="node-websocket">
|
||||
<rect class="node-shape kind-adapter emphasis-normal role-source" data-evidence="315-320" x="101.5" y="540.0" width="153.0" height="88.0" rx="7" />
|
||||
<text class="node-label" x="178.0" y="567.0">websocket</text>
|
||||
<line class="node-detail-divider" x1="115.5" y1="588.0" x2="240.5" y2="588.0" />
|
||||
<text class="node-detail" x="117.5" y="605.0">WebSocket message</text>
|
||||
<text class="node-detail" x="117.5" y="621.0">STOMP · realtime</text>
|
||||
</g>
|
||||
<g id="node-command-query">
|
||||
<rect class="node-shape kind-interface emphasis-primary role-service" data-evidence="322-328" x="446.0" y="308.5" width="153.0" height="71.0" rx="22" />
|
||||
<text class="node-label" x="522.5" y="335.5">Command / Query</text>
|
||||
<line class="node-detail-divider" x1="460.0" y1="356.5" x2="585.0" y2="356.5" />
|
||||
<text class="node-detail" x="462.0" y="373.5">application input</text>
|
||||
</g>
|
||||
<g id="node-use-case">
|
||||
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="322-328" x="759.0" y="308.5" width="174.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="846.0" y="335.5">Application use case</text>
|
||||
<line class="node-detail-divider" x1="773.0" y1="356.5" x2="919.0" y2="356.5" />
|
||||
<text class="node-detail" x="775.0" y="373.5">transport type 없음</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1480" height="720" viewBox="0 0 1480 720" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Lock timeout routing gap</title>
|
||||
<desc id="desc">The adapter contract declares a lock timeout and a classifier maps its code to HTTP 409, but zero production callers and zero dedicated web handlers leave that route disconnected.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1480" height="720" fill="#FFFFFF"/>
|
||||
<rect x="40" y="45" width="930" height="300" fill="#FBFCFE" stroke="#AEB5BF" stroke-width="1.3" rx="10" />
|
||||
<rect x="49" y="34" width="131.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="56" y="50" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="start">계약과 분류 · 존재함</text>
|
||||
<rect x="80" y="145" width="250" height="90" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="205" y="184" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">DistributedLockPort</text>
|
||||
<text x="205" y="209" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">adapter 계약</text>
|
||||
<rect x="385" y="145" width="260" height="90" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="515" y="195" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">LockAcquisitionTimeoutException</text>
|
||||
<rect x="700" y="145" width="230" height="90" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="815" y="195" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">CONCURRENCY_LOCK_TIMEOUT</text>
|
||||
<path d="M 330 190 L 385 190" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="300.3" y="165" width="114.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="357.5" y="181" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">Adapter contract</text>
|
||||
<path d="M 645 190 L 700 190" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="640.4" y="165" width="64.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="672.5" y="181" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Declares</text>
|
||||
<rect x="1015" y="45" width="420" height="300" fill="#FBFCFE" stroke="#AEB5BF" stroke-width="1.3" rx="10" />
|
||||
<rect x="1024" y="34" width="67.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1031" y="50" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="start">HTTP 분류</text>
|
||||
<rect x="1060" y="145" width="330" height="90" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="1225" y="184" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">LOCK_TIMEOUT → HTTP 409</text>
|
||||
<text x="1225" y="209" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">분류 계약</text>
|
||||
<path d="M 930 190 L 1060 190" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="956.6" y="165" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="995" y="181" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Classifies</text>
|
||||
<rect x="110" y="430" width="320" height="100" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="270" y="474" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">프로덕션 애플리케이션/유스케이스</text>
|
||||
<text x="270" y="499" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">호출자 0</text>
|
||||
<rect x="560" y="430" width="270" height="100" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="695" y="474" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">전용 웹 핸들러</text>
|
||||
<text x="695" y="499" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">0</text>
|
||||
<rect x="1040" y="430" width="340" height="100" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="1210" y="474" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">현재 HTTP 409</text>
|
||||
<text x="1210" y="499" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">보장 없음</text>
|
||||
<path d="M 430 480 L 500 480 L 500 275 L 515 275 L 515 235" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" />
|
||||
<path d="M 482 337 L 518 373" fill="none" stroke="#D64550" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 518 337 L 482 373" fill="none" stroke="#D64550" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<rect x="450.2" y="308" width="99.7" height="21" fill="#FFFFFF" rx="2" />
|
||||
<text x="500" y="325" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#D64550" text-anchor="middle">현재 연결 없음</text>
|
||||
<path d="M 830 480 L 940 480 L 940 275 L 1225 275 L 1225 235" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" />
|
||||
<path d="M 922 387 L 958 423" fill="none" stroke="#D64550" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 958 387 L 922 423" fill="none" stroke="#D64550" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<rect x="898.8" y="358" width="82.4" height="21" fill="#FFFFFF" rx="2" />
|
||||
<text x="940" y="375" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#D64550" text-anchor="middle">Not routed</text>
|
||||
<rect x="560" y="600" width="360" height="70" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="740" y="629" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">GlobalExceptionHandler</text>
|
||||
<text x="740" y="654" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">Exception fallback</text>
|
||||
<path d="M 920 635 L 1040 500" fill="none" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-muted)" />
|
||||
<rect x="949.7" y="574" width="120.6" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1010" y="590" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="middle">Fallback response</text>
|
||||
<path d="M 972 462 L 1008 498" fill="none" stroke="#D64550" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 1008 462 L 972 498" fill="none" stroke="#D64550" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.6 KiB |
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="760" viewBox="0 0 1440 760" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Logical ownership rings</title>
|
||||
<desc id="desc">Project modules depend inward while adapter-owned framework surfaces and bootstrap-owned composition surfaces remain distinct.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1440" height="760" fill="#FFFFFF"/>
|
||||
<rect x="70" y="55" width="1300" height="640" fill="#FBFCFE" stroke="#7E8792" stroke-width="1.5" rx="18" />
|
||||
<rect x="98" y="45" width="123.7" height="21" fill="#FFFFFF" rx="2" />
|
||||
<text x="105" y="62" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#667085" text-anchor="start">프로젝트 소유 표면</text>
|
||||
<rect x="265" y="145" width="910" height="475" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.8" rx="18" />
|
||||
<rect x="293" y="135" width="99.7" height="21" fill="#FFFFFF" rx="2" />
|
||||
<text x="300" y="152" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#0B5CC4" text-anchor="start">안쪽 모듈 경계</text>
|
||||
<rect x="460" y="250" width="520" height="270" fill="#EAF3FF" stroke="#0B5CC4" stroke-width="2" rx="18" />
|
||||
<rect x="565" y="300" width="310" height="82" fill="#FFFFFF" stroke="#0B5CC4" stroke-width="1.5" rx="8" />
|
||||
<text x="720" y="335" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">application-core</text>
|
||||
<text x="720" y="360" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">Spring DI · SLF4J</text>
|
||||
<rect x="565" y="420" width="310" height="68" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="720" y="448" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">domain-core</text>
|
||||
<text x="720" y="473" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">main 외부 의존 0</text>
|
||||
<rect x="95" y="210" width="280" height="105" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="235" y="256.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">web adapter</text>
|
||||
<text x="235" y="280.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">
|
||||
<tspan x="235" dy="0">MVC · Security</tspan>
|
||||
<tspan x="235" dy="16.2">Validation</tspan>
|
||||
</text>
|
||||
<rect x="95" y="410" width="280" height="105" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="235" y="456.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">persistence adapter</text>
|
||||
<text x="235" y="480.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">
|
||||
<tspan x="235" dy="0">JPA · PostgreSQL</tspan>
|
||||
<tspan x="235" dy="16.2">DB 구체 의존</tspan>
|
||||
</text>
|
||||
<rect x="1065" y="280" width="250" height="150" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="1190" y="349" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">app-bootstrap</text>
|
||||
<text x="1190" y="373" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">
|
||||
<tspan x="1190" dy="0">Boot · Flyway · 관측</tspan>
|
||||
<tspan x="1190" dy="16.2">Security 조립</tspan>
|
||||
</text>
|
||||
<path d="M 375 262 L 480 262 L 480 325 L 565 325" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="406.6" y="234" width="126.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="470" y="250" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Project dependency</text>
|
||||
<path d="M 375 462 L 500 462 L 500 360 L 565 360" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="414.6" y="434" width="126.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="478" y="450" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Project dependency</text>
|
||||
<path d="M 1065 355 L 950 355 L 950 340 L 875 340" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="906.6" y="314" width="126.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="970" y="330" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Project dependency</text>
|
||||
<path d="M 720 382 L 720 420" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-green)" />
|
||||
<rect x="656.6" y="376" width="126.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="720" y="392" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">Project dependency</text>
|
||||
<rect x="590" y="555" width="127.7" height="30" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.2" rx="15" />
|
||||
<text x="653.9" y="575" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#168A5B" text-anchor="middle">DOMAIN_IS_PURE</text>
|
||||
<path d="M 720 555 L 720 488" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-green)" />
|
||||
<rect x="751.0" y="521" width="108.0" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="805" y="537" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">Enforces purity</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="960" viewBox="0 0 1600 960" role="img" aria-labelledby="title desc">
|
||||
<title id="title">MDC request and asynchronous lifecycle</title>
|
||||
<desc id="desc">The primary request lifecycle, configured task decorator propagation, cleanup failure window, and unsupported executor path are separated.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1600" height="960" fill="#FFFFFF"/>
|
||||
<rect x="35" y="40" width="1530" height="320" fill="#FBFCFE" stroke="#AEB5BF" stroke-width="1.3" rx="10" />
|
||||
<rect x="44" y="29" width="149.1" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="51" y="45" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="start">요청 스레드 · 정리 전제</text>
|
||||
<rect x="70" y="145" width="185" height="78" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="162.5" y="178" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">요청 헤더</text>
|
||||
<text x="162.5" y="203" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">traceparent</text>
|
||||
<rect x="300" y="145" width="185" height="78" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="392.5" y="189" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">살균·채택·생성</text>
|
||||
<rect x="540" y="145" width="185" height="78" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="632.5" y="178" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">요청 MDC</text>
|
||||
<text x="632.5" y="203" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">5키 put</text>
|
||||
<rect x="780" y="145" width="185" height="78" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="872.5" y="189" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">요청 처리</text>
|
||||
<rect x="1020" y="145" width="185" height="78" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="1112.5" y="178" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">사용자 가명화</text>
|
||||
<text x="1112.5" y="203" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">MDC에는 가명만</text>
|
||||
<rect x="1260" y="145" width="185" height="78" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="1352.5" y="189" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">http_request log</text>
|
||||
<rect x="1460" y="145" width="120" height="78" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="1520" y="178" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">5키 remove</text>
|
||||
<text x="1520" y="203" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">앞 단계 완료 시</text>
|
||||
<path d="M 255 184 L 300 184" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<path d="M 485 184 L 540 184" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<path d="M 725 184 L 780 184" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<path d="M 965 184 L 1020 184" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<path d="M 1205 184 L 1260 184" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<path d="M 1445 184 L 1460 184" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="179.8" y="154" width="70.4" height="20" fill="#FBFCFE" rx="2" />
|
||||
<text x="215" y="170" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="middle">Normalize</text>
|
||||
<rect x="488.6" y="154" width="32.8" height="20" fill="#FBFCFE" rx="2" />
|
||||
<text x="505" y="170" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="middle">Put</text>
|
||||
<rect x="901.1" y="154" width="57.9" height="20" fill="#FBFCFE" rx="2" />
|
||||
<text x="930" y="170" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="middle">finally</text>
|
||||
<rect x="545" y="265" width="250" height="58" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="670" y="299" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">응답 헤더 · Envelope meta</text>
|
||||
<rect x="830" y="265" width="250" height="58" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="955" y="288" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">OutboundCorrelation</text>
|
||||
<text x="955" y="313" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">같은 스레드에서 read</text>
|
||||
<rect x="1110" y="279" width="130.1" height="30" fill="#FFF7E5" stroke="#B7791F" stroke-width="1.2" rx="15" />
|
||||
<text x="1175.0" y="299" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#B7791F" text-anchor="middle">MDC 비면 UNKNOWN</text>
|
||||
<path d="M 632 223 L 670 265" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="596.1" y="238" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="625" y="254" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Project</text>
|
||||
<path d="M 725 223 L 930 265" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="805.5" y="232" width="39.1" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="825" y="248" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Read</text>
|
||||
<path d="M 1080 294 L 1110 294" fill="none" stroke="#B7791F" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-amber)" />
|
||||
<rect x="1062.9" y="269" width="64.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1095" y="285" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#B7791F" text-anchor="middle">Fallback</text>
|
||||
<rect x="1010" y="385" width="515" height="92" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="10" />
|
||||
<text x="1035" y="420" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="800" fill="#D64550" text-anchor="start">정리 실패 창</text>
|
||||
<text x="1035" y="450" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="650" fill="#25282D" text-anchor="start">가명화 또는 log 실패 시 5키 제거 보장 없음</text>
|
||||
<path d="M 1110 223 L 1110 385" fill="none" stroke="#D64550" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" />
|
||||
<path d="M 1340 223 L 1340 385" fill="none" stroke="#D64550" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" />
|
||||
<rect x="35" y="530" width="1110" height="370" fill="#FBFCFE" stroke="#1677FF" stroke-width="1.3" rx="10" />
|
||||
<rect x="44" y="519" width="282.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="51" y="535" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="start">구성된 비동기 경계 · applicationTaskExecutor</text>
|
||||
<rect x="55" y="625" width="230" height="75" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="170" y="656.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">applicationTaskExecutor</text>
|
||||
<text x="170" y="681.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">configured</text>
|
||||
<rect x="315" y="625" width="210" height="75" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="420" y="667.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">AsyncContextTaskDecorator</text>
|
||||
<rect x="580" y="575" width="210" height="75" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="685" y="606.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">caller MDC 캡처</text>
|
||||
<text x="685" y="631.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">제출 시</text>
|
||||
<rect x="580" y="705" width="210" height="75" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="685" y="736.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">worker 이전 MDC</text>
|
||||
<text x="685" y="761.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">보관</text>
|
||||
<rect x="850" y="625" width="220" height="75" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="960" y="656.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">task 동안 caller MDC</text>
|
||||
<text x="960" y="681.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">설치</text>
|
||||
<path d="M 285 662 L 315 662" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="246.0" y="634" width="108.0" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="300" y="650" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">Configured with</text>
|
||||
<path d="M 525 650 L 580 612" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="523.6" y="606" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="552.5" y="622" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Capture</text>
|
||||
<path d="M 525 675 L 580 742" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="533.0" y="683.5" width="39.1" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="552.5" y="699.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Save</text>
|
||||
<path d="M 790 612 L 850 650" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="791.1" y="606" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="820" y="622" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Install</text>
|
||||
<path d="M 790 742 L 850 688" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-blue)" />
|
||||
<rect x="787.9" y="690" width="64.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="820" y="706" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Preserve</text>
|
||||
<rect x="850" y="790" width="220" height="70" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="960" y="819" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">이전 worker MDC 복원</text>
|
||||
<text x="960" y="844" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">finally</text>
|
||||
<path d="M 960 700 L 960 790" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-green)" />
|
||||
<rect x="931.1" y="720" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="960" y="736" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">Restore</text>
|
||||
<path d="M 632 223 L 632 500 L 170 500 L 170 625" fill="none" stroke="#1677FF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" />
|
||||
<rect x="286.4" y="477" width="227.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="400" y="493" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="middle">Submit through configured executor</text>
|
||||
<rect x="1190" y="530" width="375" height="370" fill="#FBFCFE" stroke="#D64550" stroke-width="1.3" rx="10" stroke-dasharray="7 6" />
|
||||
<rect x="1199" y="519" width="120.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1206" y="535" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="start">구성 밖 비동기 경계</text>
|
||||
<rect x="1230" y="640" width="295" height="80" fill="#FFFFFF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="1377.5" y="685" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">원시 스레드 · 다른 executor</text>
|
||||
<rect x="1230" y="790" width="295" height="70" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="1377.5" y="830" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">MDC 자동 복사 없음</text>
|
||||
<path d="M 1377 720 L 1377 790" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" marker-end="url(#arrow-red)" />
|
||||
<rect x="1313.6" y="730" width="126.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1377" y="746" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="middle">Does not auto-copy</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1480" height="780" viewBox="0 0 1480 780" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Measured module policy excerpt</title>
|
||||
<desc id="desc">Five centered source rows point to allowed targets on each side, exposing asymmetric access to domain-core, shared-contract, and support.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1480" height="780" fill="#FFFFFF"/>
|
||||
<rect x="480" y="25" width="296.9" height="30" fill="#F6F7F9" stroke="#AEB5BF" stroke-width="1.2" rx="15" />
|
||||
<text x="628.4" y="45" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#667085" text-anchor="middle">정책 비대칭 선택 발췌 · 전체 그래프 아님</text>
|
||||
<rect x="55" y="120" width="270" height="68" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="190" y="159" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">domain-core</text>
|
||||
<rect x="475" y="112" width="530" height="84" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="740" y="148" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">support · 공유 기반</text>
|
||||
<text x="740" y="173" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">source</text>
|
||||
<rect x="1155" y="120" width="270" height="68" fill="#FFFFFF" stroke="#68717B" stroke-width="1.5" rx="8" />
|
||||
<text x="1290" y="159" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">application-core</text>
|
||||
<path d="M 475 154 L 325 154" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="371.1" y="129" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="400" y="145" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Allowed</text>
|
||||
<path d="M 1005 154 L 1155 154" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="1051.1" y="129" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1080" y="145" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Allowed</text>
|
||||
<rect x="55" y="245" width="270" height="68" fill="#FFFFFF" stroke="#68717B" stroke-width="1.5" rx="8" />
|
||||
<text x="190" y="284" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">application-core</text>
|
||||
<rect x="475" y="237" width="530" height="84" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="740" y="273" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">messaging · cache · notification · httpclient</text>
|
||||
<text x="740" y="298" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">source</text>
|
||||
<rect x="1155" y="245" width="270" height="68" fill="#FFFFFF" stroke="#68717B" stroke-width="1.5" rx="8" />
|
||||
<text x="1290" y="284" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">support</text>
|
||||
<path d="M 475 279 L 325 279" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="371.1" y="254" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="400" y="270" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Allowed</text>
|
||||
<path d="M 1005 279 L 1155 279" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="1051.1" y="254" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1080" y="270" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Allowed</text>
|
||||
<rect x="55" y="370" width="270" height="68" fill="#FFFFFF" stroke="#68717B" stroke-width="1.5" rx="8" />
|
||||
<text x="190" y="409" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">application-core</text>
|
||||
<rect x="475" y="362" width="530" height="84" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="740" y="398" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">objectstorage · fileserver · persistence-mongo</text>
|
||||
<text x="740" y="423" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">source</text>
|
||||
<rect x="1155" y="370" width="270" height="68" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1290" y="409" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">shared-contract</text>
|
||||
<path d="M 475 404 L 325 404" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="371.1" y="379" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="400" y="395" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Allowed</text>
|
||||
<path d="M 1005 404 L 1155 404" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="1051.1" y="379" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1080" y="395" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Allowed</text>
|
||||
<rect x="55" y="495" width="270" height="68" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="190" y="534" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">domain-core</text>
|
||||
<rect x="475" y="487" width="530" height="84" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="740" y="523" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">identifier · support 없음</text>
|
||||
<text x="740" y="548" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">source</text>
|
||||
<rect x="1155" y="495" width="270" height="68" fill="#FFFFFF" stroke="#68717B" stroke-width="1.5" rx="8" />
|
||||
<text x="1290" y="534" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">application-core</text>
|
||||
<path d="M 475 529 L 325 529" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="371.1" y="504" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="400" y="520" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Allowed</text>
|
||||
<path d="M 1005 529 L 1155 529" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="1051.1" y="504" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1080" y="520" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Allowed</text>
|
||||
<rect x="55" y="620" width="270" height="68" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="190" y="659" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">domain-core</text>
|
||||
<rect x="475" y="612" width="530" height="84" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="740" y="648" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">application-core</text>
|
||||
<text x="740" y="673" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">source</text>
|
||||
<rect x="1155" y="620" width="270" height="68" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1290" y="659" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">shared-contract</text>
|
||||
<path d="M 475 654 L 325 654" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="371.1" y="629" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="400" y="645" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Allowed</text>
|
||||
<path d="M 1005 654 L 1155 654" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="1051.1" y="629" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1080" y="645" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Allowed</text>
|
||||
<text x="740" y="755" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="550" fill="#667085" text-anchor="middle">각 행의 가운데 source → 양쪽 allowed target · 간선 교차 없음</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1420" height="670" viewBox="0 0 1420 670" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Multi-module versus single-module enforcement</title>
|
||||
<desc id="desc">Two parallel panels contrast isolated compile classpaths with one shared classpath and an ArchUnit-only boundary.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1420" height="670" fill="#FFFFFF"/>
|
||||
<rect x="40" y="55" width="650" height="535" fill="#FBFCFE" stroke="#1677FF" stroke-width="1.3" rx="10" />
|
||||
<rect x="49" y="44" width="164.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="56" y="60" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="start">멀티모듈 · 분리 클래스패스</text>
|
||||
<rect x="95" y="135" width="220" height="85" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="205" y="171.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">domain-core</text>
|
||||
<text x="205" y="196.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">Spring/JPA 타입 없음</text>
|
||||
<rect x="410" y="135" width="220" height="85" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="520" y="171.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">adapter</text>
|
||||
<text x="520" y="196.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">Spring/JPA 소유</text>
|
||||
<rect x="80" y="285" width="565" height="80" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="362" y="318" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#D64550" text-anchor="middle">금지 import</text>
|
||||
<text x="362" y="346" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#0B5CC4" text-anchor="middle">타입이 classpath에 없어 javac 실패</text>
|
||||
<path d="M 315 178 L 410 178" fill="none" stroke="#D64550" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" />
|
||||
<path d="M 346 162 L 378 194" fill="none" stroke="#D64550" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 378 162 L 346 194" fill="none" stroke="#D64550" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<rect x="220" y="430" width="96.4" height="30" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.2" rx="15" />
|
||||
<text x="268.2" y="450" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#168A5B" text-anchor="middle">독립 컴파일</text>
|
||||
<text x="362" y="505" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="650" fill="#667085" text-anchor="middle">§26 테스트 독립성의 뿌리</text>
|
||||
<rect x="730" y="55" width="650" height="535" fill="#FBFCFE" stroke="#7556D8" stroke-width="1.3" rx="10" />
|
||||
<rect x="739" y="44" width="164.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="746" y="60" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="start">단일모듈 · 공유 클래스패스</text>
|
||||
<rect x="780" y="125" width="550" height="170" fill="#FFFFFF" stroke="#737B85" stroke-width="1.5" rx="10" />
|
||||
<rect x="820" y="175" width="200" height="70" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="920" y="215" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">domain package</text>
|
||||
<rect x="1090" y="175" width="200" height="70" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="1190" y="215" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">adapter package</text>
|
||||
<text x="1055" y="150" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#7556D8" text-anchor="middle">하나의 compile classpath</text>
|
||||
<path d="M 1020 210 L 1090 210" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<rect x="982.1" y="185" width="145.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1055" y="201" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Spring import도 컴파일</text>
|
||||
<rect x="865" y="360" width="380" height="90" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1055" y="399" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">ArchUnit</text>
|
||||
<text x="1055" y="424" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">실행 전까지 위반 코드가 존재</text>
|
||||
<text x="1055" y="520" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="650" fill="#667085" text-anchor="middle">방어선이 테스트 실행 시점으로 늦어짐</text>
|
||||
<rect x="500" y="615" width="311.9" height="30" fill="#F6F7F9" stroke="#AEB5BF" stroke-width="1.2" rx="15" />
|
||||
<text x="655.9" y="635" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#667085" text-anchor="middle">두 패널은 진행 단계가 아니라 강제력의 병렬 비교</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1450" height="730" viewBox="0 0 1450 730" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Outbox state machine</title>
|
||||
<desc id="desc">Pending is claimed into in-flight, which can publish, fail for retry, become dead, or be reclaimed after visibility timeout.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1450" height="730" fill="#FFFFFF"/>
|
||||
<circle cx="55" cy="345" r="10" fill="#25282D" />
|
||||
<rect x="125" y="300" width="190" height="90" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="220" y="339" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">PENDING</text>
|
||||
<text x="220" y="364" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">append 결과</text>
|
||||
<rect x="500" y="290" width="220" height="110" fill="#FFF7E5" stroke="#B7791F" stroke-width="1.5" rx="8" />
|
||||
<text x="610" y="339" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">IN_FLIGHT</text>
|
||||
<text x="610" y="364" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">선점됨</text>
|
||||
<rect x="970" y="95" width="210" height="90" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="1075" y="134" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">PUBLISHED</text>
|
||||
<text x="1075" y="159" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">종착 상태</text>
|
||||
<rect x="970" y="300" width="210" height="90" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="1075" y="339" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">FAILED</text>
|
||||
<text x="1075" y="364" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">재시도 가능</text>
|
||||
<rect x="970" y="515" width="210" height="90" fill="#F6F7F9" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="1075" y="554" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">DEAD</text>
|
||||
<text x="1075" y="579" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">종착 · FIFO 차단</text>
|
||||
<rect x="1290" y="95" width="120" height="90" fill="#FFFFFF" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="1350" y="134" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">삭제</text>
|
||||
<text x="1350" y="159" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">보존기간 후</text>
|
||||
<path d="M 65 345 L 125 345" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="69.2" y="320" width="51.6" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="95" y="336" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">append</text>
|
||||
<path d="M 315 345 L 500 345" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="369.1" y="320" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="407.5" y="336" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">claimBatch</text>
|
||||
<path d="M 720 320 L 850 320 L 850 140 L 970 140" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-green)" />
|
||||
<rect x="806.9" y="109" width="86.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="850" y="125" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">publish 성공</text>
|
||||
<path d="M 720 345 L 970 345" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-red)" />
|
||||
<rect x="765.1" y="320" width="159.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="845" y="336" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="middle">실패 · attemptCount < 3</text>
|
||||
<path d="M 720 370 L 850 370 L 850 560 L 970 560" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="753.6" y="529" width="192.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="850" y="545" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">attemptCount ≥ 3 · markDead</text>
|
||||
<path d="M 1180 140 L 1290 140" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-green)" />
|
||||
<rect x="1206" y="115" width="58" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1235" y="131" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">보존기간</text>
|
||||
<path d="M 970 375 L 820 450 L 610 450 L 610 400" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-red)" />
|
||||
<rect x="685.4" y="425" width="209.1" height="21" fill="#FFFFFF" rx="2" />
|
||||
<text x="790" y="442" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#D64550" text-anchor="middle">next_attempt_at 경과 후 재선점</text>
|
||||
<path d="M 560 290 C 430 170, 760 140, 680 290" fill="none" stroke="#B7791F" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" marker-end="url(#arrow-ink)" />
|
||||
<rect x="523.3" y="158" width="173.4" height="21" fill="#FFFFFF" rx="2" />
|
||||
<text x="610" y="175" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#B7791F" text-anchor="middle">가시성 제한 시간 후 재선점</text>
|
||||
<text x="1075" y="650" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#667085" text-anchor="middle">DEAD에는 자동 후속 전이 없음</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="810" viewBox="0 0 1500 810" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Outbox write and relay paths</title>
|
||||
<desc id="desc">The atomic write path and the post-commit relay path are separated; a configured five-second poll connects the pending row to claimBatch.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1500" height="810" fill="#FFFFFF"/>
|
||||
<rect x="40" y="45" width="1420" height="260" fill="#FBFCFE" stroke="#1677FF" stroke-width="1.3" rx="10" />
|
||||
<rect x="49" y="34" width="188.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="56" y="50" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="start">원자적 쓰기 경로 · 트랜잭션 안</text>
|
||||
<rect x="95" y="125" width="230" height="78" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="210" y="169" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">비즈니스 유스케이스</text>
|
||||
<rect x="475" y="105" width="340" height="118" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="645" y="158" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">tx.inWrite</text>
|
||||
<text x="645" y="183" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">도메인 쓰기 + append</text>
|
||||
<polygon points="1050,95 1284,95 1320,131 1320,235 1050,235" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.7" />
|
||||
<path d="M 1284 95 L 1284 131 L 1320 131" fill="none" stroke="#1677FF" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="1068" y="131" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="700" fill="#0B5CC4" text-anchor="start">outbox_event</text>
|
||||
<text x="1068" y="160" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="500" fill="#667085" text-anchor="start">
|
||||
<tspan x="1068" dy="0">status: PENDING</tspan>
|
||||
<tspan x="1068" dy="17.4">같은 write transaction</tspan>
|
||||
</text>
|
||||
<path d="M 325 164 L 475 164" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="380.5" y="139" width="39.1" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="400" y="155" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Flow</text>
|
||||
<path d="M 815 164 L 1050 164" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="891.0" y="139" width="83.0" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="932.5" y="155" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Creates row</text>
|
||||
<rect x="40" y="400" width="1420" height="355" fill="#FBFCFE" stroke="#7556D8" stroke-width="1.3" rx="10" />
|
||||
<rect x="49" y="389" width="277.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="56" y="405" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="start">커밋 이후 릴레이 경로 · publish는 트랜잭션 밖</text>
|
||||
<rect x="80" y="500" width="210" height="82" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="185" y="535" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">OutboxRelayScheduler</text>
|
||||
<text x="185" y="560" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">fixedDelay=PT5S</text>
|
||||
<rect x="345" y="500" width="210" height="82" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="450" y="546" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">handle()</text>
|
||||
<rect x="565" y="500" width="210" height="82" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="670" y="535" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">claimBatch</text>
|
||||
<text x="670" y="560" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">SKIP LOCKED + FIFO</text>
|
||||
<rect x="825" y="500" width="210" height="82" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="930" y="535" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">occurredAt 재정렬</text>
|
||||
<text x="930" y="560" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">오름차순</text>
|
||||
<rect x="1085" y="500" width="210" height="82" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1190" y="535" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">publish</text>
|
||||
<text x="1190" y="560" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">트랜잭션 밖</text>
|
||||
<path d="M 290 541 L 345 541" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<path d="M 555 541 L 565 541" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<path d="M 775 541 L 825 541" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<path d="M 1035 541 L 1085 541" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<path d="M 1185 582 L 1185 660 L 1010 660" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-green)" />
|
||||
<rect x="790" y="625" width="220" height="70" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="900" y="654" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">markPublished</text>
|
||||
<text x="900" y="679" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">PUBLISHED</text>
|
||||
<rect x="1061.1" y="634" width="57.9" height="20" fill="#FBFCFE" rx="2" />
|
||||
<text x="1090" y="650" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">Success</text>
|
||||
<path d="M 1260 582 L 1260 640" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-red)" />
|
||||
<rect x="1220" y="640" width="210" height="70" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="1325" y="669" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">markFailed / markDead</text>
|
||||
<text x="1325" y="694" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">backoff 또는 종착</text>
|
||||
<rect x="1291.1" y="604" width="57.9" height="20" fill="#FBFCFE" rx="2" />
|
||||
<text x="1320" y="620" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="middle">Failure</text>
|
||||
<path d="M 1185 235 L 1185 355 L 670 355 L 670 500" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" marker-end="url(#arrow-purple)" />
|
||||
<rect x="834.9" y="328" width="190.3" height="21" fill="#FFFFFF" rx="2" />
|
||||
<text x="930" y="345" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="650" fill="#7556D8" text-anchor="middle">설정된 fixedDelay=PT5S 폴링</text>
|
||||
<rect x="430" y="700" width="311.6" height="30" fill="#FFF7E5" stroke="#B7791F" stroke-width="1.2" rx="15" />
|
||||
<text x="585.8" y="720" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#B7791F" text-anchor="middle">IN_FLIGHT stuck → 가시성 제한 시간 후 재선점</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,17 @@
|
||||
<?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="production-vs-optin" name="app-bootstrap의 main 클래스패스에는 어댑터 11개가 포함되고 참조 어댑터 3개는 의존 목록 밖에 있다">
|
||||
<mxGraphModel dx="680" dy="300" 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_main-classpath-adapters" value="main 의존 포함 · 11<br/>수량: 11개<br/>프로젝트 의존: 선언됨<br/>클래스패스: 포함<br/>어댑터: 포함 대상 11개<br/>활성화: 클래스패스와 별도<br/>측정 범위: main 의존 선언" tooltip="app-bootstrap의 main 프로젝트 의존에 포함되어 main 클래스패스에 들어오는 어댑터 집합 | Evidence: L449-L456, L476-L480, L500-L501, L517-L517" 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="45.0" y="45.0" width="251.0" height="156.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_omitted-optin-adapters" value="main 의존 목록 밖 · 3<br/>수량: 3개<br/>프로젝트 의존: 선언하지 않음<br/>클래스패스: 제외<br/>어댑터: grpc · graphql · websocket<br/>활성화: 등록하면 기본 활성<br/>측정 범위: main 의존 선언" tooltip="저장소에는 있지만 app-bootstrap의 main 프로젝트 의존에는 선언되지 않은 참조 어댑터 집합 | Evidence: L481-L489, L500-L501, L517-L517" 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="348.0" y="45.0" width="251.0" height="156.0" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="300" viewBox="0 0 680 300" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">app-bootstrap의 main 클래스패스에는 어댑터 11개가 포함되고 참조 어댑터 3개는 의존 목록 밖에 있다</title>
|
||||
<desc id="diagram-description">왼쪽 비교 항목은 app-bootstrap의 main 프로젝트 의존에 포함되어 main 클래스패스에 들어오는 어댑터 11개를 나타낸다. 클래스패스 포함과 실제 빈 활성화는 별개이며 런타임 조건이 활성화를 추가로 결정한다. 오른쪽 비교 항목은 현재 main 의존 목록에 없는 grpc, graphql, websocket 세 참조 어댑터를 나타낸다. 이 셋은 클래스패스에 등록되면 기본 활성화되므로 의존성 선언을 하지 않는 것이 opt-in 수단이다. 두 수치는 main 의존 선언을 비교한 것이며 실행 시 활성 빈 전체를 측정한 값이 아니다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"production-vs-optin","profile":"comparison"},"source_context":{"document":"/home/donghyeon/workspace/ai-tool/topic-arrange/executable-clean-architecture/claridoc-rewrite/document.md","document_sha256":"81fb5cb8cd16eaae6916a0d0f2b3cddfabc39e58a87559466b52f922ca95a95b","anchor":{"kind":"marker","value":"production-vs-optin","line":502}},"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="300" />
|
||||
<g id="node-main-classpath-adapters">
|
||||
<rect class="node-shape kind-concept emphasis-primary role-option" data-evidence="449-456,476-480,500-501,517-517" x="45.0" y="45.0" width="251.0" height="156.0" rx="7" />
|
||||
<text class="node-label" x="170.5" y="73.0">main 의존 포함 · 11</text>
|
||||
<line class="node-detail-divider" x1="59.0" y1="94.0" x2="282.0" y2="94.0" />
|
||||
<text class="node-detail" x="61.0" y="111.0">수량: 11개</text>
|
||||
<text class="node-detail" x="61.0" y="127.0">프로젝트 의존: 선언됨</text>
|
||||
<text class="node-detail" x="61.0" y="143.0">클래스패스: 포함</text>
|
||||
<text class="node-detail" x="61.0" y="159.0">어댑터: 포함 대상 11개</text>
|
||||
<text class="node-detail" x="61.0" y="175.0">활성화: 클래스패스와 별도</text>
|
||||
<text class="node-detail" x="61.0" y="191.0">측정 범위: main 의존 선언</text>
|
||||
</g>
|
||||
<g id="node-omitted-optin-adapters">
|
||||
<rect class="node-shape kind-concept emphasis-warning role-option" data-evidence="481-489,500-501,517-517" x="348.0" y="45.0" width="251.0" height="156.0" rx="7" />
|
||||
<text class="node-label" x="473.5" y="73.0">main 의존 목록 밖 · 3</text>
|
||||
<line class="node-detail-divider" x1="362.0" y1="94.0" x2="585.0" y2="94.0" />
|
||||
<text class="node-detail" x="364.0" y="111.0">수량: 3개</text>
|
||||
<text class="node-detail" x="364.0" y="127.0">프로젝트 의존: 선언하지 않음</text>
|
||||
<text class="node-detail" x="364.0" y="143.0">클래스패스: 제외</text>
|
||||
<text class="node-detail" x="364.0" y="159.0">어댑터: grpc · graphql · websocket</text>
|
||||
<text class="node-detail" x="364.0" y="175.0">활성화: 등록하면 기본 활성</text>
|
||||
<text class="node-detail" x="364.0" y="191.0">측정 범위: main 의존 선언</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="660" viewBox="0 0 1400 660" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Runtime call versus source dependency</title>
|
||||
<desc id="desc">Two lanes separate runtime dispatch from source dependencies and contract ownership.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1400" height="660" fill="#FFFFFF"/>
|
||||
<rect x="35" y="45" width="1330" height="245" fill="#FBFCFE" stroke="#C5CBD3" stroke-width="1.3" rx="10" />
|
||||
<rect x="44" y="34" width="233.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="51" y="50" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="start">실행 시점 관계 · 실선 = 호출·디스패치</text>
|
||||
<rect x="100" y="125" width="245" height="90" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="222.5" y="164" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">FeedController</text>
|
||||
<text x="222.5" y="189" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">driving adapter</text>
|
||||
<rect x="555" y="105" width="285" height="130" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="697.5" y="164" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">GetFeedUseCase</text>
|
||||
<text x="697.5" y="189" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">concrete service</text>
|
||||
<rect x="1060" y="125" width="250" height="90" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="1185" y="164" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">SpringTransactionPort</text>
|
||||
<text x="1185" y="189" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">runtime implementation</text>
|
||||
<path d="M 345 170 L 555 170" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="405.4" y="145" width="89.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="450" y="161" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">Runtime call</text>
|
||||
<path d="M 840 170 L 1060 170" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="892.8" y="145" width="114.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="950" y="161" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">Runtime dispatch</text>
|
||||
<rect x="35" y="350" width="1330" height="255" fill="#FBFCFE" stroke="#C5CBD3" stroke-width="1.3" rx="10" />
|
||||
<rect x="44" y="339" width="284.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="51" y="355" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="start">계약 소유·소스 의존 · 점선 = 타입·계약을 향함</text>
|
||||
<rect x="95" y="440" width="260" height="85" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="225" y="461" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="600" fill="#667085" text-anchor="middle" letter-spacing="0.8"><<interface>></text>
|
||||
<text x="225" y="482.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">QueryUseCase<Q,R></text>
|
||||
<text x="225" y="507.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">application-core contract</text>
|
||||
<rect x="565" y="420" width="270" height="125" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="700" y="476.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">GetFeedUseCase</text>
|
||||
<text x="700" y="501.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">implements · calls</text>
|
||||
<rect x="1060" y="440" width="240" height="85" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1180" y="461" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="600" fill="#667085" text-anchor="middle" letter-spacing="0.8"><<interface>></text>
|
||||
<text x="1180" y="482.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">TransactionPort</text>
|
||||
<text x="1180" y="507.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">application-core contract</text>
|
||||
<path d="M 565 465 L 355 465" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" marker-end="url(#arrow-purple)" />
|
||||
<rect x="421.6" y="440" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="460" y="456" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Implements</text>
|
||||
<path d="M 835 465 L 1060 465" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" marker-end="url(#arrow-purple)" />
|
||||
<rect x="902.9" y="440" width="89.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="947.5" y="456" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Runtime call</text>
|
||||
<rect x="1080" y="370" width="200" height="50" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="1180" y="400" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">SpringTransactionPort</text>
|
||||
<path d="M 1180 420 L 1180 440" fill="none" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="7 5" marker-end="url(#arrow-muted)" />
|
||||
<rect x="1221.7" y="422" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1260" y="438" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="middle">Implements</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1460" height="790" viewBox="0 0 1460 790" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Feed runtime sequence</title>
|
||||
<desc id="desc">Eight numbered runtime messages connect four lifelines; FeedQueryPort is shown separately as a compile-time contract rather than a lifeline.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1460" height="790" fill="#FFFFFF"/>
|
||||
<rect x="20" y="35" width="230" height="70" fill="#F6F7F9" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="135" y="75" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">FeedController</text>
|
||||
<path d="M 135 105 L 135 685" fill="none" stroke="#B6BDC7" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 6" />
|
||||
<rect x="360" y="35" width="230" height="70" fill="#F6F7F9" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="475" y="75" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">GetFeedUseCase</text>
|
||||
<path d="M 475 105 L 475 685" fill="none" stroke="#B6BDC7" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 6" />
|
||||
<rect x="720" y="35" width="230" height="70" fill="#F6F7F9" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="835" y="64" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">TransactionPort.inRead</text>
|
||||
<text x="835" y="89" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">경계</text>
|
||||
<path d="M 835 105 L 835 685" fill="none" stroke="#B6BDC7" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 6" />
|
||||
<rect x="1075" y="35" width="230" height="70" fill="#F6F7F9" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="1190" y="75" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#25282D" text-anchor="middle">FeedQueryAdapter</text>
|
||||
<path d="M 1190 105 L 1190 685" fill="none" stroke="#B6BDC7" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 6" />
|
||||
<rect x="463" y="145" width="24" height="470" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.3" rx="2" />
|
||||
<rect x="823" y="205" width="24" height="320" fill="#F6F7F9" stroke="#737B85" stroke-width="1.2" rx="2" />
|
||||
<rect x="1178" y="350" width="24" height="115" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.2" rx="2" />
|
||||
<path d="M 135 160 L 463 160" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="229.3" y="134" width="139.4" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="299" y="150" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">handle(GetFeedQuery)</text>
|
||||
<circle cx="28" cy="160" r="14" fill="#1677FF" />
|
||||
<text x="28" y="165" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="800" fill="#FFFFFF" text-anchor="middle">1</text>
|
||||
<path d="M 487 220 L 823 220" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="597.8" y="194" width="114.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="655" y="210" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">inRead(callback)</text>
|
||||
<circle cx="28" cy="220" r="14" fill="#25282D" />
|
||||
<text x="28" y="225" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="800" fill="#FFFFFF" text-anchor="middle">2</text>
|
||||
<path d="M 823 285 L 487 285" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-ink)" />
|
||||
<rect x="580.6" y="259" width="148.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="655" y="275" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">Supplier callback 실행</text>
|
||||
<circle cx="28" cy="285" r="14" fill="#25282D" />
|
||||
<text x="28" y="290" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="800" fill="#FFFFFF" text-anchor="middle">3</text>
|
||||
<path d="M 487 365 L 1178 365" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="728.3" y="339" width="208.5" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="832.5" y="355" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">loadFeed(page,size) · DI 구현체</text>
|
||||
<circle cx="28" cy="365" r="14" fill="#1677FF" />
|
||||
<text x="28" y="370" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="800" fill="#FFFFFF" text-anchor="middle">4</text>
|
||||
<path d="M 1178 445 L 487 445" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-ink)" />
|
||||
<rect x="772.2" y="419" width="120.6" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="832.5" y="435" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">List<FeedSummary></text>
|
||||
<circle cx="28" cy="445" r="14" fill="#25282D" />
|
||||
<text x="28" y="450" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="800" fill="#FFFFFF" text-anchor="middle">5</text>
|
||||
<path d="M 487 505 L 823 505" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-ink)" />
|
||||
<rect x="608.8" y="479" width="92.4" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="655" y="495" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">callback 결과</text>
|
||||
<circle cx="28" cy="505" r="14" fill="#25282D" />
|
||||
<text x="28" y="510" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="800" fill="#FFFFFF" text-anchor="middle">6</text>
|
||||
<path d="M 823 565 L 487 565" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-ink)" />
|
||||
<rect x="615.1" y="539" width="79.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="655" y="555" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">inRead 결과</text>
|
||||
<circle cx="28" cy="565" r="14" fill="#25282D" />
|
||||
<text x="28" y="570" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="800" fill="#FFFFFF" text-anchor="middle">7</text>
|
||||
<path d="M 463 625 L 135 625" fill="none" stroke="#25282D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-ink)" />
|
||||
<rect x="259.1" y="599" width="79.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="299" y="615" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#25282D" text-anchor="middle">handle 결과</text>
|
||||
<circle cx="28" cy="625" r="14" fill="#25282D" />
|
||||
<text x="28" y="630" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="800" fill="#FFFFFF" text-anchor="middle">8</text>
|
||||
<rect x="1035" y="705" width="350" height="55" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1210" y="726.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">FeedQueryPort</text>
|
||||
<text x="1210" y="751.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">컴파일 시점 계약 · lifeline 아님</text>
|
||||
<path d="M 1190 705 L 1190 685" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1229.1" y="682" width="101.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1280" y="698" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Implemented by</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1420" height="720" viewBox="0 0 1420 720" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Static analysis coverage subset</title>
|
||||
<desc id="desc">A smaller static-analysis set sits inside the set of all real boundary violations; caught and missed examples occupy their respective regions.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1420" height="720" fill="#FFFFFF"/>
|
||||
<ellipse cx="710" cy="370" rx="620" ry="290" fill="#FBFCFE" stroke="#68717B" stroke-width="1.8" />
|
||||
<rect x="599.1" y="75" width="221.8" height="24" fill="#FFFFFF" stroke="#68717B" stroke-width="1" rx="2" />
|
||||
<text x="710" y="95" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="650" fill="#25282D" text-anchor="middle">실제 경계 위반 전체 · 현실</text>
|
||||
<ellipse cx="520" cy="375" rx="330" ry="205" fill="#EAF3FF" stroke="#1677FF" stroke-width="2" />
|
||||
<rect x="355.2" y="171" width="329.6" height="23" fill="#FFFFFF" stroke="#1677FF" stroke-width="1" rx="2" />
|
||||
<text x="520" y="190" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="650" fill="#0B5CC4" text-anchor="middle">정적 분석이 보는 영역 · Gradle + ArchUnit</text>
|
||||
<text x="360" y="300" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="650" fill="#0B5CC4" text-anchor="start">
|
||||
<tspan x="360" dy="0">✅ 모듈 의존</tspan>
|
||||
<tspan x="360" dy="23.2">✅ import · 호출</tspan>
|
||||
<tspan x="360" dy="23.2">✅ @Transactional</tspan>
|
||||
<tspan x="360" dy="23.2">✅ JPA · Lombok</tspan>
|
||||
</text>
|
||||
<text x="930" y="300" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="650" fill="#D64550" text-anchor="start">
|
||||
<tspan x="930" dy="0">❌ 문자열 조회</tspan>
|
||||
<tspan x="930" dy="24.8">❌ 리플렉션</tspan>
|
||||
<tspan x="930" dy="24.8">❌ 조건부 런타임 배선</tspan>
|
||||
</text>
|
||||
<rect x="555" y="625" width="181.9" height="30" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.2" rx="15" />
|
||||
<text x="646.0" y="645" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#0B5CC4" text-anchor="middle">정적 분석 ⊂ 실제 위반</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1420" height="650" viewBox="0 0 1420 650" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Layered and port-based test contrast</title>
|
||||
<desc id="desc">An illustrative framework-collaborator replacement is contrasted with the observed anonymous TransactionPort test double pattern.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1420" height="650" fill="#FFFFFF"/>
|
||||
<rect x="45" y="55" width="635" height="505" fill="#FBFCFE" stroke="#AEB5BF" stroke-width="1.3" rx="10" />
|
||||
<rect x="54" y="44" width="75.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="61" y="60" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="start">설명용 대조</text>
|
||||
<rect x="100" y="130" width="230" height="85" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="215" y="177.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">Layered service test</text>
|
||||
<rect x="405" y="110" width="220" height="70" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="515" y="150" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">Spring context</text>
|
||||
<rect x="405" y="225" width="220" height="70" fill="#FDEDEF" stroke="#D64550" stroke-width="1.5" rx="8" />
|
||||
<text x="515" y="265" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">Mockito collaborator</text>
|
||||
<path d="M 330 165 L 405 145" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-red)" />
|
||||
<rect x="335.4" y="130" width="64.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="367.5" y="146" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="middle">직접 대체</text>
|
||||
<path d="M 330 185 L 405 260" fill="none" stroke="#D64550" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-red)" />
|
||||
<rect x="335.4" y="197.5" width="64.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="367.5" y="213.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#D64550" text-anchor="middle">직접 대체</text>
|
||||
<text x="362" y="365" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="650" fill="#667085" text-anchor="middle">컨텍스트·Mockito는 결합도 설명용 예시</text>
|
||||
<rect x="195" y="430" width="195.4" height="30" fill="#F6F7F9" stroke="#AEB5BF" stroke-width="1.2" rx="15" />
|
||||
<text x="292.7" y="450" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#667085" text-anchor="middle">저장소의 대칭 측정 결과 아님</text>
|
||||
<rect x="740" y="55" width="635" height="505" fill="#FBFCFE" stroke="#1677FF" stroke-width="1.3" rx="10" />
|
||||
<rect x="749" y="44" width="136.5" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="756" y="60" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="start">저장소에서 관찰된 패턴</text>
|
||||
<rect x="795" y="145" width="245" height="95" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="917.5" y="186.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">포트 유스케이스 테스트</text>
|
||||
<text x="917.5" y="211.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">core-owned seam</text>
|
||||
<circle cx="1105" cy="192" r="10" fill="#FFFFFF" stroke="#1677FF" stroke-width="2" />
|
||||
<text x="1105" y="165" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="700" fill="#0B5CC4" text-anchor="middle">port</text>
|
||||
<polygon points="1160,125 1296,125 1330,159 1330,260 1160,260" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.7" />
|
||||
<path d="M 1296 125 L 1296 159 L 1330 159" fill="none" stroke="#1677FF" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<text x="1178" y="161" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="700" fill="#0B5CC4" text-anchor="start">TransactionPort</text>
|
||||
<text x="1178" y="190" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="500" fill="#667085" text-anchor="start">
|
||||
<tspan x="1178" dy="0">익명 테스트 더블</tspan>
|
||||
</text>
|
||||
<path d="M 1040 192 L 1095 192" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="1048.0" y="167" width="39.1" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1067.5" y="183" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">uses</text>
|
||||
<path d="M 1115 192 L 1160 192" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1099.2" y="167" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1137.5" y="183" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">implements</text>
|
||||
<text x="1057" y="365" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="700" fill="#0B5CC4" text-anchor="middle">프레임워크 대신 코어가 소유한 계약을 대체</text>
|
||||
<rect x="905" y="430" width="152.5" height="30" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.2" rx="15" />
|
||||
<text x="981.3" y="450" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#168A5B" text-anchor="middle">실제 테스트 더블 패턴</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1420" height="660" viewBox="0 0 1420 660" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Observed test inventory and enforced rules</title>
|
||||
<desc id="desc">Observed sample tests and separately enforced ArchUnit rules are shown as independent evidence scopes, not a complete taxonomy.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1420" height="660" fill="#FFFFFF"/>
|
||||
<rect x="45" y="70" width="625" height="450" fill="#FBFCFE" stroke="#1677FF" stroke-width="1.3" rx="10" />
|
||||
<rect x="54" y="59" width="158.4" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="61" y="75" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="start">관찰된 sample 테스트 표본</text>
|
||||
<circle cx="175" cy="215" r="78" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.8" />
|
||||
<text x="175" y="210" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="18" font-weight="800" fill="#0B5CC4" text-anchor="middle">15 classes</text>
|
||||
<text x="175" y="238" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="14" font-weight="650" fill="#0B5CC4" text-anchor="middle">71 methods</text>
|
||||
<rect x="330" y="165" width="280" height="100" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="470" y="209" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">domain / application</text>
|
||||
<text x="470" y="234" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">framework import 0</text>
|
||||
<path d="M 253 215 L 330 215" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="259.4" y="190" width="64.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="291.5" y="206" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Measures</text>
|
||||
<rect x="115" y="340" width="95.9" height="30" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.2" rx="15" />
|
||||
<text x="162.9" y="360" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#0B5CC4" text-anchor="middle">domain 표본</text>
|
||||
<rect x="115" y="388" width="130.1" height="30" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.2" rx="15" />
|
||||
<text x="180.0" y="408" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#0B5CC4" text-anchor="middle">application 표본</text>
|
||||
<rect x="115" y="436" width="145.7" height="30" fill="#FFFFFF" stroke="#AEB5BF" stroke-width="1.2" rx="15" />
|
||||
<text x="187.8" y="456" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#667085" text-anchor="middle">완전한 인벤토리 아님</text>
|
||||
<rect x="750" y="70" width="625" height="450" fill="#FBFCFE" stroke="#7556D8" stroke-width="1.3" rx="10" />
|
||||
<rect x="759" y="59" width="149.0" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="766" y="75" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="start">별도 ArchUnit 강제 범위</text>
|
||||
<rect x="800" y="165" width="220" height="100" fill="#FFFFFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="910" y="209" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="700" fill="#25282D" text-anchor="middle">TestTaxonomyArchitectureTest</text>
|
||||
<text x="910" y="234" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">ArchUnit rule set</text>
|
||||
<rect x="1080" y="125" width="245" height="60" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1202.5" y="160" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">Testcontainers 금지</text>
|
||||
<path d="M 1020 190 L 1080 155" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1017.9" y="147.5" width="64.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1050" y="163.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Enforces</text>
|
||||
<rect x="1080" y="220" width="245" height="60" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1202.5" y="255" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">slice 혼용 금지</text>
|
||||
<path d="M 1020 210 L 1080 250" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1017.9" y="205" width="64.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1050" y="221" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Enforces</text>
|
||||
<rect x="1080" y="315" width="245" height="60" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1202.5" y="350" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">fixture 누출 금지</text>
|
||||
<path d="M 1020 230 L 1080 345" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1017.9" y="262.5" width="64.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1050" y="278.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Enforces</text>
|
||||
<rect x="265" y="565" width="890" height="55" fill="#F6F7F9" stroke="#AEB5BF" stroke-width="1.3" rx="27" />
|
||||
<text x="710" y="599" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="15" font-weight="700" fill="#667085" text-anchor="middle">표본 인벤토리 ≠ 완전한 taxonomy · 두 근거 범위는 독립</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="660" viewBox="0 0 1440 660" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Three independent enforcement gates</title>
|
||||
<desc id="desc">Classpath isolation and Gradle policy are grouped as module-dependent scopes; ArchUnit remains an independent scope, with no implied execution order.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1440" height="660" fill="#FFFFFF"/>
|
||||
<rect x="530" y="25" width="224.5" height="30" fill="#F6F7F9" stroke="#AEB5BF" stroke-width="1.2" rx="15" />
|
||||
<text x="642.2" y="45" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#667085" text-anchor="middle">세 범위 · 고정 실행 순서 없음</text>
|
||||
<rect x="45" y="105" width="880" height="430" fill="#FBFCFE" stroke="#1677FF" stroke-width="1.3" rx="10" />
|
||||
<rect x="54" y="94" width="142.8" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="61" y="110" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="start">모듈 분리에 기대는 범위</text>
|
||||
<rect x="100" y="180" width="340" height="260" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.6" rx="10" />
|
||||
<text x="270" y="225" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="18" font-weight="750" fill="#0B5CC4" text-anchor="middle">컴파일 클래스패스 격리</text>
|
||||
<rect x="145" y="275" width="250" height="65" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="270" y="312.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">금지 타입이 없음</text>
|
||||
<rect x="180" y="370" width="89.0" height="30" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.2" rx="15" />
|
||||
<text x="224.5" y="390" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#168A5B" text-anchor="middle">javac 차단</text>
|
||||
<rect x="530" y="180" width="340" height="260" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.6" rx="10" />
|
||||
<text x="700" y="225" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="18" font-weight="750" fill="#0B5CC4" text-anchor="middle">Gradle 의존 정책</text>
|
||||
<rect x="575" y="275" width="250" height="65" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8" />
|
||||
<text x="700" y="312.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">allowedProjectDependencies</text>
|
||||
<rect x="600" y="370" width="136.9" height="30" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.2" rx="15" />
|
||||
<text x="668.5" y="390" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#168A5B" text-anchor="middle">project edge 차단</text>
|
||||
<rect x="990" y="105" width="405" height="430" fill="#FBFCFE" stroke="#7556D8" stroke-width="1.3" rx="10" />
|
||||
<rect x="999" y="94" width="64.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1006" y="110" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="start">독립 범위</text>
|
||||
<text x="1192" y="225" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="18" font-weight="750" fill="#7556D8" text-anchor="middle">ArchUnit 구조 규칙</text>
|
||||
<rect x="1065" y="275" width="255" height="65" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1192.5" y="312.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">import · annotation · package</text>
|
||||
<rect x="1100" y="370" width="109.7" height="30" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.2" rx="15" />
|
||||
<text x="1154.8" y="390" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#168A5B" text-anchor="middle">구조 위반 차단</text>
|
||||
<path d="M 180 590 L 1260 590" fill="none" stroke="#AEB5BF" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle cx="270" cy="590" r="7" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.8" />
|
||||
<circle cx="700" cy="590" r="7" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.8" />
|
||||
<circle cx="1192" cy="590" r="7" fill="#FFFFFF" stroke="#7556D8" stroke-width="1.8" />
|
||||
<text x="720" y="630" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="600" fill="#667085" text-anchor="middle">동일 축의 독립 범위 · 화살표 없음</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="850" viewBox="0 0 1500 850" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Transaction and lock contracts</title>
|
||||
<desc id="desc">Current transaction and distributed-lock wiring are shown independently; a dashed future-only strip records acquire, commit, release ordering and database constraints.</desc>
|
||||
<metadata>{"generator":"_work/regenerate-technical-assets.py","canvas_policy":"diagram-only","decorative_effects":false}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow-ink" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#25282D"/></marker>
|
||||
<marker id="arrow-muted" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#667085"/></marker>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#1677FF"/></marker>
|
||||
<marker id="arrow-purple" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#7556D8"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#168A5B"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#D64550"/></marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 4 L 0 8 z" fill="#B7791F"/></marker>
|
||||
</defs>
|
||||
<rect width="1500" height="850" fill="#FFFFFF"/>
|
||||
<rect x="560" y="25" width="352.4" height="30" fill="#F6F7F9" stroke="#AEB5BF" stroke-width="1.2" rx="15" />
|
||||
<text x="736.2" y="45" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#667085" text-anchor="middle">상단 = 현재 배선 · 하단 점선 = FUTURE 계약 입력</text>
|
||||
<rect x="40" y="85" width="680" height="430" fill="#FBFCFE" stroke="#1677FF" stroke-width="1.3" rx="10" />
|
||||
<rect x="49" y="74" width="181.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="56" y="90" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#0B5CC4" text-anchor="start">TransactionPort · 현재 배선</text>
|
||||
<rect x="105" y="155" width="250" height="90" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="230" y="176" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="600" fill="#667085" text-anchor="middle" letter-spacing="0.8"><<interface>></text>
|
||||
<text x="230" y="200" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">TransactionPort</text>
|
||||
<text x="230" y="225" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">application-core contract</text>
|
||||
<rect x="110" y="315" width="71.9" height="30" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.2" rx="15" />
|
||||
<text x="145.9" y="335" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#0B5CC4" text-anchor="middle">inWrite</text>
|
||||
<rect x="280" y="315" width="65.0" height="30" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.2" rx="15" />
|
||||
<text x="312.5" y="335" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#0B5CC4" text-anchor="middle">inRead</text>
|
||||
<rect x="450" y="315" width="58.2" height="30" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.2" rx="15" />
|
||||
<text x="479.1" y="335" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#0B5CC4" text-anchor="middle">inNew</text>
|
||||
<rect x="430" y="155" width="240" height="90" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="550" y="194" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">SpringTransactionPort</text>
|
||||
<text x="550" y="219" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">implements</text>
|
||||
<path d="M 430 200 L 355 200" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-purple)" />
|
||||
<rect x="354.1" y="175" width="76.7" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="392.5" y="191" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Implements</text>
|
||||
<path d="M 230 245 L 230 280" fill="none" stroke="#1677FF" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle cx="230" cy="280" r="4" fill="#1677FF" />
|
||||
<path d="M 230 280 L 145 315" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<path d="M 230 280 L 315 315" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="252.9" y="258" width="64.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="285" y="274" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#1677FF" text-anchor="middle">Declares</text>
|
||||
<path d="M 230 280 L 485 315" fill="none" stroke="#1677FF" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-blue)" />
|
||||
<rect x="780" y="85" width="680" height="430" fill="#FBFCFE" stroke="#7556D8" stroke-width="1.3" rx="10" />
|
||||
<rect x="789" y="74" width="206.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="796" y="90" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="start">DistributedLockPort · 현재 배선</text>
|
||||
<rect x="830" y="150" width="245" height="85" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="952.5" y="171" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="600" fill="#667085" text-anchor="middle" letter-spacing="0.8"><<interface>></text>
|
||||
<text x="952.5" y="192.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="16" font-weight="700" fill="#25282D" text-anchor="middle">DistributedLockPort</text>
|
||||
<text x="952.5" y="217.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">application-core contract</text>
|
||||
<rect x="820" y="310" width="250" height="82" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="945" y="345" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">false · in-process adapter</text>
|
||||
<text x="945" y="370" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">default</text>
|
||||
<rect x="1125" y="310" width="245" height="82" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8" />
|
||||
<text x="1247.5" y="345" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">true · JDBC lock adapter</text>
|
||||
<text x="1247.5" y="370" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">conditional</text>
|
||||
<path d="M 920 235 L 920 310" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<rect x="891.1" y="247.5" width="57.9" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="920" y="263.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Default</text>
|
||||
<path d="M 1010 235 L 1247 310" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1087.0" y="247.5" width="83.0" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1128.5" y="263.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Conditional</text>
|
||||
<rect x="1125" y="425" width="245" height="58" fill="#FFFFFF" stroke="#7556D8" stroke-width="1.5" rx="8" />
|
||||
<text x="1247.5" y="459" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#25282D" text-anchor="middle">MeteredDistributedLockPort</text>
|
||||
<path d="M 1247 392 L 1247 425" fill="none" stroke="#7556D8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow-purple)" />
|
||||
<rect x="1189.8" y="383.5" width="114.3" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1247" y="399.5" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#7556D8" text-anchor="middle">Conditional wrap</text>
|
||||
<rect x="835" y="435" width="128.5" height="30" fill="#FDEDEF" stroke="#D64550" stroke-width="1.2" rx="15" />
|
||||
<text x="899.3" y="455" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#D64550" text-anchor="middle">프로덕션 호출자 0</text>
|
||||
<path d="M 835 450 L 795 450 L 795 192 L 830 192" fill="none" stroke="#D64550" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" />
|
||||
<path d="M 799 179 L 825 205" fill="none" stroke="#D64550" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M 825 179 L 799 205" fill="none" stroke="#D64550" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<rect x="120" y="600" width="1260" height="195" fill="#FBFCFE" stroke="#667085" stroke-width="1.3" rx="10" stroke-dasharray="8 6" />
|
||||
<rect x="129" y="589" width="232.2" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="136" y="605" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#667085" text-anchor="start">FUTURE 계약 · 현재 프로덕션 실행 없음</text>
|
||||
<rect x="170" y="660" width="71.9" height="30" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.2" rx="15" />
|
||||
<text x="205.9" y="680" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#0B5CC4" text-anchor="middle">acquire</text>
|
||||
<path d="M 315 675 L 520 675" fill="none" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-muted)" />
|
||||
<rect x="520" y="660" width="65.0" height="30" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.2" rx="15" />
|
||||
<text x="552.5" y="680" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#0B5CC4" text-anchor="middle">commit</text>
|
||||
<path d="M 650 675 L 855 675" fill="none" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-muted)" />
|
||||
<rect x="855" y="660" width="71.9" height="30" fill="#FFFFFF" stroke="#1677FF" stroke-width="1.2" rx="15" />
|
||||
<text x="890.9" y="680" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="700" fill="#0B5CC4" text-anchor="middle">release</text>
|
||||
<rect x="1060" y="642" width="270" height="70" fill="#EAF7F1" stroke="#168A5B" stroke-width="1.5" rx="8" />
|
||||
<text x="1195" y="671" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="13" font-weight="700" fill="#25282D" text-anchor="middle">DB 제약 · 낙관적 동시성</text>
|
||||
<text x="1195" y="696" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="12" font-weight="450" fill="#667085" text-anchor="middle">최종 정합성 방어선</text>
|
||||
<path d="M 230 515 L 230 580 L 230 600" fill="none" stroke="#667085" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" />
|
||||
<path d="M 1000 515 L 1000 580 L 1000 600" fill="none" stroke="#667085" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" />
|
||||
<path d="M 1060 677 L 980 677" fill="none" stroke="#168A5B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 5" marker-end="url(#arrow-green)" />
|
||||
<rect x="959.7" y="652" width="120.6" height="20" fill="#FFFFFF" rx="2" />
|
||||
<text x="1020" y="668" font-family='"Noto Sans CJK KR", "Apple SD Gothic Neo", sans-serif' font-size="11" font-weight="650" fill="#168A5B" text-anchor="middle">Correctness guard</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "production-vs-optin",
|
||||
"title": "app-bootstrap의 main 클래스패스에는 어댑터 11개가 포함되고 참조 어댑터 3개는 의존 목록 밖에 있다",
|
||||
"question": "app-bootstrap의 main 프로젝트 의존에 포함된 어댑터와 의존 목록 밖의 opt-in 참조 어댑터는 어떻게 구분되는가?",
|
||||
"type": "concept",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"멀티모듈 Spring Boot 애플리케이션 설계자",
|
||||
"클린 아키텍처 구현 독자"
|
||||
],
|
||||
"summary": "main 프로젝트 의존은 어댑터 11개를 클래스패스에 올리고, grpc·graphql·websocket 세 참조 어댑터는 의존성을 선언하지 않는 방식으로 opt-in한다.",
|
||||
"alt": "왼쪽의 app-bootstrap main 의존 포함 어댑터 11개와 오른쪽의 main 의존 목록 밖 grpc·graphql·websocket 세 개를 비교한 그림. 클래스패스 구성 비교이며 활성 빈 수를 뜻하지 않는다.",
|
||||
"long_description": "왼쪽 비교 항목은 app-bootstrap의 main 프로젝트 의존에 포함되어 main 클래스패스에 들어오는 어댑터 11개를 나타낸다. 클래스패스 포함과 실제 빈 활성화는 별개이며 런타임 조건이 활성화를 추가로 결정한다. 오른쪽 비교 항목은 현재 main 의존 목록에 없는 grpc, graphql, websocket 세 참조 어댑터를 나타낸다. 이 셋은 클래스패스에 등록되면 기본 활성화되므로 의존성 선언을 하지 않는 것이 opt-in 수단이다. 두 수치는 main 의존 선언을 비교한 것이며 실행 시 활성 빈 전체를 측정한 값이 아니다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/executable-clean-architecture/claridoc-rewrite/document.md",
|
||||
"document_sha256": "81fb5cb8cd16eaae6916a0d0f2b3cddfabc39e58a87559466b52f922ca95a95b",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "production-vs-optin",
|
||||
"line": 502
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "comparison",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"contract-comparison"
|
||||
],
|
||||
"rationale": "본문이 main 프로젝트 의존에 포함된 집합과 의존 목록 밖의 opt-in 집합을 명시적으로 비교하므로, 호출 관계나 시간 순서를 만들지 않고 같은 필드로 두 집합을 정렬하는 비교 문법이 독자의 질문에 가장 직접적으로 답한다."
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "main-classpath-adapters",
|
||||
"label": "main 의존 포함 · 11",
|
||||
"kind": "concept",
|
||||
"role": "option",
|
||||
"description": "app-bootstrap의 main 프로젝트 의존에 포함되어 main 클래스패스에 들어오는 어댑터 집합",
|
||||
"details": [
|
||||
"수량: 11개",
|
||||
"프로젝트 의존: 선언됨",
|
||||
"클래스패스: 포함",
|
||||
"어댑터: 포함 대상 11개",
|
||||
"활성화: 클래스패스와 별도",
|
||||
"측정 범위: main 의존 선언"
|
||||
],
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 449,
|
||||
"end_line": 456
|
||||
},
|
||||
{
|
||||
"start_line": 476,
|
||||
"end_line": 480
|
||||
},
|
||||
{
|
||||
"start_line": 500,
|
||||
"end_line": 501
|
||||
},
|
||||
{
|
||||
"start_line": 517,
|
||||
"end_line": 517
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "omitted-optin-adapters",
|
||||
"label": "main 의존 목록 밖 · 3",
|
||||
"kind": "concept",
|
||||
"role": "option",
|
||||
"description": "저장소에는 있지만 app-bootstrap의 main 프로젝트 의존에는 선언되지 않은 참조 어댑터 집합",
|
||||
"details": [
|
||||
"수량: 3개",
|
||||
"프로젝트 의존: 선언하지 않음",
|
||||
"클래스패스: 제외",
|
||||
"어댑터: grpc · graphql · websocket",
|
||||
"활성화: 등록하면 기본 활성",
|
||||
"측정 범위: main 의존 선언"
|
||||
],
|
||||
"emphasis": "warning",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 481,
|
||||
"end_line": 489
|
||||
},
|
||||
{
|
||||
"start_line": 500,
|
||||
"end_line": 501
|
||||
},
|
||||
{
|
||||
"start_line": 517,
|
||||
"end_line": 517
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "14개 어댑터를 각각 카드로 반복하면 비교 필드가 흐려지고 밀도 예산을 넘으므로, 두 집합을 수량·의존 상태·클래스패스 상태·활성화 의미·측정 범위로 정렬했다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "baseline-schema",
|
||||
"title": "기준선 스키마의 관계",
|
||||
"question": "현재 기준선에서 users, pages, feed_items, highlights는 어떻게 연결되는가?",
|
||||
"type": "erd",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"백엔드 개발자"
|
||||
],
|
||||
"summary": "users와 pages가 feed_items에 연결되고, 각 feed_item은 여러 highlights를 가진다.",
|
||||
"alt": "users와 pages에서 feed_items로 모이고 highlights로 이어지는 기준선 관계도.",
|
||||
"long_description": "왼쪽의 users와 pages가 각각 중앙의 feed_items에 연결된다. feed_items는 오른쪽의 highlights로 이어진다. 간선은 user와 page 각각에 여러 feed_item이 연결되고, 한 feed_item에 여러 highlight가 연결되는 관계를 나타낸다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "baseline-schema",
|
||||
"line": 39
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "component-flow",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"rationale": "두 시작 엔티티가 feed_items로 모이고 highlights로 이어지는 명시 관계를 왼쪽에서 오른쪽으로 읽는 연결 구조가 가장 직접적이다.",
|
||||
"focus_node": "feed-items"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "users",
|
||||
"label": "users",
|
||||
"kind": "entity",
|
||||
"role": "source",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "pages",
|
||||
"label": "pages",
|
||||
"kind": "entity",
|
||||
"role": "source",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 36,
|
||||
"end_line": 36
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-items",
|
||||
"label": "feed_items",
|
||||
"kind": "entity",
|
||||
"role": "store",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "highlights",
|
||||
"label": "highlights",
|
||||
"kind": "entity",
|
||||
"role": "sink",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 37,
|
||||
"end_line": 37
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "users-have-feed-items",
|
||||
"from": "users",
|
||||
"to": "feed-items",
|
||||
"label": "여러 feed_item을 가짐",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "pages-have-feed-items",
|
||||
"from": "pages",
|
||||
"to": "feed-items",
|
||||
"label": "여러 feed_item이 딸림",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 36,
|
||||
"end_line": 36
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-items-have-highlights",
|
||||
"from": "feed-items",
|
||||
"to": "highlights",
|
||||
"label": "여러 highlights를 가짐",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 37,
|
||||
"end_line": 37
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "기준선에 명시된 세 관계만 같은 엔티티 추상화 수준에서 표현했다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "eager-lazy-query-sequence",
|
||||
"title": "EAGER 2차 조회는 반환 전에, LAZY highlights 조회는 매핑 접근 뒤에 실행된다",
|
||||
"question": "루트 피드 조회부터 EAGER ToOne과 LAZY highlights 조회까지 SQL은 어떤 순서로 발생하는가?",
|
||||
"type": "sequence",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"JPA·Hibernate를 사용하는 백엔드 개발자",
|
||||
"쿼리 성능 분석자"
|
||||
],
|
||||
"summary": "Hibernate는 feed_items를 먼저 조회하고 EAGER user·page를 2차 SELECT로 채운 뒤 반환하며, 매핑 중 getHighlights() 접근이 생긴 다음 LAZY highlights SELECT를 실행한다.",
|
||||
"alt": "loadFeed 매핑, Hibernate, PostgreSQL 사이에서 루트 SELECT, EAGER user·page 2차 SELECT, getHighlights 접근, LAZY highlights SELECT가 차례로 일어나는 시퀀스.",
|
||||
"long_description": "세 참가자를 왼쪽부터 loadFeed DTO 매핑, Hibernate, PostgreSQL 순으로 읽는다. loadFeed가 findAllBy 파생 쿼리를 호출하면 Hibernate가 PostgreSQL에서 feed_items를 먼저 조회한다. 이어 fetch join되지 않은 EAGER user와 page를 별도의 2차 SELECT로 채우고, 반환 시점까지 로딩된 FeedItem을 loadFeed에 돌려준다. 이후 DTO 매핑이 getHighlights()에 접근하면 Hibernate가 해당 아이템의 highlights 컬렉션 SELECT를 실행한다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "eager-lazy-query-sequence",
|
||||
"line": 324
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "sequence",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"payment-approval-sequence"
|
||||
],
|
||||
"rationale": "문서가 루트 조회, EAGER 2차 SELECT, 반환, 매핑 접근, LAZY SELECT의 시간 순서를 명시하므로 참가자별 메시지를 위에서 아래로 배열하는 sequence 구성이 적합하다.",
|
||||
"focus_node": "hibernate"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "load-feed-mapping",
|
||||
"label": "loadFeed DTO 매핑",
|
||||
"kind": "participant",
|
||||
"role": "participant",
|
||||
"description": "FeedItem을 순회하며 응답 DTO를 조립하고 highlights 게터에 접근하는 호출자.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 322,
|
||||
"end_line": 322
|
||||
},
|
||||
{
|
||||
"start_line": 449,
|
||||
"end_line": 451
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "hibernate",
|
||||
"label": "Hibernate",
|
||||
"kind": "participant",
|
||||
"role": "participant",
|
||||
"emphasis": "primary",
|
||||
"description": "파생 쿼리의 루트 조회와 EAGER 2차 SELECT, LAZY 컬렉션 초기화를 수행하는 JPA provider.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 320,
|
||||
"end_line": 322
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "postgresql",
|
||||
"label": "PostgreSQL",
|
||||
"kind": "participant",
|
||||
"role": "participant",
|
||||
"shape": "database",
|
||||
"description": "Hibernate가 루트 및 연관 SELECT를 실행하는 데이터베이스.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 417,
|
||||
"end_line": 422
|
||||
},
|
||||
{
|
||||
"start_line": 447,
|
||||
"end_line": 447
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "m1-find-all",
|
||||
"from": "load-feed-mapping",
|
||||
"to": "hibernate",
|
||||
"label": "findAllBy(...)",
|
||||
"kind": "request",
|
||||
"order": 1,
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 320,
|
||||
"end_line": 320
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "m2-root-select",
|
||||
"from": "hibernate",
|
||||
"to": "postgresql",
|
||||
"label": "SELECT feed_items",
|
||||
"kind": "data",
|
||||
"order": 2,
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 320,
|
||||
"end_line": 320
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "m3-eager-secondary-selects",
|
||||
"from": "hibernate",
|
||||
"to": "postgresql",
|
||||
"label": "SELECT user / page · EAGER 2차",
|
||||
"kind": "data",
|
||||
"order": 3,
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 318,
|
||||
"end_line": 321
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "m4-return-eager-loaded-items",
|
||||
"from": "hibernate",
|
||||
"to": "load-feed-mapping",
|
||||
"label": "EAGER 연관이 채워진 FeedItem 반환",
|
||||
"kind": "response",
|
||||
"style": "dashed",
|
||||
"order": 4,
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 318,
|
||||
"end_line": 320
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "m5-access-highlights",
|
||||
"from": "load-feed-mapping",
|
||||
"to": "hibernate",
|
||||
"label": "매핑 중 getHighlights() 접근",
|
||||
"kind": "request",
|
||||
"order": 5,
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 322,
|
||||
"end_line": 322
|
||||
},
|
||||
{
|
||||
"start_line": 449,
|
||||
"end_line": 451
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "m6-lazy-highlights-select",
|
||||
"from": "hibernate",
|
||||
"to": "postgresql",
|
||||
"label": "SELECT highlights WHERE feed_item_id = ?",
|
||||
"kind": "data",
|
||||
"order": 6,
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 322,
|
||||
"end_line": 322
|
||||
},
|
||||
{
|
||||
"start_line": 429,
|
||||
"end_line": 430
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "EAGER와 LAZY의 차이를 정적 관계가 아니라 실제 SQL 발생 순서와 접근 시점으로 보여준다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "nplus1-query-fanout",
|
||||
"title": "반환 부모 수 N이 컬렉션 초기화와 자식 SELECT 횟수를 결정한다",
|
||||
"question": "왜 한 번의 피드 요청에서 반환한 FeedItem 수 N이 Highlight 추가 조회 N회로 이어지는가?",
|
||||
"type": "data-flow",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"JPA 기반 피드 조회의 N+1 원인을 진단하는 개발자"
|
||||
],
|
||||
"summary": "한 페이지에서 N개의 FeedItem을 반환하면 각 부모의 Highlight 컬렉션을 한 번씩 초기화해 추가 SELECT도 N회 발생한다.",
|
||||
"alt": "FeedItem N개를 반환하는 loadFeed 요청이 컬렉션 초기화 N회와 Highlight SELECT N회로 이어지는 인과 흐름도.",
|
||||
"long_description": "왼쪽의 loadFeed 요청은 한 페이지에서 N개의 FeedItem을 반환한다. 매핑이 각 부모의 Highlight 컬렉션에 접근하면 컬렉션 초기화가 부모마다 한 번씩 일어나 총 N회가 된다. 현재 기준선에서는 배치나 서브셀렉트가 없어 초기화 한 번마다 Highlight SELECT도 한 번 실행되므로 추가 조회가 N회 발생한다. 각 SELECT는 해당 부모의 Highlight 자식 행을 전부 읽는다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "nplus1-query-fanout",
|
||||
"line": 380
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "component-flow",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"rationale": "원문은 서로 다른 저장소로 분산되는 라우팅이 아니라 반환 부모 수가 컬렉션 초기화와 반복 SELECT를 차례로 유발하는 인과 경로를 설명하므로 component-flow가 가장 정확하다.",
|
||||
"focus_node": "collection-initializations"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "feed-request",
|
||||
"label": "loadFeed(0, N) → FeedItem N개",
|
||||
"kind": "request",
|
||||
"role": "source",
|
||||
"shape": "box",
|
||||
"details": [
|
||||
"page size = 반환 부모 수 N"
|
||||
],
|
||||
"description": "한 요청에서 반환한 부모 수 N이 N+1 증가 계수가 되는 피드 조회.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 341,
|
||||
"end_line": 341
|
||||
},
|
||||
{
|
||||
"start_line": 398,
|
||||
"end_line": 398
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "collection-initializations",
|
||||
"label": "Highlight 컬렉션 초기화 N회",
|
||||
"kind": "operation",
|
||||
"role": "service",
|
||||
"shape": "box",
|
||||
"details": [
|
||||
"collectionFetches = N"
|
||||
],
|
||||
"emphasis": "primary",
|
||||
"description": "각 FeedItem의 지연 컬렉션 접근이 부모마다 한 번의 초기화를 만든 결과.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 337,
|
||||
"end_line": 337
|
||||
},
|
||||
{
|
||||
"start_line": 378,
|
||||
"end_line": 384
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "highlight-selects",
|
||||
"label": "Highlight SELECT N회",
|
||||
"kind": "query",
|
||||
"role": "sink",
|
||||
"shape": "box",
|
||||
"details": [
|
||||
"부모별 자식 행 전부 조회"
|
||||
],
|
||||
"description": "배치와 서브셀렉트가 없는 기준선에서 컬렉션 초기화마다 실행되는 자식 SELECT.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 337,
|
||||
"end_line": 337
|
||||
},
|
||||
{
|
||||
"start_line": 384,
|
||||
"end_line": 385
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "parents-trigger-initialization",
|
||||
"from": "feed-request",
|
||||
"to": "collection-initializations",
|
||||
"label": "아이템마다 컬렉션 접근",
|
||||
"kind": "request",
|
||||
"style": "solid",
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 384,
|
||||
"end_line": 384
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "initialization-runs-select",
|
||||
"from": "collection-initializations",
|
||||
"to": "highlight-selects",
|
||||
"label": "초기화마다 SELECT 1회",
|
||||
"kind": "request",
|
||||
"style": "solid",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 337,
|
||||
"end_line": 337
|
||||
},
|
||||
{
|
||||
"start_line": 384,
|
||||
"end_line": 384
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "정량 표는 본문에 남기고, 그림은 부모 수 N이 초기화와 SELECT 횟수 N을 만드는 단일 인과 관계에 집중한다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "query-port-boundary",
|
||||
"title": "조회 전략은 FeedQueryPort 뒤의 퍼시스턴스 어댑터에 격리된다",
|
||||
"question": "GET /feed 조회는 어떤 상위 계층을 거쳐 포트에 도달하며, 실제 조회 전략은 어디에 격리되는가?",
|
||||
"type": "architecture",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"백엔드 개발자",
|
||||
"아키텍처 검토자"
|
||||
],
|
||||
"summary": "FeedController는 조회 유스케이스를 호출하고, 유스케이스는 FeedQueryPort에 의존하며, FeedQueryAdapter가 포트를 구현해 PostgreSQL 조회 전략을 맡는다.",
|
||||
"alt": "GET /feed를 받는 FeedController에서 GetFeedUseCase와 FeedQueryPort로 이어지고 FeedQueryAdapter가 포트를 구현하는 포트·어댑터 구조.",
|
||||
"long_description": "왼쪽의 FeedController가 GET /feed 요청을 받아 중앙의 GetFeedUseCase에 조회를 위임한다. 유스케이스는 오른쪽의 FeedQueryPort에 조회를 의존한다. FeedQueryAdapter는 FeedQueryPort를 구현하는 아웃바운드 어댑터이며 PostgreSQL 조회를 수행한다. Fetch Join, Batch Fetch, DTO Projection, 윈도우 함수 같은 구체 전략은 이 어댑터의 책임이므로 상위 계층은 전략 교체의 영향을 받지 않는다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "query-port-boundary",
|
||||
"line": 297
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "ports-adapters",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"order-ports-adapters"
|
||||
],
|
||||
"rationale": "문서의 핵심은 상위 웹·애플리케이션 계층과 교체 가능한 조회 전략 사이의 포트 의존 및 어댑터 구현 방향이므로 ports-adapters 구성이 직접 답한다.",
|
||||
"focus_node": "get-feed-use-case"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "feed-controller",
|
||||
"label": "FeedController",
|
||||
"kind": "adapter",
|
||||
"role": "inbound-adapter",
|
||||
"details": [
|
||||
"GET /feed"
|
||||
],
|
||||
"description": "조회 입력과 FeedSummary 반환 형태만 아는 웹 계층.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "get-feed-use-case",
|
||||
"label": "GetFeedUseCase",
|
||||
"kind": "application",
|
||||
"role": "core",
|
||||
"shape": "hexagon",
|
||||
"emphasis": "primary",
|
||||
"description": "조회 사용자, 페이지 크기, FeedSummary 계약만 아는 애플리케이션 계층.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-query-port",
|
||||
"label": "FeedQueryPort",
|
||||
"kind": "interface",
|
||||
"role": "port",
|
||||
"shape": "port",
|
||||
"description": "상위 계층과 구체 조회 전략을 분리하는 조회 포트.",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
},
|
||||
{
|
||||
"start_line": 299,
|
||||
"end_line": 299
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-query-adapter",
|
||||
"label": "FeedQueryAdapter",
|
||||
"kind": "adapter",
|
||||
"role": "outbound-adapter",
|
||||
"description": "FeedQueryPort를 구현하며 구체 조회 전략을 책임지는 퍼시스턴스 어댑터.",
|
||||
"details": [
|
||||
"PostgreSQL 조회",
|
||||
"Fetch Join · Batch Fetch",
|
||||
"DTO Projection · 윈도우 함수"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
},
|
||||
{
|
||||
"start_line": 299,
|
||||
"end_line": 299
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "controller-to-use-case",
|
||||
"from": "feed-controller",
|
||||
"to": "get-feed-use-case",
|
||||
"label": "GET /feed 조회 위임",
|
||||
"kind": "request",
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "use-case-to-port",
|
||||
"from": "get-feed-use-case",
|
||||
"to": "feed-query-port",
|
||||
"label": "조회 의존",
|
||||
"kind": "dependency",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "adapter-implements-port",
|
||||
"from": "feed-query-adapter",
|
||||
"to": "feed-query-port",
|
||||
"label": "implements",
|
||||
"kind": "dependency",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 295,
|
||||
"end_line": 295
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "구체 조회 기법보다 웹·애플리케이션 계층, FeedQueryPort, 퍼시스턴스 어댑터 사이의 의존 경계를 한 수준에서 보여준다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "skew-profile",
|
||||
"title": "Zipf-like 분포만 무거운 머리와 긴 꼬리를 함께 재현한다",
|
||||
"question": "균일·정규분포와 비교할 때 왜 Zipf-like 분포가 하이라이트 조회의 스트레스 데이터에 적합한가?",
|
||||
"type": "concept",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"백엔드 엔지니어",
|
||||
"성능 실험 설계를 검토하는 독자"
|
||||
],
|
||||
"summary": "균일분포와 정규분포는 극단적으로 많은 소수를 없애지만, 선택한 Zipf-like 합성 분포는 무거운 머리와 긴 꼬리를 만들어 대량 하이라이트와 Top-N 필요성을 재현한다.",
|
||||
"alt": "균일분포, 정규분포, Zipf-like 합성 분포를 분포 형태와 극단적 소수, 스트레스 조건 재현 여부, 선택 결과로 나란히 비교한 도표.",
|
||||
"long_description": "왼쪽부터 균일분포, 정규분포, Zipf-like 합성 분포를 같은 네 기준으로 비교한다. 균일분포는 모든 아이템이 3개이고, 정규분포는 평균 근처에 몰려 둘 다 극단적으로 많은 소수를 만들지 못하므로 제외된다. Zipf-like 분포는 소수의 인기 아이템이 압도적인 무거운 머리와 나머지의 긴 꼬리를 만들며, 지수 s=1.15와 상한 500·하한 1을 사용해 매우 많은 하이라이트 조건과 Top-N 필요성을 재현하는 합성 스트레스 분포로 선택된다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "skew-profile",
|
||||
"line": 192
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "comparison",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"contract-comparison"
|
||||
],
|
||||
"rationale": "문서가 균일·정규분포를 제외하고 Zipf-like 분포를 선택한 근거를 같은 비교 기준으로 직접 대조하므로, 호출 관계를 만들지 않는 정렬된 comparison 구성이 핵심 주장에 가장 적합하다.",
|
||||
"focus_node": "zipf-like"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "uniform",
|
||||
"label": "균일분포",
|
||||
"kind": "distribution",
|
||||
"role": "option",
|
||||
"description": "모든 아이템에 하이라이트 3개를 주는 분포로, 매우 많은 하이라이트 조건을 재현하지 못한다.",
|
||||
"details": [
|
||||
"분포 형태: 모두 3개",
|
||||
"극단적 소수: 없음",
|
||||
"스트레스 조건: 재현 못함",
|
||||
"선택 결과: 제외"
|
||||
],
|
||||
"emphasis": "muted",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 177,
|
||||
"end_line": 177
|
||||
},
|
||||
{
|
||||
"start_line": 195,
|
||||
"end_line": 195
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "normal",
|
||||
"label": "정규분포",
|
||||
"kind": "distribution",
|
||||
"role": "option",
|
||||
"description": "평균 근처에 몰려 극단적으로 많은 소수와 무거운 머리를 만들지 못하는 분포다.",
|
||||
"details": [
|
||||
"분포 형태: 평균 근처 집중",
|
||||
"극단적 소수: 없음",
|
||||
"스트레스 조건: 재현 못함",
|
||||
"선택 결과: 제외"
|
||||
],
|
||||
"emphasis": "muted",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 177,
|
||||
"end_line": 177
|
||||
},
|
||||
{
|
||||
"start_line": 196,
|
||||
"end_line": 196
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "zipf-like",
|
||||
"label": "Zipf-like 합성 분포",
|
||||
"kind": "distribution",
|
||||
"role": "option",
|
||||
"description": "순위 기반 지수로 편중 강도를 조절하며 무거운 머리와 긴 꼬리를 재현하는 합성 스트레스 분포다.",
|
||||
"details": [
|
||||
"분포 형태: 무거운 머리 + 긴 꼬리",
|
||||
"극단적 소수: 있음 · 1위 500개",
|
||||
"스트레스 조건: 재현",
|
||||
"선택 결과: s=1.15 합성 분포"
|
||||
],
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 177,
|
||||
"end_line": 177
|
||||
},
|
||||
{
|
||||
"start_line": 181,
|
||||
"end_line": 181
|
||||
},
|
||||
{
|
||||
"start_line": 184,
|
||||
"end_line": 184
|
||||
},
|
||||
{
|
||||
"start_line": 188,
|
||||
"end_line": 190
|
||||
},
|
||||
{
|
||||
"start_line": 197,
|
||||
"end_line": 197
|
||||
},
|
||||
{
|
||||
"start_line": 203,
|
||||
"end_line": 203
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "세 분포를 동일한 네 항목으로 맞춰 비교하고, 문서가 직접 제시한 제외·선택 이유만 포함했다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "strategy-journey",
|
||||
"title": "두 형제 문제는 Fetch Join에서 합류한 뒤 단계별 해법으로 최종 구조가 된다",
|
||||
"question": "과제 요구사항에서 시작해 기준선의 N1·N2 분기와 실패·개선을 거쳐 최종 피드 조회 구조는 어떻게 발전하는가?",
|
||||
"type": "flow",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"백엔드 엔지니어",
|
||||
"성능 개선 과정을 검토하는 독자"
|
||||
],
|
||||
"summary": "요구사항과 모델을 거친 기준선에서 N1·N2가 동시에 갈라져 Fetch Join으로 합류하고, 실패 뒤 Batch Fetch·DTO Projection·Top-3·Keyset·가시성 인덱싱을 차례로 거쳐 최종 피드 조회 구조에 도달한다.",
|
||||
"alt": "요구사항과 모델에서 기준선으로 진행한 뒤 N1과 N2로 분기하고 Fetch Join에서 합류해, 실패와 다섯 개선 단계를 거쳐 최종 피드 조회 구조에 이르는 흐름도.",
|
||||
"long_description": "왼쪽에서 과제 요구사항, 도메인·데이터 모델, 최초 피드 조회 기준선 순으로 시작한다. 기준선에서 컬렉션 N+1(N1)과 User·Page 연관의 숨은 쿼리(N2)가 서로 앞뒤가 아닌 형제 문제로 동시에 갈라지고, 두 경로는 Fetch Join 시도에서 합류한다. 이 시도는 다중 컬렉션·페이징 실패로 이어진다. 마지막 노드는 Batch Fetch, DTO Projection, 아이템별 Top-3, Keyset Pagination, 가시성 조건 인덱싱을 거쳐 최종 피드 조회 구조에 도달하는 순서를 담는다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "strategy-journey",
|
||||
"line": 27
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "component-flow",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"rationale": "요구사항에서 최종 조회 구조까지 한 방향으로 발전하면서 기준선의 두 형제 문제가 분기했다가 Fetch Join 시도에서 합류하므로, 좌측 출발점·중앙의 분기와 합류·우측 종착점을 갖는 component-flow가 전체 여정을 가장 직접적으로 드러낸다.",
|
||||
"focus_node": "fetch-join-failure"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "requirements-model-baseline",
|
||||
"label": "요구사항·모델·기준선",
|
||||
"kind": "journey-stage",
|
||||
"role": "source",
|
||||
"description": "전체 여정은 과제 요구사항에서 도메인·데이터 모델을 거쳐 최초 피드 조회 기준선으로 시작한다.",
|
||||
"details": [
|
||||
"1 과제 요구사항",
|
||||
"2 도메인·데이터 모델",
|
||||
"3 최초 피드 조회"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "collection-n-plus-one",
|
||||
"label": "컬렉션 N+1 (N1)",
|
||||
"kind": "problem",
|
||||
"role": "service",
|
||||
"description": "기준선에서 N2와 동시에 나타나는 컬렉션 조회 문제다.",
|
||||
"details": [
|
||||
"유형: 컬렉션 N+1",
|
||||
"발생: 기준선과 동시에",
|
||||
"관계: N2와 형제 문제",
|
||||
"진행: Fetch Join으로 합류"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "hidden-to-one-queries",
|
||||
"label": "User·Page 숨은 쿼리 (N2)",
|
||||
"kind": "problem",
|
||||
"role": "service",
|
||||
"description": "기준선에서 N1과 동시에 나타나는 User·Page 연관의 숨은 쿼리 문제다.",
|
||||
"details": [
|
||||
"유형: User·Page 숨은 쿼리",
|
||||
"발생: 기준선과 동시에",
|
||||
"관계: N1과 형제 문제",
|
||||
"진행: Fetch Join으로 합류"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "fetch-join-failure",
|
||||
"label": "Fetch Join → 실패",
|
||||
"kind": "query-strategy",
|
||||
"role": "service",
|
||||
"description": "N1과 N2가 Fetch Join 시도에서 합류한 뒤 다중 컬렉션·페이징 실패로 이어진다.",
|
||||
"details": [
|
||||
"1 N1·N2 합류",
|
||||
"2 다중 컬렉션·페이징 실패"
|
||||
],
|
||||
"emphasis": "warning",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "improvement-chain",
|
||||
"label": "후속 개선 → 최종 구조",
|
||||
"kind": "query-strategy",
|
||||
"role": "sink",
|
||||
"description": "실패 뒤 다섯 조회 전략 단계가 명시된 순서로 발전해 최종 피드 조회 구조를 만든다.",
|
||||
"details": [
|
||||
"1 Batch Fetch",
|
||||
"2 DTO Projection",
|
||||
"3 아이템별 Top-3",
|
||||
"4 Keyset Pagination",
|
||||
"5 가시성 조건 인덱싱",
|
||||
"6 최종 피드 조회 구조"
|
||||
],
|
||||
"emphasis": "primary",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "baseline-reveals-n1",
|
||||
"from": "requirements-model-baseline",
|
||||
"to": "collection-n-plus-one",
|
||||
"label": "기준선에서 갈라짐",
|
||||
"kind": "problem",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "baseline-reveals-n2",
|
||||
"from": "requirements-model-baseline",
|
||||
"to": "hidden-to-one-queries",
|
||||
"label": "기준선에서 갈라짐",
|
||||
"kind": "problem",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "n1-joins-fetch-join",
|
||||
"from": "collection-n-plus-one",
|
||||
"to": "fetch-join-failure",
|
||||
"label": "Fetch Join으로 합류",
|
||||
"kind": "flow",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "n2-joins-fetch-join",
|
||||
"from": "hidden-to-one-queries",
|
||||
"to": "fetch-join-failure",
|
||||
"label": "Fetch Join으로 합류",
|
||||
"kind": "flow",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "failure-to-improvements",
|
||||
"from": "fetch-join-failure",
|
||||
"to": "improvement-chain",
|
||||
"label": "실패 뒤 단계별 개선",
|
||||
"kind": "improvement",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 25,
|
||||
"end_line": 25
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "line 25의 전체 순서를 유지하면서 문서 폭에 맞추기 위해 연속된 출발 세 단계, Fetch Join과 그 실패, 실패 뒤 다섯 개선 단계를 각각 하나의 단계 노드 안에 번호로 묶었다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"version": "1.1",
|
||||
"id": "target-schema",
|
||||
"title": "목표 스키마에 추가되는 mention 관계",
|
||||
"question": "목표 모델에서 기존 users는 mentioned 사용자 역할로 feed_item_mentions에 어떻게 연결되는가?",
|
||||
"type": "erd",
|
||||
"direction": "LR",
|
||||
"audience": [
|
||||
"백엔드 개발자"
|
||||
],
|
||||
"summary": "기준선 관계에 feed_items와 기존 users를 잇는 feed_item_mentions 관계가 추가된다.",
|
||||
"alt": "기존 users를 mentioned 사용자 역할로 재사용해 feed_item_mentions와 연결한 5노드 목표 관계도.",
|
||||
"long_description": "왼쪽의 users와 pages가 중앙의 feed_items에 연결된다. 오른쪽에는 highlights와 feed_item_mentions가 놓인다. feed_items는 두 엔티티에 각각 연결되고, 기존 users도 mentioned 사용자 역할로 feed_item_mentions에 연결된다.",
|
||||
"source_context": {
|
||||
"document": "/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md",
|
||||
"document_sha256": "1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde",
|
||||
"anchor": {
|
||||
"kind": "marker",
|
||||
"value": "target-schema",
|
||||
"line": 45
|
||||
}
|
||||
},
|
||||
"composition": {
|
||||
"profile": "component-flow",
|
||||
"diagram_only": true,
|
||||
"reference_ids": [
|
||||
"payment-event-flow"
|
||||
],
|
||||
"rationale": "기준선의 연결 경로와 feed_item_mentions를 통한 추가 연결을 하나의 방향성 있는 관계망으로 읽게 하는 구조가 목표 모델의 차이를 직접 드러낸다.",
|
||||
"focus_node": "feed-items"
|
||||
},
|
||||
"groups": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "users",
|
||||
"label": "users",
|
||||
"kind": "entity",
|
||||
"role": "source",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "pages",
|
||||
"label": "pages",
|
||||
"kind": "entity",
|
||||
"role": "source",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 36,
|
||||
"end_line": 36
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-items",
|
||||
"label": "feed_items",
|
||||
"kind": "entity",
|
||||
"role": "store",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "highlights",
|
||||
"label": "highlights",
|
||||
"kind": "entity",
|
||||
"role": "sink",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 37,
|
||||
"end_line": 37
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-item-mentions",
|
||||
"label": "feed_item_mentions",
|
||||
"kind": "entity",
|
||||
"role": "sink",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 43,
|
||||
"end_line": 43
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "users-have-feed-items",
|
||||
"from": "users",
|
||||
"to": "feed-items",
|
||||
"label": "여러 feed_item을 가짐",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 35,
|
||||
"end_line": 35
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "pages-have-feed-items",
|
||||
"from": "pages",
|
||||
"to": "feed-items",
|
||||
"label": "여러 feed_item이 딸림",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 36,
|
||||
"end_line": 36
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-items-have-highlights",
|
||||
"from": "feed-items",
|
||||
"to": "highlights",
|
||||
"label": "여러 highlights를 가짐",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 37,
|
||||
"end_line": 37
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "feed-items-to-mentions",
|
||||
"from": "feed-items",
|
||||
"to": "feed-item-mentions",
|
||||
"label": "피드 아이템을 연결",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 43,
|
||||
"end_line": 43
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
},
|
||||
{
|
||||
"id": "users-to-feed-item-mentions",
|
||||
"from": "users",
|
||||
"to": "feed-item-mentions",
|
||||
"label": "mentioned 사용자로 연결",
|
||||
"kind": "relationship",
|
||||
"evidence": [
|
||||
{
|
||||
"start_line": 43,
|
||||
"end_line": 43
|
||||
}
|
||||
],
|
||||
"assumption": false
|
||||
}
|
||||
],
|
||||
"legend": [],
|
||||
"metadata": {
|
||||
"rationale": "기준선 관계를 유지하면서 line 43의 feed_item_mentions가 기존 users를 mentioned 사용자 역할로 참조하는 관계만 추가했다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# assets — 발표 슬라이드용 스크린샷
|
||||
|
||||
[../n+1liner.md](../n+1liner.md)는 실측 수치·실행계획을 텍스트로 담아 그대로 렌더된다. 슬라이드에서 화면 캡처로 보여주고 싶을 때 아래를 여기에 저장한다.
|
||||
|
||||
| 파일명 | 캡처 대상 |
|
||||
|---|---|
|
||||
| `feed-nplus1-sql-log.png` | 피드 조회 시 highlights 조회가 아이템마다 반복되는 SQL 로그 |
|
||||
| `curve-console.png` | `=== L1 N=10/100/1000 ... collectionFetches=10/100/1000 ...` 곡선 콘솔 |
|
||||
| `explain-highlights-index-scan.png` | 반복되는 하이라이트 조회 EXPLAIN(`Index Scan ... Execution Time 0.173ms`) |
|
||||
| `explain-feed-items-seqscan.png` | 목록 쿼리 EXPLAIN(정렬키 인덱스 없어 Seq Scan + Sort) |
|
||||
|
||||
콘솔·실행계획 원문은 `./gradlew :app-bootstrap:test --tests '*FeedPersistenceIT*'` 실행 후 `src/app-bootstrap/build/test-results/test/TEST-*FeedPersistenceIT*.xml`의 system-out에서 뽑을 수 있다.
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="baseline-schema" name="기준선 스키마의 관계">
|
||||
<mxGraphModel dx="885" dy="305" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="885" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_users" value="users" tooltip="entity | Evidence: L35-L35" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="60.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_pages" value="pages" tooltip="entity | Evidence: L36-L36" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="196.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-items" value="feed_items" tooltip="entity | Evidence: L35-L35" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="380.0" y="128.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_highlights" value="highlights" tooltip="entity | Evidence: L37-L37" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="690.0" y="128.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_feed-items-have-highlights" value="여러 highlights를 가짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-items" target="n_highlights">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="610.0" y="132.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_pages-have-feed-items" value="여러 feed_item이 딸림" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_pages" target="n_feed-items">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="324.0" y="198.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_users-have-feed-items" value="여러 feed_item을 가짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_users" target="n_feed-items">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="324.0" y="121.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="885" height="305" viewBox="0 0 885 305" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">기준선 스키마의 관계</title>
|
||||
<desc id="diagram-description">왼쪽의 users와 pages가 각각 중앙의 feed_items에 연결된다. feed_items는 오른쪽의 highlights로 이어진다. 간선은 user와 page 각각에 여러 feed_item이 연결되고, 한 feed_item에 여러 highlight가 연결되는 관계를 나타낸다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"baseline-schema","profile":"component-flow"},"source_context":{"document":"/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md","document_sha256":"1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde","anchor":{"kind":"marker","value":"baseline-schema","line":39}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="885" height="305" />
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="530.0,160.0 610.0,160.0 610.0,160.0 690.0,160.0" data-evidence="37-37" />
|
||||
<rect class="edge-label-bg" x="544.0" y="118.0" width="131.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="610.0" y="133.0">여러 highlights를 가짐</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="220.0,228.0 300.0,228.0 300.0,169.0 380.0,169.0" data-evidence="36-36" />
|
||||
<rect class="edge-label-bg" x="261.4" y="184.5" width="125.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="324.0" y="199.5">여러 feed_item이 딸림</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="220.0,92.0 300.0,92.0 300.0,151.0 380.0,151.0" data-evidence="35-35" />
|
||||
<rect class="edge-label-bg" x="261.4" y="107.5" width="125.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="324.0" y="122.5">여러 feed_item을 가짐</text>
|
||||
<g id="node-users">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-source" data-evidence="35-35" x="70.0" y="60.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="90.0">users</text>
|
||||
</g>
|
||||
<g id="node-pages">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-source" data-evidence="36-36" x="70.0" y="196.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="226.0">pages</text>
|
||||
</g>
|
||||
<g id="node-feed-items">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-store" data-evidence="35-35" x="380.0" y="128.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="455.0" y="158.0">feed_items</text>
|
||||
</g>
|
||||
<g id="node-highlights">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-sink" data-evidence="37-37" x="690.0" y="128.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="765.0" y="158.0">highlights</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="eager-lazy-query-sequence" name="EAGER 2차 조회는 반환 전에, LAZY highlights 조회는 매핑 접근 뒤에 실행된다">
|
||||
<mxGraphModel dx="680" dy="542" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_load-feed-mapping" value="loadFeed DTO 매핑" tooltip="FeedItem을 순회하며 응답 DTO를 조립하고 highlights 게터에 접근하는 호출자. | Evidence: L322-L322, L449-L451" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_hibernate" value="Hibernate" tooltip="파생 쿼리의 루트 조회와 EAGER 2차 SELECT, LAZY 컬렉션 초기화를 수행하는 JPA provider. | Evidence: L320-L322" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="255.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_postgresql" value="PostgreSQL" tooltip="Hibernate가 루트 및 연관 SELECT를 실행하는 데이터베이스. | Evidence: L417-L422, L447-L447" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=cylinder3;boundedLbl=1;backgroundOutline=1;fillColor=#eef6fb;" vertex="1" parent="1">
|
||||
<mxGeometry x="465.0" y="35.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_m1-find-all" value="findAllBy(...)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_load-feed-mapping" target="n_hibernate">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="225.0" y="128.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_m2-root-select" value="SELECT feed_items" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_hibernate" target="n_postgresql">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="435.0" y="190.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_m3-eager-secondary-selects" value="SELECT user / page · EAGER 2차" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_hibernate" target="n_postgresql">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="435.0" y="252.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_m4-return-eager-loaded-items" value="EAGER 연관이 채워진 FeedItem 반환" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_hibernate" target="n_load-feed-mapping">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="225.0" y="314.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_m5-access-highlights" value="매핑 중 getHighlights() 접근" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_load-feed-mapping" target="n_hibernate">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="225.0" y="376.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_m6-lazy-highlights-select" value="SELECT highlights WHERE feed_item_id = ?" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_hibernate" target="n_postgresql">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="435.0" y="438.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="542" viewBox="0 0 680 542" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">EAGER 2차 조회는 반환 전에, LAZY highlights 조회는 매핑 접근 뒤에 실행된다</title>
|
||||
<desc id="diagram-description">세 참가자를 왼쪽부터 loadFeed DTO 매핑, Hibernate, PostgreSQL 순으로 읽는다. loadFeed가 findAllBy 파생 쿼리를 호출하면 Hibernate가 PostgreSQL에서 feed_items를 먼저 조회한다. 이어 fetch join되지 않은 EAGER user와 page를 별도의 2차 SELECT로 채우고, 반환 시점까지 로딩된 FeedItem을 loadFeed에 돌려준다. 이후 DTO 매핑이 getHighlights()에 접근하면 Hibernate가 해당 아이템의 highlights 컬렉션 SELECT를 실행한다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"eager-lazy-query-sequence","profile":"sequence"},"source_context":{"document":"/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md","document_sha256":"1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde","anchor":{"kind":"marker","value":"eager-lazy-query-sequence","line":324}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="680" height="542" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="322-322,449-451" x="45.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="120.0" y="65.0">loadFeed DTO 매핑</text>
|
||||
<line class="lifeline" x1="120.0" y1="99.0" x2="120.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-primary role-participant" data-evidence="320-322" x="255.0" y="35.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="330.0" y="65.0">Hibernate</text>
|
||||
<line class="lifeline" x1="330.0" y1="99.0" x2="330.0" y2="512.0" />
|
||||
<rect class="node-shape kind-participant emphasis-normal role-participant" data-evidence="417-422,447-447" x="465.0" y="45.7" width="150.0" height="42.7" /><ellipse class="node-shape kind-participant emphasis-normal role-participant" cx="540.0" cy="45.7" rx="75.0" ry="10.7" /><path class="storage-bottom" d="M 465.0 88.3 A 75.0 10.7 0 0 0 615.0 88.3" />
|
||||
<text class="node-label" x="540.0" y="65.0">PostgreSQL</text>
|
||||
<line class="lifeline" x1="540.0" y1="99.0" x2="540.0" y2="512.0" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,140.0 330.0,140.0" data-evidence="320-320" />
|
||||
<rect class="edge-label-bg" x="159.1" y="114.0" width="131.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="129.0">1. findAllBy(...)</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-primary" points="330.0,202.0 540.0,202.0" data-evidence="320-320" />
|
||||
<rect class="edge-label-bg" x="359.0" y="176.0" width="152.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="191.0">2. SELECT feed_items</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="330.0,264.0 540.0,264.0" data-evidence="318-321" />
|
||||
<rect class="edge-label-bg" x="318.8" y="238.0" width="232.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="253.0">3. SELECT user / page · EAGER 2차</text>
|
||||
<polyline class="edge kind-response style-dashed emphasis-normal semantic-dashed" points="330.0,326.0 120.0,326.0" data-evidence="318-320" />
|
||||
<rect class="edge-label-bg" x="122.2" y="300.0" width="205.6" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="315.0">4. EAGER 연관이 채워진 FeedItem 반환</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="120.0,388.0 330.0,388.0" data-evidence="322-322,449-451" />
|
||||
<rect class="edge-label-bg" x="128.9" y="362.0" width="192.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="225.0" y="377.0">5. 매핑 중 getHighlights() 접근</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-primary" points="330.0,450.0 540.0,450.0" data-evidence="322-322,429-430" />
|
||||
<rect class="edge-label-bg" x="281.9" y="424.0" width="306.1" height="22" rx="3" />
|
||||
<text class="edge-label" x="435.0" y="439.0">6. SELECT highlights WHERE feed_item_id = ?</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="nplus1-query-fanout" name="반환 부모 수 N이 컬렉션 초기화와 자식 SELECT 횟수를 결정한다">
|
||||
<mxGraphModel dx="964" dy="300" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="964" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_feed-request" value="loadFeed(0, N) → FeedItem N개<br/>page size = 반환 부모 수 N" tooltip="한 요청에서 반환한 부모 수 N이 N+1 증가 계수가 되는 피드 조회. | Evidence: L341-L341, L398-L398" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="60.0" width="181.0" height="91.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_collection-initializations" value="Highlight 컬렉션 초기화 N회<br/>collectionFetches = N" tooltip="각 FeedItem의 지연 컬렉션 접근이 부모마다 한 번의 초기화를 만든 결과. | Evidence: L337-L337, L378-L384" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="411.0" y="70.0" width="181.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_highlight-selects" value="Highlight SELECT N회<br/>부모별 자식 행 전부 조회" tooltip="배치와 서브셀렉트가 없는 기준선에서 컬렉션 초기화마다 실행되는 자식 SELECT. | Evidence: L337-L337, L384-L385" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="752.0" y="70.0" width="167.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_initialization-runs-select" value="초기화마다 SELECT 1회" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_collection-initializations" target="n_highlight-selects">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="672.0" y="77.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_parents-trigger-initialization" value="아이템마다 컬렉션 접근" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-request" target="n_collection-initializations">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="331.0" y="77.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="964" height="300" viewBox="0 0 964 300" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">반환 부모 수 N이 컬렉션 초기화와 자식 SELECT 횟수를 결정한다</title>
|
||||
<desc id="diagram-description">왼쪽의 loadFeed 요청은 한 페이지에서 N개의 FeedItem을 반환한다. 매핑이 각 부모의 Highlight 컬렉션에 접근하면 컬렉션 초기화가 부모마다 한 번씩 일어나 총 N회가 된다. 현재 기준선에서는 배치나 서브셀렉트가 없어 초기화 한 번마다 Highlight SELECT도 한 번 실행되므로 추가 조회가 N회 발생한다. 각 SELECT는 해당 부모의 Highlight 자식 행을 전부 읽는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"nplus1-query-fanout","profile":"component-flow"},"source_context":{"document":"/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md","document_sha256":"1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde","anchor":{"kind":"marker","value":"nplus1-query-fanout","line":380}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="964" height="300" />
|
||||
<polyline class="edge kind-request style-solid emphasis-normal" points="592.0,105.5 672.0,105.5 672.0,105.5 752.0,105.5" data-evidence="337-337,384-384" />
|
||||
<rect class="edge-label-bg" x="612.8" y="63.5" width="118.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="672.0" y="78.5">초기화마다 SELECT 1회</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="251.0,105.5 331.0,105.5 331.0,105.5 411.0,105.5" data-evidence="384-384" />
|
||||
<rect class="edge-label-bg" x="281.8" y="63.5" width="98.4" height="22" rx="3" />
|
||||
<text class="edge-label" x="331.0" y="78.5">아이템마다 컬렉션 접근</text>
|
||||
<g id="node-feed-request">
|
||||
<rect class="node-shape kind-request emphasis-normal role-source" data-evidence="341-341,398-398" x="70.0" y="60.0" width="181.0" height="91.0" rx="7" />
|
||||
<text class="node-label" x="160.5" y="87.0">loadFeed(0, N) →</text>
|
||||
<text class="node-label" x="160.5" y="105.0">FeedItem N개</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="126.0" x2="237.0" y2="126.0" />
|
||||
<text class="node-detail" x="86.0" y="143.0">page size = 반환 부모 수 N</text>
|
||||
</g>
|
||||
<g id="node-collection-initializations">
|
||||
<rect class="node-shape kind-operation emphasis-primary role-service" data-evidence="337-337,378-384" x="411.0" y="70.0" width="181.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="501.5" y="97.0">Highlight 컬렉션 초기화 N회</text>
|
||||
<line class="node-detail-divider" x1="425.0" y1="118.0" x2="578.0" y2="118.0" />
|
||||
<text class="node-detail" x="427.0" y="135.0">collectionFetches = N</text>
|
||||
</g>
|
||||
<g id="node-highlight-selects">
|
||||
<rect class="node-shape kind-query emphasis-normal role-sink" data-evidence="337-337,384-385" x="752.0" y="70.0" width="167.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="835.5" y="97.0">Highlight SELECT N회</text>
|
||||
<line class="node-detail-divider" x1="766.0" y1="118.0" x2="905.0" y2="118.0" />
|
||||
<text class="node-detail" x="768.0" y="135.0">부모별 자식 행 전부 조회</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="query-port-boundary" name="조회 전략은 FeedQueryPort 뒤의 퍼시스턴스 어댑터에 격리된다">
|
||||
<mxGraphModel dx="1090" dy="360" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1090" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_get-feed-use-case" value="GetFeedUseCase" tooltip="조회 사용자, 페이지 크기, FeedSummary 계약만 아는 애플리케이션 계층. | Evidence: L295-L295" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=hexagon;perimeter=hexagonPerimeter2;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="420.0" y="185.0" width="230.0" height="130.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-controller" value="FeedController<br/>GET /feed" tooltip="조회 입력과 FeedSummary 반환 형태만 아는 웹 계층. | Evidence: L295-L295" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="85.0" width="210.0" height="94.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-query-adapter" value="FeedQueryAdapter<br/>PostgreSQL 조회<br/>Fetch Join · Batch Fetch<br/>DTO Projection · 윈도우 함수" tooltip="FeedQueryPort를 구현하며 구체 조회 전략을 책임지는 퍼시스턴스 어댑터. | Evidence: L295-L295, L299-L299" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="825.0" y="85.0" width="220.0" height="126.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-query-port" value="FeedQueryPort" tooltip="상위 계층과 구체 조회 전략을 분리하는 조회 포트. | Evidence: L295-L295, L299-L299" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;rounded=1;arcSize=50;fillColor=#f6f1fb;" vertex="1" parent="1">
|
||||
<mxGeometry x="670.0" y="155.0" width="125.0" height="58.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_adapter-implements-port" value="implements" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-query-adapter" target="n_feed-query-port">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="834.8" y="231.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_controller-to-use-case" value="GET /feed 조회 위임" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-controller" target="n_get-feed-use-case">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="361.5" y="191.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_use-case-to-port" value="조회 의존" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_get-feed-use-case" target="n_feed-query-port">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="704.0" y="132.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1090" height="360" viewBox="0 0 1090 360" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">조회 전략은 FeedQueryPort 뒤의 퍼시스턴스 어댑터에 격리된다</title>
|
||||
<desc id="diagram-description">왼쪽의 FeedController가 GET /feed 요청을 받아 중앙의 GetFeedUseCase에 조회를 위임한다. 유스케이스는 오른쪽의 FeedQueryPort에 조회를 의존한다. FeedQueryAdapter는 FeedQueryPort를 구현하는 아웃바운드 어댑터이며 PostgreSQL 조회를 수행한다. Fetch Join, Batch Fetch, DTO Projection, 윈도우 함수 같은 구체 전략은 이 어댑터의 책임이므로 상위 계층은 전략 교체의 영향을 받지 않는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"query-port-boundary","profile":"ports-adapters"},"source_context":{"document":"/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md","document_sha256":"1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde","anchor":{"kind":"marker","value":"query-port-boundary","line":297}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="1090" height="360" />
|
||||
<polyline class="edge kind-dependency style-solid emphasis-normal" points="935.0,211.0 935.0,259.0 732.5,259.0 732.5,213.0" data-evidence="295-295" />
|
||||
<rect class="edge-label-bg" x="792.2" y="217.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="834.8" y="232.0">implements</text>
|
||||
<polyline class="edge kind-request style-solid emphasis-primary" points="255.0,132.0 337.5,132.0 337.5,250.0 420.0,250.0" data-evidence="295-295" />
|
||||
<rect class="edge-label-bg" x="302.2" y="177.0" width="118.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="361.5" y="192.0">GET /feed 조회 위임</text>
|
||||
<polyline class="edge kind-dependency style-solid emphasis-normal" points="650.0,250.0 680.0,250.0 680.0,119.0 640.0,119.0 640.0,184.0 670.0,184.0" data-evidence="295-295" />
|
||||
<rect class="edge-label-bg" x="678.2" y="118.0" width="51.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="704.0" y="133.0">조회 의존</text>
|
||||
<g id="node-get-feed-use-case">
|
||||
<polygon class="node-shape kind-application emphasis-primary role-core" data-evidence="295-295" points="454.0,185.0 616.0,185.0 650.0,250.0 616.0,315.0 454.0,315.0 420.0,250.0" />
|
||||
<text class="node-role" x="535.0" y="204.0">«core»</text>
|
||||
<text class="node-label" x="535.0" y="248.0">GetFeedUseCase</text>
|
||||
</g>
|
||||
<g id="node-feed-controller">
|
||||
<rect class="node-shape kind-adapter emphasis-normal role-inbound-adapter" data-evidence="295-295" x="45.0" y="85.0" width="210.0" height="94.0" rx="7" />
|
||||
<text class="node-role" x="150.0" y="104.0">«inbound-adapter»</text>
|
||||
<text class="node-label" x="150.0" y="131.0">FeedController</text>
|
||||
<line class="node-detail-divider" x1="59.0" y1="152.0" x2="241.0" y2="152.0" />
|
||||
<text class="node-detail" x="61.0" y="169.0">GET /feed</text>
|
||||
</g>
|
||||
<g id="node-feed-query-adapter">
|
||||
<rect class="node-shape kind-adapter emphasis-normal role-outbound-adapter" data-evidence="295-295,299-299" x="825.0" y="85.0" width="220.0" height="126.0" rx="7" />
|
||||
<text class="node-role" x="935.0" y="104.0">«outbound-adapter»</text>
|
||||
<text class="node-label" x="935.0" y="131.0">FeedQueryAdapter</text>
|
||||
<line class="node-detail-divider" x1="839.0" y1="152.0" x2="1031.0" y2="152.0" />
|
||||
<text class="node-detail" x="841.0" y="169.0">PostgreSQL 조회</text>
|
||||
<text class="node-detail" x="841.0" y="185.0">Fetch Join · Batch Fetch</text>
|
||||
<text class="node-detail" x="841.0" y="201.0">DTO Projection · 윈도우 함수</text>
|
||||
</g>
|
||||
<g id="node-feed-query-port">
|
||||
<rect class="node-shape kind-interface emphasis-normal role-port" data-evidence="295-295,299-299" x="670.0" y="155.0" width="125.0" height="58.0" rx="22" />
|
||||
<text class="node-role" x="732.5" y="174.0">«port»</text>
|
||||
<text class="node-label" x="732.5" y="201.0">FeedQueryPort</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="skew-profile" name="Zipf-like 분포만 무거운 머리와 긴 꼬리를 함께 재현한다">
|
||||
<mxGraphModel dx="854" dy="300" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="854" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_uniform" value="균일분포<br/>분포 형태: 모두 3개<br/>극단적 소수: 없음<br/>스트레스 조건: 재현 못함<br/>선택 결과: 제외" tooltip="모든 아이템에 하이라이트 3개를 주는 분포로, 매우 많은 하이라이트 조건을 재현하지 못한다. | Evidence: L177-L177, L195-L195" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#9ca3af;fillColor=#f9fafb;" vertex="1" parent="1">
|
||||
<mxGeometry x="45.0" y="45.0" width="220.0" height="122.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_normal" value="정규분포<br/>분포 형태: 평균 근처 집중<br/>극단적 소수: 없음<br/>스트레스 조건: 재현 못함<br/>선택 결과: 제외" tooltip="평균 근처에 몰려 극단적으로 많은 소수와 무거운 머리를 만들지 못하는 분포다. | Evidence: L177-L177, L196-L196" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#9ca3af;fillColor=#f9fafb;" vertex="1" parent="1">
|
||||
<mxGeometry x="317.0" y="45.0" width="220.0" height="122.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_zipf-like" value="Zipf-like 합성 분포<br/>분포 형태: 무거운 머리 + 긴 꼬리<br/>극단적 소수: 있음 · 1위 500개<br/>스트레스 조건: 재현<br/>선택 결과: s=1.15 합성 분포" tooltip="순위 기반 지수로 편중 강도를 조절하며 무거운 머리와 긴 꼬리를 재현하는 합성 스트레스 분포다. | Evidence: L177-L177, L181-L181, L184-L184, L188-L190, L197-L197, L203-L203" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="589.0" y="45.0" width="220.0" height="122.0" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="854" height="300" viewBox="0 0 854 300" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">Zipf-like 분포만 무거운 머리와 긴 꼬리를 함께 재현한다</title>
|
||||
<desc id="diagram-description">왼쪽부터 균일분포, 정규분포, Zipf-like 합성 분포를 같은 네 기준으로 비교한다. 균일분포는 모든 아이템이 3개이고, 정규분포는 평균 근처에 몰려 둘 다 극단적으로 많은 소수를 만들지 못하므로 제외된다. Zipf-like 분포는 소수의 인기 아이템이 압도적인 무거운 머리와 나머지의 긴 꼬리를 만들며, 지수 s=1.15와 상한 500·하한 1을 사용해 매우 많은 하이라이트 조건과 Top-N 필요성을 재현하는 합성 스트레스 분포로 선택된다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"skew-profile","profile":"comparison"},"source_context":{"document":"/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md","document_sha256":"1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde","anchor":{"kind":"marker","value":"skew-profile","line":192}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="854" height="300" />
|
||||
<g id="node-uniform">
|
||||
<rect class="node-shape kind-distribution emphasis-muted role-option" data-evidence="177-177,195-195" x="45.0" y="45.0" width="220.0" height="122.0" rx="7" />
|
||||
<text class="node-label" x="155.0" y="72.0">균일분포</text>
|
||||
<line class="node-detail-divider" x1="59.0" y1="93.0" x2="251.0" y2="93.0" />
|
||||
<text class="node-detail" x="61.0" y="110.0">분포 형태: 모두 3개</text>
|
||||
<text class="node-detail" x="61.0" y="126.0">극단적 소수: 없음</text>
|
||||
<text class="node-detail" x="61.0" y="142.0">스트레스 조건: 재현 못함</text>
|
||||
<text class="node-detail" x="61.0" y="158.0">선택 결과: 제외</text>
|
||||
</g>
|
||||
<g id="node-normal">
|
||||
<rect class="node-shape kind-distribution emphasis-muted role-option" data-evidence="177-177,196-196" x="317.0" y="45.0" width="220.0" height="122.0" rx="7" />
|
||||
<text class="node-label" x="427.0" y="72.0">정규분포</text>
|
||||
<line class="node-detail-divider" x1="331.0" y1="93.0" x2="523.0" y2="93.0" />
|
||||
<text class="node-detail" x="333.0" y="110.0">분포 형태: 평균 근처 집중</text>
|
||||
<text class="node-detail" x="333.0" y="126.0">극단적 소수: 없음</text>
|
||||
<text class="node-detail" x="333.0" y="142.0">스트레스 조건: 재현 못함</text>
|
||||
<text class="node-detail" x="333.0" y="158.0">선택 결과: 제외</text>
|
||||
</g>
|
||||
<g id="node-zipf-like">
|
||||
<rect class="node-shape kind-distribution emphasis-primary role-option" data-evidence="177-177,181-181,184-184,188-190,197-197,203-203" x="589.0" y="45.0" width="220.0" height="122.0" rx="7" />
|
||||
<text class="node-label" x="699.0" y="72.0">Zipf-like 합성 분포</text>
|
||||
<line class="node-detail-divider" x1="603.0" y1="93.0" x2="795.0" y2="93.0" />
|
||||
<text class="node-detail" x="605.0" y="110.0">분포 형태: 무거운 머리 + 긴 꼬리</text>
|
||||
<text class="node-detail" x="605.0" y="126.0">극단적 소수: 있음 · 1위 500개</text>
|
||||
<text class="node-detail" x="605.0" y="142.0">스트레스 조건: 재현</text>
|
||||
<text class="node-detail" x="605.0" y="158.0">선택 결과: s=1.15 합성 분포</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="strategy-journey" name="두 형제 문제는 Fetch Join에서 합류한 뒤 단계별 해법으로 최종 구조가 된다">
|
||||
<mxGraphModel dx="1236" dy="421" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1236" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_requirements-model-baseline" value="요구사항·모델·기준선<br/>1 과제 요구사항<br/>2 도메인·데이터 모델<br/>3 최초 피드 조회" tooltip="전체 여정은 과제 요구사항에서 도메인·데이터 모델을 거쳐 최초 피드 조회 기준선으로 시작한다. | Evidence: L25-L25" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="165.5" width="150.0" height="105.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_collection-n-plus-one" value="컬렉션 N+1 (N1)<br/>유형: 컬렉션 N+1<br/>발생: 기준선과 동시에<br/>관계: N2와 형제 문제<br/>진행: Fetch Join으로 합류" tooltip="기준선에서 N2와 동시에 나타나는 컬렉션 조회 문제다. | Evidence: L25-L25" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="383.5" y="60.0" width="167.0" height="122.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_hidden-to-one-queries" value="User·Page 숨은 쿼리 (N2)<br/>유형: User·Page 숨은 쿼리<br/>발생: 기준선과 동시에<br/>관계: N1과 형제 문제<br/>진행: Fetch Join으로 합류" tooltip="기준선에서 N1과 동시에 나타나는 User·Page 연관의 숨은 쿼리 문제다. | Evidence: L25-L25" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="380.0" y="254.0" width="174.0" height="122.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_fetch-join-failure" value="Fetch Join → 실패<br/>1 N1·N2 합류<br/>2 다중 컬렉션·페이징 실패" tooltip="N1과 N2가 Fetch Join 시도에서 합류한 뒤 다중 컬렉션·페이징 실패로 이어진다. | Evidence: L25-L25" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
|
||||
<mxGeometry x="714.0" y="174.0" width="150.0" height="88.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_improvement-chain" value="후속 개선 → 최종 구조<br/>1 Batch Fetch<br/>2 DTO Projection<br/>3 아이템별 Top-3<br/>4 Keyset Pagination<br/>5 가시성 조건 인덱싱<br/>6 최종 피드 조회 구조" tooltip="실패 뒤 다섯 조회 전략 단계가 명시된 순서로 발전해 최종 피드 조회 구조를 만든다. | Evidence: L25-L25" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="1024.0" y="140.0" width="167.0" height="156.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_baseline-reveals-n1" value="기준선에서 갈라짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_requirements-model-baseline" target="n_collection-n-plus-one">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="325.8" y="165.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_baseline-reveals-n2" value="기준선에서 갈라짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_requirements-model-baseline" target="n_hidden-to-one-queries">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="324.0" y="271.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_failure-to-improvements" value="실패 뒤 단계별 개선" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_fetch-join-failure" target="n_improvement-chain">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="944.0" y="190.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_n1-joins-fetch-join" value="Fetch Join으로 합류" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_collection-n-plus-one" target="n_fetch-join-failure">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="656.2" y="165.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_n2-joins-fetch-join" value="Fetch Join으로 합류" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_hidden-to-one-queries" target="n_fetch-join-failure">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="658.0" y="271.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1236" height="421" viewBox="0 0 1236 421" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">두 형제 문제는 Fetch Join에서 합류한 뒤 단계별 해법으로 최종 구조가 된다</title>
|
||||
<desc id="diagram-description">왼쪽에서 과제 요구사항, 도메인·데이터 모델, 최초 피드 조회 기준선 순으로 시작한다. 기준선에서 컬렉션 N+1(N1)과 User·Page 연관의 숨은 쿼리(N2)가 서로 앞뒤가 아닌 형제 문제로 동시에 갈라지고, 두 경로는 Fetch Join 시도에서 합류한다. 이 시도는 다중 컬렉션·페이징 실패로 이어진다. 마지막 노드는 Batch Fetch, DTO Projection, 아이템별 Top-3, Keyset Pagination, 가시성 조건 인덱싱을 거쳐 최종 피드 조회 구조에 도달하는 순서를 담는다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"strategy-journey","profile":"component-flow"},"source_context":{"document":"/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md","document_sha256":"1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde","anchor":{"kind":"marker","value":"strategy-journey","line":27}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="1236" height="421" />
|
||||
<polyline class="edge kind-problem style-solid emphasis-normal" points="220.0,209.0 301.8,209.0 301.8,121.0 383.5,121.0" data-evidence="25-25" />
|
||||
<rect class="edge-label-bg" x="286.6" y="151.0" width="78.3" height="22" rx="3" />
|
||||
<text class="edge-label" x="325.8" y="166.0">기준선에서 갈라짐</text>
|
||||
<polyline class="edge kind-problem style-solid emphasis-normal" points="220.0,227.0 300.0,227.0 300.0,315.0 380.0,315.0" data-evidence="25-25" />
|
||||
<rect class="edge-label-bg" x="284.9" y="257.0" width="78.3" height="22" rx="3" />
|
||||
<text class="edge-label" x="324.0" y="272.0">기준선에서 갈라짐</text>
|
||||
<polyline class="edge kind-improvement style-solid emphasis-normal" points="864.0,218.0 944.0,218.0 944.0,218.0 1024.0,218.0" data-evidence="25-25" />
|
||||
<rect class="edge-label-bg" x="898.1" y="176.0" width="91.7" height="22" rx="3" />
|
||||
<text class="edge-label" x="944.0" y="191.0">실패 뒤 단계별 개선</text>
|
||||
<polyline class="edge kind-flow style-solid emphasis-normal" points="550.5,121.0 632.2,121.0 632.2,209.0 714.0,209.0" data-evidence="25-25" />
|
||||
<rect class="edge-label-bg" x="597.0" y="151.0" width="118.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="656.2" y="166.0">Fetch Join으로 합류</text>
|
||||
<polyline class="edge kind-flow style-solid emphasis-normal" points="554.0,315.0 634.0,315.0 634.0,227.0 714.0,227.0" data-evidence="25-25" />
|
||||
<rect class="edge-label-bg" x="598.8" y="257.0" width="118.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="658.0" y="272.0">Fetch Join으로 합류</text>
|
||||
<g id="node-requirements-model-baseline">
|
||||
<rect class="node-shape kind-journey-stage emphasis-normal role-source" data-evidence="25-25" x="70.0" y="165.5" width="150.0" height="105.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="192.5">요구사항·모델·기준선</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="213.5" x2="206.0" y2="213.5" />
|
||||
<text class="node-detail" x="86.0" y="230.5">1 과제 요구사항</text>
|
||||
<text class="node-detail" x="86.0" y="246.5">2 도메인·데이터 모델</text>
|
||||
<text class="node-detail" x="86.0" y="262.5">3 최초 피드 조회</text>
|
||||
</g>
|
||||
<g id="node-collection-n-plus-one">
|
||||
<rect class="node-shape kind-problem emphasis-normal role-service" data-evidence="25-25" x="383.5" y="60.0" width="167.0" height="122.0" rx="7" />
|
||||
<text class="node-label" x="467.0" y="87.0">컬렉션 N+1 (N1)</text>
|
||||
<line class="node-detail-divider" x1="397.5" y1="108.0" x2="536.5" y2="108.0" />
|
||||
<text class="node-detail" x="399.5" y="125.0">유형: 컬렉션 N+1</text>
|
||||
<text class="node-detail" x="399.5" y="141.0">발생: 기준선과 동시에</text>
|
||||
<text class="node-detail" x="399.5" y="157.0">관계: N2와 형제 문제</text>
|
||||
<text class="node-detail" x="399.5" y="173.0">진행: Fetch Join으로 합류</text>
|
||||
</g>
|
||||
<g id="node-hidden-to-one-queries">
|
||||
<rect class="node-shape kind-problem emphasis-normal role-service" data-evidence="25-25" x="380.0" y="254.0" width="174.0" height="122.0" rx="7" />
|
||||
<text class="node-label" x="467.0" y="281.0">User·Page 숨은 쿼리 (N2)</text>
|
||||
<line class="node-detail-divider" x1="394.0" y1="302.0" x2="540.0" y2="302.0" />
|
||||
<text class="node-detail" x="396.0" y="319.0">유형: User·Page 숨은 쿼리</text>
|
||||
<text class="node-detail" x="396.0" y="335.0">발생: 기준선과 동시에</text>
|
||||
<text class="node-detail" x="396.0" y="351.0">관계: N1과 형제 문제</text>
|
||||
<text class="node-detail" x="396.0" y="367.0">진행: Fetch Join으로 합류</text>
|
||||
</g>
|
||||
<g id="node-fetch-join-failure">
|
||||
<rect class="node-shape kind-query-strategy emphasis-warning role-service" data-evidence="25-25" x="714.0" y="174.0" width="150.0" height="88.0" rx="7" />
|
||||
<text class="node-label" x="789.0" y="201.0">Fetch Join → 실패</text>
|
||||
<line class="node-detail-divider" x1="728.0" y1="222.0" x2="850.0" y2="222.0" />
|
||||
<text class="node-detail" x="730.0" y="239.0">1 N1·N2 합류</text>
|
||||
<text class="node-detail" x="730.0" y="255.0">2 다중 컬렉션·페이징 실패</text>
|
||||
</g>
|
||||
<g id="node-improvement-chain">
|
||||
<rect class="node-shape kind-query-strategy emphasis-primary role-sink" data-evidence="25-25" x="1024.0" y="140.0" width="167.0" height="156.0" rx="7" />
|
||||
<text class="node-label" x="1107.5" y="168.0">후속 개선 → 최종 구조</text>
|
||||
<line class="node-detail-divider" x1="1038.0" y1="189.0" x2="1177.0" y2="189.0" />
|
||||
<text class="node-detail" x="1040.0" y="206.0">1 Batch Fetch</text>
|
||||
<text class="node-detail" x="1040.0" y="222.0">2 DTO Projection</text>
|
||||
<text class="node-detail" x="1040.0" y="238.0">3 아이템별 Top-3</text>
|
||||
<text class="node-detail" x="1040.0" y="254.0">4 Keyset Pagination</text>
|
||||
<text class="node-detail" x="1040.0" y="270.0">5 가시성 조건 인덱싱</text>
|
||||
<text class="node-detail" x="1040.0" y="286.0">6 최종 피드 조회 구조</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
|
||||
<diagram id="target-schema" name="목표 스키마에 추가되는 mention 관계">
|
||||
<mxGraphModel dx="895" dy="305" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="895" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="n_users" value="users" tooltip="entity | Evidence: L35-L35" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="60.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_pages" value="pages" tooltip="entity | Evidence: L36-L36" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="70.0" y="196.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-items" value="feed_items" tooltip="entity | Evidence: L35-L35" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="380.0" y="128.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_feed-item-mentions" value="feed_item_mentions" tooltip="entity | Evidence: L43-L43" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="690.0" y="60.0" width="160.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_highlights" value="highlights" tooltip="entity | Evidence: L37-L37" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
|
||||
<mxGeometry x="695.0" y="196.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_feed-items-have-highlights" value="여러 highlights를 가짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-items" target="n_highlights">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="636.5" y="198.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_feed-items-to-mentions" value="피드 아이템을 연결" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-items" target="n_feed-item-mentions">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="634.0" y="126.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_pages-have-feed-items" value="여러 feed_item이 딸림" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_pages" target="n_feed-items">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="324.0" y="198.5" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_users-have-feed-items" value="여러 feed_item을 가짐" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_users" target="n_feed-items">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="324.0" y="126.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_users-to-feed-item-mentions" value="mentioned 사용자로 연결" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_users" target="n_feed-item-mentions">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="455.0" y="55.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="895" height="305" viewBox="0 0 895 305" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">목표 스키마에 추가되는 mention 관계</title>
|
||||
<desc id="diagram-description">왼쪽의 users와 pages가 중앙의 feed_items에 연결된다. 오른쪽에는 highlights와 feed_item_mentions가 놓인다. feed_items는 두 엔티티에 각각 연결되고, 기존 users도 mentioned 사용자 역할로 feed_item_mentions에 연결된다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"target-schema","profile":"component-flow"},"source_context":{"document":"/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md","document_sha256":"1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde","anchor":{"kind":"marker","value":"target-schema","line":45}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.canvas { fill: #ffffff; }
|
||||
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
|
||||
.group-label-bg { fill: #ffffff; }
|
||||
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
|
||||
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
|
||||
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
|
||||
.edge.style-dotted { stroke-dasharray: 2 5; }
|
||||
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.edge.emphasis-muted { stroke: #9ca3af; }
|
||||
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
|
||||
.edge-label-bg { fill: #ffffff; }
|
||||
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
|
||||
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
|
||||
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
|
||||
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
|
||||
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
|
||||
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
|
||||
.node-shape.assumption { stroke-dasharray: 4 4; }
|
||||
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
|
||||
.controller-led { fill: #4b5563; }
|
||||
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
|
||||
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
|
||||
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
|
||||
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
|
||||
.node-detail { font-size: 11px; fill: #374151; }
|
||||
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
|
||||
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
|
||||
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
|
||||
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
|
||||
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
|
||||
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
|
||||
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
|
||||
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
|
||||
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="895" height="305" />
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="530.0,169.0 612.5,169.0 612.5,228.0 695.0,228.0" data-evidence="37-37" />
|
||||
<rect class="edge-label-bg" x="570.5" y="184.5" width="131.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="636.5" y="199.5">여러 highlights를 가짐</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="530.0,151.0 610.0,151.0 610.0,101.0 690.0,101.0" data-evidence="43-43" />
|
||||
<rect class="edge-label-bg" x="591.5" y="112.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="634.0" y="127.0">피드 아이템을 연결</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="220.0,228.0 300.0,228.0 300.0,169.0 380.0,169.0" data-evidence="36-36" />
|
||||
<rect class="edge-label-bg" x="261.4" y="184.5" width="125.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="324.0" y="199.5">여러 feed_item이 딸림</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="220.0,101.0 300.0,101.0 300.0,151.0 380.0,151.0" data-evidence="35-35" />
|
||||
<rect class="edge-label-bg" x="261.4" y="112.0" width="125.2" height="22" rx="3" />
|
||||
<text class="edge-label" x="324.0" y="127.0">여러 feed_item을 가짐</text>
|
||||
<polyline class="edge kind-relationship style-solid emphasis-normal" points="220.0,83.0 455.0,83.0 455.0,83.0 690.0,83.0" data-evidence="43-43" />
|
||||
<rect class="edge-label-bg" x="389.1" y="41.0" width="131.9" height="22" rx="3" />
|
||||
<text class="edge-label" x="455.0" y="56.0">mentioned 사용자로 연결</text>
|
||||
<g id="node-users">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-source" data-evidence="35-35" x="70.0" y="60.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="90.0">users</text>
|
||||
</g>
|
||||
<g id="node-pages">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-source" data-evidence="36-36" x="70.0" y="196.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="226.0">pages</text>
|
||||
</g>
|
||||
<g id="node-feed-items">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-store" data-evidence="35-35" x="380.0" y="128.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="455.0" y="158.0">feed_items</text>
|
||||
</g>
|
||||
<g id="node-feed-item-mentions">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-sink" data-evidence="43-43" x="690.0" y="60.0" width="160.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="770.0" y="90.0">feed_item_mentions</text>
|
||||
</g>
|
||||
<g id="node-highlights">
|
||||
<rect class="node-shape kind-entity emphasis-normal role-sink" data-evidence="37-37" x="695.0" y="196.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="770.0" y="226.0">highlights</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,29 @@
|
||||
-- Crown Task 4 — deep-page keyset on the precompute parent path (index-range seek, ~19 rows)
|
||||
-- FeedCrownIT.crownDeepPageKeysetSeeksFewerRowsWithPrecomputeThanSingleOr (seed 2000, user008, deepest page)
|
||||
-- 읽기 포인트 (★ 실측 정정): 깊은 커서에선 사전계산 경로도 작은 Sort 가 붙는다 — Bitmap Index Scan on
|
||||
-- ix_feed_visible 이 남은 19행만 인덱스 range(Index Cond 에 ROW(...) < ROW(cursor))로 훑고, Bitmap 은
|
||||
-- 정렬 출력을 안 하므로 19행을 quicksort(26kB). 핵심: 훑는 행수 19, 부모 buffers 3 — 페이지 근방만 만진다
|
||||
-- (OFFSET 의 scan-then-discard 도, 단일 OR 의 전체 가시성 재해소도 아니다).
|
||||
|
||||
Nested Loop (cost=22.44..299.62 rows=60 width=686) (actual time=0.023..0.059 rows=19 loops=1)
|
||||
Buffers: shared hit=60
|
||||
-> Limit (cost=22.17..22.22 rows=20 width=24) (actual time=0.013..0.015 rows=19 loops=1)
|
||||
Buffers: shared hit=3
|
||||
-> Sort (cost=22.17..22.22 rows=21 width=24) (actual time=0.013..0.014 rows=19 loops=1)
|
||||
Sort Key: feed_visible.first_highlighted_at DESC, feed_visible.feed_item_id DESC
|
||||
Sort Method: quicksort Memory: 26kB
|
||||
Buffers: shared hit=3
|
||||
-> Bitmap Heap Scan on feed_visible (cost=4.49..21.71 rows=21 width=24) (actual time=0.005..0.006 rows=19 loops=1)
|
||||
Recheck Cond: ((viewer_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid) AND (ROW(first_highlighted_at, feed_item_id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)))
|
||||
Heap Blocks: exact=1
|
||||
Buffers: shared hit=3
|
||||
-> Bitmap Index Scan on ix_feed_visible (cost=0.00..4.49 rows=21 width=0) (actual time=0.003..0.003 rows=19 loops=1)
|
||||
Index Cond: ((viewer_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid) AND (ROW(first_highlighted_at, feed_item_id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)))
|
||||
Buffers: shared hit=2
|
||||
-> Limit (cost=0.28..13.83 rows=3 width=670) (actual time=0.002..0.002 rows=1 loops=19)
|
||||
Buffers: shared hit=57
|
||||
-> Index Scan using ix_highlights_feed_items_created on highlights h (cost=0.28..36.42 rows=8 width=670) (actual time=0.002..0.002 rows=1 loops=19)
|
||||
Index Cond: (feed_item_id = feed_visible.feed_item_id)
|
||||
Buffers: shared hit=57
|
||||
Planning Time: 0.075 ms
|
||||
Execution Time: 0.118 ms
|
||||
@@ -0,0 +1,47 @@
|
||||
-- Crown Task 4 — deep-page keyset on the single-OR parent path (re-resolves visibility every page)
|
||||
-- FeedCrownIT.crownDeepPageKeysetSeeksFewerRowsWithPrecomputeThanSingleOr (seed 2000, user008, deepest page)
|
||||
-- 읽기 포인트: 단일 OR 부모선택은 사전계산 읽기 모델(ix_feed_visible)을 못 쓴다(구조적). 매 페이지 가시성
|
||||
-- 3분기를 BitmapOr 로 다시 풀고(public/mentioned = ix_feed_items_visibility_sort, private = ix_feed_items_private),
|
||||
-- 멘션 EXISTS 는 hashed SubPlan 2 로 user008 의 멘션 200행을 materialize 한다 → 훑는 행수 200(사전계산 19 대비).
|
||||
-- 커서는 세 분기 Index Cond 에 ROW(...) < ROW(cursor) 로 들어가 seek 은 하나, 페이지마다 전체 가시성을 재계산한다.
|
||||
|
||||
Nested Loop (cost=94.10..163.18 rows=15 width=686) (actual time=0.128..0.161 rows=19 loops=1)
|
||||
Buffers: shared hit=88
|
||||
-> Limit (cost=93.82..93.83 rows=5 width=24) (actual time=0.118..0.120 rows=19 loops=1)
|
||||
Buffers: shared hit=31
|
||||
-> Sort (cost=93.82..93.83 rows=5 width=24) (actual time=0.117..0.119 rows=19 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id DESC
|
||||
Sort Method: quicksort Memory: 26kB
|
||||
Buffers: shared hit=31
|
||||
-> Bitmap Heap Scan on feed_items fi (cost=12.90..93.76 rows=5 width=24) (actual time=0.027..0.112 rows=19 loops=1)
|
||||
Recheck Cond: ((((visibility)::text = 'PUBLIC'::text) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid))) OR (((visibility)::text = 'MENTIONED'::text) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid))) OR ((user_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)) AND ((visibility)::text = 'PRIVATE'::text)))
|
||||
Filter: (((visibility)::text = 'PUBLIC'::text) OR (((visibility)::text = 'MENTIONED'::text) AND (hashed SubPlan 2)) OR (((visibility)::text = 'PRIVATE'::text) AND (user_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid)))
|
||||
Rows Removed by Filter: 4
|
||||
Heap Blocks: exact=6
|
||||
Buffers: shared hit=31
|
||||
-> BitmapOr (cost=12.90..12.90 rows=7 width=0) (actual time=0.019..0.019 rows=0 loops=1)
|
||||
Buffers: shared hit=6
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (cost=0.00..4.31 rows=3 width=0) (actual time=0.013..0.014 rows=48 loops=1)
|
||||
Index Cond: (((visibility)::text = 'PUBLIC'::text) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)))
|
||||
Buffers: shared hit=2
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (cost=0.00..4.31 rows=3 width=0) (actual time=0.003..0.003 rows=18 loops=1)
|
||||
Index Cond: (((visibility)::text = 'MENTIONED'::text) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)))
|
||||
Buffers: shared hit=2
|
||||
-> Bitmap Index Scan on ix_feed_items_private (cost=0.00..4.28 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1)
|
||||
Index Cond: ((user_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)))
|
||||
Buffers: shared hit=2
|
||||
SubPlan 2
|
||||
-> Bitmap Heap Scan on feed_item_mentions m (cost=4.33..24.04 rows=7 width=16) (actual time=0.014..0.038 rows=200 loops=1)
|
||||
Recheck Cond: (mentioned_user_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid)
|
||||
Heap Blocks: exact=16
|
||||
Buffers: shared hit=19
|
||||
-> Bitmap Index Scan on ix_mentions_user (cost=0.00..4.33 rows=7 width=0) (actual time=0.011..0.011 rows=200 loops=1)
|
||||
Index Cond: (mentioned_user_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid)
|
||||
Buffers: shared hit=3
|
||||
-> Limit (cost=0.28..13.83 rows=3 width=670) (actual time=0.002..0.002 rows=1 loops=19)
|
||||
Buffers: shared hit=57
|
||||
-> Index Scan using ix_highlights_feed_items_created on highlights h (cost=0.28..36.42 rows=8 width=670) (actual time=0.002..0.002 rows=1 loops=19)
|
||||
Index Cond: (feed_item_id = fi.id)
|
||||
Buffers: shared hit=57
|
||||
Planning Time: 0.153 ms
|
||||
Execution Time: 0.250 ms
|
||||
@@ -0,0 +1,24 @@
|
||||
-- Crown Task 4 — unified feed query (precompute parent path): visibility + keyset + Top-N in ONE plan
|
||||
-- FeedCrownIT.crownUnifiedPlanStacksVisibilityKeysetAndTopN (seed 2000, viewer user008, page 1)
|
||||
-- 읽기 포인트: 세 기법이 한 플랜에 재정렬(Sort) 없이 겹쳐 있다 —
|
||||
-- (1) 가시성+keyset = feed_visible 커버링 인덱스의 Index Only Scan (viewer_id 조건, Heap Fetches 20),
|
||||
-- (2) Top-N = 부모 20건당 ix_highlights_feed_items_created 로 top-3 index seek (Nested Loop = LATERAL),
|
||||
-- (3) Sort 노드 없음 — 두 순서(부모 keyset·자식 created_at)가 모두 인덱스에서 나온다.
|
||||
|
||||
Nested Loop (cost=0.56..271.10 rows=60 width=686) (actual time=0.036..0.092 rows=60 loops=1)
|
||||
Buffers: shared hit=65 read=2
|
||||
-> Limit (cost=0.28..1.84 rows=20 width=24) (actual time=0.025..0.028 rows=20 loops=1)
|
||||
Buffers: shared hit=1 read=2
|
||||
-> Index Only Scan using ix_feed_visible on feed_visible (cost=0.28..117.22 rows=1500 width=24) (actual time=0.025..0.027 rows=20 loops=1)
|
||||
Index Cond: (viewer_id = '9ed28556-7ab3-4f5a-b327-7e0dd9536bf8'::uuid)
|
||||
Heap Fetches: 20
|
||||
Buffers: shared hit=1 read=2
|
||||
-> Limit (cost=0.28..13.42 rows=3 width=670) (actual time=0.003..0.003 rows=3 loops=20)
|
||||
Buffers: shared hit=64
|
||||
-> Index Scan using ix_highlights_feed_items_created on highlights h (cost=0.28..48.47 rows=11 width=670) (actual time=0.002..0.003 rows=3 loops=20)
|
||||
Index Cond: (feed_item_id = feed_visible.feed_item_id)
|
||||
Buffers: shared hit=64
|
||||
Planning:
|
||||
Buffers: shared hit=11 read=1
|
||||
Planning Time: 0.122 ms
|
||||
Execution Time: 0.120 ms
|
||||
@@ -0,0 +1,20 @@
|
||||
Plan A — 반복되는 하이라이트 자식 쿼리의 실행계획
|
||||
출처: FeedPersistenceIT.l1ExplainRepeatedHighlightChildQuery 콘솔 출력
|
||||
조건: 대량 시드 직후, ANALYZE 미실행. warm buffer cache(shared read=0).
|
||||
쿼리: SELECT * FROM highlights WHERE feed_item_id = ? (ORDER BY / LIMIT 없음)
|
||||
|
||||
Index Scan using ix_highlights_feed_items_created on highlights
|
||||
(cost=0.27..8.29 rows=1 width=710) (actual time=0.026..0.123 rows=500 loops=1)
|
||||
Index Cond: (feed_item_id = '2b5b931f-...'::uuid)
|
||||
Buffers: shared hit=14
|
||||
Planning Time: 0.086 ms
|
||||
Execution Time: 0.173 ms
|
||||
|
||||
주의(문서 §6.4 caveat와 동일):
|
||||
- 플래너 추정 rows=1 vs 실제 rows=500 → 500배 오추정. 시드 후 ANALYZE 미실행으로 통계가
|
||||
feed_item_id별 편중을 반영하지 못한 것으로 보임. EXPLAIN 전 `ANALYZE highlights` 필요.
|
||||
- Buffers: shared hit=14, read=0 → warm buffer cache 결과. cold 디스크 I/O 실행시간으로 읽지 말 것.
|
||||
- Execution Time 0.173 ms는 PostgreSQL executor 내부 시간. ORM 엔티티 생성·JDBC 결과 전달·
|
||||
DTO 매핑·직렬화·HTTP를 포함하지 않으므로 애플리케이션 지연(§6.2)과 같은 지표가 아니다.
|
||||
|
||||
Plan B (`ANALYZE highlights` 실행 후 재측정) — 예정(pending). 아직 미실행이므로 값 없음.
|
||||
@@ -0,0 +1,29 @@
|
||||
-- L14 index toggle — the SAME LATERAL query with ix_highlights_feed_items_created DROPPED
|
||||
-- FeedTopNIT.l14LateralDependsOnCompositeIndex (seed 1000, page 20, K=3; index dropped then restored)
|
||||
-- 읽기 포인트: 인덱스가 없으면 부모마다 highlights 를 Seq Scan 하고(Rows Removed by Filter: 2842/loop)
|
||||
-- top-N heapsort 로 3개를 고른다 → child 쪽 buffers shared hit=4340, 전체 4446 (인덱스판 168 의 ~26배),
|
||||
-- Execution 5.472 ms (인덱스판 0.336 ms 의 ~16배). 결론: LATERAL 이 빠른 건 LATERAL 이 아니라
|
||||
-- (feed_item_id, created_at DESC) 인덱스 seek 덕. 인덱스가 없으면 LATERAL 도 무너진다.
|
||||
|
||||
Nested Loop (cost=376.28..5070.14 rows=60 width=686) (actual time=0.599..5.451 rows=60 loops=1)
|
||||
Buffers: shared hit=4446
|
||||
-> Limit (cost=129.28..129.33 rows=20 width=24) (actual time=0.218..0.221 rows=20 loops=1)
|
||||
Buffers: shared hit=106
|
||||
-> Sort (cost=129.28..130.87 rows=636 width=24) (actual time=0.218..0.219 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
Sort Method: top-N heapsort Memory: 26kB
|
||||
Buffers: shared hit=106
|
||||
-> Seq Scan on feed_items fi (cost=0.00..112.36 rows=636 width=24) (actual time=0.086..0.150 rows=1000 loops=1)
|
||||
Buffers: shared hit=106
|
||||
-> Limit (cost=246.99..247.00 rows=3 width=670) (actual time=0.261..0.261 rows=3 loops=20)
|
||||
Buffers: shared hit=4340
|
||||
-> Sort (cost=246.99..247.02 rows=12 width=670) (actual time=0.259..0.259 rows=3 loops=20)
|
||||
Sort Key: h.created_at DESC
|
||||
Sort Method: top-N heapsort Memory: 25kB
|
||||
Buffers: shared hit=4340
|
||||
-> Seq Scan on highlights h (cost=0.00..246.84 rows=12 width=670) (actual time=0.171..0.249 rows=75 loops=20)
|
||||
Filter: (feed_item_id = fi.id)
|
||||
Rows Removed by Filter: 2842
|
||||
Buffers: shared hit=4340
|
||||
Planning Time: 0.079 ms
|
||||
Execution Time: 5.472 ms
|
||||
@@ -0,0 +1,26 @@
|
||||
-- L14 (b) LATERAL top-3 per parent — the winning strategy (index seek)
|
||||
-- FeedTopNIT.l14ExplainThreeWayPlanCompareIsTheCrownJewel (seed 1000, page 20, K=3)
|
||||
-- SELECT p.id, top3.color, top3.text, top3.created_at
|
||||
-- FROM (SELECT fi.id FROM feed_items fi ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20) p
|
||||
-- CROSS JOIN LATERAL (SELECT h.color, h.text, h.created_at FROM highlights h
|
||||
-- WHERE h.feed_item_id = p.id ORDER BY h.created_at DESC LIMIT 3) top3
|
||||
-- 읽기 포인트: 부모마다 ix_highlights_feed_items_created 를 Index Scan 하고 Limit 3 에서 멈춘다
|
||||
-- (loops=20, 각 rows=3). buffers shared hit=204 로 세 해법 중 최소.
|
||||
|
||||
Nested Loop (cost=172.25..432.10 rows=60 width=686) (actual time=0.257..0.310 rows=60 loops=1)
|
||||
Buffers: shared hit=204
|
||||
-> Limit (cost=171.97..172.02 rows=20 width=24) (actual time=0.239..0.240 rows=20 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Sort (cost=171.97..174.09 rows=846 width=24) (actual time=0.238..0.239 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
Sort Method: top-N heapsort Memory: 26kB
|
||||
Buffers: shared hit=141
|
||||
-> Seq Scan on feed_items fi (cost=0.00..149.46 rows=846 width=24) (actual time=0.111..0.175 rows=1000 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Limit (cost=0.28..12.96 rows=3 width=670) (actual time=0.003..0.003 rows=3 loops=20)
|
||||
Buffers: shared hit=63
|
||||
-> Index Scan using ix_highlights_feed_items_created on highlights h (cost=0.28..80.61 rows=19 width=670) (actual time=0.003..0.003 rows=3 loops=20)
|
||||
Index Cond: (feed_item_id = fi.id)
|
||||
Buffers: shared hit=63
|
||||
Planning Time: 0.068 ms
|
||||
Execution Time: 0.323 ms
|
||||
@@ -0,0 +1,26 @@
|
||||
-- L14 (c) two-step IN + app-side cut — correct result but transfers ALL page-parent highlights
|
||||
-- FeedTopNIT.l14ExplainThreeWayPlanCompareIsTheCrownJewel (seed 1000, page 20)
|
||||
-- SELECT h.feed_item_id, h.color, h.text, h.created_at FROM highlights h
|
||||
-- WHERE h.feed_item_id IN (<page-20 ids>) ORDER BY h.feed_item_id, h.created_at DESC
|
||||
-- 읽기 포인트: window 와 동일한 Hash Semi Join(rows=1509) — 다만 위에 WindowAgg 가 없어 1509행을 전량
|
||||
-- 애플리케이션으로 전송한다(앱에서 부모별 top-3 컷). buffers shared hit=430 (window 과 동일 = 같은 스캔).
|
||||
-- = L6 프로젝션이 남긴 잔여(childRows=1509)의 정체. 전송 낭비: 60행이면 되는데 1509행.
|
||||
|
||||
Sort (cost=531.52..532.49 rows=388 width=686) (actual time=1.586..1.625 rows=1509 loops=1)
|
||||
Sort Key: h.feed_item_id, h.created_at DESC
|
||||
Sort Method: quicksort Memory: 155kB
|
||||
Buffers: shared hit=430
|
||||
-> Hash Semi Join (cost=172.47..514.84 rows=388 width=686) (actual time=0.493..0.833 rows=1509 loops=1)
|
||||
Hash Cond: (h.feed_item_id = "ANY_subquery".id)
|
||||
Buffers: shared hit=430
|
||||
-> Seq Scan on highlights h (cost=0.00..327.85 rows=3885 width=686) (actual time=0.239..0.363 rows=2917 loops=1)
|
||||
Buffers: shared hit=289
|
||||
-> Hash (cost=172.22..172.22 rows=20 width=16) (actual time=0.251..0.252 rows=20 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Subquery Scan on "ANY_subquery" (cost=171.97..172.22 rows=20 width=16) (actual time=0.239..0.241 rows=20 loops=1)
|
||||
-> Limit (cost=171.97..172.02 rows=20 width=24) (actual time=0.238..0.240 rows=20 loops=1)
|
||||
-> Sort (cost=171.97..174.09 rows=846 width=24) (actual time=0.238..0.238 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
-> Seq Scan on feed_items fi (cost=0.00..149.46 rows=846 width=24) (actual time=0.112..0.175 rows=1000 loops=1)
|
||||
Planning Time: 0.060 ms
|
||||
Execution Time: 1.686 ms
|
||||
@@ -0,0 +1,33 @@
|
||||
-- L14 (a) window row_number() <= 3 — cuts in the DB but scans the whole partition
|
||||
-- FeedTopNIT.l14ExplainThreeWayPlanCompareIsTheCrownJewel (seed 1000, page 20, K=3)
|
||||
-- SELECT t.feed_item_id, t.color, t.text, t.created_at FROM (
|
||||
-- SELECT h.feed_item_id, h.color, h.text, h.created_at,
|
||||
-- row_number() OVER (PARTITION BY h.feed_item_id ORDER BY h.created_at DESC) AS rn
|
||||
-- FROM highlights h WHERE h.feed_item_id IN (<page-20 ids>)) t WHERE t.rn <= 3
|
||||
-- 읽기 포인트: Hash Semi Join 이 페이지 부모들의 하이라이트 전량(rows=1509)을 읽고 Sort 한 뒤 WindowAgg 가
|
||||
-- 순번을 매긴다. PG 15+ 는 rn<=3 을 WindowAgg 의 Run Condition 으로 밀어넣지만, 파티션 정렬은
|
||||
-- 이미 1509행 전량을 훑는다. 반환은 60행이지만 buffers shared hit=430 (two-step 과 같다 = 같은 스캔).
|
||||
|
||||
Subquery Scan on t (cost=531.52..543.16 rows=388 width=686) (actual time=1.376..1.486 rows=60 loops=1)
|
||||
Buffers: shared hit=430
|
||||
-> WindowAgg (cost=531.52..539.28 rows=388 width=694) (actual time=1.375..1.482 rows=60 loops=1)
|
||||
Run Condition: (row_number() OVER (?) <= 3)
|
||||
Buffers: shared hit=430
|
||||
-> Sort (cost=531.52..532.49 rows=388 width=686) (actual time=1.369..1.406 rows=1509 loops=1)
|
||||
Sort Key: h.feed_item_id, h.created_at DESC
|
||||
Sort Method: quicksort Memory: 155kB
|
||||
Buffers: shared hit=430
|
||||
-> Hash Semi Join (cost=172.47..514.84 rows=388 width=686) (actual time=0.590..0.941 rows=1509 loops=1)
|
||||
Hash Cond: (h.feed_item_id = "ANY_subquery".id)
|
||||
Buffers: shared hit=430
|
||||
-> Seq Scan on highlights h (cost=0.00..327.85 rows=3885 width=686) (actual time=0.289..0.421 rows=2917 loops=1)
|
||||
Buffers: shared hit=289
|
||||
-> Hash (cost=172.22..172.22 rows=20 width=16) (actual time=0.287..0.288 rows=20 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Subquery Scan on "ANY_subquery" (cost=171.97..172.22 rows=20 width=16) (actual time=0.277..0.280 rows=20 loops=1)
|
||||
-> Limit (cost=171.97..172.02 rows=20 width=24) (actual time=0.277..0.279 rows=20 loops=1)
|
||||
-> Sort (cost=171.97..174.09 rows=846 width=24) (actual time=0.276..0.277 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
-> Seq Scan on feed_items fi (cost=0.00..149.46 rows=846 width=24) (actual time=0.146..0.212 rows=1000 loops=1)
|
||||
Planning Time: 0.123 ms
|
||||
Execution Time: 1.552 ms
|
||||
@@ -0,0 +1,16 @@
|
||||
-- L15 keyset WITH sort-key index (range seek) — same deep page, offset 1980 equivalent cursor
|
||||
-- FeedKeysetIT.l15ExplainOffsetScansThenDiscardsKeysetSeeksAndNeedsIndex (seed 2000)
|
||||
-- SELECT fi.id, fi.first_highlighted_at FROM feed_items fi
|
||||
-- WHERE (fi.first_highlighted_at, fi.id) < (TIMESTAMPTZ '...', '...'::uuid) -- cursor = 이전 페이지 마지막 행
|
||||
-- ORDER BY fi.first_highlighted_at DESC, fi.id DESC LIMIT 20
|
||||
-- 읽기 포인트: Index Only Scan(커버링) 으로 커서 이후 20행만 seek — actual rows=20, Heap Fetches=20, buffers 1(+2 read).
|
||||
-- 순서가 인덱스로 보장돼 Sort 노드가 없다. 페이지 깊이와 무관하게 상수(vs OFFSET 의 2000).
|
||||
|
||||
Limit (cost=0.28..18.14 rows=20 width=24) (actual time=0.054..0.060 rows=20 loops=1)
|
||||
Buffers: shared hit=1 read=2
|
||||
-> Index Only Scan using ix_feed_items_keyset on feed_items fi (cost=0.28..595.95 rows=667 width=24) (actual time=0.054..0.057 rows=20 loops=1)
|
||||
Index Cond: (ROW(first_highlighted_at, id) < ROW('2026-04-17 13:00:00+00'::timestamp with time zone, '17ab2b68-0981-43cc-a673-5757f7214899'::uuid))
|
||||
Heap Fetches: 20
|
||||
Buffers: shared hit=1 read=2
|
||||
Planning Time: 0.052 ms
|
||||
Execution Time: 0.076 ms
|
||||
@@ -0,0 +1,18 @@
|
||||
-- L15 keyset WITHOUT the sort-key index — same query, ix_feed_items_keyset absent
|
||||
-- FeedKeysetIT.l15ExplainOffsetScansThenDiscardsKeysetSeeksAndNeedsIndex (seed 2000)
|
||||
-- 읽기 포인트: 결과 행(20)은 필터로 같지만, 정렬키 인덱스가 없어 Seq Scan 으로 2000 heap 행을 훑고
|
||||
-- (Rows Removed by Filter: 1980) Sort 한다 → buffers shared hit=141 (Index Only Scan 판의 ~140배).
|
||||
-- OFFSET(141)과 같은 buffers = 둘 다 전량 heap 접근. 정렬키 인덱스가 keyset 의 전제라는 증거.
|
||||
|
||||
Limit (cost=204.50..204.55 rows=20 width=24) (actual time=0.338..0.341 rows=20 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Sort (cost=204.50..206.72 rows=887 width=24) (actual time=0.337..0.339 rows=20 loops=1)
|
||||
Sort Key: first_highlighted_at DESC, id DESC
|
||||
Sort Method: quicksort Memory: 26kB
|
||||
Buffers: shared hit=141
|
||||
-> Seq Scan on feed_items fi (cost=0.00..180.90 rows=887 width=24) (actual time=0.314..0.317 rows=20 loops=1)
|
||||
Filter: (ROW(first_highlighted_at, id) < ROW('2026-04-17 13:00:00+00'::timestamp with time zone, '17ab2b68-0981-43cc-a673-5757f7214899'::uuid))
|
||||
Rows Removed by Filter: 1980
|
||||
Buffers: shared hit=141
|
||||
Planning Time: 0.074 ms
|
||||
Execution Time: 0.373 ms
|
||||
@@ -0,0 +1,19 @@
|
||||
-- L15 OFFSET deep page (scan-then-discard) — page 100 of 100, offset 1980
|
||||
-- FeedKeysetIT.l15ExplainOffsetScansThenDiscardsKeysetSeeksAndNeedsIndex (seed 2000, keyset index present)
|
||||
-- SELECT fi.id, fi.first_highlighted_at FROM feed_items fi
|
||||
-- ORDER BY fi.first_highlighted_at DESC, fi.id DESC OFFSET 1980 LIMIT 20
|
||||
-- 읽기 포인트: 정렬키 인덱스가 있어도 깊은 페이지에선 Seq Scan(2000)+Sort(2000) 로 전량을 훑고 20만 남긴다.
|
||||
-- Limit 하위 actual rows=2000 = 결과 20행을 위해 훑은 행(over-scan = offset+20). buffers shared hit=141.
|
||||
|
||||
Limit (cost=275.61..275.66 rows=20 width=24) (actual time=0.945..0.949 rows=20 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Sort (cost=270.66..275.66 rows=2000 width=24) (actual time=0.759..0.865 rows=2000 loops=1)
|
||||
Sort Key: first_highlighted_at DESC, id DESC
|
||||
Sort Method: quicksort Memory: 189kB
|
||||
Buffers: shared hit=141
|
||||
-> Seq Scan on feed_items fi (cost=0.00..161.00 rows=2000 width=24) (actual time=0.124..0.369 rows=2000 loops=1)
|
||||
Buffers: shared hit=141
|
||||
Planning:
|
||||
Buffers: shared hit=5 read=1
|
||||
Planning Time: 0.137 ms
|
||||
Execution Time: 0.996 ms
|
||||
@@ -0,0 +1,33 @@
|
||||
-- L15 probe (→ L16): keyset + visibility OR/EXISTS — the sort-key index is lost
|
||||
-- FeedKeysetIT.l15ProbeVisibilityOrBreaksKeysetIndex (seed 2000, ix_feed_items_keyset present)
|
||||
-- SELECT fi.id, fi.first_highlighted_at FROM feed_items fi
|
||||
-- WHERE (fi.visibility='PUBLIC'
|
||||
-- OR (fi.visibility='MENTIONED' AND EXISTS(SELECT 1 FROM feed_item_mentions m WHERE m.feed_item_id=fi.id AND m.mentioned_user_id=:me))
|
||||
-- OR (fi.visibility='PRIVATE' AND fi.user_id=:me))
|
||||
-- AND (fi.first_highlighted_at, fi.id) < (:cursor)
|
||||
-- ORDER BY fi.first_highlighted_at DESC, fi.id DESC LIMIT 20
|
||||
-- 읽기 포인트: ix_feed_items_keyset(정렬키) 를 못 탄다. 대신 BitmapOr(visibility 3분기 각각 ix_feed_items_visibility_sort)
|
||||
-- + BitmapAnd(private = visibility ∩ user_id) + SubPlan(mentions EXISTS). bitmap 은 순서를 안 주므로
|
||||
-- Sort 노드가 재등장 = keyset 의 "순서 seek, Sort 없음" 이점 소멸 → L16(UNION 분해로 각 분기를 정렬 보장 인덱스로).
|
||||
|
||||
Limit (cost=100.32..100.34 rows=5 width=24) (actual time=0.189..0.193 rows=13 loops=1)
|
||||
Buffers: shared hit=26
|
||||
-> Sort (cost=100.32..100.34 rows=5 width=24) (actual time=0.188..0.190 rows=13 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id DESC
|
||||
-> Bitmap Heap Scan on feed_items fi (actual time=0.114..0.178 rows=13 loops=1)
|
||||
Recheck Cond: (((visibility='PUBLIC') AND (ROW(first_highlighted_at, id) < ROW(cursor)))
|
||||
OR ((visibility='MENTIONED') AND (ROW(first_highlighted_at, id) < ROW(cursor)))
|
||||
OR ((visibility='PRIVATE') AND (ROW(first_highlighted_at, id) < ROW(cursor)) AND (user_id = :me)))
|
||||
Filter: ((visibility='PUBLIC') OR ((visibility='MENTIONED') AND (SubPlan 1)) OR ((visibility='PRIVATE') AND (user_id = :me)))
|
||||
Rows Removed by Filter: 3
|
||||
-> BitmapOr (actual time=0.093..0.094 rows=0 loops=1)
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='PUBLIC' AND ROW(...) < ROW(cursor))
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='MENTIONED' AND ROW(...) < ROW(cursor))
|
||||
-> BitmapAnd
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='PRIVATE' AND ROW(...) < ROW(cursor))
|
||||
-> Bitmap Index Scan on uq_feed_items_user_page (Index Cond: user_id = :me)
|
||||
SubPlan 1
|
||||
-> Index Only Scan using uq_feed_item_mentions on feed_item_mentions m (loops=4)
|
||||
Index Cond: ((feed_item_id = fi.id) AND (mentioned_user_id = :me))
|
||||
Planning Time: 0.319 ms
|
||||
Execution Time: 0.325 ms
|
||||
@@ -0,0 +1,18 @@
|
||||
-- L16 (c) precompute (CQRS read model) — per-viewer feed_visible table, single covering index scan
|
||||
-- FeedVisibilityIT.l16ExplainThreeWayPlanCompare / l16PrecomputeIsSingleIndexScanNoOrNoSort (seed 2000)
|
||||
-- CREATE TABLE feed_visible AS SELECT :me AS viewer_id, fi.id AS feed_item_id, fi.first_highlighted_at
|
||||
-- FROM feed_items fi WHERE <visible to me>;
|
||||
-- CREATE INDEX ix_feed_visible ON feed_visible (viewer_id, first_highlighted_at DESC, feed_item_id DESC);
|
||||
-- SELECT feed_item_id AS id, first_highlighted_at FROM feed_visible
|
||||
-- WHERE viewer_id=:me ORDER BY first_highlighted_at DESC, feed_item_id DESC LIMIT 20
|
||||
-- 읽기 포인트: 단일 Index Only Scan(커버링) — OR 도 조인도 Sort 도 없다. buffers shared hit=1(+2 read),
|
||||
-- 훑는 행 20. 단일 OR(122)·UNION(200) 대비 order-of-magnitude 적음 = CQRS 읽기 모델의 정체.
|
||||
|
||||
Limit (cost=0.28..1.84 rows=20 width=24) (actual time=0.021..0.025 rows=20 loops=1)
|
||||
Buffers: shared hit=1 read=2
|
||||
-> Index Only Scan using ix_feed_visible on feed_visible (cost=0.28..117.22 rows=1500 width=24) (actual time=0.021..0.023 rows=20 loops=1)
|
||||
Index Cond: (viewer_id = :me)
|
||||
Heap Fetches: 20
|
||||
Buffers: shared hit=1 read=2
|
||||
Planning Time: 0.102 ms
|
||||
Execution Time: 0.034 ms
|
||||
@@ -0,0 +1,30 @@
|
||||
-- L16 (a) single OR — the naive visibility filter: BitmapOr + top-N Sort + hashed SubPlan
|
||||
-- FeedVisibilityIT.l16ExplainThreeWayPlanCompare (seed 2000, viewer user008)
|
||||
-- SELECT fi.id, fi.first_highlighted_at FROM feed_items fi
|
||||
-- WHERE (fi.visibility='PUBLIC'
|
||||
-- OR (fi.visibility='MENTIONED' AND EXISTS(SELECT 1 FROM feed_item_mentions m WHERE m.feed_item_id=fi.id AND m.mentioned_user_id=:me))
|
||||
-- OR (fi.visibility='PRIVATE' AND fi.user_id=:me))
|
||||
-- ORDER BY fi.first_highlighted_at DESC, fi.id DESC LIMIT 20
|
||||
-- 읽기 포인트: seq scan 이 아니라 BitmapOr(3분기 인덱스)로 후보 1500 을 heap scan → top-N Sort(순서 손실)
|
||||
-- + 멘션 EXISTS 는 hashed SubPlan(후보마다 반복 아님). buffers shared hit=122.
|
||||
|
||||
Limit (cost=224.45..224.49 rows=15 width=24) (actual time=0.713..0.716 rows=20 loops=1)
|
||||
Buffers: shared hit=122
|
||||
-> Sort (cost=224.45..224.49 rows=15 width=24) (actual time=0.712..0.713 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id DESC
|
||||
Sort Method: top-N heapsort Memory: 26kB
|
||||
Buffers: shared hit=122
|
||||
-> Bitmap Heap Scan on feed_items fi (actual time=0.256..0.590 rows=1500 loops=1)
|
||||
Recheck Cond: ((visibility='PUBLIC') OR (visibility='MENTIONED') OR ((user_id=:me) AND (visibility='PRIVATE')))
|
||||
Filter: ((visibility='PUBLIC') OR ((visibility='MENTIONED') AND (hashed SubPlan 2)) OR ((visibility='PRIVATE') AND (user_id=:me)))
|
||||
Rows Removed by Filter: 200
|
||||
Buffers: shared hit=122
|
||||
-> BitmapOr (actual time=0.185..0.185 rows=0 loops=1)
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='PUBLIC') rows=2400
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='MENTIONED') rows=800
|
||||
-> Bitmap Index Scan on ix_feed_items_private (Index Cond: user_id=:me) rows=100
|
||||
SubPlan 2
|
||||
-> Bitmap Heap Scan on feed_item_mentions m (Recheck Cond: mentioned_user_id=:me) rows=200
|
||||
-> Bitmap Index Scan on ix_mentions_user (Index Cond: mentioned_user_id=:me) rows=200
|
||||
Planning Time: 0.144 ms
|
||||
Execution Time: 0.808 ms
|
||||
@@ -0,0 +1,20 @@
|
||||
-- L16 union branches — each visibility branch rides its own optimal plan (what single-OR can't)
|
||||
-- FeedVisibilityIT.l16LowSelectivityBranchesRideTheirIndex (seed 2000, viewer user008)
|
||||
-- 읽기 포인트: 저선택도 분기는 자기 인덱스를 탄다 — mentioned=ix_mentions_user 조인, private=ix_feed_items_private
|
||||
-- partial 의 Index Only Scan. public(60% 고선택도)은 Bitmap Heap Scan+top-N Sort 가 최적.
|
||||
-- 단일 OR 은 3분기를 하나의 bitmap 으로 묶어 분기별 최적 플랜을 못 가진다.
|
||||
|
||||
== mentioned branch (JOIN feed_item_mentions on ix_mentions_user) ==
|
||||
Limit -> Sort (top-N) -> Hash Join (fi.id = m.feed_item_id)
|
||||
-> Bitmap Heap Scan on feed_items fi (visibility='MENTIONED')
|
||||
-> Hash -> Bitmap Heap Scan on feed_item_mentions m
|
||||
-> Bitmap Index Scan on ix_mentions_user (Index Cond: mentioned_user_id = :me) rows=200
|
||||
|
||||
== private branch (partial index ix_feed_items_private WHERE visibility='PRIVATE') ==
|
||||
Limit -> Incremental Sort (Presorted Key: first_highlighted_at)
|
||||
-> Index Only Scan using ix_feed_items_private on feed_items fi (Index Cond: user_id = :me) Heap Fetches: 21
|
||||
|
||||
== public branch (60% selectivity -> seq/bitmap + top-N is optimal, not an index range) ==
|
||||
Limit -> Sort (top-N heapsort)
|
||||
-> Bitmap Heap Scan on feed_items fi (Recheck Cond: visibility='PUBLIC')
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='PUBLIC')
|
||||
@@ -0,0 +1,27 @@
|
||||
-- L16 (b) UNION decompose — 3 visibility branches, each index-ordered, Merge Append + Hash Join
|
||||
-- FeedVisibilityIT.l16ExplainThreeWayPlanCompare (seed 2000, viewer user008)
|
||||
-- (public branch) UNION ALL (mentioned branch: JOIN feed_item_mentions) UNION ALL (private branch: partial idx)
|
||||
-- ORDER BY first_highlighted_at DESC, id DESC LIMIT 20
|
||||
-- 읽기 포인트: Merge Append 가 미리 정렬된 분기 스트림을 병합(전체 재정렬 없음). 멘션 EXISTS 가 Hash Join(집합 기반)
|
||||
-- 으로, private 는 Index Only Scan(partial)+Incremental Sort 로. 구조는 우수하나 buffers 200(분기별 스캔).
|
||||
|
||||
Limit (cost=97.16..97.40 rows=13 width=24) (actual time=0.655..0.661 rows=20 loops=1)
|
||||
Buffers: shared hit=200
|
||||
-> Merge Append (cost=97.16..97.40 rows=13 width=24) (actual time=0.654..0.659 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id DESC
|
||||
Buffers: shared hit=200
|
||||
-> Limit (rows=17) -- public branch
|
||||
-> Sort (top-N heapsort)
|
||||
-> Bitmap Heap Scan on feed_items fi (Recheck Cond: visibility='PUBLIC')
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort
|
||||
-> Limit (rows=3) -- mentioned branch: EXISTS -> Hash Join
|
||||
-> Sort (top-N heapsort)
|
||||
-> Hash Join (Hash Cond: fi_1.id = m.feed_item_id)
|
||||
-> Bitmap Heap Scan on feed_items fi_1 (visibility='MENTIONED')
|
||||
-> Hash -> Bitmap Heap Scan on feed_item_mentions m
|
||||
-> Bitmap Index Scan on ix_mentions_user (mentioned_user_id=:me)
|
||||
-> Limit (rows=2) -- private branch: partial index, index-only
|
||||
-> Incremental Sort (Presorted Key: fi_2.first_highlighted_at)
|
||||
-> Index Only Scan using ix_feed_items_private on feed_items fi_2 (user_id=:me) Heap Fetches: 21
|
||||
Planning Time: 0.398 ms
|
||||
Execution Time: 0.780 ms
|
||||
@@ -0,0 +1,29 @@
|
||||
컬렉션 하나만 fetch join한 조인의 실행계획 (Fetch Join 시도 — 카테시안 행 곱)
|
||||
출처: FeedPersistenceIT.l3ExplainCollectionJoinRowMultiplication 콘솔 출력
|
||||
조건: seed(100) 직후. warm buffer cache(shared read=0).
|
||||
쿼리: EXPLAIN (ANALYZE, BUFFERS)
|
||||
SELECT fi.id, h.id FROM feed_items fi JOIN highlights h ON h.feed_item_id = fi.id
|
||||
(fetch join `select f from FeedItemJpaEntity f join fetch f.highlights`가 발행하는 조인과 같은 shape)
|
||||
|
||||
Hash Join (cost=77.18..512.34 rows=4202 width=32) (actual time=0.589..0.894 rows=1961 loops=1)
|
||||
Hash Cond: (h.feed_item_id = fi.id)
|
||||
Buffers: shared hit=450
|
||||
-> Seq Scan on highlights h (cost=0.00..424.02 rows=4202 width=32) (actual time=0.471..0.570 rows=1961 loops=1)
|
||||
Buffers: shared hit=382
|
||||
-> Hash (cost=72.08..72.08 rows=408 width=16) (actual time=0.112..0.112 rows=100 loops=1)
|
||||
Buckets: 1024 Batches: 1 Memory Usage: 13kB
|
||||
Buffers: shared hit=68
|
||||
-> Seq Scan on feed_items fi (cost=0.00..72.08 rows=408 width=16) (actual time=0.084..0.092 rows=100 loops=1)
|
||||
Buffers: shared hit=68
|
||||
Planning Time: 0.099 ms
|
||||
Execution Time: 0.959 ms
|
||||
|
||||
관찰(문서 §9):
|
||||
- §6.4는 반복되는 자식 단건 쿼리를, §7.4는 반복되는 부모 단건 쿼리를 봤다. 여기서는 조인 한 방을 본다 —
|
||||
Hash Join 노드의 actual rows=1961이 카테시안의 실체다. 부모 feed_items는 100행(Hash 노드)인데,
|
||||
조인 결과는 1,961행(= Σ highlights)으로 부푼다. 쿼리는 하나인데 그 하나가 실어 나르는 행이 곱이다.
|
||||
- 이 1,961이 N2 랩(§7)의 아이템 수 100이 아니라 자식 총량(1,961)과 같다는 게 핵심 — 전송 비용이
|
||||
'왕복 수'에서 '전송 행수'로 옮겨갔다.
|
||||
- Buffers: shared read=0 → warm buffer cache. cold 디스크 I/O 실행시간으로 읽지 말 것.
|
||||
- Execution Time 0.959 ms는 executor 내부 시간(§6.4 caveat와 동일). 애플리케이션 지연이 아니다.
|
||||
- rows=4202(추정) vs rows=1961(실제)의 오차는 대량 시드 직후 ANALYZE 미실행 탓(§6.4 Plan A와 같은 통계 이슈).
|
||||
@@ -0,0 +1,35 @@
|
||||
컬렉션 fetch join + 페이징이 발행하는 조인의 실행계획 (a) — LIMIT 노드 없음
|
||||
출처: FeedPersistenceIT.l4ExplainCollectionJoinHasNoLimitButEntityPagingDoes 콘솔 출력 (a)
|
||||
조건: seed(100) 직후. warm buffer cache(shared read=0).
|
||||
쿼리: EXPLAIN (ANALYZE, BUFFERS)
|
||||
SELECT fi.*, h.* FROM feed_items fi JOIN highlights h ON h.feed_item_id = fi.id
|
||||
ORDER BY fi.first_highlighted_at DESC, fi.id ASC
|
||||
(fetch join `select f from FeedItemJpaEntity f join fetch f.highlights order by ...`가
|
||||
페이징(setMaxResults) 시 발행하는 조인과 같은 shape — 단, SQL에 LIMIT이 붙지 않는다)
|
||||
|
||||
Sort (cost=293.30..297.76 rows=1782 width=1904) (actual time=1.219..1.266 rows=1961 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
Sort Method: quicksort Memory: 445kB
|
||||
Buffers: shared hit=173
|
||||
-> Hash Join (cost=12.48..197.08 rows=1782 width=1904) (actual time=0.279..0.636 rows=1961 loops=1)
|
||||
Hash Cond: (h.feed_item_id = fi.id)
|
||||
Buffers: shared hit=173
|
||||
-> Seq Scan on highlights h (cost=0.00..179.82 rows=1782 width=710) (actual time=0.231..0.330 rows=1961 loops=1)
|
||||
Buffers: shared hit=162
|
||||
-> Hash (cost=11.66..11.66 rows=66 width=1194) (actual time=0.035..0.036 rows=100 loops=1)
|
||||
Buckets: 1024 Batches: 1 Memory Usage: 22kB
|
||||
Buffers: shared hit=11
|
||||
-> Seq Scan on feed_items fi (cost=0.00..11.66 rows=66 width=1194) (actual time=0.018..0.023 rows=100 loops=1)
|
||||
Buffers: shared hit=11
|
||||
Planning Time: 0.135 ms
|
||||
Execution Time: 1.369 ms
|
||||
|
||||
관찰(문서 §10):
|
||||
- 계획 어디에도 Limit 노드가 없다 = DB가 페이징을 하지 않았다. 조인 결과 전체(actual rows=1961 = Σ highlights)를
|
||||
quicksort로 445kB 정렬한 뒤 그대로 반환한다. 페이지 크기(20)로 자르는 일은 SQL 밖 — Hibernate가 메모리에서 한다.
|
||||
- 부모 feed_items는 100행(Hash 노드)인데 Hash Join 노드 actual rows=1961(= Σ highlights, §9.3)로 부푼다 —
|
||||
컬렉션 fetch join의 카테시안이 그대로다. 그 곱해진 행에 DB LIMIT을 걸면 "20개 부모"가 아니라 "20개 조인 행"을
|
||||
잘라 어떤 부모는 하이라이트가 잘린 반쪽으로 로드될 위험 → 그래서 Hibernate가 LIMIT을 빼고 인메모리 페이징한다.
|
||||
- Buffers: shared read=0 → warm buffer cache. cold 디스크 I/O 실행시간으로 읽지 말 것.
|
||||
- Execution Time 1.369 ms는 executor 내부 시간(§6.4 caveat와 동일). 애플리케이션 지연이 아니다.
|
||||
- 대조군은 l4-entity-paging-limit.txt (엔티티만 페이징 → Limit 노드 존재).
|
||||
@@ -0,0 +1,25 @@
|
||||
엔티티만 페이징한 SQL의 실행계획 (b) — Limit 노드 존재 (대조군)
|
||||
출처: FeedPersistenceIT.l4ExplainCollectionJoinHasNoLimitButEntityPagingDoes 콘솔 출력 (b)
|
||||
조건: seed(100) 직후. warm buffer cache(shared read=0).
|
||||
쿼리: EXPLAIN (ANALYZE, BUFFERS)
|
||||
SELECT fi.* FROM feed_items fi ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20
|
||||
(fetch join 없이 엔티티만 페이징한 SQL — DB가 정상적으로 페이징하는 모습)
|
||||
|
||||
Limit (cost=13.42..13.47 rows=20 width=1194) (actual time=0.038..0.040 rows=20 loops=1)
|
||||
Buffers: shared hit=11
|
||||
-> Sort (cost=13.42..13.58 rows=66 width=1194) (actual time=0.038..0.038 rows=20 loops=1)
|
||||
Sort Key: first_highlighted_at DESC, id
|
||||
Sort Method: top-N heapsort Memory: 28kB
|
||||
Buffers: shared hit=11
|
||||
-> Seq Scan on feed_items fi (cost=0.00..11.66 rows=66 width=1194) (actual time=0.015..0.019 rows=100 loops=1)
|
||||
Buffers: shared hit=11
|
||||
Planning Time: 0.029 ms
|
||||
Execution Time: 0.050 ms
|
||||
|
||||
관찰(문서 §10):
|
||||
- 계획 최상단에 Limit 노드가 있고 그 아래 Sort가 top-N heapsort(28kB)로 상위 20행만 취한다 = DB가 페이징을 했다.
|
||||
(a) l4-collection-join-no-limit.txt는 Limit 노드가 없어 전체 1961행을 quicksort(445kB)로 정렬했다 — 대조가 요점.
|
||||
- 28kB(top-N heapsort, 20행) vs 445kB(quicksort, 1961행): "DB 페이징 vs 인메모리 페이징"의 메모리 비용 차이가
|
||||
계획 레벨로 드러난다. (a)에 Limit이 없다는 것 자체가 "DB가 페이징을 안 했다 → Hibernate가 메모리에서 했다"의 증거.
|
||||
- 컬럼명·리터럴 하드코딩이라 인젝션 무관. PG 계획 문구는 버전·통계에 따라 흔들릴 수 있어 강가드 대신 눈 대조로 둔다.
|
||||
- Buffers: shared read=0 → warm buffer cache. Execution Time 0.050 ms는 executor 내부 시간(§6.4 caveat와 동일).
|
||||
@@ -0,0 +1,38 @@
|
||||
배치 IN 조회의 실행계획 (b) — 행을 곱하지 않는다 (카테시안 소멸)
|
||||
출처: FeedBatchFetchIT.l5ExplainEntityPagingHasLimitAndBatchInHasNoRowMultiplication 콘솔 출력 (b)
|
||||
조건: seed(100) 직후, default_batch_fetch_size=100 세션. warm buffer cache.
|
||||
쿼리: EXPLAIN (ANALYZE, BUFFERS)
|
||||
SELECT h.* FROM highlights h
|
||||
WHERE h.feed_item_id IN (SELECT fi.id FROM feed_items fi
|
||||
ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20)
|
||||
(배치 페치가 페이지 부모 20개의 highlights 를 IN 한 방으로 채우는 것과 같은 shape)
|
||||
|
||||
Hash Semi Join (cost=72.46..317.64 rows=234 width=710) (actual time=0.295..0.541 rows=1509 loops=1)
|
||||
Hash Cond: (h.feed_item_id = "ANY_subquery".id)
|
||||
Buffers: shared hit=272
|
||||
-> Seq Scan on highlights h (cost=0.00..236.43 rows=2343 width=710) (actual time=0.207..0.286 rows=1961 loops=1)
|
||||
Buffers: shared hit=213
|
||||
-> Hash (cost=72.21..72.21 rows=20 width=16) (actual time=0.084..0.085 rows=20 loops=1)
|
||||
Buckets: 1024 Batches: 1 Memory Usage: 9kB
|
||||
Buffers: shared hit=59
|
||||
-> Subquery Scan on "ANY_subquery" (cost=71.96..72.21 rows=20 width=16) (actual time=0.077..0.080 rows=20 loops=1)
|
||||
Buffers: shared hit=59
|
||||
-> Limit (cost=71.96..72.01 rows=20 width=24) (actual time=0.077..0.078 rows=20 loops=1)
|
||||
Buffers: shared hit=59
|
||||
-> Sort (cost=71.96..72.84 rows=354 width=24) (actual time=0.076..0.077 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
Sort Method: top-N heapsort Memory: 26kB
|
||||
Buffers: shared hit=59
|
||||
-> Seq Scan on feed_items fi (cost=0.00..62.54 rows=354 width=24) (actual time=0.053..0.059 rows=100 loops=1)
|
||||
Buffers: shared hit=59
|
||||
Planning:
|
||||
Buffers: shared hit=28
|
||||
Planning Time: 0.206 ms
|
||||
Execution Time: 0.592 ms
|
||||
|
||||
관찰(문서 §11):
|
||||
- Semi Join 이 반환하는 행 = 1509(페이지 20개 부모의 highlights). 부모 M행 × 자식 = M×K 로 곱하지 않는다.
|
||||
L3 카테시안(조인이 feed_items ⋈ highlights 를 1961행으로 곱함)과 정반대 — 자식 K행만 반환(합, 곱 아님).
|
||||
- 배치 페치가 하는 일이 이 shape다: 페이지 부모 키를 모아 WHERE feed_item_id IN (…) 로 한 방에 채운다.
|
||||
Hibernate 는 이를 default_batch_fetch_size 만큼 쪼개 ceil(pageItems/batch) 번 발행한다.
|
||||
- Buffers: read≈0 → warm buffer cache. Execution Time 0.592 ms 는 executor 내부 시간(§6.4 caveat와 동일).
|
||||
@@ -0,0 +1,26 @@
|
||||
엔티티만 페이징한 SQL의 실행계획 (a) — Limit 노드 존재 (배치 페치 해법의 페이징)
|
||||
출처: FeedBatchFetchIT.l5ExplainEntityPagingHasLimitAndBatchInHasNoRowMultiplication 콘솔 출력 (a)
|
||||
조건: seed(100) 직후, default_batch_fetch_size=100 세션. warm buffer cache.
|
||||
쿼리: EXPLAIN (ANALYZE, BUFFERS)
|
||||
SELECT fi.* FROM feed_items fi ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20
|
||||
(배치 해법은 fetch join을 버리고 엔티티만 페이징한다 → DB가 정상 페이징)
|
||||
|
||||
Limit (cost=71.96..72.01 rows=20 width=1194) (actual time=0.108..0.109 rows=20 loops=1)
|
||||
Buffers: shared hit=65
|
||||
-> Sort (cost=71.96..72.84 rows=354 width=1194) (actual time=0.107..0.108 rows=20 loops=1)
|
||||
Sort Key: first_highlighted_at DESC, id
|
||||
Sort Method: top-N heapsort Memory: 28kB
|
||||
Buffers: shared hit=65
|
||||
-> Seq Scan on feed_items fi (cost=0.00..62.54 rows=354 width=1194) (actual time=0.075..0.080 rows=100 loops=1)
|
||||
Buffers: shared hit=59
|
||||
Planning:
|
||||
Buffers: shared hit=14 read=1
|
||||
Planning Time: 0.085 ms
|
||||
Execution Time: 0.118 ms
|
||||
|
||||
관찰(문서 §11):
|
||||
- 계획 최상단에 Limit 노드가 있다 = DB가 페이징을 했다. top-N heapsort 28kB로 상위 20행만 취한다.
|
||||
- L4 (a)(컬렉션 fetch join)는 Limit 노드가 없어 전체 1961행을 quicksort(445kB)로 정렬했다 — 정반대.
|
||||
fetch join을 버리니(엔티티만 페이징) 페이징이 DB로 내려간다(HHH000104 인메모리 페이징 소멸).
|
||||
- 대조군: l5-batch-in-semijoin.txt (페이지 부모들의 highlights 를 IN 한 방으로 — 행을 곱하지 않는다).
|
||||
- Buffers: read≈0 → warm buffer cache. Execution Time 0.118 ms 는 executor 내부 시간(§6.4 caveat와 동일).
|
||||
@@ -0,0 +1,27 @@
|
||||
-- L6 (b) child scalar IN projection — SELECT new carrier(h.feedItem.id, h.color, h.text, h.createdAt)
|
||||
-- FeedProjectionIT.l6ExplainProjectionHasLimitAndSemiJoinNotNarrowerWidth (seed 100, page size 20)
|
||||
-- SELECT h.feed_item_id, h.color, h.text, h.created_at FROM highlights h
|
||||
-- WHERE h.feed_item_id IN (SELECT fi.id FROM feed_items fi ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20)
|
||||
-- 읽기 포인트: Hash Semi Join 이라 자식 행(1509)만 반환 — 부모 M × 자식 K 로 곱하지 않는다(카테시안 없음).
|
||||
-- L5 배치 IN (b)와 같은 shape/행수(1509). 프로젝션은 필요 컬럼만(color/text/created_at) 읽는다.
|
||||
|
||||
Hash Semi Join (cost=50.51..235.83 rows=177 width=686) (actual time=0.218..0.452 rows=1509 loops=1)
|
||||
Hash Cond: (h.feed_item_id = "ANY_subquery".id)
|
||||
Buffers: shared hit=202
|
||||
-> Seq Scan on highlights h (cost=0.00..178.71 rows=1771 width=686) (actual time=0.148..0.226 rows=1961 loops=1)
|
||||
Buffers: shared hit=161
|
||||
-> Hash (cost=50.26..50.26 rows=20 width=16) (actual time=0.067..0.067 rows=20 loops=1)
|
||||
Buckets: 1024 Batches: 1 Memory Usage: 9kB
|
||||
Buffers: shared hit=41
|
||||
-> Subquery Scan on "ANY_subquery" (cost=50.01..50.26 rows=20 width=16) (actual time=0.060..0.062 rows=20 loops=1)
|
||||
Buffers: shared hit=41
|
||||
-> Limit (cost=50.01..50.06 rows=20 width=24) (actual time=0.059..0.060 rows=20 loops=1)
|
||||
Buffers: shared hit=41
|
||||
-> Sort (cost=50.01..50.62 rows=246 width=24) (actual time=0.059..0.059 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
Sort Method: top-N heapsort Memory: 26kB
|
||||
Buffers: shared hit=41
|
||||
-> Seq Scan on feed_items fi (cost=0.00..43.46 rows=246 width=24) (actual time=0.035..0.041 rows=100 loops=1)
|
||||
Buffers: shared hit=41
|
||||
Planning Time: 0.064 ms
|
||||
Execution Time: 0.497 ms
|
||||
@@ -0,0 +1,34 @@
|
||||
-- L6 (a) parent scalar projection — SELECT new carrier(f.id, u.name, u.username, p.url, p.title, f.firstHighlightedAt)
|
||||
-- FeedProjectionIT.l6ExplainProjectionHasLimitAndSemiJoinNotNarrowerWidth (seed 100, page size 20)
|
||||
-- SELECT fi.id, u.name, u.username, p.url, p.title, fi.first_highlighted_at
|
||||
-- FROM feed_items fi JOIN users u ON u.id = fi.user_id JOIN pages p ON p.id = fi.page_id
|
||||
-- ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20
|
||||
-- 읽기 포인트: Limit 노드 존재(페이징 정상)이나 width=2088 로 엔티티 SELECT fi.*(L5 (a) width 1194)보다 넓다
|
||||
-- — users/pages 조인 + PG varchar 추정치 탓. 프로젝션 이득은 EXPLAIN 아니라 ORM 층(entityLoadCount 0).
|
||||
|
||||
Limit (cost=102.56..102.61 rows=20 width=2088) (actual time=0.187..0.190 rows=20 loops=1)
|
||||
Buffers: shared hit=81
|
||||
-> Sort (cost=102.56..103.17 rows=246 width=2088) (actual time=0.187..0.188 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
Sort Method: top-N heapsort Memory: 27kB
|
||||
Buffers: shared hit=81
|
||||
-> Hash Join (cost=51.24..96.01 rows=246 width=2088) (actual time=0.137..0.161 rows=100 loops=1)
|
||||
Hash Cond: (fi.page_id = p.id)
|
||||
Buffers: shared hit=81
|
||||
-> Hash Join (cost=10.68..54.79 rows=246 width=1072) (actual time=0.072..0.087 rows=100 loops=1)
|
||||
Hash Cond: (fi.user_id = u.id)
|
||||
Buffers: shared hit=43
|
||||
-> Seq Scan on feed_items fi (cost=0.00..43.46 rows=246 width=56) (actual time=0.053..0.057 rows=100 loops=1)
|
||||
Buffers: shared hit=41
|
||||
-> Hash (cost=10.30..10.30 rows=30 width=1048) (actual time=0.010..0.010 rows=20 loops=1)
|
||||
Buckets: 1024 Batches: 1 Memory Usage: 10kB
|
||||
Buffers: shared hit=2
|
||||
-> Seq Scan on users u (cost=0.00..10.30 rows=30 width=1048) (actual time=0.003..0.004 rows=20 loops=1)
|
||||
Buffers: shared hit=2
|
||||
-> Hash (cost=39.14..39.14 rows=114 width=1048) (actual time=0.059..0.059 rows=100 loops=1)
|
||||
Buckets: 1024 Batches: 1 Memory Usage: 17kB
|
||||
Buffers: shared hit=38
|
||||
-> Seq Scan on pages p (cost=0.00..39.14 rows=114 width=1048) (actual time=0.040..0.045 rows=100 loops=1)
|
||||
Buffers: shared hit=38
|
||||
Planning Time: 0.134 ms
|
||||
Execution Time: 0.228 ms
|
||||
@@ -0,0 +1,20 @@
|
||||
반복되는 Page ToOne 부모 쿼리의 실행계획 (N2 — 선형 범인)
|
||||
출처: FeedPersistenceIT.l2ExplainRepeatedPageToOneQuery 콘솔 출력
|
||||
조건: seed(100) 직후. warm buffer cache(shared read=0). id는 시드된 실제 pages.id 1건.
|
||||
쿼리: SELECT * FROM pages WHERE id = ? (@ManyToOne EAGER가 행마다 반복하는 2차 SELECT)
|
||||
|
||||
Index Scan using pk_pages on pages
|
||||
(cost=0.14..8.15 rows=1 width=2104) (actual time=0.009..0.009 rows=1 loops=1)
|
||||
Index Cond: (id = 'a069f5ac-fa46-41f8-bcde-174153789467'::uuid)
|
||||
Buffers: shared hit=2
|
||||
Planning Time: 0.031 ms
|
||||
Execution Time: 0.021 ms
|
||||
|
||||
주의(문서 §7.3 / §6.4 caveat와 동일):
|
||||
- PK 조회라 pk_pages Index Scan으로 1건 0.021 ms. "쿼리가 느려서"가 아니다 — page는 아이템당
|
||||
고유(dedup 없음)라 이 빠른 계획이 정확히 N번 반복되는 게 문제다(왕복 N회).
|
||||
- users 계획(toone-users-plan.txt)과 실행계획이 사실상 동일하다. 비용을 가르는 것은 계획이 아니라
|
||||
반복 횟수(page=N vs user=distinct≤20)다 — 카디널리티가 곡선을 가른다.
|
||||
- Buffers: shared hit=2, read=0 → warm buffer cache. cold 디스크 I/O 실행시간으로 읽지 말 것.
|
||||
- Execution Time 0.021 ms는 executor 내부 시간. 애플리케이션 지연(§6.2)과 같은 지표가 아니다.
|
||||
- 인덱스로 안 풀린다(계획이 이미 PK Index Scan). 왕복 횟수 자체를 줄이는 fetch 전략이 필요(§9).
|
||||
@@ -0,0 +1,19 @@
|
||||
반복되는 User ToOne 부모 쿼리의 실행계획 (N2 — 평탄, 1차 캐시 dedup)
|
||||
출처: FeedPersistenceIT.l2ExplainRepeatedPageToOneQuery 콘솔 출력
|
||||
조건: seed(100) 직후. warm buffer cache(shared read=0). id는 시드된 실제 users.id 1건.
|
||||
쿼리: SELECT * FROM users WHERE id = ? (@ManyToOne EAGER가 반복하는 2차 SELECT)
|
||||
|
||||
Index Scan using pk_users on users
|
||||
(cost=0.14..8.15 rows=1 width=2104) (actual time=0.013..0.014 rows=1 loops=1)
|
||||
Index Cond: (id = '0a2a85ed-f8f3-47f0-b957-c477f4b077ab'::uuid)
|
||||
Buffers: shared hit=2
|
||||
Planning Time: 0.027 ms
|
||||
Execution Time: 0.022 ms
|
||||
|
||||
주의(문서 §7.3):
|
||||
- 단건 실행계획은 pages(toone-pages-plan.txt)와 사실상 동일하다: 둘 다 pk Index Scan, ~0.02 ms.
|
||||
- 그러나 반복 횟수가 다르다. user는 소수 풀(≤20)을 재사용하고 한 번 로드된 대상은 영속성
|
||||
컨텍스트(1차 캐시)에 남아 재조회되지 않으므로, 서로 다른 대상(distinct target) 수만큼만
|
||||
나간다 → N과 무관하게 ≤20에서 평탄. page는 아이템당 고유라 N번.
|
||||
- 결론: 같은 @ManyToOne(EAGER)·같은 실행계획인데 곡선이 갈리는 원인은 계획이 아니라
|
||||
데이터 분포(카디널리티)다. EXPLAIN만 보면 둘이 똑같아 보이는 것이 '숨은' N+1의 얼굴이다.
|
||||
@@ -0,0 +1,7 @@
|
||||
metric,precompute,single_or,note
|
||||
page1_unified_parents,20,20,keyset page — both parent paths return the same 20
|
||||
page1_unified_rows,60,60,LATERAL top-3 per parent (<=60)
|
||||
page1_buffers_shared_hit,63,181,env-dependent (relative only — warm cache)
|
||||
deep_keyset_parent_scanned,19,200,precompute index-range(19) vs BitmapOr + hashed mention SubPlan(200)
|
||||
deep_keyset_buffers_shared_hit,60,88,env-dependent (relative only — warm cache)
|
||||
viewer_visible_set,1500,1500,feed_visible count for user008 (= single-OR page-1 candidate set)
|
||||
|
@@ -0,0 +1,4 @@
|
||||
N,collection_init,prepared_total,toone
|
||||
10,10,25,13
|
||||
100,100,222,120
|
||||
1000,1000,2022,1020
|
||||
|
@@ -0,0 +1,8 @@
|
||||
rank,highlights
|
||||
1,500
|
||||
2,225
|
||||
3,141
|
||||
5,79
|
||||
10,35
|
||||
50,6
|
||||
100,3
|
||||
|
@@ -0,0 +1,4 @@
|
||||
K,window_rows,window_buffers,window_ms,lateral_rows,lateral_buffers,lateral_ms
|
||||
3,60,162,1.388,60,114,0.271
|
||||
50,695,216,1.540,695,155,0.908
|
||||
500,1509,269,2.905,1509,171,1.259
|
||||
|
@@ -0,0 +1,3 @@
|
||||
variant,top_node,child_access,buffers_shared_hit,exec_ms
|
||||
with_index,Nested Loop,Index Scan using ix_highlights_feed_items_created (Limit 3),168,0.336
|
||||
without_index,Nested Loop,Seq Scan on highlights (Rows Removed by Filter 2842/loop),4446,5.472
|
||||
|
@@ -0,0 +1,4 @@
|
||||
strategy,top_node,returned_rows,buffers_shared_hit,exec_ms
|
||||
window,WindowAgg (Subquery Scan on t),60,430,1.552
|
||||
lateral,Nested Loop (Index Scan + Limit 3),60,204,0.323
|
||||
twostep,Sort (Hash Semi Join),1509,430,1.686
|
||||
|
@@ -0,0 +1,5 @@
|
||||
strategy,returned_rows,parents_covered,max_per_parent
|
||||
window,60,20,3
|
||||
lateral,60,20,3
|
||||
twostep_full,1509,20,unbounded
|
||||
naive_wrong_limit3,3,1,3
|
||||
|
@@ -0,0 +1,4 @@
|
||||
variant,top_node,returned_rows,scanned_rows,buffers_shared_hit,exec_ms
|
||||
offset,Limit<-Sort<-Seq Scan,20,2000,141,0.996
|
||||
keyset_with_index,Limit<-Index Only Scan,20,20,1,0.076
|
||||
keyset_without_index,Limit<-Sort<-Seq Scan (filter),20,20,141,0.373
|
||||
|
@@ -0,0 +1,4 @@
|
||||
offset,page,offset_scanned,offset_buffers,keyset_scanned,keyset_buffers
|
||||
0,1,20,1,20,3
|
||||
980,50,1000,18,20,3
|
||||
1980,100,2000,106,20,2
|
||||
|
@@ -0,0 +1,4 @@
|
||||
approach,top_node,sort,mentions_handling,candidate_rows,buffers_shared_hit,exec_ms
|
||||
single_or,Bitmap Heap Scan + top-N Sort,re-sort,hashed SubPlan,1500,122,0.808
|
||||
union_decompose,Merge Append (per-branch index),per-branch merge,Hash Join,,200,0.780
|
||||
precompute,Index Only Scan on feed_visible,none,pre-materialized,20,1,0.034
|
||||
|
@@ -0,0 +1,4 @@
|
||||
N,page_fetch,user_fetch,entity_fetch,collection_init,prepared_total
|
||||
10,10,3,13,10,25
|
||||
100,100,20,120,100,222
|
||||
1000,1000,20,1020,1000,2022
|
||||
|
@@ -0,0 +1,4 @@
|
||||
N,transferred_rows_join_card,list_size_hibernate6_dedup,distinct_items,seeded_highlights,blowup_x,prepared_total
|
||||
10,1285,10,10,1285,128.5,14
|
||||
100,1961,100,100,1961,19.6,121
|
||||
1000,2917,1000,1000,2917,2.9,1021
|
||||
|
@@ -0,0 +1,4 @@
|
||||
N,p50_ms,p99_ms,thread_alloc_kb
|
||||
10,6.184,6.566,1582
|
||||
100,13.890,16.062,3061
|
||||
1000,79.452,83.526,10230
|
||||
|
@@ -0,0 +1,4 @@
|
||||
N,returned_page,feed_item_loaded,over_fetch_x,seeded_highlights
|
||||
10,10,10,1.0,1285
|
||||
100,20,100,5.0,1961
|
||||
1000,20,1000,50.0,2917
|
||||
|