Spring BeanはSingletonだから、状態持たせるとバグるよ。


Spring Beanに関するハマりポイントの一つです。

SpringでBean指定したクラスは、デフォルトでは暗黙的にSingletonでインスタンスが生成されます。なので、ここに状態を持たせると複数のスレッド間で状態が共有されて思わぬバグの原因になります。

これに対処する方法は2つあります。

そもそも状態を持たせないような設計にする

「同じ入力に対してはいつも同じ出力を返す」ようにクラスを設計しましょう。

SpringのBeanのデフォルトがSingletonなのも、こういった設計を前提にしているからだと思われます。

「同じ入力に対していつも同じ出力を返す」メソッドを「関数的」といったりしますが、こちらのほうがテストしやすくメンテナンス性が高まります。

基本的に、後述する「逐一インスタンスを作成する」場合よりもSingletonのほうがパフォーマンス上優れています。(ただし、アプリケーション起動時に一斉にインスタンスを作成してDIコンテナに突っ込むので、起動だけ少し遅くなります。)

コンポーネントのライフサイクルを指定する

@Scopeアノテーションを付与することで、Singleton以外のライフサイクルでBeanを管理することができます。

コンポーネントのライフサイクルを指定するには以下のように記述します。

例えば、ScopeがPrototypeであれば、そのクラスのメソッドが呼ばれた時にインスタンスを生成してDIします。


@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE) //このクラスのメソッドが呼ばれるたびに、インスタンスを再作成
public class AwesomeService{

  private StatefulSomething something;

  public Awesome makeAwesome(YourRequest req){
    //処理
  }

}

Requestスコープでインスタンスを使いまわしたい場合は、@Scopeに以下のように指定します。


@Service
@Scope(WebApplicationContext.SCOPE_REQUEST) //リクエストごとにインスタンスを使い回す
public class AwesomeService{

  private StatefulSomething something;

  public Awesome makeAwesome(YourRequest req){
    //処理
  }

}

同一Sessionで使い回す場合はこちら。


@Service
@Scope(WebApplicationContext.SCOPE_SESSION) //sessionが保持されていれば、このインスタンスを使い回す
public class AwesomeService{

  private StatefulSomething something;

  public Awesome makeAwesome(YourRequest req){
    //処理
  }

}

これらの場合、Singletonよりも実行時のパフォーマンスが落ちることに留意しましょう。(前述したとおり、あるタイミングでインスタンスを作成する処理が入るからですね)

また、singletonのBeanに対してsessionやrequestのBeanをDIしようとすると起動時にエラーになることに注意しましょう。

理由は簡単で、Singletonインスタンスはアプリケーション起動時に生成され、そのインスタンスに必要な依存インスタンスをインジェクションしようとしますが、sessionやrequestのBeanはこのアプリケーション起動時点でインスタンスを生成していないため、インジェクションに失敗します。

Singleton以外のライフサイクルでBeanを管理するときは、パフォーマンス、状態管理、クラス間の依存関係に十分注意しましょう。

基本的に、「まず状態を持たせない実装にするように設計にこだわる。どうしてもどうしても無理な場合、上述した@Scopeの利用を考える」という方針が良さそうです。