본문 바로가기
Spring Framework

[Spring Core] Bean Scope(빈 스코프)와 Bean의 LifeCycle(빈의 생명주기)

by kgvovc 2021. 4. 5.
반응형

Bean Scope

DI 컨테이너는 빈 간의 의존 관계를 관리할 뿐만 아니라 빈의 생존 기간도 관리한다. 빈의 생존 기간을 빈 스코프(Bean Scope)라고 하는데 개발자가 직접 빈의 스코프를 다루지 않아도 된다는 점은 DI 컨테이너를 사용하는 큰 이유이기도 하다.

 

DI 컨테이너가 관리하는 빈은 기본적으로 싱글턴으로 만들어진다.

 

 

 

 

스프링 프레임워크에서 사용 가능한 스코프의 종류는 다음과 같다. 이 중에는 웹 환경에서만 사용 가능한 것도 있다.

 

스프링 프레임워크에서 사용 가능한 스코프

스코프 설명
singleton DI 컨테이너를 기동할 때 빈 인스턴스 하나가 만들어지고, 이후부터는 그 인스턴스를 공유하는 방식이다. 기본 스코프이기 때문에 별도로 스코프를 지정하지 않았다면 singleton으로 간주한다.
prototype DI 컨테이너에 빈을 요청할 때마다 새로운 빈 인스턴스가 만들어진다. 멀티 스레드 환경에서 오동작이 발생하지 않아야 하는(thread-safe) 빈이라면 singleton 스코프가 아닌 prototype을 사용해야 한다.
request HTTP 요청이 들어올 때마다 새로운 빈 인스턴스가 만들어진다. 웹 애플리케이션을 만들 때만 사용할 수 있다.
session HTTP 세션이 만들어질 때마다 새로운 빈 인스턴스가 만들어진다. 웹 애플리케이션을 만들 때만 사용할 수 있다.
global session 포틀릿(portlet) 환경에서 글로벌 HTTP 세션이 만들어질 때마다 새로운 빈 인스턴스가 만들어진다. 포틀릿을 사용한 웹 애플리케이션을 만들 때만 사용할 수 있다.
application 서블릿 컨텍스트(Servlet Context)가 만들어질 때마다 빈 인스턴스가 만들어진다. 웹 애플리케이션을 만들 때만 사용할 수 있다.
custom 스코프 이름을 직접 정할 수 있고 정의한 규칙에 따라 빈 인스턴스를 만들 수 있다.

 

 

 

 

 

[스코프 설정]

DI 컨테이너에 등록된 빈은 빈 스코프singleton이다. 즉 DI 컨테이너에서 빈을 가져오려 할 때 같은 것이 없으면 새로 만들고, 같은 것이 있을 때는 이미 만들어진 것을 공유한다. 그리고 DI 컨테이너가 파괴될 때 그 안에 있던 빈도 파괴된다.

 

만약 기본 스코프(Singleton)가 아닌 다른 스코프로 빈을 사용하고 싶다면 빈을 정의하는 단계에서 스코프를 명시해야 한다. 자바 기반 설정 방식이나 XML 기반 설정 방식, 애너테이션 기반 설정 방식 모두에서 설정 가능하다.

 

 

 

자바 기반 설정 방식

자바 기반의 설정 방식에서는 @Bean 애너테이션이 붙은 메서드에 @Scope 애너테이션(org.springframework.context.annotation.Scope)을 추가해서 스코프를 명시한다.

 

  • UserService를 prototype 스코프로 설정(자바 기반 설정 방식)
@Bean
@Scope("prototype")
UserService userService() {
	return new UserServiceImpl();
}

 

이 같이 설정된 경우, 다음의 userService1userService2는 서로 다른 인스턴스가 된다.

 

  • DI 컨테이너에서 prototype 스코프의 빈 가져오기
UserService userService1 = context.getBean(UserService.class);
UserService userService2 = context.getBean(UserService.class);

 

 

 

 

XML 기반 설정 방식

XML 기반 설정 방식에서는 <bean> 요소의 scope 속성에서 스코프를 지정할 수 있다.

 

  • UserService를 prototype 스코프로 설정(XML 기반 설정 방식)
