GlobalExceptionHandler과 CustomException을 통한 공통 예외 처리하기 🚨

박준형·2024년 10월 25일

스프링 개발

목록 보기
3/20
post-thumbnail

전 포스팅에서 공통 응답 통일화의 주제에 대해 정리하였다면 이번에는 예외처리에 관한 포스팅을 작성하려고 한다!!

개발에서 예외 처리는 매우 중요한 요소이다. 시스템이 오류를 제대로 처리하지 않으면, 사용자 경험이 저하될 뿐만 아니라, 애플리케이션의 신뢰성도 크게 떨어질 수 있다. CustomException 클래스를 사용한 커스텀 예외 처리로 코드의 유지보수성과 가독성을 높이고, 클라이언트에게 더 명확한 오류 정보를 제공해 줄 수 있다.

이 글에서는 내가 이번에 하는 프로젝트에 적용한 CustomException을 활용한 예외 처리 방법을 단계별로 설명해 보려고 한다.



1. BaseErrorCode

public interface BaseErrorCode {
    ErrorReasonDto getReason();
    ErrorReasonDto getReasonHttpStatus();
}

공통 응답처리에서도 봤던 BaseErrorCode 인터페이스로 HttpStatus와 성공/실패 유무, 커스텀 코드, 메시지를 담은 ErrorReasonDto를 반환해줄 수 있는 인터페이스이다.


2. CustomException

CustomException 클래스는 RuntimeException을 상속받아 커스텀 예외 처리를 위한 기본 틀을 제공한다. 예외 처리에서 중요한 것은 일관된 에러 메시지와 상태 코드를 반환하는 것이다. 이를 위해, BaseErrorCode 인터페이스를 활용하여 에러 코드를 관리한다.

@Getter
@RequiredArgsConstructor
public class CustomException extends RuntimeException {
    private final BaseErrorCode errorCode;

    @Override
    public String getMessage() {
        return errorCode.getReasonHttpStatus().getMessage();
    }

    public String getCode() {
        return errorCode.getReasonHttpStatus().getCode();
    }

    public HttpStatus getHttpStatus() {
        return errorCode.getReasonHttpStatus().getHttpStatus();
    }
}

BaseErrorCode

이 인터페이스를 통해 각 예외에 대한 에러 코드, 메시지, HTTP 상태 코드 등을 관리한다. 이를 통해 CustomException 클래스는 다양한 예외 상황에서 일관된 형식으로 에러 정보를 클라이언트에게 반환할 수 있다.

getMessage()

예외 발생 시, BaseErrorCode로부터 에러 메시지를 가져와 반환한다. 이를 통해 클라이언트는 예외에 대한 상세 메시지를 쉽게 받을 수 있다.

getCode()

에러에 대한 커스텀 응답 코드를 반환한다. 이 코드는 클라이언트가 오류의 종류를 정확하게 식별할 수 있도록 한다.
그렇기 때문에 미리 클라이언트와 함께 커스텀 코드에 해당하는 내용이 무엇인지 자료 공유가 되어 있어야 한다!!

getHttpStatus()

HTTP 상태 코드를 반환하여, 서버가 클라이언트에게 적절한 상태 코드를 전송할 수 있도록 한다. 예를 들어, 404 (Not Found) 또는 500 (Internal Server Error) 등을 반환한다.



3. ErrorStatus Enum을 통한 에러 코드 관리

예외 처리 시, ErrorStatus 열거형을 사용하여 발생하는 에러들을 관리할 수 있다.

@Getter
@RequiredArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
    _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500", "서버 내부 오류가 발생했습니다. 자세한 사항은 백엔드 팀에 문의하세요."),
    _BAD_REQUEST(HttpStatus.BAD_REQUEST, "400", "입력 값이 잘못된 요청 입니다."),
    _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "401", "인증이 필요합니다."),
    _FORBIDDEN(HttpStatus.FORBIDDEN, "403", "금지된 요청입니다."),
    _METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "405", "허용되지 않은 요청 메소드입니다.");

    private final HttpStatus httpStatus;
    private final String code;
    private final String message;

    @Override
    public ErrorReasonDto getReason() {
        return ErrorReasonDto.builder()
                .isSuccess(false)
                .code(code)
                .message(message)
                .build();
    }

    @Override
    public ErrorReasonDto getReasonHttpStatus() {
        return ErrorReasonDto.builder()
                .isSuccess(false)
                .httpStatus(httpStatus)
                .code(code)
                .message(message)
                .build();
    }
}

특징

에러 상태 관리
ErrorStatus는 발생하는 에러 상황에 대한 HTTP 상태 코드, 커스텀 코드, 메시지를 미리 정의한다.

재사용성
이 구조는 다양한 예외 상황에서 동일한 에러 응답을 제공할 수 있어 재사용성이 높다.

