← all posts
DEV 2026.05.02 · 14 min read Intermediate

아키텍처 패턴, 어떻게 고르는가

Layered, Hexagonal, Clean Architecture의 의존성 방향·테스트 속도·복잡도 차이부터, 혼합 전략·ADR·MSA 연계까지 선택 기준을 추적한다.


아키텍처 패턴을 선택할 때 흔한 실수는 “어느 것이 더 좋은가”를 묻는 것이다. Layered는 구식이고 Hexagonal이 낫고 Clean Architecture가 가장 이론적으로 완벽하다는 서열 감각이 생기는 순간, 단순 CRUD 서비스에 파일 24개짜리 구조가 들어서거나 복잡한 금융 도메인이 1200줄짜리 OrderService 하나로 버텨내야 한다. 그렇다면 진짜 질문은 무엇인가?

세 패턴의 핵심 차이

세 패턴의 차이는 의존성이 어느 방향으로 흐르는가에서 시작한다.

기본 Layered에서 OrderServiceJpaRepositoryKakaoPayClient를 직접 의존한다. 의존성이 인프라 방향으로 흐른다. DB를 바꾸면 Service 코드가 영향을 받고, 단위 테스트를 짜려면 JPA 컨텍스트가 필요하다.

Hexagonal과 Clean은 이 방향을 뒤집는다. PlaceOrderServicePaymentPort(인터페이스)만 알고, KakaoPayAdapter가 그 인터페이스를 구현한다. 인프라가 도메인을 향해 의존한다. 결과적으로 Order.place() 안의 비즈니스 규칙은 Spring 없이 0.001초에 테스트할 수 있다.

Hexagonal과 Clean의 실질적 차이는 크지 않다. 비즈니스 로직의 위치(Domain Entity)와 테스트 방식은 동일하다. 차이는 Presenter 패턴의 유무, 용어(Port/Adapter vs Input/Output Boundary), 그로 인한 파일 수다. 주문 생성 기능 하나 기준으로 Hexagonal은 약 18개 파일, Clean은 약 24개다.

트레이드오프

단순성 vs 순수성의 맞교환이다. Layered는 빠른 시작을 주는 대신 인프라 의존을 수용한다. Hexagonal/Clean은 도메인 순수성과 테스트 속도를 주는 대신 초기 파일 수와 학습 비용을 요구한다. CI 실행 시간은 100개 테스트 기준 Layered 약 43분, Hexagonal/Clean 약 5분이다.

선택 기준 네 가지

“어느 것이 더 좋은가”가 아니라 “우리 상황에 무엇이 맞는가”를 결정하는 기준은 네 가지다.

도메인 복잡도. CRUD가 90% 이상이고 비즈니스 검증이 “빈 값 확인” 수준이라면 Layered+DIP로 충분하다. 주문 상태 전이가 5단계 이상이고 할인 정책·재고 확인 같은 규칙이 10개를 넘으면 Hexagonal을 시작할 시점이다. Entity당 비즈니스 메서드가 15개를 넘고 도메인 전문가 협업이 필요한 수준이라면 Hexagonal 또는 Clean이 필수다.

외부 시스템 교체 가능성. “이 외부 시스템이 3년 내 교체될 가능성이 30% 이상인가?” 결제 수단, SMS 발송 벤더, 검색 엔진은 교체된다. 이 경우 Port로 추상화하면 KakaoPayAdapter 하나를 교체하는 것으로 완결된다. 전사 표준 Kafka처럼 교체 가능성이 없는 시스템에 Port를 씌우는 것은 과잉이다.

테스트 자동화 목표. CI 5분 이내를 목표로 한다면 Hexagonal은 필수다. 단, 구조만 Hexagonal로 만들고 @SpringBootTest를 계속 쓰면 이익이 없다. InMemoryOrderRepository를 실제로 작성하고 단위 테스트 비율을 70% 이상으로 올려야 5분이 된다.

팀 역량. Hexagonal 경험이 없는 팀에 한 번에 도입하면 패키지 이름만 domain/port/out이고 JpaRepository를 직접 import하는 코드가 나온다. 이 경우 Layered+DIP로 시작해 점진적으로 전환하는 것이 더 현실적이다.

