본문 바로가기

Spring 프레임워크/이론

Spring의 빈 생명주기(Bean Lifecycle)

스프링의 컨테이너는 Bean 객체들을 관리한다. 객체들을 관리한다는 것은 단순히 싱글턴으로 제공하거나 필요한 곳에 주입하는 것뿐 아니라 객체의 생성과 소멸, 즉 생명주기(Lifecycle)를 관리한다는 것을 의미한다.

컨테이너와 빈의 생명주기

스프링 컨테이너 자체도 생명 주기가 있다. 간단하게는 다음처럼 ApplicationContext 구현 클래스를 이용하여 초기화하고 close 메서드로 종료하는 것을 예로 들 수 있다.

public class Main {
    public static void main(String[] args){
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppContext.class);
        Greeter g1 = ctx.getBean("greeter", Greeter.class);
        Greeter g2 = ctx.getBean("greeter", Greeter.class);

        String msg1 = g1.greet("Spring");
        g1.setFormat("%s, bye!");
        String msg2 = g2.greet("Spring");

        System.out.println(msg1); // Spring, Hello!
        System.out.println(msg2); // Spring, bye!
        System.out.println(String.format("%s: %s", "Is g1 and g2 same?", g1 == g2)); // Is g1 and g2 same?: true
        ctx.close();
    }
}

위의 코드에서는 AnnotationConfigApplicationContext 클래스의 객체(ctx 변수)를 이용하여 스프링 컨테이너를 초기화하고 있다. 초기화가 끝난 후 컨테이너에는 Bean 객체들이 등록되었기 때문에 위의 코드처럼 "greeter"라는 이름의 객체를 얻어와서 사용할 수 있다.

마지막에 close 메서드를 호출하여 컨테이너를 종료하고 있는데 이때 컨테이너 내부의 Bean 객체들은 소멸된다. 그렇기 때문에 더 이상 getBean 메서드로 Bean 객체들을 얻어올 수 없으며 만약 "greeter" 객체를 얻어오려고 하면 위처럼 IllegalStateException을 발생시킨다.

 

즉 컨테이너는 초기화될 때 Bean 객체들을 등록, 생성, 주입하고 종료할 때 Bean 객체들을 소멸시키면서 그 생명 주기를 관리한다는 것을 알 수 있다. 이 Bean 객체들의 생명주기는 간단하게 말하면 객체의 생성, 초기화, 사용, 소멸이지만 자세히 보면 다음과 같다.

https://javaslave.tistory.com/49

먼저 다양한 방법(자바 코드, 어노테이션, XML 등)으로 설정 파일을 읽어서 Bean 객체를 생성, 정확히는 인스턴스화(instantiation)한다. 그 후 프로퍼티 설정 파일을 읽어서 해당되는 Bean 객체에 의존성을 주입하고 Aware 시리즈 인터페이스(BeanNameAware, BeanFactoryAware 등)를 구현한 경우 해당되는 메서드를 호출한다.

 

그리고 객체를 생성하는 생명 주기 콜백 메서드는 @PostConstruct, InitializingBean 인터페이스, init-method 속성 순으로 호출된다. 이렇게 등록된 Bean 객체는 필요한 곳에서 사용되다가 컨테이너가 종료되면 객체가 소멸할 때는 비슷하게 @PreDestroy, DisposableBean 인터페이스, destroy-method 속성 순으로 호출된다.

 

즉 Bean 객체의 생명주기는 해당 객체가 언제, 어떻게 생성되어 소멸되기 전까지 어떤 작업을 수행하고 언제, 어떻게 소멸되는지 위와 같은 일련의 과정을 이르는 말이다. 스프링의 컨테이너는 이런 Bean 객체의 생명 주기를 컨테이너의 생명 주기 내에서 관리하고 객체 생성이나 소멸 시 호출될 수 있는 콜백 메서드를 제공하고 있다. 이는 콜백 인터페이스 구현, Aware 시리즈 인터페이스 구현, Bean 클래스 내부에 커스텀 메서드 구현, @PostConstruct, @PreDestroy 어노테이션 등으로 구현할 수 있으며 이 중 몇 가지를 살펴보면 아래와 같다.

InitializingBean, DisposableBean

스프링에서는 InitializingBean, DisposableBean이라는 인터페이스를 제공하고 있다. 이 인터페이스는 Bean 객체의 생성, 소멸 시 호출되는 콜백 메서드를 정의하고 있으며 사용자는 이를 구현한 Bean 객체를 작성해야 한다. 각 인터페이스는 afterPropertiesSet(), destroy() 콜백 메서드를 정의하고 있는데 각각 객체 생성 후, 객체 소멸 중(during)에 호출되는 메서드다.

 

InitializingBean 인터페이스는 BeanFactory에 의해 해당 Bean 객체의 프로퍼티가 모두 설정된 후 단 한 번만 반응(react), 즉 afterPropertiesSet 메서드를 호출한다. 이는 객체에 특별한 프로퍼티를 추가적으로 설정하거나 필수적으로 요구되는 사항들이 모두 충족되었는지 검사하는 등 다양한 목적으로 사용된다.

 

