【Spring Boot】Spring Bootの循環参照エラーの原因と解決法を中級者向けに解説

Java

こんにちは、かつコーチです。

サービスクラスを増やしていくと、ある日突然アプリが起動しなくなり、BeanCurrentlyInCreationExceptionという見慣れないエラーに出会うことがあります。

これは循環参照(AがBに依存し、BがAに依存するというように、依存関係が輪になってしまう状態)が原因のケースがほとんどです。

今回は、循環参照がなぜ問題になるのか、実際のエラーメッセージを見ながら解決法を整理します。

循環参照エラーとは

エラーメッセージの実例

僕がこのエラーに実際にぶつかったのは、注文処理と在庫管理を別サービスに切り出したときでした。

org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'orderService': Requested bean is currently
in creation: Is there an unresolvable circular reference?

OrderServiceStockServiceをコンストラクタで受け取り、StockServiceOrderServiceをコンストラクタで受け取るように書いていたのが原因でした。

@Service
public class OrderService {
    private final StockService stockService;

    public OrderService(StockService stockService) {
        this.stockService = stockService;
    }
}

@Service
public class StockService {
    private final OrderService orderService;

    public StockService(OrderService orderService) {
        this.orderService = orderService;
    }
}

なぜコンストラクタインジェクションだと検知されるのか

Spring BootのDIコンテナは、Beanを生成する順番を解決しながらアプリケーションコンテキストを組み立てます。

OrderServiceを作るには先にStockServiceが必要で、StockServiceを作るには先にOrderServiceが必要という状態になり、どちらを先に作ればいいか決められなくなります。

コンストラクタインジェクション(コンストラクタの引数として依存を受け取る方式)では、インスタンス生成の時点で依存先が完成している必要があるため、この矛盾がそのままエラーとして検知されます。

一方、フィールドインジェクション(@Autowiredをフィールドに直接付ける方式)では、先にインスタンスの器だけを作ってから後から依存を注入できるため、循環参照があってもエラーにならず起動してしまうことがあります。

これは一見ラッキーに見えますが、実際には設計上の問題を隠してしまっているだけで、根本的な解決にはなりません。

循環参照の解決法

解決策1:責務を見直して依存の方向を一方向にする

最も本質的な解決策は、そもそも循環参照が起きないように設計を見直すことです。

// ❌Before:OrderServiceとStockServiceが互いに依存し合う
@Service
public class OrderService {
    private final StockService stockService;
    // ...
}

@Service
public class StockService {
    private final OrderService orderService; // 循環の原因
    // ...
}

在庫を減らす処理をStockService側だけの責務にして、OrderServiceから一方的に呼び出す形に整理すると、依存の向きが一方向になります。

// ✅After:OrderServiceからStockServiceへの一方向の依存にする
@Service
public class OrderService {
    private final StockService stockService;

    public OrderService(StockService stockService) {
        this.stockService = stockService;
    }

    public void placeOrder(Order order) {
        stockService.decrease(order.getProductId(), order.getQuantity());
    }
}

@Service
public class StockService {
    // OrderServiceには依存しない
    public void decrease(Long productId, int quantity) {
        // 在庫を減らす処理
    }
}

循環参照は、多くの場合「1つのサービスに責務を詰め込みすぎている」というサインでもあります。

エラーの解消だけでなく、設計の見直しの機会として捉えるのがおすすめです。

解決策2:共通処理を第3のクラスに切り出す

どうしても双方向のやり取りが必要に見える場合、実は両方が依存している処理を第3のサービスに切り出せることが多いです。

@Service
public class InventoryCoordinator {
    private final StockRepository stockRepository;
    private final OrderRepository orderRepository;

    // OrderServiceとStockServiceの両方から呼ばれる処理をここに集約する
    public void syncOrderAndStock(Order order) {
        // ...
    }
}

OrderServiceStockServiceはそれぞれInventoryCoordinatorに依存するだけになり、循環がなくなります。

解決策3:@Lazyで遅延解決する(応急処置)

設計をすぐには見直せない事情がある場合、@Lazyを使う方法もあります。

@Service
public class StockService {
    private final OrderService orderService;

    public StockService(@Lazy OrderService orderService) {
        this.orderService = orderService;
    }
}

@Lazyを付けると、実際にそのBeanが使われるタイミングまで生成を遅らせるため、生成順序の矛盾を回避できます。

ただし、これはあくまで応急処置であり、循環参照という設計上の問題自体は残ったままです。

僕は現場でこの方法を一時的に使ったことがありますが、後日レイヤードアーキテクチャを見直すきっかけとして、責務の再分割を行いました。

応用・一歩先の使い方

setter/フィールドインジェクションで隠れた循環参照に気づく方法

前述の通り、フィールドインジェクションでは循環参照があってもエラーにならず起動してしまうことがあります。

これを未然に防ぐには、application.propertiesで循環参照自体を禁止する設定が有効です。

spring.main.allow-circular-references=false

Spring Boot 2.6以降はこの設定がデフォルトでfalse(禁止)になっているため、意図せず循環参照を作ってしまった場合はコンストラクタインジェクションでなくても起動時にエラーとして検知できます。

古いプロジェクトを引き継いだ場合は、この設定がtrueに上書きされていないか確認してみるとよいでしょう。

まとめ

この記事のポイント

  • 循環参照はコンストラクタインジェクションを使うとBeanCurrentlyInCreationExceptionとして検知される
  • フィールドインジェクションは循環参照を隠してしまうことがあるため注意する
  • 根本解決は責務の見直し、または共通処理の第3クラスへの切り出し
  • @Lazyは応急処置であり、設計の見直しに代わるものではない
  • spring.main.allow-circular-references=false(デフォルト)で早期検知できる

次に読むべき記事

  • コンストラクタインジェクションと@Autowiredの使い分け
  • レイヤードアーキテクチャの考え方
  • よくあるSpring Bootのエラーまとめ(BeanCreationException等)

タグ: Spring Boot, 中級者向け, エラー解決

タイトルとURLをコピーしました