Spring 애플리케이션 시작 시 실행되는 로직 작성하기

Spring 애플리케이션 시작 시 실행되는 로직 작성하기

본 글에서는 Spring 애플리케이션 시작 도중, 혹은 시작 직후에 특정 로직이 실행되도록 하기 위한 다양한 방법을 알아본다.

우선 용어부터 정확히 하겠다. 애플리케이션 시작 도중이라 함은 특정 빈이 초기화된 직후이며, 시작 직후라 함은 모든 빈이 초기화된 직후라고 정의하겠다.

또한, 빈이 초기화 되었다 함은 빈에 등록된 의존성들이 모두 주입되었다는 뜻으로 정의하겠다.

IoC 의 특성상 우리는 애플리케이션 실행 흐름의 제어권을 어느정도 포기해야하기 때문에,

Spring 에서 초기 셋업 로직을 작성하는 것은 아무래도 조금 귀찮기 마련이다.

단순히 Bean 의 생성자에 초기화 로직을 넣기만 해서는 안될 수도 있다.

다음의 예를 보자.

@Component
public class InvalidInitExampleBean {

@Autowired
private Environment env;

public InvalidInitExampleBean() {
env.getActiveProfiles();
}
}

InvalidInitExampleBean 객체가 생성되는 시점에는 아직 env 가 초기화되지 않은 상태이므로 NullPointerException이 발생하게 된다.

그럼 어떤 방법으로 초기 셋업 로직을 작성해야 할까?

초기 셋업 로직을 정의하는 방법

본 글에서는 초기 셋업 로직을 작성하는 8가지 방법에 대해 알아본다.

@PostConstruct 어노테이션

@PostConstruct 어노테이션은 특정 클래스의 메소드에 붙여서 해당 클래스의 객체 내 모든 의존성(Bean) 들이 초기화 된 직후딱 한 번만 실행되도록 해준다.

만약 객체에 의존성이 하나도 없더라도 실행된다.

다음의 코드를 보자.

