스프링에선 어떻게 Exception을 처리해야할까?

홍혁준·2023년 4월 24일
1

고민

처음으로 Spring이라는 웹 프레임워크를 사용했습니다.

페어와 같이 1단계 미션을 진행하다, 스프링에서 어떻게 Exception을 처리할지 고민하고, 여러 방법을 찾아봤습니다.

후술할 내용은 이번 미션을 진행하면서 겪은 경험 반 고민 반이 섞인 글입니다.

시도해본 방법

에러 페이지로 리다이렉트

가장 먼저 시도해본 것은 에러페이지로 리다이렉트 시키는 것이었습니다.
에러페이지를 만들어 보내줄 때도, 저희가 도메인에서 정의한 에러 메시지를 통해 에러페이지를 랜더링해서 보내려고 했습니다.

처음엔 금방 완성할 것 같았습니다. @ExceptionHandler가 붙은 hanlde 메서드에서는 파라미터로 Exception을 받을 수 있었고, 해당 exception의 message를 이용하면 동적으로 전달해 줄 수 있을 것 같았죠.

그리고, Thymleaf를 이용하면 에러메시지를 보여줄 수 있는 이쁜 페이지를 만들 수 있을 것 같았죠.

하지만 하나 큰 문제가 있었습니다. 저희가 만드는 애플리케이션은 RESTful Application이었기에, 서버사이드 랜더링 페이지는 의미가 없었습니다…

ResponseEntity에 메시지 담기

다음으로 떠올린 방법은 학습테스트에서 사용했던 방법이었습니다.

Http응답으로 badRequest에 에러메시지를 담아서 반환해줍니다.

@ExceptionHandler(RacingCarException.class)
public ResponseEntity<String> handle(RacingCarExcetption e) {
    return ResponseEntity.badRequest().body(e.getMessage());
}

일단, 현재 Spring과 프로젝트 구조 상 이 상태가 최선이라고 여겼습니다.

추가적인 의문

미션을 진행하다, 리뷰어가 새로운 질문을 저에게 던져주었습니다.

애플리케이션에서 예상하지 못한 다른 예외가 터지면 어떻게 될까? 레벨 1단계에서는 생각지도 못한 질문이었습니다.

이제 웹 애플리케이션으로 넘어가면서, 이전 콘솔과 달리 에러가 발생할 포인트가 늘어났습니다.
단적인 예로, DB와의 연결문제나, 외부 API를 사용할 때의 문제가 있겠죠.

이 경우는 자바 코드 내부에서 발생하는 문제가 아니라, 의존하는 외부 환경(DB, API)에 따라 발생하는 문제입니다. 어떻게 예방할 수 있는 문제가 아니죠

이에 대해서 어떻게 처리를 할 지 고민을 해봤습니다.

발생하는 Exception의 종류

먼저 Exception을 크게 다음과 같이 분류할 수 있을 것입니다.

  1. 클라이언트의 잘못으로 발생한 Exception
  2. 서버의 잘못으로 발생한 Exception

클라이언트의 잘못으로 발생한 Exception

사실 이 부분은 크게 고민할 여지가 없었습니다. 클라이언트의 잘못으로 발생한 Exception이란 말은 다음 말과 유사합니다.

클라이언트의 잘못된 요청으로 발생한 Exception

여기서 잘못된 요청이란 크게 두 가지가 있겠죠

  1. 잘못된 Http 요청(존재하지 않는 API, 권한)

위와 같은 경우는 Spring이 자동으로 반환해주는 Error 가 있기에 크게 걱정할 필요는 없습니다.(Ex. 404 Not Found.)

  1. 도메인 로직상 유효하지 않는 값이 입력된 경우

위와 같은 경우는 ExceptionHandler에서 400대 에러로 변환시켜서 반환해주면 좋을 것입니다.

추가적으로 상세한 ErrorMessage를 Response body에 추가해 반환해준다면 더 좋겠죠.
위에서 본 예시가 있을 것입니다.

@ExceptionHandler(RacingCarException.class)
public ResponseEntity<String> handle(RacingCarExcetption e) {
    return ResponseEntity.badRequest().body(e.getMessage());
}

서버의 잘못으로 발생한 Exception

서버의 잘못으로 발생한 Exception 또한 크게 두 가지로 나뉠 것입니다.

  1. 자바 내부 로직에서 미쳐 정의하지 못한 Exception

이 Exception 같은 경우에는 잘 찾아서, 최대한 Exception이 발생하는 원인을 파악하고, 로직에서 조취를 취하면 됩니다. (validation로직 추가 작성 등)

  1. 외부 환경에 의존하기 때문에 발생하는 Exception

DB 서버의 연결과 같은 에러가 있겠죠. 아쉽게도 이 부분은 자바 내부의 코드를 고친다고 해결되는 문제가 아닙니다.

그렇지만 이와 같은 Exception을 어쩔 수 없지 하고 방치해둘 수는 없습니다.

떠올린 해결 방안

첫 번째 해안

가장 처음에 떠올렸던 단순한 해결 방법은, 모든 Exception을 400대 에러로 포장해서 반환해주는 것이었습니다. 이유는 다음과 같았습니다.

