Spring/Dependency Injection

[Spring] ObjectProvider 사용해 Bean 생성하고 언제 사용돼야 하는지 알아보기

Dev.Cho 2024. 11. 22. 07:23
반응형

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는 다음 페이지에서 확인할 수 있습니다.
반응형