@Value annotation을 사용하여 properties에 있는 메타 정보 값을 가져오는 과정에서, 값이 null으로 들어오는 문제를 해결하며 기록한 내용입니다.
@Value Annotation
쉽게 @Value 어노테이션은 데이터베이스 연결 정보나, 외부 API 주소 등, 메타 정보를 관리하기 위한 프로퍼티(properties) 파일이나, 야믈(yml, yaml) 파일에서 메타 정보를 가져오기 위해 사용하는 어노테이션입니다.
properties 또는 yml 파일에 메타 정보를 기록해놓고 @Value 어노테이션을 통해 해당 값을 가지고 오는 방식을 통해 로컬(local) 개발 환경에서의 설정 값과 개발(dev), 실제 서버(prod)의 설정 값을 따로 분리할 수 있고, 수정과 관리가 용이하다는 장점이 있습니다.
#Filter skip path
filter.skip.paths=/api/auth/login,/api/auth/signup
(application.properties에서 관리되는 메타 정보)
@Component
public class JwtAuthenticationFilter extends GenericFilterBean {
....
@Value("${filter.skip.paths}")
private List<String> skipPath;
....
}
(@Value 어노테이션으로 application.properties에서 관리되는 test.value의 값을 가져오는 방법)
@Value 어노테이션 null 해결방안
...
# Logging
spring.output.ansi.enabled=always
# JWT
jwt.secret=VlwEy3BsYt917zqB7TejMnVaYzblYcfPQye08f7MGVc95kHN
#Filter skip path
filter.skip.paths=/api/auth/login,/api/auth/signup
# Redis
spring.redis.host=localhost
spring.redis.port=6379
...
JWT를 사용한 로그인을 구현하는 과정 중, 인증 여부를 확인하는 JwtAuthenticationFilter를 통과하는 부분에서 인증 없이 통과할 수 있는 skip path를 관리하기 위해 properties 파일에서 paths 정보를 가져오는 과정에서, 값을 가져오지 못하고 계속해서 Null 값을 뱉어내는 문제가 발생했습니다.
@Value("${filter.skip.paths}")
private List<String> skipPath;
신기하게도 같은 properties에 기록된 나머지 값들은 전부 정상적으로 가지고 오는 것을 확인했는데요.
문제가 된 부분 (핵심)
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
....
@Value("${filter.skip.paths}")
private List<String> skipPath;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//0. skipPath 에 등록된 uri 인 경우 통과
String uri = request.getRequestURI();
if (skipPath.contains(uri)) {
filterChain.doFilter(request, response);
return;
}
....
}
skipPath가 null이기 때문에 .contains 메서드에서 계속해서 오류가 발생했습니다.
그리고 여러 시도 끝에 문제가 되는 부분을 찾았는데요.
@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
private final RedisTemplate redisTemplate;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, redisTemplate), UsernamePasswordAuthenticationFilter.class);
// JwtAuthenticationFilter 를 UsernamePasswordAuthenticationFilter 전에 적용시킨다.
}
...
}
문제의 원인은 바로 configure 메서드 과정 중 new JwtAuthenticationFilter(jwtTokenProvider, redisTemplate) 부분이었습니다. 이 부분이 문제가 되는 이유를 알기 위해서는 '스프링 빈(Spring Bean)'과 '싱글톤 패턴(Singleton pattern)'에 대한 이해가 필요한데요.
'스프링 빈(Spring Bean)'
스프링 IoC(Inversion of Control) 컨테이너에 의해서 관리되며, 애플리케이션의 핵심을 이루는 객체들을 스프링 빈(Beans)라고 합니다. 빈은 쉽게 스프링 컨테이너에 의해서 인스턴스화 되어 조립되고 관리되는 자바 객체라고 생각할 수 있습니다.
'싱글톤 패턴(Singleton pattern)'
싱글톤 패턴이란 애플리케이션이 시작될 때, 어떤 클래스가 최초 한 번만 메모리를 할당하고(static), 그 메모리에 인스턴스를 만들어서 사용하는 디자인 패턴입니다. 쉽게 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴인데요.
스프링의 빈 객체는 기본적으로 싱글톤으로 생성되는데, 그 이유는 사용자의 요청이 있을 때마다 애플리케이션 로직까지 모두 포함하고 있는 오브젝트를 매번 생산하는 것이 비효율적이기 때문입니다. (= 스프링 컨테이너가 등록되는 빈들을 알아서 싱글톤으로 관리)
***
실제로 스프링 컨테이너 내부에서는 모든 빈들을 등록할 때 @Value("${}") 안의 내용에 맞는 값을 application.properties 또는 application.yml 파일에서 찾아서 넣어주게 됩니다.
조금 더 자세하게는 'BeanPostProcessor Interface'를 구체화한 'PropertySourcesPlaceholderConfigurer Class'에서 동작합니다. (스프링 IoC 컨테이너에서는 빈 인스턴스를 인스턴스화한 다음에 BeanPostProcessor가 자신의 일을 수행합니다.)
결론
JwtAuthenticationFilter를 보면 @Component 어노테이션을 통해 인스턴스화 되어 빈으로 등록된 상태입니다.
빈으로 등록된 JwtAuthenticationFilter 객체는 등록될 때 BeanPostProcessor 인터페이스의 동작에 의해 @Value() 안에 값이 들어간 상태로 객체가 만들어졌을 텐데요.
WebSecutiryConfiguration Class를 보면 'new JwtAuthenticationFilter(jwtTokenProvider, redisTemplate)'부분에서 이미 빈으로 등록된 객체를 사용하는 것이 아니라 new 키워드를 통해 새로운 인스턴스를 생성하고 있기 때문이었습니다.
결론은 스피링 빈으로 등록된 인스턴스가 아니기 때문에 @Value 어노테이션이 작동하지 않은 것입니다.
@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// JwtAuthenticationFilter 를 UsernamePasswordAuthenticationFilter 전에 적용시킨다.
}
...
}
(수정된 코드)
수정은 Bean으로 등록된 JwtAuthenticationFilter 객체를 사용함으로써 application.properties에 저장된 메타 정보를 정상적으로 가지고 오게 되었습니다.
***
그 외 해결 방안으로는
1. 잘못된 라이브러리를 import 했을 경우
import com.google.api.client.util.Value;
2. 변수의 static 선언
스프링에서는 정적 변수로 선언된 변수에는 Injection을 할 수 없습니다.
< 참고 자료 >
'Programming > Spring Boot' 카테고리의 다른 글
Spring swagger 3 사용방법(springdoc-openapi-ui) (2) | 2022.06.25 |
---|---|
@RequestBody @ResponseBody 어노테이션 이해하고 사용하기 (2) | 2022.05.13 |
JPA @Modifying clearAutomatically 속성 발생할 수 있는 문제 (0) | 2022.04.20 |
생성자 주입과 필드 주입, 수정자 주입 정리 (feat. 의존성 관계 주입) (0) | 2022.03.31 |
Spring Batch 개념과 구조, 동작 방식에 대한 정리 (0) | 2022.03.15 |