N계층 아키텍처를 넘어 버티컬 슬라이스 채택하기
James Reed
Infrastructure Engineer · Leapcell

모놀리식 분열: 애플리케이션 구조 재고
끊임없이 진화하는 소프트웨어 개발 환경에서 애플리케이션 구조 방식은 유지보수성, 확장성 및 개발자 경험에 지대한 영향을 미칩니다. 수십 년 동안 프레젠테이션, 비즈니스 로직, 데이터 액세스를 위한 별도의 계층으로 구성된 N계층 아키텍처가 사실상의 표준이었습니다. 우려 사항을 명확하게 분리하는 이점은 있지만, 이 접근 방식은 종종 수평적 결합을 초래합니다. 즉, 계층의 한 부분에서의 변경이 전체 애플리케이션에 파급되어 개발을 번거롭게 하고 배포를 위험하게 만들 수 있으며, 특히 애플리케이션이 복잡해짐에 따라 더욱 그렇습니다. 이러한 어려움은 기존 N계층 모델이 오늘날의 애자일 개발 환경의 요구 사항을 진정으로 충족하는지 의문을 갖게 합니다. 이 글에서는 특히 ASP.NET Core 및 FastAPI의 맥락에서 보다 응집력 있고 관리 가능한 애플리케이션을 구축하기 위한 매력적인 솔루션으로 "버티컬 슬라이스 아키텍처"라는 대안적인 패러다임을 소개합니다.
버티컬 슬라이스 및 핵심 원칙 이해하기
실용적인 내용으로 들어가기 전에 버티컬 슬라이스 아키텍처를 뒷받침하는 핵심 개념을 명확히 해보겠습니다.
N계층 아키텍처: 프레젠테이션, 비즈니스 로직(서비스 계층), 데이터 액세스(리포지토리 계층)와 같은 논리적 계층으로 애플리케이션이 분할되는 전통적인 아키텍처 패턴입니다. 각 계층은 특정 책임을 가지며, 일반적으로 계층 간에 단방향으로 통신합니다.
버티컬 슬라이스: N계층의 수평적 관심사 분할과 달리, 버티컬 슬라이스는 단일 기능 또는 사용 사례를 처음부터 끝까지 제공하는 데 필요한 모든 구성 요소를 캡슐화합니다. 여기에는 API 엔드포인트, 비즈니스 로직, 데이터 액세스, 그리고 특정 UI 구성 요소(이 글은 주로 백엔드에 초점을 맞춥니다)까지 포함됩니다. 각 슬라이스는 독립적이며 종종 격리하여 개발, 테스트 및 배포될 수 있습니다.
도메인 주도 설계 (DDD): 엄격하게 요구되지는 않지만, 버티컬 슬라이스는 일반적으로 기능이 비즈니스 도메인을 중심으로 구성되는 DDD 원칙과 잘 맞습니다. 이는 특정 도메인 기능을 나타내는 응집력 있는 슬라이스로 자연스럽게 이어집니다.
클린 아키텍처 / 헥사고날 아키텍처: 이러한 아키텍처는 핵심 비즈니스 로직과의 거리를 기준으로 관심사를 분리하는 것을 강조합니다. 버티컬 슬라이스는 이러한 아키텍처 스타일 내에서 실질적인 구현 전략으로 볼 수 있으며, 각 슬라이스가 이러한 원칙을 독립적으로 준수할 수 있도록 합니다.
버티컬 슬라이스의 핵심 원칙은 기술적 관심사별 응집력보다 기능별 응집력을 우선시하는 것입니다. 모든 사용자 관련 작업을 처리하는 단일 UserService를 갖는 대신, "사용자 생성", "사용자 세부 정보 가져오기", "사용자 프로필 업데이트"와 같은 별도의 슬라이스를 가질 수 있습니다. 각 슬라이스는 미니어처 독립형 애플리케이션으로, 관련 없는 기능 간의 결합을 크게 줄입니다.
ASP.NET Core에서 버티컬 슬라이스 구현하기
간단한 ASP.NET Core 제품 관리 애플리케이션으로 버티컬 슬라이싱을 설명해 보겠습니다. ProductService와 ProductRepository 대신 CreateProduct, GetProductById, ListProducts에 대한 별도의 슬라이스를 만들 것입니다. 요청 및 명령 처리를 위해 MediatR을 활용할 것이며, 이는 요청을 특정 핸들러로 라우팅함으로써 버티컬 슬라이스 패턴에 자연스럽게 맞습니다.
먼저 MediatR을 설치합니다:
dotnet add package MediatR dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
CreateProduct 기능을 고려해 보세요. 엔드포인트부터 데이터베이스까지 모든 범위는 단일 전용 폴더 내에 있습니다.
// Features/Products/CreateProduct.cs using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Threading; using System.Threading.Tasks; namespace MyWebApp.Features.Products { public static class CreateProduct { // 1. Command (Input) public class Command : IRequest<Response> { public string Name { get; set; } public decimal Price { get; set; } } // 2. Response (Output) public class Response { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } // 3. Handler (Business Logic & Data Access) public class Handler : IRequestHandler<Command, Response> { private readonly ProductContext _context; public Handler(ProductContext context) { _context = context; } public async Task<Response> Handle(Command request, CancellationToken cancellationToken) { var product = new Product { Name = request.Name, Price = request.Price }; _context.Products.Add(product); await _context.SaveChangesAsync(cancellationToken); return new Response { Id = product.Id, Name = product.Name, Price = product.Price }; } } // 4. API Endpoint (Controller) [ApiController] [Route("api/products")] public class ProductsController : ControllerBase { private readonly IMediator _mediator; public ProductsController(IMediator mediator) { _mediator = mediator; } [HttpPost] public async Task<ActionResult<Response>> Post(Command command) { var response = await _mediator.Send(command); return CreatedAtAction(nameof(Post), new { id = response.Id }, response); } } } // Shared: A simple Entity Framework Core DbContext and Product entity public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } public class ProductContext : DbContext { public DbSet<Product> Products { get; set; } public ProductContext(DbContextOptions<ProductContext> options) : base(options) { } } }
Program.cs 또는 Startup.cs에서 MediatR 및 DbContext를 구성합니다:
// Program.cs using Microsoft.EntityFrameworkCore; using MediatR; using MyWebApp.Features.Products; // Important for reflection var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext<ProductContext>(options => options.UseInMemoryDatabase("ProductDb")); // Using in-memory for simplicity builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); // Scan for MediatR handlers var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
CreateProduct.cs에는 해당 특정 기능과 관련된 거의 모든 것이 포함되어 있습니다. 컨트롤러는 명령을 MediatR 파이프라인으로 전달하는 얇은 파사드 역할을 합니다.
FastAPI에서 버티컬 슬라이스 구현하기
Pydantic 모델 및 종속성 주입에 대한 강력한 강조 기능을 갖춘 FastAPI 또한 버티컬 슬라이싱에 아름답게 적용됩니다. 전용 모듈 또는 디렉토리 내에서 기능의 라우트, 모델 및 로직을 정의하여 유사한 구조를 달성할 수 있습니다.
# app/features/products/create_product.py from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from typing import Optional # 데모를 위한 간단한 인메모리 "데이터베이스" 가정 # 실제 앱에서는 데이터베이스와 상호 작용하는 SQLAlchemy와 같은 ORM이 될 것입니다 class ProductDB: def __init__(self): self.products = [] self.next_id = 1 def create_product(self, name: str, price: float): product_data = {"id": self.next_id, "name": name, "price": price} self.products.append(product_data) self.next_id += 1 return product_data def get_product(self, product_id: int): for product in self.products: if product["id"] == product_id: return product return None # 데이터베이스 인스턴스를 제공하기 위한 종속성 (실제 DB 세션으로 교체 가능) def get_db(): return ProductDB() # 1. 요청 본문 모델 class CreateProductRequest(BaseModel): name: str price: float # 2. 응답 모델 class ProductResponse(BaseModel): id: int name: str price: float # 3. 라우터 (API 엔드포인트 & 비즈니스 로직) router = APIRouter(prefix="/products", tags=["products"]) @router.post("/", response_model=ProductResponse, status_code=status.HTTP_201_CREATED) async def create_product_endpoint( request: CreateProductRequest, db: ProductDB = Depends(get_db) # "데이터베이스" 주입 ): """ 새 제품을 생성합니다. """ created_product_data = db.create_product(request.name, request.price) return ProductResponse(**created_product_data) @router.get("/{product_id}", response_model=ProductResponse) async def get_product_endpoint( product_id: int, db: ProductDB = Depends(get_db) ): """ ID로 제품을 검색합니다. """ product = db.get_product(product_id) if product is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") return ProductResponse(**product)
app/main.py에서:
# app/main.py from fastapi import FastAPI from .features.products import create_product app = FastAPI(title="Vertical Slice Product API") # 기능별 라우터 포함 app.include_router(create_product.router) @app.get("/") async def root(): return {"message": "Welcome to the Vertical Slice API!"}
여기서 create_product.py는 자체 모델, 라우터(엔드포인트 역할을 하며 비즈니스 로직 처리), 심지어 특정 "데이터베이스" 상호 작용까지 캡슐화합니다. FastAPI 내의 종속성 주입 (Depends(get_db))은 각 기능이 다른 기능에 영향을 주지 않고 특정 종속성을 선언할 수 있도록 보장합니다.
버티컬 슬라이스를 고려해야 할 때
버티컬 슬라이스 아키텍처는 여러 시나리오에서 빛을 발합니다:
- 성장하는 모놀리식: N계층 애플리케이션을 탐색하고 수정하기 어려울 때, 버티컬 슬라이스는 새로운 기능을 격리하고 기존 기능을 슬라이스로 점진적으로 리팩토링하는 데 도움이 될 수 있습니다.
 - 마이크로서비스 전환: 각 버티컬 슬라이스가 나중에 별도의 서비스로 추출될 후보가 될 수 있으므로 마이크로서비스로 가는 훌륭한 디딤돌 역할을 할 수 있습니다.
 - 기능 중심 개발: 기술 계층이 아닌 기능 중심으로 조직하는 팀은 이 패턴이 워크플로와 자연스럽게 일치한다는 것을 알게 될 것입니다.
 - 소규모 팀: 인지 부하를 줄이고 변경의 파급 효과를 제한함으로써 소규모 팀은 기능을 더 독립적으로 개발하고 배포할 수 있습니다.
 - 고도로 반복적인 제품: 기능이 자주 추가, 변경 또는 제거되는 경우, 버티컬 슬라이스가 제공하는 격리를 통해 이러한 작업을 더 안전하고 빠르게 수행할 수 있습니다.
 
