Side Project/SWYP

[SWYP] @RestControllerAdvice와 @ExceptionHandler를 활용한 예외처리

newtownboy 2024. 5. 16. 14:31


[Version]
⦁ 2024.05.14 / [SWYP] @RestControllerAdvice와 @ExceptionHandler를 활용한 예외처리

 

자바에서 제공하는 예외는 크게 두 가지 종류로 구분할 수 있다.

  • Checked Exception
  • Unchecked Exception

위 두 가지 예외에 대해 자세하게 알아보고, 실제 프로젝트에 어떻게 적용하여 예외 상황에 대해 처리했는지 알아보자!

Checked Exception

Checked Exception은 java.lang.Exception 클래스를 상속받은 예외를 말한다. 이러한 예외는 개발자가 코드를 작성할 때 try-catch 구문을 사용하여 예외를 처리하거나, 해당 메서드에서 발생하는 예외를 호출하는 곳으로 던지기 위해 메서드 시그니처에 throws 키워드를 명시해야 한다.

 

만약 Checked Exception을 처리하지 않거나 throws로 예외를 던지지 않으면 컴파일러가 이를 감지하여 컴파일 에러를 발생시킨다. 

Unchecked Exception

Unchecked Exception은 java.lang.RuntimeException을 상속받는 예외를 말한다. 이러한 예외들은 예외 처리를 하지 않아도 컴파일러가 에러를 발생시키지 않는다. 따라서 명시적으로 try-catch 구문이다 throws 키워드를 사용하여 예외 처리를 하지 않아도 된다.

 

그러나, 발생한 Unchecked Exception은 호출 스택을 따라 상위 메서드로 올라가게 된다. 결국, Unchecked Exception도 어디선가 적절한 처리를 해주어야 한다.

 

REST-API 애플리케이션에도 Unchecked Exception을 처리하여 클라이언트에게 적절한 에러 메시지를 전달할 수 있다. 스프링 프레임워크에서는 대부분의 예외 클래스가 RuntimeException을 상속받는 Unchecked Exception이다. 데이터베이스 관련 예외인 o.s.dao.DataAccessException도 RuntimeException을 상속받는다. 이러한 예외들은 개발자가 직접 처리하기보다 프레임워크에서 제공하는 기능을 활용해 일관된 방식으로 처리된다. 비즈니스 로직과 예외를 처리하는 부분이 분리됨으로써 개발자는 비즈니스 로직을 개발하는데 더욱 집중할 수 있게 된다.

 

위 그림은 회원 가입을 하는 REST-API `POST /members`의 흐름을 그림으로 표현한 것이다. 클라이언트가 REST API를 호출하면 해당 요청은 백엔드에서 @PostMapping 애너테이션이 달린 핸들러 메서드로 라우팅 된다. 이 메서드는 MemberService의 signup() 메서드를 호출하여 기능을 처리한다. 이 과정에서 예외가 발생하면 이 예외는 ExceptionResponseHandler로 전달된다. 

 

@RestControllerAdvice
public class ExceptionResponseHandler {
    @ExceptionHandler(StorageException.class)
    public ResponseEntity<CommonApiResponse<?>> handleStorageException(StorageException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(CommonApiResponse.createError(e.getMessage()));
    }

    @ExceptionHandler(NoSuchElementException.class)
    public ResponseEntity<CommonApiResponse<?>> handleNoSuchElementException(NoSuchElementException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(CommonApiResponse.createError(e.getMessage()));
    }
    
    ...
}

 

@ExceptionHandler 애너테이션은 특정 예외를 처리하는 메서드를 지정할 때 사용된다. 이 애너테이션의 value 속성을 사용하여 처리할 예외 클래스를 명시하는데, 여기서 지정한 예외만 해당 메서드가 처리할 수 있다.

 

예를 들어, StorageException.class를 value 속성으로 설정한 경우 handleStorageException() 메서드는 StorageException 예외를 처리할 수 있다. 이때, handleStorageException() 메서드의 매개변수로 StorageException e를 선언하면 스프링 프레임워크는 해당 예외 객체를 주입한다.

 

@ExceptionHandler 애너테이션은 @Controller가 선언된 컨트롤러 클래스나 @ControllerAdvice가 선언된 컨트롤러 어드바이스 클래스에 사용할 수 있다. 컨트롤러 클래스에 선언된 예외 처리 메서드는 해당 컨트롤러 클래스의 핸들러 메서드에서 발생한 예외만 처리할 수 있다. 이 말은 즉, 다른 컨트롤러 클래스에서 발생한 StorageException 예외는 처리할 수 없다.

 

이와 반대로, @ControllerAdvice는 스프링 애플리케이션 전체에서 예외 처리를 담당하는 스프링 빈이다. 이 클래스를 생성하고 @ControllerAdvice 애너테이션을 선언하면 애플리케이션 전역에서 예외 처리를 담당하는 스프링 빈으로 등록된다. 이 스프링 빈 내부에 @ExceptionHandler를 설정하면 애플리케이션 전체에 예외 처리 메서드가 동작한다.

 

내가 사용한 @RestControllerAdvice는 @ControllerAdvice와 ResponseEntity 기능을 합친 애너테이션이다. 그러므로 @ExceptionHandler가 정의된 예외 처리 메서드가 리턴하는 객체는 HttpMessageConverter로 마셜링된다. 그리고 변경된 JSON 메시지가 클라이언트에 전달된다.

 

public class StorageException extends RuntimeException {
    public StorageException(String message) {
        super(message);
    }
}

StorageException은 이미지 파일 저장에 실패한 예외를 추상화한 것이다. StorageException 생성자의 파라미터 값인 message는 클라이언트에 전달할 목적으로 사용된다. 즉, 서비스 클래스에서 오류 메세지와 함께 예외를 던지면 ExceptionResponseHandler에서 클라이언트에 오류 메시지를 전달할 수 있다.

 

@RestControllerAdvice
public class ExceptionResponseHandler {
    @ExceptionHandler({StorageException.class, OtherException.class})
    public ResponseEntity<CommonApiResponse<?>> handleStorageException(RuntimeException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(CommonApiResponse.createError(e.getMessage()));
    }
    ...
}

 

@ExceptionHandler 애너테이션의 속성이 value 하나이기 때문에 내가 위에서 작성한 코드처럼 생략이 가능하다. 또한 여러 예외 클래스를 설정하여 하나의 핸들러 메서드로 동일하게 처리할 수 있다. 여러 클래스를 value 속성으로 설정하려면 핸들러 메서드의 인자를 선언할 때 주의해야 한다. 인자의 클래스 타입은 예외 클래스들의 상위 클래스로 선언해야 스프링 프레임워크가 예외 객체를 주입하는 과정에서 에러 없이 예외를 처리할 수 있다.(위 예시는 Exception 클래스가 RuntimeException을 상속받는다 가정}

 

@RestControllerAdvice
public class ExceptionResponseHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<CommonApiResponse<?>> handleException(Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(CommonApiResponse.createError(e.getMessage()));
    }
    ...
}

 

또한 프로젝트에서 발생할 수 있는 부가적인 예외 상황을 대비하기 위해 모든 예외의 상위 클래스인 Exception.class 핸들러를 생성하였다. 이로써, REST-API를 호출한 클라이언트에 잘못된 메세지가 전달되거나 시스템 내부 정보가 노출되는 것을 막을 수 있게 되었다.