Exception 핸들러 예외 컨트롤 하기

이원찬·2023년 12월 25일
0

Spring

목록 보기
4/13

API 응답 통일을 먼저 보고 오자

API는 응답을 통일한다.

에러 핸들러를 만들어 에러 API응답도 통일하고 일관성있게 구현하자

먼저 에러 관련 Code 추가 하기

@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", "이거는 테스트");
...

}

커스텀 Exception을 만든다!! (GeneralException)

@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
    private BaseErrorCode code;

    public ErrorReasonDTO getErrorReason() {
        return code.getReason();
    }

    public ErrorReasonDTO getErrorReasonHttpStatus() {
        return code.getReasonHttpStatus();
    }
}
  • 커스텀 exception은 RuntimeException을 상속받아 GeneralException을 던지면 런타임오류가 나는거랑 똑같다.
  • 롬복의 @AllArgsConstructor 를 가지고 있기에 BaseErrorCode 를 인자로 생성자를 만들어 준다.
  • BaseErrorCode를 필드로 가지고 있고 getErrorReason, getErrorReasonHttpStatus를 만들어
    필드 code에서 필요한 정보를 뽑아오는 getter를 만든다.

Handler 패키지에 Temp에 대한 핸들러 추가하기

// GeneralException을 캐치할때 
public class TempHandler extends GeneralException {
    public TempHandler(BaseErrorCode errorCode) {
        super(errorCode);
    }
}
  • 핸들러는 커스텀 Exception을 상속받았기 때문에 throw로 핸들러를 던지면 런타임 에러나는것과 똑같다!!
  • 핸들러는 BaseErrorCode 를 인자로 받는 생성자를 하나 구현해 놓는다.
  • 생성자는 부모 클래스의 생성자를 호출한다 (GeneralException의 생성자를 호출하는것!!)

에러를 캐치하는 ExceptionAdvice를 구현하자

만약

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이면 에러를 던진다!!! (밑에는 에러 캐치 순서)

  • 우리가 만든 TempHandler이다.
  • 커스텀 Exception인 GeneralException이 만들어진다.
  • RuntimeException을 상속받은 GeneralException이 생성될때 @RestControllerAdvice 어노테이션을 가지는 ExceptionAdvice클래스가 에러를 캐치한다.
  • ExceptionAdvice클래스의 ExceptionHandler 어노테이션중 GeneralException을 핸들링하는 메서드가 호출되고
  • 우리가 TempHandler를 생성할때 인자로 주었던 BaseErrorCode 에서 httpStatus를 뽑아 낸다.
  • 뽑아낸 httpStatus를 이용해 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의 부모 클래스인

ResponseEntityExceptionHandlerhandleExceptionInternal메서드에 APIResponse 객체를 넣어 호출한다.

물론 handleExceptionInternal 의 반환값은 ResponseEntity<Object> 이다!!

따라서 우리가 넣어준 APIResponse 객체를 메세지 컨버터가 직렬화 하여 json으로 뿌려 주는것!!!

profile
소통과 기록이 무기(Weapon)인 개발자

0개의 댓글