<bean id="userService" class="com.example.deom.UserServiceImpl" scope="prototype" />

 

 

 

 

 

애너테이션 기반 설정 방식

애너테이션 기반 설정 방식에서는 스캔 대상이 되는 클래스에 @Scope 애너테이션을 추가해서 스코프를 명시한다.

 

  • UserService를 prototype 스코프로 설정(애너테이션 기반 설정 방식)
@Component
@Scope("prototype")
public class UserServiceImpl implements UserService {
	// 생략
}

 

 

 

 

 

 

 

[다른 스코프의 빈 주입]

스코프빈의 생존 기간을 의미한다. 그래서 빈 간의 스코프가 서로 다르다는 말은 곧 각 빈의 수명이 다르다는 말이기도 하다. 예를 들어, singleton 스코프가 prototype 스코프보다 더 오래 산다. 웹 애플리케이션 환경이라면 request < session < singleton 순으로 뒤로 갈수록 더 오래 산다.

 

DI 컨테이너에서는 빈 간의 의존 관계가 형성되는데, 만약 하나의 빈이 또 다른 빈에 의존하고 있다면 DI 컨테이너에 의해 주입된 빈은 자신의 스코프와 상관없이 주입받는 빈의 스코프를 따르게 된다.

예를 들어, prototype 스코프의 빈을 singleton 스코프의 빈에 주입한 경우를 생각해보자. 주입된 prototype 스코프의 빈은 자신을 주입받은 singleton 스코프의 빈이 살아 있는 한 DI 컨테이너에서 다시 만들 필요가 없기 때문에 결과적으로 singleton과 같은 수명을 살게 된다.

 

조금 더 구체적인 예를 들어보자. 다음은 prototype 스코프로 설정된 PasswordEncoder다. 참고로 이 빈은 멀티 스레드 환경에서 안전(thread-safe)하지 않기 때문에 반드시 요청을 받을 때마다 새로 생성하는 prototype 스코프로 동작해야 한다. 만약 singleton 스코프로 처리되어 여러 스레드가 동시에 이 빈을 이용하면 오동작을 일으킬 수 있다.

 

  • Prototype 스코프로 PasswordEncoder 빈 정의
@Bean
@Scope("prototype")
PasswordEncoder passwordEncoder() {
    // 멀티 스레드 환경에서 안전하지 않으므로 singleton으로 사용하면 안 됨
	return new ThreadUnsafePasswordEncoder();
}

 

 

다음은 이 빈을 UserService 빈이 사용하는 경우다. 참고로 UserService의 스코프는 singleton이다.

 

  • singleton 스코프인 UserService에 prototype 스코프인 PasswordEncoder 주입
@Component
public class UserServiceImpl implements UserService {
    @Autowired
    PasswordEncoder passwordEncoder;
    
    public void register(User user, String rawPassword) {
        String encodedPassword = passwordEncoder.encode(rawPassword);
        // 생략
    }
}

 

이런 구성이면 PasswordEncoderprototype 스코프로 정의돼 있다 하더라도 UserService의 스코프가 singleton이므로 PasswordEncoder매번 새로운 인스턴스가 만들어지지 않고 이미 만든 인스턴스를 재사용하게 된다. 즉 register 메서드를 두 번 실행하면 같은 PasswordEncoder 인스턴스가 두 번 사용되는 셈이다. 이렇게 되면 굳이 PasswordEncoderprototype 스코프로 정의한 것이 소용없어진다. 또한 여러 스레드가 동시에 사용할 때 오동작이 발생할 수 있는 멀티 스레드에 안전하지 않은 빈이라면 반드시 prototype 스코프로 동작해야 한다. 그러면 이럴 때 어떻게 해야 할까?

 

 

 

 

 

 

 

[룩업 메서드 인젝션으로 해결]

위 문제를 해결하는 가장 좋은 방법은 PasswordEncoder를 주입하지 않는 것이다. 그 대신 필요할 때마다 DI 컨테이너에서 빈을 찾아오면 된다. 우선 가장 쉬운 방법부터 살펴보자.

 

 