혼합 전략과 점진적 전환

현실의 대부분 프로젝트는 처음부터 완전한 Hexagonal이 아니다. 올바른 혼합은 명확한 규칙이 있는 혼합이다. 주문·결제처럼 복잡한 핵심 도메인은 Hexagonal, 공지·설정처럼 단순한 지원 기능은 Layered+DIP로 운영하고, ARCHITECTURE.md에 각 기능의 패턴을 명시한 뒤 ArchUnit으로 자동 강제한다.

기존 Layered 코드베이스에서 Hexagonal로 전환할 때는 Strangler Fig 방식이 유효하다. 한 번에 전체를 바꾸는 대신 Phase 단위로 진행한다. Phase 2(Repository 인터페이스 추출)는 하루 작업으로 InMemory 단위 테스트를 가능하게 만든다. Phase 3(외부 API Port 추출)은 결제 수단 교체를 Adapter 파일 교체로 국한한다. 각 Phase는 독립적으로 배포 가능하며, 완료 후 즉시 가치가 생긴다.

ADR로 결정을 남기는 이유

“왜 Repository 인터페이스가 domain 패키지에 있나요?” — 이 질문에 답할 수 없다면 6개월 후 신규 팀원이 infrastructure에 같은 인터페이스를 새로 만들고, 두 스타일이 혼재한다.

ADR(Architecture Decision Record)은 결정 자체가 아니라 왜 그 결정을 내렸는가를 기록한다. 맥락(어떤 문제였는가), 결정(무엇을 선택했는가), 검토한 대안, 수용한 트레이드오프. 이 네 섹션이 있으면 신규 팀원은 문서를 보고 이해하고, ArchUnit 규칙이 위반됐을 때 “ADR-002를 보세요”로 대화를 시작할 수 있다.

ADR은 코드 저장소의 docs/adr/ 디렉토리에 PR 형태로 팀 리뷰를 거쳐 병합한다. 한 번 승인된 ADR은 수정하지 않고, 상황이 바뀌면 새 ADR로 대체한다. 결정의 이력이 보존된다.

MSA와 내부 아키텍처의 관계

MSA를 도입해도 각 서비스 내부가 Fat Service 구조라면 분산 Fat Service가 생긴다. 서비스를 나눈다고 내부 아키텍처 문제가 자동으로 해결되지 않는다.

반대로 내부를 Hexagonal로 구성했다면 MSA 전환 비용이 낮아진다. PlaceOrderServicePaymentPort만 알고 있다면, 결제 서비스 분리 시 KakaoPayAdapterPaymentServiceAdapter(HTTP 클라이언트)로 교체하는 것으로 완결된다. PlaceOrderService 코드는 변경이 없다. 동기(REST)에서 비동기(Kafka)로 전환할 때도 Adapter만 교체하면 된다.

현실적인 전환 경로는 Modular Monolith에서 시작하는 것이다. 각 Bounded Context를 독립 패키지로 분리하고 내부에 Hexagonal을 적용한 뒤, 모듈 간 통신은 Port 인터페이스나 이벤트로만 허용한다(ArchUnit으로 강제). 트래픽이 집중되는 모듈이 생기면 그때 독립 서비스로 분리한다. 대부분의 성공적인 MSA는 이 경로를 따라 진화했다.

정리

  • 패턴 선택의 기준은 도메인 복잡도, 외부 시스템 교체 가능성, 테스트 자동화 목표, 팀 역량이다. 유행이나 이론적 완결성이 아니다.
  • Hexagonal과 Clean의 실질 차이는 작다. 핵심은 두 패턴이 공유하는 원칙 — 의존성 역전과 도메인 보호다.
  • 혼합 아키텍처는 문서화와 ArchUnit 자동 강제가 없으면 최악의 혼재 상태가 된다.
  • ADR은 아키텍처 결정의 이유를 팀 기억이 아닌 코드 저장소에 남긴다.
  • 내부 아키텍처가 MSA 전환 비용을 결정한다. Hexagonal 구조가 있으면 모듈을 서비스로 이동하는 것이 자연스러워진다.