メインコンテンツへスキップ
  1. Posts/

なぜSpringのシングルトンBeanはステートレスでなければならないのか? スレッド、Entity、@Configurationまで

NineKoo9
著者
NineKoo9
目次

このドキュメントは、疑問を持っていた点をAIエージェントと対話しながら整理したメモです。Springシングルトン Beanがステートレスでなければならない理由と、ローカル変数、Entity、@Configurationの動作をまとめます。

なぜSpringのシングルトンBeanはステートレスでなければならないのか? スレッド、Entity、@Configurationまで
#

💡 Springのシングルトン Bean、JVMスレッド構造、DTO/Entityのライフサイクルに関する疑問と整理


1. シングルトンBeanはなぜステートレス(Stateless)でなければならないのか?
#

疑問
#

シングルトンはステートレスで使用しなければならない。マルチスレッドで動作すると値が変更される余地があるからだ。もし値を設定しておいて、特定のクライアントが値を変更できるフィールドがあってはならない。

回答
#

Springコンテナはデフォルトでは Beanをシングルトンとして管理する。正確には、通常Springコンテナ内でBean定義(bean definition)ごとに1つのインスタンスを共有するという意味であり、一般的なSpring MVCサーバーアプリケーションでは複数のリクエストスレッドがこのインスタンスを共有して使用する。

@Service
public class OrderService {

    // ❌ 絶対にこうしてはいけない - インスタンス変数 = 共有状態
    private int orderCount = 0;

    public void createOrder() {
        orderCount++; // スレッドAとBが同時にアクセスすると? -> レースコンディション
    }
}
  • スレッドAがorderCountを読んで1を加えようとしている間にスレッドBも同じ値を読んでしまうと、2回呼び出したのに1しか増えないという問題が発生する。
  • そのためシングルトン Beanには状態を変更できるインスタンスフィールドがあってはならない。
  • どうしても必要な場合はThreadLocalのような、より狭いスコープの保存方法を検討できる。ただしスレッドプール環境では後始末(remove)が必要だ。
@Service
public class UserContextHolder {

    // ✅ 各スレッドで独立した保存領域
    private static final ThreadLocal<Long> currentUserId = new ThreadLocal<>();

    public void set(Long userId) {
        currentUserId.set(userId);  // スレッドAがsetしてもスレッドBには影響しない
    }

    public Long get() {
        return currentUserId.get();
    }

    public void clear() {
        currentUserId.remove();
    }
}

核心まとめ
#

  • シングルトン Bean = 通常コンテナ内にインスタンスが1つ -> 複数のスレッドが共有
  • インスタンス変数に状態を保存するとレースコンディションが発生
  • 解決策: ステートレスに設計するか、やむを得ない場合はより狭いscopeやThreadLocalなどを検討

2. 一般的なドメインオブジェクトはなぜこのルールに当てはまらないのか?
#

疑問
#

私たちが一般的に定義したドメインはなぜ適用されないのか…? シングルトンで管理するものではないから、そうなのか。

回答
#

おおむね正しい。ドメインオブジェクト(Entity、VOなど)はSpringコンテナが管理する基本シングルトン Beanではない。 通常はアプリケーションコードがnewで生成するか、JPAがPersistence Context内で管理する。

// ドメインオブジェクト - Springが管理しない(シングルトンではない)
Order order = new Order(1L, "チキン", 20000);

// Spring Bean - シングルトンで管理される
@Service
public class OrderService { ... }

シングルトンで管理されるのはSpring IoC コンテナによってDI(依存性注入)されるものだ。@Component@Service@Repository@Controllerなどのアノテーションが付いたクラスがこれに該当する。

ただし「ドメインオブジェクトは必ずリクエストごとに生成されて、そのスレッドでのみ使われる」と一般化するのは過剰だ。実際のライフサイクルはどこで生成したか、どこに参照を保持しているかによって異なる。それでも一般的なWebリクエスト処理コードでは、Springシングルトン Beanのように共有されるオブジェクトとして扱わない場合がほとんどだ。


3. StringBufferを使わない理由 - メソッドローカル変数のスレッド安全性
#

疑問
#

StringBufferのように同期化が適用されたデータ構造を実際には使わないということは…、通常はメソッド内で1つのスレッドが処理するローカル変数として使用されるからなのか?

回答
#

正確だ。StringBuffersynchronizedがかかっているためマルチスレッドでは安全だが、Spring環境での文字列操作はほぼ必ずメソッド内ローカル変数として行われる。

@Service
public class ReportService {

