항상 거의 혼자서(혹은 백엔드 개발자들만 모여서) 프론트부터 백엔드, 배포까지 진행하다보니. 자연스럽게 매번 프로젝트에 RestAPI 도전을 해야지 해야지 하다 못하곤 했었는데(프론트가 탄탄하지 못해 쉽게 도전할 수 없었다) 이번에 좋은 기회로 UI/UX, 앱 개발자(IOS, AOS)분들과 프로젝트를 진행하게 되어
드디어 Restful API에 도전할 수 있는 기회가 생겨 공부를 시작하려고 한다.
이제서야?
Restful API를 본격적으로 설계하기 전에, 컨트롤러단에서 발생할 수 있는 예외들을 처리하기 위해 @RestControllerAdvice 어노테이션을 사용한다는 정보를 알았다.
해당 어노테이션은 여러 컨트롤러에서 발생할 수 있는 예외를 한 곳에서 중앙 집중적으로 처리할 수있도록 도움을 주는 어노테이션 이라고 한다.
해당 어노테이션이 줄 수 있는 이점은
해당 어노테이션을 활용한 예제를 아래와 같이 찾을 수 있었다.
@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??)
을 사용하고 있는데, 작성하면 할수록 예외의 처리와 상태 코드, 반환 방식이 얼마나 중요한지 알게 되었고,
클라이언트 분들과 소통하는 것에 익숙치 않다 보니 고쳐야 할 부분이 많아 보였다.
예외처리 실력이 백엔드 실력의 절반이다 라는 팀원 분의 귀한 말씀...?
열심히 하도록 하자...