본문 바로가기
Backend/Spring

Bean Scope(어떤 메커니즘?, 왜 쓰는건지, 주의점, 어떻게 생성되는지)

by 쭈돌s 2024. 9. 30.

https://docs.spring.io/spring-framework/reference/core/beans/factory-scopes.html

배경

김영한님 강의 코어 어쩌고 듣다가 섹션9쪽의 스코프에 대해 공부하다가 익숙하지 않은 내용이라 한번 정리를 해보았다.

Bean Scope 는 무엇인가?

빈이 존재 할 수 있는 범위이다. 생애 기간(생성~소멸) + 공간(쓰레드, 요청, 세션)

Bean Scope 는 왜 사용되는가?

크게 2가지 이유때문이다.
하나는 메모리의 최적 사용을 위해서이다. 한정적인 메모리에서 재사용이 많이 되는 경우 싱글턴으로 빈을 관리해 메모리 낭비를 줄인다. 두번
두번째는 새로운 빈 생성의 필요성 때문이다. 기능의 특성상 새로운 빈을 매번 사용해야 하는 경우가 있다. 예를들면 http 요청별 한정된 범위에서 한정된 기간동안 살아 있는 request Bean이 있다.

Bean Scope의 종류

[spring-beans 패키지]
- 싱글턴 : 스프링 생애 주기 전체에 활동하며 재사용에 용이하다. 스프링이 종료될 때 소멸 한다.
- 프로토타입 : 매번 새로운 객체가 필요한경우 사용하며. 생성 -> 사용 -> 소멸 패턴을 가진다.
[spring-web 패키지]
- request : 웹요청이 들어오고 나갈때 까지 유지되는 스코프.
- session : 세션이 생성되고 종료될 때까지 유지되는 스코프

Scope 에 대한 정의가 어디에 있는지 살펴보자

스프링의 기본적인 빈스코프 정의는 2개이다. (org.springframework:spring-beans 패지키지)

public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
    String SCOPE_SINGLETON = "singleton";
    String SCOPE_PROTOTYPE = "prototype";

    void setParentBeanFactory(BeanFactory parentBeanFactory) throws IllegalStateException;

    void setBeanClassLoader(@Nullable ClassLoader beanClassLoader);
    // ... 중략
}

나머지 위에서 언급한 빈스코프는 org.springframework:spring-web 패지키지에 정의돼 있다.

public interface WebApplicationContext extends ApplicationContext {
    String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
    String SCOPE_REQUEST = "request";
    String SCOPE_SESSION = "session";
    String SCOPE_APPLICATION = "application";
    String SERVLET_CONTEXT_BEAN_NAME = "servletContext";
    String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters";
    String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes";

    @Nullable
    ServletContext getServletContext();
}

스코프들은 어떤 행동 패턴을 가질까?
(특이하다! singleton, prototype은 Scope 인터페이스를 쓰지 않는다)

스코프 인터페이스가 존재한다. 인터페이스를 살펴보면 빈을 사용과 소멸에대한 책임을 가지고 있다.
(registerDestructionCallback 은 빈 소멸시 어떤 행동을 할지 등록을 해주는 구간이고 remove는 소멸에대한 행위를 한다. 기능의 등록과 실행이 나뉘어져 있다.) 
하지만 singleton, prototype 스코프는 이 인터페이스를 구현하지 않는다. 웹에 관련된 request, session 스코프에서 이 스코프를 구현해서 사용한다. 하지만 생성시, 소멸시에 대한 책임에 대한 기능 구현은 일치하기 때문에 메커니즘상 일치한다고 볼 수 있을 거 같다.

public interface Scope {
    Object get(String name, ObjectFactory<?> objectFactory);

    @Nullable
    Object remove(String name);

    void registerDestructionCallback(String name, Runnable callback);

    @Nullable
    Object resolveContextualObject(String key);

    @Nullable
    String getConversationId();
}

그러면 prototype 스코프만 발췌해서 어떻게 생성하고
어떤방식으로 빈 소멸에 대한 책임을 지지 않는지 살펴보도록 하자.