“500대 에러를 클라이언트에 노출해봤자, 어차피 클라이언트가 해줄 수 있는 것은 없으니까, 그냥 400대 에러로 포장해서 반환해주자.”

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handle() {
    return ResponseEntity.badRequest().body();
}

하지만, 코드를 작성하자마자 “이게 맞나?” 라는 생각이 떠올랐습니다.

이런식으로 다 400대로 묶어서 처리할거면 500대 에러 코드는 왜 있을까?

만약 서버의 문제인데, 클라이언트가 본인이 잘못 요청했다고 판단한다면, 서버에서 발생한 문제는 어떻게 발견할 수 있을 것인가?

조금 관점을 바꿔 봤습니다. 만약 이미 완성된 어플리케이션이 있다 하더라도, 그 어플리케이션에는 아직 발견되지 않은 에러가 있을 수 있습니다.

정말 정말 잘 만들어서 로직에서 발생할 수 있는 모든 예외처리를 한 로직이 있다 하더라도, 의존하는 외부환경에서 발생하는 Exception은 어떻게 해결할 수 없죠.

두 번째 해결방안

생각을 조금 바꿔봤습니다. 기존에는 Exception을 미리 예방을 하는 방식이었습니다. 계속하여 validation 로직을 짰던 것이 그 일환이죠.

그것이 개발자로서 중요한 덕목이라 생각했고, 실제로 중요하니까요.

하지만, 저는 모든 Exception을 미리 예방한다는 것은 불가능하다라는 생각이 이번 미션에서 들었습니다.

예방을 못하니까, 빠른 조치를 할 수 있는 방법을 생각해봤습니다.

그 방법으로 생각해낸 것이 log였습니다.

서버를 관리하다보면, 아마 Error log를 주기적으로 확인할 것 같습니다.(그런게 없다면…돔황챠)

서버가 문제가 생겼다면 가장 먼저 log로 원인을 파악할 테니까요.

사실 저희가 작성한 프로젝트가 계속 사용될 게 아니기에, 로그를 남기는 로직은 오버엔지니어링이지만, 학습을 위한 목적으로 한번 작성해보았습니다.

@RestControllerAdvice
public class RacingCarControllerAdvice {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    ...

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<String> loggingUnExpectedException(final RuntimeException e) {
        logger.error(UNEXPECTED_ERROR_LOG_FORMAT, convertToString(e));
        return ResponseEntity.internalServerError()
                .body(UNEXPECTED_ERROR_MESSAGE);
    }

    private String convertToString(final Exception e) {
        final StringWriter sw = new StringWriter();
        final PrintWriter pw = new PrintWriter(sw);
        e.printStackTrace(pw);
        return sw.toString();
    }
}

간단하게 RuntimeException이 발생하면 해당 exception의 stackTrace를 로그로 남기는 로직입니다.

다른 세련되게 로그를 남기는 방법이 있긴 하겠지만, stackTrace를 찍어서, 에러가 발생된 시점을 로깅하는게 지금 제가 생각하기에 가장 좋은 방법이었습니다.

(다른 좋은 방법이 있다면 댓글 부탁드립니다)

로깅을 한 후 500대 에러를 반환합니다. 하지만, 에러 메시지는 고정된 메시지를 반환합니다. 클라이언트가 추가적으로 조치를 취해줄 것이 없기 때문에, 반환되는 바디에는 굳이 유의미한 값을 담지는 않습니다.

대신 개발자가 에러로그를 보았을 때, 외부 환경의 문제면 외부 환경을 개선할 것이고,
어플리케이션의 문제면, 어플리케이션의 로직에서 validation 로직을 추가하여 프로그램을 개선할 수 있을 것입니다.

결론

위에서의 내용을 간략하게 요약해보겠습니다.

  1. 클라이언트의 잘못된 입력인 경우, 에러 메시지를 상세하게 하여 반환합니다.
  2. 서버의 잘못인 경우 로깅은 상세하게 하고, 추상적인 에러메시지를 반환합니다.

결과적으로 다음과 같은 클래스를 추가하여 예외처리를 하였습니다.

@RestControllerAdvice
public class RacingCarControllerAdvice {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    ...

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<String> loggingUnExpectedException(final RuntimeException e) {
        logger.error(UNEXPECTED_ERROR_LOG_FORMAT, convertToString(e));
        return ResponseEntity.internalServerError()
                .body(UNEXPECTED_ERROR_MESSAGE);
    }

    private String convertToString(final Exception e) {
        final StringWriter sw = new StringWriter();
        final PrintWriter pw = new PrintWriter(sw);
        e.printStackTrace(pw);
        return sw.toString();
    }

    @ExceptionHandler(RacingCarException.class)
    public ResponseEntity<String> handle(RacingCarExcetption e) {
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}

미션 링크
미션 PR 링크

profile
끊임없이 의심하고 반증하기

2개의 댓글

comment-user-thumbnail
2023년 4월 24일

안녕하세요 홍실, 히이로입니다! 예외처리에 대해서 어떤 기준을 세우고 구현해야 할지 고민이었는데 인사이트 많이 얻어가용 홍실~

1개의 답글