@Bean의 라이프사이클, 그리고 콜백

namkun·2022년 3월 29일
0

Spring

목록 보기
14/18

해당 내용은 '스프링 입문을 위한 자바 객체 지향의 원리와 이해'와 인프런 김영한님의 '스프링 핵심 원리 - 기본편' 강의를 참고하였습니다.


예를 들어 DB 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료 작업이 필요하다.

한 번 테스트로 외부 커넥션이랑 연결한다고 가정하고 (실제로는 메세지만 띄울꺼지만..) 테스트 코드를 작성해보자.

NetworkClient.java

public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("initializing msg");
    }

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

    // 서비스 시작시 호출
    public void connect(){
        System.out.println("connect : " + url);
    }

    public void call(String msg){
        System.out.println("call : " + url + " / msg : " + msg);
    }

    // 서비스 종료시에 호출
    public void disConnect(){
        System.out.println("close " + url);
    }
}

BeanLifeCycleTest.java

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(LifeCycleConfig.class); // close 기능을 사용하기 위해서 ConfigurableApplicationContext를 사용하였음.
        NetworkClient client = applicationContext.getBean(NetworkClient.class);
        applicationContext.close();
    }

    @Configuration
    static class LifeCycleConfig{
        @Bean
        public NetworkClient networkClient(){
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://Network-Tst.com");
            return networkClient;
        }
    }
}

실행해보면 다음과 같은 결과가 나온다.

생성자 호출, url = null
connect : null
call : null msg : initializing msg

우리가 setUrl을 했음에도 불구하고 null 이 나온다.

잘 생각해보면 우리가 객체를 생성하는 시점에는 url이 세팅이 안되어 있고, 객체를 생성하고 난 뒤에 url을 set 하기에 이런 일이 발생한 것이다.

스프링 빈은 다음과 같은 라이프사이클을 갖는다.

객체 생성 -> 의존 관계 주입

(생성자 주입은 제외한다. 생성자 주입은 생성자가 생성됨과 동시에 빈을 주입하기에.. 여기서는 setter나 field injection을 이야기 한다)

스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에서야 데이터를 사용할 수 있게 된다.

따라서 초기화 작업은 의존관계 주입이 다 마무리 된 이후에 호출해야만 한다.

그런데, 개발자가 이 시점을 어떻게 알 수 있을까?

스프링 빈의 이벤트 라이프사이클을 알아보자 (싱글톤 기준)

  1. 스프링 컨테이너 생성
  2. 스프링 빈 생성
  3. 의존관계 주입
  4. 초기화 콜백
  5. 사용
  6. 소멸전 콜백
  7. 스프링 종료

위에서 볼 수 있듯이 스프링은 콜백 메서드를 통해서 특정 시점을 알려주는 역할이 있다. 이를 통해서 개발자는 시점에 대해 알 수 있는 것이다.

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

한 번 알아보도록 하자.

인터페이스 InitialzingBean, DisposalbleBean

사용법은 간단하다

기존의 클래스에 다음과 같이 인터페이스를 상속받는다.

물론 그에 따른 구현 메서드도 추가해준다.

InitializingBean에는 afterPropertiesSet()이라는 메서드가,

DisposableBean에는 destroy()라는 메서드가 구현이 필요 된다.

NetworkClient.java

public class NetworkClient  implements InitializingBean, DisposableBean {
	
    ...
    
    // propreties의 Setting이 끝나면 (의존관계 주입이 끝나면) 호출
    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call("initializing msg");
    }

    // bean이 소멸될 때 호출하겠다.
    @Override
    public void destroy() throws Exception {
        disConnect();
    }
}

주석에도 적어놓았듯, 해당 메서드들은 위의 빈의 이벤트 라이프 사이클에서 초기화 콜백과 소멸 콜백에 해당되는 메서드들이다.

자 그럼 이제 위의 코드처럼 우리가 기존에 의존관계가 주입되기전에 호출되었던 메서드들을 초기화 콜백에서 호출하고, 의존관계가 종료될때 호출되어야 했던 메서드를 소멸 콜백에서 호출해보자.

connect : http://Network-Tst.com
call : http://Network-Tst.com / msg : initializing msg
00:16:58.523 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@33dd6bed, started on Wed Mar 30 00:16:58 KST 2022
close http://Network-Tst.com

다음과 같이 우리가 setter에서 세팅한 url이 들어온 것을 알 수 있고, 빈이 소멸될 때 close 되는 것을 역시 확인할 수 있다.

