[Spring] 스프링 예외처리 (@ExceptionHandler, @RestControllerAdvice)
지금까지 작성한 샘플 프로젝트 코드들에 예외 처리를 적용해본다.
예외를 처리하는 적절한 방법과, 해당 예외에 대한 적절한 메세지를
클라이언트 쪽에 어떻게 알려줄 수 있는지 배우게 되었다.
올바른 방식(코드 중복X, 역할 분리O)으로 코드가 수정된 흐름에 따라 정리하고,
최종적으로는 사용된 어노테이션과 역할별 작성된 클래스들에 대해서도 정리했다.
예외를 처리해주는 이유?
예외를 처리해주지 않은 코드의 경우, 클라이언트 측에서
Response Body 내용만으로는 어떤 항목이 유효성 검증에 실패한 것인지 알 수가 없다.
유효성 검증 뿐만 아니라 어플리케이션의 규모가 커지면 커질 수록
비즈니스 로직에서 던져지는 의도된 예외, 웹 애플리케이션 실행중 발생하는 RuntimeException 예외 등
너~무나도 다양한 예외가 발생할 수 있어
'어디서' '어떻게' 예외가 일어나는 것인지에 대한 표시가 필요하다.
클라이언트가 에러 메세지를 조금 더 친절하게 알아 볼 수 있게 하는 것이 바로 예외 처리!
* Spring에서의 예외는 애플리케이션의 문제 발생시 문제를 알려서 처리하는 것 뿐만 아니라,
유효성 검증 실패 같이 실패를 하나의 예외로 간주한다!
이 예외를 던져서(throw) 예외 처리를 유도함.
코드 수정 흐름에 따른 정리
샘플 코드에서 예외 처리 필요했던 부분들
1. DTO 데이터에 대한 유효성 검증 실패시
2. URI 변수에 대한 유효성 검증 실패시
1.
Member Controller에 직접
@ExceptionHandler 달아서 적용해준다.
(클라이언트가 핸들러 메서드에 요청을 전송하면, 유효하지 않은 데이터 발견시 exception 발생한다.
여기서 해당하는 @ExceptionHandler 달린 메서드로 전달받아서 에러 정보 뱉어준다.
에러 메세지가 전송되지만 지나치게 구체적이다.
⬇
2.
에러메세지에 정보가 너무 많아서 필요한 것만 받으려고 한다.
ErrorResponse 클래스를 만들어서 필요한 정보만 담아줄 수 있도록 한다.
(속에 static class인 클래스 만들어서 포함해줌)
➡ 만든 ErrorResponse 클래스를
컨트롤러의 @ExceptionHandler에 적용해 주었다.
이때 stream 이용해서 필요한 정보만 선택적으로 골라서 list 변환후,
얘를 ResponseEntity에 실어서 전달해준다.
⬇
3.
필요한 메세지만 잘 나오긴 했지만,
그럼 모든 컨트롤러에 exception별로 @ExceptionHandler 일일히 달아서 만들어줘야하나?
중복 코드도 발생하고, 코드도 너무 길어진다.
다양한 유형의 예외를 처리하기에는 적절하지 않은 방식이다!
⬇
4.
이 경우에 예외 처리를 공통화 해줄 수 있었던 것이
@RestControllerAdvice !!!
이 어노테이션을 추가하면
여러개의 컨트롤러에서 @ExceptionHandler, @InitBinder, @ModelAttribute 추가된 메서드 공유해서 사용할 수 있다.
어노테이션 적용을 위해 컨트롤러의 @ExceptionHandler를 모두 삭제하고,
⬇
5. @RestControllerAdvice 달아줄
GlobalExceptionAdvice 클래스 만들어준다. ( = 컨트롤러에 작성할 오류메서드들 전부 여기서 처리하겠다!)
➡ controller 클래스에서 발생하는 예외를 도맡아서 처리하게 된다.
여기다가 @ExceptionHandler 코드들 고대로 옮겨주면,
1번(DTO 유효성 검사) 처리는 완료가 된다.
⬇
이후에 2번인 URI 유효성 검사 예외를 처리하기 위해 ErrorResponse 클래스를 수정하고,
이 수정된 ErrorResponse 클래스의 메서드를 사용하도록 GlobalExceptionAdvice를 수정한다.
결론적으로
Error Response를 만드는 역할을 ErrorResponse 클래스가 모두 하기 때문에,
에러 정보를 담는 역할이 명확히 분리되며, 코드가 간결해졌다.
(+)
ErrorResponse 객체에서는 ResponseEntity로 래핑해서 리턴을 해줬었는데,
GlobalExceptionAdvice에서는 그냥 ErrorResponse 객체를 바로 리턴해주고 있다.
@RestControllerAdvice = @ControllerAdvice + @ResponseBody 이기 때문이다!!
(따라서 JSON으로 만들기 위해 ResponseEntity로 래핑해줄 필요가 없음! ResponseBody를 포함하고 있어서~_~)
또한
@ResponseStatus : HTTP Status 대신 표현할 수 있다.
@어노테이션, 클래스별 정리
오류처리를 위해 생성된 클래스는
1. GlobalExceptionAdvice
2. ErrorResponse
이 두 클래스이다.
결론적으로 컨트롤러에는 아무런 오류 처리 코드 없어도 된다!
컨트롤러는 api
1. GlobalExceptionAdvice
@RestControllerAdvice 어노테이션을 달고 있다.
예외 처리를 해주고 있는데,
이 클래스 안에서 각각의 exception에 해당하는 메서드에 @ExceptionHandler가 달려있다.
@ResponseStatus로 HTTP status 확인할 수 있음
@RestControllerAdvice
public class GlobalExceptionAdvice{
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST){
public 예외처리 1(
)
final ErrorResponse response = ~~
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST){
public 예외처리 2(
)
final ErrorResponse response = ~~
return response;
}
...
}
GlobalExceptionAdvice 코드가 깔끔한 이유?
바로 ErrorResponse 클래스를 만들어서 여기서 Error Response 정보를 만들기 때문!!
2. ErrorResponse
@Getter
public class ErrorResponse{
private ErrorResponse(예외처리1, 예외처리2){
this.예외처리1 = 예외처리1;
...
//생성자 주입
}
@Getter
public static class 예외처리1{
...
}
@Getter
public static class 예외처리2{
...
}
}
@RestControllerAdvice는 @ControllerAdvice + @ResponseBody 이다.
이미 json 값으로 받아오기 때문에 ErrorResponse 객체를 ResponseEntity로 묶어줄 필요가 없다.
ErrorResponse에서 원하는 값만 가져올 수 있도록 해놓았다!
이렇게 두 클래스를 작성함으로서
컨트롤러는 api의 요청만 깔끔하게 받아올 수 있고,
GlobalException 클래스를 통해 공통된 예외 처리를 받아올 수 있으며, (@RestControllerAdvice)
받아올 Error 정보의 선택적 데이터를 ErrorResponse로 처리할 수 있다!