Spring에서 예외처리를 하는 건 중요하다. 여러가지 이유가 있겠지만 예외처리가 필요한 이유 중 하나는 서버에서 오류가 발생했을 때 클라이언트에서 아래와 같이 너무 많은 정보가 노출되는 것을 볼 수 있다. 그럼 보안에도 좋지 않다.
그래서 익셉션이 발생했을 경우 그에 맞는 에러 응답을 처리하는 방식으로 수정하기로 했다.
사용자는 자신의 상황에 맞게 다양하게 예외처리 방법을 구현할 수 있다.
그 중 하나가 @RestControllerAdvice 어노테이션을 이용하는 방법이다.
나는 다른 블로그(하단에 링크)를 참고하여 내 입맛에 맞게 수정했다.
필요한 파일들은 다음과 같다.
1. 에러코드 정의
원래 인터페이스를 이용하여 추상화한 ErrorCode를 정의한 후 사용자 상황에 맞게 구현하는 방식으로 많이 구현한다. 그렇게 하는게 프로그래밍적으로도 좋다.
하지만 나는 그렇게 많은 정의가 필요하지도 않고 간단하게 사용할 거라 추상화는 생략했다.
@Getter
public class ErrorCode {
private EnumServerCode codeEnum;
public ErrorCode() {
}
public ErrorCode(EnumServerCode codeEnum) {
this.codeEnum = codeEnum;
}
/**
* 오류 상태 코드
*/
public static enum EnumServerCode {
SQL_ERROR(HttpStatus.BAD_REQUEST, "DB 오류 발생"),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "internal server error");
EnumServerCode(HttpStatus httpStatus, String msg) {
this.httpStatus = httpStatus;
this.msg = msg;
}
private HttpStatus httpStatus;
private String msg;
public HttpStatus getHttpStatus() {
return httpStatus;
}
public String getMsg() {
return msg;
}
}
}
우선 생각나는 에러만 간단하게 작성했다. 이 에러코드는 HTTP 상태 값과 메세지를 가지고 있는 클래스이다.
2. 에러 응답 클래스 생성
위 에러 코드를 받아서 그걸 response로 던져야 한다.
@Getter
@Builder
@RequiredArgsConstructor
public class ErrorResponse {
private final String code;
private final String msg;
//에러가 없다면 응답 발생x
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private final List<ValidationError> errors;
@Getter
@Builder
@RequiredArgsConstructor
public static class ValidationError {
//@valid에서 에러 발생 -> 어느 필드에서 발생했는가
private final String field;
private final String message;
public static ValidationError of(final FieldError fieldError) {
return ValidationError.builder()
.field(fieldError.getField())
.message(fieldError.getDefaultMessage())
.build();
}
}
}
사실 나에겐 ValidationError 발생도 아직은 필요가 없음22... 하지만 언젠가를 위해 남겨둔다.. (그럴거면 추상화도 남겨두지)
3. @RestControllerAdvice 구현
이제 에러를 처리하는 클래스만 만들면 된다. 여기서 드디어 @RestControllerAdvice를 쓸 수 있다.
전역으로 에러를 처리하는 GlobalExceptionHandler를 추가한다. 익셉션들은 상황에 맞게 계속 추가하면 된다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({SQLException.class, DataAccessException.class})
public ResponseEntity<ErrorResponse> handleSQLException(SQLException ex) {
log.warn("SQL Exception >>>> " + ex.getMessage());
ErrorCode errorCode = new ErrorCode(ErrorCode.EnumServerCode.SQL_ERROR);
return handleExceptionInternal(errorCode);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handlerException(Exception ex) {
log.warn("Global Exception >>>> " + ex.getMessage());
ErrorCode errorCode = new ErrorCode(ErrorCode.EnumServerCode.INTERNAL_SERVER_ERROR);
return handleExceptionInternal(errorCode);
}
private ResponseEntity<ErrorResponse> handleExceptionInternal(ErrorCode errorCode) {
return ResponseEntity
.status(errorCode.getCodeEnum().getHttpStatus())
.body(makeErrorResponse(errorCode));
}
private ErrorResponse makeErrorResponse(ErrorCode errorCode) {
return ErrorResponse.builder()
.code(errorCode.getCodeEnum().name())
.msg(errorCode.getCodeEnum().getMsg())
.build();
}
}
요것도 ResponseEntityExceptionHandler 라는 이미 제공하는 추상클래스 상속받아서 처리하면 더 좋겠지만 아직...333
이제 끝!
4. 응답 확인
구현은 끝났으니 확인해보자.
에러가 발생하는 API를 호출했을 때 서버 로그에서는 상세하게 잘 찍히고 응답값은 내가 원하는 대로 내려오는 것을 볼 수 있다.
시간이 없기도 했고 일단 진짜 최대한으로 필요한 것만 추려서 간단하게 구현한 코드이다. (확장성은.. 미래의 제가 하겠습니다)
당연히 사용자 예외 클래스도 직접 추가할 수도 있다. 하지만 나는 아직 필요하지 않기 때문에 필요하면 그때 추가하도록 하겠다.
더 자세하게 알고 싶으면 아래 블로그를 참고하십쇼
▼ 참고