직접 컨테이너에서 꺼내오기(안좋은 코드)

 

  • ApplicationContext에서 빈을 직접 찾아서 꺼내오기
@Component
public class UserServiceImpl implements UserService {
    @Autowired
    ApplicationContext context;
    public void register(User user, String rawPassword) {
        PasswordEncoder passwordEncoder = passwordEncoder();
        String encodedPassword = passwordEncoder.encode(rawPassword);
        
       	// 생략
    }
    
    PasswordEncoder passwordEncoder() {
        return this.context.getBean(PasswordEncoder.class);
    }
}

 

이 코드는 동작하는 데 큰 문제는 없다. 의존 관계에 있는 빈끼리 낮은 결합도를 유지하기 위해 DI 컨테이너를 사용했다. 하지만 한 가지 흠이라면 DI 컨테이너를 사용하는 과정에서 DI 컨테이너 의존적인 클래스와 API가 소스코드 상에 노출됐다는 것이다. 빈 간의 의존 관계는 해결했으나 DI 컨테이너와의 의존 관계가 소스코드 상에 남았으니 이런 방식은 사실상 바람직하지 못하며 될 수 있으면 피하는 것이 좋다.

 

 

 

 

 

Lookup Method Injection

그렇다면 어떻게 해야 DI 컨테이너와 관련된 코드를 소스코드에 남기지 않고 빈을 찾아오게 만들까? DI 컨테이너에는 앞서 살펴본 passwordEncoder 메서드 같은 코드를 바이트코드 형태로 만드는 기능이 있다. 즉 DI 컨테이너가 빈을 룩업(Lookup)하는 메서드를 만든 다음, 그 메서드를 의존할 빈에게 주입하면 되는데, 이 기능을 룩업 메서드 인젝션(Lookup Method Injection)이라 부른다.

 

이 기능을 사용하려면 DI 컨테이너에게 룩업을 대행하게 하고 싶은 메서드에 @Lookup 애너테이션(org.springframework.beans.factory.annotation.Lookup)을 붙여주면 된다. 그러면 이 빈이 DI 컨테이너에 등록되는 시점에 DI 컨테이너에서 빈을 찾는 실제 코드가 @Lookup 애너테이션이 붙은 메서드 자리에 주입된다.

 

  • 룩업 메서드 인젝션을 활용한 의존 관계 정의(애너테이션 기반 설정 방식)
@Component
public class UserServiceImpl implements UserService {
    public void register(User user, String rawPassword) {
        PasswordEncoder passwordEncoder = passwordEncoder();
        String encodedPassword = passwordEncoder.encode(rawPassword);
        // 생략
    }
    
    @Lookup
    PasswordEncoder passwordEncoder() {
        return null; // 반환값은 널이라도 상관없다.
    }
}

 

 

이 동작 원리를 조금 더 구체적으로 설명하자면, 우선 DI 컨테이너는 UserServiceImpl 클래스의 서브 클래스를 동적으로 만든다. 이때 DI 컨테이너는 기존의 passwordEncoder 메서드를 DI 컨테이너가 직접 만든 룩업 메서드로 override한다. 따라서 @Lookup을 붙인 메서드에는 private이나 final을 지정하면 안 된다. 그리고 메서드의 매개변수 역시 지정하면 안 된다. 왜냐하면 DI 컨테이너가 해당 메서드를 오버라이드하는 데 방해되기 때문이다.

 

그렇다면 룩업할 대상은 어떻게 찾을까? @Lookup 애너테이션의 value 속성에는 빈의 이름을 지정할 수 있다. 만약 별도의 value 속성을 지정하지 않았다면 그때는 메서드의 반환값 타입을 보고 룩업 대상 빈을 찾게 된다.

 

한편, XML 기반 설정 방식에서는 <lookup-method> 요소를 통해 룩업 메서드 인젝션을 사용할 수 있다. 다음 예를 보자.

 

  • 룩업 메서드 인젝션을 활용한 의존 관계 정의(XML 기반 설정 방식)
