본문 바로가기
Back-end/java spring

[Spring] 빈 생명주기 콜백

by kkkdh 2023. 1. 6.
728x90

빈 생명주기 콜백 시작

데이터베이스 connection pool이나, 네트워크 socket처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화 및 종료 작업이 필요하다.

 

이번에는 스프링을 통해 이러한 초기화 + 종료 작업이 어떻게 진행되는지 예제와 함께 정리해 보자.

 

예시 설명)

  • 외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정하자.
  • 실제로 네트워크에 연결되는 것은 아니고 단순히 문자열을 출력하는 코드이다.
  • NetworkClient 객체는 애플리케이션의 시작 시점에 connect() method를 호출해서 연결을 맺고
  • 애플리케이션이 종료되면, disconnect() method를 호출해서 연결을 끊는다.

이러한 동작을 수행할 예시 코드를 테스트 하위에 다음과 같이 생성한다.

package hello.core.lifecycle;

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("disconnect: " + url);
    }
}

테스트용 수동 빈 등록 클래스를 만들고, 이것을 이용해 테스트를 진행했다.

이 객체를 테스트 코드를 통해 실행해 보면, 다음과 같은 결과가 출력된다.

예시 코드가 생성자를 호출하며 connect, call method를 호출

생성자를 이용해 객체가 생성된 이후에 setter method를 통해 url 필드가 설정되기 때문에, 위처럼 url이 null인 것이 정상이다.

 

너무 당연하게도 외부에서 수정자 주입을 통해 setUrl() method가 호출된 이후가 되어야 url이 존재할 것이다.

 

앞서 계속 언급한 바와 같이 스프링 빈의 라이프사이클

  1. 객체 생성
  2. 의존관계 주입

두 단계를 거쳐서 일어난다. 그렇기 때문에 객체가 생성되고, 이에 따른 의존관계가 주입된 이후에야 필요한 데이터를 사용할 수 있는 준비가 완료된다.

 

그런데, 개발자의 입장에서 의존관계가 주입 완료된 시점을 어떻게 알 수 있을까??

 

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

(여기서 초기화는 객체의 생성을 말하는 것이 아니라 객체에 필요한 값이 전부 갖춰져 처음 제대로 일을 시작할 수 있는 상태가 되는 것을 의미)

 

또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다.

 

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

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

 

스프링 빈이 생성된 이후에 의존관계 주입이 이루어지고, 의존관계 주입까지 갖춰진 이후에 초기화 작업을 수행해 스프링 빈이 제 역할을 할 수 있도록 만들어진다.

  • 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후에 호출된다.
  • 소멸자 콜백: 빈이 소멸되기 직전에 호출된다.
참고: 객체의 생성과 초기화를 분리하자.
그렇다면, 웬만한 모든 것들을 생성자 호출 시에 함께 처리하는 것이 복잡하지 않고 좋지 않을까??라는 의문이 생기는데, 이것은 약간 객체 지향 설계 5가지 원칙 중 하나인 SRP(Single Responsibility Principle)과 연관된다.

생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해 객체를 생성하는 작업만을 책임진다. 반면, 초기화는 이렇게 생성된 값을 이용해 외부 커넥션을 연결하는 등의 무거운 동작을 수행할 책임을 진다.

따라서 생성자 안에 무거운 초기화 작업을 함께 하는 것보다는 초기화 부분을 따로 분리하는 것이 유지보수 관점에서 더 좋다고 한다.

물론 초기화 작업이 내부 값들을 약간만 변경하는 것과 같이 아주 단순한 경우에는 생성자에서 한 번에 처리하는 것이 나을 수도 있다고
참고: 싱글톤 빈들은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료되기 때문에, 스프링 컨테이너가 종료되기 직전 소멸 전 콜백이 일어난다고 한다.

뒤에서 설명이 나오지만, 스프링 컨테이너 시작부터 종료까지 생존하는 빈도 있지만, 이보다 생명주기가 짧은 빈들도 있는데, 이 빈들은 컨테이너와 무관하게 해당 빈이 종료되기 직전 소멸 전 콜백이 실행된다. 자세한 내용은 빈 스코프 파트에서 공부해 보자.

 

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

  1. 인터페이스(InitializingBean, DisposableBean)
  2. 설정 정보에 초기화 메서드, 종료 메서드를 지정
  3. @PostConstruct, @PreDestroy 어노테이션 지원

