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

김태훈·2023년 3월 7일
0

Spring 핵심 원리

목록 보기
15/15

본 게시글은 인프런 '김영한'님의 '스프링 핵심원리 - 기본편' 강의를 듣고 정리한 글입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

1.빈 생명주기 콜백 시작

데이터베이스 커넥션 풀(아직은 모르겠다..)이나, 네트워크 소켓처럼 어플리케이션 시작 시점에 필요한 연결을 미리 해두고, 어플리케이션 종료 시점에 연결을 모두 종료하는 작업을 하기 위해서 객체의 초기화와 종료 작업이 필요하다.

1. 외부 네트워크와 미리 연결하는 객체 생성

public class NetworkClient {

    private String url; //접속하려고 하는 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);
    }
}

2. 테스트 코드 생성

public class BeanLifeCycleTest {
    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close(); //ac를 받기 위해서 ConfigurableApplicationContext로 받아야함
    }

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

이 때, ApplicationContext로 하면 ac.close()를 호출할 수 없다.
ApplicationContext는 기본적으로 닫는 메서드가 정의가 되어있어서 이렇게 수동으로 하지 않아도 된다. 따라서, ConfigurableApplicationContext로 바꾸어서 진행한다.
ApplicationContext 안에 ConfigurableApplicationContext가 존재한다.(ConfigurableApplicationContext가 ApplicationContext를 상속받음)
또한 ConfigurableApplicationContext가 AnnotationConfigApplicationContext의 상위 부모이다. 따라서 new로 생성할 수 있음

3. 왜 결과물이 저럴까?

new생성자 메서드를 먼저 호출하면서 아직 url이 설정이 안되어서 그렇다.
즉, 객체를 생성한 다음에 외부에서 수정자주입(set)을 통해 url을 설정해야 url이 제대로 나온다.

4. 스프링 빈의 작동 방식 복기

스프링 빈은 (생성자 주입할 때는 아니고)
1. 객체 생성
2. 의존관계 주입
의 순으로 작동한다.
객체를 생성해 놔야, 의존관계로 무엇인가를 할 것인가 아닌가?
물론 생성자 주입같은 경우에는 예외일 것이다.

따라서, 스프링 빈은 객체를 생성한 후, 의존관계 주입이 완료가 되어야 데이터를 사용할 수 있게 된다.
그렇다면, 개발자가 어떻게 해당 과정이 다 끝났는지 알 수 있는가?

여기서 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해 초기화 시점(데이터 준비 완료)를 알려주는 기능을 제공한다.
또한, 스프링 컨테이너가 종료되기 직전에도 소멸 콜백을 주어서, 안전하게 종료시킨다.

5. 그래서 스프링 빈의 라이프 사이클은?

1. 스프링 컨테이너 생성

2. 스프링 빈 생성 (생성자 주입같은 경우 의존관계 주입이 여기에서 이루어짐)

3. 의존 관계 주입

4. 초기화 콜백 (빈 생성, 의존관계 주입 완료 후)

5. 데이터 사용

6. 소멸 전 콜백 (빈이 소멸되기 직전)

7. 스프링 종료

참고 : 객체의 생성과 초기화를 분리할 것!
유지보수 관점 때문에 그러하다.

  • 생성자 - 필수 정보(파라미터)를 받고 메모리 할당 후 객체 생성해야함
  • 초기화 - 생성된 객체를 활용하여 외부 커넥션을 연결하는 등 무거운 작업 수행해야함
    (객체 내부의 값을 변경하는 정도는 한꺼번에 생성과 초기화를 해도 된다.)

이 때, 스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.
1. 인터페이스 (InitializingBean, DisposableBean)
2. 설정 정보에 초기화 메서드, 종료 메서드 지정
3. @PostConstruct, @PreDestory 어노테이션 지원

2. 인터페이스로 빈 생명주기 콜백

1. InitializingBean

이미 만들었던 NetworkClient 클래스를 InitializingBean을 구현하게 끔 implements코드를 넣어주고, 메서드를 오버라이드 하면

@Override
public void afterPropertiesSet() throws Exception {

}

