목적 스프링 부트에서 잘못된 타입으로 파라미터를 보내는 예외 상황에 어떤 예외가 발생하는 지 확인해보고 이를 검증하는 코드를 만들어본다!
컨트롤러 코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("/api/equipments") public class EquipmentController { private final EquipmentService equipmentService; public EquipmentController (final EquipmentService equipmentService) { this .equipmentService = equipmentService; } @GetMapping("/{id}") public EquipmentDetailResponse getEquipment (@PathVariable final Long id) { return equipmentService.findById(id); } }
테스트 코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @WebMvcTest(controllers = {EquipmentController.class}) class EquipmentControllerTest { @Autowired private MockMvc mockMvc; @Test @DisplayName("id가 문자열이면 400을 응답한다.") void getEquipment_400_idIsNotNumeric () throws Exception { String invalidId = "hi" ; mockMvc.perform(get("/api/equipments/" + invalidId)) .andExpect(status().isBadRequest()); } }
위 테스트 코드는 PathVariable이 Long이어야 하는데 문자열이 전달된 경우이다. 이 테스트 코드를 실행하고 DispatcherServlet
의 doDispatch
에 break point를 걸고 디버깅을 해보자.
디버깅하면서 찾아보기 디버깅을 하다보면 InvocableHandlerMethod
(컨트롤러에서 요청을 처리하는 메서드)라는 객체에서 invokeForRequest
라는 메서드를 호출하면서 인자를 Object
배열로 변환한다. 이 변환 로직을 각 리졸버에게 위임한다. 리졸버는 Object
로 변환한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class InvocableHandlerMethod extends HandlerMethod { @Nullable public Object invokeForRequest (NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); if (logger.isTraceEnabled()) { logger.trace("Arguments: " + Arrays.toString(args)); } return doInvoke(args); } protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } Object[] args = new Object [parameters.length]; for (int i = 0 ; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this .parameterNameDiscoverer); args[i] = findProvidedArgument(parameter, providedArgs); if (args[i] != null ) { continue ; } if (!this .resolvers.supportsParameter(parameter)) { throw new IllegalStateException (formatArgumentError(parameter, "No suitable resolver" )); } try { args[i] = this .resolvers.resolveArgument(parameter, mavContainer, request, this .dataBinderFactory); } catch (Exception ex) { if (logger.isDebugEnabled()) { String exMsg = ex.getMessage(); if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { logger.debug(formatArgumentError(parameter, exMsg)); } } throw ex; } } return args; }
Object
로 변환된 인자는 DataBinder
가 적절한 타입으로 바꿔준다. 이 과정에서 ConversionException
이 발생하고
1 2 3 4 5 6 7 8 9 10 11 public class DataBinder implements PropertyEditorRegistry , TypeConverter { @Override @Nullable public <T> T convertIfNecessary (@Nullable Object value, @Nullable Class<T> requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException { return getTypeConverter().convertIfNecessary(value, requiredType, methodParam); } }
DataBinder
에서 시작한 형 변환 로직 중 형 변환에 실패하면 ConversionException
이 발생하고 이 예외는 TypeMismatchException
이 대신 던져진다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport implements TypeConverter { @Nullable @Override public <T> T convertIfNecessary (@Nullable Object value, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { Assert.state(this .typeConverterDelegate != null , "No TypeConverterDelegate" ); try { return this .typeConverterDelegate.convertIfNecessary(null , null , value, requiredType, typeDescriptor); } catch (ConverterNotFoundException | IllegalStateException ex) { throw new ConversionNotSupportedException (value, requiredType, ex); } catch (ConversionException | IllegalArgumentException ex) { throw new TypeMismatchException (value, requiredType, ex); } } }
그리고 TypeMismatchException
은 AbstarctNamedValueMethodArgumentResolver
에서 MethodArgumentTypeMismatchException
으로 바뀌어 던져지게 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver { @Override @Nullable public final Object resolveArgument (MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); MethodParameter nestedParameter = parameter.nestedIfOptional(); Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); if (resolvedName == null ) { throw new IllegalArgumentException ( "Specified name must not resolve to null: [" + namedValueInfo.name + "]" ); } Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); if (arg == null ) { if (namedValueInfo.defaultValue != null ) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); } arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); } else if ("" .equals(arg) && namedValueInfo.defaultValue != null ) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } if (binderFactory != null ) { WebDataBinder binder = binderFactory.createBinder(webRequest, null , namedValueInfo.name); try { arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } catch (ConversionNotSupportedException ex) { throw new MethodArgumentConversionNotSupportedException (arg, ex.getRequiredType(), namedValueInfo.name, parameter, ex.getCause()); } catch (TypeMismatchException ex) { throw new MethodArgumentTypeMismatchException (arg, ex.getRequiredType(), namedValueInfo.name, parameter, ex.getCause()); } if (arg == null && namedValueInfo.defaultValue == null && namedValueInfo.required && !nestedParameter.isOptional()) { handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest); } } handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); return arg; } }
결론만 말하자면 타입이 맞지 않으면 MethodArgumentTypeMismatchException
이 발생한다.
예외 처리 코드 1 2 3 4 5 6 7 8 9 10 @RestControllerAdvice public class GlobalExceptionAdvice { @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity<String> handleMethodArgumentTypeMismatch (final MethodArgumentTypeMismatchException e) { return ResponseEntity.badRequest() .body(String.format("%s이 잘못된 타입으로 입력됐습니다. 입력값 : %s" , e.getParameter().getParameter().getName(), e.getValue())); } }
@RestControllerAdvice
로 예외를 처리해주면 된다. 파라미터의 이름과 입력값을 가져와서 에러 메시지를 출력하는 코드이다.