Framework/Spring

[Spring] Bean의 LifeCycle는 어떻게 될까?

KOOCCI 2022. 7. 31. 20:34

목표 : Spring에서 Bean의 LifeCycle에 대해 설명할 수 있다.


Bean에도 LifeCycle이 있을 것이고, 그에 따라 여러 설정값들도 있을 것이다.

그에 대해 알아보면서, 결국 서블릿 컨테이너를 통해 서블릿이 관리되고, 하나의 서블릿에서 Application Context가 관리하는 Bean들이 실행되는 전체적인 관점도 알아보도록 하자.

 

Customizing the Nature of a Bean

계속 참고하여 보던 문서의 제목이며, Bean의 성질이 어떻게 되는지 보고, 수정할 수 있는 방향을 알아보자.

그럼 LifeCycle에 대해 알아보자.

 

LifeCycle

Spring에서는 객체를 다루므로, 객체를 만들거나 객체를 없애는 행위를 할 수 있다.

객체를 만들 때는 new로 객체를 만드는 작업을 Spring이 한다. 즉, 해당 시점에 Callback을 호출 할 수 있다.

또한, 특정 Bean에 대해 Close, 자원 Release할 때도 Callback을 할 수 있다.

어떻게 할 수 있는지에 대해서는 크게 3가지 방법을 설명한다.

  1. InitializingBean,DisposableBean이라는 인터페이스를 구현하는 방법
  2. JSR-250의 @PostConstruct 및 @PreDestroy를 사용하는 방법
  3. XML 설정을 통해, init-method, destroy-method를 하는 방법

어노테이션을 활용하는 것은 다음에 진행할 예정이므로, 2번을 제외한 다른 것들부터 보도록 하자.

 

Initialization Callbacks

Bean이 만들어질 때, CallBack 함수가 실행되는 것들을 알아보자.

InitializingBean

InitializingBean 을 보면, 하나의 메서드를 지원하고 있다. (afterPropertiesSet())

해당 메서드의 역할도 함께 적혀있다.

Invoked by the containing BeanFactory after it has set all bean properties and satisfied BeanFactoryAware, ApplicationContextAware etc.

모든 bean Property 설정과 BeanFactoryAware, ApplicationContextAware 등이 끝난 후에, container인 BeanFactory에 의해 호출된다.

init-method

XML로 설정하는 방법이다.

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>

init-method 이름이 init이니, 해당 init 이란 이름의 메서드를 만들어 활용하면 된다.

 

Destruction Callbacks

Bean이 사라질 때의 Callback을 알아보자.

DisposableBean

InitailizingBean과 반대되는 개념이다.

이 역시, 문서를 보면 destroy() 메서드 한가지만 구현할 수 있게 되어있다.

bean에 대해, Interface를 구현하여 사용하면 되지만, 한가지 문제가 생길 수 있다.

ApplicationContext context = new ClassPathXmlApplicationContext("test.xml");

 

ClassPathXmlApplicationContext는 정말 많은 추상클래스, 인터페이스들을 상속받고 있는데, 실제로 Assign한 타입은 ApplicationContext이다.

 

다시말해, Java에서의 형변환 업캐스팅으로 ClassPathXmlApplicationContext모든 멤버 변수에 대한 메모리는 생성되지만, 변수 타입이 ApplicationContext이므로 실제로 접근 가능한 변수나 메서드는ApplicationContext의 변수와 메서드가 된다.

 

ClassPathXmlApplicationContext 에서는 결국 ConfigurableApplicationContext 라는 Interface를 상속받고 있고, ConfigurableApplicationContext는 ApplicationContext, Lifecycle, Closeable 을 구현하게 된다.

이중 중요한 것은 Closeable 이다.

 

ApplicationContext 에는 Closeable이 없기 때문에, Close 함수가 호출될 수 없다.

즉, DisposableBean을 상속받아 destroy 메서드를 구현하더라도, 해당 메서드가 호출될 수 없다.

 

따라서, 다음과 같이 구현해야 정상적으로 destroy 메서드가 호출되는 것을 볼 수 있다.

 ConfigurableApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
 
 /**
 * TODO :
 */
 
 context.close();

Destroy-method

XML로 설정하는 방법이며 사용법은 동일하다.

<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>

 

Default Initialization and Destroy Methods

매번 Bean들에게 init이나 destroy 메서드를 할 필요 없이, beans에 default 설정을 할 수 있다.

