Spring

스프링 기본편 7

woohap 2025. 1. 20. 00:04

빈 생명주기 콜백

빈 생명주기 콜백 시작

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

- 간단하게 외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정
  실제로 네트워크에 연결하는 것은 아니고, 단순히 문자만 출력하도록 했을 때
  NewWorkClient는 애플리케이션 시작 시점에 connect()를 호출해서 연결을 맺어두어야 하고 
  애플리케이션이 종료되면 disConnect()를 호출   해서 연결을 끊어야 한다.

[참고]
- 애플리케이션 서버가 올라올 때
  데이터베이스랑 연결을 미리 맺어놓는다.
  클라이언트가 요청을 할 때 미리 연결된 것을 사용
  응답을 빠르게 할 수 있게 됨

- 애플리케이션 서버가 종료될 때,
  안전하게 종료되어야 한다.
  이를 스프링이 지원해준다.

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);
    }
}



public class BeanLifeCycleTest {

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

        // ac.close()는 ApplicationContext에 존재하지 않음, ConfigurableApplicationContext - AnnotationConfigApplicationContext의 부모
        ac.close();
    }

    @Configuration
    static class LifeCycleConfig {

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

     }
}


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


- 생성자 부분을 보면 url 정보 없이 connect가 호출되는 것을 확인할 수 있다.
  객체를 생성하는 단계에는 url이 없고, 객체를 생성한 다음에 외부에서 
  수정자 주입을 통해 setUrl()이 호출되어야 url이 존재하게 된다.

[스프링 빈은 간단하게 다음과 같은 라이프사이클을 갖는다.]

- 객체 생성 → 의존관계 주입 순서 
  생성자 주입은 예외

- (1)스프링 빈은 객체를 생성하고, (2)의존관계 주입이 다 끝난 다음에야 
  필요한 데이터를 사용할 수 있는 준비가 완료된다.
  (3)초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.

- 초기화란 객체 안에 필요한 값이 다 연결되어있는 상태

[개발자가 어떻게 의존관계 주입이 모두 완료된 시점을 알 수 있는가 ??]
- 스프링의 의존관계 주입이 완료되면, 스프링 빈에게 콜백 메서드를 통해
  초기화 시점을 알려주는 다양한 기능을 제공한다.

- 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다.
  (일반적인 싱글톤 객체의 경우)

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

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

- 초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
- 소멸전 콜백 : 빈이 소멸되기 직전에 호출

[객체의 생성과 초기화를 분리하자 !!]
- 생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 갖는다.
  객체를 생성할 때는 필요한 필수 값들을 넣어서 자바에서 생성되는 것 까지만 집중하는것이 좋다.
  → 즉, 객체를 생성해서 메모리에 올리고 필요한 값을 세팅하는 것 까지만 하는 것이 좋다.

- 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다.
  초기화 작업을 하는 것은 객체가 동작하는 것을 의미
  → 최초의 액션이 발생했을 때 초기화할 수 있다.(지연 로딩 느낌)

- 생성자 안에서 무거운 초기화 작업을 함께하는 것보다
  객체를 생성하는 부분과 초기화하는 부분을 명확하게 나누는 것이
  유지보수 관점에서 좋다.

- 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는
  생성자에서 한 번에 다 처리하는 것이 더 나을 수 있다.

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

인터페이스(InitializingBean, DisposableBean)

public class NetworkClient implements InitializingBean, DisposableBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        // 의존 관계 주입이 끝나면 호출
        System.out.println("NetworkClient.afterPropertiesSet");
        connect();
        call("초기화 연결 메시지");
    }

    @Override
    public void destroy() throws Exception {
        // 스프링 빈이 종료되기 직전
        System.out.println("NetworkClient.destroy");
        disConnect();

    }
}

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

[참고]
- 인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들
  지금은 다음의 더 나은 방법들이 있어서 거의 사용하지 않는다.

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

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

@Configuration
    static class LifeCycleConfig {

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

[설정 정보 사용 특징]
- 메서드 이름을 자유롭게 줄 수 있다.
- 스프링 빈이 스프링 코드에 의존하지 않는다.
- 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 
  외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

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

AutoCloseble 찾아서 공부해볼 것

세 가지 중에 이 방법을 사용해라

애너테이션 @PostConstruct, @PreDestory

@PostConstruct
public void init() throws Exception {
    // 의존 관계 주입이 끝나면 호출
    System.out.println("NetworkClient.init");
    connect();
    call("초기화 연결 메시지");
}

@PreDestroy
public void close() throws Exception {
    // 스프링 빈이 종료되기 직전
    System.out.println("NetworkClient.close");
    disConnect();
}

- 최신 스프링에서 가장 권장하는 방법
- 애노테이션 하나만 붙이면 되므로 매우 편리
- 패키지를 잘 보면 javax.annotation.PostConstruct
  스프링에 종속적인 기술이 아니라 
  JSR-250라는 자바 표준 스프링이 아닌 다른 컨테이너에도 적용 가능

- 컴포넌트 스캔과 잘 어울린다.
  → 애너테이션만 붙이면 되기 때문에

- 유일한 단점은 외부 라이브러리에는 적용하지 못한다.
  외부 라이브러리를 초기화 종료해야 한다면 @Bean의 기능을 사용하자

[정리]
- @PostConstruct, @PreDestroy 애너테이션을 사용하자
- 코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면
  @Bean의 initMethod, destroyMethod를 사용하자

내용 출처

인프런 - 스프링 핵심 원리 (기본편)
강사명 - 김영한

노션 링크

https://earthy-grouse-d42.notion.site/7-17c5723e06ee80fe88f4df92b5cd45e1

'Spring' 카테고리의 다른 글

스프링 기본편 8  (0) 2025.01.21
스프링 기본편 - 6  (1) 2025.01.19
스프링 기본편 5  (0) 2025.01.14
스프링 기본편 4  (0) 2025.01.13
스프링 기본편 3  (0) 2025.01.11