![[Spring] @Value는 static 변수에 사용할 수 없다.](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkHFy4%2FbtsJIiQXGTc%2FUXODidAC6pYRa86TkqBjiK%2Fimg.png)
문제 시나리오
- CorsConfig 클래스에 작성된 domain 필드가 존재한다.
- @Value 애너테이션을 사용하여 application.yml에 작성한 값을 domain 필드에 주입한다.
application.yml
... 생략
server:
domain: ${SERVER_DOMAIN}
CorsConfig
@Configuration
public class CorsConfig {
@Value("${server.domain}")
static String domain;
public static CorsConfigurationSource corsConfiguration() {
CorsConfiguration configuration = new CorsConfiguration();
// allowedOriginPatterns : 리소스를 허용할 URL
List<String> allowedOriginPatterns = new ArrayList<>();
allowedOriginPatterns.add("http://localhost:3000");
allowedOriginPatterns.add("http://127.0.0.1:3000");
allowedOriginPatterns.add(domain);
List<String> allowedHttpMethods = new ArrayList<>();
allowedHttpMethods.add("GET");
allowedHttpMethods.add("POST");
allowedHttpMethods.add("PUT");
allowedHttpMethods.add("DELETE");
allowedHttpMethods.add("PATCH");
configuration.setAllowedOrigins(allowedOriginPatterns);
configuration.setAllowedMethods(allowedHttpMethods);
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
@Value를 통해 application.yml에 작성한 server.domain 값을 주입받고 있는지 확인하기 위해 TestController를 작성하였다.
GET /test를 호출한 결과 domain 필드에 값을 주입받지 못하고 null이 저장된 것을 확인할 수 있다.
@RestController
public class TestController {
@GetMapping("/test")
public void test() {
System.out.println("domain : " + CorsConfig.domain);
}
}
@Value 메커니즘
@Value 애너테이션은 스프링 라이프사이클에서 의존 관계 주입(DI) 단계에서 작동한다. 애플리케이션이 실행되면 스프링 컨테이너는 실행에 필요한 모든 빈을 생성하고 의존성을 주입한다. 이때, 빈의 필드에 @Value 애너테이션이 붙어있다면 외부 설정 파일(ex. application.yml)에 작성한 값이 주입된다.
static 변수의 생성 시점
static 필드가 생성되는 시점은 클래스 로더(Class Loader)가 클래스를 JVM 메모리 영역인 Method Area에 로드할 때이다.
자바로 작성한 프로그램이 실행되면 자바 컴파일러(javac)에 의해 클래스 파일이 생성되고, 이 파일은 JVM 메모리 영역인 Method Area에 적재된다. static 필드는 프로그램이 실행될 때 클래스 로더에 의해 한 번만 메모리에 적재되며, 이후에는 해당 클래스의 모든 인스턴스가 static 필드를 공유하게 된다.
static 필드와 @Value의 생성 시점은 서로 다르다.
- static 필드
- 클래스 로더(Class Loader)가 클래스 파일(.class)을 읽어 들여 JVM의 메모리 영역인 Method Area에 로드한다.
- @Value
- 스프링 컨테이너가 생성된 후 각 빈을 생성하고, 빈들 간의 의존성을 주입할 때, @Value 애너테이션을 통해 외부 설정 파일(ex. application.yml)에서 값을 주입받는다.
static 필드는 스프링 애플리케이션이 실행되기 전에 이미 JVM의 Method Area에 적재되지만, @Value는 스프링 컨테이너가 빈을 생성하고 의존성을 주입할 때 적용되므로 생성 및 사용 시점이 다르다.
이러한 이유로 인해 static을 사용하는 domain 필드가 application.yml에 작성한 값을 주입받지 못했던 것이다.
@Value는 멤버 변수와 사용한다.
결론적으로, static 필드와 @Value 애너테이션의 생성 시점 차이로 인해 static 필드에는 외부 설정 값을 주입받을 수 없었다.
가장 근본적인 해결책은 static 필드를 사용하는 대신 멤버 변수를 사용하는 것이다.
필자는 처음에 SecurityConfig 클래스에서 CORS 설정 정보로 CorsConfig.corsConfiguration()를 사용하였다. 초기에는 문제가 없었으나, 도메인 주소로 HTTP 요청을 허용하는 과정에서 문제가 발생하였다. 허용할 도메인 주소를 application.yml에 작성하고 @Value로 주입받으려 했으나 해당 필드에 null 값이 저장되는 상황이 발생한 것이다.
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final AuthenticationEntryPoint authenticationEntryPoint;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.cors(cors -> {
cors.configurationSource(CorsConfig.corsConfiguration());
})
// .. 생략
.build();
}
}
기존의 CorsConfig 클래스에서 static을 제거하고 GET /test를 호출해보니 정상적으로 주입받는 것을 확인할 수 있다.
@Configuration
public class CorsConfig {
@Value("${server.domain}")
String domain;
public CorsConfigurationSource corsConfiguration() {
CorsConfiguration configuration = new CorsConfiguration();
// allowedOriginPatterns : 리소스를 허용할 URL
List<String> allowedOriginPatterns = new ArrayList<>();
allowedOriginPatterns.add("http://localhost:3000");
allowedOriginPatterns.add("http://127.0.0.1:3000");
allowedOriginPatterns.add(domain);
List<String> allowedHttpMethods = new ArrayList<>();
allowedHttpMethods.add("GET");
allowedHttpMethods.add("POST");
allowedHttpMethods.add("PUT");
allowedHttpMethods.add("DELETE");
allowedHttpMethods.add("PATCH");
configuration.setAllowedOrigins(allowedOriginPatterns);
configuration.setAllowedMethods(allowedHttpMethods);
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}