<bean id="passwordEncoder" class="com.example.demo.ThreadUnsafePasswordEncoder" scope="prototype" />

<bean id="userService" class="com.example.demo.UserServiceImpl">
	<lookup-method name="passwordEncoder" bean="passwordEncoder" />
    <!-- name 속성에 룩업 메서드명을 지정하고 bean 속성에 룩업할 빈의 이름을 지정한다. -->
    <!-- 생략 -->
</bean>

 

이처럼 룩업 메서드 인젝션은 서로 다른 스코프의 빈을 조합하면서 생기는 문제를 해결할 뿐만 아니라 소스코드에서 직접 DI 컨테이너를 사용하는 것을 방지하는 용도로도 활용할 수 있다. 참고로 자바 기반 설정 방식에서는 룩업 메서드 인젝션을 사용하지 못한다.

 

 

 

 

 

 

 

[Scoped Proxy]

앞서 살펴본 것처럼 의존 관계에 있는 빈의 스코프가 다를 경우, 의도치 않은 오동작이 발생할 수 있다. 이것을 해결할 방법으로 룩업 메서드 인젝션을 살펴봤는데, 그 밖에도 Scoped Proxy라는 방법도 활용할 수 있다. 이 방법은 이름에서 짐작할 수 있듯이 기존의 빈을 Proxy로 감싼 후, 이 proxy를 다른 빈에 주입하고, 주입받은 빈에서 이 proxy의 메서드를 호출하면 proxy 내부적으로 DI 컨테이너에서 빈을 룩업하고 룩업된 빈의 메서드를 실행하는 방식이다.

 

이 방법은 보통 request 스코프나 session 스코프와 같이 수명이 짧은 빈을 singleton 스코프와 같은 상대적으로 수명이 긴 빈에 주입할 때 많이 사용한다.

 

구체적인 예로 앞서 살펴본 ThreadUnsafePasswordEncoderrequest 스코프로 사용하는 경우를 생각해보자. Scoped Proxy를 활성화할 때는 @Scope 애너테이션을 붙인 다음, proxyMode 속성에 proxy를 만드는 방법을 지정하면 된다.

 

  • Scoped Proxy 활성화(자바 기반 설정 방식)
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
PasswordEncoder passwordEncoder() {
	return new ThreadUnsafePasswordEncoder();
}

 

한편 PasswordEncoder를 사용하는 UserServiceImpl은 다음과 같다.

 

  • UserServiceImpl에 PasswordEncoder 주입
@Component
public class UserServiceImpl implements UserService {
    @Autowired
    PasswordEncoder passwordEncoder;
    
    public void register(User user, String rawPassword) {
        String encodedPassword = passwordEncoder.encode(rawPassword);
        // 생략
    }
}

 

Scoped proxy가 활성화된 상태이기 때문에 위의 passwordEncoder 필드에는 PasswordEncoderProxy가 주입되고 encode 메서드가 호출될 때마다 request 스코프의 PasswordEncoder 인스턴스가 만들어진다.

 

Scoped Proxy를 사용하려면 proxyMode 속성에 다음 중 하나를 지정한다.

  • ScopedProxyMode.INTERFACES: JDK의 동적 proxy(java.lang.reflect.Proxy)를 사용해 인터페이스 기반의 proxy를 만든다.
  • ScopedProxyMode.TARGET_CLASS: 스프링 프레임워크에 내장된 CGLIB을 사용해 서브 클래스 기반의 proxy를 만든다.

 

 

 

 

[참고]

Scoped Proxy 방식은 proxy 모드에 따라 인터페이스를 기반으로 proxy를 만들거나 서브클래스를 기반으로 proxy를 만든다. 다음은 인터페이스를 기반으로 만들어진 proxy와 서브클래스를 기반으로 만들어진 proxy의 소스코드다. 동적으로 생성되는 만큼 반드시 이렇게 구현된다는 보장은 없지만 두 proxy 모드의 차이를 확인하기에는 충분하니 어떤 형태로 구현되는지 봐두자.

 

