본문 바로가기

spring

스프링 빈의 생명주기와 초기화 분리

스프링 빈도 객체이기 때문에 초기화가 필요할 것이다.

그리고 우리는 이 초기화를 보통 생성자에서 처리하게 된다.

필드같은 간단한 초기화는 괜찮지만, 만약 데이터베이스의 커넥션풀을 미리 셋팅하는 것과 같은 무거운 작업들은 ?

소켓 통신을 위해 소켓을 미리 열어두는 초기화 작업들은 ?

 

객체의 생성과 초기화는 분리해야한다.
객체의 생성과 객체의 초기화는 다른 책임이다. SRP 의 원칙에 따라 이 두 로직은 분리되는 것이 좋다. 물론 간단한 필드 한두개를 초기화하는 데에 생성자를 쓰지 않고 따로 하는 것은 비효율적일 수 있지만, 위의 예인 커넥션풀처럼 무거운 초기화 작업들은 분리하는 것이 좋다.

오늘은 이  초기화라는 작업을 스프링에서 어떻게 분리하여 처리하게 도와주는지 알아본다.

 

 


스프링 컨테이너를 사용해 빈을 등록해 사용할 때에는 다음과 같은 과정이 일어난다.

 

  1. 스프링 컨테이너가 생성된다.
  2. 스프링 빈이 등록된다.
    1. @Configuration 의 @Bean 에너테이션을 이용한 수동 등록
    2. @ComponentScan 을 이용한 자동 등록
  3. 스프링 빈이 모두 등록되면 빈들의 의존관계를 주입한다.
  4. 의존관계 주입이 끝나면 스프링은 콜백을 주고, 그때 빈들의 초기화를 한다.
  5. 셋팅된 빈을 가지고 에플리케이션을 돌린다.
  6. 스프링 컨테이너는 스프링 컨테이너가 종료되기 전 소멸 콜백을 준다.
  7. 스프링 컨테이너가 정지된다.

물론 생성자 주입이나 수동 주입같은 경우에는 빈의 등록과 의존 관계 주입이 같이 일어날 수 있다.

 

의존 관계의 빈을 초기화 하는 것은 빈 등록과 의존 관계 주입이 다 끝난 후 일어나야 하는 일이다.

의존 관계가 다 끝나지 않은 채로 그 의존 빈의 초기화작업을 진행하려 하면 당연히 null 을 초기화하는 행위와 같다.

따라서 스프링은 이 빈 등록과 의존성 주입이 모두 끝나고 초기화를 하는 콜백 기능을 제공한다.

 

 

콜백

  • 초기화 콜백 : 빈이 생성되고, 의존 관계 주입이 끝났나 초기화를 진행해도 될 때 호출되는 콜백
  • 소멸전 콜백 : 스프링 컨테이너가 종료되어 소멸되거나, 생명주기가 끝나는 빈들이 소멸되기 직전에 호출되는 콜백

 

스프링에서 제공하는 생명주기 콜백 방법은 3가지가 있다.

 

 

InitializingBean, DisposableBean

이 인터페이스들을 까보자.

 

public interface InitializingBean {

	void afterPropertiesSet() throws Exception;
}
public interface DisposableBean {

	void destroy() throws Exception;
}

 

 

유겐 휠러라는 사람이 2003 년에 만든 메서드라 적혀있다. (굉장히 오래되었다. 사실 3가지 방법중 아래로 내려갈수록 요즘 더 많이 쓰는 좋은(?) 방법이다.)

 

초기화 콜백 메서드와 소멸전 콜백 메서드가 있으며, 사용하고 싶은 클래스에 이 인터페이스를 구현하면 된다.

 

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("url = " + url + " message = " + message);
    }
    
    public void disconnect() {
        System.out.println("close = " + url);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
    }
    
    @Override
    public void destroy() throws Exception {
        disconnect();
    }
}

 

예시 코드이다.

객체를 생성하고, 커넥션을 초기화하여 만드는 connect() 를 초기화 콜백 메서드(afterPropertiesSet)에 등록시켰다.

객체가 소멸하기전, 소멸전 콜백 메서드(destroy)에 disconnect() 를 등록시켜 연결을 끊게 한다.

 

    @DisplayName("빈 생명주기 확인")
    @Test
    void lifeCycle() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleBean.class);
        NetworkClient ne = ac.getBean(NetworkClient.class);
        ne.call("호출");
        ac.close();
    }

    @Configuration
    static class LifeCycleBean {

        @Bean
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://springSite.com");
            return networkClient;
        }
    }
// 위 테스트의 결과
생성자 호출, url = null
connect = http://springSite.com
url = http://springSite.com message = 호출
close = http://springSite.com

 

코드를 보면 알겠지만, 빈을 등록하는 과정에서 따로 connect() 나 disconnect() 를 호출하지 않았다.

 

그런데 afterPropertiesSet 에 등록된 메서드가 빈 생성 후 자동으로 실행된다. (url 값이 null 이었다가 들어가 있는 것을 보면 빈 등록 후임을 알 수 있다.)

 

