SpringBootのBeanスコープを@Scopeで使い分ける

エキサイト株式会社エンジニアの佐々木です。SpringBootのBeanスコープのデフォルトはSingletonですが、@Scopeを利用して使い分けていこうと思います。

Beanスコープとは?

SpringBootはDIコンテナを保有していて、起動時に多くのBeanを生成します。そのBeanがどのように生成され、どこで再利用されるかを@Scopeで制御できます。

singleton

Beanのデフォルトのスコープになります。DIコンテナの中で最初の1つのインスタンスしか作られません。1つのインスタンスをアプリケーション全体で利用するのでメモリ消費は少なくパフォーマンスがいいですが、インスタンスフィールドは、マルチスレッドでの利用は注意する必要があります。

// デフォルトsingleton
@Component
class SingletonData {
   ... 
}

request

リクエストごとに1つのインスタンスが作られます。1つのリクエスト内で共有してもいいようなデータの管理に使用します。

@Component
@RequestScope
class ReuestData {
   ... 
}

session

HTTPSessionごとに1つのインスタンスが作られます。SpringSessionを利用し、同一のセッション内で共有してもいいようなデータの管理に使用します。

@Component
@SessionScope
class SessionData {
   ... 
}

prototype

DIコンテナから取り出される毎にインスタンスが生成されるスコープになります。Listの中に含まれてるようなものに使用します。

@Component
@Scope("prototype")
class ProtoTypeData {
   ... 
}

singletonスコープ、prototypeスコープの違い

今回はsingletonスコープ、prototypeスコープ、requestスコープの検証を行います。(sessionスコープは、SpringSessionの導入などがあり煩雑なので割愛します)

今回は、ApplicationContextを使用したDIの方法で検証します。

singletonスコープ

singletonスコープのデータクラスは下記になります。

@Component
@Data
class SingletonScopeData {
   private int id = new SecureRandom().nextInt();
   private String uuid = UUID.randomUUID().toString();
}

テストコードは下記になります。ApplicationContextを使用して、2回SingletonScopeDataを呼び出していますが、同じインスタンスが返っています。

@SpringBootTest(classes = {SingletonScopeData.class})
class SingletonScopeDataTest {

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    void testSingleton() {
        SingletonScopeData bean1 = applicationContext.getBean(SingletonScopeData.class);
        SingletonScopeData bean2 = applicationContext.getBean(SingletonScopeData.class);
        Assertions.assertSame(bean1, bean2);   // true
    }
}

requestスコープ

requestスコープのデータクラスは下記になります。

@Component
@Data
@RequestScope
class RequestScopeData {
   private int id = new SecureRandom().nextInt();
   private String uuid = UUID.randomUUID().toString();
}

リクエスト単位でインスタンスが作成されますので、下記のようなコントローラーを用意して、このコントローラーに対してテストコードを書きます。

@RestController
@RequestMapping("scope")
@Slf4j
public class ScopeController {

    @Autowired
    private RequestScopeData requestScopeData;

    @GetMapping("request")
    public String requestScode() {
        return requestScopeData.toString();
    }
}

テストコードは下記になります。1回目のリクエストと2回目のリクエストで異なる値が返却されています。

@WebMvcTest(controllers = ScopeController.class)
@Import(RequestScopeData.class)
@Slf4j
class ScopeControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void testRequest() throws Exception {
        String result1 = mockMvc.perform(
                get("/scope/request"))
                .andExpect(status().isOk())
                .andReturn()
                .getResponse()
                .getContentAsString();
        String result2 = mockMvc.perform(
                        get("/scope/request"))
                .andExpect(status().isOk())
                .andReturn()
                .getResponse()
                .getContentAsString();

        log.info("result1: {}", result1);  // RequestScopeData(id=-1265028686, name=555946a3-9dbf-4833-9d73-a41c3474bcf5)
        log.info("result2: {}", result2); // RequestScopeData(id=415725629, name=9fe5ad43-6a22-43b1-89f8-272077d1f448)
        Assertions.assertNotEquals(result1,result2);  // true
    }
}

prototypeスコープ

@Component
@Data
@Scope("prototype")
class PrototypeScopeData {
   private int id = new SecureRandom().nextInt();
   private String uuid = UUID.randomUUID().toString();
}

テストコードは下記になります。PrototypeScopeを2回呼び出して比較をしていますが、同一インスタンスでもなく、値も異なります。

@SpringBootTest(classes = {PrototypeScopeData.class})
class PrototypeScopeDataTest {
    @Autowired
    private ApplicationContext applicationContext;

    @Test
    void testPrototype() {
        PrototypeScopeData bean1 = applicationContext.getBean(PrototypeScopeData.class);
        PrototypeScopeData bean2 = applicationContext.getBean(PrototypeScopeData.class);
        assertNotEquals(bean1, bean2);  // true
        assertNotSame(bean1, bean2);  // true
    }
}

補足:ApplicationContextについて

ApplicationContextは、Springの中のDIコンテナのコア部分で、Beanライフサイクル(オブジェクトの生成、破棄)を管理しているクラスになります。データクラスやDTOなどでprototypeスコープで利用する場合に便利です。

@SpringBootTest(classes = {PrototypeScopeData.class})
class PrototypeScopeDataTest {
    @Autowired
    private ApplicationContext applicationContext;

    @Test
    void testPrototype() {

        // prototypeの場合は、新しいインスタンスが返却される
        PrototypeScopeData prototype = applicationContext.getBean(PrototypeScopeData.class);

        // singletonの場合は、すでにインスタンス化されたオブジェクトが返却される
        SingletonScopeData singleton = applicationContext.getBean(SingletonScopeData.class);
    }
}

まとめ

Beanスコープを理解しながら使えば、単体テストが書きやすくより保守性の高いコードがかけると思います。 staticメソッドなどの濫用も防げます。検索してもあんまりでてこなかったので、簡単にですがまとめておきます。