어 나 갱수.

[Spring] Spring 예외 처리 본문

Spring

[Spring] Spring 예외 처리

김경수 2024. 8. 21. 17:21
728x90

Spring으로 개발을 하다 보면 다양한 Exception을 처리하게 됩니다. 에러를 처리하는 방법은 매우 다양합니다. 저는 그 방법들 중에 반복적인 작업을 줄이고 전역에서 공통적으로 Exception을 처리할 수 있는 @ExceptionHandler와 @ControllerAdvice를 사용하여 에러를 처리해 보겠습니다. 

@ExceptionHandler 예외 처리

컨트롤러 코드에서 @ExceptionHandler 어노테이션을 선언하고, 해당 컨트롤러에서 캐시 하고 싶은 예외를 지정하면 됩니다.

해당 컨트롤러에서 지정한 예외가 발생하면 예외에 일치하는 메서드가 실행됩니다.

아래 코드로 예시를 들어보겠습니다. 아래 코드에서는 회원가입 API에 대한 컨트롤러 코드가 있습니다. 회원가입 API를 요청하는 도중에 MethodArgumentNotValidException이 발생하게 된다면 AuthController에서는 그 예외를 캐치하고 시스템이 정상작동하면서 에러를 클라이언트에게 반환할 수 있도록 해줍니다.

@RestController
@RequestMapping("api/v2/auth")
class AuthController(
    private val authDataMapper: AuthDataMapper,
    private val signUpUseCase: SignUpUseCase
) {

    @ExceptionHandler(GomsException::class)
    fun handleGomsException(e: GomsException): ResponseEntity<ErrorResponse> =
        ResponseEntity(ErrorResponse(e.errorCode.message, e.errorCode.status), HttpStatus.valueOf(e.errorCode.status))

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ResponseEntity<ValidationErrorResponse> =
        ResponseEntity(ErrorResponse.of(e), HttpStatus.BAD_REQUEST)

    @PostMapping("signup")
    fun signUp(@RequestBody @Valid signUpHttpRequest: SignUpHttpRequest): ResponseEntity<Void> =
        signUpUseCase.execute(authDataMapper.toDto(signUpHttpRequest))
            .run { ResponseEntity.status(HttpStatus.CREATED).build() }
}

 

우선순위

스프링에서는 항상 자세한 것에 우선순위를 둡니다. 예를 들어서 같은 컨트롤러 코드에 부모 클래스, 자식 클래스가 아래와 같이 지정돼있다면 자식 클래스인 MethodArgumentNotValidException 에러가 호출됩니다. 

@ExceptionHandler(Exception::class)
fun handleException(e: Exception): ResponseEntity<ErrorResponse> =
    ResponseEntity(ErrorResponse.of(e), HttpStatus.INTERNAL_SERVER_ERROR)

@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ResponseEntity<ValidationErrorResponse> =
    ResponseEntity(ErrorResponse.of(e), HttpStatus.BAD_REQUEST)

 

아래와 같이 @ExceptionHandler 어노테이션에 예외를 지정하지 않으면 파라미터에 있는 예외를 처리합니다. 아래에서는 HttpMessageNotReadableException에 대해 처리합니다.

@ExceptionHandler
fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity<HttpMessageNotReadableErrorResponse> =
    ResponseEntity(ErrorResponse.of(e), HttpStatus.BAD_REQUEST)

에러가 발생했을 때의 동작 흐름입니다.

  1. 클라이언트가 데이터를 담아서 서버로 API 호출을 합니다.
  2. Controller가 요청에 대한 처리를 수행합니다.
  3. Controller에서 요청 결과에 대해서 MethodArgumentNotValidException 예외를 Controller 밖으로 던집니다.
  4. 예외가 발생했으므로 해당 Controller 안에 있는 ExceptionHandler가 작동합니다.
  5. 발생한 예외에 대해 처리할 수 있는 ExceptionHandler가 있는지 찾습니다.
  6. 발생한 예외에 대해 처리할 수 있는 메서드가 handleException(), handleMethodArgumentNotValidException() 이렇게 두 개가 존재합니다. 스프링에서는 자식 클래스인 후자 메서드를 실행시킵니다.
  7. 실행된 메서드에서 에러 메시지와 에러 코드를 사용해서 ResponseEntity 객체를 생성하고 반환합니다.
  8. 클라이언트는 정상작동을 하면서 에러가 발생하지 않고 에러 코드와 에러 메시지를 반환합니다.

ControllerAdvice와 Rest ControllerAdvice

