framework/spring

스프링 핵심 원리 이해 7 - bean 생명주기 callback

wooweee 2023. 10. 29. 20:34
728x90

1. 빈 생명주기 콜백 시작

  • Spring bean 생성, 종료 직전  @Bean 객체 내부 method를 호출해주는 기능
    1. 생성후 초기화시 호출
    2. 죽기 직전 안전 종료 메서드 호출
  • 필요한 이유
    • db connection pool, network socket처럼 app시작 시점에 필요한 연결을 미리 해둘 경우
    • app 종료 시점에 연결을 모두 종료하는 작업을 진행하기 위해서 객체 내부 종료 작업 필요

 

1.1. 예제

  • 문제 코드 - 미리 connect()를 하고 싶어서 생성자 내부에 connect() 넣어봤다 - 시도를 한 것. 잘못된 시도
package hello.core.lifecycle;

public class NetworkClient {
    private String url;
    
    // 생성자 내부에 connect() 메서드 호출 및 call() 메서드를 호출
    public NetworkClient(){
    	System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지")
    }
    
    public void setUrl(String url){ this.url = url; }
    
    public void connect(){ System.out.println("connnect: " + url); }
    
    public void call(String message){ System.out.println("call: " + url + ", message = " + message); }
    
    // 해당 bean 종료 직전 호출 메서드
    public void disconnect(){
    	System.out.println("close: " + url);
    }
}
  • test 코드
package hello.core.lifecycle;

@Test
public void lifeCycleTest(){
    // spring container에 bean 등록
    ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
    
    NetworkClient client = ac.getBean(NetworkClient.class); // spring 시작
    ac.close(); // spring container 종료 method
}

// 설정 파일
@Configuration
Static class LifeCycleConfig{
    @Bean
    public NetworkClient networkClient(){
        // bean 등록 원하는 객체 생성
    	NetworkClient networkClient = new NetworkClient();
        // setter 의존관계 주입
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }
}

 

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

 

  • 해당 결과 원인
    • 객체 생성시 url이 없지만 생성자 block 내부에 url 호출 코드가 존재해서 null인 상태로 method() 수행
    • 객체를 생성한 후 외부에서 url을 setter로 주입한다. - DI는 완료 되었지만 미리 연결 되진 않음
    • 이후 외부에서 url connect()를 호출 시, 그때 작동

 

  • 현 문제코드에서 어떻게든 해결하려는 code
public class NetworkClient {
    private String url;
    
    // 굳이 초기화 단계에서 connect하려고 할 경우
    public NetworkClient(){
        url = "url 객체 정보 넣어주기"; // 생성 초기화 분리 안되는 경우
    	System.out.println("생성자 호출, url = " + url);
        // connect(), call()과 같이 객체 생성이 아닌 뭔갈 수행하는 부분이 생성자랑 같이 있으면 유지보수에 안좋다.
        connect();
        call("초기화 연결 메시지")
    }
    
    public void connect(){ System.out.println("connnect: " + url); }
}

 

  • 문제점
    • 생성과 초기화 단계 분리가 되어있지 않다.
    • 현재 방식은 생성자 내부에 setUrl 값을 넣어서 수행하려는 시도와 동일
    • 유지 보수에 좋지않다.
    • 생성자는 해당 instance 생성에 집중해야하는데 url과 같이 외부 작업에도 집중하는 문제점 발생

 

  • 정리
    • Network class
      • 생성자 존재
      • connection()
      • disconnect()
      • 등등 여러 method 존재

    • @Configuration class
      • network.class를 빈으로 등록
      • url bean과 의존 관계 주입
      • 이후 초기화 call back으로 connect() 수행 == 외부 호출 전 미리 connect() 하려는 동작
        <- 이렇게 하고 싶지만 현재는 network.class에 어떻게 해야할지 모른다.
      • 소멸전 call back으로 disconnect()를 하고 싶은 것
         
    • 이전 까지 했던 작동 방법
      • network.class를 빈으로 등록
      • url bean과 의존 관계 주입
      • 그리고 외부 호출이 오면 그 때 connect()를 수행

 

  • 목표
    • "빈 생성 - 의존 관계 주입" 후에 미리 connect()와 같은 작업을 외부 요청 없이 수행을 원한다.
    • 생성자 주입이라고 해도 수정자 주입과 현 문제점이 다를게 없다. 얘도 미리 connect()할 방법이 없다.

 

