@Value 의 값이 바인딩 되지 않으신가요?
-
Spring Boot 애플리케이션을 실행시킬 떄 @Value로 (org.springframework.beans.factory.annotation.Value) 값을 주입하려고 할떄 생각과는 다르게 @Value로 주입한 값이 제대로 바인딩되지 않는 경우가 있습니다.
-
이러한 현상은 왜 생기며, 어떻게 해결할 수 있는지 알아보겠습니다.
-
우선 Spring Boot를 실행시킬떄 loading 순서는 어떻게 되는지 먼저 살펴보겠습니다.
-
Spring Boot 애플리케이션이 실행될 때의 로딩 순서는 크게 다음과 같은 단계로 이루어집니다. 이 순서는 Spring Framework의 동작 원리에 기반하며, Spring Boot 특유의 기능들이 추가적으로 포함됩니다.
- JVM 초기화 및 Main 클래스 실행 애플리케이션이 실행되면 JVM이 메인 클래스의 main() 메서드를 호출합니다. Spring Boot 애플리케이션의 진입점은 일반적으로 @SpringBootApplication이 선언된 클래스입니다.
- SpringApplication 초기화 SpringApplication.run() 메서드가 호출됩니다. SpringApplication 객체가 생성되며, 실행 환경(ApplicationEnvironment)과 초기화 설정이 수행됩니다. 이떄 프로파일 Spring Boot 실행시 -Dspring.profiles.acitve=dev 등으로 설정한 프로파일에 따라서 속성(application.properties 또는 application.yml)이 로드됩니다.
그다음 main(String[] args) 에서 넘긴 arg = 커맨드 라인 인자가 처리됩니다.
- Application Context 생성 및 준비 Spring Boot는 애플리케이션 컨텍스트를 생성합니다. 기본적으로 AnnotationConfigApplicationContext 또는 ServletWebServerApplicationContext를 사용합니다. ApplicationContextInitializer가 실행되어 초기 설정을 수행합니다.
- EnvironmentPostProcessor 실행 application.properties 또는 application.yml에서 정의된 설정과 커맨드 라인 인자 등을 기반으로 Environment 객체를 구성합니다. 이를 통해 애플리케이션의 프로파일, 포트, 데이터베이스 연결 정보 등이 설정됩니다.
- Bean Definition 등록 및 로드 Spring은 컴포넌트 스캔(@Component, @Service, @Repository 등) 또는 명시적으로 정의된 설정 클래스(@Configuration)로부터 빈 정의를 로드합니다. 각 빈의 의존성이 분석되고 생성이 준비됩니다.
- Auto-Configuration 처리 Spring Boot의 핵심 기능인 Auto-Configuration이 실행됩니다. @EnableAutoConfiguration에 의해 정의된 클래스들이 활성화됩니다. 자동 설정은 spring-boot-autoconfigure 모듈의 빈들을 조건(@ConditionalOn…)에 따라 동적으로 등록합니다.
- Servlet/WebServer 초기화 (웹 애플리케이션일 경우) 내장 웹 서버(Tomcat, Jetty, Undertow 등)가 시작됩니다. 서블릿 컨테이너와 DispatcherServlet이 초기화됩니다.
- Application Context Refresh ApplicationContext가 리프레시(refresh) 됩니다. 빈 생성, 의존성 주입, 초기화 메서드 호출(@PostConstruct 등)이 수행됩니다. 이벤트 리스너(ApplicationListener)가 초기화됩니다.
- CommandLineRunner 및 ApplicationRunner 실행 애플리케이션이 실행 준비를 완료한 후, CommandLineRunner 또는 ApplicationRunner 인터페이스를 구현한 빈들이 실행됩니다. 초기화 로직을 추가하고 싶을 때 이 단계에서 구현할 수 있습니다.
- 애플리케이션 준비 완료 애플리케이션이 요청을 받을 준비를 마칩니다. 이 시점 이후부터 애플리케이션은 클라이언트 요청을 처리하거나, 비동기 작업을 수행합니다.
추가적인 주요 컴포넌트
Event Mechanism: Spring Boot는 로딩 과정에서 여러 이벤트를 발생시킵니다. (예: ApplicationStartedEvent, ApplicationReadyEvent) Actuator Monitoring: Spring Boot Actuator가 활성화된 경우, 애플리케이션의 상태 정보가 제공됩니다.
그럼 왜 바인딩이 안돼?
- 위 설명대로 설정파일(application.properties 혹은 yml)을 먼저 로드한다면 @Value를 통해 주입한 값이 바인딩되지 않는 경우는 없어야 하지만, 실제로 발생하죠. 왜 그럴까요?
- 프로퍼티 소스가 아직 ApplicationContext에 반영되지 않은 경우 @Value는 Spring이 빈을 생성할 때 사용하는 Environment로부터 값을 가져옵니다. 하지만 프로퍼티 로딩과 @Value 바인딩이 일어나는 타이밍에 차이가 있다면, 바인딩이 실패할 수 있습니다. 예를 들어, @Value가 바인딩되기 전에 설정이 완전히 로드되지 않았거나 Environment에 반영되지 않았다면 값이 주입되지 않습니다.
해결 방법: @ConfigurationProperties를 사용하는 것이 좋습니다. 이는 Spring Boot에서 프로퍼티 값을 객체로 매핑하는 권장 방식으로, 보다 안정적이고 타입 안전합니다.
- 잘못된 프로파일 활성화 활성화된 프로파일이 올바르게 설정되지 않았거나, 원하는 프로파일의 설정이 application.yml에 없을 수 있습니다. Spring Boot는 기본적으로 application.properties 또는 application.yml 파일을 로드하며, 활성화된 프로파일에 따라 다른 설정을 적용합니다.
확인 방법: 올바른 프로파일이 활성화되었는지 확인 (spring.profiles.active 설정). @Value로 주입하려는 키가 해당 프로파일에 존재하는지 확인.
- @Value에서 사용하는 키가 존재하지 않음 @Value에 명시된 키가 yml 또는 properties 파일에 정의되지 않았을 수 있습니다. 키가 없으면 @Value는 기본적으로 바인딩을 실패합니다.
확인 방법: application.yml 또는 application.properties에 해당 키가 정확히 정의되어 있는지 확인하세요. 예: @Value("${app.name}")는 app.name 키가 정의되어 있어야 합니다.
- 스프링 컨텍스트 초기화 순서의 문제 특정 빈의 초기화 시점이 너무 빨라 Environment로부터 값을 가져오기 전에 @Value가 처리될 수 있습니다. 이는 @Component가 등록될 때 발생할 수 있습니다.
해결 방법: @PostConstruct를 사용하여 초기화 시점 이후 값을 확인하거나, ApplicationContextAware를 구현하여 ApplicationContext에서 직접 값을 가져오도록 설계할 수 있습니다.
- 타입 불일치 @Value는 문자열 기반으로 값을 주입합니다. yml이나 properties 파일에서 제공된 값이 바인딩 대상 필드와 타입이 맞지 않을 경우, 주입이 실패합니다.
해결 방법: 주입하려는 필드의 타입이 설정된 값과 호환되는지 확인하세요. 필요하다면 값에 명시적인 변환을 적용합니다.
- SpEL(Sprig Expression Language) 구문 문제 @Value가 SpEL 표현식(#{})을 사용하고 있다면, 구문 오류가 있을 가능성이 있습니다. SpEL은 복잡한 표현식을 처리할 때 정확한 구문을 요구합니다.
해결 방법: SpEL 표현식을 다시 확인하고, 테스트합니다.
내가 겪은 상황과 해결 방법
@Component로 만든 class에서 사용하는 static member에서 @Value 바인딩이 안되서 null로 처리되는 경우
- load 안되는 case
@Component
@RefreshScope
public class SomeComponentClass {
@Value("${some-static-member}")
public static String SOME_STATIC_MEMBER; // null로 처리됨
}
- -> 이렇게 바꾸면 해결
@Component
@RefreshScope
public class SomeComponentClass {
public static String SOME_STATIC_MEMBER;
@Value("${some-static-member}")
public void setSomeStaticMember(String someStaticMember) {
SOME_STATIC_MEMBER = someStaticMember;
}
}