용로그
article thumbnail

서론


개발을 하다가 문득 궁금한게 생겼다. CustomException이 발생하면 내가 설정한대로 예외가 터져줬지만, 그렇지 않은 예외들이 발생하면 모두 INTERNAL-SERVER-ERROR를 띄워줬다. 이게 맞는걸까?

 

우리는 보통 애플리케이션에 커스텀 예외(Custom Exception)을 선언하고 각각의 도메인에 맞게 사용한다. 예를 들면 ApplicationException이 RuntimeException을 상속받고 해당 Exception이 터지면 커스텀 해놓은 상태코드를 반환하고 그렇지 않는다면 그에 맞는 상태코드를 반환해야 할 것이다.

 

예를 들면 JPA를 사용해서 개발하다가 DataIntegrityViolationException 이런 예외를 한 번쯤 만나본적 있을 것이다. 이 예외는 일반적으로 데이터 무결성을 위반했기 때문에 발생하는 예외다.

 

하지만 이는 어떻게 보면 서버의 예외라고 판단할 수 있지만, 애초에 클라이언트가 잘못된 값을 넘겨주어서 무결성을 위반하는 경우도 있을 것이다.

 

그렇다면 위의 예외는 상태코드 400을 반환하는게 옳을까 500을 반환하는게 옳을까? 물론 상황에 따라 달라야 하겠지만, 무결성에 대해 예기치 못한 상황이면 500, 요청이 잘못된 경우는 400을 반환하는게 맞을 것이다. 그렇기 때문에 위 예외가 발생하면 각 상황에 맞는 상태코드를 반환해줘야할 의무가 있다.

 

위 같은 상황들을 해결하기 위해서는 적절한 ControllerAdvice 설정 또는 Filter를 더 구체적으로 커스텀하는 방법이 있다. 내 생각에 Filter는 Exception을 처리하는 것과는 다른 쓰임새로 만들어졌다고 생각하기 때문에 다른 글에서 더 자세히 설명하겠다.

 

상위 수준의 예외를 지정하지 않았을 때


현재 아래와 같은 코드로 ExceptionHandler들이 작성되어 있다. 이와 같이 예외를 구분하게 된다면 각 예외들이 반환해주는 값을 구체적으로 커스텀할 수 있다는 장점도 있지만, 단점이 너무나 명확하다.

 

제일 상위의 예외를 별도로 처리해주지 않았기 때문에 애플리케이션에서 발생할 수 있는 모든 예외를 다 명시해줘야 한다는 것이다.

 

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(BebeException.class)
    public ResponseEntity<ErrorResponse> handleGlobal(BebeException e) {
        ~~~
    }

    @ExceptionHandler({DataIntegrityViolationException.class})
    public ResponseEntity<?> dataIntegrityViolationException(DataIntegrityViolationException e) {
        ~~~
    }
    
    @ExceptionHandler({ValidationException.class})
    public ResponseEntity<?> validationException(ValidationException e) {
        ~~~
    }

    @ExceptionHandler({BindException.class})
    public ResponseEntity<?> bindException(BindException e) {
        ~~~
    }

    @ExceptionHandler({ConstraintViolationException.class})
    public ResponseEntity<?> constraintViolationException(ConstraintViolationException e) {
        ~~~
    }

    @ExceptionHandler({NestedServletException.class})
    public ResponseEntity<?> nestedServerException(NestedServletException e) {
        ~~~
    }
    ...
    
}

 

상위 수준의 예외를 지정했을 때


그래서 생각해볼 수 있는 방법은 명시적으로 각 예외를 구분할 수 있는 부분은 한계가 있으니, 무조건 구분해야하는 일부분만 ExceptionHandler로 정의하고 그 외의 예외들은 공통으로 처리하는 방법을 생각해 볼 수 있다.

 

참고로 가장 높은 수준의 예외를 정의하지 않으면 스프링에서 자동으로 500에러로 처리한다. 그럼 가장 높은 수준의 에러를 처리하기 전, 적당한 수준의 예외를 잡고 그 예외의 자식들을 같은 코드로 터뜨리는 정도면 각 상황에 맞고, 예외의 경계도 내가 설정할 수 있을 수준이 될 것이다.

 

위에서 언급한 DataIntegrityViolationException는 NonTransientDataAccessException을 상속받고 NonTransientDataAccessException는 DataAccessException을 상속받는다.

 

처음 스프링을 만들 때 Dao를 만든 사람이 데이터베이스 계층에서 일어나는 예외는 대부분 DataAccessException을 상속받도록 만들었다고한다. 

 

그렇기 때문에 데이터베이스 관련 Exception을 정의하고 싶다면 DataAccessException정도의 레벨에서 처리해주면 적당한 경계가 될 수 있다.

 

물론 더 디테일하게 구분하고 싶다면 이렇게 많은 예외들이 있으니 상황에 맞게 적절히 써보도록 하자. (참고로 DataAccessException은 Unchecked Exception이다.)

 

DataAccessException을 상속받는 예외들

 

예외 경계를 적절하게 설정 했을 때 코드는 다음과 같다.

 

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(WoowaAuthException.class)
    public ResponseEntity<ErrorResponse> handleGlobal(WoowaAuthException e) {
        final ErrorCode errorCode = e.getErrorCode();
        log.error("status:{}", errorCode.status());
        log.error("code:{}", errorCode.code());
        log.error("message:{}", errorCode.message());

        return new ResponseEntity<>(
                new ErrorResponse(
                        errorCode.status(),
                        errorCode.code(),
                        errorCode.message()),
                HttpStatus.valueOf(errorCode.status())
        );
    }

    @ExceptionHandler({ConstraintViolationException.class})
    public ResponseEntity<?> constraintViolationException(ConstraintViolationException e) {
        Map<String, String> errorMap = new HashMap<>();

        for (ConstraintViolation<?> error : e.getConstraintViolations()) {
            errorMap.put("constraint error", error.getMessage());
            log.error(error.toString());
        }

        return new ResponseEntity<>(errorMap, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler({DataAccessException.class})
    public ResponseEntity<?> dataIntegrityViolationException(DataAccessException e) {
        log.error(e.getMessage());
        e.printStackTrace();
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorCode(500, "DATABASE-500-1", "데이터베이스 예외 발생"));
    }

    @ExceptionHandler({Exception.class})
    public ResponseEntity<?> runtimeExceptionHandler(Exception e) {
        if (e.getMessage() == null) {
            log.error(e.getClass().toString());
            log.error(Arrays.stream(e.getStackTrace()).map(it -> it.toString())
                    .collect(Collectors.joining("\n")));
        } else {
            log.error(e.getMessage());
        }
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorCode(500, "SERVER-500-1", "알 수 없는 에러입니다."));
    }

}

 

있는 예외 없는 예외 다 잡아야 하는 코드가 아니라 자신이 원하는 수준의 레벨에서 처리할 수 있고 직접 커스텀하지 않은 예외들도 Exception이라는 제일 상위의 예외로 가기 전까지 ExceptionHandler들을 거치게 된다.

 

물론 ExceptionHandler가 거쳐지는 순서는 메서드의 순서가 아니라 Exception의 레벨이 낮을수록 먼저 핸들링된다. 

profile

용로그

@용로그

벨덩보단 용덩 github.com/wonyongChoi05