Skip to content

安全に壊れるための設計原則

「正常に動く」よりも「異常時に安全に壊れる」ことを優先する設計原則を詳しく解説します。

外部(API・DB・ユーザ入力)からのデータは常に汚染されていると仮定し、型・形式・範囲を検査してからロジックに渡す。

// ❌ 悪い例: 無防備な入力受付
@RestController
public class UserController {
@PostMapping("/users")
public User createUser(@RequestBody Map<String, Object> data) {
// 問題: 型チェックなし、バリデーションなし
String name = (String) data.get("name");
Integer age = (Integer) data.get("age");
return userService.createUser(name, age);
}
}
// ✅ 良い例: 境界防御の実装
@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
// バリデーション: Bean Validationで型・形式・範囲を検査
User user = userService.createUser(request);
return ResponseEntity.ok(user);
}
}
// DTOで境界を定義
public class CreateUserRequest {
@NotBlank
@Size(min = 1, max = 100)
private String name;
@NotNull
@Min(0)
@Max(150)
private Integer age;
@Email
private String email;
}

なぜ重要か:

  • 型安全性: コンパイル時に型エラーを検出
  • バリデーション: 実行時に形式・範囲を検査
  • セキュリティ: SQLインジェクション、XSSなどの攻撃を防止

DB更新・通知・外部呼出などの副作用をロジックの末尾に集約し、それ以前を状態を持たない純粋処理として保つ。

// ❌ 悪い例: 副作用が散在
@Service
public class OrderService {
public Order createOrder(OrderData orderData) {
// 副作用1: DB更新
Order order = orderRepository.save(new Order(orderData));
// ビジネスロジック(副作用が混在)
if (order.getAmount() > 10000) {
// 副作用2: 外部API呼び出し
notificationService.sendEmail(order.getUserId());
}
// 副作用3: 別のDB更新
auditLogRepository.save(new AuditLog("ORDER_CREATED", order.getId()));
return order;
}
}
// ✅ 良い例: 副作用の局所化
@Service
public class OrderService {
public Order createOrder(OrderData orderData) {
// 1. 純粋処理: ビジネスロジック(副作用なし)
Order order = validateAndCreateOrder(orderData);
// 2. 副作用の集約: すべての副作用を末尾に
persistOrder(order);
notifyIfNeeded(order);
auditOrderCreation(order);
return order;
}
// 純粋関数: 副作用なし
private Order validateAndCreateOrder(OrderData orderData) {
// バリデーションとオブジェクト作成のみ
if (orderData.getAmount() <= 0) {
throw new IllegalArgumentException("Invalid amount");
}
return new Order(orderData);
}
// 副作用: DB更新
private void persistOrder(Order order) {
orderRepository.save(order);
}
// 副作用: 通知
private void notifyIfNeeded(Order order) {
if (order.getAmount() > 10000) {
notificationService.sendEmail(order.getUserId());
}
}
// 副作用: 監査ログ
private void auditOrderCreation(Order order) {
auditLogRepository.save(new AuditLog("ORDER_CREATED", order.getId()));
}
}

なぜ重要か:

  • テスト容易性: 純粋関数は単体テストが容易
  • 可読性: 副作用が明確に分離される
  • デバッグ容易性: 副作用の発生箇所が明確

ビジネスロジックが特定ライブラリやORMの仕様に依存しないよう、インターフェース層で抽象化する。

// ❌ 悪い例: ORMに直接依存
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository; // JPAに直接依存
public Order findOrder(Long id) {
// JPAの仕様に依存
return orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
}
// ✅ 良い例: インターフェースで抽象化
// ドメイン層のインターフェース
public interface OrderRepository {
Order findById(Long id);
void save(Order order);
}
// インフラ層の実装
@Repository
public class JpaOrderRepository implements OrderRepository {
private final SpringDataOrderRepository springDataRepo;
@Override
public Order findById(Long id) {
return springDataRepo.findById(id)
.map(this::toDomain)
.orElseThrow(() -> new OrderNotFoundException(id));
}
}
// サービス層: ドメイン層のインターフェースに依存
@Service
public class OrderService {
private final OrderRepository orderRepository; // インターフェースに依存
public Order findOrder(Long id) {
return orderRepository.findById(id);
}
}

なぜ重要か:

  • 交換容易性: ORMを変更してもビジネスロジックは変更不要
  • テスト容易性: モックで簡単にテスト可能
  • 保守性: フレームワークの変更に強い

安全に壊れるための設計原則のポイント:

  • 境界防御: 外部データは常に汚染されていると仮定し、型・形式・範囲を検査
  • 副作用の局所化: 副作用をロジックの末尾に集約し、純粋処理と分離
  • 依存の隔離: ビジネスロジックが特定ライブラリに依存しないよう抽象化

これらの原則により、「異常時に安全に壊れる」堅牢なシステムを構築できます。