API 응답 통일을 먼저 보고 오자
@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
...
// 멤버 관련 에러
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER40001", "사용자가 없습니다."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER40002", "닉네임은 필수 입니다."),
// 테스트 에러 코드
TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트");
...
}
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
private BaseErrorCode code;
public ErrorReasonDTO getErrorReason() {
return code.getReason();
}
public ErrorReasonDTO getErrorReasonHttpStatus() {
return code.getReasonHttpStatus();
}
}
// GeneralException을 캐치할때
public class TempHandler extends GeneralException {
public TempHandler(BaseErrorCode errorCode) {
super(errorCode);
}
}
만약
throw new TempHandler(BaseErrorCode code)
를 실행한다면 이 에러를 캐치해갈 친구가 필요하다.
일단 validation을 의존성 추가하고
//build.gradle
//validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
ExceptionAdvice 를 구현한다 (길어서 이해하기 힘들지만 만들어 놓으면 편하다 주석을 잘보자
)
// 이 어노테이션으로 RestController에서 일어나는 에러를 모두 캐치 한다.
@RestControllerAdvice(annotations = {RestController.class})
// ResponseEntityExceptionHandler를 상속받아 기본 ExceptionHandler를 커스텀 한다
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
// ExceptionHandler 어노테이션으로 특정 에러를 처리할 로직을 작성 가능하다.
@ExceptionHandler
// ConstraintViolationException을 캐치하는 메소드를 구현한다.
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
// ConstraintViolationException이 발생했을때 관련 메세지들중 하나를 골라
String errorMessage = e.getConstraintViolations().stream()
.map(constraintViolation -> constraintViolation.getMessage())
.findFirst()
.orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));
// handleExceptionInternalConstraint메소드에 넣어주면 호출한다. (밑에 구현해놨음)
return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY,request);
}
@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
Map<String, String> errors = new LinkedHashMap<>();
e.getBindingResult().getFieldErrors().stream()
.forEach(fieldError -> {
String fieldName = fieldError.getField();
String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse("");
errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage);
});
return handleExceptionInternalArgs(e,HttpHeaders.EMPTY,ErrorStatus.valueOf("_BAD_REQUEST"),request,errors);
}
@ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest request) {
e.printStackTrace();
return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),request, e.getMessage());
}
// 우리가 방금 만든 GeneralException을 캐치하는 메서드 이다.
@ExceptionHandler(value = GeneralException.class)
// generalException의 메서드인 getErrorReasonHttpStatus 를 호출하여 ErrorReasonDTO를 가져오고
public ResponseEntity<Object> onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
// ErrorReasonDTO 를 인자로 handleExceptionInternal 메소드를 호출한다 (밑에 구현되어있음)
// 또한 ResponseEntity<Object>를 반환한다.
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
// 내부에서 사용되는 handleExceptionInternal 메서드이다.
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason,
HttpHeaders headers, HttpServletRequest request) {
// 인자로 받은 ErrorReasonDTO의 code와 message필드를 이용해 ApiResponse 객체를 만든다.
ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null);
// 부모 클래스의 handleExceptionInternal 메서드를 이용하기 위해 WebRequest를 만들어주고 메서드를 호출한다.
WebRequest webRequest = new ServletWebRequest(request);
// ResponseEntity<Object>를 반환한다.
**// 우리가 만든 ApiResponse 객체를 body로 넣어 호출한다.**
return super.handleExceptionInternal(
e,
body,
headers,
reason.getHttpStatus(),
webRequest
);
}
private ResponseEntity<Object> handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint);
return super.handleExceptionInternal(
e,
body,
headers,
status,
request
);
}
private ResponseEntity<Object> handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus,
WebRequest request, Map<String, String> errorArgs) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}
private ResponseEntity<Object> handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, WebRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}
}
쿼리 스트링으로 flag가 1이면 에러를 던지는 로직을 구현 해보자
controller
@GetMapping("/exception")
public ApiResponse<TempResponse.TempExceptionDTO> exceptionAPI(@RequestParam Integer flag){
tempQueryService.CheckFlag(flag);
return ApiResponse.onSuccess(TempConverter.toTempExceptionDTO(flag));
}
TempQueryService의 CheckFlag 메서드
@Override
public void CheckFlag(Integer flag) {
if (flag == 1)
throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
}
만약 flag가 1이면 에러를 던진다!!! (밑에는 에러 캐치 순서)
@RestControllerAdvice
어노테이션을 가지는 ExceptionAdvice클래스가 에러를 캐치한다.ResponseEntityExceptionHandler
클래스의 handleExceptionInternal
메서드에 우리가 만든 APIResponse 를 바디에 넣어주어 호출하여 에러를 처리한다!!Service가 호출 될때 Service에서는 에러만 던져 줬다…
@Override
public void CheckFlag(Integer flag) {
if (flag == 1)
throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
}
TempHandler를 보자
public class TempHandler extends GeneralException {
public TempHandler(BaseErrorCode errorCode) {
super(errorCode);
}
}
지금 보면 상속받은 GeneralException
의 생성자를 호출한다. GeneralException
를 살펴보자
GeneralException
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
private BaseErrorCode code;
...
}
위 @ALLArgsConstructor
가 있기 때문에 생성자가 자동으로 생성된다!! 또한 RuntimeException
을 상속 받았기 때문에
@RestControllerAdvice
가 잡는다!!
Controller
에서 에러를 던지면 @RestControllerAdvice
가 잡는다!!
@RestControllerAdvice
를 살펴보자
@RestControllerAdvice
ExceptionAdvice
@RestControllerAdvice(annotations = RestController.class)
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
...
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
...
}
GeneralException
이 난 @ExceptionHandler
에서 캐치 한다!!
GeneralException
의 메소드인 getErrorReasonHttpStatus()를 호출하여 ErrorReasonDTO
를 생성한다!!
물론 getErrorReasonHttpStatus메서드는 GeneralException
의 필드인 code에서 httpStatus를 빼오는 것이다.
**ErrorStatus implements BaseErrorCode**
@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
...
TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트");
...
@Override
public ErrorReasonDTO getReasonHttpStatus() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build()
;
}
}
ExceptionAdvice 에서 마지막으로 handleExceptionInternal 메서드를 호출한다. 살펴보자
handleExceptionInternal 메서드
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason,
HttpHeaders headers, HttpServletRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null);
// e.printStackTrace();
WebRequest webRequest = new ServletWebRequest(request);
return super.handleExceptionInternal(
e,
body,
headers,
reason.getHttpStatus(),
webRequest
);
}
APIResponse
객체를 여기서 만들고 ExceptionAdvice
의 부모 클래스인
ResponseEntityExceptionHandler
의 handleExceptionInternal
메서드에 APIResponse
객체를 넣어 호출한다.
물론 handleExceptionInternal
의 반환값은 ResponseEntity<Object>
이다!!