<beans default-init-method="init" default-destroy-method="destroy">

    <bean id="blogService" class="com.something.DefaultBlogService">
        <property name="blogDao" ref="blogDao" />
    </bean>

</beans>

즉, 각 bean들에게서 init이나 destroy 메서드가 있다면, 각각의 bean들에서 생성/삭제될 때, 해당 메서드들이 실행되는 것이다.

 

Startup and Shutdown Callbacks

앞서, DisposableBean 을 찾아볼 때,  ConfigurableApplicationContext에 대해 찾아보았다.

public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {
    /*...*/
}

Lifecycle이라는 Interface가 있는 것을 볼 수 있다.

따라서, 우리가 context에서 해당 Lifecycle.class를 getBean으로 가져올 수 있고, 이를 사용할 수 있다.

public interface Lifecycle {
    void start();

    void stop();

    boolean isRunning();
}
public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
    Lifecycle lifecycle = context.getBean(Lifecycle.class);
    log.info("isRunning : >> " + lifecycle.isRunning()); // isRunning : >> true
    context.close();
    log.info("isRunning : >> " + lifecycle.isRunning()); // isRunning : >> false
}

해당 Interface를 통해, Spring Container의 Running 유무에 따른 설정에 사용할 수 있다.

 

ApplicationContextAware

우리는 앞서, ClassPathXmlApplicationContext 라는 ApplicationContext를 사용하고 있었다.

이를 Bean에서 사용하고 싶을 때, ApplicationContextAware가 사용된다.

 

아래 예시를 보자.

@Slf4j
public class Aca implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    
    void init() {
      log.info("Null Check >> " + applicationContext);  
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

Aca라는 클래스는 ApplicationContextAware를 구현하고 있고, init 메서드를 통해, Bean 시작점에 자동으로 실행될 메서드도 구현하였다.

ApplicationContextAware 인터페이스는 setApplicationContext 를 구현하도록 되어 있고, 간단히 필드의 ApplicationContext에 값을 넣어주도록 하였다.

 

이렇게 함으로서, Aca를 Bean으로 등록만 한다면, 자동으로 Bean이 생성될 때, init 메서드가 실행될 것이고, Aca에서는 applicationContext 를 set으로 전달받아 사용할 수 있는 상태가 된다.

 

이렇게 ApplicationContextAware를 사용하는 방법을 알아보았다.

 

Aware라는 단어로 붙은 인터페이스들은 다음과 같이, 특정 목적에 따라 사용하는 방법이 위 ApplicationContextAware와 유사하다.

이를 잘 알아보고 사용하도록 하자.

 

Wrap Up

이렇게 Spring 내에서 Bean의 LifeCycle을 알아보았다.

빠진 내용은 아마 Annotation일 것이고, 이건 다시 한 번 정리해 나가도록 하자.

그리고, 앞서 Servlet을 알아보며 여러 자료를 찾게 되다가, 제목이 Coltoller 1개가 어떻게 수 많은 Request를 처리하는가? 라는 블로그를 보자마자 나도 순간 헤깔리게 되었다.

 

Bean이 생성되고 삭제될 때, 기본적으로 Singleton으로 객체가 이루어진다. (Default Bean Scope다)

그리고 Servlet은 이 Bean들을 통해 구현되어지고, Servlet은 Servlet Container 를 통해 관리되어 진다.

 

그렇다면, 하나의 Servlet이 하나의 Path에 대한 Client Request를 처리한다고 할 때 (DispatcherServlet이더라도), 결국 하나의 Bean Instance를 사용하는 것이다.

논리적으로 요청이 동시에 천이든 만이든 큰 단위로 들어올 수 있는데, 그럼 해당 path로 전달받는 Request는 Servlet Container의 Thread 갯수만큼 받는다고 해도, Controller를 담당하는 Servlet이 같은 Bean Instance를 바라보면 데이터가 섞이거나, 문제가 생기지 않을까?

결과부터 보면, Controller 를 담당하는 Bean은 한 개가 생성되며, 그 하나의 컨트롤러가 모든 요청을 처리한다.

 

어찌보면 당연하다.

Controller의 로직은 함께 쓸 수 있지만, 각 요청과 개별적인 Thread의 정보는 동기화될 필요가 없다.

Bean은 Stateless해야 하며, 멤버변수에 값을 저장하는 행위가 위험한 이유다.

멤버변수에 값을 저장하는 순간, 정보는 동기화가 되어버릴 수 있다.

 

 

Spring Bean의 LifeCycle과 함께 헤깔리지 않도록 잘 정리해두자.