코드스테이츠 백엔드 부트캠프 45일차 - [Spring MVC] 예외 처리2

wish17·2023년 2월 17일
0
post-thumbnail

Daily Coding 24번

부분집합인지 여부를 리턴하라.

    public boolean isSubsetOf(int[] base, int[] sample) {
        boolean result = true;
        List<Integer> baseList = Arrays.stream(base) // Array를 stream으로 변환
                .boxed() // primitive(기본형) 타입을 wrapper 타입으로 박싱하여 반환
                .collect(Collectors.toList()); // stream을 List로 변환

        for(int o : sample){
            if(!baseList.contains(o)) {
                result = false;
                break;
            }
        }

        return result;
    }

[Spring MVC] 예외 처리

비즈니스 로직에대한 예외 처리

비즈니스적인 예외 던지기(throw) 및 예외 처리

개념정리

체크 예외

  • 발생한 예외를 잡아서(catch) 체크한 후에 해당 예외를 복구 하든가 아니면 회피 하든가 등의 어떤 구체적인 처리를 해야 하는 예외

언체크 예외

  • 잡아서(catch) 해당 예외에 대한 어떤 처리를 할 필요가 없는 예외
  • ex) NullPointerException, ArrayIndexOutOfBoundsException
    코드를 잘못 작성해서 발생하는 RuntimeException을 상속한 예외들
  • RuntimeException을 이용해서 일부러 예외(Exception)를 만들어야 할 경우도 있다.

@ResponseStatus = 고정된 Exception(예외)를 처리할 경우에 사용

ResponseEntity = 다양한 유형의 Exception(예외)을 처리하고자 할 경우에 사용

Custom Exception(예외) 만들기

  1. 예외 상수를 정의
  • Custom Exception에 사용할 ExceptionCode를 enum으로 정의
import lombok.Getter;

public enum ExceptionCode { // Custom 예외 정의하기 - 예외 멘트(상수) 정의
    MEMBER_NOT_FOUND(404, "Member Not Found");
    @Getter
    private int status;
    @Getter
    private String message;

    ExceptionCode(int status, String message) {
        this.status = status;
        this.message = message;
    }
}
  1. Custom Exception 정의
import lombok.Getter;

public class BusinessLogicException extends RuntimeException{ // Custom Exception 정의
    @Getter
    private ExceptionCode exceptionCode;

    public BusinessLogicException(ExceptionCode exceptionCode) {
        super(exceptionCode.getMessage());
        this.exceptionCode = exceptionCode;
    }
}

이렇게 만든 Custom Exception을 아래와 같이 사용하면 된다.

@Service
public class MemberService {
	
    ~~~
    
    public Member findMember(long memberId) {
		
        ~~~
        
        if(a==0 ~~~)
        throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
    }

}
@RestControllerAdvice
public class GlobalExceptionAdvice {
	
    ~~~
    
    @ExceptionHandler
    public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {

        return new ResponseEntity(HttpStatus.valueOf(e.getExceptionCode().getStatus()));
    }
}

실습

ExceptionCode 클래스

  • 커스텀 예외 상수 정의
import lombok.Getter;

public enum ExceptionCode {
    MEMBER_NOT_FOUND(404, "Member Not Found"),
    METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
    INTERNAL_SERVER_ERROR(500, "Internal Server Error");

    @Getter
    private int status;

    @Getter
    private String message;

    ExceptionCode(int code, String message) {
        this.status = code;
        this.message = message;
    }
}

BusinessLogicException 클래스

  • 커스텀 예외 정의
import lombok.Getter;

public class BusinessLogicException extends RuntimeException {
    @Getter
    private ExceptionCode exceptionCode;

    public BusinessLogicException(ExceptionCode exceptionCode) {
        super(exceptionCode.getMessage());
        this.exceptionCode = exceptionCode;
    }
}

ErrorResponse 클래스

  • 예외별 응답을 간략화하는 클래스
@Getter
public class ErrorResponse {
    private List<FieldError> fieldErrors;
    private List<ConstraintViolationError> violationErrors;
    private ServiceError serviceErrors;
    private MethodNotAllowed methodErrors;
    private ExceptionError exceptionError;

    public ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors, ServiceError serviceErrors) {
        this.fieldErrors = fieldErrors;
        this.violationErrors = violationErrors;
        this.serviceErrors = serviceErrors;
    }