인터페이스 기반으로 만들어진 proxy

public class PasswordEncoderProxy implements PasswordEncoder {
 @Autowired
 ApplicationContext context;

 @Override
 public String encode(String rawPassword) {
     PasswordEncoder passwordEncoder = context.getBean("passwordEncoder", PasswordEncoder.class);
     return passwordEncoder.encode(rawPassword);
 }
}

 

서브클래스 기반으로 만들어진 proxy

public class PasswordEncoderProxy extends ThreadUnsafePasswordEncoder {
 @Autowired
 ApplicationContext context;

 @Override
 public String encode(String rawPassword) {
     PasswordEncoder passwordEncoder = context.getBean("passwordEncoder", PasswordEncoder.class);
     return passwordEncoder.encode(rawPassword);
 }
}

 

만약 Scoped proxy를 적용할 대상 빈이 인터페이스를 가지고 있지 않은 경우에는 서브클래스 기반의 proxy를 사용해야 한다. 참고로 서브클래스 기반의 proxy는 메서드를 오버라이드해야 하기 때문에 메서드나 클래스에 final을 붙일 수 없다.

 

 

 

 

 

XML 기반 설정 방식으로 Scoped Proxy를 표현할 때는 <aop:scoped-proxy> 요소를 사용한다. 그리고 <beans> 요소는 aop 요소를 사용하기 위한 XML 네임스페이스(xmlns)와 스키마(xsi) 정보가 추가돼 있어야 한다.

 

  • XML 기반 설정 방식으로 Scoped Proxy 설정
<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 
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd">
    
	<bean id="passwordEncoder" class="com.example.demo.ThreadUnsafePasswordEncoder" scope="request">
    	<aop:scoped-proxy proxy-target-class="false" />    
    </bean>
    
    <!-- 
	Scoped Proxy를 적용할 빈의 <bean> 요소 아래에 <aop:scoped-proxy> 요소를 정의한다. 
	proxy-target-class 속성을 false로 지정하면 인터페이스를 기반으로 한 proxy가 만들어지고
	true인 경우에는 서브클래스 기반 proxy가 만들어진다.
	-->
    
    <bean id="userService" class="com.example.demo.UserServiceImpl">
    	<property name="passwordEncoder" ref="passwordEncoder" />
        <!-- 생략 -->
    </bean>
</beans>

 

 

애너테이션 기반 설정 방식으로 Scoped Proxy를 표현할 때는 스캔 대상 클래스에 붙인 @Scope 애너테이션에 proxyMode 속성을 추가하면 된다.

 

  • 애너테이션 기반 설정 방식으로 Scoped Proxy 설정하기
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
public class ThreadUnsafePasswordEncoder implements PasswordEncoder {
	// 생략
}

 

스프링 프레임워크 공식 문서에서는 request, session, globalSession 스코프에서 Scoped Proxy를 사용하고, prototype 스코프에 대해서는 룩업 메서드 인젝션을 사용하도록 안내하고 있다.

 

prototype 스코프에서 Scoped Proxy를 사용하지 못하는 것은 아니지만, 주입된 필드에서 Proxy 안에 있는 메서드를 한번 더 호출하기 때문에 매번 새로운 인스턴스가 만들어질 때마다 각 프락시의 메서드가 반복해서 호출되므로 효율성 측면에서 바람직하지 않다는 점을 감안해야 한다.

 

 

 

 

 

 

[커스텀 스코프 만들기]

스프링 프레임워크에서는 미리 만들어져서 제공되는 스코프 외에도 사용자가 직접 정의한 커스텀 스코프(custom scope)를 만들 수 있다. 커스텀 스코프를 구현하려면 Scope 인터페이스(org.springframework.beans.factory.config.Scope)를 구현하고 CustomScopeConfigurer 클래스(org.springframework.beans.factory.config.CustomScopeConfigurer)에 자신이 만든 스코프를 스코프명과 함께 설정하면 된다.

 

