レイヤードアーキテクチャを超えて FastAPIにおけるバーティカルスライスでのスケーラブルなAPI構築
Min-jun Kim
Dev Intern · Leapcell

はじめに
何十年もの間、レイヤードアーキテクチャはバックエンドシステム設計の基盤となってきました。プレゼンテーション、ビジネスロジック、データアクセス層への関心の厳密な分離に慣れ親しんできました。このアプローチは明確な構造的利点を提供し、コード編成を促進しますが、現代のアプリケーションの複雑さはしばしばその限界を露呈させます。サービスが成長するにつれて、一見単純な新機能がすべての層に波及し、広範囲にわたる横断的な変更、認識負荷の増加、開発サイクルの遅延につながる可能性があります。この記事では、バックエンドの世界で注目を集めている代替パラダイム、バーティカルスライスアーキテクチャについて掘り下げます。FastAPIのようなモダンなフレームワークにこのアプローチを適用することで、より集中的で、保守可能で、最終的にはよりスケーラブルなAPIサービスにつながる可能性を探り、API設計に新たな視点をもたらします。
コアコンセプトの説明
バーティカルスライスの実用的な側面に入る前に、議論するコア用語について明確な理解を確立しましょう。
レイヤードアーキテクチャ: この伝統的なアーキテクチャスタイルは、コードを明確な水平層に編成し、各層が特定の責任を持ちます。たとえば、典型的なWebアプリケーションには、プレゼンテーション層(コントローラー/ルーター)、ビジネスロジック層(サービス)、データアクセス層(リポジトリ)があるかもしれません。通信は一般に下向きに流れ、各層は上の層の実装の詳細をほとんど意識しません。
バーティカルスライスアーキテクチャ (VSA): レイヤードアプローチとは根本的に異なり、VSAはコードを明確な機能またはユースケース、しばしば「バーティカルスライス」または「機能」と呼ばれるもの周辺に編成します。各スライスは、APIエンドポイント定義からデータ永続化まで、特定の機能を提供するために必要なすべてのコンポーネントをカプセル化します。ケーキを垂直にスライスすることを想像してください。各スライスには、すべての層から少しずつ含まれています。
ドメイン駆動設計 (DDD): VSAに厳密に縛られているわけではありませんが、DDDの原則はしばしばそれに美しく補完されます。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/のような単一のディレクトリに配置される可能性があります。このディレクトリには、エンドポイント定義、リクエスト/レスポンスモデル、ビジネスロジック、さらにはユーザー作成のためのデータ永続化ロジックが含まれます。
その利点は数多くあります。
- 認識負荷の軽減: 機能に取り組む際、関連するすべてのコードが1か所にあります。複数のディレクトリや異なる層のファイル間を移動する必要はありません。
- 凝集度の向上: スライス内のコンポーネントは高度に凝集しており、単一の機能に直接貢献します。
- 疎結合: スライスは、ほとんど独立しています。1つのスライス内の変更は、他のスライスに影響を与える可能性が低く、退行のリスクを低減します。
- テストの容易さ: 各スライスは、その依存関係がスライス内で自己完結しているか、明示的に管理されているため、独立してテストできます。
- オンボーディングの簡素化: 新しい開発者は、単一の自己完結型コードユニットに焦点を当てることで、個々の機能をより迅速に理解できます。
- スケーラビリティと保守性の向上: アプリケーションが成長するにつれて、新しい機能の追加は、既存の、潜在的にモノリシックなレイヤーの変更や拡張ではなく、新しいスライスの追加になります。
FastAPIでのバーティカルスライスの実装
実践的な例で、FastAPIアプリケーション内にVSAを実装する方法を説明しましょう。単純なeコマースアプリケーションでProductエンティティを扱います。ここでは、「製品作成」と「IDによる製品取得」の2つの機能に焦点を当てます。
まず、バーティカルスライスに基づいたプロジェクト構造を以下に示します。
├── 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)
次に、「IDによる製品取得」スライスです。
app/features/get_product_by_id/schemas.py
from pydantic import BaseModel from app.features.create_product.schemas import ProductResponse # 一貫性のために再利用しますが、特定のスキーマでも構いません # 単純なIDによるGETには、特定の要求スキーマは必要ありません # ProductResponseは、作成スライスから再利用できます(同一の場合)
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サービスを構築するための説得力のある選択肢となります。バーティカルスライスを採用し、ビジネス機能に真に一致したシステムを構築しましょう。

