이 포스팅은 Java 17, Spring Boot 3.1.5, IntelliJ 기준으로 작성되었다.
스프링 애플리케이션이 기동된 직후 실행시키고 싶은 코드가 있다면 ApplicationRunner
나 CommandLineRunner
인터페이스를 구현하면 된다. 두 인터페이스 모두 동일한 방식으로 동작하고, 단일 메서드(run()
)를 제공하며 Runner 인터페이스를 상속한다는 공통점이 있다. 그럼 어떤 차이점이 있는 걸까?
interface Runner {
}
@FunctionalInterface
public interface ApplicationRunner extends Runner {
void run(ApplicationArguments args) throws Exception;
}
@FunctionalInterface
public interface CommandLineRunner extends Runner {
void run(String... args) throws Exception;
}
차이점은 메서드의 파라미터에 있다. ApplicationRunner
의 run()
메서드는 ApplicationArguments 타입의 객체를, CommandLineRunner
는 String 타입의 가변 인자를 받는다.
ApplicationArguments 인터페이스
애플리케이션의 main() 함수에서 인자로 받은 문자열 배열(
String[]
)과 파싱된 option, non-option 인자에 대한 액세스를 제공하는 인터페이스이다. 구현체로는DefaultApplicationArguments
가 있다.
CommandLineRunner
는 스프링 부트 버전 1.0.0에서 추가되었고,ApplicationRunner
는 버전 1.3.0에서 추가되었다.
ApplicationRunner
인터페이스 구현 예제는 아래와 같다. run()
메서드에 실행시키고 싶은 코드를 작성하면 된다.
@Component
public class ApplicationRunnerBean implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
System.out.println("mangoo"); // 여기!
}
}
여기서 중요한 점은 ApplicationRunner
나 CommandLineRunner
를 구현한 클래스를 빈으로 등록해야 한다는 것이다. 그 이유는 뒤에 나오는 동작 방식을 살펴보면 알 수 있다.
동일한 애플리케이션 컨텍스트 안에서 여러 ApplicationRunner
, CommandLineRunner
타입의 빈을 등록할 수 있다. 따라서, Ordered 인터페이스나 @Order 애노테이션을 통해 빈의 run()
메서드 실행 순서를 지정할 수 있다. 간단한 예제를 살펴보자.
@Configuration
public class OrderConfiguration {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public CommandLineRunner getRunnerBean1() {
return args -> System.out.println("Runner Bean #1"); // 1
}
@Bean
public CommandLineRunner getRunnerBean2() {
return args -> System.out.println("Runner Bean #2"); // 2
}
@Bean
@Order(1)
public ApplicationRunner getRunnerBean3() {
return args -> System.out.println("Runner Bean #3"); // 3
}
}
결과는 아래와 같이 1, 3, 2 순서대로 출력된다. @Order를 지정하지 않으면 LOWEST_PRECEDENCE로 설정되기 때문에 2번이 가장 마지막에 출력된다.
그렇다면 run()
메서드는 정확히 어느 시점에 실행되는 걸까?
ApplicationRunner
, CommandLineRunner
의 run()
메서드는 SpringApplicaiton.run()
이 완료되기 직전에 호출된다. 즉, 애플리케이션 기동이 완료되고 트래픽을 받아들이기 직전에 실행된다고 보면 된다.
@SpringBootApplication
public class BlogDemoApplication {
public static void main(String[] args) {
SpringApplication.run(BlogDemoApplication.class, args);
}
}
조금 더 정확한 파악을 위해 SpringApplication.run()
코드부터 하나씩 살펴보자.
이 블로그의 Spring MVC 시리즈물을 봤다면 아래 코드는 익숙할 것이다. 부가적인 설명은 생략하고, ApplicationRunner
와 CommandLineRunner
의 run()
메서드가 실행되는 부분은 callRunners()
메서드이다.
// SpringApplication.java
public ConfigurableApplicationContext run(String... args) {
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.started(context, timeTakenToStartup);
callRunners(context, applicationArguments); // 여기!
return context;
}
private void callRunners(ApplicationContext context, ApplicationArguments args) {
// (1) Runner 타입의 빈을 찾아 정렬한다
context.getBeanProvider(Runner.class).orderedStream()
// (2) run() 메서드를 실행한다
.forEach((runner) -> {
if (runner instanceof ApplicationRunner applicationRunner) {
callRunner(applicationRunner, args);
}
if (runner instanceof CommandLineRunner commandLineRunner) {
callRunner(commandLineRunner, args);
}
});
}
context.getBeanProvider(Runner.class).orderedStream()
Spring MVC 프레임워크를 사용하고 있기 때문에 context는 AnnotationConfigServletWebServerApplicationContext
객체이고, 해당 객체의 부모 타입인 AbstractApplicationContext
인터페이스와 GenericApplicationContext
클래스를 거친 뒤, 마지막으로 DefaultListableFactory
클래스를 거치게 된다.
// AbstractApplicationContext.java
public <T> ObjectProvider<T> getBeanProvider(Class<T> requiredType) {
assertBeanFactoryActive();
return getBeanFactory().getBeanProvider(requiredType);
}
// GenericApplicationContext.java
@Override
public final ConfigurableListableBeanFactory getBeanFactory() {
return this.beanFactory; // DefaultListableBeanFactory 객체 리턴
}
// DefaultListableBeanFactory.java
@Override
public <T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType, boolean allowEagerInit) {
return new BeanObjectProvider<>() {
@SuppressWarnings("unchecked")
@Override
public Stream<T> orderedStream() {
// (1-1) Runner 타입의 빈을 찾는다
String[] beanNames = getBeanNamesForTypedStream(requiredType, allowEagerInit);
if (beanNames.length == 0) {
return Stream.empty();
}
Map<String, T> matchingBeans = CollectionUtils.newLinkedHashMap(beanNames.length);
for (String beanName : beanNames) {
Object beanInstance = getBean(beanName);
if (!(beanInstance instanceof NullBean)) {
matchingBeans.put(beanName, (T) beanInstance);
}
}
Stream<T> stream = matchingBeans.values().stream();
// (1-2) 찾은 빈을 정렬한다.
return stream.sorted(adaptOrderComparator(matchingBeans));
}
...
};
}
AnnotationCofnigServletWebServerApplicationContext
에 대해 잘 모른다면 [Spring MVC #1] ApplicationContext 생성 포스팅을 읽어보면 좋다.
getBeanProvider()
메서드는 BeanObjectProvider 타입의 익명 객체를 생성해 리턴한다. 따라서, 뒤에 오는 orderedStream()
은 익명 객체에 정의된 함수가 실행됨을 알 수 있다.
orderedStream()
에서는 Runner 타입의 빈을 찾고 정렬하는 작업이 이루어진다. 앞서 ApplicationRunner
나 CommandLineRunner
를 구현한 클래스를 빈으로 등록해야 한다고 강조한 이유가 여기에 있다. 인터페이스를 구현하고 빈으로 등록하지 않으면 스프링은 찾지 못한다. 따라서, 아래의 코드에서는 아무것도 출력되지 않고, IntelliJ에서 'no usages'라는 메시지가 뜬다.
// @Component 삭제
public class ApplicationRunnerBean implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
System.out.println("mangoo"); // 실행 안됨
}
}
다시 코드로 돌아와서, (1-2)의 adaptOrderComparator()
메서드는 AnnotationAwareOrderComparator
객체를 리턴하는데, 이 클래스는 Ordered 인터페이스와 @Order, @Priority를 지원하는 OrderComparator 클래스를 상속한다.
public class AnnotationAwareOrderComparator extends OrderComparator {
public static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator();
@Override
@Nullable
protected Integer findOrder(Object obj) {
Integer order = super.findOrder(obj);
if (order != null) {
return order;
}
return findOrderFromAnnotation(obj);
}
public static void sort(List<?> list) {
if (list.size() > 1) {
list.sort(INSTANCE);
}
}
...
}
따라서, 위 comparator 객체가 sorted에 인자로 넘어가기 때문에 앞서 언급했던 Ordered 인터페이스나 @Order 애노테이션에 따라 run()
메서드가 실행되는 것이다.
이제 Runner 타입의 정렬된 빈을 하나씩 순회하며 run()
메서드를 실행한다.
.forEach((runner) -> {
if (runner instanceof ApplicationRunner applicationRunner) {
callRunner(applicationRunner, args);
}
if (runner instanceof CommandLineRunner commandLineRunner) {
callRunner(commandLineRunner, args);
}
});
앞에서 ApplicationRunner
와 CommandLineRunner
의 차이점은 run()
메서드의 파라미터에 있다고 언급했었다. 따라서, CommandLineRunner
타입의 인자를 받는 callRunner()
메서드를 보면 ApplicationArguments
에서 문자열 배열(String[]
)을 추출하는 작업이 동반된다.
private void callRunner(ApplicationRunner runner, ApplicationArguments args) {
try {
(runner).run(args);
}
catch (Exception ex) {
throw new IllegalStateException("Failed to execute ApplicationRunner", ex);
}
}
private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
try {
(runner).run(args.getSourceArgs()); // 여기!
}
catch (Exception ex) {
throw new IllegalStateException("Failed to execute CommandLineRunner", ex);
}
}
업무에서 ApplicationRunner
를 사용했었는데 당시에 코드를 뜯어볼 땐 스프링 버전 3.1.2이었고, 포스팅 작성 시점에는 예제 코드의 버전을 3.1.5로 가져가서 이전 버전의 코드와는 차이가 있었다. 눈에 띄는 차이점은 Runner 인터페이스가 생겼고, 아래와 같이 callRunners()
메서드의 로직은 동일하지만 구현이 변경되었다는 점이다.
// Spring Boot 3.1.2
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner applicationRunner) {
callRunner(applicationRunner, args);
}
if (runner instanceof CommandLineRunner commandLineRunner) {
callRunner(commandLineRunner, args);
}
}
}