Antock Public Data Harvester 프로젝트에서 @WebMvcTest
를 사용하여 컨트롤러 계층을 테스트하던 중, @CurrentUser
로 AuthenticatedUser
객체를 주입받는 API 테스트에서 예상과 다른 결과가 발생했습니다.
// 커스텀 ArgumentResolver
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class) &&
parameter.getParameterType().equals(AuthenticatedUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ...) {
// JWT 토큰에서 사용자 정보 추출
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// ...
return authenticatedUser;
}
}
// 컨트롤러 메소드
@GetMapping("/profile")
public ApiResponse<MemberResponse> getCurrentMemberInfo(@CurrentUser AuthenticatedUser user) {
MemberResponse response = memberApplicationService.getCurrentMemberInfo(user.getId());
return ApiResponse.success(response);
}
테스트 실행 시 다음과 같은 현상들이 나타났습니다:
String
, Long
등 기본 타입들이 cannot be resolved to a type
오류 발생@WithMockUser
사용 시 AuthenticatedUser
가 null로 주입됨CurrentUserArgumentResolver
에서 토큰을 찾을 수 없어 사용자 정보 생성 실패@WithMockUser
사용@WebMvcTest(MemberApiController.class)
@WithMockUser(roles = "ADMIN")
class MemberApiControllerTest {
// 테스트 실행 시 AuthenticatedUser가 null로 주입됨
}
문제점:
@WithMockUser
는 org.springframework.security.core.userdetails.User
객체를 principal로 설정CurrentUserArgumentResolver
는 JWT 토큰에서 사용자 정보를 추출하려 하지만, MockMvc 테스트에서는 실제 JWT 토큰이 없음AuthenticatedUser
객체 생성에 실패하여 null이 주입됨Linter 결과에서 확인한 바와 같이:
Err | String cannot be resolved to a type
Err | Long cannot be resolved to a type
Err | Exception cannot be resolved to a type
원인:
테스트 환경에서 CurrentUserArgumentResolver
를 모킹하는 대신, 테스트용 설정을 통해 명시적으로 AuthenticatedUser
를 주입합니다.
@TestConfiguration
public class TestSecurityConfig {
@Bean
@Primary
public CurrentUserArgumentResolver testCurrentUserArgumentResolver() {
return new CurrentUserArgumentResolver(null) {
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
return AuthenticatedUser.builder()
.id(1L)
.username("test@test.com")
.nickname("테스트유저")
.role("ADMIN")
.build();
}
};
}
}
@WithUserDetails
활용한 통합 테스트실제 UserDetailsService
를 사용하는 통합 테스트 환경을 구성합니다.
@SpringBootTest
@AutoConfigureMockMvc
@WithUserDetails(value = "admin0001", userDetailsServiceBeanName = "customUserDetailsService")
class MemberApiIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void getCurrentMemberInfo_Success() throws Exception {
mockMvc.perform(get("/api/v1/members/profile"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.username").exists());
}
}
컨트롤러 테스트에서 ArgumentResolver
보다는 서비스 레이어를 모킹하여 테스트를 단순화합니다.
@WebMvcTest(MemberApiController.class)
@Import(TestSecurityConfig.class)
class MemberApiControllerTest {
@MockBean
private MemberApplicationService memberApplicationService;
@Test
void getCurrentMemberInfo_Success() throws Exception {
// given
MemberResponse mockResponse = MemberResponse.builder()
.id(1L)
.username("test@test.com")
.build();
given(memberApplicationService.getCurrentMemberInfo(any(Long.class)))
.willReturn(mockResponse);
// when & then
mockMvc.perform(get("/api/v1/members/profile"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.username").value("test@test.com"));
}
}
@TestConfiguration
public class TestSecurityConfig {
@Bean
@Primary
public CurrentUserArgumentResolver mockCurrentUserArgumentResolver() {
return new CurrentUserArgumentResolver(null) {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class) &&
parameter.getParameterType().equals(AuthenticatedUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
return AuthenticatedUser.builder()
.id(1L)
.username("test@example.com")
.nickname("테스트사용자")
.role("ADMIN")
.build();
}
};
}
@Bean
public WebMvcConfigurer testWebMvcConfigurer(CurrentUserArgumentResolver resolver) {
return new WebMvcConfigurer() {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(resolver);
}
};
}
}
@WebMvcTest(MemberApiController.class)
@Import({TestSecurityConfig.class, TestExceptionHandler.class})
@WithMockUser(roles = "ADMIN")
class MemberApiControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MemberApplicationService memberApplicationService;
@Test
@DisplayName("현재 사용자 정보 조회 성공")
void getCurrentMemberInfo_Success() throws Exception {
// given
MemberResponse response = MemberResponse.builder()
.id(1L)
.username("test@example.com")
.nickname("테스트사용자")
.email("test@example.com")
.status(MemberStatus.APPROVED)
.role(Role.ADMIN)
.build();
given(memberApplicationService.getCurrentMemberInfo(eq(1L)))
.willReturn(response);
// when & then
mockMvc.perform(get("/api/v1/members/profile"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.username").value("test@example.com"))
.andExpect(jsonPath("$.data.nickname").value("테스트사용자"));
verify(memberApplicationService).getCurrentMemberInfo(1L);
}
}
@CurrentUser AuthenticatedUser
파라미터 정상 주입@WithMockUser
와 커스텀 ArgumentResolver
간의 상호작용 이해@WebMvcTest
+ TestConfiguration
으로 ArgumentResolver 모킹@SpringBootTest
+ @WithUserDetails
로 실제 인증 플로우 검증이를 통해 Spring Security를 사용하는 프로젝트에서 커스텀 인증 객체를 안정적으로 테스트할 수 있는 방법을 확립했습니다.