Clean Architecture의 Vertical Slice와 Horizontal Slice
목차
- Horizontal Slice란
- 프로젝트가 커지면 생기는 일
- Vertical Slice란
- 둘의 차이
- Clean Architecture와 반대일까
- Vertical Slice의 진짜 장점
- 모든 것을 Slice 안에 넣어야 할까
- 결국은 Layer냐 Feature냐
- 마무리
Clean Architecture를 처음 접하면 대부분 계층별 폴더부터 만들게 됩니다. 처음에는 꽤 합리적으로 보이지만, 프로젝트가 커지면 이상한 일이 생깁니다. 기능 하나를 고치기 위해 여러 폴더를 돌아다녀야 합니다. 주문(Order) 기능을 예로 들어 Horizontal Slice와 Vertical Slice의 차이를 정리해 보겠습니다.
Horizontal Slice란
Clean Architecture를 설명하는 자료에서 가장 흔히 보는 구조는 계층(Layer)을 기준으로 코드를 나누는 방식입니다. Controller는 Controller끼리, Service는 Service끼리, Repository는 Repository끼리 모읍니다.
src/
├── API
│ └── Controllers
│ └── OrderController.cs
│
├── Application
│ ├── Services
│ │ └── OrderService.cs
│ └── DTOs
│ └── OrderDto.cs
│
├── Domain
│ ├── Entities
│ │ └── Order.cs
│ └── Interfaces
│ └── IOrderRepository.cs
│
└── Infrastructure
└── Repositories
└── OrderRepository.cs
이 구조에서 하나의 Order 기능은 여러 계층에 걸쳐 존재합니다. 가로로 쌓인 계층을 세로로 관통하면서
하나의 기능이 완성되는 셈입니다.
이 방식의 가장 큰 장점은 구조가 명확하다는 것입니다. 프로젝트에 처음 들어온 개발자도 "Controller는 여기, 비즈니스 로직은 여기, Repository는 여기"를 금방 파악합니다. Clean Architecture가 강조하는 관심사의 분리도 눈에 잘 보입니다.
프로젝트가 커지면 생기는 일
처음에는 OrderService.cs 하나로 충분했던 코드가 점점 커집니다.
OrderService.cs
CreateOrder() CancelOrder() GetOrder()
GetOrders() UpdateOrder() ChangeAddress()
PayOrder() RefundOrder() ...
OrderController도 같은 속도로 커집니다. 결국 Order라는 하나의 도메인 아래에
성격이 다른 기능들이 계속 섞여 들어갑니다.
이 상태에서 주문 생성 기능 하나를 수정한다고 해봅시다. 열어야 하는 파일은 대략 이렇습니다.
OrderController.cs → OrderService.cs → Order.cs
→ IOrderRepository.cs → OrderRepository.cs
→ CreateOrderValidator.cs → CreateOrderDto.cs
기능 하나를 바꾸려고 프로젝트의 일곱 군데를 이동합니다. 게다가 각 파일에는 이번 변경과 아무 상관 없는 다른 기능들의 코드가 함께 들어 있습니다. 계층은 "무엇에 의존해도 되는가"는 알려주지만, "무엇을 함께 바꾸게 되는가"는 알려주지 않습니다.
Vertical Slice란
Vertical Slice는 계층이 아니라 기능(Feature)을 기준으로 코드를 자르는 방식입니다. 주문 도메인을 이렇게 바라봅니다.
Order
├── Create Order
├── Cancel Order
├── Get Order
├── Pay Order
└── Refund Order
그리고 각 기능에 필요한 코드를 세로로 잘라 하나의 덩어리로 묶습니다.
src/
└── Features
└── Orders
├── CreateOrder
│ ├── CreateOrderEndpoint.cs
│ ├── CreateOrderCommand.cs
│ ├── CreateOrderHandler.cs
│ ├── CreateOrderValidator.cs
│ └── CreateOrderRepository.cs
│
├── CancelOrder
│ ├── CancelOrderEndpoint.cs
│ ├── CancelOrderCommand.cs
│ └── CancelOrderHandler.cs
│
└── PayOrder
├── PayOrderEndpoint.cs
├── PayOrderCommand.cs
└── PayOrderHandler.cs
이제 CreateOrder를 수정하려면 그 폴더 하나만 열면 됩니다. 기능을 통째로 지우는 것도 폴더 하나를
지우는 일이 됩니다.
둘의 차이
차이는 한 문장으로 정리됩니다.
Horizontal Slice는 "무슨 종류의 코드인가"를 기준으로 나눕니다.
Vertical Slice는 "무슨 기능을 하는 코드인가"를 기준으로 나눕니다.
| Horizontal Slice | Vertical Slice | |
|---|---|---|
| 나누는 기준 | 코드의 종류 | 코드의 기능 |
| 한 기능의 코드 | 여러 계층에 흩어짐 | 한 폴더에 모임 |
| 찾기 쉬운 것 | "Service가 어디 있지" | "주문 생성이 어디 있지" |
| 중복 | 적음 | 늘어남 (감수하는 쪽) |
| 잘 맞는 규모 | 작고 단순한 앱 | 기능이 많고 팀이 큰 앱 |
Clean Architecture와 반대일까
처음 접할 때 가장 많이 헷갈리는 지점입니다. 그렇지 않습니다.
Clean Architecture의 핵심은 폴더 구조가 아니라 의존성 방향입니다. 안쪽(도메인)이 바깥쪽(DB·UI·프레임워크)을 몰라야 한다는 규칙 하나가 본체이고, 계층별 폴더는 그 규칙을 지키기 위해 흔히 쓰이는 한 가지 방법일 뿐입니다.
폴더를 기능 중심으로 구성하면서도 의존성 규칙은 그대로 유지할 수 있습니다. 슬라이스 안에서 화살표는 여전히 안쪽을 향합니다.
정리하면 이렇습니다. Vertical Slice는 코드를 어떻게 조직할 것인가의 방법이고, Clean Architecture는 코드 사이의 의존성을 어떻게 관리할 것인가의 원칙입니다. 둘은 경쟁하는 개념이 아닙니다.
Vertical Slice의 진짜 장점
폴더가 예뻐지는 것이 장점이 아닙니다. 가장 큰 장점은 변경의 범위가 명확해진다는 것입니다.
이런 요구사항이 들어왔다고 해봅시다.
"주문 생성 시 쿠폰을 적용할 수 있게 해주세요."
Horizontal 구조에서는 OrderController, OrderService,
OrderDto, Order, IOrderRepository,
OrderRepository, CouponService… 어디까지 건드려야 하는지부터 조사해야 합니다.
그리고 그 파일들에는 주문 취소나 결제 코드가 함께 들어 있어서, 내 변경이 그쪽에 영향을 주는지도 같이 확인해야 합니다.
Vertical Slice에서는 Features/Orders/CreateOrder를 열고 시작합니다.
요구사항의 변경 단위와 코드의 변경 단위가 가까워집니다. 이것이 기능이 많은 애플리케이션에서 Vertical
Slice가 매력적인 이유입니다.
모든 것을 Slice 안에 넣어야 할까
아닙니다. Order 같은 도메인 엔티티는 여러 기능이 함께 씁니다. 이런 코드는 슬라이스 밖에 두는 편이
맞습니다.
src/
├── Features
│ └── Orders
│ ├── CreateOrder
│ ├── CancelOrder
│ └── PayOrder
│
└── Domain
└── Orders
└── Order.cs
Vertical Slice를 도입하면 중복이 늘어납니다. 그건 버그가 아니라 이 방식이 지불하기로 한 비용입니다. 다만 어디까지 중복을 허용할지 미리 정해두지 않으면, 슬라이스마다 조금씩 다른 규칙이 자라서 나중에는 기능마다 사고방식이 달라집니다. 업무 규칙 자체는 공용으로 두고, 그 규칙을 엮는 방식만 슬라이스에 맡기는 선이 보통 무난합니다.
어떤 코드를 어디에 놓을지보다 중요한 것은 변경의 단위와 결합도를 어떻게 관리할 것인가입니다.
결국은 Layer냐 Feature냐
Clean Architecture를 공부하면 자연스럽게 Controller → Service → Repository → Database
라는 그림을 떠올리게 되고, 모든 기능을 그 틀에 맞춰 넣으려 하게 됩니다.
하지만 실제로 들어오는 요구사항은 이런 모양입니다.
- "주문 생성 기능을 변경해주세요."
- "주문 취소에 정책을 추가해주세요."
- "결제 기능을 변경해주세요."
비즈니스의 변화는 Layer 단위가 아니라 Feature 단위로 발생합니다. Vertical Slice는 바로 이 지점에서 출발합니다. 그래서 이걸 "폴더를 세로로 나누는 방법"이라고 이해하기보다, "비즈니스 기능을 하나의 변경 단위로 바라보는 방법"이라고 이해하는 편이 정확합니다.
마무리
둘 중 하나가 무조건 정답인 것은 아닙니다. 작은 프로젝트에서는 전통적인 Layered 구조가 훨씬 단순하고 이해하기 쉽습니다.
다만 이런 신호가 보이기 시작한다면 Vertical Slice를 고려해볼 만합니다.
- 기능 하나를 바꾸는 데 여러 계층을 오가야 한다
- Service와 Controller가 계속 비대해진다
- 서로 관련 없는 기능들이 한 클래스에 섞여 있다
결국 질문은 하나로 좁혀집니다. 우리 코드를 기술적인 종류별로 관리할 것인가, 비즈니스 기능별로 관리할 것인가.
Clean Architecture의 원칙은 유지하면서 코드의 구조를 비즈니스의 변화 방식에 맞추는 것 — 그것이 Vertical Slice를 바라보는 가장 중요한 관점이라고 생각합니다.