티스토리 뷰

JVM/Spring

[Spring] Scope (Prototype & Singleton)

글을 쓰는 개발자 2022. 9. 13. 08:59
반응형

Bean

Prototype Scope

싱글톤 방식으로 동작하는 것이 아닌, bean deployment의 prototype의 범위는 해당 특정 Bean에 대한 요청이 이루어질 때마다 새로운 Bean 인스턴스가 생성(즉, 다른 Bean에 주입되거나 컨테이너의 programmatic getBean() method 호출을 통해 요청됨)

원칙적으로 stateful 한 모든 Bean에는 prototype scope 을 사용하고, stateless Bean에는 singleton scope을 사용해야 한다.

프로토 타입을 사용하고자 할 때, bean의 lifecycle의 미묘한 변화을 알아야 한다.

컨테이너는 프로토타입 객체를 인스턴스화, 구성, 장식 및 조립하고 Client에게 전달한 후 해당 prototype 인스턴스에 대해 더 이상 알지 못한다.

즉, 초기화 lifecycle callback method는 범위에 관계업이 모든 object에 대해 호출되지만 prototype의 경우 구성된 destroy lifecycle은 호출되지 않는다.

prototype scope object를 정리하고 프로토타입 Bean이 보유하고 있는 값비싼 리소스를 방출하는 것은 클라이언트 코드의 책임이다.

어떤 면에서는 prototype scope bean에 대해 이야기 할 때 Spring Container 역할을 Java new연산자를 대체하는 것으로 생각할 수 있다.


prototype으로 범위가 지정된 bean에 종속성이 있는 singleton 범위 bean을 사용할 경우 dependencies는 인스턴스화 시 resolve 된다는 것을 인지해야 한다.
즉, 의존성이 prototype scope bean을 singleton scope bean에 주입하면 새로운 prototype bean이 인스턴스화 되고 singleton bean에 의존성이 주입된다.

하지만 그게 전부다. 정확히 동일한 프로토타입 인스턴스가 singleton scope bean에 공급되는 유일한 인스턴스가 될 것이다. (중요!)

때때로 우리가 실제로 원하는 것은 singleton scoped bean이 런타임에 prototype scoped bean의 새로운 인스턴스를 계속해서 획득한다는 것입니다. 이 경우, 위에서 설명한 것처럼 Spring Container가 singleton bean을 인스턴스화하고 의존성을 해결하고 주입할 때 단 한 번만 발생하기 때문에 prototype scoped bean을 singleton bean에 주입하는 것은 아무런 소용이 없습니다. 런타임에 (prototype) 빈의 새로운 인스턴스를 계속해서 가져와야 하는 시나리오에 있는 경우, Method Injection 을 참조하면 좋다.

예시를 보며 알아보자!

MyObject.java

@Getter  
@ToString  
@AllArgsConstructor  
public class MyObject {  
    private Long value;  
    private final String name;  

    public void update() {  
        this.value++;  
    }  
}

MyObject.java는 Bean으로 등록하기 위한 클래스

ScopeConfiguration.java

@Configuration  
public class ScopeConfiguration {  

    @Bean  
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)  
    public MyObject prototypeBean() {  
        return new MyObject(1L, "protoType");  
    }  

    @Bean  
    public MyObject singletonBean() {  
        return new MyObject(1L, "singleton");  
    }  
}

prototypeBean 의 경우에는 Scope 를 Prototype으로 설정
singletonBean 의 경우에는 Scope 를 Singleton으로 설정

ScopeService.java

@Slf4j  
@Service  
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)  
public class ScopeService {  
    private final MyObject proto;  
    private final MyObject single;  

    private final ApplicationContext context;  

    public ScopeService(ApplicationContext context,  
                        @Qualifier("prototypeBean") MyObject proto,  
                        @Qualifier("singletonBean") MyObject single) {  
        this.context = context;  
        this.proto = proto;  
        this.single = single;  
    }  

