Clean Architecture의 Vertical Slice와 Horizontal Slice

발행일: Jul, 2026
조회수: 0
단어수: 792

목차

Clean Architecture를 처음 접하면 대부분 계층별 폴더부터 만들게 됩니다. 처음에는 꽤 합리적으로 보이지만, 프로젝트가 커지면 이상한 일이 생깁니다. 기능 하나를 고치기 위해 여러 폴더를 돌아다녀야 합니다. 주문(Order) 기능을 예로 들어 Horizontal Slice와 Vertical Slice의 차이를 정리해 보겠습니다.

계층 중심의 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 기능은 여러 계층에 걸쳐 존재합니다. 가로로 쌓인 계층을 세로로 관통하면서 하나의 기능이 완성되는 셈입니다.

Order 기능 하나 Presentation OrderController.cs Application OrderService.cs Domain Order.cs · IOrderRepository.cs Infrastructure OrderRepository.cs 폴더는 계층으로 나뉘어 있지만, 기능은 계층을 가로지릅니다
그림 1. Horizontal Slice — 코드는 계층으로 모여 있고, 기능 하나가 그 계층들을 관통합니다.

이 방식의 가장 큰 장점은 구조가 명확하다는 것입니다. 프로젝트에 처음 들어온 개발자도 "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
Create Order Endpoint Command Handler Repository Cancel Order Endpoint Command Handler Pay Order Endpoint Command Handler 기능 하나에 필요한 코드가 한 폴더에 모여 있습니다
그림 2. Vertical Slice — 자르는 방향이 바뀌면 "함께 바뀌는 것"이 함께 놓입니다.

이제 CreateOrder를 수정하려면 그 폴더 하나만 열면 됩니다. 기능을 통째로 지우는 것도 폴더 하나를 지우는 일이 됩니다.

둘의 차이

차이는 한 문장으로 정리됩니다.

Horizontal Slice는 "무슨 종류의 코드인가"를 기준으로 나눕니다.
Vertical Slice는 "무슨 기능을 하는 코드인가"를 기준으로 나눕니다.

Horizontal Slice Vertical Slice
나누는 기준 코드의 종류 코드의 기능
한 기능의 코드 여러 계층에 흩어짐 한 폴더에 모임
찾기 쉬운 것 "Service가 어디 있지" "주문 생성이 어디 있지"
중복 적음 늘어남 (감수하는 쪽)
잘 맞는 규모 작고 단순한 앱 기능이 많고 팀이 큰 앱

Clean Architecture와 반대일까

처음 접할 때 가장 많이 헷갈리는 지점입니다. 그렇지 않습니다.

Clean Architecture의 핵심은 폴더 구조가 아니라 의존성 방향입니다. 안쪽(도메인)이 바깥쪽(DB·UI·프레임워크)을 몰라야 한다는 규칙 하나가 본체이고, 계층별 폴더는 그 규칙을 지키기 위해 흔히 쓰이는 한 가지 방법일 뿐입니다.

폴더를 기능 중심으로 구성하면서도 의존성 규칙은 그대로 유지할 수 있습니다. 슬라이스 안에서 화살표는 여전히 안쪽을 향합니다.

Features / Orders / CreateOrder Infrastructure Repository Application Handler Domain Order 폴더를 기능으로 잘라도 화살표 방향은 바뀌지 않습니다
그림 3. 슬라이스 안에서도 의존성은 안쪽을 향합니다. 바뀐 것은 폴더를 자르는 방향뿐입니다.

정리하면 이렇습니다. 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를 바라보는 가장 중요한 관점이라고 생각합니다.

Table of Contents

When you first meet Clean Architecture, the folders you create are almost always layers. It looks reasonable enough at the start, but something odd shows up as the project grows: changing one feature means walking through several folders. Using an Order feature as the example, here is how horizontal and vertical slices differ.

A poster comparing a layer-oriented horizontal slice structure with a feature-oriented vertical slice structure side by side

What a horizontal slice is

The structure you see most often in Clean Architecture material is code divided by layer. Controllers live with controllers, services with services, repositories with repositories.

src/
├── API
│   └── Controllers
│       └── OrderController.cs
│
├── Application
│   ├── Services
│   │   └── OrderService.cs
│   └── DTOs
│       └── OrderDto.cs
│
├── Domain
│   ├── Entities
│   │   └── Order.cs
│   └── Interfaces
│       └── IOrderRepository.cs
│
└── Infrastructure
    └── Repositories
        └── OrderRepository.cs

In this structure a single Order feature exists across several layers. The layers are stacked horizontally, and one feature is completed by cutting down through all of them.

One feature Presentation OrderController.cs Application OrderService.cs Domain Order.cs · IOrderRepository.cs Infrastructure OrderRepository.cs The folders are split by layer, but the feature runs across them
Figure 1. Horizontal slice — the code is gathered by layer, and one feature cuts through all of them.

The biggest advantage of this approach is that the structure is obvious. A developer joining the project works out "controllers here, business logic there, repositories over there" almost immediately. The separation of concerns that Clean Architecture emphasises is easy to see.