Spring에서는 전역적으로 예외를 처리할 수 있는 @ControllerAdivce와 @RestControllerAdvice 어노테이션이 있습니다.

두 개의 차이는 @Controller와 @RestController의 차이와 같이 @RestControllerAdvice에 @ResponseBody가 붙어 있어 응답을 Json으로 한다는 점입니다.

 

ControllerAdvice의 Advice는 AOP에서 나온 용어로 @ControllerAdvice는 AOP 기술이 적용되어 있음을 알 수 있습니다.

@RestControllerAdvice
class GlobalExceptionHandler {

	@ExceptionHandler(GomsException::class)
	fun handleGomsException(e: GomsException): ResponseEntity<ErrorResponse> =
		ResponseEntity(ErrorResponse(e.errorCode.message, e.errorCode.status), HttpStatus.valueOf(e.errorCode.status))

	@ExceptionHandler(MethodArgumentNotValidException::class)
	fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ResponseEntity<ValidationErrorResponse> =
		ResponseEntity(ErrorResponse.of(e), HttpStatus.BAD_REQUEST)

	@ExceptionHandler(HttpMessageNotReadableException::class)
	fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity<HttpMessageNotReadableErrorResponse> =
		ResponseEntity(ErrorResponse.of(e), HttpStatus.BAD_REQUEST)

	@ExceptionHandler(DataIntegrityViolationException::class)
	fun handleDataIntegrityViolationException(e: DataIntegrityViolationException): ResponseEntity<DataIntegrityViolationErrorResponse> {
		return ResponseEntity(ErrorResponse.of(e), HttpStatus.BAD_REQUEST)
	}

}

 

@ControllerAdvice 어노테이션은 여러 컨트롤러에 대해 전역적으로 @ExceptionHandler를 적용시켜 줍니다.
@ControllerAdvice 에는 @Component 어노테이션이 붙어져있어서 ControllerAdvice가 선언된 클래스는 스프링 빈으로 등록됩니다.
그렇기 때문에 우리는 위와 같은 GlobalExceptionHandler에 @ExceptionHandler 어노테이션을 통해 에러를 핸들링하는 클래스를 만들어 에러를 처리할 수 있습니다.

 

GlobalExceptionHandler코드를 보면 GomsException이라는 개발자가 직접 설정한 ErrorCode 내에서 발생하는 에러를 캐치할 수 있는 handleGomsException이 있습니다. GomsException은 아래와 같이 RuntimeException을 상속받아 구현됩니다.

open class GomsException(
    val errorCode: ErrorCode
): RuntimeException(errorCode.message)

ErrorCode는 아래와 같이 enum 클래스로 이루어져있습니다.

enum class ErrorCode(
    val message: String,
    val status: Int
) {

    // GOMS
    GOMS_SERVER_ERROR("GOMS 서버 오류 입니다.", ErrorStatus.INTERNAL_SERVER_ERROR),

    /* INTERNAL */
    BAD_REQUEST("잘못된 요청입니다.", ErrorStatus.BAD_REQUEST),
    FORBIDDEN("FORBIDDEN", ErrorStatus.FORBIDDEN),
    EXPIRED_TOKEN("토큰이 만료되었습니다.", ErrorStatus.UNAUTHORIZED),
    NULL_EXCEPTION_MESSAGE("예외 객체가 null 입니다.", ErrorStatus.BAD_REQUEST),

}

이렇게 GlobalExceptionHandler에서 @RestControllerAdvice 어노테이션을 통해 에러를 처리하면

  • 하나의 클래스 (GlobalExceptionHandler)에서 전역적으로 에러를 처리할 수 있습니다.
  • 일관성 있는 에러 응답을 할 수 있습니다.
  • try-catch 문 없이 에러를 캐치할 수 있어 가독성이 높아집니다.
  • RestControllerAdvice에 대상을 지정하지 않으면 모든 컨트롤러에 지정됩니다.

대상 컨트롤러 지정 방법

 // Target all Controllers annotated with @RestController
 @ControllerAdvice(annotations = RestController.class)
 public class ExampleAdvice1 {}
 
 // Target all Controllers within specific packages
 @ControllerAdvice("org.example.controllers")
 public class ExampleAdvice2 {}
 
 // Target all Controllers assignable to specific classes
 @ControllerAdvice(assignableTypes = {ControllerInterface.class,
 AbstractController.class})
public class ExampleAdvice3 {}

 

스프링 공식 문서를 보면 위와 같이 특정 어노테이션 클래스를 지정할 수 있고, 특정 패키지를 지정할 수 있습니다.

스프링 공식문서

728x90