또, close() 로 스프링 컨테이너가 소멸되어 빈도 소멸시킬때 destroy() 에 등록된 disconnect() 가 호출된 것을 볼 수 있다.

 

오래된 만큼 단점이 몇가지 존재한다.

  • 스프링 전용 인터페이스다. 코드가 자바가 아닌 스프링에 의존하게 된다.
  • 초기화, 소멸 메서드의 이름을 변경할 수 없다.
  • 외부 라이브러리를 사용할 때, 그 라이브러리의 초기화화 소멸 콜백 메서드를 제공할 방법이 없다. (외부 라이브러리에 이 인터페이스를 구현할 수 없다.)

그래서 요즘에는 거의 쓰지 않는다.

 

 

@Bean 등록시 옵션주는 방법

@Bean 에너테이션에는 initMethod, destroyMethod 라는 옵션을 줄 수 있다.

값으로 각각 초기화 메서드 이름, 소멸시 메서드 이름을 지정해주면 된다.

 

    @DisplayName("빈 생명주기 확인")
    @Test
    void lifeCycle() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleBean.class);
        NetworkClient ne = ac.getBean(NetworkClient.class);
        ne.call("호출");
        ac.close();
    }

    @Configuration
    static class LifeCycleBean {

        @Bean(initMethod = "connect", destroyMethod = "disconnect")
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://springSite.com");
            return networkClient;
        }
    }

 

 

이 방법의 장점은 아래와 같다.

  • 메서드의 이름을 자유롭게 지정할 수 있다. (보통 init 처럼 관례적으로 쓰이는 단어들이 있긴하다.)
  • 스프링 빈이 스프링 코드를 의존하지 않는다.
  • 비즈니스 코드를 만지지 않고 설정정보를 사용하기 때문에 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

 

 

종료 메서드 추론

여기서 특이한 점이 있는데, destroyMethod 옵션의 기본값을 보자.

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {

    ...
	String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;

}
public static final String INFER_METHOD = "(inferred)";

 

이런 값이 초기화되어 있다.

 

라이브러리들은 종료하는 메서드를 보통 close(), shutdown() 등의 이름을 관례적으로 사용하는데,

이 inferred 는 추론이란 뜻으로 말 그대로 이 close, shutdown 같은 이름을 추론하는 것이다.

 

종료할 메서드의 이름을 close, shutdown 으로 만들고, 굳이 destroyMethod 옵션을 주지 않아도 알아서 추론하여 실행해준다.

추론 기능을 사용하기 싫다면 초기화 값을 명시적으로 destroyMethod="" 이렇게 하면 된다.

 

public class NetworkClient {

    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("url = " + url + " message = " + message);
    }

    public void close() { // 이름을 close 로 바꿈
        System.out.println("close = " + url);
    }
}

 

    @DisplayName("빈 생명주기 확인")
    @Test
    void lifeCycle() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleBean.class);
        NetworkClient ne = ac.getBean(NetworkClient.class);
        ne.call("호출");
        ac.close();
    }

    @Configuration
    static class LifeCycleBean {

        @Bean(initMethod = "connect") // destroyMethod 는 생략하여 기본값 사용.
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://springSite.com");
            return networkClient;
        }
    }

 

// 위 테스트의 결과
생성자 호출, url = null
connect = http://springSite.com
url = http://springSite.com message = 호출
close = http://springSite.com

 

 

 

 

@PostConstruct, @PreDestory

세번째 방법이다. 가장 최근에 나왔으며 가장 많이 쓰인다.

 

초기화, 종료 메서드 위에 붙여주기만 하면 된다.

 

public class NetworkClient {

    private String url;

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

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

    @PostConstruct
    public void connect() {
        System.out.println("connect = " + url);
    }

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

    @PreDestroy
    public void close() {
        System.out.println("close = " + url);
    }
}

 

 

이 방법은 스프링에서도 권장하므로 이 방법을 쓰자.

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

위에서 보듯 javax 패키지에 있는 자바 표준 기술이다.

따라서 스프링에 종속적이지 않고, 스프링이 아닌 다른 컨테이너에서도 동작한다.

 

하지만 유일한 단점으로, 코드에 작성하는 것이다 보니 외부 라이브러리에는 적용하지 못한다는 점이다.

 

 

 

 

결론

  • 평소에는 @PostConstruct, @PreDestroy 를 쓰자.
  • 외부 라이브러리처럼 코드를 고칠 수 없는 상황에는 @Bean 에 initMethod, destroyMethod 옵션을 쓰자.

'spring' 카테고리의 다른 글

spring interceptor  (0) 2021.10.30
빈 스코프  (0) 2021.03.21
자동 주입시 빈이 2개 이상일 때 문제 해결  (0) 2021.03.19
생성자 자동 주입의 장점, @RequiredArgsConstructor  (0) 2021.03.19
@Autowired  (0) 2021.03.18