빈 생명주기 콜백

gorapaduckoo·2023년 7월 17일
0

스프링 기본편

목록 보기
8/10
post-thumbnail

인프런 김영한님의 스프링 핵심 원리 - 기본편 강의 내용을 바탕으로 작성한 글입니다.


1. 초기화 작업의 필요성

데이터베이스와의 커넥션은 연결을 설정하는 데 오래 걸리기 때문에, 애플리케이션 서버와 DB 사이의 커넥션을 미리 생성해둔다. 그리고 DB 연결 요청이 오면 연결해두었던 커넥션을 빌려주는 방식으로 동작한다.

이 커넥션은 보통 애플리케이션이 시작되는 시점에 생성되고, 애플리케이션이 종료될 때 함께 종료된다. 때문에 초기화와 종료 작업이 필요하다.

💡초기화 작업을 생성자에서 진행하면 안되는 이유?
생성자는 이름 그대로 객체 생성을 위한 메서드이다. 생성자는 초기화 작업보단 객체 생성에 집중하는 것이 SRP에 더 알맞고, 유지보수적인 측면에서 좋다. 초기화 작업에는 외부 커넥션 연결 등 무겁고 예외를 발생시킬 수 있는 작업이 포함되기 때문이다. 이러한 이유로 생성자에서 초기화 작업을 모두 수행해줄 수 없기 때문에, 별도의 초기화 메서드가 필요하다.

위와 같은 이유로, 커넥션 url 등은 초기화 메서드를 통해 별도로 세팅해주어야 한다.
이제 코드를 통해 웹 서버가 떠있는 상태에서, url로 요청이 들어온 상황을 살펴보자.

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() {
        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;
        }
    }
}

💡 ConfigurableAppicationContext?
close()는 잘 쓰지 않는 메서드라서, ApplicationContext 인터페이스에서는 제공해주지 않는다. ConfigurableApplicationContextAnnotationConfigApplicationContext 인터페이스로 교체하자.

위 코드를 실행하면 아래와 같은 결과가 나온다.

당연한 결과다. NetworkClient의 생성자가 실행되는 시점에서는 url 정보가 없고, 인스턴스 생성 후에야 외부에서 수정자 주입을 통해 url을 넣어주고 있기 때문이다.


2. 스프링 빈의 라이프사이클

이처럼 스프링 빈은 객체를 다 생성하고, 의존관계 주입을 끝낸 뒤에야 필요한 데이터를 사용할 수 있다. 그래서 초기화 작업은 의존관계 주입 이후에 진행된다.

개발자는 의존관계 주입이 언제 완료되는지 알 수 없다. 그래서 개발자가 의존관계 주입 끝나면 이 메서드 실행해줘! 라는 의미로 메서드를 만들어두면, 의존관계 주입이 완료된 시점에 스프링이 해당 메서드를 호출하여 초기화 작업을 진행한다. 이러한 메서드를 콜백 함수라고 하는데, 초기화 작업 때 실행되는 메서드를 초기화 콜백, 빈 소멸 때 실행되는 메서드를 소멸전 콜백이라고 한다.

정리하면, 스프링 빈은 아래와 같은 라이프 사이클을 가진다. (단, 생성자 주입은 객체를 생성할 때 파라미터로 스프링 빈을 받기 때문에, 객체 생성과 의존관계 주입이 동시에 일어난다.)
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 스프링 종료

스프링에서는 크게 3가지 방식으로 콜백을 지원한다.

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

3. InitinalizingBean, DisposableBean 인터페이스

public class ExampleBean implements InitializingBean, DisposableBean {

	@Override
    public void afterPropertiesSet() throws Exception {
        // 의존관계 주입이 끝나면 호출되는 메서드
    }

    @Override
    public void destroy() throws Exception {
        // 빈이 종료되기 직전에 호출되는 메서드
    }

첫 번째 방법은 인터페이스를 상속받아 콜백 메서드를 구현하는 것이다.
InitializingBeanafterPropertiesSet() 메서드로 초기화를 지원하고, DisposableBeandestroy() 메서드로 소멸을 지원한다.

