카툰으로 이해하는 소프트웨어 아키텍처 4가지
같은 주문 처리 시스템을 Layered·Hexagonal·Onion·Clean Architecture로 나란히 배치해 경계, 의존성 방향, 공통점과 선택 기준을 쉽게 비교합니다.
소프트웨어 아키텍처 이름은 많지만 질문은 의외로 하나로 모인다. 업무 규칙을 화면, 데이터베이스, 프레임워크의 변화에서 어떻게 보호할 것인가? Layered, Hexagonal, Onion, Clean Architecture를 같은 주문 처리 시스템에 적용해 보면 이름보다 의존성 방향과 경계의 차이가 먼저 보인다.
먼저 볼 기준
네 아키텍처를 비교할 때 폴더 모양만 보면 거의 반드시 헷갈린다. 다음 네 질문부터 던지는 편이 낫다.
- 주문 할인, 결제 가능 여부 같은 업무 규칙은 어디에 있는가?
- 그 업무 규칙이 HTTP, ORM, 데이터베이스 SDK를 직접 import하는가?
- 외부 기술을 교체할 때 어느 코드까지 바뀌는가?
- 데이터베이스 없이도 핵심 규칙을 빠르게 테스트할 수 있는가?
이 글에서는 고객이 주문하고, 할인 규칙을 계산하고, 결제를 요청하고, 주문을 저장하는 작은 배달 서비스를 공통 예시로 쓴다. 등장인물도 같다. 고객은 입력, 요리사는 업무 규칙, 배달원은 외부 서비스, 기록 담당자는 데이터 저장을 뜻한다.

Layered
“위층의 요청을 아래층으로 차례대로 전달하자.” 가장 익숙한 형태는 Presentation → Application 또는 Service → Data Access 순서다. 화면이나 API 컨트롤러가 서비스를 호출하고, 서비스가 저장소를 호출한다.
Presentation → Application → Data Access → Database
주문 API 주문 서비스 주문 저장소장점은 진입 장벽이 낮고 요청 흐름이 직선이라 찾기 쉽다는 점이다. 작은 CRUD 서비스나 변경 폭이 좁은 관리 시스템에서는 이 단순함이 큰 이점이다. 문제는 호출 흐름과 소스 코드 의존성까지 모두 아래로 향하게 만들기 쉽다는 데 있다. 주문 서비스가 ORM 모델과 SQL 예외를 직접 알기 시작하면 데이터베이스 변경이 업무 규칙까지 번진다.
Layered 자체가 나쁜 것은 아니다. 각 층의 책임을 지키고 인터페이스로 의존성을 뒤집으면 충분히 건강하게 운영할 수 있다. 다만 “폴더를 controller/service/repository로 나눴다”는 사실만으로 업무 규칙이 보호되지는 않는다.

Hexagonal
“애플리케이션의 문은 포트이고, 기술별 통역사는 어댑터다.” Hexagonal Architecture는 Ports and Adapters라고도 부른다. 고객의 HTTP 요청, CLI, 테스트 코드는 애플리케이션을 움직이는 어댑터이고, 데이터베이스·결제 API·메시지 큐는 애플리케이션이 사용하는 바깥 장치다.
HTTP Adapter ─┐ ┌─ PostgreSQL Adapter
CLI Adapter ─┼→ Inbound Port → App → Outbound Port ┼─ Memory Adapter
Test Adapter ─┘ └─ Payment Adapter핵심은 육각형 모양이 아니다. 애플리케이션은 PlaceOrder 같은 입력 포트와 SaveOrder, ChargePayment 같은 출력 포트를 알 뿐, Express나 PostgreSQL이라는 구체 기술을 모른다. 테스트에서는 같은 포트에 메모리 어댑터를 꽂을 수 있다.
Alistair Cockburn의 원문이 강조한 목적도 UI나 데이터베이스 없이 애플리케이션을 실행하고 테스트할 수 있게 하는 것이다. 외부 장치가 다양하거나 기술 교체 가능성이 큰 통합 시스템에 특히 잘 맞는다.

