ベストプラクティス
✅ ベストプラクティス
Section titled “✅ ベストプラクティス”Javaでの正しい構造とベストプラクティスを詳しく解説します。
📋 1. Outboxパターンの実装
Section titled “📋 1. Outboxパターンの実装”✅ 正しい構造
Section titled “✅ 正しい構造”@Servicepublic class OrderService { @Autowired private OrderRepository orderRepository;
@Autowired private OutboxRepository outboxRepository;
@Transactional public Order createOrder(OrderData orderData) { // ✅ 正しい: トランザクション内でOutboxに記録 Order order = orderRepository.save(new Order(orderData));
// Outboxテーブルに外部API呼び出しのタスクを記録 OutboxEvent event = new OutboxEvent(); event.setEventType("PAYMENT_CHARGE"); event.setAggregateId(order.getId()); event.setPayload(JSON.toJSONString(Map.of( "orderId", order.getId(), "amount", orderData.getAmount() ))); event.setStatus("PENDING"); event.setIdempotencyKey("payment-" + order.getId() + "-" + System.currentTimeMillis()); outboxRepository.save(event);
// トランザクションをコミット(外部APIは呼ばない) return order; }}
// 別のプロセス/ワーカーでOutboxを処理@Componentpublic class OutboxProcessor { @Autowired private OutboxRepository outboxRepository;
@Autowired private PaymentApiClient paymentApiClient;
@Scheduled(fixedRate = 5000) // 5秒ごと public void processOutbox() { List<OutboxEvent> pendingEvents = outboxRepository.findByStatus("PENDING");
for (OutboxEvent event : pendingEvents) { try { // 外部APIを呼ぶ(トランザクション外) Map<String, Object> payload = JSON.parseObject(event.getPayload(), Map.class); PaymentResult result = paymentApiClient.chargePayment( (Long) payload.get("orderId"), (BigDecimal) payload.get("amount") );
if (result.isSuccess()) { event.setStatus("COMPLETED"); outboxRepository.save(event); } else { event.setStatus("FAILED"); event.setRetryCount(event.getRetryCount() + 1); outboxRepository.save(event); } } catch (Exception e) { // エラーハンドリング event.setStatus("FAILED"); event.setRetryCount(event.getRetryCount() + 1); outboxRepository.save(event); log.error("Failed to process outbox event: " + event.getId(), e); } } }}なぜ正しいか:
- トランザクションの短縮: データベースのロック時間が短縮される
- 外部障害の分離: 外部APIの障害がトランザクションに影響しない
- 再実行の容易さ: Outboxテーブルから再実行可能
- 冪等性の保証: 冪等キーにより重複実行を防止
2. Serverless環境での非同期処理
Section titled “2. Serverless環境での非同期処理”@RestControllerpublic class ReportController { @Autowired private ReportService reportService;
@Autowired private MessageQueue messageQueue;
@PostMapping("/reports/generate") public ResponseEntity<ReportResponse> generateReport(@RequestBody ReportRequest request) { // ✅ 正しい: 即座にレスポンスを返す String reportId = UUID.randomUUID().toString();
// ジョブをキューに投入 messageQueue.send("report.generate", Map.of( "reportId", reportId, "request", request ));
return ResponseEntity.accepted() .body(new ReportResponse(reportId, "PROCESSING")); }}
// ワーカー(常駐プロセス環境)で処理@Componentpublic class ReportWorker { @Autowired private ReportService reportService;
@RabbitListener(queues = "report.generate") public void processReportGeneration(Map<String, Object> message) { String reportId = (String) message.get("reportId"); ReportRequest request = (ReportRequest) message.get("request");
// 長時間実行される処理(常駐プロセス環境で実行) Report report = reportService.generateLargeReport(request);
// 結果を保存 reportService.saveReport(reportId, report); }}なぜ正しいか:
- 即座にレスポンス: ユーザーは即座にレスポンスを受信
- 非同期処理: 長時間実行される処理は別プロセスで実行
- スケーラビリティ: ワーカーをスケールして処理能力を向上
3. 適切なエラーハンドリング
Section titled “3. 適切なエラーハンドリング”@Servicepublic class FileService { public void processFile(String filePath) { // ✅ 正しい: try-with-resources文を使用 try (FileInputStream fis = new FileInputStream(filePath); BufferedInputStream bis = new BufferedInputStream(fis)) {
// ファイル処理 byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = bis.read(buffer)) != -1) { // 処理... } } catch (FileNotFoundException e) { // 適切なエラーハンドリング log.error("File not found: " + filePath, e); throw new FileProcessingException("File not found: " + filePath, e); } catch (IOException e) { // 適切なエラーハンドリング log.error("IO error while processing file: " + filePath, e); throw new FileProcessingException("IO error: " + e.getMessage(), e); } }}なぜ正しいか:
- リソースの自動解放: try-with-resources文により自動的にリソースがクローズされる
- 適切なエラーハンドリング: エラーを適切に処理し、ログに記録
- エラーの伝播: 適切な例外をスローして呼び出し元に伝播
4. スレッドセーフティの確保
Section titled “4. スレッドセーフティの確保”@Servicepublic class CounterService { // ✅ 正しい: AtomicIntegerを使用 private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); // アトミック操作 }
public int getCount() { return count.get(); // スレッドセーフな読み取り }}
// または、synchronizedキーワードを使用@Servicepublic class CounterService { private int count = 0; private final Object lock = new Object();
public void increment() { synchronized (lock) { count++; } }
public int getCount() { synchronized (lock) { return count; } }}なぜ正しいか:
- アトミック操作:
AtomicIntegerによりアトミック操作が保証される - 可視性: 変更がすべてのスレッドに可視になる
- 競合状態の回避: 競合状態が発生しない
5. トランザクションの適切な管理
Section titled “5. トランザクションの適切な管理”@Servicepublic class OrderService { @Autowired private OrderRepository orderRepository;
@Autowired private InventoryService inventoryService;
@Autowired private PaymentService paymentService;
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) public Order createOrder(OrderData orderData) { // ✅ 正しい: トランザクション内でのデータベース操作のみ Order order = orderRepository.save(new Order(orderData));
// ✅ 正しい: 同じトランザクション内での在庫更新 inventoryService.reduceStock(order.getItems());
// ✅ 正しい: トランザクションコミット後に外部APIを呼ぶ TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCommit() { // トランザクションコミット後に外部APIを呼ぶ paymentService.chargePaymentAsync(order.getId(), orderData.getAmount()); } } );
return order; }}なぜ正しいか:
- トランザクションの短縮: データベース操作のみをトランザクション内で実行
- 外部APIの分離: 外部APIはトランザクション外で実行
- 一貫性の保証: トランザクションがコミットされた後にのみ外部APIを呼ぶ
ベストプラクティスのポイント:
- Outboxパターン: トランザクション内で外部API呼び出しを記録し、別プロセスで処理
- 非同期処理: Serverless環境では即座にレスポンスを返し、長時間処理は別プロセスで実行
- 適切なエラーハンドリング: try-with-resources文を使用し、適切なエラーハンドリングを実装
- スレッドセーフティ:
AtomicIntegerやsynchronizedキーワードを使用 - トランザクションの適切な管理: トランザクション内でのデータベース操作のみ、外部APIはトランザクション外で実行
適切なベストプラクティスの実装により、安全で信頼性の高いシステムを構築できます。