😅 우선 글을 작성하기에 앞서 이 글은 스프링을 처음 공부하는 입장에서 Spring 공식 문서를 자의적으로 해석한 글이며 (번역기의 도움을 받아) 제게 필요하다고 생각되는 부분을 우선적으로 해석할 예정이라 중간중간 빈 부분이 있을 수 있으며 틀린 내용이 포함되었을 수 있습니다. 잘못된 부분이 있을 때 피드백 남겨주시면 능력껏 반영하도록 하겠습니다.
1.5. Bean Scopes
빈 정의를 만들 때, 빈 정의에 정의된 클래스의 실제 인스턴스를 생성하기 위한 레시피를 만듭니다. 빈 정의가 일종의 레시피라는 관점은 중요합니다. 그 뜻은, 클래스와 마찬가지로, 하나의 레시피로부터 많은 인스턴스를 생성할 수 있다는 의미이기 때문입니다.
당신은 특정한 빈 정의에 의해서 객체가 생성될 때 주입되는 다양한 의존성들과 구성요소들을 다룰 수 있을 뿐만 아니라 특정한 빈 정의에 의해 만들어지는 객체의 범위(scope) 역시 다룰 수 있습니다. 이러한 접근은 Java 클래스 수준에서 객체의 범위를 정하는 대신 구성(configuration) 단계를 통해 생성되는 객체의 범위를 정할 수 있기에 강력하고 유연합니다. 빈들은 빈이 가질 수 있는 여러 범위 중 하나를 가지도록 정의될 수 있습니다. 스프링 프레임워크는 6개의 범위를 지원하는데, 그 중 넷은 오직 ApplicationContext에서만 사용가능합니다. 당신은 또한 새로운 빈 범위를 커스텀할 수도 있습니다.
Supported scopes
singleton
- (Default) 각각의 스프링 IoC의 컨테이너에서 단일 빈 정의를 단일 객체 인스턴스로 범위를 지정
prototype
- 단일 빈 정의를 여러 객체 인스턴스로 범위를 지정
request
- 단일 빈 정의의 범위를 단일 HTTP 요청의 생명주기로 지정. 즉, 각 HTTP 요청에는 단일 빈 정의에서 생성된 자체 빈 인스턴스가 있다. (ApplicationContext를 사용하는 웹-인식 환경에서만 사용 가능합니다.)
session
- 단일 빈 정의를 HTTP Session의 생명주기로 범위를 지정. (ApplicationContext)
application
- 단일 빈 정의를 ServletContext의 생명주기로 범위를 지정. (ApplicationContext)
websocket
- 단일 빈 정의를 WebSocket의 생명주기로 범위를 지정. (ApplicationContext)
1.5.1. The Singleton Scope
오직 하나의 공유되는 싱글톤 빈의 인스턴스만 관리하고, 해당 빈 정의와 일치하는 ID를 가지는 빈에 대한 모든 요청은 스프링 컨테이너에 의해 그 특정한 빈 인스턴스에서 결과를 반환합니다.
달리 표현하자면, 빈 정의를 정의하고 그것이 싱글톤으로 범위가 지정되면, 스프링 IoC 컨테이너는 해당 빈 정의에 의해 정의된 객체의 정확히 하나의 인스턴스만 생성합니다. 이 단일 인스턴스는 이러한 싱글톤 빈의 캐시에 저장되며 해당 명명된 빈에 대한 모든 후속 요청 및 참조는 캐시된 개체를 반환합니다. 다음 이미지는 싱글톤 범위의 작동 방식을 보여줍니다:
스프링의 싱글톤 빈 개념은 GoF(Gang of four) 패턴 책에 정의된 싱글톤 패턴과 다릅니다. GoF 싱글톤은 클래스 로더에 의해 만들어지는 특정한 클래스의 인스턴스를 오직 하나만 생성되도록 객체의 범위를 하드코딩합니다. 스프링 싱글톤의 범위는 컨테이너 하나당 빈 하나라는 의미로 잘 설명됩니다. 이는 만약 스프링 컨테이너에서 특정한 클래스에 대해 하나의 빈을 정의한다면, 스프링 컨테이너는 해당 빈 정의에 정의된대로 오직 하나의 인스턴스만을 만든다는 의미입니다. 스플이에서 싱글톤 범위는 기본으로 설정되어 있습니다. XML에서 빈을 싱글톤으로 정의하려면, 다음 예시와 같이 빈을 정의해주면 됩니다:
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
1.5.2. The Prototype Scope
싱글톤이 아닌 프로토타입 범위 빈은 특정한 빈에 대한 요청이 들어올 때마다 새로운 빈 인스턴스를 생성합니다. 즉, 빈이 다른 빈에 주입되거나 컨테이너의 getBean() 메서드를 통해 요청됩니다. 일반적으로 모든 상태를 가지는 빈에는 프로토타입 범위를 사용하고, 상태를 가지지 않는 빈에는 싱글톤 범위를 사용해야 합니다. 다음 그림은 스프링 프로토타입 범위에 대해 보여줍니다:
(한 가지 예시로, 데이터 접근 객체(DAO)는 일반적으로 프로토타입으로 구성되지 않는데, 그 이유는 전형적인 DAO는 어떠한 상태를 가지지 않기 때문입니다.)
다음 예시는 XML에서 빈을 프로토타입으로 정의하는 예시입니다:
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
다른 범위들과 반대로, 스프링은 프로토타입 빈의 생명주기를 완벽히 관리하지 않습니다. 컨테이너는 프로토타입 객체를 인스턴스화하고 구성 및 조랍하여 클라이언트에 전달합니다. 이 경우 더 이상 프로토타입 인스턴스들에 대해 추가적인 기록을 하지 않습니다. 따라서 범위에 관계없이 모든 객체에 대해 초기화 생명주기 콜백 메서드가 호출되더라도, 프로토타입 범위 객체의 경우에는 소멸 생명주기 콜백이 호출되지 않습니다. 따라서 클라이언트는 프로토타입-객체를 정리하고 프로토타입 빈이 보유하고 있는 값비싼 자원을 해제해주어야 합니다. 스프링 컨테이너가 프로토타입-범위 빈이 들고 있는 자원을 해제하도록 하기 위해, 빈이 제거되기 위해 필요한 참조를 가지고 있는 custom bean post-processor를 사용하는 것을 고려할 수 있습니다.
몇몇 관점에서, 프로토타입 범위 빈에 관한 스프링 컨테이너의 역할은 자바의 new 연산자를 대체하는 것입니다. 생성된 이후의 모든 수명 주기 관리는 클라이언트 측에서 처리해야 합니다.
1.5.3. Singleton Beans with Prototype-bean Dependencies
프로토타입 빈에 대한 의존성을 가지는 싱글톤 범위 빈을 사용할 때, 인스턴스화할 때 의존성이 해결된다는 점을 유의해야 합니다. 즉, 만약 싱글톤 범위 빈에 프로토타입 범위 빈을 의존성 주입해준다면 새로운 프로토타입 빈이 인스턴스화된 다음 싱글톤 빈에 의존성 주입됩니다. 그 때 생성된 프로토타입 인스턴스는 싱글톤 범위 빈에 제공되는 유일한 인스턴스입니다.
그러나 싱글톤 범위의 빈이 런타임 시 반복적으로 프로토타입 범위 빈의 새 인스턴스를 획득하기를 원한다고 가정해봅시다. 스프링 컨테이너가 싱글톤 빈을 인스턴스화하고 의존성을 주입하여 의존성을 해결해줄 때 한 번만 의존성 주입이 발생하기 때문에 프로토타입 빈을 싱글톤 빈에 의존성 주입해줄 수 없습니다. 런타임 시 프로토타입 빈의 새 인스턴스가 두 번 이상 필요하다면 Method Injection (Spring Core 1.4.6) 을 참고하십시오.
1.5.4. Request, Session, Application, and WebSocket Scopes
(생략)
참고자료 : Baeldung - Quick Guide to Spring Bean Scopes
1.5.5. Custom Scopes
빈 범위 지정 메커니즘은 확장가능합니다. 사용자 자신만의 범위를 지정하거나 기존에 존재하던 범위를 재정의할 수도 있지만, 후자는 나쁜 관습으로 여겨지기 때문에 싱글톤이나 프로토타입 범위를 재정의하는 것은 허용되지 않습니다.
Creating a Custom Scope
커스텀 범위를 스프링 컨테이너에 결합시키기 위해서, org.springframework.beans.factory.config.Scope 인터페이스를 구현해야 합니다. 자신의 범위를 구현하는 방법에 대한 아이디어는 스프링 프레임워크에서 제공되는 Scope 구현 방법과 Scope Javadoc 문서를 참조하십시오.
Scope 인터페이스는 범위로부터 객체를 얻고, 객체를 제거하고, 소멸되도록 하기 위한 네 개의 메서드가 있습니다. 예를 들어 Session 범위 구현은 Session 범위 빈을 반환합니다. (해당 시점에 존재하지 않을 경우, 새 인스턴스를 만든 뒤 추후의 참조를 위해 세션 범위에 바인딩한 뒤 반환합니다.) 다음의 메서드는 자신의 범위에서 개체를 반환합니다:
Object get(String name, ObjectFactory<?> objectFactory)
예를 들어 Session 범위 구현은 자신의 범위에서 session 범위 빈을 제거합니다. 반드시 객체가 반환되어야 하지만 만약 해당 이름을 가진 객체가 발견되지 않는다면 null을 반환합니다. 다음 메서드는 자신의 범위에서 객체를 제거합니다:
Object remove(String name)
다음 메서드는 범위가 소멸되거나 범위의 지정된 개체가 소멸될 때 호출해야 하는 콜백을 등록합니다.
void registerDestructionCallback(String name, Runnable destructionCallback)
다음 메서드는 자신의 범위에 대한 대화 식별자(conversation Id)를 가져옵니다. 이 식별자는 범위마다 다릅니다. Session 범위 구현의 경우 이 식별자는 session 식별자일 수 있습니다.
String getConversationId()
Using a Custom Scope
자신만의 Scope 구현을 작성하고 테스트했다면, 스프링컨테이너가 당신이 작성한 범위를 인식하도록 만들 필요가 있습니다. 다음 메서드는 스프링 컨테이너에 새로운 Scope를 등록하기 위한 주요 메서드입니다:
void registerScope(String scopeName, Scope scope);
이 메서드는 스프링과 함께 제공되는 대부분의 구체적인 ApplicationContext 구성의 BeanFactory 속성을 통해 사용가능한 ConfigurableBeanFactory에 정의되어 있습니다.
registerScope() 메서드의 첫 번째 파라미터는 범위와 관련된 특정한 이름입니다. 스프링 컨테이너에서 이러한 특정한 이름의 예시로는 singletone과 prototype이 있습니다. 두 번째 파라미터는 당신이 등록하고 사용하고 싶은 커스텀 Scope 구현체의 실제 인스턴스입니다.
커스텀 Scope 구현을 작성하고, 다음 예시와 같이 등록한다고 가정해봅시다. (다음 예시는 스프링에는 포함되어 있지만 기본적으로는 등록되어 있지는 않은 SimpleThreadScope를 사용합니다. 커스텀 범위와 같다고 생각하면 됩니다.)
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
다음과 같이, 커스텀 Scope의 규칙을 따르도록 빈 정의를 만들 수 있습니다:
<bean id="..." class="..." scope="thread">
커스텀 범위의 구현을 사용하면 범위의 프로그래밍 방식 등록에 제한되지 않습니다. 또한 다음 예시와 같이 CustomScopeConfigurer 클래스를 사용하여 범위 등록을 선언할 수 있습니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread">
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
<bean id="thing2" class="x.y.Thing2" scope="thread">
<property name="name" value="Rick"/>
<aop:scoped-proxy/>
</bean>
<bean id="thing1" class="x.y.Thing1">
<property name="thing2" ref="thing2"/>
</bean>
</beans>
참고
FactoryBean 구현을 위해 <bean> 정의 내부에 <aop:scoped-proxy/>를 사용할 때, 지정되는 범위는 팩토리 빈 자체이지 getObject()에서 반환되는 객체가 아닙니다.
'Spring' 카테고리의 다른 글
@DirtiesContext를 사용하지 않고 테스트 데이터베이스 초기화하기 (0) | 2023.07.16 |
---|---|
[Spring 공식문서] Spring Core 1.4 (0) | 2023.04.30 |
[Spring] DAO는 어떤 값을 주고 받아야 할까?(DAO, DTO, Entity) (2) | 2023.04.20 |
[Spring 공식문서] Spring Core 1.1 ~ 1.3 (0) | 2023.04.16 |