Private

[ezfarm] Spring Security를 추가한 뒤 단위 테스트(Controller)에서 생기는 문제 해결하기

highright96 2021. 6. 18.

해당 글의 프로젝트는 다음 URL에서 확인할 수 있다.

프로젝트 링크

 

2021-ict-hanium/ezfarm-back

Contribute to 2021-ict-hanium/ezfarm-back development by creating an account on GitHub.

github.com

원인

프로젝트에 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은 모든 컨트롤러 테스트에 사용되므로, 따로 클래스로 만든 후 상속을 받았다.

댓글