개발 기록

> [Spring] Spring 애플리케이션 시작 시 실행되는 로직 작성하기(@PostConstruct, ApplicationListener, CommandLineRunner 등) 본문

Spring

> [Spring] Spring 애플리케이션 시작 시 실행되는 로직 작성하기(@PostConstruct, ApplicationListener, CommandLineRunner 등)

1z 2020. 7. 10. 17:45

 

0. 개요  

스프링부트 애플리케이션이 시작 도중, 혹은 시작 직후 더미 데이터를 메모리에 올리거나, 모니터링 애플리케이션에게 메세지를 보내는 작업  것과 같이 특정 기능을 실행시켜야 하는 경우가 있다. 

 

하지만 이는 여러 가지 문제를 야기하기도 하는데, IoC  컨테이너가 객체의 life-cycle 을 관리하고 있기 때문이다. 예를 들어 bean 의 생성자에 초기화 로직을 넣을 때 객체의 life-cycle 에 부합하지 않다면 에러가 발생한다. 다음의 예를 보자

@Component
public class InvalidInitExampleBean {

    @Autowired
    private Environment env;

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

 

InvalidInitExampleBean  생성자가 호출될 때,  autowired 로 주입받은 필드에 접근할려고 하면 NullPointerException 이  발생한다. 생성자가 호출되는 시점에서 Spring 빈(여기서는 env)은 아직 완전히 초기화되지 상태이기 때문이다. 이러한 상황을 관리하기 위해 Spring 이 제공하는 몇 가지 방법이 있다. 

 

▶Bean 설정 방법

ⓛ @PostConstruct

② InitializingBean 인터페이스 afterPropertiesSet() 메소드 구현 

③  @Bean 의 initMethod 속성

④ 생성자 주입

 

▶Spring Application context 시작시 호출 방법 

 ApplicationListener

@EventListener 어노테이션

⑦  CommandLineRunner

⑧ ApplicationRunner

 


1. @PostConstruct  

해당 bean 의 모든 의존성 주입이 이루어진 후 수행하는 메서드 어노테이션이다.

@PreDestroy 로 주석이 달린 메소드는애플리케이션 컨텍스트에서 빈의 초기화 직후 한 번만 호출된다.

@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()));
    }
}

 

이와 유사하게 동작하는 것이 InitializingBean 인터페이스의 afterPropertiesSet() 와 @Bean 의 initMethod 속성 이다.

afterPropertiesSet() 메서드를 구현하거나, @Bean 어노테이션의 initMethod 속성으로 메소드의 이름을 지정하면 된다.


2. Constructor Injection (생성자 주입) 

생성자 주입을 사용하여 fields 를 주입하는 경우에는 생성자에 원하는 로직을 넣으면 간단히 해결 된다.

Field injection 의 경우 Bean 객체를 생성한 뒤에 의존성을 주입하기 때문에 주입하기 이전에는 해당 fileds 는 null 인 상태지만 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()));
    }
}

 

 


3. CommandLineRunner  

Spring Boot는 CommandLineRunner 인터페이스를 제공하는데,  이 인터페이스 가진  run() 메서드는 Spring 애플리케이션 컨텍스트가 인스턴스화된 후 애플리케이션 시작 시  호출된다.

CommandLineRunner 인터페이스를 구현한 클래스를 스프링 빈으로 등록하게되면, 스프링이 초기화 작업을 마친 후(모든 Bean 이 초기화된)  해당 클래스의 run(String... args) 메서드를 실행시켜주는 방법으로 이 안에 원하는 로직을 작성하면 된다.

 

참고

:여러 CommandLineRunner 빈은 동일한 애플리케이션 컨텍스트 내에서 정의될 수 있으며 @Ordered 인터페이스 또는 @Order 주석을 사용하여 실행 순서를 정할 수 있다.  

@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++;
    }
}

 

 

