アンチパターン
Spring Bootアプリケーションのアンチパターン
Section titled “Spring Bootアプリケーションのアンチパターン”アンチパターンは、一般的に見られるが、実際には問題を引き起こす設計や実装パターンです。この章では、Spring Bootアプリケーションでよく見られるアンチパターンと、その解決方法について解説します。
1. 循環参照(Circular Dependency)
Section titled “1. 循環参照(Circular Dependency)”問題のコード
Section titled “問題のコード”@Servicepublic class UserService { @Autowired private OrderService orderService;
public void createUser() { // ... }}
@Servicepublic class OrderService { @Autowired private UserService userService;
public void createOrder() { // ... }}問題点:
- Beanの初期化時に循環参照が発生し、エラーになる可能性がある
- コードの結合度が高くなる
- テストが困難になる
方法1: コンストラクタインジェクションを避けて、セッターインジェクションまたは@Lazyを使用
@Servicepublic class UserService { private OrderService orderService;
@Autowired @Lazy // 遅延初期化で循環参照を回避 public void setOrderService(OrderService orderService) { this.orderService = orderService; }}方法2: インターフェースを使用して依存関係を分離
public interface UserServiceInterface { void createUser();}
@Servicepublic class UserService implements UserServiceInterface { @Autowired private OrderService orderService;
@Override public void createUser() { // ... }}
@Servicepublic class OrderService { @Autowired private UserServiceInterface userService; // インターフェースに依存
public void createOrder() { // ... }}方法3: イベントを使用して依存関係を解消
@Servicepublic class UserService { @Autowired private ApplicationEventPublisher eventPublisher;
public void createUser() { // ユーザー作成処理 eventPublisher.publishEvent(new UserCreatedEvent(userId)); }}
@Servicepublic class OrderService { @EventListener public void handleUserCreated(UserCreatedEvent event) { // イベントを受け取って処理 }}2. 過度な@Autowired(Field Injection)
Section titled “2. 過度な@Autowired(Field Injection)”問題のコード
Section titled “問題のコード”@Servicepublic class UserService { @Autowired private UserRepository userRepository;
@Autowired private EmailService emailService;
@Autowired private OrderService orderService;
@Autowired private PaymentService paymentService;
@Autowired private NotificationService notificationService;
// 多くの依存関係がフィールドインジェクションで注入されている}問題点:
- テストが困難(モックの注入が難しい)
- 依存関係が不明確
- finalフィールドにできない(不変性が保証されない)
コンストラクタインジェクションを使用(推奨)
@Servicepublic class UserService { private final UserRepository userRepository; private final EmailService emailService; private final OrderService orderService;
// コンストラクタインジェクション(推奨) public UserService( UserRepository userRepository, EmailService emailService, OrderService orderService) { this.userRepository = userRepository; this.emailService = emailService; this.orderService = orderService; }}
// Lombokを使用した簡潔な記述@Service@RequiredArgsConstructorpublic class UserService { private final UserRepository userRepository; private final EmailService emailService; private final OrderService orderService;}3. 不適切なスコープの使用
Section titled “3. 不適切なスコープの使用”問題のコード
Section titled “問題のコード”@Component@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)public class SingletonDataHolder { private Map<String, Object> data = new HashMap<>();
public void setData(String key, Object value) { data.put(key, value); }
public Object getData(String key) { return data.get(key); }}問題点:
- プロトタイプスコープなのに状態を保持している
- スレッドセーフではない
- データの整合性が保証されない
// 状態を保持する場合はシングルトンにする@Component@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)public class SingletonDataHolder { private final ConcurrentHashMap<String, Object> data = new ConcurrentHashMap<>();
public void setData(String key, Object value) { data.put(key, value); }
public Object getData(String key) { return data.get(key); }}
// または、スレッドローカルを使用@Component@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)public class ThreadLocalDataHolder { private final ThreadLocal<Map<String, Object>> data = ThreadLocal.withInitial(HashMap::new);
public void setData(String key, Object value) { data.get().put(key, value); }
public void clear() { data.remove(); }}4. トランザクション境界の誤り
Section titled “4. トランザクション境界の誤り”問題のコード
Section titled “問題のコード”@Servicepublic class UserService { @Autowired private UserRepository userRepository;
public void createUser(User user) { userRepository.save(user); sendWelcomeEmail(user.getEmail()); // トランザクション外で実行される }
@Transactional public void sendWelcomeEmail(String email) { // メール送信処理 }}問題点:
- 同じクラス内のメソッド呼び出しでは
@Transactionalが効かない - トランザクションが適切に管理されない
@Service@Transactionalpublic class UserService { @Autowired private UserRepository userRepository;
@Autowired private ApplicationContext applicationContext;
public void createUser(User user) { userRepository.save(user); // プロキシ経由で呼び出すことでトランザクションが効く UserService self = applicationContext.getBean(UserService.class); self.sendWelcomeEmail(user.getEmail()); }
@Transactional public void sendWelcomeEmail(String email) { // メール送信処理 }}
// または、別のサービスクラスに分離@Service@Transactionalpublic class UserService { @Autowired private UserRepository userRepository;
@Autowired private EmailService emailService;
public void createUser(User user) { userRepository.save(user); emailService.sendWelcomeEmail(user.getEmail()); }}5. 過度な@Transactional
Section titled “5. 過度な@Transactional”問題のコード
Section titled “問題のコード”@Service@Transactional // すべてのメソッドにトランザクションが適用されるpublic class UserService {
@Transactional(readOnly = true) // 読み取り専用メソッド public User findById(Long id) { return userRepository.findById(id).orElse(null); }
@Transactional(readOnly = true) public List<User> findAll() { return userRepository.findAll(); }
@Transactional(readOnly = true) public boolean existsById(Long id) { return userRepository.existsById(id); }}問題点:
- 読み取り専用のメソッドにもトランザクションが適用される(オーバーヘッド)
- 不要なトランザクション管理
@Servicepublic class UserService { @Autowired private UserRepository userRepository;
// 読み取り専用メソッドには@Transactionalを付けない public User findById(Long id) { return userRepository.findById(id).orElse(null); }
public List<User> findAll() { return userRepository.findAll(); }
// 書き込みメソッドにのみ@Transactionalを付ける @Transactional public User createUser(User user) { return userRepository.save(user); }
@Transactional public User updateUser(User user) { return userRepository.save(user); }}6. 例外の不適切な処理
Section titled “6. 例外の不適切な処理”問題のコード
Section titled “問題のコード”@Service@Transactionalpublic class UserService { @Autowired private UserRepository userRepository;
public User createUser(User user) { try { return userRepository.save(user); } catch (Exception e) { // すべての例外をキャッチしてログに記録するだけ log.error("Error creating user", e); return null; // nullを返す(非推奨) } }}問題点:
- 例外が隠蔽される
- 呼び出し元がエラーを検知できない
- トランザクションがロールバックされない可能性がある
@Service@Transactionalpublic class UserService { @Autowired private UserRepository userRepository;
public User createUser(User user) { try { return userRepository.save(user); } catch (DataIntegrityViolationException e) { // データ整合性エラーは適切な例外に変換 throw new DuplicateResourceException("User", user.getEmail(), e); } catch (Exception e) { // 予期しないエラーはログに記録して再スロー log.error("Unexpected error creating user", e); throw new UserCreationException("Failed to create user", e); } }}7. N+1問題の発生
Section titled “7. N+1問題の発生”問題のコード
Section titled “問題のコード”@Servicepublic class OrderService { @Autowired private OrderRepository orderRepository;
@Autowired private UserRepository userRepository;
public List<OrderDTO> getAllOrders() { List<Order> orders = orderRepository.findAll(); return orders.stream() .map(order -> { // 各OrderごとにUserを取得(N+1問題) User user = userRepository.findById(order.getUserId()) .orElseThrow(); return convertToDTO(order, user); }) .collect(Collectors.toList()); }}問題点:
- データベースへのクエリが大量に発生する
- パフォーマンスが大幅に低下する
@Servicepublic class OrderService { @Autowired private OrderRepository orderRepository;
public List<OrderDTO> getAllOrders() { // JOIN FETCHを使用して一度に取得 List<Order> orders = orderRepository.findAllWithUser(); return orders.stream() .map(this::convertToDTO) .collect(Collectors.toList()); }}
@Repositorypublic interface OrderRepository extends JpaRepository<Order, Long> { @Query("SELECT o FROM Order o JOIN FETCH o.user") List<Order> findAllWithUser();
// または@EntityGraphを使用 @EntityGraph(attributePaths = {"user"}) List<Order> findAll();}8. 不適切な例外の再スロー
Section titled “8. 不適切な例外の再スロー”問題のコード
Section titled “問題のコード”@Service@Transactionalpublic class PaymentService { public void processPayment(PaymentRequest request) { try { // 決済処理 paymentGateway.charge(request); } catch (PaymentGatewayException e) { // チェック例外をラップして再スロー throw new RuntimeException(e); // 非推奨 } }}問題点:
- 元の例外情報が失われる可能性がある
- 適切な例外型が使用されない
@Service@Transactionalpublic class PaymentService { public void processPayment(PaymentRequest request) { try { paymentGateway.charge(request); } catch (PaymentGatewayException e) { // 適切な例外型で再スロー throw new PaymentProcessingException("Payment processing failed", e); } }}
// カスタム例外クラスpublic class PaymentProcessingException extends RuntimeException { public PaymentProcessingException(String message, Throwable cause) { super(message, cause); }}9. グローバル変数の使用
Section titled “9. グローバル変数の使用”問題のコード
Section titled “問題のコード”@Servicepublic class UserService { private static Map<String, Object> cache = new HashMap<>(); // 非推奨
public User findUser(String id) { if (cache.containsKey(id)) { return (User) cache.get(id); } User user = userRepository.findById(id).orElseThrow(); cache.put(id, user); return user; }}問題点:
- スレッドセーフではない
- メモリリークの可能性
- テストが困難
@Servicepublic class UserService { @Autowired private UserRepository userRepository;
@Cacheable("users") public User findUser(String id) { return userRepository.findById(id).orElseThrow(); }}
// または、Spring Cacheを使用@Configuration@EnableCachingpublic class CacheConfig { @Bean public CacheManager cacheManager() { return new ConcurrentMapCacheManager("users"); }}10. 過度な依存関係
Section titled “10. 過度な依存関係”問題のコード
Section titled “問題のコード”@Servicepublic class OrderService { @Autowired private UserRepository userRepository; @Autowired private ProductRepository productRepository; @Autowired private PaymentRepository paymentRepository; @Autowired private ShippingRepository shippingRepository; @Autowired private NotificationRepository notificationRepository; @Autowired private EmailService emailService; @Autowired private SmsService smsService; @Autowired private PushNotificationService pushNotificationService;
// 多くの依存関係を持つ巨大なサービスクラス}問題点:
- 単一責任の原則に違反
- テストが困難
- 保守性が低い
// 責務を分離@Servicepublic class OrderService { private final OrderRepository orderRepository; private final OrderValidator orderValidator; private final OrderEventPublisher eventPublisher;
public OrderService( OrderRepository orderRepository, OrderValidator orderValidator, OrderEventPublisher eventPublisher) { this.orderRepository = orderRepository; this.orderValidator = orderValidator; this.eventPublisher = eventPublisher; }
@Transactional public Order createOrder(OrderCreateRequest request) { orderValidator.validate(request); Order order = orderRepository.save(convertToEntity(request)); eventPublisher.publishOrderCreated(order); return order; }}
// 別のサービスクラスに分離@Servicepublic class OrderNotificationService { private final EmailService emailService; private final SmsService smsService;
@EventListener public void handleOrderCreated(OrderCreatedEvent event) { emailService.sendOrderConfirmation(event.getOrder()); smsService.sendOrderNotification(event.getOrder()); }}主なアンチパターンと解決方法:
- 循環参照: @Lazy、インターフェース、イベントを使用
- 過度な@Autowired: コンストラクタインジェクションを使用
- 不適切なスコープ: 用途に応じた適切なスコープを選択
- トランザクション境界の誤り: プロキシ経由の呼び出しまたはサービスの分離
- 過度な@Transactional: 必要な箇所にのみ適用
- 例外の不適切な処理: 適切な例外型で再スロー
- N+1問題: JOIN FETCHや@EntityGraphを使用
- 不適切な例外の再スロー: 適切な例外型を使用
- グローバル変数: Spring Cacheを使用
- 過度な依存関係: 責務の分離とサービスの分割
これらのアンチパターンを避けることで、保守性が高く、テストしやすいアプリケーションを構築できます。