다음은 Scope 인터페이스를 직접 구현하는 대신 스프링 프레임워크에서 제공하는 샘플 구현체를 사용한 예다. 샘플로 제공되는 SimpleThreadScope 클래스(org.springframework.context.support.SimpleThreadScope)를 커스텀 스코프라고 생각하고 자바 기반 설정 방식으로 어떻게 설정하는지 확인해보자.

 

  • 커스텀 스코프 설정
@Bean
static CustomScopeConfigurer customScopeConfigurer() {
    CustomScopeConfigurer configurer = new CustomScopeConfigurer();
    configurer.addScope("thread", new SimpleThreadScope());
    return configurer;
}

 

여기까지 하고 나면 스레드 단위로 스코프를 주고 싶은 빈에 @Scope("thread") 애너테이션만 붙이면 된다. 그러면 DI 컨테이너에 해당 빈을 요청할 때마다 스레드 단위로 인스턴스가 만들어질 것이다.

 

 

빈의 생명주기

DI 컨테이너에서 관리되는 빈의 생명 주기는 크게 다음의 세 가지 단계로 구분할 수 있다.

  1. 빈 초기화 단계(initialization)
  2. 빈 사용 단계(activation)
  3. 빈 종료 단계(destruction)

 

위의 세 가지 단계 중 대부분의 시간은 빈의 사용 단계, 즉 애플리케이션이 실행 중인 상태다. 그 밖의 빈 초기화 단계종료 단계에서는 DI 컨테이너가 내부적으로 많은 작업을 하게 되는데 이러한 내부 동작을 이해한다면 전처리나 후처리와 같은 콜백(callback)을 활용할 수 있을 것이다.

 

 

 

 

초기화 단계

초기화 단계는 다시 크게 세 개의 과정으로 나눌 수 있는데

첫 번째빈을 설정하는 과정이고,

두 번째빈을 인스턴스화하고 의존성을 주입하는 과정,

세 번째빈을 생성한 다음의 후처리를 하는 과정이다.

 

(출처: https://longbeom.tistory.com/70)

 

 

 

 

빈 설정 정보 읽기 및 보완

우선 빈을 생성하는 데 필요한 정보를 수집한다. 빈 정의 내용자바 기반 설정 방식으로 Java Configuration 파일에서 읽어오거나, XML 기반 설정 방식으로 XML 파일에서 읽어오거나, 또는 애너테이션 기반 설정 방식으로 컴포넌트 스캔을 통해 읽어온다. 이 단계에서는 정보만 불러올 뿐 아직 빈을 생성한 것은 아니다.

 

빈이 정의된 설정 정보를 모두 수집했다면, 다음으로 Bean Factory Post Processor(BFPP)를 사용해 빈의 정보를 보완하는 작업이 이뤄진다. 이 처리는 BeanFactoryPostProcessor 인터페이스(org.springframework.beans.factory.config.BeanFactoryPostProcessor)를 구현한 클래스가 수행한다.

 

  • BeanFactoryPostProcessor 인터페이스
public interface BeanFactoryPostProcessor {
	void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory);
}

 

빈을 정의할 때 사용되는 프로퍼티 플레이스홀더(placeholder)는 이 시점에 실제 프로퍼티 값으로 치환된다. 마찬가지로 BeanFactoryPostProcessor 인터페이스를 직접 구현해서 빈으로 정의한다면 자신만의 빈 정보 보완 처리도 할 수 있다.

 

 

 

 

 

빈 생성 및 의존 관계 해결

빈 정의 정보를 읽고, 빈 인스턴스를 성공적으로 생성했다면 다음은 빈 간의 의존관계를 해결하기 위해 의존성 주입을 할 차례다. 앞서 세 가지 의존성 주입 방법을 설명했는데 실제로 수행되는 순서는 다음과 같다.

  1. 생성자 기반 의존성 주입
  2. 필드 기반 의존성 주입
  3. 세터 기반 의존성 주입

 

 

 

 

 

 

빈 생성 후 초기화 작업

빈 간의 의존 관계까지 정리되면 마지막으로 빈 생성 후의 초기화 작업(Post Construct)이 수행된다. 이 작업은 크게 전처리, 초기화 처리, 후처리로 구분된다.

 

