chore: snapshot working tree before harness removal
미추적 파일과 미커밋 수정을 전부 담아 pre-harness-removal 태그의 복구 범위를 확보한다. .agents/skills/writing-natural-korean 9개와 korean-technical-blog-skills-bundle-v1 61개가 여기 포함된다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b101b6e717
commit
1099834617
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="330" viewBox="0 0 1400 330" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Runtime call</title>
|
||||
<desc id="desc">FeedController calls GetFeedUseCase, which dispatches to SpringTransactionPort at runtime.</desc>
|
||||
<metadata>{"source":"runtime-call-source-dependency.svg","panel":"upper","canvas_policy":"diagram-only"}</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>
|
||||
</defs>
|
||||
<rect width="1400" height="330" 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>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="340" viewBox="0 0 1400 340" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Source dependency and contract ownership</title>
|
||||
<desc id="desc">GetFeedUseCase depends on application-core contracts, while SpringTransactionPort implements TransactionPort.</desc>
|
||||
<metadata>{"source":"runtime-call-source-dependency.svg","panel":"lower","canvas_policy":"diagram-only"}</metadata>
|
||||
<defs>
|
||||
<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-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>
|
||||
</defs>
|
||||
<rect width="1400" height="340" fill="#FFFFFF"/>
|
||||
<rect x="35" y="45" width="1330" height="255" fill="#FBFCFE" stroke="#C5CBD3" stroke-width="1.3" rx="10"/>
|
||||
<rect x="44" y="34" width="284.2" 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="95" y="135" width="260" height="85" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8"/>
|
||||
<text x="225" y="156" 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="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">QueryUseCase<Q,R></text>
|
||||
<text x="225" y="202.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="115" width="270" height="125" fill="#EAF3FF" stroke="#1677FF" stroke-width="1.5" rx="8"/>
|
||||
<text x="700" 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">GetFeedUseCase</text>
|
||||
<text x="700" 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">implements · calls</text>
|
||||
<rect x="1060" y="135" width="240" height="85" fill="#F1EDFF" stroke="#7556D8" stroke-width="1.5" rx="8"/>
|
||||
<text x="1180" y="156" 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="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">TransactionPort</text>
|
||||
<text x="1180" y="202.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 160 L 355 160" 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="135" width="76.7" height="20" fill="#FFFFFF" rx="2"/>
|
||||
<text x="460" y="151" 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 160 L 1060 160" 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="135" width="89.2" height="20" fill="#FFFFFF" rx="2"/>
|
||||
<text x="947.5" y="151" 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="65" width="200" height="50" fill="#FFFFFF" stroke="#59616B" stroke-width="1.5" rx="8"/>
|
||||
<text x="1180" y="95" 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 115 L 1180 135" 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="117" width="76.7" height="20" fill="#FFFFFF" rx="2"/>
|
||||
<text x="1260" y="133" 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: 4.7 KiB |
@@ -236,12 +236,12 @@ Port는 아웃바운드 어댑터가 코어에 제공해야 할 기능을 정합
|
||||
코드가 바뀌는 이유를 기준으로 `ca-tmpl`의 모듈에 대응시켰습니다. 같은 이유로 바뀌는 코드는 한 경계에
|
||||
두고 다른 이유로 바뀌는 코드는 의존 방향을 나눴습니다.
|
||||
|
||||
| 링 | `ca-tmpl` 모듈 | 대표 책임 | 이곳에 두는 이유 |
|
||||
| -------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| 링 | `ca-tmpl` 모듈 | 대표 책임 | 이곳에 두는 이유 |
|
||||
| -------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| 엔터프라이즈 업무 규칙(Enterprise Business Rules) | `domain-core` | Aggregate·Value Object — 예:`FeedItem` | 핵심 불변식은 HTTP·DB·Spring 교체와 무관하게 유지돼야 합니다. 그래서 JPA와 Spring 타입을 클래스패스에서 제외합니다. |
|
||||
| 애플리케이션 업무 규칙(Application Business Rules) | `application-core` | Use Case·Input/Output Port — 예:`GetFeedUseCase`, `FeedQueryPort` | 유스케이스는 업무 흐름과 필요한 외부 능력을 정의하되, 그 능력을 어떤 기술로 구현하는지는 몰라야 합니다. |
|
||||
| 인터페이스 어댑터(Interface Adapters) | `adapter:inbound:*`, `adapter:outbound:*` | Controller·영속성 어댑터 — 예:`FeedController`, `FeedQueryAdapter` | HTTP·JPA 같은 외부 모델을 코어 계약으로 변환하는 책임을 모아 기술 변경의 직접 파급을 경계 밖에 가둡니다. |
|
||||
| 프레임워크와 드라이버(Frameworks & Drivers) | 어댑터의 구체 기술 의존,`app-bootstrap` | Spring MVC·Spring Data JPA·PostgreSQL·Boot/Flyway·관측·보안 배선 | 구체 프레임워크 선택과 실행 시점 조립은 배포 환경에 따라 바뀌므로 가장 바깥에서 결정합니다. |
|
||||
| 애플리케이션 업무 규칙(Application Business Rules) | `application-core` | Use Case·Input/Output Port — 예:`GetFeedUseCase`, `FeedQueryPort` | 유스케이스는 업무 흐름과 필요한 외부 능력을 정의하되, 그 능력을 어떤 기술로 구현하는지는 몰라야 합니다. |
|
||||
| 인터페이스 어댑터(Interface Adapters) | `adapter:inbound:*`, `adapter:outbound:*` | Controller·영속성 어댑터 — 예:`FeedController`, `FeedQueryAdapter` | HTTP·JPA 같은 외부 모델을 코어 계약으로 변환하는 책임을 모아 기술 변경의 직접 파급을 경계 밖에 가둡니다. |
|
||||
| 프레임워크와 드라이버(Frameworks & Drivers) | 어댑터의 구체 기술 의존,`app-bootstrap` | Spring MVC·Spring Data JPA·PostgreSQL·Boot/Flyway·관측·보안 배선 | 구체 프레임워크 선택과 실행 시점 조립은 배포 환경에 따라 바뀌므로 가장 바깥에서 결정합니다. |
|
||||
|
||||
`domain-core`를 별도 모듈로 둔 이유는 도메인 규칙을 프레임워크 변경에서 보호하기 위해서입니다. 이 모듈에는
|
||||
Spring Web, JPA, Spring TX가 없으므로 도메인 코드가 해당 타입을 참조하면 컴파일 단계에서 실패합니다.
|
||||
@@ -391,7 +391,9 @@ Inbound와 outbound는 테스트 전략도 다릅니다.
|
||||
- Outbound adapter : 데이터 매핑, 외부 시스템 연동, timeout-retry, 기술 예외 변환
|
||||
|
||||
<!-- techviz:begin id=inbound-transport-boundary context-sha256=04fbab095d33d301746c34f7cca305730919bad3c341bcf63b8ad3ee3b396d31 -->
|
||||
|
||||
<!-- techviz:generate id=inbound-transport-boundary -->
|
||||
|
||||

