[ezfarm] Spring Security를 추가한 뒤 단위 테스트(Controller)에서 생기는 문제 해결하기
해당 글의 프로젝트는 다음 URL에서 확인할 수 있다.
원인
프로젝트에 Spring Security를 추가하고, 컨트롤러 테스트를 실행하니 다음과 같은 에러가 발생하는 것을 확인할 수 있었다.
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityConfig' defined in file [D:\github\ezfarm-back\out\production\classes\com\ezfarm\ezfarmback\config\security\SecurityConfig.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.ezfarm.ezfarmback.security.local.CustomUserDetailsService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1354)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1204)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:564)
생략 ...
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.ezfarm.ezfarmback.security.local.CustomUserDetailsService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1790)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1346)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
생략 ...
에러를 요약하자면 다음과 같다. org.springframework.beans.factory.UnsatisfiedDependencyExceptio n: Error creating bean with name 'securityConfig'
securityConfig라는 이름의 빈을 생성하는데 UnsatisfiedDependencyException(의존성 주입 예외)이 발생하였다.
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.ezfarm. ezfarm back.security.local.CustomUserDetailsService' available:expected at least 1 bean which qualifies as autowire candidate.
이름이 CustomUserDetailsService인 빈이 존재하지 않는다.
에러의 원인 SpringConfig 클래스 파일을 보자.
SpringConfig.class
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String USER = "USER";
private final CustomUserDetailsService customUserDetailsService;
private final TokenProvider tokenProvider;
private final CorsFilter corsFilter;
생략 ...
}
SpringConfig 클래스를 보니 빈으로 등록된 CustomUserDetailsService, TokenProvider, CorsFilter를 주입받고 있다.
코드를 보니 왜 에러가 발생했는지 알 것 같다.
원인은 테스트가 실행되면 SecurityConfig가 빈으로 등록(@Configuration 애노테이션이 붙어 컴포넌트의 스캔 대상이 된다.) 되는데, 이 때 생성자 주입을 받을 CustomUserDetailsService라는 이름의 빈이 없는 것이다.(TokenProvider, CorsFilter 빈도 없을 것이다.)
테스트가 실행될 때 위의 빈들은 컴포넌트 스캔의 대상이 아니기 때문에 발생하는 것 같다.
확실하게 알기 위해 테스트 코드를 보자.
테스트 코드
DisplayName("유저 단위 테스트(Controller)")
@WebMvcTest(controllers = UserController.class)
public class UserControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@MockBean
UserService userService;
@MockBean
UserRepository userRepository;
@DisplayName("유저 회원가입을 한다.")
@Test
void createUser() throws Exception {
//given
SignUpRequest signUpRequest = new SignUpRequest("highright", "highright@mail.com", "비밀번호");
//when, then
when(userService.createUser(any())).thenReturn(1L);
mockMvc.perform(post("/api/user/signup")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(signUpRequest)))
.andExpect(status().isCreated())
.andDo(print());
}
}
테스트 코드를 보니 클래스에 @WebMvcTest 애노테이션이 붙어있다.
이 부분이 문제였다.
@WebMvcTest 애노테이션은 MVC를 테스트할 때(테스트가 가벼워지기 때문이다.) 쓰이며, 다음과 같은 애노테이션이 붙어있는 클래스만 스캔한다. @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor
그럼 SpringSecurity 클래스가 주입 받는 빈들은 어떤 애노테이션이 붙어있는지 살펴보자.
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
...
}
CustomUserDetailsService는 @Service 애노테이션을 통해 빈으로 등록된다. 다른 클래스들도 이와 비슷하다.
따라서 위 테스트 환경의 스캔 대상이 아니기 때문에 빈으로 등록되지 않는 것이다.
그럼 이 문제를 해결하려면 어떻게 해야할까?
해결방법
SpringSecurity 클래스가 주입받는 클래스들을 컴포넌트 스캔의 대상으로 설정해주면 된다.
아래와 같이 @ComponentScan 애노테이션을 사용해서 빈으로 등록해준다.
@ComponentScan의 basePackages는 명시한 패키지 내의 @Component, @Service, @Controller, @Repository... 등 클래스들을 등록시킨다.
DisplayName("유저 단위 테스트(Controller)")
@WebMvcTest(controllers = UserController.class)
public class UserControllerTest extends CommonApiTest {
...
}
@ComponentScan(basePackages = {"com.ezfarm.ezfarmback.security", "com.ezfarm.ezfarmback.config.security"})
public class CommonApiTest {
...
}
@ComponentScan은 모든 컨트롤러 테스트에 사용되므로, 따로 클래스로 만든 후 상속을 받았다.
'Private' 카테고리의 다른 글
[ezfarm] 차집합을 이용해 쿼리 수정하기 (2) | 2021.07.30 |
---|---|
[ezfarm] 도메인 등록 및 HTTPS 설정 (0) | 2021.07.24 |
[ezfarm] CI/CD 서버 구축과 배포 (0) | 2021.07.12 |
댓글