Bean Scope

Bean의 유효범위(Scope)에 따라 Bean 생명주기가 결정되곤 합니다. Bean의 Scope는 다음과 같은 내용들이 있습니다.

Singleton Scope

기본으로 사용되는 Bean의 유효범위 입니다. 하나의 Bean을 만들어 각 요청마다 같은 Bean을 되돌려줍니다.

Prototype Scope

요청마다 새로운 객체를 만들어 클라이언트에게 제공하는 방식입니다.

Singleton와 Prototype을 같이 쓰면 발생하는 문제

싱글톤과 Prototype을 같이 쓰게 되면 예상치 못한 결과가 발생하게 됩니다. Singleton 인스턴스가 처음 의존성 주입 된 객체를 지속적으로 저장하고 재사용하기 때문에 Proto 객체가 새로 생성되지 않고 재사용됩니다.

@Scope("prototype")
class PrototypeBean {
	var count = 0
	fun addCount() {
		count++
		println("count = $count")
	}
}
 
// 싱글톤 Bean 안에 Proto Bean을 사용 중
@Scope("singleton")
class ClientBean(
	private val prototypeBean: PrototypeBean
) {
	fun logic() {
		prototypeBean.addCount()
	}
}
 
@Test
fun singletonClientUsePrototype() {
	val ac = AnnotationConfigApplicationContext(ClientBean::class.java, PrototypeBean::class.java)
	val clientBean1 = ac.getBean(ClientBean::class.java)
	val clientBean2 = ac.getBean(ClientBean::class.java)
	clientBean1.logic() // 1
	clientBean2.logic() // 2 (재사용되기 때문에 값이 증가됨)
	ac.close()
 
}

이를 해결하기 위해 ObjectProvider를 이용할 수 있습니다. ObjectProviderSpring 에서 제공하는 특별한 객체로, 지연 로딩, 필요한 객체를 조회하고 생성하는 역할을 하곤 합니다. ObjectProvider를 통해서 필요한 객체를 직접 호출해오는 방식입니다.

@Scope("singleton")
class ClientBean(
   private val prototypeBeanProvider: ObjectProvider<PrototypeBean>
) {
	fun logic(): Int {
		val prototypeBean = prototypeBeanProvider.getObject()
		prototypeBean.addCount()
		return prototypeBean.count
	}
}

Request Scope

요청마다 각각의 새로운 Instance를 가지는 방식입니다. 해당 방식을 사용함으로써 서로 간의 데이터가 섞이는 문제와 같은 현상을 최소화 할 수 있습니다.

Request Scope Bean과 Proxy

이처럼 요청이 들어오면 생성되는 Bean이기 때문에 실제 Spring 초기화 과정에서 Bean을 생성하지 못하는 에러가 발생하게 됩니다.

  • ObjectProvider를 통해서 문제를 해결할 수 있습니다. (위와 동일)
  • Proxy를 활용하여 문제를 해결할 수 있습니다.
    ObjectProvider와 Proxy 모두 객체의 생성을 늦추어 필요한 시점에 생성을 하는 것에 목적을 둔다는 점에서 매우 유사합니다. Proxy는 사용에 간편함을 제공하는 만큼 단순한 로직(AOP 등)에 적합하며, ObjectProvider는 불편한 만큼 다양한 조건에 적용할 수 있고, 보다 복잡한 로직에 적용할 수 있다는 점이 다르다고 생각합니다.

Reference