ObjectProvider가 필요한 이유
앞서 우리는 다음과 같이 ShoppingCart를 프로토 타입 Bean으로 만들고, 이를 싱글톤으로 선언된 CartController에서 사용하고자 했다.
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Component
class ShoppingCart {
private val items: MutableList<String> = mutableListOf()
fun addItem(item: String) {
items.add(item)
}
fun removeItem(item: String) {
items.remove(item)
}
}
@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
class CartController() {
fun addToCart(userId: String, item: String) {
println("[Add Item] User $userId added $item to cart")
}
}
하지만, 싱글톤으로 선언된 CardController에 ShoppingCart 객체를 주입 받으면 ShoppingCart도 하나만 생기기 때문에 문제가 생겼다.
이를 해결하기 위해 Bean을 제공 받을 수 있는 ObjectProvider가 필요하다.
ObjectProvider 사용해 Bean 제공 받기
ObjectProvider을 사용하는 방법은 간단하다. 단순히 Bean 으로 등록된 클래스를 ObjectProvider로 감싸면, ObjectProvider의 getObject 함수가 호출됐을 때 getBean을 통해 Bean을 가져온 것과 같은 효과를 낸다.
예를 들어 CartController에서 유저의 아이디마다 새로운 ShoppingCart Bean을 가져오고 싶다면 다음과 같이 만들면 된다.
@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
class CartController(
private val cartProvider: ObjectProvider<ShoppingCart>
) {
val shoppingCartMap: MutableMap<String, ShoppingCart> = mutableMapOf()
fun addToCart(userId: String, item: String) {
if (!shoppingCartMap.containsKey(userId)) {
shoppingCartMap[userId] = cartProvider.getObject()
}
shoppingCartMap[userId]?.addItem(item)
println("[Add Item] User $userId added $item to cart")
}
}
이 코드에서는 CartController는 ObjectProvider<ShoppingCart>를 주입 받으며, 내부에는 userId당 하나의 ShoppingCart 객체를 갖도록 Map으로 관리한다.
addToCart 함수가 호출되면 shoppingCartMap에 userId에 대응되는 ShoppingCart가 있는지 확인한 후 없으면 cartProvider.getObject() 함수를 호출해 새로운 ShoppingCart를 생성한다.
이것이 제대로 동작하는지 확인하기 위해 다음과 같은 코드를 만들어보자.
fun main(args: Array<String>) {
val context = AnnotationConfigApplicationContext(UserContainerConfiguration::class.java)
val controller = context.getBean(CartController::class.java)
controller.addToCart("1", "item1")
controller.addToCart("2", "item2")
controller.shoppingCartMap.forEach {
println("User: ${it.key}, Items: ${it.value}")
}
}
이 코드에서는 CartController Bean을 가져온 후 userId "1"에 "item1" 을 더하고 userId "2"에 "item2" 를 더한다. 그런 후 만들어진 shoppingCartMap의 모든 key-value 쌍을 출력한다. 이제 코드를 실행해보자. 그러면 다음과 같은 결과가 나온다.
유저마다 ShoppingCart 객체가 서로 다른 것을 확인할 수 있다.
싱글톤 Bean에 대해 ObjectProvider을 사용하기
앞서 우리는 프로토타입 Bean에 대해 ObjectProvider을 사용했다. 따라서 ObjectProvider 객체에 대해 getObject()를 호출하면 매번 다른 객체가 반환됐다. 하지만, 싱글톤 Bean에 대해 ObjectProvider을 사용하면 Bean 이 하나만 생성되기 때문에 매번 같은 객체가 반환된다.
이를 확인하기 위해 CartControllerProvider을 다음과 같이 만들어보자. 이곳에서는 앞서 만든 싱글톤 Bean인 CartController을 제공하는 ObjectFactory<CartController>를 주입 받아, provideCartController 함수가 호출됐을 때 ObjectFactory<CartController>.getObject() 함수를 호출한다.
@Component
class CartControllerProvider(
private val cartControllerProvider: ObjectProvider<CartController>
) {
fun provideCartController(): CartController {
return cartControllerProvider.getObject()
}
}
이 ObjectFactory<CartController>가 매번 같은 객체를 반환하는지 확인하기 위해 다음과 같은 코드를 작성한 후 실행해보자. 이 코드에서는 provideCartController 함수를 두 번 호출해 각 객체를 출력한다.
fun main(args: Array<String>) {
println(context.getBean(CartControllerProvider::class.java).provideCartController())
println(context.getBean(CartControllerProvider::class.java).provideCartController())
}
그러면 결과는 다음과 같이 나온다.
같은 객체가 출력 되는 것을 볼 수 있다.
즉, 싱글톤 Bean에 대해 ObjectProvider을 사용하면 싱글톤 객체가 반환된다.
ObjectProvider을 사용해야 하는 경우
1. 만약 프로토 타입 Bean을 싱글톤 Bean에서 주입 받게 될 경우 프로토 타입 Bean은 싱글톤 Bean과 묶여 있어 메모리에서 제거되지 않는다. 따라서 만약 함수에서 사용한 후 바로 제거해야 하는 프로토 타입 Bean의 경우 ObjectProvider을 통해 생성해야 한다.
2. 위에서 다룬 것과 같이 호출 시마다 새로운 객체가 생성돼야 하는 경우 프로토 타입 Bean과 함께 ObjectProvider을 사용하면 된다.
3. @Lazy가 붙어 지연 초기화 싱글톤 Bean의 경우 로 설정돼 있다고 하더라도, 만약 이 Bean에 의존성을 가진 Bean이 지연 초기화 설정돼 있지 않다면 바로 메모리에 올라가버린다. 이런 경우 ObjectProvider을 사용하면 지연 초기화를 유지할 수 있다.
전체 코드: GitHub
이 프로젝트가 도움이 되셨다면 저장소에 Star⭐️를 눌러주세요! Stargazers는 다음 페이지에서 확인할 수 있습니다.