레이어드 아키텍처를 넘어서: FastAPI에서 버티컬 슬라이스로 확장 가능한 API 구축하기
Min-jun Kim
Dev Intern · Leapcell

소개
수십 년 동안 계층형 아키텍처는 백엔드 시스템 설계의 기반이었습니다. 우리는 관심사의 분리를 프레젠테이션, 비즈니스 로직, 데이터 액세스 계층으로 엄격하게 나누는 데 익숙해졌습니다. 이 접근 방식은 명확한 구조적 이점을 제공하고 코드 구성을 촉진하지만, 현대 애플리케이션의 복잡성으로 인해 종종 한계가 드러납니다. 서비스가 성장함에 따라 간단해 보이는 새로운 기능이 모든 계층에 파급 효과를 일으켜 광범위한 교차 편집 변경, 인지 부하 증가, 개발 주기 지연으로 이어질 수 있습니다. 이 글에서는 백엔드 세계에서 주목받고 있는 대안 패러다임인 버티컬 슬라이스 아키텍처를 살펴봅니다. 이 접근 방식을 FastAPI와 같은 최신 프레임워크에 적용할 때 어떻게 더 집중적이고 유지 관리 가능하며 궁극적으로 확장 가능한 API 서비스를 만들 수 있는지, API 디자인에 대한 신선한 관점을 열어갈 수 있는지 탐구합니다.
핵심 개념 설명
버티컬 슬라이스의 실질적인 부분에 들어가기 전에 논의할 핵심 용어에 대한 명확한 이해를 정립해 봅시다.
계층형 아키텍처: 이 전통적인 아키텍처 스타일은 특정 책임을 가진 별개의 수평 계층으로 코드를 구성합니다. 예를 들어, 일반적인 웹 애플리케이션에는 프레젠테이션 계층(컨트롤러/라우터), 비즈니스 로직 계층(서비스), 데이터 액세스 계층(리포지토리)이 있을 수 있습니다. 통신은 일반적으로 아래쪽으로 흐르며, 각 계층은 위에 있는 계층의 구현 세부 사항을 거의 알지 못합니다.
버티컬 슬라이스 아키텍처(VSA): 계층형 접근 방식과는 극적으로 다르며, VSA는 별도의 기능 또는 사용 사례, 종종 "버티컬 슬라이스" 또는 "기능"이라고 불리는 것을 중심으로 코드를 구성합니다. 각 슬라이스는 API 엔드포인트 정의부터 데이터 지속성까지 특정 기능 조각을 제공하는 데 필요한 모든 구성 요소를 캡슐화합니다. 케이크를 수직으로 자르는 것을 상상해 보세요. 각 슬라이스에는 모든 계층의 일부가 포함됩니다.
도메인 중심 설계(DDD): VSA에 엄격하게 묶여 있지는 않지만, DDD 원칙은 종종 VSA를 아름답게 보완합니다. DDD는 비즈니스 도메인을 깊이 이해하고 해당 도메인과 밀접하게 소프트웨어를 모델링하는 데 중점을 둡니다. 기능 중심 개발에 중점을 둔 VSA는 각 도메인 관심사에 대한 보편 언어 및 제한된 컨텍스트의 강조와 잘 일치합니다.
CQRS (Command Query Responsibility Segregation): 일부 VSA 구현에서 CQRS는 자연스러운 적합처가 될 수 있습니다. 이는 데이터 수정(명령)의 책임과 데이터 쿼리(쿼리)의 책임을 분리할 것을 제안합니다. 버티컬 슬라이스 내에서 특정 기능에 대한 해당 작업을 관리하는 별도의 명령 처리기 및 쿼리 처리기를 찾을 수 있습니다.
버티컬 슬라이스의 원칙
버티컬 슬라이스 아키텍처의 핵심 원칙은 수평 계층화를 버리고 수직적이며 기능 중심적인 구성을 채택하는 것입니다. 애플리케이션 전체의 모든 비즈니스 로직을 포함하는 services 디렉토리와 모든 데이터 액세스를 위한 repositories 디렉토리가 있는 대신, VSA는 단일 기능에 대한 모든 관련 코드를 함께 그룹화하도록 제안합니다.
예를 들어 사용자 프로필을 관리하는 애플리케이션을 생각해 봅시다. 계층형 아키텍처에서는 다음과 같이 구성할 수 있습니다.
app/api/endpoints/users.py(FastAPI 라우터)app/services/user_service.py(비즈니스 로직)app/repositories/user_repository.py(데이터 액세스)app/schemas/user_schemas.py(Pydantic 모델)
버티컬 슬라이스 아키텍처에서는 "사용자 생성" 기능에 대한 이러한 모든 구성 요소가 app/features/create_user/와 같은 단일 디렉토리에 있을 수 있습니다. 이 디렉토리에는 엔드포인트 정의, 요청/응답 모델, 비즈니스 로직, 심지어 사용자를 생성하기 위한 데이터 지속성 로직까지 포함됩니다.
이점은 다음과 같습니다.
- 인지 부하 감소: 기능을 작업할 때 관련 코드가 모두 한 곳에 있습니다. 여러 디렉토리와 다른 계층의 파일을 이동할 필요가 없습니다.
- 응집도 증가: 슬라이스 내의 구성 요소는 높은 응집도를 가지며 단일 기능에 직접적으로 기여합니다.
- 느슨한 결합: 슬라이스는 대체로 독립적입니다. 한 슬라이스 내의 변경 사항은 다른 슬라이스에 영향을 미칠 가능성이 낮아 회귀 위험을 줄입니다.
- 테스트 용이성: 각 슬라이스의 종속성은 자체 포함되거나 슬라이스 내에서 명시적으로 관리되므로 각 슬라이스를 독립적으로 테스트할 수 있습니다.
- 온보딩 간소화: 신규 개발자는 단일의 자체 포함된 코드 단위에 집중함으로써 개별 기능을 더 빠르게 파악할 수 있습니다.
- 더 나은 확장성 및 유지 관리성: 애플리케이션이 성장함에 따라 새 기능을 추가하는 것은 기존, 잠재적으로 모놀리식 계층을 수정하거나 확장하는 것이 아니라 새 슬라이스를 추가하는 것이 됩니다.
FastAPI에서 버티컬 슬라이스 구현하기
실습 예제를 통해 FastAPI 애플리케이션에서 VSA를 구현하는 방법을 설명하겠습니다. 간단한 전자 상거래 애플리케이션과 Product 엔티티를 고려해 보겠습니다. "제품 생성" 및 "ID로 제품 가져오기"라는 두 가지 기능에 중점을 둘 것입니다.
먼저, 버티컬 슬라이스를 기반으로 한 프로젝트 구조를 정의합니다.
├── app/
│ ├── main.py
│ ├── database.py
│ ├── models.py
│ ├── features/
│ │ ├── create_product/
│ │ │ ├── __init__.py
│ │ │ ├── endpoint.py # FastAPI 라우터가 엔드포인트 정의
│ │ │ ├── schemas.py # 요청/응답을 위한 Pydantic 모델
│ │ │ ├── service.py # 제품 생성을 위한 비즈니스 로직
│ │ │ └── repository.py # 제품 생성에 특화된 데이터 액세스 로직
│ │ ├── get_product_by_id/
│ │ │ ├── __init__.py
│ │ │ ├── endpoint.py
│ │ │ ├── schemas.py
│ │ │ ├── service.py
│ │ │ └── repository.py
│ └── __init__.py
create_product 슬라이스의 코드를 살펴봅시다.
app/models.py (공유 데이터베이스 모델, VSA는 이러한 광범위한 공유 구성 요소를 줄이는 것을 목표로 하지만, 핵심 도메인 모델은 때때로 공유됨)
from sqlalchemy import Column, Integer, String, Float from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class Product(Base): __tablename__ = "products" id = Column(Integer, primary_key=True, index=True) name = Column(String, index=True) description = Column(String) price = Column(Float) def to_dict(self): return { "id": self.id, "name": self.name, "description": self.description, "price": self.price, }
app/database.py (공유 데이터베이스 설정)
from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker DATABASE_URL = "sqlite:///./test.db" engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def get_db(): db = SessionLocal() try: yield db finally: db.close()
app/features/create_product/schemas.py
from pydantic import BaseModel class ProductCreate(BaseModel): name: str description: str | None = None price: float class ProductResponse(BaseModel): id: int name: str description: str | None = None price: float class Config: from_attributes = True # Pydantic v2 # orm_mode = True # Pydantic v1
app/features/create_product/repository.py
from sqlalchemy.orm import Session from app.models import Product from app.features.create_product.schemas import ProductCreate def create_product(db: Session, product: ProductCreate) -> Product: db_product = Product(name=product.name, description=product.description, price=product.price) db.add(db_product) db.commit() db.refresh(db_product) return db_product
app/features/create_product/service.py
from sqlalchemy.orm import Session from app.features.create_product import repository from app.features.create_product.schemas import ProductCreate, ProductResponse def create_new_product(db: Session, product_data: ProductCreate) -> ProductResponse: # 여기서 저장소 호출 전후에 비즈니스 로직을 추가할 수 있습니다. # 가격 검증, 재고 확인, 할인 적용 등 db_product = repository.create_product(db, product_data) return ProductResponse.model_validate(db_product)
app/features/create_product/endpoint.py
from fastapi import APIRouter, Depends, status from sqlalchemy.orm import Session from app.database import get_db from app.features.create_product import service from app.features.create_product.schemas import ProductCreate, ProductResponse router_create_product = APIRouter(tags=["Products"]) @router_create_product.post("/products/", response_model=ProductResponse, status_code=status.HTTP_201_CREATED) def create_product_endpoint(product: ProductCreate, db: Session = Depends(get_db)): return service.create_new_product(db, product)
이제 get_product_by_id 슬라이스입니다.
app/features/get_product_by_id/schemas.py
from pydantic import BaseModel from app.features.create_product.schemas import ProductResponse # 일관성을 위해 재사용하지만, 특정할 수도 있습니다. # 간단한 GET by ID에는 특정 요청 스키마가 필요하지 않습니다. # ProductResponse는 동일한 경우 create_product 슬라이스에서 재사용될 수 있습니다.
app/features/get_product_by_id/repository.py
from sqlalchemy.orm import Session from app.models import Product def get_product(db: Session, product_id: int) -> Product | None: return db.query(Product).filter(Product.id == product_id).first()
app/features/get_product_by_id/service.py
from fastapi import HTTPException, status from sqlalchemy.orm import Session from app.features.get_product_by_id import repository from app.features.create_product.schemas import ProductResponse # 스키마 재사용 def retrieve_product_by_id(db: Session, product_id: int) -> ProductResponse: product = repository.get_product(db, product_id) if product is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Product with id {product_id} not found" ) return ProductResponse.model_validate(product)
app/features/get_product_by_id/endpoint.py
from fastapi import APIRouter, Depends, status, HTTPException from sqlalchemy.orm import Session from app.database import get_db from app.features.get_product_by_id import service from app.features.create_product.schemas import ProductResponse # 스키마 재사용 router_get_product_by_id = APIRouter(tags=["Products"]) @router_get_product_by_id.get("/products/{product_id}", response_model=ProductResponse) def get_product_endpoint(product_id: int, db: Session = Depends(get_db)): return service.retrieve_product_by_id(db, product_id)
마지막으로 app/main.py에서 모든 것을 연결합니다.
from fastapi import FastAPI from app.database import Base, engine from app.features.create_product.endpoint import router_create_product from app.features.get_product_by_id.endpoint import router_get_product_by_id from app.models import Product # Base.metadata.create_all을 위해 모델을 가져와야 함 Base.metadata.create_all(bind=engine) app = FastAPI(title="Vertical Slice Product API") app.include_router(router_create_product) app.include_router(router_get_product_by_id) @app.get("/") async def root(): return {"message": "Welcome to Vertical Slice Product API"}
이 설정에서 각 기능 디렉토리(create_product, get_product_by_id)는 자체 포함된 단위로 작동합니다. 제품을 생성하는 방법을 수정해야 할 때 create_product 디렉토리 내의 파일만 건드리면 됩니다. app/models.py 및 app/database.py와 같은 일부 구성 요소는 여전히 공유되지만, 목표는 이러한 공유 개체를 최소화하고 각 슬라이스 내에서 최대한 캡슐화하는 것입니다. 이렇게 하면 변경 사항의 영향이 국지적으로 유지됩니다.
애플리케이션 시나리오
버티컬 슬라이스 아키텍처는 여러 시나리오에서 빛을 발합니다.
- 마이크로서비스 또는 마이크로 경계가 있는 모놀리스: VSA는 명확한 기능 경계를 제공하여 필요한 경우 특정 슬라이스를 새 마이크로서비스로 추출하거나 마이크로서비스와 유사한 내부 구성을 가진 모놀리식 애플리케이션을 유지 관리하기 쉽게 만듭니다.
- 다른 기능을 작업하는 팀: 여러 팀이 별도의 기능을 작업할 때 VSA는 병합 충돌을 최소화하고 팀이 더 큰 자율성으로 운영할 수 있도록 합니다.
- 복잡한 비즈니스 도메인: 풍부하고 복잡한 비즈니스 로직을 가진 애플리케이션의 경우 VSA는 도메인을 관리 가능한 문제별 슬라이스로 분해하여 복잡성을 관리하는 데 도움이 됩니다.
- 빠른 프로토타이핑 및 반복: 슬라이스의 자체 포함 특성으로 인해 개별 기능을 더 빠르게 개발하고 배포할 수 있습니다.
- 이벤트 기반 아키텍처: 각 슬라이스는 자체 이벤트 생성기 및 소비자에게 정의하여 이벤트 기반 통신 패턴을 단순화할 수 있습니다.
결론
더 나은 아키텍처 패턴에 대한 추구는 소프트웨어 개발에서 지속적인 여정입니다. 계층형 아키텍처가 우리에게 잘 봉사했지만, 버티컬 슬라이스 아키텍처는 특히 현대적이고 빠르게 발전하는 애플리케이션에 대해 신선하고 매우 효과적인 대안을 제공합니다. 기능 중심 개발에 초점을 맞춤으로써 FastAPI의 VSA는 매우 응집력 있고 느슨하게 결합되며 독립적으로 배포 가능한 기능 단위를 촉진합니다. 이 패러다임 전환은 유지 관리성을 크게 향상시키고, 인지 부하를 줄이며, 개발을 가속화하여 확장 가능하고 강력한 API 서비스를 구축하는 데 매력적인 선택이 될 수 있습니다. 버티컬 슬라이스를 받아들이고 비즈니스 기능과 진정으로 일치하는 시스템을 구축하십시오.

