Skip to content

アンチパターン

Spring Bootアプリケーションのアンチパターン

Section titled “Spring Bootアプリケーションのアンチパターン”

アンチパターンは、一般的に見られるが、実際には問題を引き起こす設計や実装パターンです。この章では、Spring Bootアプリケーションでよく見られるアンチパターンと、その解決方法について解説します。

@Service
public class UserService {
@Autowired
private OrderService orderService;
public void createUser() {
// ...
}
}
@Service
public class OrderService {
@Autowired
private UserService userService;
public void createOrder() {
// ...
}
}

問題点:

  • Beanの初期化時に循環参照が発生し、エラーになる可能性がある
  • コードの結合度が高くなる
  • テストが困難になる

方法1: コンストラクタインジェクションを避けて、セッターインジェクションまたは@Lazyを使用

@Service
public class UserService {
private OrderService orderService;
@Autowired
@Lazy // 遅延初期化で循環参照を回避
public void setOrderService(OrderService orderService) {
this.orderService = orderService;
}
}

方法2: インターフェースを使用して依存関係を分離

public interface UserServiceInterface {
void createUser();
}
@Service
public class UserService implements UserServiceInterface {
@Autowired
private OrderService orderService;
@Override
public void createUser() {
// ...
}
}
@Service
public class OrderService {
@Autowired
private UserServiceInterface userService; // インターフェースに依存
public void createOrder() {
// ...
}
}

方法3: イベントを使用して依存関係を解消

@Service
public class UserService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void createUser() {
// ユーザー作成処理
eventPublisher.publishEvent(new UserCreatedEvent(userId));
}
}
@Service
public class OrderService {
@EventListener
public void handleUserCreated(UserCreatedEvent event) {
// イベントを受け取って処理
}
}

2. 過度な@Autowired(Field Injection)

Section titled “2. 過度な@Autowired(Field Injection)”
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@Autowired
private NotificationService notificationService;
// 多くの依存関係がフィールドインジェクションで注入されている
}

問題点:

  • テストが困難(モックの注入が難しい)
  • 依存関係が不明確
  • finalフィールドにできない(不変性が保証されない)

コンストラクタインジェクションを使用(推奨)

@Service
public 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
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
private final OrderService orderService;
}
@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. トランザクション境界の誤り”
@Service
public 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
@Transactional
public 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
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
public void createUser(User user) {
userRepository.save(user);
emailService.sendWelcomeEmail(user.getEmail());
}
}
@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);
}
}

問題点:

  • 読み取り専用のメソッドにもトランザクションが適用される(オーバーヘッド)
  • 不要なトランザクション管理
@Service
public 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);
}
}
@Service
@Transactional
public 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
@Transactional
public 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);
}
}
}
@Service
public 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());
}
}

問題点:

  • データベースへのクエリが大量に発生する
  • パフォーマンスが大幅に低下する
@Service
public 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());
}
}
@Repository
public 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();
}
@Service
@Transactional
public class PaymentService {
public void processPayment(PaymentRequest request) {
try {
// 決済処理
paymentGateway.charge(request);
} catch (PaymentGatewayException e) {
// チェック例外をラップして再スロー
throw new RuntimeException(e); // 非推奨
}
}
}

問題点:

  • 元の例外情報が失われる可能性がある
  • 適切な例外型が使用されない
@Service
@Transactional
public 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);
}
}
@Service
public 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;
}
}

問題点:

  • スレッドセーフではない
  • メモリリークの可能性
  • テストが困難
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Cacheable("users")
public User findUser(String id) {
return userRepository.findById(id).orElseThrow();
}
}
// または、Spring Cacheを使用
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users");
}
}
@Service
public 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;
// 多くの依存関係を持つ巨大なサービスクラス
}

問題点:

  • 単一責任の原則に違反
  • テストが困難
  • 保守性が低い
// 責務を分離
@Service
public 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;
}
}
// 別のサービスクラスに分離
@Service
public class OrderNotificationService {
private final EmailService emailService;
private final SmsService smsService;
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
emailService.sendOrderConfirmation(event.getOrder());
smsService.sendOrderNotification(event.getOrder());
}
}

主なアンチパターンと解決方法:

  1. 循環参照: @Lazy、インターフェース、イベントを使用
  2. 過度な@Autowired: コンストラクタインジェクションを使用
  3. 不適切なスコープ: 用途に応じた適切なスコープを選択
  4. トランザクション境界の誤り: プロキシ経由の呼び出しまたはサービスの分離
  5. 過度な@Transactional: 必要な箇所にのみ適用
  6. 例外の不適切な処理: 適切な例外型で再スロー
  7. N+1問題: JOIN FETCHや@EntityGraphを使用
  8. 不適切な例外の再スロー: 適切な例外型を使用
  9. グローバル変数: Spring Cacheを使用
  10. 過度な依存関係: 責務の分離とサービスの分割

これらのアンチパターンを避けることで、保守性が高く、テストしやすいアプリケーションを構築できます。