Spring, Apache, Java

Bean LifeCycle Callback

cwchoiit 2024. 5. 27. 14:57
728x90
반응형
SMALL

스프링 컨테이너에 등록하는 빈은 생명주기를 가진다. 생각해보자. 스프링이 나 대신 빈으로 등록하고 관리해주면 등록하는 과정이나 삭제하는 과정이 분명 있는게 어찌보면 당연하다.

 

그리고, 스프링 빈으로 등록하고 관리하는 과정중엔 초기화 하거나 삭제할때 꼭 해줘야하는 작업이 있을수도 있다.

예를 들면 데이터베이스를 사용할 때 스프링을 띄우면서 동시에 커넥션 풀에 커넥션들을 확보한다던지 스프링 서버를 내릴때 썼던 자원을 다시 반납하는 작업이라던지 이런 필수적으로 시작과 끝에 해줘야하는 작업들이 종종있다.

 

스프링에 내가 빈을 등록할 때 그런 작업이 필요하다면 어떻게 해야 적절하게 할 수 있을까? 스프링이 제공하는 대표적인 방법이 3가지 정도 있다.

  • InitializingBean, DisposableBean
  • 빈 등록 초기화, 소멸 메서드
  • @PostConstruct, @PreDestroy

하나씩 모두 알아보자.

 

InitializingBean, DisposableBean

우선, 약간의 가정이 필요하다. 어떤 애플리케이션에서 다른 네트워크로 연결해야 하는 빈이 있다고 가정하고 그 빈을 직접만들때 초기화와 소멸 작업을 해줘야 한다고 생각해보자. 그리고 그 클래스는 다음과 같다.

 

NetworkClient

package org.example.springcore.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);
    }
}

 

코드를 보면, connect(), disconnect()가 각각 초기화 때 실행할 메서드 소멸 시 실행할 메서드이다.

만약 저렇게 생성자에 connect()를 넣으면 원하는대로 동작할까? 테스트 해보자.

 

BeanLifeCycleTest

package org.example.springcore.lifecycle;

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

public class BeanLifeCycleTest {

    @Test
    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("https://hello-spring.dev");
            return networkClient;
        }
    }
}

우선, 저 NetworkClient를 빈으로 등록을 해야 한다. 빈으로 등록할 때 생성자로 인스턴스를 만들고 URL을 세팅한다. 그리고 반환하는데 스프링 컨테이너에서 빈을 꺼내올 때 어떤 결과를 도출하는지 보자. 

 

@Test 애노테이션이 붙은 테스트 코드를 보자.

  • AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
    • 우선 스프링 컨테이너를 가져온다. 
  • NetworkClient client = ac.getBean(NetworkClient.class);
    • 스프링 컨테이너를 가져와서 빈으로 등록한 NetworkClient 타입의 빈을 꺼낸다.
  • ac.close();
    • 그 후 스프링 컨테이너를 종료한다.

 

실행결과

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

 

URL이 제대로 설정되지 않았다. 왜냐하면 생성자에서 connect()를 호출하는데 생성자로 인스턴스를 만든 다음에 setUrl()을 실행했으니 당연한 결과다. 그럼 초기화를 안전하고 적절하게 하는 방법인 InitializingBean, DisposableBean를 사용해보자.

public class NetworkClient implements InitializingBean {
    ...
    
    @Override
    public void afterPropertiesSet() throws Exception {

    }
}

InitializingBean을 구현하면 구현해야 하는 메서드인 afterPropertiesSet()이 있다. 이 메서드는 빈으로 등록될 준비가 모두 끝난 상태에서 실행되는 메서드이다. 이 메서드에 위에서 사용했던 connect()를 넣으면 된다. 다른 말로 초기화할 때 필요한 작업을 이 메서드 안에서 하면 된다.

@Override
public void afterPropertiesSet() throws Exception {
    connect();
    call("초기화 연결 메시지");
}

그럼 빈으로 등록된 후 이 메서드는 자동으로 실행되게 된다. 다시 위 테스트 코드를 실행해보자.

 

실행결과

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

