본문 바로가기

Java/Spring

스프링 애플리케이션이 시작, 종료될 때 수행할 메서드 지정하는 방법 + 스프링 빈(Bean)이 생성, 소멸될 때 수행할 메서드 지정하는 방법(graceful 종료, CommandLineRunner, ApplicationListener, InitializingBea..

스프링 애플리케이션 시작과 끝

스프링(스프링부트)으로 애플리케이션을 개발했을 때, 애플리케이션이 시작할 때 어떤 동작을 하고 싶은 경우가 있고, 반대로 애플리케이션이 종료되기 직전에 어떤 동작을 하고 싶은 경우가 있다.

예를들면 어떤 Configuration에서 값을 가져와서 초기 값을 설정한다든지, 애플리케이션이 종료되기 전에 하던 작업과 객체들을 우아하게(graceful) 종료한다든지 하는 작업들이다.

더 쉽게 표현하면 애플리케이션 입장에서의 생성자와 소멸자가 필요할 때가 있다는 것이다.

이 문제를 스프링에서는 인터페이스로 아주 쉽게 제공해준다.

뿐만 아니라 Spring bean이 생성될 때와 삭제될 때 수행하는 메서드를 구현할 수 있게 인터페이스로 제공한다.

아래에 코드를 보자!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.demo.service;
 
import javax.annotation.PostConstruct;
 
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Service;
 
@Service
public class TestService implements CommandLineRunner, ApplicationListener<ContextClosedEvent>, InitializingBean, DisposableBean {
    @PostConstruct
    private void init() {
        System.err.println("PostConstruct annotation으로 빈이 완전히 생성된 후에 한 번 수행될 메서드에 붙입니다.");
    }
    @Override
    public void run(String... args) throws Exception {
        System.err.println("commandLineRunner 인터페이스 구현 메서드입니다. '애플리케이션'이 실행될 때 '한 번' 실행됩니다.");
    }
    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        System.err.println("ApplicationListener<ContextClosedEvent> 인터페이스 구현 메서드 입니다. '애플리케이션'이 죽었을 때 '한 번' 실행됩니다.");
        System.err.println("이벤트 발생 시간(timestamp) : " + event.getTimestamp());
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        System.err.println("InitializingBean 인터페이스 구현 메서드입니다. TestService 'Bean'이 생성될 때 마다 호출되는 메서드 입니다.");
    }
    @Override
    public void destroy() throws Exception {
        System.err.println("DisposableBean 인터페이스 구현 메서드입니다. TestService 'Bean'이 소멸될 때 마다 호출되는 메서드입니다");
    }
}
cs

이것이 전부다.

임의의 서비스(@Service)가 있다고 할 때, 해당 인터페이스들을 구현하면 위에서 말했던 시점(application 시작/종료, bean 생성/삭제)에 원하는 작업을 할 수 있다.

- CommandLineRunner

CommandLineRunner는 main 메서드와 거의 같다고 이해하면 된다. (run 메서드)

"스프링 애플리케이션"이 시작할 때 "1회"만 호출되는 점이 똑같고 다른 점은 static이 아니라는 점에서 이득을 가져갈 수 있는 부분이 있다. argument도 main에 들어오는 것과 똑같다!

애플리케이션이 시작되었을 때 하고 싶은 초기 작업이 있다면 이 인터페이스를 구현하도록 하자.


- ApplicationListener

ApplicationListener는 이벤트와 관련이 있다. (onApplicationEvent 메서드)

ApplicationEvent(추상클래스)를 상속받은 모든 이벤트들을 넣을 수 있다.

따라서 내가 ApplicationEvent를 상속받은 클래스를 만들어서 내가 만든 이벤트가 발생했을 때 호출될 메서드를 만들 수도 있는 것이다.

위의 예제에서는 ContextClosedEvent를 감지하는 인터페이스를 구현했기 때문에, 애플리케이션이 종료되기 직전에 1회만 호출된다.

실무에서 graceful shutdown이 필요할 때 이 메서드를 구현하도록 하자.