  • 단점
    • 이 인터페이스들은 스프링 전용 인터페이스이기 때문에, 스프링 프레임워크에 종속된다.
    • 개발자가 초기화 메서드, 소멸 메서드의 이름을 변경할 수 없다.
    • 외부 라이브러리에 적용할 수 없다. 외부 라이브러리는 개발자가 코드를 수정하지 못하므로 인터페이스를 상속받도록 할 수 없기 때문이다.

4. 빈 등록 시 초기화, 소멸 메서드 지정

설정 파일에서 빈을 등록할 때, @Bean(initMethod = "init", destroyMethod = "close") 처럼 속성을 주어 초기화 메서드와 소멸 메서드를 지정할 수 있다.

예시 코드로 살펴보자.


public class ExampleBean {

    String url;

    public ExampleBean() {
        System.out.println("create ExampleBean, url = " + url);
    }

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

    public void init() throws Exception {
        System.out.println("ExampleBean.init, url = " + url);
    }

    public void close() throws Exception {
        System.out.println("ExampleBean.close, url = " + url);
    }
}
    
public class ExampleTest {

    @Test
    public void exampleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(ExampleConfig.class);
        ExampleBean exampleBean = ac.getBean(ExampleBean.class);
        ac.close();
    }

    @Configuration
    static class ExampleConfig {
    	// 초기화 메서드로 init(), 소멸 메서드로 close() 지정
        @Bean(initMethod = "init", destroyMethod = "close")
        public ExampleBean exampleBean() {
            ExampleBean exampleBean = new ExampleBean();
            exampleBean.setUrl("exmapleUrl");
            return exampleBean;
        }
    }
}

위와 같은 코드를 실행하면, 아래와 같이 정상적으로 실행되는 것을 확인할 수 있다.

  • 장점
    • 메서드 이름을 자유롭게 설정할 수 있다.
    • 스프링 빈이 스프링 코드에 의존하지 않는다. (설정 정보에는 의존함)
    • 설정 정보를 통해 콜백 메서드를 지정하기 때문에, 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

이 방법은 종료 메서드 추론 기능도 제공한다. 라이브러리에서는 대부분 close, shutdown 같은 이름의 종료 메서드를 사용하는데, destroyMethod 속성은 close, shutdown이라는 이름을 가진 메서드를 자동으로 종료 메서드로 등록해준다.


5. @PostConstruct, @PreDestroy 어노테이션

세 방법 중에 가장 권장되는 방법으로, 빈 초기화 시 호출하고 싶은 메서드에 @PostConstruct 어노테이션을, 빈 소멸 직전에 호출하고 싶은 메서드에 @PreDestroy 어노테이션을 추가하는 방법이다.
마찬가지로 예시 코드를 통해 확인해보자.

package hello.core.lifecycle;

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

public class ExampleBean {

    String url;

    public ExampleBean() {
        System.out.println("create ExampleBean, url = " + url);
    }

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

	// 초기화 메서드
    @PostConstruct
    public void init() throws Exception {
        System.out.println("ExampleBean.init, url = " + url);
    }

	// 소멸 메서드
    @PreDestroy
    public void close() throws Exception {
        System.out.println("ExampleBean.close, url = " + url);
    }
}
public class ExampleTest {

    @Test
    public void exampleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(ExampleConfig.class);
        ExampleBean exampleBean = ac.getBean(ExampleBean.class);
        ac.close();
    }

    @Configuration
    static class ExampleConfig {
        @Bean
        public ExampleBean exampleBean() {
            ExampleBean exampleBean = new ExampleBean();
            exampleBean.setUrl("exmapleUrl");
            return exampleBean;
        }
    }
}

아래와 같이 정상적으로 실행되는 것을 확인할 수 있다.

  • 장점
    • 스프링에서 가장 권장하는 방법
    • 매우 편리하다.
    • javax 패키지에 속한 어노테이션이기 때문에, 스프링에 종속적이지 않다.
      (=스프링이 아닌 다른 컨테이너에서도 동작한다.)
  • 단점
    • 외부 라이브러리에는 적용할 수 없다.

결론적으로 외부 라이브러리에 초기화 및 종료 메서드를 적용해야 한다면 @BeaninitMethod,destoryMethod 속성을 사용하고, 그 외에는 @PostConstruct, @PreDestroy 어노테이션을 사용하는 것이 좋다.

0개의 댓글