빈 생명주기 콜백

2023. 5. 10. 23:04JAVA/Spring

1. 빈 생명주기 콜백

빈 생명주기 콜백이란 무엇인가?

스프링 빈 생명주기 콜백은 스프링 컨테이너가 스프링 빈 인스턴스를 초기화, 소멸하는 과정에서 일어나는 콜백 메소드 호출을 의미합니다.

 

빈 생명주기 콜백의 필요성

콜백은 콜백 함수를 부를 때 사용되는 용어입니다. 콜백 함수란 특정 이벤트가 발생했을때 호출되는 함수를 콜백함수라고 합니다. 콜백 함수는 이벤트 조건에 따라 발생할수도 발생하지 않을수도 있는 함수입니다.

 

콜백이 필요한 이유는 데이터베이스 커넥션 풀(Connection Pool)이나 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요합니다. 이때 객체의 초기화와 종료 작업시 데이터베이스 커넥션 풀이나 네트워크 소켓의 연결과 해제를 하기 위해서 콜백 함수가 필요합니다. 즉, 콜백이 필요한 이유 중 하나는 객체의 초기화와 종료 작업 수행시 호출하기 위해서입니다.

 

빈 생명주기 콜백 방법의 준비 예제

빈 생명주기 콜백 3가지 방법을 실습하기 위해서 다음과 같은 예제를 먼저 준비합니다. 

public class NetworkClient{

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }

    public void setUrl(String url) {
        this.url = url;
    }

    // 서비스 시작시 호출
    public void connect() {
        System.out.println("connect: " + url);
    }

    public void call(String message) {
        System.out.println("call : " + url + ", message = " + message);
    }

    // 서비스 종료시 호출
    public void disConnect() {
        System.out.println("close : " + url);
    }
}

위 예제를 기반으로 테스트 코드를 작성합니다.

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest() {
        //given
        ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(
            LifeCycleConfig.class);
        //when
        NetworkClient networkClient = ctx.getBean(NetworkClient.class);
        ctx.close();
        //then
    }

    @Configuration
    static class LifeCycleConfig {

        @Bean
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
}

실행결과는 다음과 같습니다.

생성자 호출, url = null
connect: null
call : null, message = 초기화 연결 메시지

빈 등록과정에서 생성자가 호출되었고 생성자 내부에서 connect(), call() 메소드가 호출되었습니다. url = null인 이유는 url 설정은 객체가 생성되고 난 이후인 setUrl 메소드를 통하여 주입되기 때문입니다. 위 준비예제에서 말하고 싶은 것은 NetworkClient라는 스프링 빈이 등록되는 과정은 객체를 생성하고 의존관계를 주입(setUrl 호출)하는 과정을 거친다음 스프링 빈으로 등록된다는 것입니다.

 

정리하면 다음과 같습니다. 스프링 빈은 다음과 같은 과정을 거쳐서 스프링 컨테이너에 등록됩니다.

객체 생성 -> 의존관계 주입

 

스프링 빈 이벤트 라이프 사이클

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료
  • 초기화 콜백 : 빈이 생성되고, 빈이 의존관계 주입이 완료된 후 호출
  • 소멸전 콜백 : 빈이 소멸되기 직전에 호출

 

왜 객체의 생성과 초기화를 분리해야 하는가?

생성자의 주요 책임은 파라미터를 받고 메모리를 할당해서 객체를 생성하는 것이 주요한 책임입니다. 반면 초기화 작업은 생성자로 생성된 객체의 필드 멤버 값들을 활용해서 외부 연결을 하는 등의 무거운 동작을 수행하는 작업입니다. 따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것보다는 객체를 생성하는 부분과 무거운 초기화 작업을 따로 나누는 것이 유지보수 관점에서 좋습니다. 만약 초기화 작업이 내부의 값들만 약간 변경하는 단순한 경우에는 생성자에서 한번에 다 처리하는 것이 더 나을 수 있습니다.

 