그 중에서도 초기화 부분은 다양한 설정 방식으로 정의할 수 있으며, 처리 순서는 다음과 같다.

  1. 애너테이션 기반 설정을 사용하는 경우, @PostConstruct 애너테이션(javax.annotation.PostConstruct)이 붙은 메서드
  2. InitializingBean 인터페이스(org.springframework.beans.factory.InitializingBean)를 구현하는 경우, afterPropertiesSet 메서드
  3. 자바 기반 설정을 사용하는 경우, @Bean의 initMethod 속성에 지정한 메서드
  4. XML 기반 설정을 사용하는 경우, <bean> 요소의 init-method 속성에 지정한 메서드

 

이렇게 빈이 생성된 후에 이뤄지는 초기화는 빈을 생성할 때 해주는 초기화와 큰 차이가 있는데, 그것은 바로 의존성 주입이 끝난 필드 값을 초기화에 활용할 수 있다는 점이다.

 

한편, 전처리후처리BeanPostProcessor(BPP) 인터페이스(org.springframework.beans.factory.config.BeanPostProcessor)의 메서드를 통해 실행된다. 이 메서드는 다음과 같은 형태다.

 

  • BeanPostProcessor 인터페이스의 전처리, 후처리 메서드
public interface BeanPostProcessor {
	// 전처리
	Object postProcessBeforeInitialization(Object bean, String beanName);
    // 전처리
    Object postProcessAfterInitialization(Object bean, String beanName);
}

 

만약 빈을 초기화하는 과정에서 전처리후처리가 필요하다면 BeanPostProcessor 인터페이스를 구현하면서 postProcessBeforeInitializationpostProcessAfterInitialization 메서드를 확장 지점으로 활용하면 된다.

 

 

 

다음은 @PostConstruct를 활용한 예다. 이때 UserServiceImpl@Component 애너테이션이 붙어 있으므로 빈 정의를 XML 기반으로 하는 경우에는 <context:annotation-config><context:component-scan> 요소를 정의해야 한다. 이것은 뒤에 설명할 @PreDestroy에서도 마찬가지다.

 

  • @PostConstruct 애너테이션 활용
@Component
public class UserServiceImpl implements UserService {
	// 생략
    
    @PostConstruct
    void populateCache() {
        // 캐시 등록
    }
}

 

주의할 점은 @PostConstruct 애너테이션이 붙는 메서드는 반환값이 void이고 메서드의 매개 변수는 없어야 한다는 것이다. 이 같은 처리는 InitializingBean 인터페이스를 구현한 다음, afterProperties 메서드로 대체할 수도 있다.

 

 

  • InitializingBean 인터페이스 구현
@Component
public class UserServiceImpl implements UserService, InitializingBean {
    // 생략
    
    @Override
    public void afterPropertiesSet() {
        // 캐시 등록
    }
}

 

만약 다루려는 빈이 직접 개발한 것이 아니거나 서드파티 라이브러리 형태로 사용하고 있어서 소스코드에 @PostConstruct 애너테이션을 붙이거나 InitializingBean 인터페이스를 구현하지 못할 수도 있다. 이 경우에는 다음과 같이 빈을 정의할 때 초기화 메서드명을 지정하는 방법으로 대체할 수 있다.

 

  • 자바 기반 설정 방식에서 초기화 메서드 지정
@Bean(initMethod = "populateCache")
UserService userService() {
	return new UserServiceImpl();
}

 

  • XML 기반 설정 방식에서 초기화 메서드 지정
<bean id="userService" class="com.example.demo.UserServiceImpl" init-method="populateCache" />

 

 

 

 

 

 

 

 

종료 단계

DI 컨테이너가 역할을 다하고 파괴될 때가 되면 그 안에 관리되던 빈도 역시 파괴되는 절차를 밟게 된다. 경우에 따라서는 파괴되기 전에 마지막으로 처리해야 하는 작업이 있을 수 있는데, 이를 위해 스프링 프레임워크에서는 빈이 파괴되기 전에 전처리(Pre Destroy)를 할 수 있는 방법을 제공한다.

 

 

