안녕하세요!

여러분.. 스프링 빈이 언제 죽는다고 생각하십니까?

심장 깊숙이 총알이 박혔을 때?
불치의 병에 걸렸을 때? 
독버섯으로 만든 스프를 마셨을 때? 

바로… 지금 알아보러 갑시다!

👉이전 글 보러가기


상황 가정

우선 저희의 shopping 프로젝트에서보단 따로 상황을 가정해서 만들어봅시다.

NetworkClient라는 클래스가 있고, 이녀석은 서비스 시작 시 connect()라는 함수를, 종료시  disconnect()라는 함수를 실행하는 녀석입니다. URL에 접속하는 친구이고, 일단 우선 url을 설정해서 접속해보는 상황으로 만들어보겠습니다.

test쪽에 lifecycle이란 패키지를 만들어주고 그 안에 NetworkClient.java를 만들어줍시다.

그리고 구현을 해봅시다. 각각의 메서드들이 실행되면서 현재 어떤 메서드가 실행된것인지 확인하기 좋게 출력해줍시다.

package com.naver.shopping.lifecycle;
 
public class NetworkClient {
    
    private String url;
    
    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }
 
    public void setUrl(String url) {
        this.url = url;
    }
 
    private 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()로 연결할 수 있게 해주었습니다.

추가적으로 LifeCycleConfig.java를 만들어 해당 클래스를 스프링에서 쓸 수 있도록 빈으로 만들어줍시다.

package com.naver.shopping.lifecycle;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class LifeCycleConfig {
 
    @Bean
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("https://www.github.com");
        return networkClient;
    }
}

이제 한번 잘 작동되나 해볼까요?

lifecycle 패키지에 BeanLifeCycleTest.java를 만들어 테스트해줍시다.

package com.naver.shopping.lifecycle;
 
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class BeanLifeCycleTest {
 
    @Test
    public void lifeCycleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close(); // 스프링 컨테이너를 아예 종료하는 기능으로, ConfigurableApplicationContext 타입 필요
    }
}

실행 결과

아니 url이 다 null이 나오다니!!!! 사실 뻔한 일입니다.

    @Bean
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("https://www.github.com");
        return networkClient;
    }

LifeCycleConfig에서 보면 new를 해서 이미 생성자를 실행하게 됩니다. 그럼 생성자 호출 단계에서 url은 아직 설정되지 않았기에 null이 되겠죠? 그리고 생성자 안에 있는 connect()도 url 주입을 못 받은 다음 실행되어서 null이고, call도 그렇습니다.

이쯤되면 슬슬 어떤순서로 스프링이 작업을 진행하는지 헷갈리실겁니다. 스프링 빈의 이벤트 라이프사이클입니다.

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

저희는 여기서 초기화 콜백, 소멸전 콜백으로 해당 부분에 작업을 시키면서 위와 같은 문제를 해결해보려 합니다.


초기화, 소멸 메서드 지정

초기화와 소멸할 때 사용할 메서드를 저희가 만든 메서드들 중에서 직접 지정할 수 있습니다. 저희는 connect, disconnect였죠? 한번 해봅시다.

우선 생성자에서 connect()와 call() 같이 시작하자마자 할 작업들을 지워줍시다. 그 후 init()이라는 아예 초기화 콜백 단계에 진행할 작업들을 넣은 메서드를 만들고, close()라는 소멸전 콜백 단계에 진행할 작업들을 넣은 메서드를 만들어줍시다.

package com.naver.shopping.lifecycle;
 
public class NetworkClient {
 
    private String url;
 
    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
    }
 
    public void setUrl(String url) {
        this.url = url;
    }
 
    private 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 void init() {
        System.out.println("==Open NetworkClient Start==");
        connect();
        call("초기화 연결 메시지");
        System.out.println("==Open NetworkClient End==");
    }
 
    public void close() {
        System.out.println("==Close NetworkClient Start==");
        disconnect();
        System.out.println("==Close NetworkClient End==");
    }
}

그 후 LifeCycleConfig에서 @Bean부분에 initMethod, destroyMethod를 추가해줍시다.

그리고 동일한 테스트를 돌려봅시다.

실행 결과

new를 통해 생성자가 실행될 때는 url이 null이지만 의존관계 주입 단계에서 setUrl이 먼저 진행이 되고 그 이후 생성자 콜백이 진행되면서 connect()와 call()에서 url이 잘 나오고 있는 것을 확인하실 수 있습니다.

그리고 ac.close()를 해서 스프링 컨테이너를 종료하며 자동으로 소멸자 기능도 실행되는 것도 확인하실 수 있습니다.


@PostConstruct와 @PreDestroy

얘들은 설정 클래스에서 설정을 해 주는게 아니어서 LifeCycleConfig를 원래대로 돌려줍시다.

package com.naver.shopping.lifecycle;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class LifeCycleConfig {
 
    @Bean
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("https://www.github.com");
        return networkClient;
    }
}

이 어노테이션은 바로 우리가 만든 NetworkClient에 직접 넣어주면 됩니다. 바로 보여드리겠습니다.

package com.naver.shopping.lifecycle;
 
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
 
public class NetworkClient {
 
    ...
 
    @PostConstruct
    public void init() {
        System.out.println("==Open NetworkClient Start==");
        connect();
        call("초기화 연결 메시지");
        System.out.println("==Open NetworkClient End==");
    }
 
    @PreDestroy
    public void close() {
        System.out.println("==Close NetworkClient Start==");
        disconnect();
        System.out.println("==Close NetworkClient End==");
    }
}

간단하죠? 설명이 크게 필요하진 않을 것 같습니다. 실행결과를 확인해봅시다.

실행 결과

같은 결과가 나오는 것을 확인하실 수 있습니다.


두 방법 중 어떤 걸 써야할까?

둘 다 쓰기 좋아보이는데 어떤걸 쓰면 좋을까요? 상황에 따라 다릅니다.

1. 외부에서 받은 클래스를 쓰는 경우

외부 클래스는 저희가 수정을 못하죠. 이 코드안에 @PostConstruct같은 어노테이션을 추가하지 못하니 저희가 설정할 설정 클래스에서 @Bean에 initMethod, destroyMethod를 넣어주는 방법이 수월할것입니다.

2. 내가 만든 클래스를 쓰는 경우

내가 만든 클래스는 중간에 어노테이션 자유롭게 넣을 수 있겠죠? @Bean에 메서드 이름을 string으로 넣어주면 컴파일이 잡지 못하는 에러가 나올 수도 있기 때문에 @PostConstruct, @PreDestroy를 사용해줍시다.


마치며

이제 슬슬 스프링의 기본 기능을 배워보는 과정이 끝이 다가오고 있습니다!

그렇다고 아예 끝은 아니긴 하죠. MVC패턴에 대해서도 배우면서 API도 만들어서 진정한 개발자로 거듭나봅시다!!!

오늘 작업한 코드 내용은 아래 커밋에서 확인하실 수 있습니다.

Learn Bean’s Lifecycle · KIMB0B/blog_spring@e837854 (github.com)

👉다음 글 보러가기