이전에 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 둜 μ‚¬μš©λ¨μ„ μ•Œ 수 μžˆλ‹€. λ”°λΌμ„œ, body λŠ” StudentErrorResponse object 둜, status λŠ” HttpStatus.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 된 것을 λ³Ό 수 μžˆλ‹€.