[스프링 핵심 원리] - 빈 생명주기 콜백

peeerr·2023년 2월 2일
0

Spring

목록 보기
7/10
post-thumbnail

이 글은 김영한님의 스프링 핵심 원리 - 기본편 강의를 수강하고 정리한 내용입니다.
강의 보러가기


📌 1. 빈 생명주기 콜백 시작


  • 서버가 뜰 때 서버와 데이터베이스의 연결을 미리 맺어놓아야 고객 요청이 올 때 바로바로 가져다 사용할 수 있다.

  • 서버가 뜰 때 소켓을 미리 열어놓아야 고객 요청이 올 때 이미 열려있는 소켓으로 빠르게 응답이 가능하다.

이처럼 애플리케이션 시작 지점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하려면 객체의 초기화와 종료 작업이 필요하다.

간단하게 외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정해보자.
(물론 실제로 네트워크에 연결하는 것은 아니고, 문자만 출력한다.)

예제 코드를 보자

package hello.core.lifecycle;

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

}
package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest() {
        AnnotationConfigApplicationContext 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;
        }
    }

}
  • close() 는 스프링 컨테이너를 종료시키는 메서드인데 ApplicationContext 로 선언하면 사용할 수 없다.
    -> 구현체인 AnnotationConfigApplicationContext 로 선언하면 사용할 수 있다.

출력결과

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

저 모든 출력은 생성자 호출시 일어나는데 그 시점에 urlnull이므로 당연한 결과이다.

그냥 간단하게 생성자 파라미터로 url 넘겨서 생성하면 되지 않나?

  • 대부분의 경우 객체를 생성하는 것과 초기화는 분리하는 것이 좋다.
    • 초기화는 무거운 작업이기 때문에 생성자는 객체를 생성하는데에만 집중하는 것이 좋다.
    • 분리해야 유지보수 관점에서 좋다.

1) 스프링 빈의 라이프 사이클

스프링 빈은 이전에도 설명했듯, 다음과 같은 간단한 라이프사이클을 가진다.
객체 생성 -> 의존관계 주입

  • 스프링 빈은 객체 생성, 의존관계 주입이 끝나야 필요한 데이터를 사용할 수 있다.
    -> 따라서 초기화 작업은 의존관계 주입이 모두 완료되고 호출되어야 한다.

그런데 개발자가 의존관계 주입이 모두 완료된 시점을 어떻게 알 수 있을까?

  • 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 려주는 기능을 제공한다.

  • 스프링은 스프링 컨테이너가 종료되기 직전에 또는 스프링 빈이 종료되기 직전에 소멸 콜백을 준다.

-> 따라서 안전하게 종료 작업을 진행할 수 있다.

스프링 빈의 이벤트 라이프사이클
스프링 컨테이너 생성 -> 스프링 빈 생성 (+생성자 주입) -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료

  • 초기화 콜백 -> 빈 생성, 의존관계 주입 완료 후 호출
  • 소멸전 콜백 -> 빈이 소멸되기 직전에 호출

📌 2. 빈 생명주기 콜백의 세 가지 방식


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

  • 인터페이스(InitializingBean, DisposableBean)

  • 설정 정보에 초기화 메서드, 종료 메서드 지정

  • @PostConstruct, @PreDestroy 어노테이션 지원

1) 인터페이스 방식

  • 인터페이스 InitializingBean, DisposableBean 사용

코드를 보자

package hello.core.lifecycle;

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

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

InitializingBean : afterPropertiesSet() 메서드로 초기화를 지원
DisposableBean : destroy() 메서드로 소멸을 지원

이제 위 테스트를 다시 돌려보자

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

이제 정상적으로 출력이 된다!

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

  • 이 인터페이스는 스프링 전용 인터페이스다.
    -> 내 코드가 전부 스프링 전용 인터페이스에 의존되게 설계해야 된다.

  • 초기화, 소멸 메서드의 이름을 변경할 수 없다.

  • 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

인터페이스 방식은 매우 오래된 방식이고, 더 나은 방법들이 있어 거의 사용하지 않는다.


2) 설정 정보에 메서드를 지정하는 방식

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

package hello.core.lifecycle;

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

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

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

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

    // 의존관계 주입이 끝나면 호출
    public void close() {
        disConnect();
    }
}

인터페이스를 제거하고 init , close 메서드를 만들었다.


테스트 코드에 있는 설정 정보에 초기화, 소멸 메서드 지정

@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는 디폴트 값이 (inferred)라는 문자이다. (inferred는 추론이라는 뜻)

    • 이 추론 기능이 자주 쓰는 메서드명인 close , shutdown을 찾아 호출을 해준다.
      -> 직접 스프링 빈으로 등록할 때, 종료 메서드는 따로 적어주지 않아도 잘 동작한다.
  • 추론 기능을 사용하지 않으려면 destroyMethod = ""처럼 공백으로 지정하면 된다.


3) 어노테이션 방식

  • @PostConstruct , @PreDestroy 어노테이션을 사용하는 방식
    -> 이름만 봐도 '생성된 후에, 소멸되기 전에' 동작하겠다는 느낌이 온다.

결론부터 말하면 그냥 이 방식을 사용하자.


최종 코드를 보자

package hello.core.lifecycle;

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

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

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

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

    // 의존관계 주입이 끝나면 호출
    @PostConstruct
    public void init() {
        System.out.println("NetworkClient.afterPropertiesSet");

        connect();
        call("초기화 연결 메시지");
    }

    // 의존관계 주입이 끝나면 호출
    @PreDestroy
    public void close() {
        System.out.println("NetworkClient.destroy");

        disConnect();
    }
}
package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest() {
        AnnotationConfigApplicationContext 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;
        }
    }

}
  • 그냥 init , close에 각각 @PostConstruct , @PreDestroy 어노테이션을 붙여주면 끝난다.

특징

  • 스프링에서 가장 권장하는 방식이다.

  • 위 코드만 봐도 알겠지만 너무 편하다.

  • 스프링 기술이 아니라 JSR-250이라는 자바 표준이다.
    -> 스프링이 아닌 다른 컨테이너에서도 동작한다.

  • 수동으로 Bean을 등록해야 하는 방식이 아니기 때문에 컴포넌트 스캔과 잘 어울린다.

  • 유일한 단점으로는 외부 라이브러리에 적용하지 못한다는 것이다.
    -> 이 경우에는 @Bean의 메서드 지정 방식을 사용하자.


📌 3. 어떤 방식을 사용해야 할까?


  • 그냥 @PostConstruct , @PreDestroy 어노테이션을 사용하자.

  • 코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 할 때만 @BeaninitMethod , destroyMethod를 사용하자.

profile
개발 공부

0개의 댓글