|
||||
|
||||
왼쪽에서 오른쪽으로 읽습니다. web은 HTTP DTO, grpc는 protobuf message, graphql은 GraphQL request, websocket은 WebSocket message를 각 어댑터 경계에서 처리합니다. 네 어댑터는 전송 기술 타입을 application-core로 넘기지 않고 Command 또는 Query로 변환합니다. 변환된 입력만 Application use case를 호출합니다.
|
||||
@@ -401,7 +403,9 @@ app-bootstrap은 프로젝트가 실제 사용할 inbound와 outbound adapter를
|
||||
이 프로젝트의 모듈 간 의존은 inbound와 outbound 모두 바깥에서 안쪽으로 향합니다. verifyCleanArchitectureDependencies는 모듈 간 프로젝트의 의존성을 검사하고, ArchUnit의 DOMAIN_IS_TRUE는 모듈 내부 코드가 금지된 프레임워크 타입을 참조하는지 검사합니다.
|
||||
|
||||
<!-- techviz:begin id=bootstrap-dependency-guards context-sha256=04fbab095d33d301746c34f7cca305730919bad3c341bcf63b8ad3ee3b396d31 -->
|
||||
|
||||
<!-- techviz:generate id=bootstrap-dependency-guards -->
|
||||
|
||||

|
||||
|
||||
가운데 Application Core를 기준으로 왼쪽에는 Inbound adapters와 Input port, 오른쪽에는 Output port와 Outbound adapters가 있습니다. 어댑터의 모듈 의존은 포트와 코어 쪽을 향합니다. 아래의 app-bootstrap은 실제 사용할 양쪽 어댑터를 선택하고 application port에 연결합니다. 별도의 두 검증 게이트 중 verifyCleanArchitectureDependencies는 모듈 간 프로젝트 의존을 검사하고 ArchUnit 규칙은 모듈 내부 코드의 금지된 프레임워크 타입 참조를 검사합니다.
|
||||
@@ -433,15 +437,16 @@ app-bootstrap은 프로젝트가 실제 사용할 inbound와 outbound adapter를
|
||||
`Command` 마커는 `sample-portfolio`의 `CreateWorkLogCommand`에 실제로 적용돼 있습니다.
|
||||
|
||||
재매핑은 두 번 일어납니다.
|
||||
|
||||
1. persistence에서 application으로 넘어갈 때입니다.
|
||||
`FeedQueryAdapter.loadFeed()`는 JPA 조회 결과를 `FeedSummary`로 직접 조립합니다.
|
||||
`FeedItemJpaEntity → FeedItem → FeedSummary`처럼 애그리게이트를 재구성하지 않고 조회 결과에서 곧장 애플리케이션 프로젝션으로 건너갑니다.
|
||||
`FeedQueryAdapter.loadFeed()`는 JPA 조회 결과를 `FeedSummary`로 직접 조립합니다.
|
||||
`FeedItemJpaEntity → FeedItem → FeedSummary`처럼 애그리게이트를 재구성하지 않고 조회 결과에서 곧장 애플리케이션 프로젝션으로 건너갑니다.
|
||||
|
||||
같은 DB 안에서 읽기 경로만 논리적으로 나누는 이 우회가 뒤에서 다룰 CQRS-lite 결정의 구체적인 모습입니다.
|
||||
CQRS는 명령(Command)과 조회(Query)의 코드·모델을 나누는 패턴이고 lite는 저장소 분리 없이 코드 경로와 모델만 나눈 수준을 뜻합니다.
|
||||
|
||||
2. application에서 web으로 나갈 때입니다.
|
||||
`FeedWebMapper.toResponse()`가 `FeedSummary`를 `FeedResponse`로 다시 조립합니다.
|
||||
`FeedWebMapper.toResponse()`가 `FeedSummary`를 `FeedResponse`로 다시 조립합니다.
|
||||
|
||||
두 매핑을 모두 어댑터가 소유하므로 `GetFeedUseCase`와 `FeedQueryPort`는 웹 응답이나 JPA 엔티티의
|
||||
모양을 모릅니다. `GetFeedUseCase`가 `FeedResponse`를 직접 만들었다면 HTTP 응답 변경이 코어 변경으로 번졌을 것입니다. 반대로 도메인 재구성이 필요한 경로에서는 어댑터의 `FeedItemPersistenceMapper`가 코어 모델 변경을 따라 바뀌는 것이 의도한 결합입니다.
|
||||
@@ -474,11 +479,11 @@ arawn은 외형 복제보다 높은 응집과 느슨한 결합을 강조했습
|
||||
`shared-contract`를 알고 SLF4J도 사용합니다. “세 모듈이 모두 완전히 순수하다”고 약속하는 대신,
|
||||
각 모듈이 알아도 되는 타입을 클래스패스와 ArchUnit 규칙으로 제한했습니다.
|
||||
|
||||
| 모듈 | 맡은 결정 | 허용한 지식 | 대표 실행 경로 | 경계를 고정하는 규칙 |
|
||||
| -------------------- | ----------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| `domain-core` | 애그리게이트·값·이벤트·식별자와 불변식 | main compile은 JDK와 자체 타입뿐 | `FeedItem`이 자체 스테레오타입·JDK 타입만 사용 | `DOMAIN_IS_PURE`, `DOMAIN_HAS_NO_LOGGER` |
|
||||
| `application-core` | 유스케이스 순서와 바깥 능력의 포트 | domain-core, shared-contract | `GetFeedUseCase`가 `tx.inRead(() -> feedQuery.loadFeed(...))` 호출 | `APPLICATION_DOES_NOT_DEPEND_ON_ADAPTERS_OR_TRANSPORT` 외 |
|
||||
| `shared-contract` | 응답·오류·추적·메트릭 같은 운영 계약 | main 외부 의존 0, 허용한 운영 패키지 prefix | `Envelope(success, data, error, meta)`와 `ApiErrorCode` | `SHARED_CONTRACT_CONTAINS_ONLY_OPERATIONAL_CONTRACT_PACKAGES` |
|
||||
| 모듈 | 맡은 결정 | 허용한 지식 | 대표 실행 경로 | 경계를 고정하는 규칙 |
|
||||
| -------------------- | ----------------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| `domain-core` | 애그리게이트·값·이벤트·식별자와 불변식 | main compile은 JDK와 자체 타입뿐 | `FeedItem`이 자체 스테레오타입·JDK 타입만 사용 | `DOMAIN_IS_PURE`, `DOMAIN_HAS_NO_LOGGER` |
|
||||
| `application-core` | 유스케이스 순서와 바깥 능력의 포트 | domain-core, shared-contract | `GetFeedUseCase`가 `tx.inRead(() -> feedQuery.loadFeed(...))` 호출 | `APPLICATION_DOES_NOT_DEPEND_ON_ADAPTERS_OR_TRANSPORT` 외 |
|
||||
| `shared-contract` | 응답·오류·추적·메트릭 같은 운영 계약 | main 외부 의존 0, 허용한 운영 패키지 prefix | `Envelope(success, data, error, meta)`와 `ApiErrorCode` | `SHARED_CONTRACT_CONTAINS_ONLY_OPERATIONAL_CONTRACT_PACKAGES` |
|
||||
|
||||
`domain-core`의 빈 의존 블록은 Spring·JPA·Servlet·Hibernate 같은 외부 프레임워크 타입이 들어올 직접 의존 통로를 없앱니다. JDK 자체의 파일·네트워크·SQL API까지 자동으로 금지한다는 뜻은 아닙니다.
|
||||
`DOMAIN_IS_PURE`는 금지 패키지 의존을 막고 `DOMAIN_HAS_NO_LOGGER`는 로깅 프레임워크까지 차단합니다.
|
||||
@@ -501,8 +506,8 @@ REST·gRPC·GraphQL·WebSocket 네 인바운드 모듈은 같은 깊이로 구
|
||||
두었습니다. 구현 범위는 달라도 전송 기술을 코어 밖에 두고 아웃바운드 구현을 직접 고르지
|
||||
않는 규칙은 같게 적용했습니다.
|
||||
|
||||
| 모듈 | 현재 제공하는 기능 | 결정적인 차이 |
|
||||
| ----------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| 모듈 | 현재 제공하는 기능 | 결정적인 차이 |
|
||||
| ----------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| `adapter:inbound:web` | `/feed` REST·health·공유 웹 인프라 | `FeedController`가 `GetFeedUseCase`를 호출하고 `FeedWebMapper`로 응답 DTO를 만듭니다 |
|
||||
| `adapter:inbound:grpc` | health·reflection | 피처 proto 없이 Netty 서버를 직접 수명주기 관리합니다 |
|
||||
| `adapter:inbound:graphql` | 최소 헬스 스키마 | 피처 스키마·리졸버 추가는 소비 프로젝트의 확장 작업입니다 |
|
||||
@@ -525,12 +530,12 @@ REST·gRPC·GraphQL·WebSocket 네 인바운드 모듈은 같은 깊이로 구
|
||||
달라질 수 있어 “바로 실행할 기준 구현”과 “소비 프로젝트가 채울 확장점”을 구분했습니다. 구현 깊이는
|
||||
달라도 인바운드나 다른 아웃바운드 구현을 직접 선택하지 않는 규칙은 같게 적용했습니다.
|
||||
|
||||
| 묶음 | 모듈 | 구현된 능력 | 도입 시 확인할 예외 |
|
||||
| --------------- | ------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| 영속성 | `persistence-jpa`, `persistence-mongo` | RDBMS 구현과 opt-in NoSQL 배선 | 멱등성·outbox·분산 락은 JPA에 구현돼 있습니다. Mongo는 드라이버·리포지토리 스캔 배선만 있고 `document`와 `repository`는 템플릿을 가져다 쓰는 프로젝트에서 추가합니다. |
|
||||
| SDK 없는 확장점 | `cache-redis`, `messaging`, `notification` | 실제 Redis·Kafka·Slack 클라이언트를 연결할 인터페이스 | 모듈이 존재해도 벤더 연결이 완성된 것은 아닙니다 |
|
||||
| 외부 시스템 | `objectstorage`, `httpclient`, `fileserver` | AWS SDK S3/MinIO, 회복성 HTTP, 순수 JDK 파일 출력 | 클라이언트 유무와 빈 활성화 조건이 서로 다릅니다 |
|
||||
| 기반 구현 | `identifier`, `support` | UUIDv7 식별자와 공통 상관관계 로깅 | `support`만 형제들이 공유할 수 있습니다 |
|
||||
| 묶음 | 모듈 | 구현된 능력 | 도입 시 확인할 예외 |
|
||||
| --------------- | ------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 영속성 | `persistence-jpa`, `persistence-mongo` | RDBMS 구현과 opt-in NoSQL 배선 | 멱등성·outbox·분산 락은 JPA에 구현돼 있습니다. Mongo는 드라이버·리포지토리 스캔 배선만 있고`document`와 `repository`는 템플릿을 가져다 쓰는 프로젝트에서 추가합니다. |
|
||||
| SDK 없는 확장점 | `cache-redis`, `messaging`, `notification` | 실제 Redis·Kafka·Slack 클라이언트를 연결할 인터페이스 | 모듈이 존재해도 벤더 연결이 완성된 것은 아닙니다 |
|
||||
| 외부 시스템 | `objectstorage`, `httpclient`, `fileserver` | AWS SDK S3/MinIO, 회복성 HTTP, 순수 JDK 파일 출력 | 클라이언트 유무와 빈 활성화 조건이 서로 다릅니다 |
|
||||
| 기반 구현 | `identifier`, `support` | UUIDv7 식별자와 공통 상관관계 로깅 | `support`만 형제들이 공유할 수 있습니다 |
|
||||
|
||||
영속성의 기준 구현은 `persistence-jpa`입니다. Feed 엔티티·리포지토리·매퍼와, 나중에 확인할
|
||||
트랜잭션·락·멱등성·outbox 구현이 이 모듈에 놓입니다. PostgreSQL 드라이버는 `runtimeOnly`라
|
||||
@@ -581,8 +586,11 @@ Mongo 자동설정 세 개를 꺼 클래스패스를 무력화하고, 모듈이
|
||||
|
||||
아래 그림은 `app-bootstrap`의 main 프로젝트 의존에 포함된 어댑터 열한 개와 현재 main 의존 목록에
|
||||
없는 참조 어댑터 세 개를 비교합니다. 실행 시 활성 빈 전체를 측정한 그림은 아닙니다.
|
||||
|
||||
<!-- techviz:begin id=production-vs-optin context-sha256=81fb5cb8cd16eaae6916a0d0f2b3cddfabc39e58a87559466b52f922ca95a95b -->
|
||||
|
||||
<!-- techviz:generate id=production-vs-optin -->
|
||||
|
||||

