アーキテクチャ設計の意思決定
🏗️ アーキテクチャ設計の意思決定
Section titled “🏗️ アーキテクチャ設計の意思決定”シニアエンジニアとして、適切なアーキテクチャを選択するためには、トレードオフを理解し、コンテキストに応じた判断が必要です。この章では、アーキテクチャ設計における重要な意思決定ポイントについて深く解説します。
📋 アーキテクチャパターンの選択
Section titled “📋 アーキテクチャパターンの選択”🏗️ レイヤードアーキテクチャ vs ヘキサゴナルアーキテクチャ
Section titled “🏗️ レイヤードアーキテクチャ vs ヘキサゴナルアーキテクチャ”レイヤードアーキテクチャの適用範囲:
// レイヤードアーキテクチャが適している場合:// - 中規模から大規模のエンタープライズアプリケーション// - CRUD操作が中心のアプリケーション// - チームが明確に分離されている場合
// 構造Controller → Service → Repository → Databaseメリット:
- 理解しやすい構造
- チーム間の責務が明確
- 既存のSpring Bootエコシステムと相性が良い
デメリット:
- ビジネスロジックがService層に集中しがち
- ドメインモデルが貧血症(Anemic Domain Model)になりやすい
- 複雑なビジネスルールの表現が困難
ヘキサゴナルアーキテクチャ(ポート&アダプター)の適用範囲:
// ヘキサゴナルアーキテクチャが適している場合:// - 複雑なビジネスロジックを持つドメイン駆動設計(DDD)アプリケーション// - 複数の外部システムとの統合が必要// - テスト容易性を最優先する場合
// 構造┌─────────────────────────────────┐│ Application Layer ││ (Use Cases / Application Services) │├─────────────────────────────────┤│ Domain Layer ││ (Entities / Value Objects / Domain Services) │├─────────────────────────────────┤│ Infrastructure Layer ││ (Adapters: DB, HTTP, Messaging) │└─────────────────────────────────┘実装例:
// ドメイン層: ビジネスロジックの核心public class Order { private OrderId id; private CustomerId customerId; private List<OrderItem> items; private OrderStatus status;
// ビジネスロジックをエンティティに持たせる public void cancel() { if (status == OrderStatus.SHIPPED) { throw new IllegalStateException("Shipped orders cannot be cancelled"); } this.status = OrderStatus.CANCELLED; // ドメインイベントの発行 DomainEventPublisher.publish(new OrderCancelledEvent(this.id)); }
public Money calculateTotal() { return items.stream() .map(OrderItem::getSubtotal) .reduce(Money.ZERO, Money::add); }}
// アプリケーション層: ユースケースのオーケストレーション@Servicepublic class OrderApplicationService { private final OrderRepository orderRepository; private final PaymentService paymentService; private final NotificationService notificationService;
@Transactional public void cancelOrder(OrderId orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException(orderId));
order.cancel(); // ドメインロジックを呼び出す
orderRepository.save(order);
// インフラ層のサービスを呼び出す paymentService.refund(order.getPaymentId()); notificationService.sendCancellationEmail(order.getCustomerId()); }}
// インフラ層: アダプター@Repositorypublic class JpaOrderRepository implements OrderRepository { @PersistenceContext private EntityManager em;
@Override public Order findById(OrderId id) { OrderEntity entity = em.find(OrderEntity.class, id.getValue()); return entity != null ? entity.toDomain() : null; }}トレードオフ分析:
| 観点 | レイヤード | ヘキサゴナル |
|---|---|---|
| 学習コスト | 低い | 高い |
| ビジネスロジックの表現力 | 中程度 | 高い |
| テスト容易性 | 中程度 | 高い |
| 既存フレームワークとの統合 | 容易 | やや困難 |
| 小規模プロジェクトへの適用 | 適している | 過剰 |
意思決定の指針:
// レイヤードアーキテクチャを選ぶべき場合:// 1. チームがSpring Bootに慣れ親しんでいる// 2. ビジネスロジックが比較的シンプル// 3. 開発速度を優先する// 4. 中規模のCRUDアプリケーション
// ヘキサゴナルアーキテクチャを選ぶべき場合:// 1. 複雑なビジネスルールがある// 2. ドメインエキスパートと密接に協業する// 3. 長期的な保守性を最優先する// 4. 複数の外部システムと統合する必要があるデータアクセスパターンの選択
Section titled “データアクセスパターンの選択”Repositoryパターン vs Active Recordパターン
Section titled “Repositoryパターン vs Active Recordパターン”Repositoryパターン(推奨):
// 利点: ドメインロジックとデータアクセスを分離public interface OrderRepository { Order findById(OrderId id); List<Order> findByCustomerId(CustomerId customerId); void save(Order order); void delete(OrderId id);}
// 実装の詳細を隠蔽// - JPA実装// - JDBC実装// - NoSQL実装// など、実装を切り替え可能Active Recordパターン(Spring Data JPAのデフォルト):
// 利点: シンプルで開発が速い@Entitypublic class Order extends AbstractPersistable<Long> { // エンティティ自体がデータアクセスメソッドを持つ // Spring Data JPAが自動実装}
public interface OrderRepository extends JpaRepository<Order, Long> { // メソッド名からクエリを自動生成 List<Order> findByCustomerId(Long customerId);}トレードオフ:
// Repositoryパターンの利点:// 1. ドメインモデルが永続化技術から独立// 2. テストが容易(モック化しやすい)// 3. 複数のデータソースに対応可能
// Active Recordパターンの利点:// 1. 開発速度が速い// 2. コード量が少ない// 3. Spring Data JPAと統合しやすい
// 意思決定:// - 小規模から中規模: Active Record(Spring Data JPA)// - 大規模で複雑なドメイン: Repositoryパターン// - マイクロサービス: Repositoryパターン(実装の切り替えが容易)トランザクション管理の戦略
Section titled “トランザクション管理の戦略”分散トランザクション vs Sagaパターン
Section titled “分散トランザクション vs Sagaパターン”分散トランザクション(2PC)の問題点:
// 問題: マイクロサービス間の分散トランザクションは複雑@Transactionalpublic void createOrderWithPayment(OrderRequest request) { // サービス1: 注文作成 Order order = orderService.create(request);
// サービス2: 決済処理 Payment payment = paymentService.process(order.getId(), request.getAmount());
// サービス3: 在庫更新 inventoryService.reduceStock(order.getItems());
// 問題: いずれかのサービスが失敗した場合のロールバックが困難 // 特にマイクロサービス間では2PCは非推奨}Sagaパターンの実装:
// Sagaパターン: 各サービスがローカルトランザクションを持ち、// 補償トランザクションで整合性を保つ
public class OrderSaga {
@Transactional public void execute(OrderRequest request) { SagaContext context = new SagaContext();
try { // Step 1: 注文作成 Order order = orderService.create(request); context.setOrderId(order.getId());
// Step 2: 決済処理 Payment payment = paymentService.process(order.getId(), request.getAmount()); context.setPaymentId(payment.getId());
// Step 3: 在庫更新 inventoryService.reduceStock(order.getItems()); context.setCompleted(true);
} catch (Exception e) { // 補償トランザクションの実行 compensate(context); throw new SagaExecutionException("Order creation failed", e); } }
private void compensate(SagaContext context) { // 逆順で補償処理を実行 if (context.isInventoryReduced()) { inventoryService.restoreStock(context.getOrderId()); } if (context.getPaymentId() != null) { paymentService.refund(context.getPaymentId()); } if (context.getOrderId() != null) { orderService.cancel(context.getOrderId()); } }}
// イベント駆動Saga(よりスケーラブル)@Componentpublic class OrderSagaOrchestrator {
@EventListener public void handle(OrderCreatedEvent event) { paymentService.processPayment(event.getOrderId(), event.getAmount()); }
@EventListener public void handle(PaymentProcessedEvent event) { inventoryService.reduceStock(event.getOrderId()); }
@EventListener public void handle(InventoryReducedEvent event) { orderService.confirm(event.getOrderId()); }
// 補償イベントハンドラー @EventListener public void handle(PaymentFailedEvent event) { orderService.cancel(event.getOrderId()); }}トレードオフ分析:
| 観点 | 分散トランザクション | Sagaパターン |
|---|---|---|
| 一貫性 | 強い一貫性 | 結果整合性 |
| パフォーマンス | 遅い(ロック待機) | 速い(非同期可能) |
| 複雑性 | 高い | 中程度 |
| スケーラビリティ | 低い | 高い |
| 適用範囲 | モノリス | マイクロサービス |
キャッシング戦略の選択
Section titled “キャッシング戦略の選択”キャッシュアサイド vs ライトスルー vs ライトビハインド
Section titled “キャッシュアサイド vs ライトスルー vs ライトビハインド”キャッシュアサイド(Cache-Aside):
// 最も一般的なパターン@Servicepublic class UserService { private final UserRepository userRepository; private final CacheManager cacheManager;
public User findById(Long id) { // 1. キャッシュから取得を試みる User cached = cacheManager.get("user:" + id, User.class); if (cached != null) { return cached; }
// 2. キャッシュにない場合はDBから取得 User user = userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException(id));
// 3. キャッシュに保存 cacheManager.put("user:" + id, user);
return user; }
@CacheEvict(value = "users", key = "#id") public void updateUser(Long id, UserUpdateRequest request) { // 更新時にキャッシュを無効化 userRepository.save(convertToEntity(request)); }}ライトスルー(Write-Through):
// 書き込み時にキャッシュとDBの両方に書き込む@Servicepublic class UserService {
public void saveUser(User user) { // 1. キャッシュに書き込み cacheManager.put("user:" + user.getId(), user);
// 2. DBに書き込み userRepository.save(user);
// 利点: 読み取り時のキャッシュヒット率が高い // 欠点: 書き込みが遅い }}ライトビハインド(Write-Behind / Write-Back):
// 非同期でDBに書き込む@Servicepublic class UserService { private final BlockingQueue<WriteTask> writeQueue = new LinkedBlockingQueue<>();
@PostConstruct public void init() { // バックグラウンドスレッドでDBに書き込み ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(() -> { while (!Thread.currentThread().isInterrupted()) { try { WriteTask task = writeQueue.take(); userRepository.save(task.getUser()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }); }
public void saveUser(User user) { // 1. キャッシュに即座に書き込み cacheManager.put("user:" + user.getId(), user);
// 2. キューに追加(非同期でDBに書き込み) writeQueue.offer(new WriteTask(user));
// 利点: 書き込みが非常に速い // 欠点: データ損失のリスク、実装が複雑 }}トレードオフ分析:
| パターン | 読み取り性能 | 書き込み性能 | データ一貫性 | 実装複雑性 |
|---|---|---|---|---|
| Cache-Aside | 中 | 中 | 高い | 低い |
| Write-Through | 高 | 低 | 高い | 中 |
| Write-Behind | 高 | 非常に高 | 低い | 高い |
意思決定の指針:
// Cache-Asideを選ぶべき場合:// - 読み取りと書き込みがバランスしている// - データの一貫性が重要// - シンプルな実装を望む
// Write-Throughを選ぶべき場合:// - 読み取りが圧倒的に多い// - データの一貫性が最重要// - 書き込みの遅延を許容できる
// Write-Behindを選ぶべき場合:// - 書き込みが非常に多い// - 一時的なデータ損失を許容できる// - 高い書き込み性能が必要非同期処理の戦略
Section titled “非同期処理の戦略”同期処理 vs 非同期処理 vs イベント駆動
Section titled “同期処理 vs 非同期処理 vs イベント駆動”同期処理の適用範囲:
// 同期処理が適している場合:// 1. レスポンス時間が短い(< 100ms)// 2. ユーザーが結果を待つ必要がある// 3. エラーハンドリングがシンプル
@PostMapping("/users")public ResponseEntity<UserDTO> createUser(@RequestBody UserCreateRequest request) { // ユーザー作成は即座に完了する必要がある UserDTO user = userService.create(request); return ResponseEntity.status(HttpStatus.CREATED).body(user);}非同期処理の適用範囲:
// 非同期処理が適している場合:// 1. 処理に時間がかかる(> 1秒)// 2. ユーザーが結果を待つ必要がない// 3. バッチ処理やレポート生成
@Servicepublic class ReportService { private final ExecutorService executor = Executors.newFixedThreadPool(10);
public CompletableFuture<Report> generateReport(ReportRequest request) { return CompletableFuture.supplyAsync(() -> { // 時間のかかる処理 return generateLargeReport(request); }, executor); }}
@PostMapping("/reports")public ResponseEntity<Map<String, String>> createReport(@RequestBody ReportRequest request) { CompletableFuture<Report> future = reportService.generateReport(request);
// 即座にレスポンスを返す return ResponseEntity.accepted() .body(Map.of("reportId", future.thenApply(Report::getId).toString()));}イベント駆動アーキテクチャ:
// イベント駆動が適している場合:// 1. 複数のシステムが連携する必要がある// 2. 疎結合な設計を望む// 3. スケーラビリティが重要
// イベント発行@Servicepublic class OrderService { private final ApplicationEventPublisher eventPublisher;
@Transactional public Order createOrder(OrderRequest request) { Order order = orderRepository.save(convertToEntity(request));
// イベントを発行(非同期で処理される) eventPublisher.publishEvent(new OrderCreatedEvent( order.getId(), order.getCustomerId(), order.getTotalAmount() ));
return order; }}
// イベントハンドラー(非同期)@EventListener@Asyncpublic void handleOrderCreated(OrderCreatedEvent event) { // 在庫更新 inventoryService.reduceStock(event.getOrderId());
// メール送信 emailService.sendOrderConfirmation(event.getCustomerId());
// 分析データの送信 analyticsService.trackOrderCreated(event);}トレードオフ分析:
| 観点 | 同期処理 | 非同期処理 | イベント駆動 |
|---|---|---|---|
| レスポンス時間 | 遅い | 速い | 速い |
| エラーハンドリング | 簡単 | 複雑 | 非常に複雑 |
| デバッグの容易さ | 高い | 中程度 | 低い |
| スケーラビリティ | 低い | 高い | 非常に高い |
| 一貫性 | 強い | 中程度 | 結果整合性 |
パフォーマンス最適化の意思決定
Section titled “パフォーマンス最適化の意思決定”早期最適化 vs 計測に基づく最適化
Section titled “早期最適化 vs 計測に基づく最適化”アンチパターン: 早期最適化
// 悪い例: パフォーマンス問題が発生する前に最適化@Servicepublic class UserService { // 問題: 100件程度のデータなのに、複雑な最適化を実装 @Cacheable("users") // キャッシュのオーバーヘッドの方が大きい可能性 public List<User> findAll() { return userRepository.findAll(); // 100件程度 }
// 問題: 並列処理のオーバーヘッドの方が大きい可能性 public List<UserDTO> processUsers(List<User> users) { return users.parallelStream() // 10件程度なのに並列処理 .map(this::toDTO) .collect(Collectors.toList()); }}ベストプラクティス: 計測に基づく最適化
// 良い例: 計測してから最適化@Servicepublic class UserService {
public List<User> findAll() { // 1. まずシンプルな実装 return userRepository.findAll();
// 2. パフォーマンス問題が発生したら計測 // long start = System.currentTimeMillis(); // List<User> result = userRepository.findAll(); // long duration = System.currentTimeMillis() - start; // log.info("findAll took {}ms", duration);
// 3. ボトルネックを特定してから最適化 // if (duration > 1000) { // // キャッシュを追加 // } }}
// プロファイリングツールの使用@Componentpublic class PerformanceMonitor {
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping)") public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); try { return joinPoint.proceed(); } finally { long duration = System.currentTimeMillis() - start; if (duration > 1000) { log.warn("Slow endpoint: {} took {}ms", joinPoint.getSignature(), duration); } } }}アーキテクチャ設計の意思決定において重要なポイント:
- コンテキストが重要: プロジェクトの規模、チームのスキル、ビジネス要件を考慮
- トレードオフを理解: すべての選択にはメリットとデメリットがある
- 計測に基づく判断: 推測ではなく、データに基づいて意思決定
- 段階的な進化: 完璧なアーキテクチャを最初から作ろうとせず、必要に応じて進化させる
- チームの合意: 技術的な選択は、チーム全体で理解し、維持できるものを選ぶ
適切なアーキテクチャの選択は、プロジェクトの成功に大きく影響します。コンテキストを理解し、トレードオフを分析した上で、最適な選択を行いましょう。