Spring Bootでstatic変数とインスタンス変数の使い分けで迷ったので少し検証した。Spring Bootで以下のカウントするコードを作成した。curl http://localhost:8080/counterを実行すれば、カウント数が返ってくる簡易な関数。

package dev.itboot.mb.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CounterController {

    private static int staticCounter = 0;
    private int instanceCounter = 0;

    @GetMapping("/counter")
    public synchronized String getSum() {
        staticCounter++;
        instanceCounter++;
        return "Static Counter: " + staticCounter + "\n" +
               "Instance Counter: " + instanceCounter;
    }
}

staticはクラスで1つなのでリクエストするたびに加算されていき、インスタンス変数はリクエストするたびに一回初期化される。そのため、static変数は常に+1され、インスタンス変数はいつでも1が表示されると思っていた。でも結果は以下。

リクエスト実行するたびにstatic Counter: 1 Instance Counter: 1、static Counter: 2 Instance Counter: 2、static Counter: 3 Instance Counter: 3という具合に両方とも加算されていった。通常、static変数はクラスに1つなので誰からアクセスが来ても、どんなブラウザからアクセスが来ても1つの変数。だからstatic変数の挙動は正しい。

static Counter: 1 Instance Counter: 1、static Counter: 2Instance Counter: 1、static Counter: 3 Instance Counter: 1のような結果を想定していた。データの持たせ方をちょっと調べ直した。

Spring Bootはシングルトン

Spring Bootのデフォルトはシングルトンということ。シングルトンとはそのクラスのインスタンスが1つしか作成されないパターン。なので1度作成されてしまうとinstanceCounterも常にプラスされていく。カウンターの挙動はstatic変数と同じ。

もしリクエストごとにインスタンスを生成したいなら、@RequestScope アノテーションをつける。

package dev.itboot.mb.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.annotation.RequestScope;

@RestController
@RequestScope // リクエストごとにインスタンスを生成
public class CounterController {

    private static int staticCounter = 0;
    private int instanceCounter = 0;

    @GetMapping("/counter")
    public synchronized String getSum() {
        staticCounter++;
        instanceCounter++;
        return "Static Counter: " + staticCounter + "\n" +
               "Instance Counter: " + instanceCounter;
    }
}

そうすると、static Counter: 1 Instance Counter: 1、static Counter: 2Instance Counter: 1、static Counter: 3 Instance Counter: 1になる。ただ、、個人的には@RequestScopeをつけるクラス、つけないクラスが発生して統制が取れなくなりそうで使いづらい。そのため今回のメソッドであればローカル変数にInstance Counterを定義するのがシンプルで無難。

ローカル変数でも期待通りの挙動になる↓

package dev.itboot.mb.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.annotation.RequestScope;

@RestController
@RequestScope // リクエストごとにインスタンスを生成
public class CounterController {

    private static int staticCounter = 0;
   

    @GetMapping("/counter")
    public synchronized String getSum() {
    	int instanceCounter = 0;
        staticCounter++;
        instanceCounter++;
        return "Static Counter: " + staticCounter + "\n" +
               "Instance Counter: " + instanceCounter;
    }
}

他の記事とかを見ると、状態をもたせない設計にする。という内容があった。今回の例ではinstanceCounterをインスタンス変数に持たずにローカル変数にする、みたいなコード設計が必要なんだと思う。ちなみにDtoクラスなどのDI対象ではないクラスはシングルトンではないっぽいのでリクエストごとにインスタンスかされる。@Serviceや@Controller,@Entity,@ComponentあたりがDI注入されるアノテーションなので、それらがつくクラスはシングルトンなのでインスタンスを意識し、スレッドセーフの設計にすべき。あまり結論のない記事になったが、誰かの役に立てば。