[Spring] Spring 메서드 초기화
초기화 method
spring에서 application이 구동될 때 해당 method가 같이 실행되도록 하는 방식
즉, spring에서 초기화 method를 구현하는 방법은 6가지가 있고 아래와 같다.
인스턴스의 라이프 사이클를 사용한 방법
- @PostConstruct
- InitializingBean
- initMethod
Runner와 EventListener 를 사용한 방법
- ApplicationRunner
- CommandLineRunner
- @EventListener
초기화란 의존성 주입이 완료된 후에 실행되어야 한다. 즉, 종속된 빈 생성이 완료된 후, 실행되는 초기화 메소드 이다. 만약 의존성 주입이 완료되지 않은 bean 을 초기화 한다면? null 값을 초기화 하는 것과 같다.
실행 우선순위
- 스프링 빈 이벤트 생명주기
스프링 컨테이너 생성 => 스프링 빈 생성 => 의존관계 주입 => 초기화 콜백 => 사용 => 소멸 콜백 => 스프링 종료
각각의 우선순위는 아래와 같다.
@PostConstruct → InitializingBean → Bean initMethod → ApplicationRunner → CommandLineRunner → @EventListener ApplicationReadyEvent
2022-10-05 00:24:49.263 DEBUG 14728 --- [ Test worker] site.yejin.sbb.base.TestInitDataConfig : [init method] postConstruct
2022-10-05 00:24:49.432 DEBUG 14728 --- [ Test worker] site.yejin.sbb.base.TestInitDataConfig : [init method] InitializingBean by Configuration
2022-10-05 00:24:50.607 DEBUG 14728 --- [ Test worker] site.yejin.sbb.base.TestInitDataDao : [init method] InitializingBean by component
2022-10-05 00:24:50.617 DEBUG 14728 --- [ Test worker] site.yejin.sbb.base.TestInitDataDao : [init method] bean(init method)
2022-10-05 00:24:50.630 DEBUG 14728 --- [ Test worker] site.yejin.sbb.SbbApplication : [init method] bean 생성
2022-10-05 00:24:52.555 DEBUG 14728 --- [ Test worker] site.yejin.sbb.base.TestInitData : [init method] 테스트 데이터 초기화 by ApplicationRunner
2022-10-05 00:24:52.571 DEBUG 14728 --- [ Test worker] site.yejin.sbb.base.TestInitData : [init method] 테스트 데이터 초기화 by CommandLineRunner
2022-10-05 00:24:52.583 DEBUG 14728 --- [ Test worker] site.yejin.sbb.base.TestInitDataHandler : [init method] ApplicationReadyEvent listen
참고 : https://madplay.github.io/post/spring-bean-lifecycle-methods
https://www.baeldung.com/running-setup-logic-on-startup-in-spring#4-the-bean-initmethod-attribute
특징 비교
InitializingBean vs initMethod vs @PostConstruct
InitializingBean 특징
- 스프링 전용 인터페이스다. 코드가 자바가 아닌 스프링에 의존하게 된다.
- 초기화, 소멸 메서드의 이름을 변경할 수 없다.
- 외부 라이브러리에 구현할 수 없다.
@Bean initMethod 특징
- 메서드의 이름 지정 가능(보편적으로 init)
- 스프링 빈이 스프링 코드를 의존하지 않는다.
- 외부 라이브러리에도 적용 가능하다.
@PostConstruct 특징
- 최신 스프링에서 가장 권장하는 방법이다.
- 애노테이션 하나만 붙이면 되므로 매우 편리하다.
- javax 패키지(javax.annotation.PostConstruct)에 있는 자바 표준 기술로 스프링에 종속적이지 않아 다른 컨테이너에서 동작가능하다.
- 외부 라이브러리에는 적용 불가하다. (외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용)
출처: https://alkhwa-113.tistory.com/entry/스프링-빈의-생명주기와-초기화-분리
https://chung-develop.tistory.com/55
1. CommandLineRunner
CommandLineRunner 이란?
CommandLineRunner는 args 에 해당하는 매개변수 값을 실행하는 run() 메소드를 가지고 있다.
@FunctionalInterface
public interface CommandLineRunner {
/**
* Callback used to run the bean.
* @param args incoming main method arguments
* @throws Exception on error
*/
void run(String... args) throws Exception;
}
Bean이 CommandLineRunner 을 리턴값으로 가진다면, commandLineRunner는 app 가 실행된 직후 @Bean 으로 등록된 메서드가 실행된다.
SpringApplication 클래스의 run 메소드가 호출되는 지점에서부터 따라가면서 callRunners 메소드를 보면
- callRunners
- 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) { callRunner((ApplicationRunner) runner, args); } **if (runner instanceof CommandLineRunner) { callRunner((CommandLineRunner) runner, args); }** } }
CommandLineRunner run 메소드를 실행하는 것을 확인 할 수 있다.
private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
try {
(runner).run(args.getSourceArgs());
}
catch (Exception ex) {
throw new IllegalStateException("Failed to execute CommandLineRunner", ex);
}
}
사용 예시
실제 수업 중에도 Test 데이터를 초기화하기 위하여 사용한 코드의 일부이다.
람다식표현을 사용하여 익명클래스를 좀더 간단하게 표현한 경우이다.
import org.springframework.boot.CommandLineRunner;
@Configuration
@Profile("test")
public class TestInitData {
// CommandLineRunner : 주로 앱 실행 직후 초기데이터 세팅 및 초기화에 사용
@Bean
CommandLineRunner init(MemberRepository memberRepository) {
return args -> { ... };
}
참고
익명클래스로 표현했을 때는 아래와 같다.
import org.springframework.boot.CommandLineRunner;
@Configuration
@Profile("test")
public class TestInitData {
// CommandLineRunner : 주로 앱 실행 직후 초기데이터 세팅 및 초기화에 사용
@Bean
CommandLineRunner init(MemberRepository memberRepository) {
return new CommandLineRunner() {
@Override
public void run(String args) throws Exception{
...
}
}
}
테스트 데이터를 초기화 하는것은 1회성이기 때문에 람다(익명클래스)를 사용하였으나, 재사용 여부가 있는 경우 내부클래스를 선언할 수 있다.
내부 클래스
import org.springframework.boot.CommandLineRunner;
@Configuration
@Profile("test")
public class TestInitData {
// CommandLineRunner : 주로 앱 실행 직후 초기데이터 세팅 및 초기화에 사용
@Bean
CommandLineRunner init(MemberRepository memberRepository) {
return new InitTestData();
class InitTestData implements CommandLineRunner {
@Override
public void run(String args) throws Exception{
...
}
}
}
2. ApplicationRunner
ApplicationRunner 이란?
CommandRunner 인터페이스와 거의 동일하나, 매개변수로 String이 아닌 ApplicationArguments 값을 가진다.
@FunctionalInterface
public interface ApplicationRunner {
/**
* Callback used to run the bean.
* @param args incoming application arguments
* @throws Exception on error
*/
void run(ApplicationArguments args) throws Exception;
}
마찬가지로 Bean이 ApplicationRunner 을 리턴값으로 가진다면, ApplicationRunner는 app 가 실행된 직후 @Bean 으로 등록된 메서드가 실행된다.
SpringApplication 클래스의 run 메소드가 호출되는 지점에서부터 따라가면서 callRunners 메소드를 보면
callRunners
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) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
ApplicationRunner run 메소드를 실행하는 것을 확인 할 수 있다.
private void callRunner(ApplicationRunner runner, ApplicationArguments args) {
try {
(runner).run(args);
}
catch (Exception ex) {
throw new IllegalStateException("Failed to execute ApplicationRunner", ex);
}
}
사용 예시
동일한 테스트 데이터를 초기화하는 코드를 ApplicationRunner로 변경해보면 아래와 같이 변경할 수 있다.
@Configuration
@Profile("test")
public class TestInitData {
@Bean
ApplicationRunner init(MemberRepository memberRepository){
return args -> { ... };
}
EventListener 사용
@EventListener(ApplicationReadyEvent.class)
public void init(){
System.out.println("[Event Listener] ApplicationReadyEvent listen");
}
참고 : https://www.daleseo.com/spring-boot-runners/
3. ApplicationEvent
@EventListener 이란?
이벤트가 발생한 경우 해당 코드가 실행되도록 하는 어노테이션이다.
Spring 이전 버전에서는 직접 event handler와 listener, provider를 인터페이스를 implement 하여 구현하여야 했지만 spirng 4.2 버전 이후로 @EventListener 어노테이션을 통해 가능하게 되었다.
@EventListener 어노테이션 코드
/**
* Annotation that marks a method as a listener for application events.
*
* <p>Events can be {@link ApplicationEvent} instances as well as arbitrary
* objects.
*
* <p>Processing of {@code @EventListener} annotations is performed via
* the internal {@link EventListenerMethodProcessor} bean which gets
* registered automatically when using Java config or manually via the
* {@code <context:annotation-config/>} or {@code <context:component-scan/>}
* element when using XML config.
*
* ... 중략
*/
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EventListener {
@EventListener에 Application이 구동되기 전, Ready 상태일때의 이벤트(ApplicationReadyEvent)를 설정하면, 앞선 CommandLinerRunner와 ApplicationRunner와 마찬가지로 application 이 구동될시 실행시킬 수 있다.
참고 : SpringApplicationEvent
출처: https://mangkyu.tistory.com/233
ApplicationStartingEvent
애플리케이션이 실행되고나서 가능한 빠른 시점에 발행됨 Environment와 ApplicationContext는 준비되지 않았지만 리스너들은 등록이 되었음 이벤트 발행 source로 SpringApplication이 넘어오는데, 이후 내부 상태가 바뀌므로 내부 상태의 변경은 최소화해야 함
ApplicationContextInitializedEvent
애플리케이션이 시작되고 애플리케이션 컨텍스트가 준비되었으며 initializer가 호출되었음 하지만 빈 정보들은 불러와지기 전에 발행됨
ApplicationEnvironmentPreparedEvent
애플리케이션이 실행되고 Environment가 준비되었을 때 발행됨
ApplicationPreparedEvent
애플리케이션이 시작되고 애플리케이션 컨텍스트가 완전히 준비되었지만 refresh 되기 전에 발행됨 빈 정보들은 불러와졌으며 Environment 역시 준비가 된 상태임
ApplicationStartedEvent
애플리케이션 컨텍스트가 refesh 되고나서 발행됨 ApplicationRunner와 CommandLineRunner가 실행되기 전의 시점임
ApplicationReadyEvent
애플리케이션이 요청을 받아서 처리할 준비가 되었을 때 발행됨 이벤트 발행 source로 SpringApplication이 넘어오는데, 이후에 초기화 스텝이 진행되므로 내부 변경은 최소화해야 함
ApplicationFailedEvent
애플리케이션이 실행에 실패했을 때 발행됨
사용 예시
@EventListener를 통해 ApplicationReadyEvent 가 발생한 경우 테스트 데이터를 생성할 수 있도록 한다.
ApplicationReadyEvent 클래스
모든 초기화 과정이 완료된 후 서비스 요청을 받을 준비가 되었을 때 ApplicationReadyEvent 가 발생한다.
/**
* Event published as late as conceivably possible to indicate that the application is
* ready to service requests. The source of the event is the {@link SpringApplication}
* itself, but beware of modifying its internal state since all initialization steps will
* have been completed by then.
*
*/
@SuppressWarnings("serial")
public class ApplicationReadyEvent extends SpringApplicationEvent {
@Component
@Slf4j
public class TestInitDataHandler {
@Autowired
private MemberRepository memberRepository;
@EventListener(ApplicationReadyEvent.class)
public void init() {
log.debug("[Event Listener] ApplicationReadyEvent listen");
... 생략
}
}
참고 : https://jeong-pro.tistory.com/206
→ EventListener 의 상세 동작과정을 확인해볼 수 있음
4. @Postconstruct 어노테이션
@Postconstruct 이란?
종속성 주입이 완료된 후 (즉, bean 초기화가 모두 완료된 후) ****실행되어야 하는 메소드에 사용되며, 다른 리소스에서 호출되지 않아도 실행된다. 따라서 bean이 여러번 초기화 되지 않고 1번만 실행되도록 할 때 사용한다.
@PostConstruct 어노테이션 코드
/**
* The <code>PostConstruct</code> annotation is used on a method that
* needs to be executed after dependency injection is done to perform
* any initialization. This method must be invoked before the class
* is put into service. This annotation must be supported on all classes
* that support dependency injection. The method annotated with
* <code>PostConstruct</code> must be invoked even if the class does
* not request any resources to be injected. Only one
* method in a given class can be annotated with this annotation.
*/
@Documented
@Retention (RUNTIME)
@Target(METHOD)
public @interface PostConstruct {
}
@PostConstruct 의 장점
- 종속성 주입이 완료되었는지 여부를 고려하지 않아도 된다.
- bean의 생애 주기에서 오직 1번만 실행된다.
사용 예시
JWT 를 이용하여 secretkey 평문을 인코딩하는 경우는 bean 생애주기에서 반드시 1회만 실행되어야 하기 때문에 다음과 같이 @PostConstruct 를 이용하여 JWT provider 내의 init 메소드를 구현할 수 있다.
@Component
public class JwtProvider {
@Value("${custom.jwt.secretKey}")
private String secretKeyPlain;
private SecretKey jwtSecretKey;
@PostConstruct
private void init(){
String keyBase64Encoded = Base64.getEncoder().encodeToString(secretKeyPlain.getBytes());
jwtSecretKey= Keys.hmacShaKeyFor(keyBase64Encoded.getBytes());
}
참고 : https://www.baeldung.com/spring-postconstruct-predestroy
5. InitializingBean 인터페이스
InitializingBean 인터페이스의 afterPropertiesSet() 이란?
bean 이 생성될 때 호출된다. 따라서 bean 의 생성과 삭제가 반복된다면 여러번 호출될 수 있다.
InitializingBean 인터페이스의 afterPropertiesSet() 메소드
public interface InitializingBean {
/**
* Invoked by the containing {@code BeanFactory} after it has set all bean properties
* and satisfied {@link BeanFactoryAware}, {@code ApplicationContextAware} etc.
* <p>This method allows the bean instance to perform validation of its overall
* configuration and final initialization when all bean properties have been set.
* @throws Exception in the event of misconfiguration (such as failure to set an
* essential property) or if initialization fails for any other reason
*/
void afterPropertiesSet() throws Exception;
}
사용 예시
앞선 예제와 비슷한 테스트 데이터를 초기화 하는 경우를 예로 들면
@Component
public class TestInitDataDao implements InitializingBean {
@Autowired
private MemberRepository memberRepository;
@Override
public void afterPropertiesSet() throws Exception {
... 생략
memberRepository.save(u1);
}
}
마찬가지로 람다식으로 표현하면
익명클래스로 표현하기위해 Configuration에서 Bean을 생성하는 형태로 변경
@Configuration
public class TestInitDataConfig {
@Bean
public InitializingBean init(){
return new InitializingBean() {
@Autowired
private MemberRepository memberRepository;
@Override
public void afterPropertiesSet() throws Exception {
... 생략
}
};
}
}
@Configuration
public class TestInitDataConfig {
@Autowired
private MemberRepository memberRepository;
@Bean
public InitializingBean init(){
return () -> {
... 생략
};
}
}
참고 : https://jeong-pro.tistory.com/179
-> 프로세스 종료에 대한 내용을 담고 있음
6. @Bean의 initMethod
@Bean 어노테이션 내부에 초기화할때 실행할 method를 입력하는 형태이다.
초기화 뿐만아니라 destroyMethod도 동일하게 제공한다.
@Bean(initMethod = “”) 이란?
해당 bean 인스턴스 초기화 단계에서 호출되는 메소드를 설정하는 것이다.
/**
* The optional name of a method to call on the bean instance during initialization.
* Not commonly used, given that the method may be called programmatically directly
* within the body of a Bean-annotated method.
* <p>The default value is {@code""}, indicating no init method to be called.
*@seeorg.springframework.beans.factory.InitializingBean
*@seeorg.springframework.context.ConfigurableApplicationContext#refresh()
*/
String initMethod() default "";
사용 예시
앞선 예시와 동일하게 테스트 데이터를 생성하는 빈에 대하여 init() 메소드를 먼저 지정할 경우, 해당 init() 호출된 후에 빈이 생성된다.
@Bean(initMethod = "init")
public TestInitDataConfig testInitDataInit(){return new TestInitDataConfig();}
init() 메소드
public class TestInitDataConfig {
@Autowired
private MemberRepository memberRepository;
public void init(){
... 생략
}
}
정리
1. Spring Application을 구동할 때 메서드를 실행시키는 방법에 대해 설명해주세요.
Application이 구동할 때 메서드를 실행시켜서 초기화 작업이 필요한 경우가 있습니다. Spring에서는CommandLineRunner, ApplicationRunner를 구현한 클래스를 만들어서 실행시키는 2가지 방법이 있으며, Spring의 ApplicationEvent를 사용한 방법도 있습니다.
또한 인스턴스의 라이프 사이클을 사용한 Spring의 ApplicationEvent를 사용한 방법, @Postconstruct를 사용한 방법, InitializingBean 인터페이스를 구현하는 방법, @Bean의 initMethod를 사용한 방법이 있습니다.
+저는 TDD 구현을 위해 test 데이터를 초기화 하기 위해 ApplicationRunner 클래스를 이용하여 구현해본 경험이 있습니다.