이번엔 생성자 호출 후 정상적으로 URL이 적용되어 있다. 아무것도 하지 않아도 빈 등록 후 실행되는 메서드를 실행하는 이 InitializingBean를 사용하면 초기화가 간단하게 진행된다. 그럼 이제 소멸 메서드도 사용해보자.

public class NetworkClient implements InitializingBean, DisposableBean {
    ...
    
    public void disconnect() {
        System.out.println("close = " + url);
    }
    
    @Override
    public void destroy() throws Exception {
        disconnect();
    }
}

이번엔 DisposableBean을 구현했다. 이 인터페이스는 destroy()를 가지고 있고 이게 스프링 컨테이너가 내려가기 전 실행되는 메서드라고 생각하면 된다. 즉, 빈이 소멸되기 바로 직전에 실행되는 메서드. 다시 위 테스트 코드를 실행해보자.

 

실행결과

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

이번엔 disconnect() 메서드까지 잘 실행됐다. 이렇게 생성과 소멸관련 메서드를 이용해서 초기화와 자원정리가 가능하다.

근데 이 방식은 우선 인터페이스를 구현해야 하는 단점이 있고, 너무 옛날 방식이라 요새는 거의 사용하지 않는다. 그래서 위 2가지 다른 방법을 또 알아보자.

 

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

이 방법은 인터페이스나 외부 다른 것에 의존하지 않고도 초기화와 소멸이 가능하다. 꽤나 깔끔한 방법으로 보인다. 바로 코드로보자.

 

NetworkClient

package org.example.springcore.lifecycle;

public class NetworkClient {
	
    ...

    public void init() {
        connect();
        call("초기화 연결 메시지");
    }

    public void close() {
        disconnect();
    }
}

이렇게 내가 초기화나 소멸 시 실행할 메서드를 작성하고 빈을 등록할때 "내 초기화 메서드와 소멸 메서드는 이거야!" 라고 알려주기만 하면 된다.

 

빈 등록 코드를 보자.

@Configuration
static class LifeCycleConfig {
    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("https://hello-spring.dev");
        return networkClient;
    }
}

 

저 코드에서 바로 이 부분이 알려주는 부분이다. 꽤나 깔끔하고 직관적이다.

@Bean(initMethod = "init", destroyMethod = "close")

 

이렇게 하고 위 테스트 코드를 그대로 실행해보자.

실행결과

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

 

의도대로 잘 동작한다. 그리고 이 destroyMethod 속성은 꽤 특별한 옵션이 있다. 기본값 추론이라는 기능인데 통상적으로 소멸 시 호출하는 메서드의 이름은 close, shutdown을 많이 쓰는데 이런 이름의 메서드로 만들어 놓으면 내가 저렇게 직접 destroyMethod = "close"라고 작성해주지 않아도 알아서 소멸 시 실행해준다. 

 

@PostConstruct, @PreDestroy

이게 젤 마지막에 있는 이유가 있다. 결론부터 말하면 그냥 이걸 쓰면 된다.

애노테이션으로 적용하는 방법이고 스프링에서도 이 방법을 권장한다.

 

NetworkClient

package org.example.springcore.lifecycle;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

public class NetworkClient {

    ...

    @PostConstruct
    public void init() {
        connect();
        call("초기화 연결 메시지");
    }

    @PreDestroy
    public void close() {
        disconnect();
    }
}

 

이렇게 적용하면 끝이다. 깔끔하고 군더더기 없다. 그리고 가장 편하다. 다시 테스트 코드를 실행해보자.

 

BeanLifeCycleTest

package org.example.springcore.lifecycle;

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

public class BeanLifeCycleTest {

    @Test
    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("https://hello-spring.dev");
            return networkClient;
        }
    }
}

이제 @Bean에 뭐 다른 옵션을 줄 필요없다. 바로 실행해보자.

 

실행결과

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

 

제일 간단하고 제일 명확하다. 유일한 단점은 코드를 고칠 수 없는 외부 라이브러리를 초기화하거나 종료해야 할 땐 사용할 수 없다는 것인데 이럴때만 두번째 방법인 @BeaninitMethod, destroyMethod 옵션을 사용하자.

728x90
반응형
LIST