빈이 파괴되기 전에 전처리 수행

빈이 파괴되기 전의 전처리(PreDestroy)는 빈 생성 후의 초기화(PostConstruct)와 동작 방식이나 구조는 대칭되면서도 내용 면에서는 정반대로 동작한다. 전처리 부분은 다양한 설정 방식으로 정의할 수 있으며, 처리 순서는 다음과 같다.

  1. 애너테이션 기반 설정을 사용하는 경우, @PreDestroy 애너테이션(javax.annotation.PreDestroy)이 붙은 메서드
  2. DisposableBean 인터페이스(org.springframework.beans.factory.DisposableBean)를 구현하는 경우, destroy 메서드
  3. 자바 기반 설정을 사용하는 경우, @Bean의 destroyMethod 속성에 지정한 메서드
  4. XML 기반 설정을 사용하는 경우, <bean> 요소의 destroy-method 속성에 지정한 메서드

 

@PreDestroy를 사용한 예는 다음과 같다.

 

  • @PreDestroy 애너테이션 활용
@Component
public class UserServiceImpl implements UserService {
    // 생략
    
    @PreDestroy
    void clearCache() {
        // 캐시 삭제
    }
}

 

이 같은 처리는 DisposableBean 인터페이스를 구현한 다음, destroy 메서드로 대체할 수도 있다.

 

  • DisposableBean 인스턴스 구현
@Component
public class UserServiceImpl implements UserService, DisposableBean {
    // 생략
    
    @Override
    public void destroy() {
        // 캐시 삭제
    }
}

 

또한 빈을 생성한 후, 초기화할 때와 마찬가지로 다루려는 빈이 직접 개발한 것이 아니거나 서드파티 라이브러리 형태로 사용하고 있어서 소스코드에 @PreDestroy 애너테이션을 붙이거나 DisposableBean 인터페이스를 구현하지 못할 수 있다. 이 경우에는 다음과 같이 빈을 정의할 때 파괴 메서드명을 지정하는 방법으로 대체할 수 있다.

 

  • 자바 기반 설정 방식에서 파괴 메서드 지정
@Bean(destroyMethod = "clearCache")
UserService userService() {
    return new UserServiceImpl();
}

 

  • XML 기반 설정 방식에서 파괴 메서드 지정
<bean id="userService" class="com.example.demo.UserServiceImpl" destroy-method="clearCache" />

 

참고로 빈 파괴 전의 전처리(PreDestroy) 작업은 prototype 스코프의 빈에서는 동작하지 않으므로 주의하자.

 

 

 

 

 

 

DI 컨테이너 종료

ConfigurableApplicationContext 인터페이스(org.springframework.context.ConfigurableApplicationContext)는 ApplicationContext 인터페이스를 확장한 서브 인터페이스로, 우리가 사용하는 DI 컨테이너가 바로 ConfigurableApplicationContext 인터페이스의 구현체다. 이 인터페이스에는 close라는 메서드가 있는데 이것을 호출하면 DI 컨테이너가 종료된다. 대부분의 경우, 직접 DI 컨테이너를 종료할 일은 없지만 굳이 프로그램적으로 종료시키고 싶다면 다음과 같이 구현한다.

 

  • DI 컨테이너 종료
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 애플리케이션 코드
context.close();

 

ConfigurableApplicationContextjava.io.Closeable 인터페이스를 구현하고 있으므로 다음과 같이 try-with-resources 구문으로 기술할 수도 있다.

 

  • try-with-resources 구문을 활용한 DI 컨테이너 종료
try (ConfigurableApplicationContext context = 
    new AnnotationConfigApplicationContext(AppConfig.class)) {
    // 애플리케이션 코드
}

 

명시적으로 close를 호출해서 닫기가 곤란한 경우에는 다음과 같이 JVM을 종료(Shutdown)할 때 함께 종료되도록 훅(Hook)으로 등록할 수 있다.

 

  • JVM 셧다운 시 DI 컨테이너를 종료하도록 훅 등록
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.registerShutdownHook();
반응형

댓글