Springboot에서 Exception을 활용한 오류 처리

Java를 가지고 개발하는 오류 처리는 Exception을 활용하는 것이 정석이다.  개인적으로 값을 오류 체크하고 어떻게든 값을 만들어 반환하기보다는 오류가 발생하면 “오류다!” 라고 떳떳하게 선언하는 것이 좋은 방법이라고 생각한다.

RESTful API를 구현한 경우,  오류의 상태를 알려주는 가장 정석적인 방법은 HTTP Status Code를 활용하는 방법이다.  Exception을 통해서 이 응답 코드를 반환해주는 건 아주 쉽다.

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason="No candidate")
public class NotExistingCandidateException extends RuntimeException {
    public NotExistingCandidateException(String candidateEmail) {
        super(candidateEmail);
    }
}

이것과 관련해서 약간 말을 보태본다.  RESTful API를 개발하면서 응답 메시지의 Body에 상태 코드와 응답 메시지를 정의하는 경우를 왕왕본다.  RESTful 세상에서 이런 방식은 정말 안좋은 습관이다.   몇 가지 이유를 적어보면.

  1. 완전 서버 혹은 API를 만든 사람 중심적이다.  클라이언트에서 오류를 알기 위해서는 반드시 메시지를 까야한다.  호출한 쪽에서는 호출이 성공한 경우에만 처리하면 되는데 구태여 메시지를 까서 성공했는지 여부를 확인해야한다.
  2. API 클라이언트의 코드를 짜증나게 만들뿐만 아니라 일관성도 없다.  한 시스템을 만드는데 이런 자기중심적인 사람이 서넛되고, 상태 변수의 이름을 제각각 정의한다면 어떻게 되겠나?  헬이다.
  3. 표준을 따른다면 클라이언트 코드가 직관적이된다. Ajax 응답을 처리한다고 했을 때 성공은 success 루틴에서 구현하면 되고, 오류 처리는 error 구문에서 처리하면 된다.  프레임웍에서 지원해주기 때문에 코드를 작성하는 혹은 읽는 사람의 입장에서도 직관적이다.  더불어 성공/실패에 대한 또 다른 분기를 만들 필요도 없다.
  4. 이 경우이긴 하지만 Exception을 적극적으로 활용하는 좋은 습관을 가지게 된다.  코드를 작성하면서 굳이 Exception을 아끼시는 분들이 많다. 하지만 Exception은 오류 상황을 가장 명시적으로 설명해주는 좋은 도구이다.  문제 상황에서 코드의 실행을 중단시키고, 명확한 오류 복구 처리를 수행할 수 있는 일관성을 제공하기 때문에 코드의 품질을 높일 수 있다.

표준이 있으니 표준을 따르자.  이게 사려깊은 개발자의 태도다.

ResponseStatus라는 어노테이션을 사용하면 Exception이 발생했을 때, 어노테이션에 정의된 API Response Status Code로 반환된다.  이때 주의할 점은 정의한 Exception이 반드시 RuntimeException을 상속받아야 한다는 것이다.  Throwable을 상속받거나 implement 하는 경우에는 어노테이션에 의해 처리되지 않기 때문에 주의하자.

    @ExceptionHandler(UnknownAccessCodeException.class)
    public String handleUnknownAccessCodeException() {
        return "unknown";
    }

만약 Thymeleaf와 같은 UI framework을 사용해서 특정 오류 케이스가 발생했을 때 특정 뷰를 제공하고 싶다면, Controller 수준에서 ExceptionHandler 어노테이션을 활용할 수 있다.  위 예제는 Exception이 발생했을 때 unknown이라는 Thymeleaf의 뷰를 제공하라고 이야기한다.  이때도 주의할 점은 Exception은 반드시 RuntimeException을 상속해야한다는 점이다.

만약 어플리케이션 전반적으로 Exception에 대한 처리 로직을 부여하고자 한다면 ControllerAdvice 어노테이션을 활용한다.  어노테이션을 특정 클래스에 부여하면 해당 클래스에서 정의된 Exception 핸들러들이 전역으로 적용된다.

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // Nothing to do
    }
}

상세한 정보는 스프링 페이지에서 참고하면 된다.