그러나 이 인터페이스에도 단점이 존재한다.

  • 해당 인터페이스는 스프링 전용 인터페이스이다. 해당 코드는 스프링 전용 인터페이스에 의존한다.
  • 초기화, 소멸 메서드의 이름을 바꿀 수 없다.
  • 외부라이브러리에 적용할 수 없다. (내가 직접적으로 고칠 수 있는 것이 아니기 때문.)

이러한 인터페이스를 사용하는 방법은 스프링 초기(2003년..)에나 나온 것들이고, 지금은 더 좋은 방법이 있어서 거의 사용되지 않는다고 한다.

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

빈을 등록하는 시점에 어떤게 초기화이고, 어떤게 소멸인지를 지정할 수 있는 방법이다.

다음과 같이 어노테이션을 붙여주기만 하면 된다.

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

이렇게 하면 다음과 같은 이점을 얻을 수 있다.

  • 메서드 이름이 강제되지 않는다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 코드가 아닌 설정 정보를 사용하는 것이기에, 외부 라이브러리에도 초기화, 종료 메서드를 지정할 수 있다.

자 기존 코드에 적용시켜보도록 하자.

NetworkClient.java

public class NetworkClient  {

    ...
    
    public void init() throws Exception {
        System.out.println("NetworkClient.init");
        connect();
        call("initializing msg");
    }

    public void close() throws Exception {
        System.out.println("NetworkClient.close");
        disConnect();
    }
}

아까 사용하던 메서드들의 이름을 바꾸고, 더이상 상속받지 않기 때문에 위의 @Override를 뗴주었다.

그리고 호출하는 부분에서 다음과 같이 바꿔준다.

BeanLifeCycleTest.java

    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = applicationContext.getBean(NetworkClient.class);
        applicationContext.close();
    }

    @Configuration
    static class LifeCycleConfig{
        @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient networkClient(){
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://Network-Tst.com");
            return networkClient;
        }
    }

Config 클래스에서 Bean을 등록할때 초기화 콜백 메서드, 소멸 콜백 메서드에 대해 적어준다.

그리고 실행해보자.

NetworkClient.init
connect : http://Network-Tst.com
call : http://Network-Tst.com / msg : initializing msg
00:35:30.817 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@bfea0460, started on Wed Mar 30 00:35:30 KST 2022
NetworkClient.close
close http://Network-Tst.com

정상적으로 잘 동작하는 것을 확인할 수 있다.

종료 메서드의 '추론' 기능

@BeandestroyMethod에는 특별한 기능이 있다.
원본 코드를 확인 해보면 다음과 같이 적혀있는 것을 볼 수 있는데,

String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;

여기서 보이는 INFER_METHOD를 타고 들어가면 다음과 같이 적혀있는 것을 볼 수 있다.

public static final String INFER_METHOD = "(inferred)";

즉, destroyMethod의 기본값이 inferred(추론) 값이라는 건데, 이 기능은 보통 라이브러리들의 종료 메서드 명인 closeshutdown 에 해당되는 메서드들을 자동으로 호출해준다.

따라서 직접 스프링 빈으로 등록만 해주면, 종료 메서드는 따로 적어주지 않아도 된다.

이 추론기능을 사용하고 싶지않다면 destroyMethod의 값을 공백("")으로 주면 된다.

Annotation @PostConstruct, @PreDestroy

결론만 말하자면...이걸 쓰면 된다!

바로 코드에 적용해보자.

NetworkClient.java

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

public class NetworkClient  {

	...
        
    @PostConstruct
    public void init() throws Exception {
        System.out.println("NetworkClient.init");
        connect();
        call("initializing msg");
    }

    @PreDestroy
    public void close() throws Exception {
        System.out.println("NetworkClient.close");
        disConnect();
    }
}

더 쉽다! 이렇게 어노테이션만 더해주자.

결과는 동일하다.

NetworkClient.init
connect : http://Network-Tst.com
call : http://Network-Tst.com / msg : initializing msg
00:55:54.404 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@df47851, started on Wed Mar 30 00:55:54 KST 2022
NetworkClient.close
close http://Network-Tst.com

자 그러면 이 어노테이션의 특징에 대해 알아보자.

  • 스프링에서 무려 '권장'하는 방법
  • import되는 패키지를 보면 javax.annotation으로 시작한다. 이는 스프링에 종속되는 것이 아니라 JSR-250 이라는 자바 표준이라는 것을 의미한다.
  • 고로 스프링이 아닌 다른 컨테이너에서도 잘 작동한다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 단점은...외부 라이브러리에 사용을 하지 못한다는 것이다. 그러니 외부라이브러리에 콜백을 적용할 일이 있다면 위에서 배운 @BeaninitMethoddestroyMethod를 이용하도록 하자.
profile
개발하는 중국학과 사람

0개의 댓글