일관성
모든 에러 응답이 동일한 구조를 가지므로, 클라이언트는 일관된 방식으로 오류를 처리할 수 있다.



4. ApiResponse를 사용한 응답

예외를 CustomException으로 통합 처리함으로써, 예외가 발생했을 때 일관된 응답 구조를 유지할 수 있다. 이때, 성공 응답과 마찬가지로, 예외 응답도 ApiResponse를 통해 일관된 형식으로 처리한다.
-> BaseErrorCode에서 에러 정보를 가져와서 응답에 보여준다!!

@Getter
@RequiredArgsConstructor
public class ApiResponse<T> {
    private final Boolean isSuccess;        // 성공 여부
    private final String code;              // 응답 코드
    private final String message;           // 응답 메시지
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private final T payload;                // 실제 응답 데이터 (성공 시 포함)

    // 실패 응답
    public static <T> ResponseEntity<ApiResponse<T>> onFailure(BaseErrorCode code) {
        ApiResponse<T> response = new ApiResponse<>(false, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(), null);
        return ResponseEntity.status(code.getReasonHttpStatus().getHttpStatus()).body(response);
    }
}


5. GlobalExceptionHandler를 통한 공통 예외 처리

Spring에서는 예외가 발생했을 때 전역 예외 처리를 통해 모든 예외를 한 곳에서 처리할 수 있다. 이를 위해 @RestControllerAdvice와 @ExceptionHandler를 사용한다.

@RestControllerAdvice는 전역적으로 예외를 처리할 수 있도록 해주는 애노테이션이다. 이를 사용하면 모든 @RestController에서 발생하는 예외를 한 곳에서 처리할 수 있다.
@ExceptionHandler는 특정 예외를 처리할 메서드를 선언할 때 사용된다. 이 메서드는 특정 예외가 발생했을 때 호출되어 그 예외를 처리하는 역할을 한다.

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    // 커스텀 예외 처리
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ApiResponse<ErrorReasonDto>> handleCustomException(CustomException e) {
        log.error("CustomException: {}", e.getMessage(), e);
        return ApiResponse.onFailure(e.getErrorCode());
    }

    // Security 인증 관련 처리
    @ExceptionHandler(SecurityException.class)
    public ResponseEntity<ApiResponse<ErrorReasonDto>> handleSecurityException(SecurityException e) {
        log.error("SecurityException: {}", e.getMessage(), e);
        return ApiResponse.onFailure(ErrorStatus._UNAUTHORIZED);
    }
}

발생한 CustomException을 잡아내고, 해당 CustomException에 있는 BaseErrorCode를 ApiResponse.onFailure에 넣어주어 공통 에러 응답 처리를 적용한다. 이 과정에서 BaseErrorCode의 오류 정보를 가져와 클라이언트에 전달 해 준다.



6. 컨트롤러에서의 예외 발생 처리

CustomException과 ErrorStatus를 통해 일관된 예외 처리 방식을 적용하였다. 다음은 예외 발생을 테스트하기 위한 컨트롤러의 코드이다.

/**
 * 에러 테스트용 API
 */
@GetMapping("/test-error")
public void getError() {
    throw new CustomException(ErrorStatus._INTERNAL_SERVER_ERROR);
}

이 API는 강제로 CustomException을 발생시켜, 서버 내부 오류(500) 상태를 클라이언트에게 반환한다.
ErrorStatus에 정의된 _INTERNAL_SERVER_ERROR 상태를 사용하여, 오류가 발생할 때 해당 ErrorStatus에 대한 상태 코드와 커스텀 코드, 메시지를 반환한다.



7. CustomException의 장점

지금까지 커스텀 예외 처리의 구조에 대해서 차례대로 정리하였다.
CustomException을 통한 예외 처리는 여러 가지 이점을 제공한다.

자세한 예외 처리

클라이언트에 더 자세한 에러 응답을 줄 수 있다.

코드 중복 제거

예외 처리 로직을 한 곳에 모아놓음으로써, 중복된 예외 처리 코드를 작성할 필요가 없다.

확장성

새로운 예외 상황이 추가되더라도 ErrorStatus와 CustomException을 통해 쉽게 확장할 수 있다.



8. 결론

CustomException을 활용한 예외 처리는 API의 일관성을 유지하고, 코드의 중복을 최소화하며, 유지보수성을 높이는 효과적인 방법이다. ErrorStatus를 통해 예외 상황을 한곳에서 관리하고, 전역 예외 처리(GlobalExceptionHandler)를 사용하여 애플리케이션의 안정성을 극대화할 수 있다.

이를 통해, 예외가 발생하더라도 클라이언트와 서버 간의 통신에서 명확한 오류 메시지와 상태 코드, 커스텀 코드를 반환할 수 있어, 에러 발생 시에도 에러 해결에 굉장히 용이할 것 같다!!!

profile
매일 매일 성장하기

0개의 댓글