뭐요

스프링 Exception Handler 적용 (@ExceptionHandler, @ControllerAdvice) 본문

Spring

스프링 Exception Handler 적용 (@ExceptionHandler, @ControllerAdvice)

욕심만 많은 사람 2023. 4. 16. 21:48

Exception의 기본 개념 : https://dmansp.tistory.com/39

왜 알아보았는가

clean code를 읽어보면 try-catch를 통한 예외처리는 최대한 지양하라는 의견이 지배적입니다. 서비스 로직을 작성할 때 throw하는 모든 exception에 대해 try-catch로 주렁주렁 예외처리를 해주면 적절한 관심사 분리가 이루어지지 않습니다. 이는 명백한 SRP 위반이며 매우 복잡한 코드가 되어 유지보수 하기 어려워집니다. 따라서 코드는 아래처럼 작성하는 것이 clean한 코드입니다.

function registerUser()
{
    var request = receiveRequest();
    validateRequest(request);
    canonicalizeEmail(request);
    updateDbFromRequest(request);
    sendEmail(request.Email);
    return sendResponse("Success");
}

서비스에서 사용자를 등록할 때 어떠한 과정을 거치는 지 누구든 한 눈에 알 수 있습니다. 그러면 각 과정에서 throw하는 exception은 어디서 예외 처리를 해줘야할까요? 바로 Exception Handler를 통해 따로 관리해줍니다.

 

@ExceptionHandler

해당 Annotation을 붙이면 value로 주는 Exception Class에 대해 예외 처리를 해줄 수 있습니다.

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponseDto> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    log.error("handleMethodArgumentNotValidException : {}", e);
    final ErrorResponseDto response = ErrorResponseDto.of(ResultCode.INVALID_METHOD_ARGUMENT);
    return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatusCode()));
}

(MethodArgumentNotValidException 는 보통 request로 받은 data에 대해 사용자가 정의한 유효성 검증에 실패한 경우 발생합니다. 궁금하면 @Valid를 알아보세요!)

위 코드를 예시로 들면, MethodArgumentNotValidException 이 발생했을 때 handleMethodArgumentNotValidException()를 호출합니다. 이후 에러 로그를 찍고, ResponseEntity의 형태로 값을 반환합니다.

이처럼 Exception Handler를 이용한다면 핵심적인 비즈니스 관심사와 예외 처리에 대한 관심사를 분리할 수 있습니다.

 

@ControllerAdvice

Controller에서 발생하는 exception들을 handler로 잡아준다고 명시적으로 지정하는 annotation입니다. 속성을 설정해서 특정 패키지, 혹은 특정 컨트롤러를 한정할 수도 있습니다.

일반적으로 스프링 프로젝트에서는 컨트롤러에서 발생할 수 있는 Exception을 범용적으로 처리해주기 위해 GlobalExceptionHandler 를 만듭니다.

아래 코드를 참고 합시다.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponseDto> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException : {}", e);
        final ErrorResponseDto response = ErrorResponseDto.of(ResultCode.INVALID_METHOD_ARGUMENT);
        return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatusCode()));
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponseDto> handleBusinessException(BusinessException e) {
        log.warn("BusinessException : {}", e.getMessage());
        final ErrorResponseDto response = ErrorResponseDto.of(e.getResultCode());
        return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatusCode()));
    }

    @ExceptionHandler(Exception.class)
    protected ResponseEntity<ErrorResponseDto> handleException(Exception e) {
        log.error("handleException", e);
        final ErrorResponseDto response = ErrorResponseDto.of(ResultCode.INTERNAL_SERVER_ERROR);
        return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatusCode()));
    }
}

 

FLOW

  1. User Defined Exception Class 생성
  1. hadling 할 메소드 생성 후 @ExceptionHandler 어노테이션 추가 후 해당 Exception을 속성값으로 넣어줌
  1. 해당 Exception 발생 시 핸들러에서 어떤 로직 처리해줄 건지 코드 작성
  1. ErrorCode 포함해서 ResponseEntity에 넣어주고 해당 객체 return 해주도록 함
  1. 이 때 return 해주는 ResponseEntity 객체의 타입은 일정하게 response해주는 게 좋음!
  1. 위 과정 적용하면 이후에 서비스 로직에서 throw new UserDefinedException() 던지면 Handler에서 알아서 처리해줌!

 

참고1. 비즈니스 예외처리

비즈니스적인 예외처리는 아래 상속구조 따르면 통일성있게 처리 가능

 

참고2. ErrorCode

ErrorCode를 관리할 수 있는 ENUM 생성 후 따로 관리해주기

  1. ErrorCode Enum 생성
  1. Exception 상속 받는 User Defined Exception의 생성자 인자로 넣어주기

 


참고

https://github.com/cheese10yun/spring-guide/blob/master/docs/exception-guide.md#exception-guide


https://velog.io/@kiiiyeon/스프링-ExceptionHandler를-통한-예외처리

https://kwangyulseo.com/2015/05/29/관심사의-분리separation-of-concerns/