|
||||
|
||||
<details>
|
||||
@@ -593,6 +601,7 @@ Mongo 자동설정 세 개를 꺼 클래스패스를 무력화하고, 모듈이
|
||||
</details>
|
||||
|
||||
[Editable source](../assets/production-vs-optin.drawio) · [Grounded VizSpec](.techviz/production-vs-optin/spec.json)
|
||||
|
||||
<!-- techviz:end id=production-vs-optin -->
|
||||
|
||||
```java
|
||||
@@ -606,6 +615,7 @@ public CacheBackend redisCacheBackend(RedisClient redisClient) {
|
||||
return new RedisCacheStore(redisClient);
|
||||
}
|
||||
```
|
||||
|
||||
`@ConditionalOnProperty`가 외부 백엔드 빈 활성화를 한 번 더 결정합니다.
|
||||
|
||||
`app-bootstrap`이 의존하는 모듈도 모두 실행되지는 않습니다. Redis나 Kafka 같은 외부 백엔드는 런타임
|
||||
@@ -861,12 +871,12 @@ Bean Validation도 범위 거부도 없습니다. 두 파라미터는
|
||||
|
||||
계층 규율을 고정하는 규칙과 대표 테스트는 다음과 같습니다.
|
||||
|
||||
| 검사 | 고정하는 경계 |
|
||||
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `VALIDATION_CONSTRAINTS_STAY_AT_WEB_BOUNDARY` | 도메인·애플리케이션 패키지의`jakarta.validation..` 의존 거부 |
|
||||
| 검사 | 고정하는 경계 |
|
||||
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `VALIDATION_CONSTRAINTS_STAY_AT_WEB_BOUNDARY` | 도메인·애플리케이션 패키지의`jakarta.validation..` 의존 거부 |
|
||||
| `VALID_CASCADE_DEPTH_AT_MOST_THREE` | `@Valid` 캐스케이드의 직접 raw 필드 사슬을 3단계로 제한하는 근사 가드 — 컨테이너 제네릭 원소와 수렴 그래프의 최장 경로는 정확히 추적하지 못할 수 있습니다 |
|
||||
| `PosterControllerWireTest` | 빈 제목은 유스케이스 전에 400`VALIDATION_FAILED`, 이미지 없는 발행은 400 `POSTER_IMAGE_REQUIRED` |
|
||||
| `PosterTest` | null/blank 제목이 NPE가 아닌`PosterInvariantException(TITLE_BLANK)`로 실패 |
|
||||
| `PosterControllerWireTest` | 빈 제목은 유스케이스 전에 400`VALIDATION_FAILED`, 이미지 없는 발행은 400 `POSTER_IMAGE_REQUIRED` |
|
||||
| `PosterTest` | null/blank 제목이 NPE가 아닌`PosterInvariantException(TITLE_BLANK)`로 실패 |
|
||||
|
||||
### 예외·오류 응답 — 두 단계 처리 사슬, 하나의 Envelope
|
||||
|
||||
@@ -1543,8 +1553,8 @@ Tudum 사례는 CQRS를 버린 사례가 아닙니다. Kafka에서 Raw Hollow로
|
||||
템플릿에서는 경계를 반복 검사할 수 있지만, 1회성 서비스에서는 같은 장치가 유지비만 늘릴 수 있었습니다.
|
||||
그래서 아래 표에는 선택의 장점만 적지 않고 반대편이 더 나은 조건도 함께 남겼습니다.
|
||||
|
||||
| 결정 | 선택 | 선택으로 얻는 것 | 반대편이 나은 조건 |
|
||||
| ---------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| 결정 | 선택 | 선택으로 얻는 것 | 반대편이 나은 조건 |
|
||||
| ---------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| ① 패키지 배치 | 계층 소유 코어·어댑터와 수직 샘플을 함께 쓰는 hybrid | 프로덕션 경계는 계층별로 통제하고, 샘플은 한 기능의 종단 구성을 보여 줍니다. | 한 가지 축만으로 충분한 작은 서비스 |
|
||||
| ② 트랜잭션 경계 | `@Transactional` 대신 코어 소유 포트 | Spring TX를 애플리케이션 클래스패스에서 빼고 트랜잭션 의도를 테스트 가능한 계약으로 만듭니다. | 단일 DB를 쓰며 간접 호출 비용이 더 큰 작은 팀 |
|
||||
| ③ 모듈화 | 멀티모듈 + ArchUnit | 금지된 타입은 컴파일에서, 허용 범위 안의 패키지 위반은 테스트에서 잡습니다. | 수명이 짧아 모듈·정책 유지비를 회수하기 어려운 서비스 |
|
||||
|
||||
Reference in New Issue
Block a user