인터페이스 InitializingBean, DisposableBean 사용

두 개의 인터페이스를 스프링 빈이 상속받게 하여, 초기화 콜백과 소멸자 콜백을 인터페이스에서 상속받아 오버라이딩하여 커스터마이징 하는 방식으로 생명주기 콜백을 사용하는 방법이다.

 

InitializingBean은 afterPropertiesSet() method로 초기화를 지원하고, DisposableBean은 destory() method로 소멸을 지원한다.

 

두 메서드를 활용해 코드를 다음과 같이 고치면, 의존관계 주입 이후에 초기화 과정이 발생해 동작이 의도대로 되는 것을 확인할 수 있다.

두 메서드를 오버라이딩한 코드를 추가해준다.
의존관계 주입 이후에 동작이 수행된다.

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

  • 이 인터페이스는 스프링 전용 인터페이스이다. 따라서 해당 코드가 스프링 전용 인터페이스에 의존하게 된다.
  • 초기화, 소멸 메서드의 이름을 변경 불가
  • 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수가 없다.

이 방식은 스프링 초창기에 나온 방식으로 최근에는 거의 사용하지 않는다고 한다.


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

단순하게 설정 정보에 @Bean(initMethod = "init", destroyMethod = "close")처럼 초기화, 소멸 메서드를 지정하면 되는 방법이다.

 

초기화, 소멸시에 호출될 메서드를 직접 정의하고
빈으로 등록할 때, 이렇게 해주면 끝이다.

설정 정보 사용의 특징

  • 메서드의 이름을 자유롭게 줄 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않게 된다.
  • 코드가 아니라 설정 정보를 사용하기 때문에, 코드를 고칠 수 없는 외부 라이브러리에도 적용이 가능하다.

종료 메서드의 추론

  • @Bean의 destroyMethod 속성에는 아주 특별한 기능이 있다.
  • 대부분의 라이브러리는 종료 메서드의 이름으로 close, shutdown을 주로 사용
  • @Bean의 destroyMethod의 기본값은 (inferred) (추론)으로 등록되어 있다.
  • 이 추론 기능은 close, shutdown라는 이름의 메서드를 자동으로 호출해 준다. 이름 그대로 종료 메서드를 추론해서 호출하는 방식을 의미
  • 따라서 직접 스프링 빈으로 등록하면, 종료 메서드는 따로 적어주지 않아도 잘 동작한다.
  • 추론 기능 사용이 싫으면 destroyMethod = ""라고 지정해 주면 된다.

사실 마지막꺼를 사용하면 된다. 위에 두 개는 빌드업 용인듯


어노테이션 @PostConstruct, @PreDestroy 사용하는 방법

그냥 이 방법을 사용하면 된다.

 

이름 또한 생성자 이후에, 소멸자 전에로 아주 직관적으로 잘 만들어져 있어 알아보기가 쉽다.

JSR-250라는 자바 표준 기술이다.
위와 같이 어노테이션을 코드에 붙여주면 알아서 동작한다.

두 개의 어노테이션만 초기화 과정과 소멸 과정에 수행하고 싶은 각각의 메서드에 붙여주면, 알아서 동작하는 방식이다.

 

@PostConstruct, @PreDestroy 어노테이션의 특징

  • 최신 스프링에서 가장 권장하는 방법
  • 어노테이션만 잘 붙어있으면 돼서 아주 편리하다.
  • 자바 표준 기술이기 때문에, 스프링 컨테이너가 아니어도 잘 동작한다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 점이다. 외부 라이브러리를 초기화, 종료하는 과정을 정의하고 싶은 경우 @Bean을 섞어서 사용하자.

 

최종 정리

  • @PostConstruct, @PreDestroy 어노테이션을 활용하자.
  • 코드를 고칠 수 없는 외부 라이브러리를 사용하는 경우 @BeaninitMethod, destroyMethod를 사용하자.
728x90

댓글