본문 바로가기

스프링

[Spring] API Exception (With Validation)

HTTP API Exception

이전에 웹에서 웹페이지로 오류를 처리하는 것을 살펴보았다.

우리는 API json을 통해서도 Error를 처리할 수 있어야 한다.

그에 따라, API에서 오류를 처리하는 방법을 알아보려고 한다.

이를 살펴보기 위해 간단한 예제를 가져왔다.

@Getter
@AllArgsConstructor
public class Person {
    @NotBlank
    private String id;
    @NotBlank
    private String pw;
    @NotBlank
    private String pwConfirm;
}
  • 값을 필히 입력해야하는 아이디가 존재한다.
  • 값을 필히 입력해야하는 패스워드가 존재한다.
  • 값을 필히 입력해야하는 패스워드 확인 값이 존재한다.
@RestController
public class Controller1 {

    @RequestMapping("/check")
    public String check(@Validated @RequestBody Person person, BindingResult bindingResult) throws BindException {

        if (!person.getPw().equals(person.getPwConfirm())) {
            bindingResult.reject("notmatchPW", null);
        }

        if (bindingResult.hasErrors()) {
            throw new BindException(bindingResult);
        }

        return "확인되었습니다!";
    }
}
  • 위의 검증들을 만족할뿐만 아니라, pw와 pwConfirm이 같아야만 한다.

이 검증들이 하나라도 실패할 경우 에러를 반환해주어야 하는데 우리는 이를 어떻게 처리할 수 있을까?

Spring에서 API 에러 처리를 위해 제공하는 것은 3가지이다.

  • DefaultHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • ExceptionHandlerExceptionResolver

이 세 가지에 대해서 알아보고 이를 이용해 처리해보자.

DefaultHandlerExceptionResolver

이것은 스프링 내부에서 발생하는 스프링 예외를 해결한다.

우리가 파라미터를 바인딩하는 시점에 타입이 맞지 않아 발생하는 TypeMismatchException과 같은 예외를 해결해준다.

이를 직접 확인해보자.

@Getter
@AllArgsConstructor
public class Person {
    @NotBlank
    private String id;
    @NotBlank
    private String pw;
    @NotBlank
    private String pwConfirm;
    private Integer test;
}
  • 임의로 test라는 Integer 타입의 정수를 추가했다.

  • 다음과 같이 BAD_REQUEST오류를 자동으로 발생해준다.

이와 같이 타입 미스매치가 발생하면 자동으로 Spring에서 해결해준다.

ExceptionHandlerExceptionResolver

(1)@ExceptionHandler

@ExceptionHandler를 처리한다.

@ExceptionHandler(BindException.class)
public ApiValidationExceptionEntity argumentException(BindException ex, Locale locale) {
    List<ValidationEntity> ve = ex.getAllErrors().stream()
            .map(error -> new ValidationEntity(error.getCode(), messageSource.getMessage(error, locale)))
            .collect(Collectors.toList());

    return new ApiValidationExceptionEntity(HttpStatus.BAD_REQUEST, ve);
}
  • 다음과 같이 발생한 예외를 명시하고 해당 예외에 대한 메소드 처리를 진행해준다.
  • 해당 예외의 자식 예외까지 모두 잡아준다.
@ExceptionHandler
public ApiValidationExceptionEntity argumentException(BindException ex, Locale locale) {
    List<ValidationEntity> ve = ex.getAllErrors().stream()
            .map(error -> new ValidationEntity(error.getCode(), messageSource.getMessage(error, locale)))
            .collect(Collectors.toList());

    return new ApiValidationExceptionEntity(HttpStatus.BAD_REQUEST, ve);
}
  • 파라미터로 받는 예외가 실제로 핸들링하는 예외와 같다면 생략 가능하다.

(2)@ControllerAdvice

@ExceptionHandler들을 따로 모아놓아 이를 적용시켜줄 수 있다.

@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {
  • 스프링 공식 문서에 있는 @ControllerAdvice의 예제이다.
  • 해당 애노테이션을 가지고 있는 곳에만 적용 가능하다.
  • 해당 패키지 안에 있는 곳에만 적용 가능하다.
  • 특정 클래스를 지정 가능하다.
  • 생략한다면 모든 컨트롤러에 지정한다.

실제로 적용하면 다음과 같다.

@RestControllerAdvice
@AllArgsConstructor
public class ApiValidationException {

    private MessageSource messageSource;

    @ExceptionHandler
    public ApiValidationExceptionEntity argumentException(BindException ex, Locale locale) {
        List<ValidationEntity> ve = ex.getAllErrors().stream()
                .map(error -> new ValidationEntity(error.getCode(), messageSource.getMessage(error, locale)))
                .collect(Collectors.toList());

        return new ApiValidationExceptionEntity(HttpStatus.BAD_REQUEST, ve);
    }
}

  • BindException에 대해 적용한다.
  • FieldError, GlobalError를 캐치해 이를 메세지에 담아준다.
  • 메세지는 errors.properties(메세지 소스)를 이용하였다.

해당 결과를 보면 다음과 같다.

이처럼 Spring에서 우리가 원하는 규약으로 메세지를 설정할 수 있다.

하지만, HTTP 상태 코드는 200 OK로 정상적으로 된 것처럼 표기되었다.

우리는 HTTP 상태 코드를 BAD_REQUEST로 변경해주어야 하는데 이를 해결해주는 것이 ResponseStatusExceptionResolver이다.


ResponseStatusExceptionResolver

HTTP 상태 코드를 직접 지정해줄 수 있다.

@ResponseStatus를 활용한다.

@RestControllerAdvice
@AllArgsConstructor
public class ApiValidationException {

    private MessageSource messageSource;

    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiValidationExceptionEntity argumentException(BindException ex, Locale locale) {
        List<ValidationEntity> ve = ex.getAllErrors().stream()
                .map(error -> new ValidationEntity(error.getCode(), messageSource.getMessage(error, locale)))
                .collect(Collectors.toList());

        return new ApiValidationExceptionEntity(HttpStatus.BAD_REQUEST, ve);
    }
}
  • @ResponseStatus를 이용하여 BAD_REQUEST를 적용해주었다.

결과를 확인해보면 다음과 같다.

  • 400 Bad Request가 잘 적용되어있다.

이렇게 API를 통해 진행하는 경우 이와 같은 방식으로 오류를 처리할 수 있다.

이 메세지는 언제든지 커스텀하게 만들 수 있으므로 상황에 맞게 잘 처리하면 될 것 같다.

'스프링' 카테고리의 다른 글

[Spring] 파일 및 이미지 업로드  (4) 2021.10.20
[JPA] N+1 문제  (0) 2021.10.19
[Spring] 오류 페이지  (0) 2021.09.27
[Spring] 필터, 인터셉터  (0) 2021.09.26
[Spring] 세션(Session)  (0) 2021.09.25