    public void app() {  
        Map<String, MyObject> map = context.getBeansOfType(MyObject.class);  
        for (String s : map.keySet()) {  
            log.info("service in key = {}, value = {}", s, map.get(s));  
        }  
    }  

    public void call() {  
        proto.update();  
        log.info("call service -> proto = {}", proto);  
        single.update();  
        log.info("call service -> single = {}", single);  
    }  
}

app() 메소드는 context 에 저장된 bean들을 조회하는 메소드
call() 메소드는 prototype 과 singleton 에 따른 변화를 확인하기 위한 메소드

ScopeController

@Slf4j  
@RestController  
@RequestMapping("/api/v1/scope")  
public class ScopeController {  
    private final ScopeService service;  
    private final MyObject single;  
    private final MyObject proto;  

    private final ConfigurableApplicationContext context;  

    public ScopeController(ConfigurableApplicationContext context,  
                           ScopeService service,  
                           @Qualifier("singletonBean") MyObject single,  
                           @Qualifier("prototypeBean") MyObject proto) {  
        this.context = context;  
        this.service = service;  
        this.single = single;  
        this.proto = proto;  
    }  

    @GetMapping("/app")  
    public void app() {  
        Map<String, MyObject> map = context.getBeansOfType(MyObject.class);  
        for (String s : map.keySet()) {  
            log.info("controller in key = {}, value = {}", s, map.get(s));  
        }  
    }  

    @GetMapping("/service/app")  
    public void app2() {  
        service.app();  
    }  


    @GetMapping("/service/call")  
    public void callService() {  
        service.call();  
    }  

    @GetMapping("/call")  
    public void call() {  
        proto.update();  
        log.info("call first -> proto = {}", proto);  
        single.update();  
        log.info("call first -> single = {}", single);  
    }  

    @GetMapping("/call2")  
    public void call2() {  
        proto.update();  
        log.info("call second -> proto = {}", proto);  
        single.update();  
        log.info("call second -> single = {}", single);  
    }  
}

여기서 확인해봐야 할 것

즉, 의존성이 prototype scope bean을 singleton scope bean에 주입하면 새로운 prototype bean이 인스턴스화 되고 singleton bean에 의존성이 주입된다.

위에서 말한 이것을 주목!

실제 위에서 Bean 등록되는 형식은 다음과 같이 진행된다고 생각하면 된다.

@Configuration  
public class TempConfiguration {  

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public ScopeService scopeService(ApplicationContext context,  @Qualifier("singletonBean") MyObject singleton) {  
        return new ScopeService(context, new MyObject(1L, "prototype"), singleton);  
    }  

    @Bean  
    public ScopeController scopeController(ConfigurableApplicationContext context, @Qualifier("singletonBean") MyObject singleton) {  
        return new ScopeController(context,scopeService(context, singleton), new MyObject(1L, "prototype"), singleton);  
    }  
}

진짜 이렇게 진행되는 것은 아니지만 이런 식으로 동작한다고 생각하면 이해하기 쉽다.

호출 방식은 다음과 같다.

http://localhost:8080/api/v1/scope/call
http://localhost:8080/api/v1/scope/call2
http://localhost:8080/api/v1/scope/app
http://localhost:8080/api/v1/scope/service/call
http://localhost:8080/api/v1/scope/service/app
call first -> proto = MyObject(value=2, name=protoType)
call first -> single = MyObject(value=2, name=singleton)
---
call second -> proto = MyObject(value=3, name=protoType)
call second -> single = MyObject(value=3, name=singleton)
---
controller in key = prototypeBean, value = MyObject(value=1, name=protoType)
controller in key = singletonBean, value = MyObject(value=3, name=singleton)
---
call service -> proto = MyObject(value=2, name=protoType)
call service -> single = MyObject(value=4, name=singleton)
---
service in key = prototypeBean, value = MyObject(value=1, name=protoType)
service in key = singletonBean, value = MyObject(value=4, name=singleton)

여기서 재밌는 점은 ~/app 을 호출 했을 때 prototype의 값은 1로 다시 세팅된다는 점이다.