Onion
“업무 규칙을 양파의 심처럼 가장 안쪽에 두자.” Onion Architecture는 도메인 모델을 중심에 놓고, 바깥 고리로 갈수록 애플리케이션 서비스와 인프라 세부사항을 둔다. 바깥쪽은 안쪽을 알 수 있지만 안쪽은 바깥쪽을 알지 않는다.
Infrastructure → Application Services → Domain Services → Domain Model주문 금액, 할인 정책, 주문 상태 전이처럼 사업의 언어로 설명되는 규칙이 중심이다. 저장소 인터페이스도 중심에 가까운 쪽이 소유하고, 바깥의 데이터베이스 코드가 그 계약을 구현한다. 따라서 도메인은 ORM 엔티티나 웹 프레임워크 타입에 끌려가지 않는다.
Onion은 도메인 규칙이 복잡하고 오래 유지되는 시스템에서 강점을 보인다. 반대로 단순 CRUD에 무거운 도메인 모델과 여러 프로젝트를 얹으면 경계보다 보일러플레이트가 더 커질 수 있다.

Clean
“고수준 정책은 저수준 도구를 몰라야 한다.” Clean Architecture의 핵심은 동심원 그림보다 Dependency Rule이다. 소스 코드 의존성은 바깥의 세부사항에서 안쪽의 정책을 향해야 하며, 안쪽 코드는 바깥 이름을 알면 안 된다.
Frameworks & Drivers → Interface Adapters → Use Cases → Entities주문을 접수하는 유스케이스는 웹 요청 객체를 직접 받지 않고 입력 모델을 받는다. 결과 역시 데이터베이스 행이 아니라 출력 경계를 통해 전달한다. 제어 흐름은 안쪽에서 바깥쪽의 구현을 호출할 수 있지만, 인터페이스를 안쪽에 두어 소스 코드 의존성은 안쪽을 향하게 만든다.
Clean은 Onion과 매우 비슷하다. 다만 도메인 중심이라는 표현뿐 아니라 유스케이스, 입력·출력 경계, 인터페이스 어댑터의 역할을 더 명시적으로 설명하는 경우가 많다. 프레임워크 독립성과 테스트 경계를 팀 규칙으로 강하게 만들고 싶을 때 유용하다.