@Component 스캔에 의해서 빈을 등록하는 방법 말고도 @ Configuration & @Bean annotation을 사용하거나 익명클래스를 사용하는 방법이 있다. 아래 예제는 익명클래스를 사용한 예시이다. 

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

   // CommandLineRunner 구현체를 생성하고 빈으로 등록
    @Bean
    public CommandLineRunner run() {
        return new CommandLineRunner() {
            @Override
            public void run(String... args) throws Exception {
                System.out.println("Hello Anonymous CommandLineRunner!!!");
            }
        };
    }
    
    // CommandLineRunner가 @FunctionalInterface(추상 메서드가 하나인 인터페이스)이기 때문에 
    // 람다(lambda)로 변경할 수 있다.
    @Bean
    public CommandLineRunner run() {
        return args -> System.out.println("Hello Lambda CommandLineRunner!!");
    }
}

 

CommandLineRunner 구현체가 다른 빈(Bean)을 주입받는 경우 아래 처럼 메서드의 파라미터로 주입받으면 된다.

@Bean
public CommandLineRunner run(UserService userService) {
     return (String[] args) -> {
            User user = new User("kim", "kim@email.com");
            userRepository.add(user);
        };
}

 


 

4 . ApplicationRunner  

Spring Boot는  CommandLineRunner와 유사한 방식으로  ApplicationRunner 인터페이스도 제공한다. CommandLineRunner  다른 점은 인자값이 String 가 아니라 ApplicationArguments instance 라는 것이다. ApplicationArguments 인터페이스에는getOptionNames(), getOptionValues() 등 인수 옵션 관련 메서드가 있어서 앱 실행시 사용된 인수값을 가져오거나, 인수명을 가져올 수 있다.  ( An argument that is prefixed with – – is an option argument.  => ex. --foo=bar)

 

ApplicationRunner의 사용법은 앞서 배운 CommandLineRunner의 사용법과 다르지 않다. 동일하게 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++;
    }
}

 

 


5 . ApplicationListner 

특정 Bean 과 관련된 것이 아니라 Spring 컨텍스트의 초기화가 완료된 후, 즉 모든 Bean 의 초기화가 완료된 후에 실행되도록 하는 방식이다.

ApplicationListener<ContextRefreshedEvent> 인터페이스를 구현하는 Bean을 정의하고 onApplicationEvent() 메소드를 Override 하여, 그 안에 원하는 로직을 작성하면 된다. 필요에 따라 ContextRefreshedEvent 대신 상황에 맞게 다른 이벤트를 넣어줄 수도 있다.

@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++;
    }
}

 

새로 도입된 @EventListener 주석을 사용하여 동일한 결과를 얻을 수 있다.

5 . @EventListener 

특정 메소드에 @EventListener 어노테이션을 붙여 그 안에 원하는 로직을 작성한다.  아래 예제에서는 ContextRefreshedEvent 가 발생하면 @EventListener annotation 이 붙은 메서드가 실행된다.  

@Component
public class EventListenerExampleBean {
 
    private static final Logger LOG 
      = Logger.getLogger(EventListenerExampleBean.class);
 
    public static int counter;
 
 // ContextRefreshedEvent: ApplicationEvent 상속 Class
    @EventListener
    public void onApplicationEvent(ContextRefreshedEvent event) {
        LOG.info("Increment counter");
        counter++;
    }
}

 

여러 개의 이벤트에 동일한 메서드가 실행되야 할 때는 아래처럼 인자를 받으면된다.

@EvnetListener({ApplicationFailedEvent.class, ApplicationReadyEvent.class})

 

 

★ Application 시작 시 호출되는 순서

 

 ApplicationContext 초기화 전 발생

:  @EventListener 를 통한 이벤트 등록으로 처리할 수 없다. 왜냐하면  ApplicationContext 시작 전이기 때문에 리스너 메서드를 가진 클래스가 스프링 빈으로 초기화되기 전에 실제 이벤트가 먼저 발생한다. 그래서 아래 이벤트를 작동하기 위햇는 어플리케이션 시작 전에 리스너를 등록해야한다.

1. ApplicationStartingEvent

2. ApplicationEnvironmentPreparedEvent

3. ApplicationContextInitializedEvent

 

☞  ApplicationContext 초기화 후 발생

4. ApplicationPreparedEvent

5. ContextRefreshedEvent

6. ApplicationStartedEvent

7. ApplicationReadyEvent

 

 

 

 

 

참고

https://sgc109.github.io/2020/07/09/spring-running-startup-logic/

https://www.baeldung.com/running-setup-logic-on-startup-in-spring

https://jeong-pro.tistory.com/206