• Controller doesn’t handle the exception. It just lets it “fly past.”
    • Your GlobalExceptionHandler is like a safety net that catches it at the very end.

The Journey of an Unchecked Exception 🚀

Think of it as a ball being thrown upwards:

  1. Thrown: Your service (e.g., toBinaryContent) throws your custom FileAccessException.
  2. Travels: The exception travels “up” through the call stack. It leaves the service and goes into your controller.
  3. Ignored by Controller: Your controller code doesn’t have a try-catch block for it. It doesn’t know what to do, so it just lets the exception continue flying upwards, out of the controller.
  4. Caught by the Global Handler: Spring sees an unhandled exception has escaped a controller and says, “Whoa, I need to find a handler for this!” It then looks at your @RestControllerAdvice (GlobalExceptionHandler) class.
  5. Handled: Your GlobalExceptionHandler looks at the type of exception. It finds the @ExceptionHandler method that matches and uses it to create a nice JSON error response. The controller’s job is to handle the happy path. The GlobalExceptionHandler’s job is to handle the unhappy paths.

The Big Picture

  • You use an unchecked exception so it can fly past the controller without being caught.
  • @Transactional uses the unchecked exception as a signal to ROLLBACK the database transaction.
  • The controller ignores the exception because handling system errors isn’t its job.
  • The GlobalExceptionHandler acts as a final safety net, catches the exception, and provides a clean error response to the client.

과제

@RestControllerAdvice  
public class GlobalExceptionHandler {  
  
  @ExceptionHandler(NoSuchElementException.class)  
  public ResponseEntity<ErrorResponse> handleException(NoSuchElementException e) {  
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND.value(), e.getMessage());  
    return ResponseEntity  
        .status(HttpStatus.NOT_FOUND)  
        .body(errorResponse);  
  }  
  
  @ExceptionHandler(IllegalArgumentException.class)  
  public ResponseEntity<ErrorResponse> handleException(IllegalArgumentException e) {  
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage());  
    return ResponseEntity  
        .status(HttpStatus.BAD_REQUEST)  
        .body(errorResponse);  
  }  
  
  @ExceptionHandler(Exception.class)  
  public ResponseEntity<String> handleException(Exception e) {  
    return ResponseEntity  
        .status(HttpStatus.INTERNAL_SERVER_ERROR)  
        .body(e.getMessage());  
  }  
  
  @ExceptionHandler(FileAccessException.class)  
  public ResponseEntity<String> handleFileAccessException(FileAccessException e) {  
    return ResponseEntity  
        .status(e.getStatus())  
        .body(e.getMessage());  
  }  
}
  • Used to return ResponseEntity<String> instead of ResponseEntity<ErrorResponse>
    • changed in order to return JSON
  • @ResponseStatus vs. ResponseEntity
    • @ResponseStatus: Simpler and cleaner for cases where the HTTP status is always the same for a given exception.
    • ResponseEntity: More powerful and flexible. You should use it when you need to dynamically set the status code or add custom headers.

ErrorResponse

@Getter  
public class ErrorResponse {  
  
  private int status;  
  private String message;  
  private long timestamp;  
  
  public ErrorResponse(int status, String message) {  
    this.status = status;  
    this.message = message;  
    this.timestamp = System.currentTimeMillis();  
  }   
}