티스토리 뷰

대부분 Spring 을 이용한 프로그래밍을 할 때에는 MVC, 요즘에는 WebFlux 을 많이 이용할 것이기 때문에 Spring Boot Starter 에서 제공하는 기능만으로도 시작 시 추가적인 동작이 필요하지 않는 경우가 대부분입니다. 하지만, 특정한 동작을 위해 서버 시작 시 어떠한 명령을 실행해야 한다던가, 반대로 종료 시 어떤 동작을 해야하는 경우는 분명 생길 수 있습니다.


가장 쉽게 생각해볼 수 있는 것은 생성자와 소멸자입니다. 그리고 jar 파일을 직접 실행할 때에는 main() method 내에 추가로 구현할 수도 있습니다. 하지만, Spring Boot 에서는 몇 가지 다른 방식으로 처리할 수 있습니다.


먼저 CommandLineRunner 나 ApplicationRunner 인터페이스를 이용해서 애플리케이션 시작 시점에 무엇인가를 실행하는 방법입니다. Spring Boot 가 시작할 때 CommandLineRunner 나 ApplicationRunner 에서 반드시 구현되어야 하는 run() method 을 작성함으로써 시작 시 특정한 기능을 실행할 수 있습니다.

이 두 Runner 을 이용하면 static 으로 되어 있는 main() 와 달리 일반 method 이기 때문에 static 으로 된 외부 값을 이용하지 않아도 된다는 점과, main() 이 있는 Class 가 아닌 @Component 로 등록되는 어떤 Class 에도 등록할 수 있기 때문에 필요에 따라 구현할 위치를 정할 수 있다는 것입니다.


@Component
public class StartConfig implements CommandLineRunner {

	@Override
	public void run(String... args) {
		// 구현 부분
	}

}
@Component
public class StartConfig implements ApplicationRunner {

	@Override
	public void run(ApplicationArguments args) throws Exception {
		// 구현 부분
	}

}


위와 같이 간단하게 구현할 수 있습니다. 다만, 두 Runner 는 run() method 의 매개변수로 받을 수 있는 변수가 다른데, 이는 main() 이 받는 명령줄 매개변수를 그대로 전달받게 됩니다. 그래서 main() 와 형식이 유사한 CommandLineRunner 가 더 자주 쓰일 것으로 예상됩니다(저 역시 CommandLineRunner 만 쓰고 있습니다). 반대로 ApplicationRunner 은 옵션을 key 와 value 로 나누어 Collection 객체로 전달받을 수 있기 때문에 처리가 좀 더 간결해질 수 있는 부분도 존재합니다.


반대로 애플리케이션이 종료할 때에는 ApplicationListener 인터페이스를 이용해 Context 가 종료되는 event 을 전달받을 수 있습니다. ApplicationListener 은 ApplicationListener<E extends ApplicationEvent> 형태로 되어 있기 때문에 ApplicationEvent 을 상속받아 구현된 몇 가지 이벤트를 받을 수 있고(그렇게에 사실 위의 Runner 대신 이걸 이용해도 됨) Context 가 종료되는 시점의 이벤트도 받을 수 있습니다.


@Component
public class ShutdownEventListener implements ApplicationListener {

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        // 구현 부분
    }

}


당연히 이를 이용해서 상태에 따른 이벤트를 구현할 수 있습니다. 종료 시 Gracefully 하게(우아하게) 종료하기 위한 구현 부분을 넣을 수 있습니다.


또다른 방법으로, Bean 의 생성과 소멸 시점에 호출되는 method 을 이용하는 방법도 존재합니다. 바로 InitializingBean, DisposableBean 인터페이스입니다. 아래의 예제만 보더라도 객체의 생성과 소멸 시점에 동작하는 method 을 구현하는 것을 알 수 있습니다. 대신에, 객체의 생성과 소멸이 계속 반복되면 여러번 실행하기 때문에 개인적으로는 @Configuraion 을 구현하는 곳과 같이 한 번 설정하고 계속 사용되는 곳에 implements 하는 것을 권합니다.


@Configuration
public class StartStopConfig implementsInitializingBean, DisposableBean {

	@Override
	public void afterPropertiesSet() throws Exception {
		// Bean 생성 시 동작할 코드 구현 부분
	}

    @Override
	public void destroy() throws Exception {
		// Bean 소멸 시 동작할 코드 구현 부분
	}

}


이러한 기능을 이용할 때 주의할 것이 있습니다. 실행 시 시작되는 것에는 크게 실수할 부분은 없으나, 애플리케이션 종료 시 실행되어야 하는 코드는 일반적으로 SIGTERM 입니다. 일반적으로 kill 명령을 실행하면 발생하는 signal 이고, 이 때 ContextClosedEvent 을 감지하거나 DisposableBean 의 destory() 을 실행하는데 문제가 없습니다. 하지만, kill -9 와 같이 SIGKILL 을 발생시키면 애플리케이션이 signal 을 감지하지 못하기 때문에 종료 시 필요한 처리를 하지 못합니다.

문제는, IDE 에서 재시작 같은 동작을 할 때 SIGTERM 이 아닌 SIGKILL 로 종료할 수도 있다는 것입니다.

아래 화면은 IntelliJ 인데, 수정 후 재시작을 하려고 할 때 많이 사용하는 것은 아래 표시된 Rerun 버튼일 것입니다. 그런데, 이 버튼을 누르면 SIGKILL 로 애플리케이션을 종료시킵니다.



SIGTERM 으로 종료하고 싶다면, 아래의 Exit 버튼을 눌러서 애플리케이션을 종료시키고, 종료가 완료된 뒤 시작을 해야 합니다.



실제로 Spring Batch 등에서 Rerun 을 실행할 경우 Job 의 상태 변화가 완벽히 이루어지지 않아서 문제가 발생하기도 합니다. 그렇기 때문에 이 부분을 항상 숙지하고 있어야 합니다.

댓글
  • 프로필사진 선배님~ 오오...감사합니다 안그래도 요즘 스프링부트 프로젝트 중이였는데 참고하겠습니다! 2019.01.30 11:19
  • 프로필사진 Favicon of https://zepinos.tistory.com zepinos 특정한 상황에서 문제가 생기는 부분이라, 생각보다 널리 알려져있지 않은 부분이더군요. 옵션에서 선택할 수 있게 해주면 좋은데 그렇질 않아서 개인적으론 좀 불편했던 부분입니다. 2019.02.27 11:10 신고
댓글쓰기 폼