ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Bean Lifecycle Callback
    Java/Spring 2025. 1. 3. 17:22

    스프링의 빈 생명주기 콜백에 대해 정리하였습니다.

     

     


    < 빈 생명주기 콜백 >

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

     

    Ex)

    • 외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정.
    • 실제로 네트워크에 연결되는 것은 아니고 단순히 문자열을 출력하는 코드.
    • NetworkClient 객체는 애플리케이션의 시작 시점에 connect() method를 호출해서 연결을 맺고
    • 애플리케이션이 종료되면, disconnect() method를 호출해서 연결 끊음.

    로직 코드 :

    package hello.core.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("connent: " + url);
        }
    
        public void call(String message) {
            System.out.println("call: " + url + ", message = " + message);
        }
    
        //서비스 종료시 호출
        public void disconnect() {
            System.out.println("disconnect: " + url);
        }
    }

     

    테스트 :

    package hello.core.lifecycle;
    
    class BeanLifeCycleTest {
    
        @Test
        public void lifeCycleTest() {
            ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
            NetworkClient client = ac.getBean(NetworkClient.class);
            ac.close(); //스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
        }
    
        @Configuration
        static class LifeCycleConfig {
            @Bean
            public NetworkClient networkClient() {
                NetworkClient networkClient = new NetworkClient();
                networkClient.setUrl("http://hello-spring.dec");
                return networkClient;
            }
        }
    }

     

    테스트 결과

    : 객체 생성 시 초기에는 url이 null입니다. setUrl() 메서드를 통해 외부에서 수정자 주입을 해야만 url 값이 설정됩니다.

     

    앞서 계속 언급한 바와 같이 스프링 빈의 라이프사이클

    1. 객체 생성
    2. 의존관계 주입

    위 두 단계를 거쳐서 일어납니다. 그렇기 때문에 객체가 생성되고, 이에 따른 의존관계가 주입된 이후에야 필요한 데이터를 사용할 수 있는 준비가 완료됩니다.

    그런데, 개발자의 입장에서 의존관계가 주입 완료된 시점을 어떻게 알 수 있을까요?

     

    이를 위해, 스프링은 의존관계 주입이 완료된 후 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알리는 기능과, 스프링 컨테이너가 종료되기 직전에 소멸 콜백 메서드을 제공하여 안전하게 종료 작업을 수행할 수 있도록 지원합니다.

    (여기서 초기화란 객체의 생성이 아니라, 객체가 필요한 모든 값을 갖추고 제대로 작동할 준비가 된 상태를 의미합니다.)

     

     


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

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

     

    스프링 빈이 생성된 이후에 의존관계 주입이 이루어지고, 의존관계 주입까지 갖춰진 이후에 초기화 작업을 수행해야 스프링 빈이 제 역할을 할 수 있도록 만들어집니다.

    • 초기화 콜: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후에 호출됩니다.
    • 소멸자 콜: 빈이 소멸되기 직전에 호출됩니다.

     

    스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원합니다.

    1. 인터페이스(InitializingBean, DisposableBean)
    2. 설정 정보에 초기화 메서드, 종료 메서드를 지정
    3. @PostConstruct, @PreDestroy 어노테이션 지원

     

     

    < 객체의 생성과 초기화를 분리하자 >
    생성자 호출 시 모든 것을 처리하는 것이 더 간단하지 않을까라는 의문이 들 수 있지만, 이는 객체 지향 설계의 원칙 중 하나인 SRP(단일 책임 원칙)와 관련이 있습니다.
    생성자는 필수 정보(파라미터)를 수집하고 메모리를 할당하여 객체를 생성하는 역할만을 맡습니다. 반면, 초기화는 생성된 객체를 이용해 외부 커넥션을 설정하는 등 추가적인 무거운 작업을 수행합니다. 따라서, 생성자에 무거운 초기화 작업을 포함시키기보다는 초기화를 별도로 분리하는 것이 유지보수 측면에서 더 바람직합니다.
    다만, 초기화 작업이 내부 값을 단순히 변경하는 정도라면 생성자에서 처리하는 것이 유리할 수도 있습니다.

     

     


    < 인터페이스 InitializingBean, DisposableBean 사용 >

    스프링 빈은 두 인터페이스, 즉 InitializingBeanDisposableBean을 상속받고 이를 오버라이드하여 커스터마이징 함으로써 생명주기 콜백을 사용합니다.


    InitializingBean은 afterPropertiesSet() method로 초기화를 지원하고, DisposableBean은 destory() method로 소멸을 지원합니다.

     

    Ex)

    package hello.core.lifecycle;
    
    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")처럼 초기화, 소멸 메서드를 지정함으로써 생명주기 콜백을 사용합니다.

     

    Ex)

    1. 클래스 내에 초기화 및 소멸 시 호출될 메서드를 직접 정의합니다.

    public class NetworkClient {
    
        //...
    
        public void init() {
            System.out.println("NetworkClient.init");
            connect();
            call("초기화 연결 메시지");
        }
    
        public void close() {
            System.out.println("NetworkClient.close");
            disconnect();
        }
    }

     

    2. 빈으로 등록 시 초기화 및 소멸 시 호출할 메서드를 지정합니다.

    package hello.core.lifecycle;
    
    class BeanLifeCycleTest {
    
        //...
    
        @Configuration
        static class LifeCycleConfig {
    
            @Bean(initMethod = "init", destroyMethod = "close")
            public NetworkClient networkClient() {
                NetworkClient networkClient = new NetworkClient();
                networkClient.setUrl("http://hello-spring.dec");
                return networkClient;
            }
        }

     

     


    설정 정보 사용의 특징

    • 메서드의 이름을 자유롭게 설정할 수 있습니다.
    • 스프링 빈이 스프링 코드에 의존하지 않게 됩니다.
    • 코드가 아니라 설정 정보를 사용하기 때문에, 코드를 고칠 수 없는 외부 라이브러리에도 적용이 가능합니다.

     

    +. 종료 메서드의 추론

    @Bean의 destroyMethod 속성에는 종료 메서드를 자동으로 호출하는 특별한 기능이 있습니다. 대부분의 라이브러리는 종료 메서드의 이름으로 close나 shutdown을 사용하며, destroyMethod의 기본값은 inferred로 설정되어 있어 이러한 이름의 메서드를 자동으로 호출합니다. 이 기능 덕분에 종료 메서드를 따로 지정하지 않아도 잘 동작하지만, 원하지 않으면 destroyMethod = ""로 설정해 비활성화할 수 있습니다.

     

     


    < 어노테이션 @PostConstruct, @PreDestroy >

    초기화와 소멸 과정에서 수행하고 싶은 메서드에 @PostConstruct와 @PreDestroy 애노테이션을 붙이면 생명주기 콜백을 사용할 수 있습니다.

    JSR-250라는 자바 표준 기술입니다.

     

     

    Ex)

    package hello.core.lifecycle;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    
    public class NetworkClient {
    
        //...
    
        @PostConstruct
        public void init() {
            System.out.println("NetworkClient.init");
            connect();
            call("초기화 연결 메시지");
        }
    
        @PreDestroy
        public void close() {
            System.out.println("NetworkClient.close");
            disconnect();
        }
    }

     

     


    @PostConstruct, @PreDestroy 어노테이션 사용의 특징

    • 최신 스프링에서 가장 권장하는 방법입니다.
    • 어노테이션만 잘 붙어있으면 돼서 아주 편리합니다.
    • 자바 표준 기술이기 때문에, 스프링 컨테이너가 아니어도 잘 동작합니다.
    • 컴포넌트 스캔과 잘 어울립니다.
    • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 점입니다. 외부 라이브러리를 초기화, 종료하는 과정을 정의하고 싶은 경우 @Bean을 섞어서 사용합시다.

     

     

    지금까지 내용을 정리하자면, 기본적으로 @PostConstruct, @PreDestroy 어노테이션을 사용하되, 코드를 고칠 수 없는 외부 라이브러리를 사용하는 경우 @Bean initMethod, destroyMethod를 사용하는 것이 좋습니다.

     

     

     

     

     

     

     

     

     

     

     


    해당 글에 포함된 코드나 그림은 김영한님이 제공해주신 자료를 바탕으로 작성되었습니다.

    스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

     

    스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

    김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보

    www.inflearn.com

     

    'Java > Spring' 카테고리의 다른 글

    Understanding Web Applications  (5) 2025.01.14
    Bean Scope  (1) 2025.01.12
    Automatic injection of dependencies  (1) 2025.01.03
    Component Scan  (3) 2024.12.20
    Singleton Container  (0) 2024.12.18

    댓글

Designed by Tistory.