1.2. 스프링 빈 라이프사이클 (중요)

  • 예외적으로 생성자 주입은 빈 생성과 의존관계 주입이 동시에 일어난다. - 자동, 수동 상관없이 동일
  • 수동(AppConfig) 같은 경우도 생성자 주입, setter 주입 다 가능하다.
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 소멸전 콜백 -> 스프링 종료

# 초기화 콜백 : DI까지 끝내고 외부 호출 전 미리 수행하고 싶은 method 수행
# 소멸전 콜백 : spring 종료 직전 해당 작업이 먼저 안전히 종료하고 싶은 method() 수행

 

2. 스프링 콜백 3가지 방법

  1. 인터페이스 - 안씀
  2. @Bean - 3.번으로 해결 안될 경우 사용
  3. @PostConstruct @Predestroy - 제일 많이 사용

 

2.1. 인터페이스(InitializingBean, DisposableBean ) 

  • 단점
    1. 스프링 전용 인터페이스로 초기화, 소멸 메서드의 이름을 변경 불가
    2. 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

  • 작동 방식
    • InitiallizingBean과 DisposableBean 인터페이스의 afterPropertiesSet(), destroy() 메서드를 이용해서 DI 후 초기화 콜백 시작
      • afterPropertiesSet() - DI 끝난 후 초기화 값을 넣어주는 과정
      • destroy()로 spring bean 소멸 지원한다.
// public class NetworkClient {
public class NetworkClient implements InitiallizingBean, DisposableBean {
    private String url;
    // 의존관계 주입
    public void setUrl(String url){ this.url = url; }
    
    public NetworkClient(){
        System.out.println("생성자 호출, url = " + url);
//      connect(); call(" 초기화 연결 메시지 ");
    }
    
    @Override // InitiallizingBean method
    public void afterPropertiesSet() throws Exception{
    	System.out.println("DI까지 끝나면 호출한다는 method - InitiallizingBean");
    	connect();
        call("초기화 연결 메시지");
    }
    
    @Override // DisposableBean method
    public void destroy() throws Exceptioin{
    	System.out.println("spring container 작업이 종료되면 소멸시키는 method - DisposableBean");
        disConnect();
    }
    
    public void connect(){ System.out.println("connnect: " + url); }
    public void call(String message){ System.out.println("call: " + url + ", message = " + message); }
    public void disconnect(){ System.out.println("close: " + url); }
}

 

 

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

  • 설정 정보에  @Bean(initMethod = "init", destroyMethod = "close")처럼 초기화, 소멸 메서드를 지정

  • 설정 정보 사용 특징
    • 메서드 이름을 자유 사용 가능 - init하든 initial 하든 상관 없다.
    • 스프링 빈이 스프링 코드에 의존하지 않는다. = 나의 source code에 스프링의 인터페이스 같은 걸 넣지 않음.
    • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드 적용 가능

  • 추가 기능 - 종료 메서드 추론 (autocloseable 학습)
    - 참고 : 라이브러리는 대부분 close(), shutdown() 이라는 이름의 종료 메서드를 사용한다.
    • @Bean의 destroyMethod 속성에는 default로  (inferred) 이 등록되어 있다. - 추론 기능

    • 추론 기능은  close(), shutdown()  라는 이름의 메서드를 자동으로 종료시점 호출 
      -> 외부 라이브러리는 대부분  close, shutdown 이름의 종료 메서드를 사용

    • 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작
      == 따로 @Bean(destroyMethod= "XXX")를 작성하지 않아도 동작한다.

    • 내 애플리케이션을 엉망으로하고싶은데하면 ->  destroyMethod="" 처럼 빈 공백을 지정 (추론 기능을 사용안하는 방법)

 

