오류 처리를 위한 @RestControllerAdvice

오목조목·2024년 3월 14일
post-thumbnail

항상 거의 혼자서(혹은 백엔드 개발자들만 모여서) 프론트부터 백엔드, 배포까지 진행하다보니. 자연스럽게 매번 프로젝트에 RestAPI 도전을 해야지 해야지 하다 못하곤 했었는데(프론트가 탄탄하지 못해 쉽게 도전할 수 없었다) 이번에 좋은 기회로 UI/UX, 앱 개발자(IOS, AOS)분들과 프로젝트를 진행하게 되어
드디어 Restful API에 도전할 수 있는 기회가 생겨 공부를 시작하려고 한다.

이제서야?

Restful API를 본격적으로 설계하기 전에, 컨트롤러단에서 발생할 수 있는 예외들을 처리하기 위해 @RestControllerAdvice 어노테이션을 사용한다는 정보를 알았다.

해당 어노테이션은 여러 컨트롤러에서 발생할 수 있는 예외를 한 곳에서 중앙 집중적으로 처리할 수있도록 도움을 주는 어노테이션 이라고 한다.

해당 어노테이션이 줄 수 있는 이점은

  1. 전역 예외 처리 : 모든 컨트롤러에서 발생할 수 있는 예외(exception)를 한 곳에서 처리
  2. 응답 형태의 표준화 : 예외에 대한 응답의 형태를 표준화(standardization)하여 클라이언트 측으로 일관된 형태의 응답을 보낼 수 있음
  3. 로깅(logging) : 예외가 발생했을 때, 로그를 기록함으로써 디버깅 및 모니터링에 용이(slf4j나 log4j 등을 사용할듯?)
  4. 커스텀 예외 응답 : 각 예외에 대해 직접 커스텀한 응답을 생성하여 클라이언트에게 전달할 수 있음

해당 어노테이션을 활용한 예제를 아래와 같이 찾을 수 있었다.

@RestControllerAdvice
@Slf4j
public class CustomRestAdvice {

    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.EXPECTATION_FAILED)
    public ResponseEntity<Map<String, String>> handleBindException(BindException e) {

        Map<String, String> errorMap = new HashMap<>();

        if(e.hasErrors()) {
            BindingResult bindingResult = e.getBindingResult();

            bindingResult.getFieldErrors().forEach(fieldError -> {
                errorMap.put(fieldError.getField(), fieldError.getCode());
            });
        }

        return ResponseEntity.badRequest().body(errorMap);
    }
}

위의 코드는 Form 데이터의 입력이 잘못되었을 경우(Binding Error) EXPECTATION_FAILED(417에러)를 상태코드로 반환할 수 있는 코드이다.
handleBindException 메서드에서 예외를 로깅하고, 에러를 추출하여 Map에 답아
클라이언트에게 반환하도록 구성되어있다.

기본적으로 찾은 내용은 이러했지만. 위에 해당 어노테이션이 줄 수 있는 장점 부분에
커스텀 예외 응답 부분의 내용과는 다르게, 위의 코드는 Binding Error에 대해서만 에러를 처리하고 있는 것을 확인할 수 있다.

커스텀 예외 응답을 처리하기 위해서는, ErrorCode와 이에 대한 Exception 부분을 커스텀 해야하는데. 이 내용은 아래와 같이 작성할 수 있다.

@Getter
@AllArgsConstructor
public enum ErrorCode {

    // MemberException
    EXAMPLE_EXCEPTION(HttpStatus.BAD_REQUEST, "예시 에러입니다."),
    MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원이 존재하지 않습니다."),
    IMPROPER_OAUTH_INFORMATION(HttpStatus.BAD_REQUEST, "올바른 OAUTH 정보가 아닙니다."),
    NOT_AUTHORIZED_MEMBER(HttpStatus.UNAUTHORIZED, "본인인증이 완료되지 않은 회원입니다.");

    private final HttpStatus httpStatus;
    private final String errorMessage;
}

이렇게 커스텀된 예외 응답을 처리하기 위한 핸들러와 BaseException, ErrorResponse도 다음과 같이 다시 구현해 주도록 하자.

@AllArgsConstructor
@Getter
public class BaseException extends RuntimeException{
    ErrorCode errorCode;
}

public record ErrorResponse(
    HttpStatus httpStatus,
    String message
) {
    public static ErrorResponse from(BaseException baseException) {
        return new ErrorResponse(baseException.errorCode.getHttpStatus(), baseException.errorCode.getErrorMessage());
    }
}

@Slf4j
@RestControllerAdvice
public class BaseExceptionHandler {

    @ExceptionHandler(BaseException.class)
    public ResponseEntity<ErrorResponse> handleBaseException(BaseException baseException) {
        ErrorCode errorCode = baseException.getErrorCode();
        ErrorResponse errorResponse = ErrorResponse.from(baseException);

        log.error("Error occurred: {} - {}", errorResponse.httpStatus(), errorResponse.message(), baseException);
        return ResponseEntity.status(baseException.errorCode.getHttpStatus()).body(errorResponse);
    }
}

이렇게 하면 예외 처리에 대한 준비는 얼추 끝났다고 볼 수 있을 것 같다.

현재 프로젝트를 진행하면서 Swagger API 명세(지금은 OpenAPI 3.0??)
을 사용하고 있는데, 작성하면 할수록 예외의 처리와 상태 코드, 반환 방식이 얼마나 중요한지 알게 되었고,
클라이언트 분들과 소통하는 것에 익숙치 않다 보니 고쳐야 할 부분이 많아 보였다.
예외처리 실력이 백엔드 실력의 절반이다 라는 팀원 분의 귀한 말씀...?
열심히 하도록 하자...

profile
블로그 이전중입니다 https://www.notion.so/2465adc69d7e805a9fd1e1ca971d657d?source=copy_link

0개의 댓글