//    public ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors, List<MethodNotAllowed> methodErrors) {
//        this.fieldErrors = fieldErrors;
//        this.violationErrors = violationErrors;
//        this.methodErrors = methodErrors;
//    } // 생성자 매개변수 갯수 똑같이 오버로딩하면 안됨

    public ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors, ServiceError serviceErrors, MethodNotAllowed methodErrors) {
        this.fieldErrors = fieldErrors;
        this.violationErrors = violationErrors;
        this.serviceErrors = serviceErrors;
        this.methodErrors = methodErrors;
    }

    public ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors, ServiceError serviceErrors, MethodNotAllowed methodErrors, ExceptionError exceptionError) {
        this.fieldErrors = fieldErrors;
        this.violationErrors = violationErrors;
        this.serviceErrors = serviceErrors;
        this.methodErrors = methodErrors;
        this.exceptionError = exceptionError;
    }

    public static ErrorResponse of(BindingResult bindingResult) {
        return new ErrorResponse(FieldError.of1(bindingResult), null, null);
    }

    public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
        return new ErrorResponse(null, ConstraintViolationError.of2(violations), null);
    }

    public static ErrorResponse of(ExceptionCode exceptionCode){
       // return new ErrorResponse(null, null, new ArrayList<>((Collection) new ServiceError(exceptionCode.getStatus(), exceptionCode.getMessage())));
        return new ErrorResponse(null, null, ServiceError.of3(exceptionCode));
    }

    public static ErrorResponse of(HttpRequestMethodNotSupportedException e){
        return new ErrorResponse(null, null, null, MethodNotAllowed.of4(e));
    }

    public static ErrorResponse of(Exception e){
        return new ErrorResponse(null, null, null, null, ExceptionError.of5(e));
    }



    @Getter
    private 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;
        }

        private static List<FieldError> of1(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
    private 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;
        }

        private static List<ConstraintViolationError> of2(
                Set<ConstraintViolation<?>> constraintViolations) {
            return constraintViolations.stream()
                    .map(constraintViolation -> new ConstraintViolationError(
                            constraintViolation.getPropertyPath().toString(),
                            constraintViolation.getInvalidValue().toString(),
                            constraintViolation.getMessage()
                    )).collect(Collectors.toList());
        }
    }

    @Getter
    private static class ServiceError{
        private int status;
        private String message;

        public ServiceError(int status, String message) {
            this.status = status;
            this.message = message;

        }
        
        
        private static ServiceError of3(ExceptionCode exceptionCode){

            return new ServiceError(exceptionCode.getStatus(), exceptionCode.getMessage());

        }

    }

    @Getter
    private static class MethodNotAllowed{
        private int status;
        private String message;

        public MethodNotAllowed(int status, String message) {
            this.status = status;
            this.message = message;
        }

        private static MethodNotAllowed of4(HttpRequestMethodNotSupportedException e){
            return new MethodNotAllowed(ExceptionCode.METHOD_NOT_ALLOWED.getStatus(), ExceptionCode.METHOD_NOT_ALLOWED.getMessage());
        }
    }

    @Getter
    private static class ExceptionError{
        private int status;
        private String message;

        public ExceptionError(int status, String message) {
            this.status = status;
            this.message = message;
        }

        private static ExceptionError of5(Exception e){
            return new ExceptionError(ExceptionCode.INTERNAL_SERVER_ERROR.getStatus(), ExceptionCode.INTERNAL_SERVER_ERROR.getMessage());
        }
    }
}

아래 캡쳐본과 같이 발생하지 않은 예외도 확인가능하게 하려고 위와 같이 코드를 작성했지만 새로운 타입의 예외가 추가됨에 따라 생성자, 메서드, 멤버클래스 등 추가되는 코드의 양이 너무 많아진다.

멤버클래스를 사용하지 않으면 더 간단하게 메소드만으로 구현가능할 것 같다.

GlobalExceptionAdvice 클래스

  • 예외별 응답을 클라이언트에 보내주는 클래스
@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) {
        System.out.println(e.getExceptionCode().getStatus());
        System.out.println(e.getMessage());

        final ErrorResponse response = ErrorResponse.of(e.getExceptionCode());
        return new ResponseEntity<>(response, HttpStatus.valueOf(e.getExceptionCode().getStatus()));
    }

    @ExceptionHandler
    public ResponseEntity handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e){
        final ErrorResponse response = ErrorResponse.of(e);

        return new ResponseEntity(response, HttpStatus.METHOD_NOT_ALLOWED);
    }

    @ExceptionHandler
    public ResponseEntity handleException(Exception e){
        final ErrorResponse response = ErrorResponse.of(e);

        return new ResponseEntity(response, HttpStatus.METHOD_NOT_ALLOWED);
    }
}

결과

없는 멤버요소를 찾으려 할 때

잘못된 형식의 요청을 보낼 때

  • HttpRequestMethodNotSupportedException 예외

서버 코드 오류일 떄

  • NullpointerException과 같은 Exception예외들

이후에도 아래와 같은 과정을 통해 최적화(?)를 해봤다.

  • 필요없는 생성자 삭제
  • 일부 null응답하는 예외 클라이언트에서 안보이게하기

최적화(?) 코드는 링크를 참조
(풀코드는 아래 빨간 박스안에 있는 3가지 버전에서 확인 가능)

최적화(?)를 통해 아래 캡쳐본과 같이 클라이언트에서 나오는 출력을 바꿔봤다.

0개의 댓글

관련 채용 정보