📘Exception Handler
예외 발생 가능성이 있는 상황
- 클라이언트 요청 데이터에 대한 유효성 검증에서 발생하는 예외
- 서비스 계층의 비즈니스 로직에서 던져지는 의도된 예외
- 런타임 예외
UserController에 Exception Handler 적용
- RequestBody에 유효하지않은 데이터 요청이 오면, 유효성 검증에 실패
- 유효성 검증에 실패시 내부적으로 던져진 예외인 MethoddArgumentNotValidException을 handleException() 이 받음
- 그 후, MethodArgumentNotValidException 개체에서 getBindingResult(), getFieldErrors() 를 통해,
발생한 에러 정보를 리스트로 확인 가능 - 객체에서 얻은 정보인 filedErrors를 ResponseEntity를 통해 ResponseBody로 전달
UserController에 ExceptionHandler 적용
HTTP Request를 보내 ExceptionHandler 적용 확인
[
{
"codes": [
"Email.userPostDto.email",
"Email.email",
"Email.java.lang.String",
"Email"
],
"arguments": [
{
"codes": [
"userPostDto.email",
"email"
],
"arguments": null,
"defaultMessage": "email",
"code": "email"
},
[],
{
"arguments": null,
"defaultMessage": ".*",
"codes": [
".*"
]
}
],
"defaultMessage": "올바른 형식의 이메일 주소여야 합니다",
"objectName": "userPostDto",
"field": "email",
"rejectedValue": "asdad23@",
"bindingFailure": false,
"code": "Email"
}
]
하지만, 반환값에는 알수없는 정보를 전부 포함한 내용은 제외하고 싶다.
ErrorResponse 클래스를 따로 만들어서 원하는 출력정보만 나오게 해보자.
- 유효성 검증 실패 시, 실패한 필드에대한 Error정보만 담아서 응답으로 전송하기 위한 클래스
- 위의 Exception 정보를 리턴받은 값을 보면 배열로 되어있다.
유효성 검증에 실패하는 멤버 변수가 하나 이상이 될 수 있기 때문. - 그렇기 때문에, 유효성 검증에 실패한 에러 정보를 담기위해 List객체를 이용하며, 이 한개의 필드 에러 정보는
Error_Detail 이라는 별도의 static class를 Error클래스의 멤버 클래스로 정의
Error는 에러 정보만 담는 클래스이기 때문에 필드의 에러 정보를 담는 Error_Detail클래스 역시,
에러라는 공통 관심사를 가지고 있으므로 Error의 멤버로 표현하는것이 적절하다.
공통 관심사를 처리할 Error 클래스 작성
UserController 내부 ExceptonHandler 수정 -> Error 객체를 응답으로 전송
원하는 에러 정보만 표시
Exception Handler의 단점
@ExceptionHandler Annotation 사용 시 문제점
- 각각의 컨트롤러에서 @ExceptionHandler를 사용해야 하므로 코드중복 발생
- 컨트롤러에서 Validation에 대한 예외뿐 아니라 여러가지 예외 처리를 해야하므로 핸들러 메소드의 증가
- 해결법은? RestControllerAdvice를 사용한 예외 처리 공통화
📘RestControllerAdvice를 사용한 예외 처리 공통화
어떤 클래스에 @RestControllerAdvice를 추가하면 여러개의 컨트롤러에서
@ExceptionHanndler, @InitBinder, @ModelAttribute가 추가된 메소드를 공유해서 사용 가능
ApplyExceptionAdviceAllControllers 클래스 생성하여 공통 처리
Throw Exception
예외의 구분
- 어플리케이션의 발생예외는 크게 체크예외, 언체크예외로 구분
- 체크예외 : 발생한 예외를 잡고(catch) 체크 후, 예외 복구&회피 등 구체적인 처리 필요
- ex: ClassNotFoundException
- 언체크예외 : 예외를 잡고(catch) 따로 처리가 필요하지 않음
- ex: NullPointerException, ArrayIndexOutOfBoundsException
- 휴먼에러로 인해 발생하는 오류는 전부 RuntimeException을 상속한 예외들임
- 자바는 많은 RuntimeException을 지원하지만 직접 예외를 만들어야 하는 경우도 있음
의도적으로 예외 throw & catch 하기
- Custom Exception(enum) 생성
- 특정 계층에서 사용할 클래스 생성 후 생성자에 CustomException을 파라미터로 사용
- ExceptionAdvice에 @ExceptionHandler를 사용하는 메소드 추가 작성
- 예외가 발생할만한 서비스에 throw new Custom_Exception(Custom_enum.Method) 작성하여 throw해주기
- 던진 예외를 예외처리를 담당하는 클래스에 던진 예외를 잡기위한 @ExceptionHandler 메소드 생성
📘GlobalExceptionAdvice구현
GlobalExceptionAdvice 클래스
처리된 에러를 Global로 한곳에 받아 적절한 응답으로 전달
@Slf4j
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleConstraintViolationException(
ConstraintViolationException e) {
final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
return response;
}
@ExceptionHandler
public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
final ErrorResponse response = ErrorResponse.of(e.getExceptionCode());
return new ResponseEntity<>(response, HttpStatus.valueOf(e.getExceptionCode()
.getStatus()));
}
@ExceptionHandler
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ErrorResponse handleHttpRequestMethodNotSupportedException(
HttpRequestMethodNotSupportedException e) {
final ErrorResponse response = ErrorResponse.of(HttpStatus.METHOD_NOT_ALLOWED);
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleHttpMessageNotReadableException(
HttpMessageNotReadableException e) {
final ErrorResponse response = ErrorResponse.of(HttpStatus.BAD_REQUEST,
"Required request body is missing");
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMissingServletRequestParameterException(
MissingServletRequestParameterException e) {
final ErrorResponse response = ErrorResponse.of(HttpStatus.BAD_REQUEST,
e.getMessage());
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleException(Exception e) {
log.error("# handle Exception", e);
/*
* TODO 애플리케이션의 에러는 에러 로그를 로그에 기록하고, 관리자에게 이메일이나 카카오 톡,
* 슬랙 등으로 알려주는 로직이 있는게 좋음.
*/
final ErrorResponse response = ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
return response;
}
}
BusinessLogicException 클래스
적절한 Custom 상태코드인 ExcetionCode 반환
public class BusinessLogicException extends RuntimeException {
@Getter
private ExceptionCode exceptionCode;
public BusinessLogicException(ExceptionCode exceptionCode) {
super(exceptionCode.getMessage());
this.exceptionCode = exceptionCode;
}
}
ExceptionCode 클래스
Custom 상태코드
public enum ExceptionCode {
MEMBER_NOT_FOUND(404, "Member not found"),
MEMBER_EXISTS(409, "Member exists"),
COFFEE_NOT_FOUND(404, "Coffee not found"),
COFFEE_CODE_EXISTS(409, "Coffee Code exists"),
ORDER_NOT_FOUND(404, "Order not found"),
CANNOT_CHANGE_ORDER(403, "Order can not change"),
NOT_IMPLEMENTATION(501, "Not Implementation"),
INVALID_MEMBER_STATUS(400, "Invalid member status");
@Getter
private int status;
@Getter
private String message;
ExceptionCode(int status, String message) {
this.status = status;
this.message = message;
}
}
ErrorResponse 클래스
직접적인 에러 처리 클래스, 용도벌 of 메소드 사용으로 특정 Exception별 에러 처리
@Getter
public class ErrorResponse {
private int status;
private String message;
private List<FieldError> fieldErrors;
private List<ConstraintViolationError> violationErrors;
private ErrorResponse(int status, String message) {
this.status = status;
this.message = message;
}
private ErrorResponse(final List<FieldError> fieldErrors,
final List<ConstraintViolationError> violationErrors) {
this.fieldErrors = fieldErrors;
this.violationErrors = violationErrors;
}
public static ErrorResponse of(BindingResult bindingResult) {
return new ErrorResponse(FieldError.of(bindingResult), null);
}
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
return new ErrorResponse(null, ConstraintViolationError.of(violations));
}
public static ErrorResponse of(ExceptionCode exceptionCode) {
return new ErrorResponse(exceptionCode.getStatus(), exceptionCode.getMessage());
}
public static ErrorResponse of(HttpStatus httpStatus) {
return new ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase());
}
public static ErrorResponse of(HttpStatus httpStatus, String message) {
return new ErrorResponse(httpStatus.value(), message);
}
@Getter
public static class FieldError {
private String field;
private Object rejectedValue;
private String reason;
private FieldError(String field, Object rejectedValue, String reason) {
this.field = field;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<FieldError> of(BindingResult bindingResult) {
final List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors();
return fieldErrors.stream()
.map(error -> new FieldError(
error.getField(),
error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
error.getDefaultMessage()))
.collect(Collectors.toList());
}
}
@Getter
public static class ConstraintViolationError {
private String propertyPath;
private Object rejectedValue;
private String reason;
private ConstraintViolationError(String propertyPath, Object rejectedValue, String reason) {
this.propertyPath = propertyPath;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<ConstraintViolationError> of(Set<ConstraintViolation<?>> constraintViolations) {
return constraintViolations.stream()
.map(constraintViolation -> new ConstraintViolationError(
constraintViolation.getPropertyPath().toString(),
constraintViolation.getInvalidValue().toString(),
constraintViolation.getMessage()))
.collect(Collectors.toList());
}
}
}
'📘 Backend > Spring' 카테고리의 다른 글
JPA & Hibernate 개념 (0) | 2023.04.03 |
---|---|
Aggregate & JDBC (0) | 2023.04.03 |
Service Layer (0) | 2023.04.03 |
정규표현식 (Regex) (0) | 2023.04.02 |
Data Transfer Object (DTO) (0) | 2023.04.02 |