코드베이스를 어떤 기준으로 나눌 것인가. 기술적 책임(Controller, Service, Repository)으로 나누면 수평 분할이다. 기능이나 도메인(User, Order, Payment)으로 나누면 수직 분할이다. 어떤 기준으로 나누느냐에 따라 변경의 범위, 팀 간 의존성, 배포 단위가 달라진다.
Horizontal Slicing
수평 분할은 기술적 책임을 기준으로 코드를 계층별로 나눈다. Layered Architecture가 대표적이다.
src/
controllers/
UserController
OrderController
PaymentController
services/
UserService
OrderService
PaymentService
repositories/
UserRepository
OrderRepository
PaymentRepository
같은 기술적 역할을 하는 코드가 한 디렉토리에 모인다. Controller는 Controller끼리, Service는 Service끼리.
장점
기술 관심사가 명확히 분리된다. HTTP 요청 처리 로직은 Controller 계층에만 있고, 데이터 접근 로직은 Repository 계층에만 있다. 계층 교체가 용이하다. REST API를 gRPC로 바꿔도 Controller 계층만 수정하면 된다.
진입 장벽이 낮다. 대부분의 프레임워크(Spring, Express, Django)가 이 구조를 기본으로 제공한다. 새 팀원이 합류해도 구조를 빠르게 파악한다.
한계
“주문 취소” 기능을 수정한다고 하면 OrderController, OrderService, OrderRepository, 경우에 따라 PaymentService까지 건드린다. 변경이 여러 계층에 걸친다.
기능이 늘어나면 각 계층 안에서 파일이 수십 개로 증가한다. services/ 디렉토리에 UserService, OrderService, PaymentService, NotificationService, InventoryService, ShippingService가 뒤섞인다. 기술 계층은 같지만 도메인 맥락은 전혀 다른 코드가 같은 공간에 있다.
Vertical Slicing
수직 분할은 기능이나 도메인을 기준으로 코드를 나눈다. 각 slice가 필요한 계층을 모두 포함한다.
src/
user/
UserController
UserService
UserRepository
order/
OrderController
OrderService
OrderRepository
payment/
PaymentController
PaymentService
PaymentRepository
“주문” 관련 코드는 전부 order/ 안에 있다. 주문 기능을 수정할 때 다른 디렉토리를 건드리지 않는다.
Vertical Slice Architecture
일반적인 수직 분할이 도메인 모듈 단위(User, Order, Payment)로 나눈다면, Jimmy Bogard가 제안한 Vertical Slice Architecture는 더 세분화된다. 모듈이 아니라 use case 단위로 코드를 분리한다.
src/
features/
CreateOrder/
CreateOrderHandler
CreateOrderRequest
CreateOrderValidator
CancelOrder/
CancelOrderHandler
CancelOrderRequest
GetOrderDetail/
GetOrderDetailHandler
GetOrderDetailQuery
각 use case가 독립적인 slice다. CreateOrder와 CancelOrder는 같은 “주문” 도메인이지만 서로 다른 slice로 분리된다. 한 slice 안에서 요청 수신부터 데이터 접근까지 전 과정이 완결된다.
장점
기능별 독립성이 높다. 한 slice를 수정해도 다른 slice에 영향을 주지 않는다. 코드 리뷰 범위가 좁고, 충돌이 줄어든다. 여러 팀이 같은 코드베이스에서 독립적으로 작업하기 좋은 구조다.
변경의 응집도가 높다. 하나의 기능 변경이 하나의 디렉토리 안에서 완결된다. 코드 리뷰 범위가 좁아지고, 변경이 다른 도메인에 영향을 주지 않는다.
한계
공통 코드 관리가 까다롭다. 인증, 로깅, 트랜잭션 처리 같은 cross-cutting concern은 여러 slice에서 동일하게 필요하다. 각 slice에 복사하면 중복이 생기고, 공통 모듈로 추출하면 slice 간 의존성이 생겨 독립성이 약해진다. “적절한 중복"과 “과도한 중복"의 경계를 판단해야 한다.
일관성 유지 비용이 있다. 각 slice가 독립적으로 변경되면 코딩 스타일, 에러 처리, 로깅 방식이 slice마다 달라질 수 있다. 팀 컨벤션과 코드 리뷰로 맞춰야 한다.
선택 기준
둘 중 하나를 선택하는 문제라기보다 상황에 따라 적절한 방향이 달라진다.
프로젝트 초기, 도메인이 작을 때는 수평 분할이 적합하다. 기능 수가 적으면 계층별 파일이 적고, 프레임워크 기본 구조를 그대로 사용할 수 있다. 구조를 잡는 데 드는 비용이 낮다.
기능이 많아지고 팀이 커질 때는 수직 분할의 이점이 드러난다. 기능별 독립 작업이 가능해지고, 변경 범위가 줄어든다. MSA로 전환할 때도 수직으로 나뉜 코드가 서비스 경계로 자연스럽게 이어진다.
혼합 사용도 흔하다. 최상위를 도메인별로 수직 분할하고, 각 도메인 안에서는 기술 계층별로 수평 분할하는 구조다.
src/
order/
controller/
service/
repository/
payment/
controller/
service/
repository/
수직 분할로 도메인 경계를 정의하고, 수평 분할로 각 도메인 내부를 정리한다. 실무에서 자주 마주치는 구조라 판단했다.
어느 쪽이 더 좋다가 아니라, 프로젝트 규모와 팀 구조에 따라 적절한 방향이 다르다. 작게 시작할 때는 수평 분할로 충분하고, 기능이 늘고 팀이 커지면 수직 분할로 도메인 경계를 정의하는 흐름이 자연스럽다고 봤다.