// DefaultListableBeanFactory.class -> 프로토타입 빈생성을 담당
	protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
        // 중략
        if (mbd.isSingleton()) {
            sharedInstance = this.getSingleton(beanName, () -> {
                try {
                    return this.createBean(beanName, mbd, args);
                } catch (BeansException var5) {
                    this.destroySingleton(beanName);
                    throw var5;
                }
            });
            beanInstance = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
        } else if (mbd.isPrototype()) {
            prototypeInstance = null;

            Object prototypeInstance;
            try {
                this.beforePrototypeCreation(beanName);
                prototypeInstance = this.createBean(beanName, mbd, args);
            } finally {
                this.afterPrototypeCreation(beanName);
            }

            beanInstance = this.getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
        } 
    	//  중략
    }
    
// AbstractbeanFactory.class -> 프로토타입 생성완료후 인스턴스 변수를 초기화
    protected void afterPrototypeCreation(String beanName) {
        Object curVal = this.prototypesCurrentlyInCreation.get();
        if (curVal instanceof String) {
            this.prototypesCurrentlyInCreation.remove();
        } else if (curVal instanceof Set) {
            Set<?> beanNameSet = (Set)curVal;
            beanNameSet.remove(beanName);
            if (beanNameSet.isEmpty()) {
                this.prototypesCurrentlyInCreation.remove();
            }
        }
    }

위 코드는 프로토 타입 빈이 생성되는 프로세스를 나타낸다. finally 쪽 코드(afterPrototypeCreation(String beanName) )를 보면 prototypesCurrentlyIncreation 인스턴스 변수가 존재하는데 빈객체를 생성하자마자 삭제를 요청한다. 쓰레드 로컬의 메모리에서 프로토타입 빈 객체를 삭제함으로써 새로 생성해서 사용하기를 강제하는 코드라 볼 수 있다.

Bean Scope를 잘못사용하면 의도치 않은 동작을 할 수 있다. (singleton + prototype)

예를들면 싱글턴 스코프 빈에 프로토타입 스코프 빈을 가져와 사용하는경우 의도하지 않은 경우가 생길 수 있다.
싱글턴 빈의 인스턴스 변수로 prototype 스코프 빈이 있는 경우 DI 컨테이너는 메모리 참조 값을 해제한다. 하지만 prototype 빈의 객체는 살아 있고 싱글턴 빈의 인스턴스 변수의 객체로 참조되고 있기 때문에 의도하지 않게 prototype 빈 객체는 영생을 누리게된다.
다른 블로그들에서 이미 코드를 통해서 잘 설명하고있는거 같아서 이쯤에서 넘어가도록 한다.

이를 해결하기 위해서는 의존관계조회 DL(Dependency Lookup)이 필요하다. DL은 의존관계를 직접 찾아서 가져오는 방법이다. Spring에서 제공하는 ObjectProvider, 자바에서 제공하는 Provider가 해결방법으로 제공된다. 어떤 것이 더 나은지에 대해서는 주관적으로 Spring에서 제공하는 패키지가 더 낫다고 본다. 왜냐하면 자바에서 제공하는 Provider는 패키지 의존성을 직접 관리해야 하므로, 관리적 측면에서 Spring이 관리하도록 하는게 좋다.

프로토타입 스코프는 현업에서 자주 쓰이진 않는다. 하지만 사용하게 된다면 주의 해야 한다. -> 이게 핵심일거 같다.

https://www.inflearn.com/community/questions/661448/%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-%EB%B9%88%EC%9D%98-%EC%8B%A4%EC%A0%9C-%ED%99%9C%EC%9A%A9%EC%B2%98?srsltid=AfmBOopraBvhNF2YyD_NlHoUGfHEuYpDXAmNuhjTMZEfLVFbxbzlwQkX

 

프로토타입 빈의 실제 활용처 - 인프런 | 커뮤니티 질문&답변

누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.

www.inflearn.com


다른 스코프들에 대해서도 할말이 있는데 다음에 써보도록 하는게 좋을거 같다. 글도 작은단위로 쓰는게 주제를 안벗어나고 이해하기가 좋은거 같다. 

문제가 있거나 수정해야할 부분이 있는 곳은 언제든 수용하려 하니 의견주시면 받아들이겠습니다.

댓글