    public String generateReport(List<Item> items) {
        // ✅ このsbはこのスレッドの実行フローでのみ使用される
        // 他のスレッドがアクセスする方法がない
        StringBuilder sb = new StringBuilder();
        for (Item item : items) {
            sb.append(item.getName()).append("\n");
        }
        return sb.toString();
    }
}
  • ローカル変数なのでそもそも共有されない。
  • より正確にはStringBuilderオブジェクトはヒープに生成されても、その参照が他のスレッドに共有されない。
  • 同期化する理由自体がないので、不要なsynchronizedオーバーヘッドのあるStringBufferよりもStringBuilderを使う。

4. シングルトンオブジェクトなのにメソッド呼び出しはなぜ安全なのか - JVMスタックの秘密
#

疑問
#

シングルトンオブジェクトだとしても1つだけ生成されるだけで、実行時には各JVMで各スレッドに割り当てられたスタックで演算が行われるから、そういうことなのか?

回答
#

正確だ。核心はJVMのメモリ構造だ。ただしもう少し正確に言うと、オブジェクト自体はヒープにあり、メソッドのローカル変数とパラメーターの参照が各スレッドのスタックフレームに置かれる。

┌──────────────────────────────────────────────┐
│                     Heap                      │
│                                               │
│   ┌────────────────┐                          │
│   │  OrderService   │  ← シングルトン、1つだけ  │
│   │  (インスタンス)  │                          │
│   └────────────────┘                          │
│                                               │
│   ┌──────┐  ┌──────┐  ┌──────┐               │
│   │DTO-1 │  │DTO-2 │  │DTO-3 │ ← リクエストごとに生成 │
│   └──────┘  └──────┘  └──────┘               │
└──────────────────────────────────────────────┘

┌───────────────┐    ┌───────────────┐
│   Thread A    │    │   Thread B    │
│   Stack       │    │   Stack       │
│ ┌───────────┐ │    │ ┌───────────┐ │
│ │createOrder│ │    │ │createOrder│ │
│ │price=1000 │ │    │ │price=2000 │ │
│ │dto (ref)  │ │    │ │dto (ref)  │ │
│ └───────────┘ │    │ └───────────┘ │
└───────────────┘    └───────────────┘
  • OrderServiceヒープ(Heap) に1つだけ存在する。
  • しかしcreateOrder()メソッドが呼び出されると、その中のローカル変数とパラメーターは各スレッドのスタックフレームに独立して生成される。
  • シングルトンオブジェクトだとしても、メソッド内ローカル変数はスレッドごとに隔離されているので、まったく問題ない。
@Service
public class OrderService {

    public OrderResponse createOrder(OrderRequest request) {
        // priceはスレッドAのスタックフレームでのみ使用される -> 安全
        int price = request.getPrice();

        // dtoオブジェクトはヒープに生成されるが、現在のスレッドの参照でのみ扱えば安全
        OrderDto dto = new OrderDto(price);

        return new OrderResponse(dto);
    }
}

5. WebリクエストスレッドはJVMスレッドと同じか?
#

疑問
#

このWebリクエスト用のスレッドとJVMのこのスレッドは同じものなのか…? 同じものなのか? それぞれコールスタックを持つ。

回答
#

従来の同期式Spring MVC/Servlet処理を基準にすると正しい。 Tomcatのスレッドプールにあるスレッドはjava.lang.Threadのインスタンスであり、それぞれJVMが管理する独立したコールスタックを持つ。

HTTPリクエスト到着
  → Tomcatスレッドプールからjava.lang.Threadを1つ割り当て
  → このThreadがFilter → DispatcherServlet → Controller → Service → Repositoryを呼び出す
  → すべてこのThreadのコールスタック上で実行
  → レスポンス返却後、Threadはプールに返却

Webサーバー(Tomcat)のスレッドとJVMのスレッドは別の存在ではなく、TomcatがJVMのスレッドをプールで管理してリクエストに割り当てる構造だ。

ただしこれは同期式リクエスト処理の説明として理解するのが正しい。

  • Servlet async(CallableDeferredResultWebAsyncTask)を使うと処理スレッドが変わりうる。
  • @AsyncやReactiveフローでは、1つのリクエストが複数のスレッドにまたがることがある。
  • そのためThreadLocalベースの状態は非同期境界を越えるときに追加の考慮が必要だ。

6. Entityは状態を変更するのになぜ問題ないのか?
#

疑問
#

Entityはどのように処理されるのか? 値を変更することもあるのに…、なぜ…? インスタンス変数として使われるんじゃないか? ああ…、単純にシングルトンじゃないからなのか。

回答
#

正しい。Entityはシングルトンではないため状態を変更しても問題ない。

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;
    private String status;  // インスタンス変数だが、シングルトンではないのでOK

    public void cancel() {
        this.status = "CANCELLED";  // このインスタンスの状態変更
    }
}
  • リクエストAで取得したOrderインスタンスとリクエストBで取得したOrderインスタンスは互いに異なるオブジェクトであるのが一般的だ。
  • 同じDB行(row)を取得しても、一般的なtransaction-scoped persistence contextでは、各トランザクションが別々のPersistence Contextを持つため、それぞれ異なるEntityインスタンスを扱うことになる。
  • 同時に同じ行を修正する同時実行の問題は、Entityのインスタンス共有の問題ではなく、DBレベルのロック/トランザクション分離の問題だ。

