128 lines
15 KiB
Markdown
128 lines
15 KiB
Markdown
---
|
|
title: blog-topic / clean-architecture-module-blueprint-2026-05-28
|
|
source_type: blog-topic
|
|
status: raw
|
|
related_branches: [feature-skeleton-package-blueprint-contract]
|
|
related_projects: [ca-skeleton]
|
|
tags: [blog-topic, ca-skeleton, architecture, gradle, clean-architecture, hexagonal, multi-module]
|
|
created: 2026-05-28
|
|
status_label: ready-for-canonical
|
|
target_audience: backend-engineer
|
|
inspiration_url:
|
|
archive_url:
|
|
---
|
|
|
|
# blog-topic: clean-architecture-module-blueprint-2026-05-28
|
|
|
|
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
|
|
|
## Parent / 부모
|
|
|
|
- [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] — Clean Architecture skeleton 의 package/module blueprint 를 single-module feature-first 에서 Gradle multi-module Hexagonal boundary 로 _수정_ 한 결정 (Decisions D1~D8 + Default Module Blueprint tree).
|
|
- [[raw/project-notes/ca-skeleton-operational-contract]] — ca-tmpl skeleton 의 운영 계약 SSOT (§20 Skeleton Blueprint Contract 영역).
|
|
|
|
## 트리거 / Trigger
|
|
|
|
- 트리거 유형: `branch-work`
|
|
- 트리거 날짜: 2026-05-27
|
|
- 트리거 연결 노트: [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] — 초기 single-module feature-first 결정 (`결정 사항 2026-05-22`) 을 `결정 사항 2026-05-27` 에서 Gradle multi-module Hexagonal 로 _명시적으로 수정_ 한 점이 글감의 핵심 사건.
|
|
|
|
## 글감 / Topic seed
|
|
|
|
- 한 문장 요지: Clean Architecture skeleton 은 패키지 이름을 예쁘게 나누는 것만으로는 부족하다. **Gradle module boundary 가 1차 강제선, package 내부 책임이 2차 분류** 가 되어야 새 도메인 기능이 들어와도 경계가 무너지지 않는다.
|
|
- 떠오른 계기: ca-tmpl 의 초기 결정이 single-module feature-first 였다가 우아한형제들 / 카카오뱅크 사례 검토 후 multi-module Hexagonal 로 _명시적으로 수정_ 된 과정 — 의사결정의 _뒤집힘_ 자체가 글감.
|
|
- 예상 제목 후보:
|
|
- Clean Architecture 템플릿에서 package 이름보다 먼저 정해야 할 것 — module boundary
|
|
- 우리는 왜 single-module feature-first 에서 multi-module Hexagonal 로 _바꿨나_
|
|
- reference code 를 production 에서 빼고 `sample-ticket` 으로 격리한 이유
|
|
|
|
## 핵심 주장 후보 / Claim candidates
|
|
|
|
> 각 항목은 branch-note Decision ID 또는 외부 source claim ID 로 근거를 인용한다.
|
|
|
|
- 사실 후보:
|
|
- ca-tmpl 의 기본 module 은 `app-bootstrap`, `domain-core`, `application-core`, `adapter-web`, `adapter-persistence`, `adapter-outbound`, `shared-contract`, `sample-ticket` 8개. module boundary 가 1차 강제선이고 module 내부 package 는 2차 책임 분류 — 근거: `feature-skeleton-package-blueprint-contract.md` D1 (Phase C2 기본 구조 = Gradle multi-module + Clean Architecture / Hexagonal), `결정 사항 2026-05-27` ("module boundary 가 1차 강제선이고, module 내부 package 는 2차 책임 분류"). 외부 근거: `raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C1`, `raw/company-tech-blogs/modulith-kakaobank-techblog-2025.md#KAKAOBANK-MOD-C4`.
|
|
- `domain-core` 는 framework-neutral POJO 로 유지. Spring annotation, JPA annotation, HTTP DTO 를 모두 모름 — 근거: `feature-skeleton-package-blueprint-contract.md` D2 (domain-core = framework-neutral). 외부 근거: `raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C1`, `raw/official-docs/arch-clean-architecture-uncle-bob.md` (Dependency Rule).
|
|
- `application-core` 는 `domain-core` 와 `shared-contract` 에만 의존. adapter 구현체 / Spring Web / JPA / Redis / Kafka / outbound HTTP client 모두 adapter 밖으로 들어오면 안 됨 — 근거: `feature-skeleton-package-blueprint-contract.md` D3 (application-core = domain + shared only). 외부 근거: `raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C2`, `raw/company-tech-blogs/modulith-kakaobank-techblog-2025.md#KAKAOBANK-MOD-C4`.
|
|
- `shared-contract` 는 response envelope / error code / header / MDC / metric / registry / annotation 같은 _skeleton-wide operational contract_ 만 허용. business / domain concept 는 금지 — 근거: `feature-skeleton-package-blueprint-contract.md` D6 (shared-contract = operational contract only). 외부 근거: `raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011.md#SCREAM-C1`.
|
|
- `sample-ticket` 은 fixture/sample consumer 이며 production module 이 import 하거나 dependency 로 선언하면 실패 — 근거: `feature-skeleton-package-blueprint-contract.md` D7 (sample-ticket production 역수입 금지) + `feature-architecture-enforcement-rules.md` D7 (자매 ArchUnit rule). 외부 근거: ca-tmpl 자체 결정 (project-decision).
|
|
- 초기 결정 (single-module feature-first) 은 2026-05-22, 수정 결정 (Gradle multi-module + Clean Architecture / Hexagonal) 은 2026-05-27 — 근거: `feature-skeleton-package-blueprint-contract.md` §결정 사항 ("2026-05-22: 초기 문서의 기본 구조는 single-module feature-first package layout이었다", "2026-05-27: Phase C2 기본 구조는 ... 로 수정한다").
|
|
- 경험 후보:
|
|
- 기존 reference code (blog domain) 를 production module 에서 `sample-ticket/src/main/java/dev/caskeleton/sample/ticket/...` 로 격리. production module 은 `package-info.java` + skeleton anchor 중심으로 정리 — 근거: `feature-skeleton-package-blueprint-contract.md` Closure §`actually-implemented` "기존 reference code는 production module에서 `sample-ticket` 내부 `dev.caskeleton.sample.ticket.*` package로 격리됨".
|
|
- production package root 를 `dev.caskeleton` 으로 rename + `BlogApplication` → `CaSkeletonApplication` + `blog.*` 설정 prefix → `ca-skeleton.*` 전환 — 근거: 동일 Closure 항목.
|
|
- 빈 skeleton anchor module 이 ArchUnit empty should failure 를 일으켜 `allowEmptyShould(true)` 를 _빈 상태가 의도된 rule 에만_ 선별 적용 — 근거: 파생 에러 [[raw/errors/archunit-empty-should-anchor-2026-05-27]] §해결 ("빈 상태가 skeleton contract상 유효한 rule에만 `allowEmptyShould(true)`").
|
|
- `sample-ticket` 격리 후 sample 내부 `GlobalExceptionHandler` 가 `InvalidBearerTokenException` 을 import 하지만 sample build.gradle 에 `spring-boot-starter-oauth2-resource-server` 가 없어서 compile 실패 → starter 명시 추가 — 근거: 파생 에러 [[raw/errors/sample-ticket-oauth2-resource-server-dependency-2026-05-27]] §해결.
|
|
- 의견 / 해석 후보:
|
|
- 템플릿 프로젝트의 완성도 기준은 "예시 도메인이 잘 돈다" 가 아니라 **"예시를 통째로 들어내도 경계가 남는다"** 이다. ca-tmpl 의 `sample-ticket` 격리는 이 기준의 직접 검증.
|
|
- `common` / `shared` 모듈은 _편의_ 보다 _오염 방지 규칙_ 을 먼저 가져야 한다. ca-tmpl 의 `shared-contract` 는 8개 sub-package allowlist (`response/error/headers/logging/tracing/metrics/registry/annotation`) 로 명시 제한.
|
|
- **single-module feature-first 도 작은 프로젝트엔 합리적**이다. ca-tmpl 이 multi-module 을 택한 건 _template repository 라서_ 새 프로젝트가 시작될 때 경계가 흐트러지지 않도록 학습 비용을 미리 흡수한다는 결정 — 근거: `feature-skeleton-package-blueprint-contract.md` D8 Open Risk ("작은 프로젝트에서는 single-module이 비용이 낮을 수 있음. ca-tmpl은 skeleton template이므로 boundary 학습/검증 비용을 감수").
|
|
- **company-tech-blog 사례 (우아한형제들 / 카카오뱅크) 는 best practice 가 아니라 case study** 다. 본 글에서 이 두 사례를 인용하되 "공식 표준" 으로 승격하지 않는 정직함이 중요 — 근거: `feature-skeleton-package-blueprint-contract.md` Decision Evidence Map 의 Evidence Strength 컬럼 (`company-case-study`).
|
|
|
|
## Outline seed
|
|
|
|
> 각 섹션 옆에 `→ 핵심 메시지` 를 함께 명시한다.
|
|
|
|
1. 의사결정의 _뒤집힘_ — 2026-05-22 의 single-module feature-first 결정을 2026-05-27 에 명시적으로 수정한 과정 → **template 의 첫 결정도 case study 검토 뒤 뒤집을 수 있다는 정직함.**
|
|
2. module boundary 가 _1차_ 강제선인 이유 — package convention 만으로는 import 가 자유롭다 → **Gradle dependency graph 가 컴파일 단계에서 위반을 막는다.**
|
|
3. 8개 module 의 책임 — `domain-core`, `application-core`, `adapter-{web,persistence,outbound}`, `shared-contract`, `sample-ticket`, `app-bootstrap` → **dependency direction 표 + 각 모듈의 forbidden import 매트릭스.**
|
|
4. `shared-contract` 를 좁게 잡는 이유 — 8개 sub-package allowlist (`response/error/headers/...`) → **business common dumping ground 방지가 _편의_ 보다 우선.**
|
|
5. `sample-ticket` 격리 — production module 의 ArchUnit rule + Gradle dependency rule + 별도 `*Application` 가 _없음_ → **"예시를 들어내도 경계가 남는다" 가 template repository 의 완성도 기준.**
|
|
6. 구현 중 드러난 작은 실패들 — 빈 anchor 의 `allowEmptyShould` 선별 적용 + sample-ticket compile classpath 누락 → **template repository 의 "비어 있음" 은 의도된 상태일 수 있다.**
|
|
7. 다른 선택지의 정직한 비교 — single-module / layer-first / pure hexagonal / Spring Modulith → **ca-tmpl 의 선택이 _유일한 정답_ 이 아니라 _이 맥락에서의 최적_ 임을 명시.**
|
|
|
|
## Canonical 전환 후보 / Canonical extraction candidates
|
|
|
|
- `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 후보:
|
|
- 실제 적용된 8 module + 각 모듈의 sub-package 책임 트리 (`feature-skeleton-package-blueprint-contract.md` §Default Module Blueprint).
|
|
- dependency direction 매트릭스 (`Module Dependency Rule` 표).
|
|
- `dev.caskeleton` 으로의 package rename + `CaSkeletonApplication` / `BootstrapSettings` / `ca-skeleton.*` 설정 prefix 전환.
|
|
- local verification 결과 4종 (`./gradlew test`, `verifyCleanArchitectureDependencies`, `:app-bootstrap:test --tests '*CleanArchitectureTest'`, `:adapter-web:test --tests '*SettingsTest'`).
|
|
- `wiki/concepts/clean-architecture-package-layout.md` 후보:
|
|
- multi-module Hexagonal-inspired skeleton layout 의 일반화 원칙 (project-agnostic).
|
|
- "module boundary 1차, package convention 2차" 분업 원칙.
|
|
- `shared` 모듈을 좁게 잡는 _operational contract only_ rule.
|
|
- "예시를 들어내도 경계가 남는다" 의 template completeness 기준.
|
|
- 필요한 추가 검증:
|
|
- canonical 문서가 최신 코드 상태 (`dev.caskeleton`, `sample-ticket` 격리, Spring Boot 3.5.14, 새로 추가된 `feature-application-port-usecase-contract` 의 `application-core` 패키지 구조) 까지 반영하는지.
|
|
- Spring Modulith named interface 를 후속 도입했을 때 Gradle multi-module + ArchUnit 구성과의 중복/대체 관계.
|
|
|
|
## Sources / 근거 후보
|
|
|
|
- [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] — 결정 D1~D8, Default Module Blueprint tree, Module Dependency Rule 표, Closure §`actually-implemented` / `locally-verified`.
|
|
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — 자매 결정 D1~D10. 본 글의 ArchUnit 단락 근거.
|
|
- [[raw/branch-notes/feature-application-port-usecase-contract]] — `application-core` package 구조의 후속 결정 (D1: inbound `*UseCase` / outbound `*Port` naming). canonical 정제 시 통합 필요.
|
|
- [[raw/errors/archunit-empty-should-anchor-2026-05-27]] — 빈 anchor 와 `allowEmptyShould(true)` 선별 적용.
|
|
- [[raw/errors/sample-ticket-oauth2-resource-server-dependency-2026-05-27]] — sample 격리 후 compile dependency 누락.
|
|
- [[raw/interviews/clean-architecture-module-blueprint]] — 같은 작업에서 파생된 예상 면접 질문.
|
|
- [[raw/interviews/shared-contract-and-sample-isolation]] — shared/sample 책임 경계 예상 질문.
|
|
- [[raw/company-tech-blogs/woowahan-hexagonal-multimodule]] — Domain / Application / Framework / Bootstrap 4-module 격리 사례 (`WW-HEX-C1`, `WW-HEX-C2`).
|
|
- [[raw/company-tech-blogs/modulith-kakaobank-techblog-2025]] — Gradle multi-module + Hexagonal + Spring Modulith 사례 (`KAKAOBANK-MOD-C2`, `KAKAOBANK-MOD-C4`).
|
|
- [[raw/official-docs/hexagonal-cockburn-wikipedia-summary]] — Ports & Adapters 원형 (`HEX-WIKI-C5`).
|
|
- [[raw/official-docs/arch-clean-architecture-uncle-bob]] — Dependency Rule 의 클래식 근거 (`engineering-blog`, official standard 아님).
|
|
- [[raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011]] — feature/use-case 가 framework 위에 드러나야 한다는 사상 (`SCREAM-C1`).
|
|
- [[raw/official-docs/hexagonal-thombergs-buckpal-github]] — feature/package 내부 port-adapter 책임 분리 참고.
|
|
- [[raw/official-docs/layer-first-baeldung-clean-architecture-spring-boot]] — _대안 1: layer-first_ 의 입문형 사례.
|
|
- [[raw/official-docs/onion-palermo-original-2008]] — _대안 4: onion_ 의 원형.
|
|
|
|
## 미해결 / Unknown
|
|
|
|
- 아직 확인해야 할 사실: ca-tmpl 의 8 module 구조가 _실제 사업 도메인_ 이 들어왔을 때 module 분할 또는 새 adapter (e.g., `adapter-messaging`) 추가가 자연스럽게 가능한지. 현재는 reference (sample-ticket) 만 검증.
|
|
- 아직 확인해야 할 사실: Spring Modulith 의 named interface 검증을 추가하면 ArchUnit rule 중 어느 것이 _중복_ 이고 어느 것이 _보완_ 인지.
|
|
- 과장하면 안 되는 부분: 본 글의 모든 검증은 **`locally-verified`** 이며 prod 운영 검증 없음. "운영에서 검증됐다" 라는 표현 금지.
|
|
- 과장하면 안 되는 부분: 우아한형제들 / 카카오뱅크 사례는 _case study_ 다. "대기업에서 표준" 또는 "industry standard" 같은 표현으로 격상시키지 말 것 — `feature-skeleton-package-blueprint-contract.md` Decision Evidence Map Open Risk 컬럼이 이 한계를 명시.
|
|
- 블로그로 쓰기 전에 필요한 canonical 정제: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 를 _최신 코드 상태_ (특히 `feature-application-port-usecase-contract` 작업으로 추가된 `application-core` 패키지 구조) 까지 반영한 뒤 verified 항목만 글로 이동.
|
|
|
|
## Decision / 처리 결정
|
|
|
|
- 액션: `promote-to-canonical`
|
|
- 이유: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 에 module/package blueprint 글감으로 반영했다.
|
|
- 다음 단계: source canonical이 `verified` 상태이므로 이후 `blogify` 대상으로 삼을 수 있다. 단 운영 검증이나 전체 Phase C2 완료처럼 쓰지 않는다.
|
|
|
|
## Related / 관련
|
|
|
|
- 관련 branch: [[raw/branch-notes/feature-skeleton-package-blueprint-contract]], [[raw/branch-notes/feature-architecture-enforcement-rules]] (자매 — boundary 강제), [[raw/branch-notes/feature-application-port-usecase-contract]] (후속 — application 내부 패키지 구조).
|
|
- 관련 errors: [[raw/errors/archunit-empty-should-anchor-2026-05-27]], [[raw/errors/sample-ticket-oauth2-resource-server-dependency-2026-05-27]].
|
|
- 관련 interview prep: [[raw/interviews/clean-architecture-module-blueprint]], [[raw/interviews/shared-contract-and-sample-isolation]], [[raw/interviews/clean-architecture-boundary-enforcement]].
|
|
- 관련 blog topics: [[raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28]] (자매 글감 — boundary 강제), [[raw/blog-topics/transaction-port-abstraction-over-spring-transactional-2026-05-28]] (자매 글감 — application 의 framework 격리).
|
|
- derived blog: 생성 전. 생성 시 `wiki/blog/clean-architecture-module-blueprint-YYYY-MM-DD.md` 후보.
|