그러나 이것이 만능 해결책은 아닙니다. 복잡성이 거의 없는 아주 작고 간단한 애플리케이션의 경우, 버티컬 슬라이스를 설정하고 준수하는 데 드는 오버헤드가 필요하지 않을 수 있습니다. 인증, 로깅과 같은 공유된 핵심 로직 또는 교차 관심사는 여전히 종종 모든 슬라이스에 영향을 미치는 미들웨어 또는 파이프라인을 사용하여 신중한 설계가 필요합니다.
응집력과 민첩성 통합
버티컬 슬라이스 아키텍처는 기술 계층화에서 기능 중심 응집력으로 초점을 전환함으로써 오래된 N계층 설계의 전통에 도전합니다. 특정 사용 사례와 관련된 모든 구성 요소를 단일 단위로 가져옴으로써 개발자는 더 큰 자율성을 달성하고, 의도하지 않은 결합을 줄이며, ASP.NET Core 및 FastAPI와 같은 프레임워크에서 복잡한 애플리케이션의 개발을 가속화할 수 있습니다. 버티컬 슬라이스를 채택하여 단순히 기능적인 것을 넘어 믿지 못할 정도로 민첩하고 유지보수하기 쉬운 애플리케이션을 구축하고, 더 복원력 있고 확장 가능한 소프트웨어 시스템을 위한 길을 닦으십시오.