つまりEntityの状態変更自体は自然なことだ。注意すべきは**EntityManagerとPersistence ContextのThread-safetyを前提に共有してはならない**ということだ。


7. なぜController、Service、RepositoryはシングルトンでDTOとEntityはそうでないのか?
#

疑問
#

なぜRepositoryやController、Serviceなどはシングルトンで管理して、DTOのようなものやドメインオブジェクトは…? この基準は何なのか…?

回答
#

実務的によく使う基準は**「このオブジェクトが振る舞いを提供するのか、データを持つのか」だ。この区別は学習用としてかなり役に立つ。ただしより厳密なSpringの基準はBeanとして登録されているか、またどのscopeを持つか**だ。

シングルトンで管理するもの: 振る舞い(ロジック)中心のオブジェクト
#

@Controller   // シングルトン - リクエストをルーティングする「振る舞い」
@Service      // シングルトン - ビジネスロジックを実行する「振る舞い」
@Repository   // シングルトン - データアクセスを実行する「振る舞い」
@Component    // シングルトン - その他インフラロジック

これらの共通点は通常状態なしに振る舞いのみを提供することだ。リクエストが100個来てもOrderServicecreateOrder()ロジック自体は同じだ。だから1つだけ作っておいて共有しても問題ない。

シングルトンで管理してはいけないもの: データ(状態)中心のオブジェクト
#

// DTO - リクエストごとに異なるデータを持つ必要がある
OrderRequest request1 = new OrderRequest("チキン", 20000);  // スレッドAのリクエスト
OrderRequest request2 = new OrderRequest("ピザ", 30000);    // スレッドBのリクエスト

// Entity - それぞれ異なる行(row)を表現
Order order1 = new Order(1L, "チキン", 20000);
Order order2 = new Order(2L, "ピザ", 30000);

もしOrderRequestをシングルトンにしたら?

// ❌ 絶対にこうしてはいけない
@Component  // シングルトンで登録されると...
public class OrderRequest {
    private String itemName;
    private int price;

    // スレッドA: setItemName("チキン")
    // スレッドB: setItemName("ピザ")
    // スレッドA: getItemName() -> "ピザ" ???  -> チキンを注文したのにピザが出てくる
}

比較まとめ表
#

区分シングルトン Bean毎回生成するオブジェクト
本質振る舞いを提供データを持つ
状態ステートレス(stateless)状態が存在する(stateful)
Controller、Service、RepositoryDTO、Entity、VO
ライフサイクルアプリ起動〜終了リクエスト処理中または必要な時点にのみ存在
生成コスト一度だけ作って節約軽量で、リクエストごとに異なる必要があるため毎回生成

8. 毎回生成するとメモリの無駄遣いではないか?
#

疑問
#

シングルトンで管理されないものは継続的に生成される余地があるということだが…、DTOやドメインオブジェクトは…? このくらいは許容しようということなのか…? この基準は何なのか…?

回答
#

DTOやEntityはほとんどの場合、寿命が非常に短い。JVM GCはこのような短命(short-lived)オブジェクトを処理することに最適化されているため、リクエストごとにDTOをいくつか作るのはパフォーマンスにほぼ影響しない。

public OrderResponse createOrder(OrderRequest request) {
    Order order = Order.create(request);        // 生成
    orderRepository.save(order);                // 保存
    return OrderResponse.from(order);           // レスポンスDTO生成
}   // メソッド終了後参照されなくなるとGC対象
  • これらのオブジェクトは通常短時間で使われ、JVMのYoung Generation GCによって素早く回収される。
  • 一方Service、Repositoryのような Beanは内部に他のBeanへの参照、コネクションプール、設定値などを持っているため初期化コストがはるかに大きい。 こういったものをリクエストごとに作ったら本当に無駄だ。

不変オブジェクトにするとさらに良い
#

// ✅ 不変DTO - recordを使用(Java 16+)
public record OrderRequest(String itemName, int price) {}

// ✅ 不変DTO - クラスを使用
public class OrderResponse {
    private final Long orderId;
    private final String status;

    public OrderResponse(Long orderId, String status) {
        this.orderId = orderId;
        this.status = status;
    }

    // getterのみ提供、setterなし
}

不変オブジェクトは生成後に状態が変更されないため、たとえ共有されても安全だ。DTOをできる限り不変にするのが良い習慣だ。


9. @ConfigurationとCGLIBプロキシ - シングルトン保証の秘密
#

