이전에 URL 에 범위를 넘어가는 index 를 입력하여 request 하면, JSON 형태로 error data 가 response 된 것을 알 수 있었다.

그렇다면 Server 에 Exception Handler 를 만들어 단순히 status code 500 의 Internal Server Error 가 아닌, 더 정확한 근거와 함께 항상 일관된 형식의 error message 를 response 하도록 만들면 어떨까?

Spring REST Exception Handling

위와 같이, bad request 가 들어왔을 때, Exception Handler 가 따로 처리하도록 하여 정돈된 error message 를 JSON 의 형태로 response 하게 만들어보자. Development Process 는 다음과 같다.

  1. custom error reponse class 를 생성
  2. custom exception class 를 생성
  3. 만약 해당하는 student 가 없을 경우 exception 을 throw 하도록 REST service 를 update
  4. @ExceptionHandler annotation 을 사용하여 exception handler method 를 추가
Step 1: Create custom error response class
public class StudentErrorResponse {  
  
    private int status;  
    private String message;  
    private long timeStamp;
    ...
}

여기서는 response message 의 body 에 들어갈 내용을 define 한다. POJO 의 형태로, 원하는 field 들을 선택하여 class 를 구성한다.

Step 2: Create custom student exception
public class StudentNotFoundException extends RuntimeException {  
    public StudentNotFoundException(String message) {  
        super(message);  
    }
}

REST service 에서 해당하는 학생을 찾지 못했을 때 exception 을 throw 하게 될 건데, 이 exception 을 define 해주어야 한다. 이에 custom student exception class 를 생성하여 여기에 exception 을 define 해준다.

해당 class 는 RuntimeException 을 상속받고, super constructor 를 사용하여 parent class 의 constructor 를 사용하여 message 를 parent class 의 field 에 저장한다.

Step 3: Update REST service to throw exception
@GetMapping("/student/{studentId}")  
Student getStudent(@PathVariable int studentId) {  
    // check the studentId against list size  
    if ((studentId >= theStudents.size()) || (studentId < 0)) {  
        throw new StudentNotFoundException("Student id not found - " + studentId);  
    }  
    
    return theStudents.get(studentId);  
}

기존의 getStudent method 에 studentId 의 범위를 체크하는 logic 을 추가하여 StudentNotFoundException 을 throw 하도록 한다.

Step 4: Add exception handler method
@ExceptionHandler  
public ResponseEntity<StudentErrorResponse> handleException(StudentNotFoundException exception) {  
  
    // create a StudentErrorResponse  
    StudentErrorResponse error = new StudentErrorResponse();  
    error.setStatus(HttpStatus.NOT_FOUND.value());  
    error.setMessage(exception.getMessage());  
    error.setTimeStamp(System.currentTimeMillis());  
  
    return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);  
}

@ExceptionHandler annotation 을 사용하여 exception handler method 를 define 한다. 해당 annotation 은 특정 exception 을 처리하기 위하여 지정하는데, 여기서는 StudentNotFoundException 이 발생하였을 때 이 method 가 call 된다. 즉, parameter 의 type 에 해당하는 exception 이 발생했을 경우에만 해당 method 가 call 되는 것이다.

exception handler 는 ResponseEntity type 의 object 를 return 하는데, 이 object 를 통하여 HTTP status code, HTTP headers, Response body 를 유연하게 설정할 수 있다.

이전에 생성한 StudentErrorResponse class 의 각 field 를 setter 를 통하여 값을 설정해주고, ResponseEntity object 를 return 한다. ResponseEntity 의 내부 구현을 보면,

public class ResponseEntity<T> extends HttpEntity<T> {  
    private final HttpStatusCode status;  
    
    public ResponseEntity(@Nullable T body, HttpStatusCode status) {  
        this(body, (MultiValueMap)null, status);  
    }
    ...
}

과 같이 되어있는데, constructor 의 첫 번째 parameter 는 body 로, 두 번째 parameter 는 status 로 사용됨을 알 수 있다. 따라서, bodyStudentErrorResponse object 로, statusHttpStatus.NOT_FOUND 로 initailize 된 ResponseEntity object 가 return 된다.

REST Exception Handling - Test

http://localhost:8080/api/student/9999 으로 request 를 보내고 response 결과를 확인해보면 위와 같다. 우선 exception handler 에서 설정해준대로, body 부분에는 StudentErrorResponse object 가 JSON formate 으로 client 에 전달된 것을 볼 수 있으며, status 는 단순한 Internal Server Error 아닌 명시적으로 설정해준 HttpStatus.NOT_FOUND, 즉 404 가 전달된 것을 볼 수 있다.

혹시 studentId 에 integer 가 아닌 문자열을 넣어 request 를 보내면 어떻게 될까?

이번에는 직접 만든 exception handler 를 거치지 않고 다른 error message 가 response 되었다. 그럼 이런 corner case, 혹은 edge case 들에 대해서도 custom exception handler 를 거치게 하려면 어떻게 처리해야 할까?

REST Exception Handling - Edge Case
@ExceptionHandler  
public ResponseEntity<StudentErrorResponse> handleException(Exception exc) {  
  
    StudentErrorResponse error = new StudentErrorResponse();  
  
    error.setStatus(HttpStatus.BAD_REQUEST.value());  
    error.setMessage(exc.getMessage());  
    error.setTimeStamp(System.currentTimeMillis());  
  
    return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);  
}

방법은 추가적인 exception handler 를 하나 더 생성하는 것이다. 이 handler 에는 parameter 로 Exception class 를 받고 있으므로, 특정 exception 이 아닌 generic 한 exception 을 처리하는 데 사용된다.

error object 에도 NOT_FOUND 가 아닌 BAD_REQUEST 를 status 로 설정한다. 이렇게 되면 request 의 형식은 맞으나 해당 학생을 찾을 수 없다는 오류와, request 형식이나 data 가 유효하지 않을 경우를 나누어서 exception handling 할 수 있게 된다.

간단하게 test 해보면 body 와 status code 도 의도된대로 잘 response 된 것을 볼 수 있다.