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

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

Architecture > 이 카테고리의 다른 포스팅

JUNA BLOG VISITORS

Today0
Total0