エキサイト株式会社エンジニアの佐々木です。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メソッドなどの濫用も防げます。検索してもあんまりでてこなかったので、簡単にですがまとめておきます。