이번에 Kotlin과 Springboot를 함께 사용하는 프로젝트에 참여하게 됐습니다.
Kotlin + Springboot의 특징은 코틀린의 장점을 가져가면서 스프링부트의 문법을 그대로 사용할 수 있습니다.
널을 안전하게 처리할 수 있고 세미콜론을 안 붙혀도 된다는 점이 가장 큰 장점이 아닐까 합니다.
프로젝트를 시작하기에 앞서 에러 처리를 하기 위해서 ControllerAdvice를 적용해봤습니다.
예외처리를 함에 있어서 가장 큰 불편함은 바로 가독성 문제일 것입니다.
일반적으로 try-catch
문을 사용해서 예외를 처리할 수 있지만, 예외의 종류가 많은 경우 catch()문이 지나치게 많아져 가독성이 떨어지게 됩니다.
추가로, 모든 로직에 try-catch
문을 넣는 것도 매우 비효율적인 구조입니다.
따라서, 예외를 체계적으로 관리하기 위한 방법이 필요합니다.
이러한 방법 중 하나가 ControllerAdvice입니다.
Spring 프레임워크에서 다양하게 에러 처리를 할 수 있는데 그 중 한 방법은 ControllerAdvice입니다.
ControllerAdvice를 사용하게 되면 가장 큰 장점이 전역적으로 예외를 처리할 수 있다는 점입니다.
RestControllerAdvice의 역할 역시 전역적으로 예외를 처리하게 해줍니다.
하지만 @RestControllerAdvice
는 @Controller
와 @RestController
의 차이처럼 @ResponseBody
가 추가되어 Json으로 응답을 해준다는 점이 @ControllerAdvice
와 다릅니다.
즉, @ControllerAdvice
+ @ResponseBody
= @RestControllerAdvice
입니다.
@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 = "커스텀 에러입니다 } }
컨트롤러 단에서 글로벌하게 에러를 처리할 수 있다는 것을 확인할 수 있었습니다.