疑問
#

AppConfigはCGLIBを使ったプロキシオブジェクトなのか? @Beanだけを使うとSpring Beanとして登録はされるが、シングルトンは保証されない。

回答
#

@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository()); // memberRepository()を呼び出し
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository());  // memberRepository()をまた呼び出し
    }
}

Javaコードだけ見るとmemberRepository()が2回呼ばれるためインスタンスが2つ生成されそうだが、Springはデフォルトで@Configuration(proxyBeanMethods = true)クラスをCGLIBでプロキシする。

CGLIBプロキシの動作原理
#

memberRepository()呼び出し時:
  if (SpringコンテナにmemberRepository Beanがすでにあれば)
      return 既存のBean;       ← シングルトン保証
  else
      新たに生成 → コンテナに登録 → return;
  • memberService()orderService()が受け取るmemberRepository同じインスタンスだ。
  • SpringはAppConfigクラスをそのまま使わず、CGLIBライブラリでAppConfigを継承したプロキシクラスを動的に生成する。
  • このプロキシが@Beanメソッドをインターセプトして、すでにコンテナに登録された Beanがあれば既存のインスタンスを返す。

@Configurationなしで@Beanだけ使うと?
#

// ⚠️ @Configurationなしで@Beanだけを使用
@Component  // @Configurationではない
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public MemberService memberService() {
        // CGLIBプロキシがないので純粋なJavaメソッド呼び出し
        // -> ここでmemberRepository()を直接呼ぶと新しいオブジェクトが生成される可能性がある
        return new MemberServiceImpl(memberRepository());
    }
}
  • この場合も各@Beanが登録されるBean自体のデフォルトscopeは依然としてsingletonだ。
  • ただしCGLIBプロキシが適用されないため、同じ設定クラス内でmemberRepository()を直接呼び出すとコンテナを迂回する通常のメソッド呼び出しになる。
  • つまり厳密に言えば「シングルトンデフォルト自体がなくなる」というよりも、@Beanメソッド間の直接呼び出しに対するコンテナの保証が壊れるということだ。

最終まとめ: 一目でわかる核心要約
#

                        Springコンテナ(Heap)
                    ┌─────────────────────────────┐
    シングルトン Bean  │  Controller ─── Service ─── Repository  │
    (振る舞い中心、   │     (ステートレス、1つだけ存在、全スレッド共有)│
     @Component系)  │                                         │
                    └─────────────────────────────┘
                            ↑ 共有使用 ↑
                    ┌───────┴───────────┴───────┐
                    │                           │
            ┌───────────────┐           ┌───────────────┐
            │   Thread A    │           │   Thread B    │
            │   (リクエスト#1)│           │   (リクエスト#2)│
            │               │           │               │
            │  ローカル変数:  │           │  ローカル変数:  │
            │  - DTO-A      │           │  - DTO-B      │
            │  - Entity-A   │           │  - Entity-B   │
            │  (このスレッドのみ│           │  (このスレッドのみ│
            │   使用、GC対象) │           │   使用、GC対象) │
            └───────────────┘           └───────────────┘
                  データオブジェクト            データオブジェクト
                  (状態中心、               (状態中心、
                   毎回new生成)              毎回new生成)

核心をひとことで
#

Springがシングルトンで管理する基準は通常「共有しても良いロジック Beanかどうか」に近く、スレッド安全性の核心は結局、共有可変状態を持たないことだ。

疑問 → 結論の流れまとめ
#

  1. シングルトンはなぜステートレスなのか? -> すべてのスレッドが共有するため、状態を持つとレースコンディションが発生する
  2. ドメインオブジェクトはなぜ当てはまらないのか? -> Springの基本シングルトン Beanではないから
  3. StringBufferを使わない理由は? -> メソッド内ローカル変数として使用されるためそもそも共有されない
  4. シングルトンなのにメソッドが安全なのはなぜ? -> メソッド実行は各スレッドのスタック上で独立して行われる
  5. WebスレッドはJVMスレッドと同じか? -> 同期式MVCでは概ね同じだ。ただしasync/reactiveまで一般化してはいけない
  6. Entityは状態変更しても問題ないのか? -> シングルトンではないので問題なく、DBの同時実行性はトランザクションとロックで解決する
  7. シングルトンの基準は? -> 実務的には振る舞いオブジェクト(Serviceなど)vsデータオブジェクト(DTO、Entity)の区別が有用で、厳密にはBean登録の有無とscopeが基準
  8. 毎回生成すると無駄なのか? -> 短命オブジェクトはYoung Gen GCに最適化されている。不変にするとさらに良い
  9. @ConfigurationのCGLIBは? -> @Beanメソッド間の直接呼び出しでもコンテナのセマンティクスを維持させる

参考資料
#