[codestates] @exceptionhandler, @restcontrolleradvice
@ExceptionHandler를 이용한 예외 처리
ExceptionHandler 를 통해서 Controller 레벨에서의 예외 처리를 해보겠습니다.
@RestController
@RequestMapping("/v6/members")
@Validated
@Slf4j
public class MemberControllerV6 {
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
Member member = mapper.memberPostDtoToMember(memberDto);
Member response = memberService.createMember(member);
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response),
HttpStatus.CREATED);
}
@ExceptionHandler
public ResponseEntity handleException(MethodArgumentNotValidException e) {
// (1)
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
// (2)
return new ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);
}
MethodArgumentNotValidException
예외가 발생하면 ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);
를 리턴하게 됩니다. 이 때 메세지는 아래와 같습니다.
[
{
"codes": [
"Email.memberPostDto.email",
"Email.email",
"Email.java.lang.String",
"Email"
],
"arguments": [
{
"codes": [
"memberPostDto.email",
"email"
],
"arguments": null,
"defaultMessage": "email",
"code": "email"
},
[],
{
"arguments": null,
"defaultMessage": ".*",
"codes": [
".*"
]
}
],
"defaultMessage": "올바른 형식의 이메일 주소여야 합니다",
"objectName": "memberPostDto",
"field": "email",
"rejectedValue": "hgd@",
"bindingFailure": false,
"code": "Email"
}
]
이 코드는 너무 길고 불필요한 정보가 많기 때문에 응답값으로 적절하지 않습니다. 따라서 아래와 같이 ErrorResponse 를 만들어줄 수 있습니다.
@Getter
@AllArgsConstructor
public class ErrorResponse {
private List<FieldError> fieldErrors;
@Getter
@AllArgsConstructor
public static class FieldError {
private String field;
private Object rejectedValue;
private String reason;
}
}
ErrorResponse 는 내부 클래스인 FieldError 를 리스트형태로 갖는 필드값 fieldErrors 가 있습니다.
이제 ErrorResponse 이용하여 ExceptionHandler 를 수정해보겠습니다.
@ExceptionHandler
public ResponseEntity handleException(MethodArgumentNotValidException e) {
// (1)
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
// (2)
List<ErrorResponse.FieldError> errors =
fieldErrors.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getRejectedValue(),
error.getDefaultMessage()))
.collect(Collectors.toList());
return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
}
이번엔 응답값이 아래와 같이 ErrorResponse 에 설정된 필요한 정보만 나가는 걸 볼 수 있습니다.
[
"fieldErrors": {
{
"field": "email",
"rejectedValue": "hgd@",
"reason": "올바른 형식의 이메일 주소여야 합니다."
},
{
"field": "name",
"rejectedValue": "",
"reason": "이름은 공백이 아니어야 합니다."
}
]
@RestControllerAdvice를 이용한 예외처리
@RestControllerAdvice 를 통해서 예외처리를 전역적으로 할 수 있습니다.
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleConstraintViolationException(
ConstraintViolationException e) {
final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
return response;
}
}
코드가 굉장히 깔끔해졌습니다. 하나하나 보겠습니다.
- @RestControllerAdvice : ControllerAdvice 에 @ResponseBody 기능을 포함하기 때문에 ResponseEntity 로 데이터를 래핑할 필요가 없습니다.
- @ExceptionHandler : ExceptionHandler 입니다. 전역적으로 처리됩니다.
- ` @ResponseStatus(HttpStatus.BAD_REQUEST)` 해당 응답값에 HTTP Status 를 포함해줍니다.
이제 ErrorResponse 를 보겠습니다.
@Getter
public class ErrorResponse {
private List<FieldError> fieldErrors; // (1)
private List<ConstraintViolationError> violationErrors; // (2)
// (3)
private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
this.fieldErrors = fieldErrors;
this.violationErrors = violationErrors;
}
// (4) BindingResult에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(BindingResult bindingResult) {
return new ErrorResponse(FieldError.of(bindingResult), null);
}
// (5) Set<ConstraintViolation<?>> 객체에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
return new ErrorResponse(null, ConstraintViolationError.of(violations));
}
// (6) Field Error 가공
@Getter
public static class FieldError {
private String field;
private Object rejectedValue;
private String reason;
private FieldError(String field, Object rejectedValue, String reason) {
this.field = field;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<FieldError> of(BindingResult bindingResult) {
final List<org.springframework.validation.FieldError> fieldErrors =
bindingResult.getFieldErrors();
return fieldErrors.stream()
.map(error -> new FieldError(
error.getField(),
error.getRejectedValue() == null ?
"" : error.getRejectedValue().toString(),
error.getDefaultMessage()))
.collect(Collectors.toList());
}
}
// (7) ConstraintViolation Error 가공
@Getter
public static class ConstraintViolationError {
private String propertyPath;
private Object rejectedValue;
private String reason;
private ConstraintViolationError(String propertyPath, Object rejectedValue,
String reason) {
this.propertyPath = propertyPath;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<ConstraintViolationError> of(
Set<ConstraintViolation<?>> constraintViolations) {
return constraintViolations.stream()
.map(constraintViolation -> new ConstraintViolationError(
constraintViolation.getPropertyPath().toString(),
constraintViolation.getInvalidValue().toString(),
constraintViolation.getMessage()
)).collect(Collectors.toList());
}
}
}
MethodArgumentNotValidException 과 ConstraintViolationException 에 따라서 값을 다르게 적용시켜 보내야하기 때문에 각각 만들어줬습니다.
댓글남기기