[ShareMore] 4. 예외 처리

CodeKong의 기술 블로그·2023년 12월 20일
1
post-thumbnail

들어가기전에...

이번 챕터는 난이도가 있어서 후반부에 설정해주셔도 됩니다!


🔧 예외상황에 대해 커스텀 해볼게요!

exception을 상속받는 클래스를 선언해줍니다

@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {

    private BaseErrorCode code;

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

    public ErrorReasonDTO getErrorReasonHttpStatus() {
        return this.code.getReasonHttpStatus();
    }
}

추가적으로 Validation을 해주기 위해 의존성을 추가해줍니다

implementation 'org.springframework.boot:spring-boot-starter-validation'

다음은 제일 중요한 예외를 캐치하여 각 예외상황으로 분기하는 클래스 입니다!

@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {

	//상황 1
    // ConstraintViolationException 발생시 -> @PathVariable 에 매핑되는 경로 파라미터 또는 @RequestParam 에 매핑되는 쿼리 파라미터를 검증하는데 실패했을 때 발생
    //해당 타입은 DefaultHandlerExceptionResolver 에 핸들러가 선언되어 있지 않기 때문에 HTTP Status 500 으로 처리됩니다.
    @ExceptionHandler
    public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
        String errorMessage = e.getConstraintViolations().stream()
                .map(ConstraintViolation::getMessage)
                .findFirst()
                .orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));

        return handleExceptionInternalConstraint(
                e,
                ErrorStatus.valueOf(errorMessage),
                HttpHeaders.EMPTY,
                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
        );
    }

	//상황 2
    // MethodArgumentNotValidException 발생시 -> @RequestBody 로 들어오는 객체를 검증하는데 실패했을 때 발생
    @Override
    protected 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
        );
    }

    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
        );
    }

    @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()
        );
    }

    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,
                errorCommonStatus.getHttpStatus(),
                request
        );
    }
    
	//GeneralException에 대한 예외처리
    @ExceptionHandler(value = GeneralException.class)
    public ResponseEntity<Object> handleGeneralException(GeneralException generalException, HttpServletRequest request) {
        ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
        return handleExceptionInternal(
                generalException,
                errorReasonHttpStatus,
                null
                , request
        );
    }

    private ResponseEntity<Object> handleExceptionInternal(
            Exception e, ErrorReasonDTO errorReasonDTO, HttpHeaders headers, HttpServletRequest request) {
        ApiResponse<Object> body = ApiResponse.onFailure(errorReasonDTO.getCode(), errorReasonDTO.getMessage(), null);
        WebRequest webRequest = new ServletWebRequest(request);
        return super.handleExceptionInternal(
                e,
                body,
                headers,
                errorReasonDTO.getHttpStatus(),
                webRequest
        );
    }
}

ConstraintViolationException vs MethodArgumentNotValidException

ConstraintViolationException 예외는 Bean Validation이 실패했을 때 발생하는데, 예를 들어 @NotNull이나 @Size 같은 제약 조건 어노테이션을 위반했을 때 발생합니다.
@PathVariable 에 매핑되는 경로 파라미터 또는 @RequestParam 에 매핑되는 쿼리 파라미터를 검증하는데 실패했을 때 발생합니다.

반면, MethodArgumentNotValidException예외는 @Valid 어노테이션을 사용하여 검증한 메소드의 인자가 유효하지 않을 때 발생합니다.
@RequestBody 로 들어오는 객체를 검증하는데 실패했을 때 발생합니다.


test 핸들러 작성해줄게요

public class TempHandler extends GeneralException {

    public TempHandler(BaseErrorCode code) {
        super(code);
    }

}

예외 dto 추가해 줄게요!
flag값에 따라 다른 응답을 반환할 수 있게 해보겠습니다!

public class TempResponse {

    ...

    @Getter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class TempExceptionDTO {
        Integer flag;
    }
}
public class TempConverter {

    ...


    public static TempResponse.TempExceptionDTO toTempExceptionDTO(Integer flag) {
        return TempResponse.TempExceptionDTO.builder()
                .flag(flag)
                .build();
    }
}

서비스 구현해 줍니다!

public interface TempQueryService {

    void CheckFlag(Integer flag);
}

flag 1이면 예러 반환하도록 해볼게요!

@Service
@RequiredArgsConstructor
public class TempQueryServiceImpl implements TempQueryService {

    @Override
    public void CheckFlag(Integer flag) {
        if (flag == 1) {
            throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
        }
    }
}

예외 컨트롤러 코드 추가해줍니다!

@RestController
@RequestMapping("/api/temp")
@RequiredArgsConstructor
public class TempController {

    private final TempQueryService tempQueryService;

    ...

    @GetMapping("/exception")
    public ApiResponse<TempResponse.TempExceptionDTO> getException(@RequestParam(name = "flag") Integer flag) {
        tempQueryService.CheckFlag(flag);
        return ApiResponse.onSuccess(TempConverter.toTempExceptionDTO(flag));
    }
}

성공!

0개의 댓글