[Spring] Spring의 예외처리 방법

윤종석·2024년 1월 4일
0

Spring 공부

목록 보기
1/3
post-thumbnail

Spring의 예외 처리 방법

요약

Spring의 기본적인 예외 처리 방법이 WAS까지 에러가 전달된다는 문제점과 코드의 가독성 등의 이유로 HandlerExceptionResolver를 구현하여 예외를 처리한다.

구현체들 중 ExceptionHandlerExceptionResolver, 그리고 이를 위해 @RestControllerAdvice를 사용하는 이유를 공부해 보았다.


Spring의 기본적인 예외 처리 방법

컨트롤러에서 예외가 발생했을 때, 별도의 예외처리를 하지 않으면 WAS까지 에러가 전달됨
→ WAS는 에플리케이션에서 처리를 못하는 예외라 판단하고 컨트롤러를 한번 더 호출


Spring이 제공하는 예외 처리 방법

Java의 예외 처리 방법: try-catch ⇒ 모든 코드에 붙이는 것은 비효율적

HandlerExceptionResolver 인터페이스

예외처리 전략 추상화 → 메인 로직으로부터 분리

예외를 catch하고 HTTP 상태, 응답 메세지 설정

WAS는 해당 요청을 정상적인 응답으로 인식하여 에러 전달X

HandlerExceptionResolver 구현체들을 빈으로 등록해서 관리한다.

  • DefaultErrorAttributes: 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.
  • ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
  • ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
  • DefaultHandlerExceptionResolver:  Spring 내부의 기본 예외들을 처리

아래의 도구들로 ExceptionResolver를 동작시켜 에러를 처리한다.

  • @ResponseStatus
  • @ResponseStatusException
  • @ExceptionHandler
  • @ControllerAdvice, RestControllerAdvice

@ResponseStatus: 에러 HTTP 상태를 변경하도록 도와주는 어노테이션

Exception 클래스 자체, 메소드에 @ExceptionHandler와 함께, 클래스에 @RestControllerAdvice와 함께 적용 가능

  • 에러 응답의 내용 수정 불가
  • 같은 예외는 같은 상태와 에러 메세지를 반환
  • 별도의 응답 상태가 필요하다면 예외 클래스를 추가해야 함
  • WAS까지 예외가 전달되고 WAS의 에러 요청 전달
  • 외부에서 정의한 예외 클래스에는 @ResponseStatus 사용할 수 없음

@ResponseStatusException: @ResponseStatus와 마찬가지로ResponseStatusExceptionResolver가 에러를 처리

기본적인 예외 처리를 빠르게 적용할 수 있어 손쉽게 프로토타이핑 할 수 있고, HttpsStatus를 직접 설정할 수 있다는 장점이 있음

  • 직접 예외 처리를 프로그래밍함
  • 예외 처리 코드가 중복될 수 있음
  • Spring 내부의 예외 처리 어려움
  • 예외가 WAS까지 전달되고 WAS의 에러 요청 전달이 진행됨

@ExceptionHandler

컨트롤러의 메소드, @ControllerAdvice@RestControllerAdvice가 있는 클래스의 메소드에 @ExceptionHandler 어노테이션을 추가하여 에러를 처리한다.

발생한 예외는 ExceptionHandlerExceptionResolver에 의해 처리

Exception 클래스들을 속성으로 받아 처리할 예외를 지정
예외 클래스를 지정하지 않는다면 파라미터에 설정된 에러 클래스를 처리

에러 응답을 자유롭게 다룰 수 있음

  • code: 어떤 종류의 에러인지 알려주는 에러 코드
  • message: 왜 에러가 발생했는지 설명
  • errors: 어느 값이 잘못되어 @Valid에 검증이 실패한 것인지를 위한 에러 목록
@ExceptionHandler(NoSuchElementFoundException.class)
public ResponseEntity<ErrorResponse> handleItemNotFoundException(NoSuchElementFoundException exception) {
    ...
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
    ...
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllUncaughtException(Exception exception) {
    ...
}

Spring은 예외 발생 시 가장 구체적인 예외 핸들러를 찾고, 없으면 부모 예외의 핸들러를 찾음

이때 @ExceptionHandler에 등록된 예외 클래스와 파라미터로 받는 예외 클래스가 동일 해야 함

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllUncaughtException(Exception exception) {
    ...
}

@ControllerAdvice와 @RestControllerAdvice

여러 컨트롤러에 대해 전역적으로 @ExceptionHandler를 적용

응답을 Json으로 내려주는 차이점

@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {

		@ExceptionHandler(value = GeneralException.class)
	  public ResponseEntity<Object> onThrowException(GeneralException generalException, HttpServletRequest request) {
	      ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
	      return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
	  }
	
}

장점:

  • 하나의 클래스로 모든 컨트롤러에 대한 전역적인 예외 처리
  • 직접 정의한 에러 응답을 일관성 있게 클라이언트에게 내려줄 수 있음
  • 별도의 try-catch문이 없음

주의점:

  • 한 프로젝트당 하나의 ControllerAdvice만 관리
  • 여러 ControllerAdvice 필요시 basePackages나 어노테이션 지정
  • 직접 구현한 Exception 클래스들은 한 공간에서 관리

실제 적용

@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {

		@ExceptionHandler(value = GeneralException.class)
    public ResponseEntity<Object> onThrowException(GeneralException generalException, HttpServletRequest request) {
        ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
        return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
    }

		...

}
@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
		
		// 가장 일반적인 응답
    _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
    _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
		
		...
		
		// 멤버 관련 응답
		MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."),

		...

		// For test
    TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "테스트용입니다.");
		
		...

}
@Service
@RequiredArgsConstructor
public class TempQueryServiceImpl implements TempQueryService {

    @Override
    public void CheckFlag(Integer flag) {
        if (flag == 1) {
            throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
        }
    }
}
public class TempHandler extends GeneralException {

    public TempHandler(BaseErrorCode errorCode) {
        super(errorCode);
    }
}
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {

    private BaseErrorCode code;

    public ErrorReasonDTO getErrorReason() {
        return this.code.getReason();
    }

    public ErrorReasonDTO getErrorReasonHttpStatus() {
        return this.code.getReasonHttpStatus();
    }
}

서비스에서 예외 발생이 확인되면 TempHandler를 통해 GeneralException을 생성하고, 이는 RuntimeException을 상속 받았기 예외로 던져진다.

그럼 @RestControllerAdvice가 있는 ExceptionAdvice에서 @ExceptionHandler인 적절한 메소드를 찾아 예외를 처리하게 되는 것이다.


참고 자료
https://mangkyu.tistory.com/204#recentEntries
[MangKyu's Diary:티스토리]

profile
공부 중인 학부생

0개의 댓글