빈 생명주기 콜백 3가지 방법

  • 인터페이스 InitializingBean, DisposableBean 구현
  • Bean 애노테이션 옵션에 초기화, 소멸 메소드 지정
  • @PostConstruct, @PreDestroy 애노테이션 사용

 

1. 인터페이스 InitializingBean, DisposableBean

InitializingBean 인터페이스는 afterPropertiesSet 메소드를 구현함으로써 스프링 빈 이벤트 라이프 사이클에서 초기화 콜백을 지원합니다.

 

DisposableBean 인터페이스는 destroy 메소드를 구현함으로써 스프링 빈 이벤트 라이프 사이클에서 소멸 전 콜백을 지원합니다.

 

public class NetworkClient implements InitializingBean, DisposableBean {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
    }

    public void setUrl(String url) {
        this.url = url;
    }

    // 서비스 시작시 호출
    public void connect() {
        System.out.println("connect: " + url);
    }

    public void call(String message) {
        System.out.println("call : " + url + ", message = " + message);
    }

    // 서비스 종료시 호출
    public void disConnect() {
        System.out.println("close : " + url);
    }

    // 의존관계 주입이 끝나면 호출됨
    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call("초기화 연결 메시지");
    }

    // 소멸전에 호출
    @Override
    public void destroy() throws Exception {
        disConnect();
    }
}
    @Test
    public void lifeCycleTest() {
        //given
        ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(
            LifeCycleConfig.class);
        //when
        NetworkClient networkClient = ctx.getBean(NetworkClient.class);
        ctx.close();
        //then
    }
    
    @Configuration
    static class LifeCycleConfig {

        @Bean
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
생성자 호출, url = null
connect: http://hello-spring.dev
call : http://hello-spring.dev, message = 초기화 연결 메시지
close : http://hello-spring.dev

실행 결과를 보면 의존관계 주입(setUrl) 후 초기화 콜백(afterPropertiesSet)을 호출한 것을 볼 수 있습니다. 그리고 스프링 컨테이너가 종료되자(ctx.close) 소멸 전 메소드(destroy)가 호출된 것을 볼 수 있습니다.

 

InitializingBean, DisposableBean 인터페이스의 단점

  • 인터페이스가 스프링 전용 인터페이스입니다. 해당 코드(NetworkClient)가 스프링 전용 인터페이스에 의존하게 될 수 밖에 없습니다. 그래서 다른 프레임워크나 스프링 프레임워크가 없는 환경에서는 사용할 수 없습니다.
  • 초기화, 소멸 메소드(afterPropertiesSet, destroy)의 이름을 변경할 수 없습니다.
  • 내가 코드를 고칠 수 없는(읽기전용) 외부 라이브러리에 적용할 수 없습니다.

 

2. 빈 등록 초기화 및 소멸 메소드 지정

수동으로 빈 등록시 빈 설저 정보에 다음처럼 @Bean 애노테이션에 초기화, 소멸 메소드를 설정할 수 있습니다.

@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient2 networkClient2() {
	...
}

 

빈 등록 초기화 및 소멸 메소드 지정 예제는 다음과 같습니다.

public class NetworkClient2 {

    private String url;

    public NetworkClient2() {
        System.out.println("생성자 호출, url = " + url);
    }

    public void setUrl(String url) {
        this.url = url;
    }

    // 서비스 시작시 호출
    public void connect() {
        System.out.println("connect: " + url);
    }

    public void call(String message) {
        System.out.println("call : " + url + ", message = " + message);
    }

    // 서비스 종료시 호출
    public void disConnect() {
        System.out.println("close : " + url);
    }

    // 의존관계 주입이 끝나면 호출됨
    public void init() throws Exception {
        connect();
        call("초기화 연결 메시지");
    }

    // 소멸전에 호출
    public void close() throws Exception {
        disConnect();
    }
}
public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest2() {
        //given
        ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(
            LifeCycleConfig.class);
        //when
        NetworkClient2 networkClient = ctx.getBean(NetworkClient2.class);
        ctx.close();
        //then
    }

    @Configuration
    static class LifeCycleConfig {

        @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient2 networkClient2() {
            NetworkClient2 networkClient = new NetworkClient2();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
}

실행 결과는 이전 방법과 동일합니다.

생성자 호출, url = null
connect: http://hello-spring.dev
call : http://hello-spring.dev, message = 초기화 연결 메시지
close : http://hello-spring.dev

 

빈 등록 초기화 및 소멸 메소드의 특징

  • 메소드 이름(init, close 등)을 자유롭게 정의할 수 있습니다.
  • 스프링 빈이 스프링 코드에 의존하지 않습니다. 이전 방법 같은 경우 인터페이스에 의존하게 되었습니다.
  • 코드가 아닌 설정 정보를 사용하기 때문에 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있습니다.

 

3. @PostConstruct, @PreDestroy

2번째 방법은 @Bean 애노테이션의 설정 정보에 초기화, 소멸 메소드를 따로 설정하였다면 @PostConstruct, @PreDestroy 애노테이션을 이용한 방법은 비슷하게 초기화, 소멸 메소드를 지정하고 싶은 메소드에 @PostConstruct, @PreDestroy 애노테이션을 지정하면 되는 방법입니다.

 

public class NetworkClient3 {

    private String url;

    public NetworkClient3() {
        System.out.println("생성자 호출, url = " + url);
    }

    public void setUrl(String url) {
        this.url = url;
    }

    // 서비스 시작시 호출
    public void connect() {
        System.out.println("connect: " + url);
    }

    public void call(String message) {
        System.out.println("call : " + url + ", message = " + message);
    }

    // 서비스 종료시 호출
    public void disConnect() {
        System.out.println("close : " + url);
    }

    // 의존관계 주입이 끝나면 호출됨
    @PostConstruct
    public void init() throws Exception {
        connect();
        call("초기화 연결 메시지");
    }

    // 소멸전에 호출
    @PreDestroy
    public void close() throws Exception {
        disConnect();
    }
}
public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest3() {
        //given
        ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(
            LifeCycleConfig.class);
        //when
        NetworkClient3 networkClient = ctx.getBean(NetworkClient3.class);
        ctx.close();
        //then
    }

    @Configuration
    static class LifeCycleConfig {

        @Bean
        public NetworkClient3 networkClient3() {
            NetworkClient3 networkClient3 = new NetworkClient3();
            networkClient3.setUrl("http://hello-spring.dev");
            return networkClient3;
        }
    }
}

빈 설정은 자동이든 수동이든 상관없지만 테스트 코드에서 수행하기 위해서 수동 빈으로 등록하였습니다. 결과는 이전 방법들과 동일합니다.

생성자 호출, url = null
connect: http://hello-spring.dev
call : http://hello-spring.dev, message = 초기화 연결 메시지
close : http://hello-spring.dev

 

@PostConstruct, @PreDestroy 특징

  • 최신 스프링에서 가장 권장하는 방법입니다.
  • 애노테이션만 붙이면 되기 때문에 매우 편리합니다. @PostConstruct, @PreDestory의 패키지는 자바 표준 패키지이기 때문에 스프링에 종속적인 기술이 아닌 JSR-250라는 자바 표준 기술입니다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작합니다.
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
  • 단점은 외부 라이브러리에는 적용할 수 없습니다. 그러나 2번째 방법인 @Bean 애노테이션의 설정 정보에 초기화, 소멸 메소드를 지정할 수 있기 때문에 같이 사용하면 됩니다.

 

정리

  • 스프링 빈의 초기화 콜백과 소멸전 콜백을 호출하기 위해서 @PostConstruct, @PreDestory를 사용합니다.
  • 그러나 코드를 고칠 수 없는 외부 라이브러리를 초기화해야 한다면 @Bean의 옵션인 initMethod, destroyMethod를 사용합니다.

 

References

스프링 핵심 원리 - 기본편
[Spring] 빈 생명주기(Bean LifeCycle) 콜백 알아보기