그렇다면 ~/app 을 호출하면 새로 인스턴스가 주입되는 것일까?

그건 또 아니다!

call first -> proto = MyObject(value=4, name=protoType)
call first -> single = MyObject(value=5, name=singleton)

우리는 공식문서에서 이 단어를 기억할 필요가 있다.

싱글톤 방식으로 동작하는 것이 아닌, bean deployment의 prototype의 범위는 해당 특정 Bean에 대한 요청이 이루어질 때마다 새로운 Bean 인스턴스가 생성
(즉, 다른 Bean에 주입되거나 컨테이너의 programmatic getBean() method 호출을 통해 요청됨)

getBean 호출 시 prototype scope가 등록되는 과정

singleton bean이 등록되는 과정

Spring에서 Instance가 생성되는 과정

AbstractAutowireCapableBeanFactory - createInstance(String beanName, RootBeanDefinintion mbd, @Nullable Object[] args)

Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); // 생성자 결정
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||  
      mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {  
   return autowireConstructor(beanName, mbd, ctors, args);  
}

AbstractAutowireCapableBeanFactory

protected BeanWrapper autowireConstructor(  
      String beanName, RootBeanDefinition mbd, @Nullable Constructor<?>[] ctors, @Nullable Object[] explicitArgs) {  

   return new ConstructorResolver(this).autowireConstructor(beanName, mbd, ctors, explicitArgs);  
}

해당 autowireConstructor 를 통해 생성자와 파라미터에 맞게 객체를 생성하고 이를 DefaultSingletonRegistry 에 등록되는 형태로 관리가 된다.

즉, 하나의 객체로 관리되므로 singleton의 의미 자체는 맞지만 singleton pattern이라고 보기엔 어려울 것 같다.

대신 객체를 가져올 때 DCL 작업을 진행한다.

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Quick check for existing instance without full singleton lock
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                // Consistent creation of early reference within full singleton lock
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

소멸은 어떻게 되는가?

prototype의 경우 debug 해본 결과 스프링에서 관리하는 것이 아니라 gc에 의해 처리되는 것으로 확인되며 해당 링크 를 보시면 custom하게 설정할 수 있다.

singleton의 경우 spring에서 처리되는 것으로 확인되었으며

    @Override
    public void close() {
        synchronized (this.startupShutdownMonitor) {
            doClose();
            // If we registered a JVM shutdown hook, we don't need it anymore now:
            // We've already explicitly closed the context.
            if (this.shutdownHook != null) {
                try {
                    Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
                }
                catch (IllegalStateException ex) {
                    // ignore - VM is already shutting down
                }
            }
        }
    }

container가 내려갈 때 close() method를 통해

    private void stopBeans() {
        Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
        Map<Integer, LifecycleGroup> phases = new HashMap<>();
        lifecycleBeans.forEach((beanName, bean) -> {
            int shutdownPhase = getPhase(bean);
            LifecycleGroup group = phases.get(shutdownPhase);
            if (group == null) {
                group = new LifecycleGroup(shutdownPhase, this.timeoutPerShutdownPhase, lifecycleBeans, false);
                phases.put(shutdownPhase, group);
            }
            group.add(beanName, bean);
        });
        if (!phases.isEmpty()) {
            List<Integer> keys = new ArrayList<>(phases.keySet());
            keys.sort(Collections.reverseOrder());
            for (Integer key : keys) {
                phases.get(key).stop();
            }
        }
    }

메소드가 동작하는 것을 볼 수가 있다.

반응형

'JVM > Spring' 카테고리의 다른 글

[Spring] @Async 비동기로 처리하는 방법  (0) 2022.10.01
[Spring] @Scheduled, @EnableScheduling  (0) 2022.10.01
[Hikari CP] 光 살펴보기 - 4  (0) 2022.04.29
[Hikari CP] 光 살펴보기 - 3  (0) 2022.04.28
[Hikari CP] 光 살펴보기 - 2  (0) 2022.04.27
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함