이는 의존관계 주입이 끝나면 진행할 코드를 담는다.
이를 이렇게 바꿔주자 (생성자에 있던 코드를 잘라 붙임)

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

2. DisposableBean

추가했던 InitializingBean과 더불아 DisposableBean 또한 구현체로 implements한다.

@Override
public void destroy() throws Exception {
    disconnect();
}

다음과 같이 코드를 작성하여 연결을 해제시키자.

3. 테스트


스프링 의존관계 주입이 끝나면(초기화가 끝나면)
afterPropertiesSet 함수로 인해
connect,
call 을 하고
AnnotationContext가 완전히 close 되기 전에, (ac.close())
빈들이 하나씩 죽어나가면서 destroy가 호출된다.

4. 단점

  • 스프링에 의존하는 인터페이스를 사용해야한다.
  • 초기화,소멸 메서드의 이름을 변경할 수 없다.
    따라서 잘 사용하지 않는다.

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

1. 코드

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

아까랑 동일하게, 같은 역할을 하는 메서드이지만, 인터페이스 구현한 부분을 빼고 새로운 메서드명으로 클래스 내부에 작성했다.

public void happy(){ // 초기화 메서드
    System.out.println("NetworkClient.init");
    connect();
    call("초기화 연결 메세지");
}
public void terrify() { // 종료 메서드
    System.out.println("NetworkClient.close");
    disconnect();
}

이러한 메서드를 지정한채로,
테스트코드 의 @Configuration 붙은 클래스 내부 Bean 어노테이션에 init, destroy 메서드의 속성으로 해당 메서드 이름을 부여하는 것이다.
그 코드는 다음과 같다.

@Configuration
static class LifeCycleConfig{
    @Bean(initMethod = "happy", destroyMethod = "terrify" ) //메서드 속성에 해당 메서드 이름을 넣는다.
    public NetworkClient networkClient(){
    NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-goat.com")
        return networkClient;
    }
}

2. 특징

  • 메서드 이름을 자유롭게 지을 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다. (빈에 등록한 클래스 자체는 순수 자바코드로 이루어짐)
  • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화,종료 메서드를 적용할 수 있다.

3. destroy 메서드의 특징

라이브러리는 대부분 종료할 때, close / shutdown 이름의 종료 메서드를 호출한다.
@Bean의 destroy 메서드는 기본값이 (inferred) 로 설정이 되어있다.
이때 이 inferred("추론") 기능은 close, shutdown이라는 이름의 메서드를 자동으로 호출해준다. 즉, 이 름 그대로 종료 메서드를 추론해 호출해준다.
따라서 기본적으로 destroyMethod= 으로 적어주지 않아도 default로 inferred가 붙어있기 때문에, 따로 설정해주지 않아도 되지만, 만약 종료 메서드를 자동으로 호출해주는 것을 막고자 한다면,

@Bean(destroyMethod="")

처럼 빈 공백으로 설정하면 된다.

4. 애노테이션 (이 방법으로 쓸 것!)

1. @PostConstruct

2. @PreDestroy

말 그대로이다. 이를 NetworkClient에 어노테이션으로 추가해보자.

public class NetworkClient {

    private String url; //접속하려고 하는 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 happy(){
        System.out.println("NetworkClient.happy");
        connect();
        call("초기화 연결 메세지");
    }
    @PreDestroy
    public void terrify() {
        System.out.println("NetworkClient.terrify");
        disconnect();
    }
}

3. 특징

  • 최신 스프링에서 가장 권장하는 방법
  • import jakarta.annotation.PostConstruct;
    import jakarta.annotation.PreDestroy;
    와 같이 패키지를 보면 jakarta(java)에서 직접 지원하는 기술로, 스프링이 아닌 다른 컨테이너에서도 적용이 가능하다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 유일한 단점은 외부 라이브러리에 적용을 못한다.
    따라서 외부 라이브러리를 초기화,종료 메서드를 호출하고자 한다면 아까 했던 @Bean(init,destroy)를 호출하면 된다.
profile
기록하고, 공유합시다

0개의 댓글