[Kotlin/Springboot] RestControllerAdvice

HeavyJ·2023년 7월 2일
0

이번에 Kotlin과 Springboot를 함께 사용하는 프로젝트에 참여하게 됐습니다.

Kotlin + Springboot의 특징은 코틀린의 장점을 가져가면서 스프링부트의 문법을 그대로 사용할 수 있습니다.

널을 안전하게 처리할 수 있고 세미콜론을 안 붙혀도 된다는 점이 가장 큰 장점이 아닐까 합니다.

프로젝트를 시작하기에 앞서 에러 처리를 하기 위해서 ControllerAdvice를 적용해봤습니다.

ControllerAdvice

ControllerAdvice가 필요한 경우

예외처리를 함에 있어서 가장 큰 불편함은 바로 가독성 문제일 것입니다.

일반적으로 try-catch문을 사용해서 예외를 처리할 수 있지만, 예외의 종류가 많은 경우 catch()문이 지나치게 많아져 가독성이 떨어지게 됩니다.
추가로, 모든 로직에 try-catch문을 넣는 것도 매우 비효율적인 구조입니다.

따라서, 예외를 체계적으로 관리하기 위한 방법이 필요합니다.

이러한 방법 중 하나가 ControllerAdvice입니다.

ControllerAdvice와 RestControllerAdvice

Spring 프레임워크에서 다양하게 에러 처리를 할 수 있는데 그 중 한 방법은 ControllerAdvice입니다.

ControllerAdvice를 사용하게 되면 가장 큰 장점이 전역적으로 예외를 처리할 수 있다는 점입니다.

RestControllerAdvice의 역할 역시 전역적으로 예외를 처리하게 해줍니다.

하지만 @RestControllerAdvice@Controller@RestController의 차이처럼 @ResponseBody가 추가되어 Json으로 응답을 해준다는 점이 @ControllerAdvice와 다릅니다.

즉, @ControllerAdvice + @ResponseBody = @RestControllerAdvice입니다.

@ExceptionHandler

@ExceptionHandler는 예외를 잡아서 처리해주는 역할을 합니다.

어노테이션을 메서드에 선언하고 특정 예외 클래스(ex. RuntimeException, IllegalArgumentException)을 지정하고 해당 예외가 발생하면 메서드에 정의된 로직을 처리할 수 있습니다.

여기서 @ControllerAdvice 내에 @ExceptionHandler를 선언한 메서드를 사용하면서 AOP 방식으로 동작하고 Application 전역에 발생하는 모든 서비스의 예외를 한 곳에서 관리할 수 있게 해줍니다. 특히, Controller 단에서 발생하는 예외를 관리하여 처리할 수 있습니다.

코드

저는 View를 사용하지 않고 Json 형태의 데이터를 사용하기 때문에 @RestControllerAdvice를 사용했습니다.

ResponseDto.kt

class ResponseDto<T>(
    val success: Boolean = false,
    val data: T?,
    val error: Error?,
) {
    data class Error(
        val code: String?,
        val message: String?,
    )

    companion object {
        fun <T> success(data: T): ResponseDto<T> {
            return ResponseDto(true, data, null)
        }
    }
}

GlobalControllerAdvice.kt

@RestControllerAdvice
class GlobalControllerAdvice {

    @ExceptionHandler(RuntimeException::class)
    fun handleRuntimeException(ex: RuntimeException): ResponseEntity<ResponseDto<Nothing>>{
        val errorResponse = ResponseDto(
            success = false,
            data = null,
            error = ResponseDto.Error(null,"런타임 에러입니다.")
        )

        return ResponseEntity(errorResponse, HttpStatus.BAD_REQUEST)
    }

    @ExceptionHandler(CustomException::class)
    fun handleCustomException(ex: CustomException): ResponseEntity<ResponseDto<Nothing>>{
        val errorResponse = ResponseDto(
            success = false,
            data = null,
            error = ResponseDto.Error(
                ex.errorCode?: "000",
                ex.errorMsg?: "커스텀 에러입니다")
        )

        return ResponseEntity(errorResponse, HttpStatus.BAD_REQUEST)
    }
}

GlobalControllerAdvice 클래스에서
handleRuntimeException 메소드에는 RuntimeException을 처리하는 @ExceptionHandler를 선언하고
handleCustomException 메소드에는 CustomeException을 처리하는 @ExceptionHandler를 선언했습니다.

메소드 내부 로직에 해당 에러가 발생했을시 원하는 결과를 반환하도록 짜주면 됩니다.

컨트롤러를 만들어서 테스트를 해줍니다.

AdviceTestController.kt

@RequestMapping("/test")
@RestController
class AdviceTestController {

    @GetMapping("/runerror")
    fun runTimeErrorTestException(): ResponseDto<String> {
        return ResponseDto.success(throw RuntimeException())
    }

    @GetMapping("/customerror")
    fun customErrorTestException(): ResponseDto<Any> {
        return ResponseDto.success(throw CustomException(null,null))
    }
}

/test/runerror로 접속시
{ success = false, data = null, error = { errorCode = null, errorMsg = "런타임 에러입니다 } }

/test/customerror로 접속시
{ success = false, data = null, error = { errorCode = "000", errorMsg = "커스텀 에러입니다 } }

컨트롤러 단에서 글로벌하게 에러를 처리할 수 있다는 것을 확인할 수 있었습니다.

profile
There are no two words in the English language more harmful than “good job”.

0개의 댓글