섹션 8. 빈 생명주기 콜백

Zion Yu·2021년 3월 31일
0
post-thumbnail

본 시리즈는 우아한형제들 개발 팀장이신 김영한님의 스프링 핵심 원리 - 기본편 강의를 들으며 개인적으로 정리한 내용을 담고 있습니다. 제가 들은 강의는 인프런에 등록되어 있습니다. 모든 다이어그램을 포함한 사진의 출처는 위 강의의 강의록임을 밝힙니다. 개인적으로 정리한 내용이기 때문에 글 내용에 오류가 있을 수 있으며 이에 대한 피드백은 댓글로 부탁드립니다.

이번 섹션에서 다룰 내용

  • 빈이 생성되거나 죽기 일보 직전에 빈 내부의 메소드를 호출해주는 기능에 대해 배워보자
  • 세 가지 방식이 있고, 각 방식 별로 특징이 있다.



빈 생명주기 콜백 시작

  • 데이터베이스 커넥션 풀: 서버가 뜨기 전에 미리 DB에 연결을 해놓는 것
  • 네트워크 소켓: 서버가 뜨기 전에 미리 다른 쪽이랑(?) 소켓을 열어두는 것

위와 같은 작업을 할 때 초기화, 종료 작업이 필요하다. 스프링에선 이 작업을 어떻게 하는지 알아보자.

서버가 뜨기 전에 외부 네트워크에 미리 연결하는 객체를 생성한다고 상정하자.

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 msg) {
        System.out.println("call: " + url + " msg = " +  msg);
    }
    //서비스 종료 시 호출
    public void disconnect() {
        System.out.println("close " + url);
    }
}

NetworkClient를 테스트하는 클래스를 작성하자.

public class BeanLifeCycleTest {
    @Test
    public void lifeCycleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close();
    }

    @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 msg = 초기화 연결 메시지
  • setUrl을 호출하기 전에 생성자를 호출했고, 생성자 안에서 connect()가 호출될테니 url을 세팅하기 전이라 null이 나오는 것이다.
  • 스프링 빈의 라이프사이클은 다음과 같다.
    • 객체 생성 → 의존관계 주입
    • 물론 생성자 주입은 예외다. (생성과 동시에 주입)
  • 스프링 빈은 객체를 생성하고, 의존관계 주입이 모두 완료되어야 사용할 수 있는 상태가 된다. (필드가 셋팅되니까)
  • 그래서 초기화 작업 (네트워크 연결, DB 연결 등)은 의존관계 주입이 모두 완료되고 나서 해야한다.
  • 그런데 이 시점을 어떻게 알 수 있는가?

스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메소드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 그리고 스프링은 스프링 컨테이너가 종료되기 직전 소멸 콜백을 준다. 이를 이용하면 종료 작업을 할 수 있다.

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

스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 스프링 종료

  • 초기화 콜백: 스프링 빈 생성, 의존관계 주입이 완료되면 호출된다.
  • 소멸 전 콜백: 빈이 소멸되기 직전에 호출된다.

참고: 객체의 생성과 초기화를 구분하자

생성자는 필수 정보를 parameter로 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 초기화는 이렇게 생성된 값을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다.

따라서 생성자 안에서 무거운 초기화 작업을 하는 것보다는 객체를 생성하는 부분과 초기화하는 부분을 명확히 나누는 것이 유지보수 관점에서 좋다. 물론 초기화 작업이 내부 값들만 약간 변경하는 단순한 경우에는 한 번에 처리하는 것도 괜찮다.

스프링은 크게 세 가지 방법으로 빈 생명주기 콜백을 지원한다.

  • 인터페이스(InitializingBean, DisposableBean)
  • 설정 정보에 초기화 메소드, 종료 메소드 지정
  • @PostConstruct, @PreDestroy 애노테이션



인터페이스InitializingBean, DisposableBean

public class NetworkClient implements InitializingBean, DisposableBean {
    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
    }
    
    ...
        
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("NetworkClient.afterPropertiesSet");
        connect();
        call("초기화 연결 메시지");
    }
    @Override
    public void destroy() throws Exception {
        System.out.println("NetworkClient.destroy");
        disconnect();
    }
}
  • InitializingBean을 구현하면 afterPropertiesSet()을 통해 초기화할 내용을 지정할 수 있다.
  • DisposableBean을 구현하면 destroy()를 통해 소멸되기 직전 수행할 내용을 지정할 수 있다.