@Component
public class PostConstructExampleBean {

private static final Logger LOG
= Logger.getLogger(PostConstructExampleBean.class);

@Autowired
private Environment environment;

@PostConstruct
public void init() {
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}

앞서 NullPointerException 이 발생했던 코드와는 달리 init() 메소드는 클래스 내 의존성인 environment 가 초기화된 직후에 호출되기 때문에 정상적으로 동작하게된다.

InitializingBean 인터페이스

이 방식은 어노테이션을 붙이는 대신 InitializingBean 인터페이스와 afterPropertiesSet() 메소드를 구현한다는 것말곤
앞서 설명한 @PostConstruct 방식과 유사하게 동작한다.

@Component
public class InitializingBeanExampleBean implements InitializingBean {

private static final Logger LOG
= Logger.getLogger(InitializingBeanExampleBean.class);

@Autowired
private Environment environment;

@Override
public void afterPropertiesSet() throws Exception {
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}

ApplicationListener

이 방식은 앞서 설명한 방식처럼 특정 Bean 과 관련된 것이 아니라 Spring 컨텍스트의 초기화가 완료된 후, 즉 모든 Bean 의 초기화가 완료된 후에 실행되도록 하는 방식이다.
ApplicationListener 인터페이스를 구현하는 Bean 을 정의하고 onApplicationEvent() 메소드를 Override 하여, 그 안에 원하는 로직을 작성하면 된다.

@Component
public class StartupApplicationListenerExample implements
ApplicationListener<ContextRefreshedEvent> {

private static final Logger LOG
= Logger.getLogger(StartupApplicationListenerExample.class);

public static int counter;

@Override public void onApplicationEvent(ContextRefreshedEvent event) {
LOG.info("Increment counter");
counter++;
}
}

필요에 따라 ContextRefreshedEvent 대신 상황에 맞게 다른 이벤트를 넣어줄 수도 있다.

@EventListener 어노테이션

앞서 설명한 ApplicationListener 처럼 Bean 을 하나 정의하고, 인터페이스를 구현하는 것이 아니라 특정 메소드에 @EventListener 어노테이션을 붙여 그 안에 원하는 로직을 작성한다.

@Component
public class EventListenerExampleBean {

private static final Logger LOG
= Logger.getLogger(EventListenerExampleBean.class);

public static int counter;

@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
LOG.info("Increment counter");
counter++;
}
}

@Bean 의 initMethod 속성

@Bean 어노테이션의 initMethod 속성으로 이 Bean 의 초기화가 완료(의존성이 모두 주입)된 뒤에 실행 되어야할 Bean 내 메소드의 이름을 지정할 수가 있다.
예를 들어 다음과 같은 Bean 이 있다고 가정하자.

public class InitMethodExampleBean {

private static final Logger LOG = Logger.getLogger(InitMethodExampleBean.class);

@Autowired
private Environment environment;

public void init() {
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}

이 때, @Bean 메소드에 initMethod 속성으로 InitMethodExampleBean Bean 내의 메소드인 init() 의 이름을 설정해준다.

@Bean(initMethod="init")
public InitMethodExampleBean exBean() {
return new InitMethodExampleBean();
}

XML 로도 가능하다.

<bean id="initMethodExampleBean"
class="com.baeldung.startup.InitMethodExampleBean"
init-method="init">
</bean>

생성자 Injection

사실 생성자를 통해 의존성을 주입하는 경우에는 그냥 생성자에 원하는 로직을 넣으면 된다.
Field injection 의 경우 Bean 객체를 생성한 뒤에 나중에 의존성을 주입하기 때문에, 주입하기 이전에는 null 인 상태지만
Constructor Injection 의 경우에는 생성과 동시에 주입을 하기 때문에 문제가 없는것이다.
이것이 Field Injection 보다 Constructor Injection 이 더 권장되는 여러가지 이유 중 하나다.
생성자 주입이 더 권장되는 여러가지 이유에 대해서는 추후 다른 포스팅에서 다룰 것이다.

@Component 
public class LogicInConstructorExampleBean {

private static final Logger LOG
= Logger.getLogger(LogicInConstructorExampleBean.class);

private final Environment environment;

@Autowired
public LogicInConstructorExampleBean(Environment environment) {
this.environment = environment;
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}

Spring Boot 의 CommandLineRunner

스프링 부트는 run() 이라는 콜백 메소드를 가진 CommandLineRunner 라는 인터페이스를 제공한다.
run() 메소드는 Spring application context 의 초기화가 완료된(모든 Bean 이 초기화된) 후에 실행되므로
이 안에 원하는 로직을 작성하면 된다.

@Component
public class CommandLineAppStartupRunner implements CommandLineRunner {
private static final Logger LOG =
LoggerFactory.getLogger(CommandLineAppStartupRunner.class);

public static int counter;

@Override
public void run(String...args) throws Exception {
LOG.info("Increment counter");
counter++;
}
}

참고로 CommandLineRunner Bean 은 같은 애플리케이션 컨텍스트 내에 여러개를 정의할 수 있으며,
Ordered 인터페이스, 혹은 @Order 어노테이션으로 실행 순서를 정해줄 수도 있다.

@Component 가 아니라 다음과 같이 @Configuration 과 @Bean 을 사용한 방식으로도 정의할 수 있다.

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
public CommandLineRunner run(UserRepository userRepository) throws Exception {
return (String[] args) -> {
User user1 = new User("John", "john@domain.com");
User user2 = new User("Julie", "julie@domain.com");
userRepository.save(user1);
userRepository.save(user2);
userRepository.findAll().forEach(user -> System.out.println(user));
};
}
}

자바에서 하나의 메소드를 가지는 인터페이스의 경우에는 기존의 익명 클래스 방식대신 위와같이 람다식으로 작성할 수 있다.

Spring Boot 의 ApplicationRunner

스프링 부트는 앞서 언급한 CommandLineRunner 인터페이스 외에 ApplicationRunner 인터페이스도 제공한다.
동일하게 run() 이라는 콜백 메소드를 가지고 있어 이 안에 원하는 로직을 작성하면 된다.
참고로 run() 메소드로 들어오는 문자열들은 커맨드 라인으로 앱을 실행할 때 들어온 명령행 인자들이다.

@Component
public class AppStartupRunner implements ApplicationRunner {
private static final Logger LOG =
LoggerFactory.getLogger(AppStartupRunner.class);

public static int counter;

@Override
public void run(ApplicationArguments args) throws Exception {
LOG.info("Application started with option names : {}",
args.getOptionNames());
LOG.info("Increment counter");
counter++;
}
}

CommandLineRunner 와 차이가 있다면, run() 메소드의 인자가 String 가 아니라 ApplicationArguments 인데
ApplicationArguments 인터페이스는 보통의 커맨드라인 인자 뿐만 아니라,
옵션 읽어 들일 수 있는 getOptionNames(), getOptionValues() 등의 메소드도 가지고 있다.

결론

앞서 설명한 다양한 방식을 한꺼번에 적용해보면 다음과같은 모양이다.

@Component
@Scope(value = "prototype")
public class AllStrategiesExampleBean implements InitializingBean {

private static final Logger LOG
= Logger.getLogger(AllStrategiesExampleBean.class);

public AllStrategiesExampleBean() {
LOG.info("Constructor");
}

@Override
public void afterPropertiesSet() throws Exception {
LOG.info("InitializingBean");
}

@PostConstruct
public void postConstruct() {
LOG.info("PostConstruct");
}

public void init() {
LOG.info("init-method");
}
}

해당 빈이 초기화된 후에 다음과 같은 로그가 찍히게 된다.

[main] INFO o.b.startup.AllStrategiesExampleBean - Constructor
[main] INFO o.b.startup.AllStrategiesExampleBean - PostConstruct
[main] INFO o.b.startup.AllStrategiesExampleBean - InitializingBean
[main] INFO o.b.startup.AllStrategiesExampleBean - init-method

참고: https://www.baeldung.com/running-setup-logic-on-startup-in-spring#4-the-bean-initmethod-attribute

Comments