applicationListener에 ApplicationReadyEvent를 감지하도록 하면 애플리케이션이 온전히 실행되고 나서 1회만 호출하도록 할 수 있다.


- InitializingBean

InitializingBean은 스프링 빈(Bean)이 생성될 때 호출된다. (afterPropertiesSet 메서드)

위의 예제에서는 @Service로 등록된 Bean인 TestService가 생성될 때 호출될 것이다.

참고로 Bean이 생성됐다가 삭제되는 것이 반복적으로 일어나면 얼마든지 반복적으로 호출될 수 있다.


- DisposableBean

DisposableBean은 스프링 빈(Bean)이 삭제될 때 호출된다. (destroy 메서드)

마찬가지로 TestService가 ApplicationContext에서 제거될 때 호출된다.

이 역시도 Bean이 제거될 때마다 메서드가 호출될 수 있다.


번외로 지난 포스트에서 알아본 @PostConstruct도 Bean이 생성될 때 1회 호출하는 메서드를 지정할 수 있다.


이제 로그를 보도록하자!

@PostConstruct annotation으로 등록한 메서드가 제일 먼저 호출됐다.

그 다음에 InitializingBean으로 등록한 메서드가 호출됐다.

이것으로 @PostConstruct 애노테이션이 Bean이 생성되고 제일 먼저 호출되고 그 다음에 InitializingBean의 구현 메서드가 호출되는 것을 알 수 있다.

그렇게 애플리케이션에 필요한 모든 Bean들이 생성되고 스프링 애플리케이션이 온전히 실행되면,

commandLineRunner의 구현 메서드가 호출된다.

이렇게 애플리케이션이 실행중에 있다가 이클립스의 중지 버튼으로 종료한다면, (= 종료 이벤트가 발생한다면)

다시 ApplicationListener<ContextClosedEvent>의 구현 메서드가 호출된다.

그 후에 DisposableBean의 구현 메서드가 호출된다.

- 정리

 요약하면 스프링이 Bean들을 다 생성하고 완전히 준비됐을 때 commandLineListener가 호출되고, 종료할 때는 반대로 ContextClosedEvent가 호출되고 생성되었던 Bean들을 제거하고 종료되는 것이다.


주의할 것. SIGTERM 과 SIGKILL

위의 메서드들중에서 종료와 관련된 작업들은 주의할 것이 있다.

주의해야할 것은 "정상(?) 종료" 되었을 때에 호출된다는 것이다.

무슨 말이냐면 애플리케이션이 종료될 때 일반적인 인터럽트는 SIGTERM 이라는 인터럽트다.

이 인터럽트(SIGTERM)가 발생하면 이벤트로 감지하고 수행하는 작업이라는 것이다. 

SIGTERM을 정상적인 종료라고 봤을 때, 비정상 종료는 SIGKILL 이다.

리눅스에서 kill -9 옵션과 같이 강제적으로 꺼버리는 것과 윈도우에서 작업관리자가 작업을 끝내버리는 등의 인터럽트가 SIGKILL이다.

위의 예제를 따라했는데 종료 이벤트에 대한 메서드가 호출되지 않았다면 SIGKILL을 이용해서 종료했을 가능성이 높다.

혹시나하고 윈도우 환경에서 커맨드창에 ctrl + c 로 종료해보았는데 이 단축키는 SIGTERM을 발생하는 이벤트라서 온전히 종료되는 것을 아래 그림에서 볼 수 있다.

또 다른 실험의 결과 작업끝내기로 프로세스를 죽여버렸을 때는 해당 메서드들을 호출되지 않았다.

* 빨간선 위로는 온전히 수행했을 때고, 빨간선일 때 ctrl+c를 눌르면 보이는 것과 같이 메서드를 호출하고 빠져나온 것을 볼 수 있다.

* 또 다른 실험으로 얻은 결과, 온전히 자바 애플리케이션이 실행되기 전에 ctrl+c를 누르면 정상적으로 종료되지 않는다.


우리 모두 스프링 애플리케이션을 우아하게 종료합시다.

참고 사이트

https://zepinos.tistory.com/41?category=797553