JavaのWebアプリケーション(特にSpring Bootなどのフレームワークを使用したアプリ)でクラスのフィールド変数を使用してはいけない理由について説明します。
1. スレッドセーフではない¶
問題点¶
JavaのWebアプリケーションは複数のユーザーからのリクエストを並行処理します。そのため、1つのクラスインスタンスが複数のスレッドで共有される可能性があります。
クラスのフィールド変数を使用すると、異なるリクエストが同じ変数を上書きしてしまうため、意図しないデータの書き換えや不具合が発生する可能性があります。
具体例¶
import org.springframework.stereotype.Service;
@Service
public class MyService {
private String message; // フィールド変数 (スレッドセーフでない)
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return this.message;
}
}
このクラスの問題点¶
-
MyServiceはSpringによって **シングルトン(1つのインスタンスが共有)**として管理される - ユーザーAが
setMessage("Hello")を呼び出した直後に、ユーザーBがsetMessage("World")を呼び出すと、ユーザーAも"World"を取得する可能性がある
→ 別のユーザーのデータを誤って取得する「スレッド競合」が発生する
解決策¶
フィールド変数をメソッド内のローカル変数にするか、スレッドごとのスコープを適用する。
import org.springframework.stereotype.Service;
@Service
public class MyService {
public String processMessage(String input) {
String message = input; // ローカル変数を使用
return "Processed: " + message;
}
}
ローカル変数はメソッドごとにスタックに積まれ、スレッドごとに独立しているため、安全に使用できます。
2. SingletonスコープのBeanで状態を持つと危険¶
Spring Bootの @Service、@Component、@Repository などのクラスはデフォルトで Singleton(アプリ全体で1つのインスタンスのみ)です。
このため、フィールド変数にデータを持つと全ユーザーに影響を与えてしまう可能性があります。
例¶
import org.springframework.stereotype.Service;
@Service
public class CounterService {
private int counter = 0; // フィールド変数 (スレッドセーフでない)
public int increment() {
return ++counter; // 別のリクエストが同じ counter を変更する可能性あり
}
}
問題点¶
- 複数のリクエストが
increment()を呼び出すと、異なるユーザーのカウンタ値が共有される - あるユーザーが
counter=1を取得した直後に、別のユーザーがcounter=2を取得する可能性がある(意図しない動作)
解決策¶
カウンターをスレッドごとに分けるために ThreadLocal を使用するか、状態を持たない設計にする。
ThreadLocal を使用¶
import org.springframework.stereotype.Service;
@Service
public class CounterService {
private final ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0);
public int increment() {
counter.set(counter.get() + 1);
return counter.get();
}
}
これでスレッドごとに異なるカウンタが管理されます。
3. マルチスレッド環境で競合が発生する¶
Webアプリケーションでは、複数のリクエストが同時に同じ Service や Controller を実行することがあります。
フィールド変数を使うと、マルチスレッド環境で競合が発生する可能性があります。
例:スレッド競合の発生¶
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private String orderStatus; // フィールド変数 (競合が発生)
public void processOrder(String status) {
this.orderStatus = status; // 別スレッドが上書きする可能性あり
}
public String getOrderStatus() {
return orderStatus; // 意図しない値が返る可能性あり
}
}
競合の例¶
- ユーザーAが
processOrder("Pending")を実行 - ユーザーBが
processOrder("Completed")を実行 - ユーザーAが
getOrderStatus()を実行すると、"Completed"(ユーザーBの値)が返る可能性がある
解決策
- ローカル変数を使用する
-
スレッドごとのスコープ(
@RequestScope)を使用する
import org.springframework.stereotype.Service;
import org.springframework.web.context.annotation.RequestScope;
@Service
@RequestScope
public class OrderService {
private String orderStatus;
public void processOrder(String status) {
this.orderStatus = status;
}
public String getOrderStatus() {
return orderStatus;
}
}
-
@RequestScopeをつけることで、リクエストごとにOrderServiceのインスタンスが生成されるため、別のリクエスト間で値が競合しなくなる
結論¶
JavaのWebアプリケーションでクラスのフィールド変数を使うのは危険です。その理由は:
- スレッドセーフではない(異なるリクエストでデータが競合)
- Singleton Beanに状態を持たせると影響範囲が広がる
- マルチスレッド環境でデータ競合が発生する
推奨される解決策¶
| 方法 | 解決策 |
|---|---|
| ローカル変数を使う | メソッド内の変数はスレッドごとに分離される |
@RequestScope を使う |
Bean をリクエストごとに生成して競合を防ぐ |
ThreadLocal を使う |
スレッドごとに異なる値を管理 |
| 状態を持たない設計にする |
Service クラスは基本的にステートレスに設計する |
特に @Service や @Component ではフィールド変数を持たない設計がベストプラクティスです。
安全な設計を心がけましょう!