Spring

[Spring] Spring 메서드 초기화

김yejin 2022. 10. 5. 20:59

 

 


 

초기화 method

 

spring에서 application이 구동될 때 해당 method가 같이 실행되도록 하는 방식

즉, spring에서 초기화 method를 구현하는 방법은 6가지가 있고 아래와 같다.

 

인스턴스의 라이프 사이클를 사용한 방법

  1. @PostConstruct
  2. InitializingBean
  3. initMethod

Runner와 EventListener 를 사용한 방법

  1. ApplicationRunner
  2. CommandLineRunner
  3. @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 의 장점

  1. 종속성 주입이 완료되었는지 여부를 고려하지 않아도 된다.
  2. 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 클래스를 이용하여 구현해본 경험이 있습니다.