[스프링 핵심원리 기본편] 빈 생명주기 콜백

흑수·2022년 2월 5일
0

김영한씨의 스프링 핵심 원리 - 기본편 강의를 듣고 공부 겸 정리하는 글입니다.

빈 생명주기 콜백 시작

애플리케이션 시작 시점에 필요한 작업이 있고, 종료 시점에 필요한 작업들이 존재합니다. 가령, 데이터 베이스 커넥션 풀, 네트워크 소켓 연결 등은 시작 시점에 필요하겠죠?

이를 가능하게 하려면 객체 초기화 작업과 종료 작업이 필요합니다.

이번 예시는, 네트워크 연결 상황을 구현해보도록 할건데 실제 연결이 아닌 문자열 출력으로 대신하겠습니다.

시작 시점에 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(); //스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
    }
    
    @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이 존재하지 않고 수정자 주입을 통해 값을 넣어주고 있으니 그 전에는 모두 null로 나옵니다.

스프링 빈은 다음과 같은 라이프 사이클을 가집니다.

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

객체를 생성(생성자를 통해)하고 의존 관계를 주입하고나서야 비로소 데이터를 이용할 준비가 완료됩니다. 따라서 초기화 작업은 의존관계 주입이 완료되고 일어나야 하는데 개발자가 이 시점을 어떻게 알 수 있을까요?

스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공합니다.
( +++ 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 줍니다. )

요약을 해보자면,

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

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

객체의 생성과 초기화 작업을 분리하자!

생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체의 생성을 담당하고 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등의 무거운 작업을 담당합니다.

따라서 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋습니다.

인터페이스 InitializingBean, DisposableBean

위의 코드에서 InitializingBean, DisposableBean을 상속해줍니다.

public class NetworkClient implements InitializingBean, DisposableBean { 
    @Override
	public void afterPropertiesSet() throws Exception {
		connect();
		call("초기화 연결 메시지");
	}
    
    @Override
    public void destroy() throws Exception {
        disConnect();
    }
}

InitializingBean은 afterPropertiesSet을 지원하고, DisposableBean은 destory를 지원합니다. 그렇기에 이 둘을 내가 쓰려는 용도에 맞게 오버라이딩을 해줍니다.

  • afterPropertiesSet: 초기화 콜백 메서드
  • destory: 소멸 직전 콜백 메서드

테스트를 다시 해보면 어떤 결과가 나타날까요?

생성자 호출, url = null
NetworkClient.afterPropertiesSet
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
13:24:49.043 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing NetworkClient.destroy
close + http://hello-spring.dev

저희가 생각했던 결과가 잘 나오게 되네요,,

다만, 이 인터페이스는 스프링 전용이기에 스프링 인터페이스에 의존하게 됩니다. 또한, 이 콜백 메서드들의 이름을 변경할 수 없고 코드를 고칠 수 없는 외부 라이브러리에는 적용할 수 없다는 단점이 존재합니다.

(거의 사용되지 않습니다.)

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

설정 정보에 initMethod와 destoryMethod를 지정할 수 있습니다.

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

public void init() { 
    connect();
	call("초기화 연결 메시지");
}
public void close() {
	disConnect();
}

결과는 동일하게 나옵니다.
또한 위의 단점을 모두 커버칠 수 있답니다.

위와 다르게 메서드 이름을 자유롭게 줄 수 있다는 특징이 있고
(initMethod = "init", destroyMethod = "close")
스프링에 의존적이지 않으며 외부 라이브러리에도 적용 가능합니다.

종료 메서드는 기본값이 'inferred' 로 되어 있어서 자동으로 추론해 찾아내기 때문에 설정하지 않아도 상관 없습니다.

애노테이션 @PostConstruct, @PreDestroy

@PostConstruct
public void init() {
	connect();
	call("초기화 연결 메시지");
}
@PreDestroy
public void close() {
    disConnect();
}

결과 역시 동일하고 위의 두 어노테이션만 달아주면 되기 때문에 굉장히 간단합니다.

최신 스프링에서 가장 권장하는 방법이고, 매우 편리합니다.
패키지를 살펴보면 자바 표준이기 때문에 스프링이 아닌 다른 컨테이너에서도 작동합니다.
유일한 단점은 외부 라이브러리에 이용 불가능하다는 점..!!

결론

@PostConstruct, @PreDestroy 애노테이션을 사용하자!
@PostConstruct, @PreDestroy 애노테이션을 사용하자!
@PostConstruct, @PreDestroy 애노테이션을 사용하자!

profile
기록용

0개의 댓글