Files
llm-wiki/wiki/concepts/clean-architecture-package-layout.md
T

16 KiB

title, source_type, status, confidence, tags, related_projects, last_reviewed
title source_type status confidence tags related_projects last_reviewed
Clean Architecture 패키지 레이아웃 (feature-first vs layer-first vs hexagonal vs modulith vs onion) llm-generated draft medium
clean-architecture
package-layout
hexagonal
modulith
ca-skeleton
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

  • hexagonal pure (feature 슬라이스 없음)

    • 최상위가 application/domain/adapter로만 잘리고 feature 슬라이스가 없으면 도메인이 늘어날수록 applicationdomain 패키지가 비대해짐.
    • inbound/outbound 분리는 명확하지만 도메인 간 boundary가 약함. 우아한형제들 기술블로그의 Hexagonal 적용도 결국 도메인별 module로 분리하는 방향으로 진화. 출처: raw/company-tech-blogs/hexagonal-woowahan-techblog-2023.
  • Spring Modulith

  • 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<T>)까지는 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

내가 설명할 수 있어야 하는 것

  • 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