What happens as the project grows

The code that started out as a single OrderService.cs keeps growing.

OrderService.cs
  CreateOrder()   CancelOrder()   GetOrder()
  GetOrders()     UpdateOrder()   ChangeAddress()
  PayOrder()      RefundOrder()   ...

OrderController grows at the same rate. Eventually a single Order domain accumulates features that have very little to do with one another.

Now suppose you have to change just the create-order feature. The files you need to open look roughly like this.

OrderController.cs  →  OrderService.cs  →  Order.cs
  →  IOrderRepository.cs  →  OrderRepository.cs
  →  CreateOrderValidator.cs  →  CreateOrderDto.cs

Seven places in the project for one change. And each of those files also contains code for other features that have nothing to do with it. Layers tell you what is allowed to depend on what; they do not tell you what changes together.

What a vertical slice is

A vertical slice cuts the code by feature rather than by layer. You look at the order domain like this.

Order
  ├── Create Order
  ├── Cancel Order
  ├── Get Order
  ├── Pay Order
  └── Refund Order

Then you gather everything a feature needs into one vertical column.

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
Create Order Endpoint Command Handler Repository Cancel Order Endpoint Command Handler Pay Order Endpoint Command Handler Everything one feature needs sits in a single folder
Figure 2. Vertical slice — change the direction of the cut and the things that change together end up together.

Changing CreateOrder now means opening one folder. Deleting the feature outright means deleting one folder.

The difference between the two

It comes down to a single sentence.

A horizontal slice divides by "what kind of code is this".
A vertical slice divides by "what does this code do".

Horizontal slice Vertical slice
Divided by Kind of code What the code does
One feature's code Scattered across layers Gathered in one folder
Easy to find "Where are the services?" "Where is create order?"
Duplication Low Higher (accepted on purpose)
Fits Small, simple apps Many features, larger teams

Is this the opposite of Clean Architecture?

This is where most people get confused at first. It is not.

The heart of Clean Architecture is not the folder layout but the direction of dependencies. The rule itself is that the inside (the domain) must not know about the outside (the database, the UI, the framework); layered folders are just one common way of keeping that rule.

You can organise folders around features and still keep the dependency rule intact. Inside a slice, the arrows still point inward.

Features / Orders / CreateOrder Infrastructure Repository Application Handler Domain Order Cutting folders by feature does not change which way the arrows point
Figure 3. Dependencies still point inward inside a slice. The only thing that changed is the direction of the cut.

To put it plainly: a vertical slice is a way of organising code, and Clean Architecture is a principle for managing the dependencies between it. They are not competing ideas.

The real benefit of vertical slices

The benefit is not that the folder tree looks nicer. It is that the blast radius of a change becomes obvious.

Suppose this requirement arrives.

"We need to be able to apply a coupon when an order is created."

In a horizontal structure you start by investigating how far the change reaches: OrderController, OrderService, OrderDto, Order, IOrderRepository, OrderRepository, CouponService… And because those files also hold cancellation and payment code, you have to check whether your change affects those too.

With a vertical slice you open Features/Orders/CreateOrder and begin. The unit of change in the requirement and the unit of change in the code move closer together. That is why vertical slices are attractive in applications with a lot of features.

Does everything belong inside a slice?

No. A domain entity such as Order is used by many features, and code like that is better placed outside the slices.

src/
├── Features
│   └── Orders
│       ├── CreateOrder
│       ├── CancelOrder
│       └── PayOrder
│
└── Domain
    └── Orders
        └── Order.cs

Adopting vertical slices increases duplication. That is not a bug; it is the price this approach has decided to pay. But if you do not agree up front on how much duplication is allowed, each slice grows slightly different rules and eventually every feature is reasoned about differently. Keeping the business rules themselves shared, and letting each slice own only the way it wires them together, is usually a safe line.

What matters more than where a given file goes is how you manage the unit of change and the coupling.

Layer or feature, in the end

Study Clean Architecture and you naturally start picturing Controller → Service → Repository → Database, and trying to fit every feature into that frame.

But the requirements that actually arrive look like this.

  • "Please change how orders are created."
  • "Please add a policy to order cancellation."
  • "Please change the payment feature."

Business change happens per feature, not per layer. That is exactly where vertical slicing starts from. So rather than reading it as "a way of cutting folders vertically", it is more accurate to read it as "a way of treating a business feature as a single unit of change".

Closing

Neither one is always the right answer. On a small project a traditional layered structure is far simpler and easier to follow.

But if you start seeing these signs, vertical slices are worth considering.

  • Changing one feature means moving between several layers
  • Services and controllers keep getting bigger
  • Unrelated features are mixed into one class

In the end the question narrows to one thing. Do we manage our code by technical kind, or by business feature?

Keeping the principles of Clean Architecture while shaping the code around the way the business changes — that, I think, is the most important way to look at vertical slices.

태그#Clean Architecture#Vertical Slice#Horizontal Slice#아키텍처#계층형 아키텍처#Feature Folder#CQRS#의존성 방향#C##.NET

JUNA BLOG VISITORS

오늘0
Total0