Skip to content

アーキテクチャ設計の意思決定

🏗️ アーキテクチャ設計の意思決定

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);
}
}
// アプリケーション層: ユースケースのオーケストレーション
@Service
public 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());
}
}
// インフラ層: アダプター
@Repository
public 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のデフォルト):

// 利点: シンプルで開発が速い
@Entity
public 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パターン(実装の切り替えが容易)

分散トランザクション vs Sagaパターン

Section titled “分散トランザクション vs Sagaパターン”

分散トランザクション(2PC)の問題点:

// 問題: マイクロサービス間の分散トランザクションは複雑
@Transactional
public 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(よりスケーラブル)
@Component
public 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パターン
一貫性強い一貫性結果整合性
パフォーマンス遅い(ロック待機)速い(非同期可能)
複雑性高い中程度
スケーラビリティ低い高い
適用範囲モノリスマイクロサービス

キャッシュアサイド vs ライトスルー vs ライトビハインド

Section titled “キャッシュアサイド vs ライトスルー vs ライトビハインド”

キャッシュアサイド(Cache-Aside):

// 最も一般的なパターン
@Service
public 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の両方に書き込む
@Service
public class UserService {
public void saveUser(User user) {
// 1. キャッシュに書き込み
cacheManager.put("user:" + user.getId(), user);
// 2. DBに書き込み
userRepository.save(user);
// 利点: 読み取り時のキャッシュヒット率が高い
// 欠点: 書き込みが遅い
}
}

ライトビハインド(Write-Behind / Write-Back):

// 非同期でDBに書き込む
@Service
public 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を選ぶべき場合:
// - 書き込みが非常に多い
// - 一時的なデータ損失を許容できる
// - 高い書き込み性能が必要

同期処理 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. バッチ処理やレポート生成
@Service
public 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. スケーラビリティが重要
// イベント発行
@Service
public 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
@Async
public void handleOrderCreated(OrderCreatedEvent event) {
// 在庫更新
inventoryService.reduceStock(event.getOrderId());
// メール送信
emailService.sendOrderConfirmation(event.getCustomerId());
// 分析データの送信
analyticsService.trackOrderCreated(event);
}

トレードオフ分析:

観点同期処理非同期処理イベント駆動
レスポンス時間遅い速い速い
エラーハンドリング簡単複雑非常に複雑
デバッグの容易さ高い中程度低い
スケーラビリティ低い高い非常に高い
一貫性強い中程度結果整合性

パフォーマンス最適化の意思決定

Section titled “パフォーマンス最適化の意思決定”

早期最適化 vs 計測に基づく最適化

Section titled “早期最適化 vs 計測に基づく最適化”

アンチパターン: 早期最適化

// 悪い例: パフォーマンス問題が発生する前に最適化
@Service
public 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());
}
}

ベストプラクティス: 計測に基づく最適化

// 良い例: 計測してから最適化
@Service
public 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) {
// // キャッシュを追加
// }
}
}
// プロファイリングツールの使用
@Component
public 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);
}
}
}
}

アーキテクチャ設計の意思決定において重要なポイント:

  1. コンテキストが重要: プロジェクトの規模、チームのスキル、ビジネス要件を考慮
  2. トレードオフを理解: すべての選択にはメリットとデメリットがある
  3. 計測に基づく判断: 推測ではなく、データに基づいて意思決定
  4. 段階的な進化: 完璧なアーキテクチャを最初から作ろうとせず、必要に応じて進化させる
  5. チームの合意: 技術的な選択は、チーム全体で理解し、維持できるものを選ぶ

適切なアーキテクチャの選択は、プロジェクトの成功に大きく影響します。コンテキストを理解し、トレードオフを分析した上で、最適な選択を行いましょう。