DisposableBean 인터페이스는 BeanFactory에 의해 객체 소멸 시 destroy 메서드를 호출한다. 이는 어떤 자원을 해제할 필요가 있는 Bean 객체들이 구현하는 인터페이스다. 스프링 컨테이너(ApplicationContext)는 종료 시 애플리케이션 생명주기에 의해 모든 싱글턴 Bean 들을 폐기(dispose)하는데 이때 이 destroy 메서드가 호출되는 것이다.

    ...
    @Override
    public void destroy() throws Exception {
        System.out.println("I'm gone.");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("Greeter is born.");
    }
    ...
    ...
    System.out.println(String.format("%s: %s", "Is g1 and g2 same?", g1 == g2));
    ctx.close();
    System.out.println("Spring container is closed.");
    ...

확인을 위해 위처럼 객체 생성 시 "Greeter is born.", 객체 소멸 시 "I'm gone."이라는 문자열을 출력시키고 스프링 컨테이너를 종료한 후에 "Spring container is closed."라는 문자열을 출력시켰을 때 위처럼 객체 생성, 객체 소멸, 스프링 컨테이너 종료 순으로 문자열이 출력된 것을 볼 수 있다. 

 

객체 소멸 메서드는 언급했듯이 어떤 자원을 해제해야 하는 경우 유용한데 예를 들면 데이터베이스 서버와 연결된 커넥션을 해제하는 작업이다. 커넥션이 계속 쌓이다 보면 데이터베이스 커넥션 풀이 가득 차서 더 이상 서버에 아무도 연결하지 못할 테니 관련 작업이 끝난 후에 해제하는 코드를 구현하는 것이다.

initMethod, destroyMethod

이런 인터페이스를 구현하지 않고 @Bean 어노테이션에서 initMethod, destroyMethod 속성을 사용하여 초기화, 소멸 메서드를 각각 지정할 수 있다. 이는 수정할 수 없는 외부 클래스, 정확히는 위의 두 인터페이스를 구현시킬 수 없는 클래스의 객체를 스프링 컨테이너에 등록할 때 유용하다.

@Configuration
public class AppContext {
    @Bean(initMethod = "construct", destroyMethod = "destroy")
    public Greeter greeter(){
        Greeter g = new Greeter();
        g.setFormat("%s, Hello!");
        return g;
    }
}
    ...
    public void construct(){
        System.out.println("Constructed with @bean.");
    }

    public void destroy() {
        System.out.println("Destroyed with @bean.");
    }
    ...

사용 방법은 단순히 위의 설정 클래스처럼 Bean 객체를 등록할 때 어노테이션의 initMethod, destroyMethod 속성에 각각 객체 생성 중이나 소멸 중에 호출될 메서드의 이름을 적어주면 된다. 실행 결과 위처럼 잘 호출된 것을 볼 수 있다.

@PostConstruct, @PreDestroy

스프링 2.5 이후부터는 메서드에 어노테이션을 붙여서 해당 메서드를 생명주기 메서드로 지정할 수 있다.

그러나 이 블로그 포스트에 따르면 현재는 deprecated 되었기 때문에 따로 의존성을 추가해줘야 사용할 수 있다. 그래서 굳이 실습하지 않고 참고 사이트에 있는 코드로 대신한다.

package com.howtodoinjava.task;
 
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
 
public class DemoBean 
{
    @PostConstruct
    public void customInit() 
    {
        System.out.println("Method customInit() invoked...");
    }
     
    @PreDestroy
    public void customDestroy() 
    {
        System.out.println("Method customDestroy() invoked...");
    }
}

생성 시 호출되는 메서드에 붙은 @PostConstruct 어노테이션은 객체의 생성 및 초기화가 끝나고 이 객체를 의존하는 곳에 주입하기 직전에 호출된다고 한다. 비슷하게 @PreDestroy 어노테이션은 이 객체가 스프링 컨테이너에서 소멸되기 직전에 호출된다고 한다.

 

이런 생명주기와 관련된 메서드들은 스프링 프레임워크에서 따로 중복 실행을 방지하거나 그런 게 없기 때문에 위의 인터페이스, @Bean 어노테이션 두 방법을 같이 적용할 경우 위처럼 인터페이스, 커스텀 메서드 순서로 실행된다. 그러므로 두 방법 중 하나만 사용해서 초기화 또는 소멸을 진행해야 할 것이다.

 

Aware 인터페이스를 구현하는 방법은 아직 학습이 부족하기 때문에 이번 포스트에서 다루지 않았다. 나중에 기회가 되면 추가하도록 하겠다.

 

 

[참고 | howtodoinjava.com/spring-core/spring-bean-life-cycle/]

[참고 | reflectoring.io/spring-bean-lifecycle/]