// public class NetworkClient implements InitiallizingBean, DisposableBean {
 public class NetworkClient {	
    private String url;
    
    public NetworkClient(){
    	System.out.println("생성자 호출, url = " + url);
    }
    
    // 의존관계 주입
    public void setUrl(String url){
    	this.url = url;
    }
    
    // 초기화 콜백 하고 싶은 method를 init() 메서드 내부에 넣기
    // -> 설정 파일 bean에서 초기화 콜백 method 명시
    public void init(){
    	System.out.println("DI까지 끝나면 호출한다는 method - InitiallizingBean");
    	connect(); // 초기화 콜백 하고 싶은 method
        call("초기화 연결 메시지"); // 초기화 콜백 하고 싶은 method
    }
    
	// 소멸전 콜백하고 싶은 method를 close() 메서드 내부에 넣기
    public void close(){
    	System.out.println("spring container 작업이 종료되면 소멸시키는 method - DisposableBean");
        disConnect(); // 소멸전 콜백 하고픈 method
    }
    ...
    }
}


// 설정 파일
@Configuration
Static class LifeCycleConfig{
	
    // 설정 파일 bean에서 초기화 콜백, 종료 콜백 method 명시
    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient(){
    	NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }
}

 

2.3. 애노테이션 @PostConstruct, @PreDestroy 

  • 제일 많이 사용
  • spring에서 권장하는 방법
  • @PostConstruct, @PreDestroy 애노테이션 특징  
    • 최신 스프링에서 가장 권장하는 방법
    • 애노테이션 하나만 붙이면 되므로 매우 편리하다.  
    • 패키지는  javax.annotation.PostConstruct 라는 스프링에 종속적인 기술이 아니라 JSR-250라는 자바 표준이다. 
    • 스프링이 아닌 다른 컨테이너에서도 동작한다. 컴포넌트 스캔과 잘 어울린다.

  • 단점
    • 외부 라이브러리에는 적용하지 못한다.
    • 외부 라이브러리를 초기화, 종료 해야 하면 @Bean(initMethod, destroyMethod)의 기능을 사용

 

 public class NetworkClient {	
    private String url;
    public void setUrl(String url){ this.url = url; }
    public NetworkClient(){ System.out.println("생성자 호출, url = " + url); }
    
    // 초기화 콜백
    @PostConstruct
    public void init(){
    	System.out.println("DI까지 끝나면 호출한다는 method - InitiallizingBean");
    	connect();
        call("초기화 연결 메시지");
    }
    
    // 소멸전 콜백
    @PreDestroy
    public void close(){
    	System.out.println("spring container 작업이 종료되면 소멸시키는 method - DisposableBean");
        disConnect();
    }
    
    // 서비스 시작 ~ 서비스 종료 까지의 코드
    public void connect(){ System.out.println("connnect: " + url); }
    public void call(String message){ System.out.println("call: " + url + ", message = " + message); }    
    public void disconnect(){ System.out.println("close: " + url); }
}


// TEST
@Configuration
Static class LifeCycleConfig{
    @Bean
    public NetworkClient networkClient(){
    	NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }
}

 

3. 외부 라이브러리

  • 오픈소스들을 외부 라이브러리라고 이해
  • 이 경우 소스코드를 포함하는것이 아니라 이미 컴파일된 class 파일이 모여있는 jar 파일을 포함
  • 따라서 소스코드 수정이 불가능

 

  • 특정 객체(NetworkClient)로 init, destory를 wrapping 하지 않고 외부 라이브러리로부터 객체를 생성해야 하는 경우
    • 코드 수정이 어려우므로 빈 설정을 통해 진행
    • --> 코드 수정이 어렵다는 것이 외부라이브러리 클래스 자체에 @PostConstruct, @PreDestroy 를 못 넣는다는 의미
@Configuration
public class BeanConfig {
  @Bean(initMethod = "externalLibraryInit", destoryMethod = "externalLibraryDestory")
  public ExternalLibraryObject externalLibraryObject() {
    return new ExternalLibraryObject();
  }

 

 

 

이전 발행글 : 스프링 핵심 원리 이해 6 - 의존관계 자동 주입

다음 발행글 : 스프링 핵심 원리 이해 8 - bean scope


출처: 인프런 스프링 핵심 원리 - 기본편