--- title: Clean Architecture 패키지 레이아웃 (feature-first vs layer-first vs hexagonal vs modulith vs onion) source_type: llm-generated status: draft confidence: medium tags: [clean-architecture, package-layout, hexagonal, modulith] related_projects: [ca-skeleton] last_reviewed: 2026-05-22 --- # Clean Architecture 패키지 레이아웃 (feature-first vs layer-first vs hexagonal vs modulith vs onion) > Layer: `wiki/concepts/` — 일반 개념. 내 프로젝트 사실은 `project-template` 사용. ## Summary feature-first 패키지 레이아웃은 최상위를 도메인 feature(`features/{name}/`)로 자르고 그 내부에 `presentation/application/domain/infrastructure`를 두는 구조로, 각 feature가 자체 inbound/outbound adapter와 application core를 갖는다는 점에서 본질적으로 "feature 단위로 잘린 mini-Hexagonal"과 동형이다. layer-first는 최상위가 기술 계층이고 도메인이 그 안에 흩어지는 점에서 응집도 축이 정반대다. ## Standard (공식 정의) - **Uncle Bob, Screaming Architecture (2011)**: 시스템의 최상위 디렉터리는 사용된 framework이 아니라 시스템이 "외치는" use case / business 영역이어야 한다고 주장. controller/service/repository로 자르는 layer-first는 framework가 외치는 구조라는 점을 비판한다. 출처: [[raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011]]. - **Cockburn, Hexagonal (Ports and Adapters)**: 응용 코어(application + domain)를 inbound adapter(driving)와 outbound adapter(driven)로부터 port interface로 격리. driving/driven adapter 분리가 본질이며 패키지 형태 자체는 비강제. 출처: [[raw/official-docs/hexagonal-cockburn-wikipedia-summary]]. - **Thombergs, BuckPal reference**: Cockburn Hexagonal을 자바/스프링 부트로 구현한 reference. 최상위가 feature이고 내부에 `domain/application/adapter(in|out)` 3-tier로 잘려 feature-first + Hexagonal이 같은 구조에서 만난다는 점을 보여줌. 출처: [[raw/official-docs/hexagonal-thombergs-buckpal-github]]. - **Palermo, Onion Architecture (2008)**: 의존성은 외부 layer(infrastructure/UI)에서 내부 layer(domain model)로만 향하며, 안쪽이 바깥쪽 interface를 알지 않는다는 의존성 역전 규칙. layer를 동심원으로 표현. 출처: [[raw/official-docs/onion-palermo-original-2008]]. - **Spring Modulith (공식 문서)**: Spring Boot 위에서 패키지 자체가 모듈 경계가 되며 `@ApplicationModule`/named-interface로 cross-module 접근을 강제. JPA event SPI 위에서 transactional event publication 등 운영 contract를 framework가 제공. 출처: [[raw/official-docs/modulith-spring-official-doc]]. ## 한계 / 주의점 각 레이아웃은 다른 트레이드오프를 가진다. - **feature-first** - cross-feature shared kernel(공통 value object, 공통 정책)을 어디에 둘지가 모호. `common/`을 두되 business concept가 새지 않도록 별도 규칙이 필요. - 도메인 인접성이 강한 feature 사이에서 model 중복 위험(같은 개념을 두 feature가 따로 정의). - feature 사이 호출은 직접 import보다는 port 또는 명시적 application API를 통해 통제해야 함 (그렇지 않으면 사실상 layer-first로 회귀). - **layer-first** - 도메인 수가 늘어나면 같은 도메인의 코드가 `controller/`, `service/`, `repository/`에 흩어져 응집도가 폭락. 한 도메인을 수정할 때 패키지 3~4곳을 동시에 건드림. Sahibinden 기술블로그는 이를 "패키지가 도메인을 외치지 않는다"로 비판함. 출처: [[raw/company-tech-blogs/feature-first-sahibinden-package-by-layer-vs-feature]]. - Baeldung식 Clean Architecture Spring Boot 가이드는 입문 학습 비용이 가장 낮지만 결과적으로 도메인 응집을 보장하지 않음. 출처: [[raw/official-docs/layer-first-baeldung-clean-architecture-spring-boot]], [[raw/company-tech-blogs/layer-first-kamilmazurek-github-template]]. - **hexagonal pure (feature 슬라이스 없음)** - 최상위가 `application/domain/adapter`로만 잘리고 feature 슬라이스가 없으면 도메인이 늘어날수록 `application`과 `domain` 패키지가 비대해짐. - inbound/outbound 분리는 명확하지만 도메인 간 boundary가 약함. 우아한형제들 기술블로그의 Hexagonal 적용도 결국 도메인별 module로 분리하는 방향으로 진화. 출처: [[raw/company-tech-blogs/hexagonal-woowahan-techblog-2023]]. - **Spring Modulith** - Spring Framework / Spring Boot 종속. framework-neutral 도메인을 외부 강제로 보호하기 어려움 (도메인까지 Spring scan에 들어옴). - transactional event publication은 JPA event SPI에 의존하는 구현체가 다수라 persistence 선택에 영향. 카카오뱅크 수신상품 사례는 Modulith가 "느슨한 modular monolith"의 좋은 진화 경로임을 보여주지만 framework lock-in 비용을 수반. 출처: [[raw/company-tech-blogs/modulith-kakaobank-techblog-2025]], [[raw/company-tech-blogs/modulith-arawn-github-modular-monoliths-spring]]. - Spring Modulith 공식 문서는 module boundary 위반을 verification API로 잡지만 빌드 실패 강제 여부는 적용 프로젝트의 CI 설정에 의존. 출처: [[raw/official-docs/modulith-spring-official-doc]]. - **onion** - 의존성 방향 규칙은 Hexagonal과 동등 (안쪽으로만 의존). - 그러나 boundary verification 도구가 framework 자체로는 제공되지 않음. ArchUnit 같은 별도 정적 분석 없이는 layer 우회를 build-time에 잡기 어려움. Allegro 기술블로그도 onion의 이상은 인정하면서 실제 강제는 별도 도구가 필요하다고 명시. 출처: [[raw/company-tech-blogs/onion-allegro-tech-blog-2023]]. 5종 모두 "의존성은 안쪽으로만"이라는 동일한 핵심 원칙을 공유하며, 차이는 (a) 최상위 자름의 기준(feature vs layer) (b) framework가 boundary를 강제하는지 (c) inbound/outbound adapter 명시 여부에 있다. ### 경계를 *강제*하는 방법 (enforcement) 레이아웃을 고른 것만으로 경계가 지켜지지 않는다. 어느 레이아웃이든 boundary drift를 막으려면 별도의 강제 수단이 필요하며, 일반적으로 두 축으로 나뉜다. - **Build-graph 검사**: multi-module 빌드에서 module 간 허용 dependency를 화이트리스트로 두고, 허용 외 module dependency 선언 시 빌드를 실패시킨다(예: Gradle custom verification task). module 경계 자체가 1차 방어선이 된다. - **Source/bytecode import 검사**: ArchUnit 같은 정적 분석 도구로 package/class 레벨 import·call·annotation을 검사한다. "`..domain..`은 `org.springframework..`에 의존 금지", "특정 class(예: `ApplicationContext`) 의존 금지(banned-class)", "특정 annotation 사용 금지", "DTO는 web adapter 안에서만 접근" 같은 fitness function을 test로 강제한다. 정적 분석의 한계는 분명하다. import/call/annotation은 bytecode에 남지만, runtime container lookup(`ApplicationContext.getBean(String)` 같은 string-key 조회), `Class.forName(String)` reflection, classloader 우회는 bytecode가 *문자열 내용*을 노출하지 않으므로 catch할 수 없다. class-literal `getBean(Class)`까지는 method-call target으로 잡히지만 string-key 변종은 false-negative가 되며, 이 영역은 code review·runtime 검증(Actuator `/beans`, Modulith verifier 등)으로만 보완 가능하다. 또 ArchUnit의 `should()` 조건이 매칭 대상이 0개인 빈 module에서 vacuous하게 통과하는 empty-anchor 함정이 있어, `allowEmptyShould` 정책과 "위반을 데이터로 보는(violations-as-data)" negative fixture로 rule이 실제로 catch하는지 별도 보증하는 패턴이 쓰인다. ArchUnit 분석 scope(classpath import vs package filter)와 empty-should 함정의 일반 지식은 [[wiki/concepts/archunit-scope-classpath-vs-package-filter]] 참조. ## Claim-backed Knowledge > 인용 가능한 출처가 직접 뒷받침하는 일반 지식만 둔다. "어느 레이아웃이 옳다"는 추론·취향은 §한계 / 주의점과 §Do Not Overclaim에서 다룬다. | Knowledge Point | Supporting Claims | Confidence | Notes | |---|---|---|---| | 최상위 디렉터리는 framework가 아니라 use case / business 영역을 드러내야 한다(layer-first 비판) | [[raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011]] | medium | Uncle Bob Screaming Architecture (2011), `engineering-blog` — 공식 표준이 아닌 영향력 있는 블로그 주장 | | Hexagonal의 본질은 응용 코어를 inbound(driving)/outbound(driven) adapter로부터 port interface로 격리하는 것이며 package 형태 자체는 비강제 | [[raw/official-docs/hexagonal-cockburn-wikipedia-summary]] | medium | Cockburn Ports & Adapters, `engineering-blog` | | 의존성은 외부 layer(infra/UI)→내부 layer(domain model) 방향으로만 향하고 안쪽은 바깥쪽 interface를 알지 않는다 | [[raw/official-docs/onion-palermo-original-2008]] | medium | Palermo Onion (2008), `engineering-blog` | | Spring Modulith는 package를 module 경계로 삼고 `@ApplicationModule`/named-interface로 접근을 강제하나, 위반의 build 실패 강제 여부는 적용 프로젝트 CI 설정에 의존(framework는 verification API만 제공) | [[raw/official-docs/modulith-spring-official-doc]] | high | 공식 문서. build 실패는 자동이 아님 | | onion/hexagonal 의존성 방향 규칙은 framework 자체로 build-time 강제되지 않으며, ArchUnit 등 별도 정적 분석 없이는 layer 우회를 빌드 시점에 잡기 어렵다 | [[raw/company-tech-blogs/onion-allegro-tech-blog-2023]] | medium | `company-tech-blog` 관점 — 공식 best practice로 승격 금지 | | 도메인 수가 늘면 layer-first에서 한 도메인 코드가 controller/·service/·repository/에 흩어져 응집도가 떨어진다 | [[raw/company-tech-blogs/feature-first-sahibinden-package-by-layer-vs-feature]] | medium | `company-tech-blog` 사례 | ## Project Application - [[wiki/projects/ca-tmpl/clean-architecture-package-layout]] — ca-tmpl package/module blueprint + enforcement-rules 적용 기록. Gradle multi-module boundary(8 module, production root `dev.caskeleton`)와 ArchUnit/Gradle guardrail은 `locally-verified`(2026-06-04 ground-truth 대조). enforcement dimension: `domain_is_pure`(Lombok ban 포함), application↔adapter 격리, `ApplicationContext` banned-class rule(D11, string-key bypass는 한계), `verifyCleanArchitectureDependencies` build-graph 검사, violations-as-data negative fixture를 기록. `sample-portfolio` fixture business flow와 Spring Modulith verifier는 범위 밖. - [[raw/branch-notes/feature-architecture-enforcement-rules]] — 경계 의존성 규칙과 forbidden annotation/import의 ArchUnit 강제 기준. - [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] — Gradle multi-module Clean Architecture / Hexagonal module blueprint SSOT. - [[raw/branch-notes/feature-domain-feature-onboarding-contract]] — 새 도메인 추가 시 New Domain Module Slice + Read/Write Difference Table 기준. - [[raw/project-notes/ca-skeleton-operational-contract]] §20 Skeleton Blueprint Contract — 위 3개 branch-note를 통합한 canonical SSOT. ## 내가 설명할 수 있어야 하는 것 - feature-first / layer-first / hexagonal / onion / modulith 5종의 공식 정의와 공통 핵심 원칙("의존성은 안쪽으로만")은 무엇인가? - 각 레이아웃이 어떤 문제를 해결하고, 어떤 상황에서는 무너지는가(특히 layer-first의 응집도 붕괴 시점)? - 레이아웃 선택만으로 경계가 지켜지지 않는 이유와, build-graph 검사 / 정적 분석(ArchUnit) 두 축의 enforcement가 각각 무엇을 막는가? - 공식 문서가 말하지 않는 부분(예: Spring Modulith가 위반의 build 실패를 자동 강제하지 않음)은 무엇인가? - 회사 기술 블로그 사례(우아한형제들·카카오뱅크·Allegro 등)를 일반 법칙처럼 말하면 안 되는 지점은? - 내 프로젝트(ca-tmpl)에서는 어떤 branch decision과 ArchUnit/Gradle rule로 연결됐는가? - 정적 분석으로 잡히지 않는 우회(runtime lookup, reflection)는 코드/운영에서 어떻게 검증·보완하는가? ## Interview Questions - feature-first 패키지 레이아웃과 layer-first(controller/service/repository) 레이아웃의 차이는 무엇인가? 어느 시점에 후자가 무너지는가? - feature-first 레이아웃이 Hexagonal Architecture와 "동형"이라는 표현은 무슨 뜻인가? buckpal 예시로 설명하라. - 도메인 수가 늘어났을 때 layer-first가 응집도 면에서 무너지는 이유는 무엇인가? 어떤 운영 신호로 그것을 감지하는가? - Spring Modulith를 즉시 도입하지 않고 Gradle multi-module + ArchUnit/Gradle guardrail로 시작하는 트레이드오프는 무엇인가? 향후 Modulith로 이행할 수 있는 조건은? - 패키지 규약을 문서로만 두지 않고 ArchUnit 같은 architecture test로 boundary를 강제하는 이유는 무엇인가? 정적 분석으로 잡히지 않는 우회(runtime lookup 등)는 어떻게 보완하는가? ## Do Not Overclaim - "feature-first가 항상 layer-first보다 우월하다"는 금지. 학습 비용은 layer-first가 가장 낮고, 도메인 수가 적은 초기 단계에서는 layer-first도 합리적인 선택이다. - "ca-tmpl이 Hexagonal Architecture다"는 단정 금지. ca-tmpl은 Gradle module boundary로 application/domain과 adapter를 물리 분리한 Clean Architecture / Hexagonal-inspired template이다. 현재 구현 어휘는 inbound = `adapter-web`, outbound = `adapter-persistence` / `adapter-outbound`이며, Cockburn 원전의 모든 어휘를 그대로 차용한 구현은 아님. - "Spring Modulith를 곧 도입할 것"이라는 단정 금지. Modulith는 framework가 boundary를 강제하는 자연스러운 진화 경로이지만, 도입은 framework lock-in과 JPA 의존 비용을 수반하며 ca-tmpl의 framework-neutral 도메인 원칙과 일부 충돌한다. 향후 검토 대안 중 하나일 뿐 도입 결정이 아니다. - "ArchUnit이 모든 경계 위반을 잡아낸다"는 단정 금지. 정적 분석은 ApplicationContext lookup, `@Lazy` reflection, runtime classloader 우회를 감지할 수 없으며 별도 코드 리뷰/SonarQube 보완이 필요하다. ## Sources - [[raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011]] — Uncle Bob Screaming Architecture (2011) - [[raw/official-docs/hexagonal-cockburn-wikipedia-summary]] — Cockburn Hexagonal Architecture (Ports & Adapters) - [[raw/official-docs/hexagonal-thombergs-buckpal-github]] — Thombergs BuckPal reference (feature 단위로 잘린 Hexagonal) - [[raw/official-docs/layer-first-baeldung-clean-architecture-spring-boot]] — Baeldung Clean Architecture Spring Boot - [[raw/official-docs/onion-palermo-original-2008]] — Palermo Onion Architecture (2008) - [[raw/official-docs/modulith-spring-official-doc]] — Spring Modulith 공식 문서 - [[raw/company-tech-blogs/feature-first-sahibinden-package-by-layer-vs-feature]] — Sahibinden: feature vs layer 응집도 비교 - [[raw/company-tech-blogs/layer-first-kamilmazurek-github-template]] — layer-first Spring Boot template - [[raw/company-tech-blogs/hexagonal-woowahan-techblog-2023]] — 우아한형제들 Hexagonal 적용 사례 - [[raw/company-tech-blogs/modulith-kakaobank-techblog-2025]] — 카카오뱅크 수신상품 Modulith 적용 - [[raw/company-tech-blogs/modulith-arawn-github-modular-monoliths-spring]] — Modular Monoliths with Spring 참조 구현 - [[raw/company-tech-blogs/onion-allegro-tech-blog-2023]] — Allegro Onion Architecture 적용기 - [[raw/project-notes/ca-skeleton-operational-contract]] — canonical operational contract (§20 Skeleton Blueprint Contract, §29 Topic 1)