닮은 점
Hexagonal, Onion, Clean은 서로 다른 이름을 쓰지만 가족처럼 닮았다.
- 업무 규칙을 UI·데이터베이스·프레임워크보다 중요하게 둔다.
- 외부 기술을 세부사항으로 취급한다.
- 인터페이스와 의존성 역전을 이용해 핵심을 보호한다.
- 실제 데이터베이스 없이 핵심 규칙을 테스트하기 쉽게 만든다.
- 그림의 모양보다 경계를 넘는 의존성의 방향이 중요하다.
Layered도 이 원칙을 적용할 수 있다. 차이는 Layered가 주로 책임과 호출 단계를 설명하는 데서 시작하고, 나머지 셋은 핵심을 외부 기술로부터 격리하는 경계와 의존성 방향을 더 전면에 둔다는 점이다.
한눈에 비교
| 구조 | 중심 생각 | 경계 표현 | 잘 맞는 경우 | 주의점 |
|---|---|---|---|---|
| Layered | 책임을 층으로 분리 | 상하 레이어 | 단순 CRUD, 작은 팀 | 업무 규칙이 ORM에 새기 쉬움 |
| Hexagonal | 외부와 포트로 통신 | 포트와 어댑터 | 입출력 채널이 다양한 시스템 | 포트를 지나치게 잘게 쪼갤 수 있음 |
| Onion | 도메인을 중심에 보호 | 안팎의 고리 | 복잡하고 오래가는 도메인 | 단순 앱에는 계층이 과할 수 있음 |
| Clean | 정책을 세부사항에서 분리 | 엔티티·유스케이스·어댑터 | 유스케이스와 테스트 경계가 중요한 시스템 | 규칙을 기계적으로 적용하면 보일러플레이트 증가 |
코드로 보는 차이
Layered 구현에서 서비스가 구체 저장소를 직접 만들면 기술이 업무 흐름에 붙는다.
class OrderService {
private orders = new PostgresOrderRepository();
async place(command: PlaceOrderCommand) {
const order = Order.create(command.items);
await this.orders.save(order);
}
}Hexagonal·Onion·Clean 계열에서는 핵심 쪽이 계약을 소유하고, 바깥에서 구현을 주입한다.
interface OrderRepository {
save(order: Order): Promise<void>;
}
class PlaceOrder {
constructor(private readonly orders: OrderRepository) {}
async execute(command: PlaceOrderCommand) {
const order = Order.create(command.items);
await this.orders.save(order);
return { orderId: order.id };
}
}
const useCase = new PlaceOrder(new PostgresOrderRepository(pool));제어 흐름은 유스케이스에서 PostgreSQL 어댑터로 나가지만, 컴파일 시점 의존성은 어댑터가 핵심의 OrderRepository 계약을 향한다. 이 한 줄의 방향 전환이 세 아키텍처의 공통 뼈대다.
선택하는 법
다음 질문에 “예”가 많을수록 Hexagonal·Onion·Clean 계열의 경계가 주는 이익이 커진다.
- 업무 규칙이 단순 CRUD보다 복잡한가?
- 같은 기능을 HTTP, 배치, 메시지 소비자가 함께 호출하는가?
- 데이터베이스나 외부 API를 바꿀 가능성이 있는가?
- 핵심 규칙을 빠른 단위 테스트로 보호해야 하는가?
- 여러 팀이 경계를 나누어 오래 개발하는가?
반대로 수명이 짧고 규칙이 단순한 내부 도구라면 명확한 Layered 구조가 더 경제적일 수 있다. “미래에 필요할지도 모른다”는 이유로 모든 엔티티에 인터페이스와 매퍼를 추가하면 변경 비용을 줄이려다 현재 비용만 키운다.
흔한 오해
- 폴더 이름이 아키텍처다:
domain,usecase,adapter폴더가 있어도 안쪽에서 ORM을 import하면 경계는 무너진다. - 레이어 수가 많을수록 좋다: 계층은 보호할 변화가 있을 때만 가치가 있다.
- 데이터베이스는 중요하지 않다: 중요하지만 업무 정책의 주인은 아니다. 격리는 무시가 아니라 교체 가능한 계약을 뜻한다.
- 모든 모델을 복사해야 한다: 경계를 넘을 때 의미와 변화 주기가 다를 때만 변환한다.
- 네 구조는 서로 배타적이다: Layered한 화면과 Hexagonal한 애플리케이션, Onion식 도메인, Clean의 의존성 규칙을 함께 적용할 수 있다.
실무 시작 순서
- 바꾸기 어려운 업무 규칙과 자주 바뀌는 기술 세부사항을 구분한다.
- 한 유스케이스를 고르고 입력·출력 경계를 문장으로 적는다.
- 핵심 코드에서 프레임워크·ORM import를 제거한다.
- 저장소와 외부 API를 포트 또는 인터페이스 뒤로 옮긴다.
- 메모리 구현으로 유스케이스 테스트를 먼저 통과시킨다.
- 실제 어댑터를 연결하고 통합 테스트로 계약을 확인한다.
- 경계가 이익보다 복잡성을 더하는 곳은 과감히 합친다.
좋은 아키텍처는 그림을 닮은 코드가 아니라 변경이 번지는 범위를 의도적으로 제한하는 코드다. 이름을 먼저 고르기보다 어떤 정책을 어떤 세부사항에서 지킬지 정하면 네 가지 구조의 차이와 공통점이 훨씬 선명해진다.