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での垂直スライスの実装
FastAPIは、Pydanticモデルと依存関係注入に重点を置いているため、垂直スライスにも素晴らしく適合します。専用のモジュールまたはディレクトリ内に機能のルート、モデル、ロジックを定義することで、同様の構造を実現できます。
# 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などのフレームワークで複雑なアプリケーションの開発を加速できます。垂直スライスを採用して、機能的であるだけでなく、信じられないほど機敏で保守可能なアプリケーションを構築し、より回復力のあるスケーラブルなソフトウェアシステムへの道を開きましょう。