생성자 호출, url = null
NetworkClient.afterPropertiesSet
connect: http://hello-spring.dev
call: http://hello-spring.dev msg = 초기화 연결 메시지
01:22:25.436 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing
NetworkClient.destroy
close http://hello-spring.dev
  • 결과를 보면 초기화, 소멸 시에 메소드가 적절히 호출되었음을 알 수 있다.

초기화, 소멸 인터페이스의 단점은

  • 스프링 전용 인터페이스에 의존해야 한다.
  • 초기화, 소멸 메솓의 이름을 변경할 수 없다.
  • 내가 코드를 적용할 수 없는 외부 라이브러리에는 적용할 수 없다.

이는 스프링 초창기에 나온 방법이고, 지금은 잘 사용하지 않는다.

빈 등록 초기화, 소멸 메소드

설정 정보에 @Bean(initMethod="init", destroyMethod="close")처럼 초기화, 소멸 메소드를 지정할 수 있다.

public class NetworkClient {
	...
        
    public void init() {
        System.out.println("NetworkClient.afterPropertiesSet");
        connect();
        call("초기화 연결 메시지");
    }

    public void close() {
        System.out.println("NetworkClient.destroy");
        disconnect();
    }
}

설정 정보를 다음과 같이 수정한다.

public class BeanLifeCycleTest {
    ...

    @Configuration
    static class LifeCycleConfig {
        @Bean(initMethod = "init", destroyMethod = "close")	// 요기에 기입한다.
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
}
  • 테스트해보면 의도한 대로 잘 동작한다.
  • 메소드 이름을 자유롭게 줄 수 있고, 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 코드가 아니라 설정 정보를 사용하기 때문에 외부 라이브러리에도 초기화, 종료 메소드를 지정할 수 있다. - 제일 큰 장점!

종료 메소드 추론

  • @BeandestroyMethod 속성에는 특별한(?) 기능이 있다.
  • 라이브러리는 대부분 (통상적으로) close, shutdown이라는 이름의 종료 메소드를 사용한다.
  • destroyMethod는 기본값으로 "(inferred)"를 갖는데, 기능은 close, shutdown이라는 이름을 가진 메소드를 자동으로 호출해주는 것이다.
  • 따라서 직접 스프링 빈으로 등록하면, (@Bean으로 등록했을 경우만, 자동 등록 제외) 종료 메소드는 따로 적어주지 않아도 잘 동작한다.
  • 추론 기능을 사용하기 싫으면 destroyMethod=""처럼 빈 공백을 지정하면 된다.

애노테이션 @PostConstruct, @PreDestroy

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

public class NetworkClient {
	...
   
    @PostConstruct
    public void init() {
        System.out.println("NetworkClient.afterPropertiesSet");
        connect();
        call("초기화 연결 메시지");
    }

    @PreDestroy
    public void close() {
        System.out.println("NetworkClient.destroy");
        disconnect();
    }
}
  • 단순히 초기화 메소드와 소멸 메소드에 @PostConstruct, @PreDestroy만 붙이면 끝이다!
    • 물론 이전에 설정했던 @Bean의 parameter들은 지워주자
  • 최신 스프링에서 가장 권장하는 방법이다.
  • 애노테이션 하나만 붙이면 되므로 매우 편리하다.
  • 패키지를 보면 javax.annotation.PostContruct이므로, 자바 표준임을 알 수 있고, 스프링이 아닌 다른 컨테이너에서도 잘 동작한다.
  • 컴포넌트 스캔과 잘 어울린다. (수동으로 등록할 필요가 없다!)
  • 유일한 단점은 외부 라이브러리에 적용하지 못한다는 점이다. 외부 라이브러리를 초기화, 종료할 때 별도 작업을 해야하면 @Bean의 parameter를 사용하자.



정리

  • @PostContructor, @PreDestroy 애노테이션을 사용하자.
  • 코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료할 때 별도 작업을 해야하면 @BeaninitMethod, destroyMethod를 이용하자.

0개의 댓글