저번주에는 응답 성공일 때의 응답 통일을 포스팅했었습니다.
이번에는 응답에 실패했을 경우, 어떻게 코드를 작성했는지 알아보겠습니다.
@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
String errorMessage = e.getConstraintViolations().stream()
.map(constraintViolation -> constraintViolation.getMessage())
.findFirst()
.orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));
return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY, request);
}
@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
Map<String, String> errors = new LinkedHashMap<>();
ex.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(ex, 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());
}
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason,
HttpHeaders headers, HttpServletRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null);
WebRequest webRequest = new ServletWebRequest(request);
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
);
}
}
@RestControllerAdvice을 쓰면 예외가 발생할 때 이 파일에서 처리를 해주게 됩니다.
@RestControllerAdvice(annotations={RestController.class})
annotations={RestController.class} 라는 것은 @RestController라고 붙여진 코드에서 예외 발생한 것을 처리하겠다는 뜻입니다.
그리고 @ExceptionHandler라고 붙은 함수들이 각 예외가 발생했을 때의 처리 방식을 나타낸 함수입니다.
//ConstraintViolationException이 발생할 때
@ExceptionHandler
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
String errorMessage = e.getConstraintViolations().stream()
.map(constraintViolation -> constraintViolation.getMessage())
.findFirst()
.orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));
return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY, request);
}
//Exception이 발생할 때
@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)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
GeneralException 부분이 바로 커스텀한 예외 처리입니다.
GeneralException 부분부터 보도록 하겠습니다.
@ExceptionHandler에서 value = GeneralException 부분을 안써줘도 되지만 눈에 잘 보이게 하기 위해 작성했습니다.
함수를 살펴보면, GeneralException에서 ErrorReasonDTO를 호출하고, 그걸 handleExceptionInternal 함수에 넣어줬습니다.
먼저 GeneralException부터 보겠습니다.
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
private BaseErrorCode code;
public ErrorReasonDTO getErrorReason() {
return this.code.getReason();
}
public ErrorReasonDTO getErrorReasonHttpStatus(){
return this.code.getReasonHttpStatus();
}
}
GeneralException에서 어떤 에러가 발생했고, code가 뭔지, 어떤 메세지를 출력해야 하는지 정보를 담고 있는 BaseErrorCode를 저장하고 있습니다.
그리고 getErrorReason과 getErrorReasonHttpStatus에서는 ErrorReasonDTO를 반환하고 있습니다.
@Getter
@Builder
@Data
public class ErrorReasonDTO {
private HttpStatus httpStatus;
private final boolean isSuccess;
private final String code;
private final String message;
}
@Data = @toString + @getter + @setter + @RequiredArgsConstructor + @EqualsAndHashCode
ErrorReasonDTO는 응답이 성공했을 때 만들었던 ReasonDTO와 형식이 같습니다. 성공 여부와 httpStatus 코드, 어떤 코드인지(ex. COMMON400 등)가 저장되어 있습니다.
// BaseErrorCode.java
public interface BaseErrorCode {
ErrorReasonDTO getReason();
ErrorReasonDTO getReasonHttpStatus();
}
BaseErrorCode는 ErrorStatus의 인터페이스입니다.
ErrorStatus에서 httpStatus랑 code, message를 지정해줍니다.
// ErrorStatus.java
@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
// 가장 일반적인 응답
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "로그인 인증이 필요합니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),
// 멤버 관련 에러
USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."),
PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "MEMBER4002", "비밀번호가 일치하지 않습니다."),
PASSWORD_NOT_MATCH_CONFIRM(HttpStatus.BAD_REQUEST, "MEMBER4003", "새비밀번호와 재입력한 새비밀번호가 일치하지 않습니다."),
USER_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4004", "이미 존재하는 사용자입니다."),
FRIEND_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4005", "이미 존재하는 친구입니다."),
FRIEND_NOT_MYSELF(HttpStatus.BAD_REQUEST, "MEMBER4006", "자기 자신과는 친구가 될 수 없습니다."),
// 토큰 관련 에러
TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "TOKEN4001", "토큰이 존재하지 않습니다."),
TOKEN_EXPIRED(HttpStatus.BAD_REQUEST, "TOKEN4002", "토큰이 만료되었습니다."),
TOKEN_MALFORM(HttpStatus.BAD_REQUEST, "TOKEN4003", "토큰이 변조되었습니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public ErrorReasonDTO getReason() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.build();
}
@Override
public ErrorReasonDTO getReasonHttpStatus() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build()
;
}
}
그럼 다시 ExceptionAdvice로 돌아와서 GeneralException이 발생했을 때, 함수를 다시 보겠습니다.
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
GeneralException에서 httpstatus, code, message, 성공 여부가 저장되어 있는 ErrorReasonDTO을 불러옵니다. 그리고 handleExceptionInternal를 호출합니다.
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason,
HttpHeaders headers, HttpServletRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null);
WebRequest webRequest = new ServletWebRequest(request);
return super.handleExceptionInternal(
e,
body,
headers,
reason.getHttpStatus(),
webRequest
);
}
ExceptionAdvice에서는 ResponseEntityExceptionHandler를 상속받고 있습니다.
ResponseEntityExceptionHandler는 스프링 예외에 대한 ExceptionHandler가 모두 구현되어 있습니다. 또한 기본적으로 에러 메세지를 반환하지 않으므로, 스프링 예외에 대한 에러 응답을 보내려면 handleExceptionInternal 메소드를 오버라이딩 해야 합니다.
여기서 통일된 응답 코드인 ApiResponse를 생성하고, ResponseEntity의 body에 이 인스턴스를 넣어주었습니다.
그렇게 되면 응답에 성공했을 때와 마찬가지로 같은 구조로 나오게 됩니다.
잘 읽고 갑니다~