[Spring] Spring Boot 예외 처리 설명 & 예제 (Kotlin)

728x90

Exception Annotation

  1. @ControllerAdvice : Global 예외 처리 및 특정 pakage / Controller 예외 처리
  2. @ExceptionHandler : 특정 Controller의 예외 처리

GlobalControllerAdvice

//@RestControllerAdvice(basePackageClasses = [ExceptionApiController::class]) -> Target 설정 가능
//@RestControllerAdvice   // RestController의 Exception이 이 컨트롤러를 통하게 됨(Global : 괄호X)
class GlobalControllerAdvice {  // 특정 예외를 잡겠다고 지정

    @ExceptionHandler(value = [RuntimeException::class])    //Runtime Error 터지면 아무거나 다잡음 -> 지정하지 않았는데 처리 되었음 -> Advice 명시 해주거나, 정말 표준적인 것만 처리
    fun exception(e: RuntimeException): String {
        return "Server Error"
    }

//    @ExceptionHandler(value = [IndexOutOfBoundsException::class])
//    fun indexOutOfBoundsException(e: IndexOutOfBoundsException): ResponseEntity<String> { // 200 OK, Catch 됨
//        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Index Error")
//    }
// 고객사를 위한 익셉션.. 등등을 글로벌 하게 잡아 줄 수 있
// 특정 클래스 , 특정 함수 등 특정할 수 도 있다.

}
@RestController
@RequestMapping("/api/exception")
@Validated
class ExceptionApiController {  // 연습용 Api, 실무는 X

    @GetMapping("/hello")
    fun hello() {
        val list = mutableListOf<String>()
        val temp = list[0]
    }

    @GetMapping("")
    fun get(
        @NotBlank
        @Size(min = 2, max = 6)
        @RequestParam name: String,

        @Min(10)
        @RequestParam age: Int
    ): String {
        //통과된 경우만 -> print(Validated)
        println(name)
        println(age)
        return name + " " + age
    }

    @PostMapping("")
    fun post(@Valid @RequestBody userRequest: UserRequest): UserRequest {  // bindingResult: BindingResult  -> 결과 받아준다, Validation 사용하기 때문에 삭제
        println(userRequest)
        return userRequest
    }

    @ExceptionHandler(value = [MethodArgumentNotValidException::class]) // createdAt -> 형식 오류
    fun methodArgumentNotValidException(
        e: MethodArgumentNotValidException,
        request: HttpServletRequest
    ): ResponseEntity<ErrorResponse> {   //매개 변수 설정
        val errors = mutableListOf<Error>()

        e.bindingResult.allErrors.forEach { errorObject ->
            val error = Error().apply {

                this.field = (errorObject as FieldError).field  // 형변환 field Name 불러 올 수 있음
                this.message = errorObject.defaultMessage
                this.value = errorObject.rejectedValue  //rejected Value
            }
            errors.add(error)
        }

        // 2. ErrorResponse
        val errorResponse = ErrorResponse().apply {
            this.resultCode = "FAIL"
            this.httpStatus = HttpStatus.BAD_REQUEST.value().toString()
            this.httpMethod = request.method
            this.message = "요청에 에러가 발생하였습니다."
            this.path = request.requestURI.toString()   // 현재 request를 찾아서 주입해 줌
            this.timestamp = LocalDateTime.now()
            this.errors = errors
        }

        // 3. Return -> ResponseEntity
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse)
    }

    @ExceptionHandler(value = [ConstraintViolationException::class])    // 어떤 에러를 가지는지 가져올 수 있음, 이번에는 GET, requestParam Error(Validation)
    fun constraintVaolationException(
        e: ConstraintViolationException,
        request: HttpServletRequest
    ): ResponseEntity<ErrorResponse> {
        // 1. 에러 분석
        val errors = mutableListOf<Error>() // 에러 하나하나 넣어준다 : forEach

        e.constraintViolations.forEach {
            val error = Error().apply {
                this.field = it.propertyPath.last().name // 마지막에 변수 이름 들어 있음
                this.message = it.message
                this.value = it.invalidValue
            }
            errors.add(error)
        }

        // 2. ErrorResponse
        val errorResponse = ErrorResponse().apply {
            this.resultCode = "FAIL"
            this.httpStatus = HttpStatus.BAD_REQUEST.value().toString()
            this.httpMethod = request.method
            this.message = "요청에 에러가 발생하였습니다."
            this.path = request.requestURI.toString()   // 현재 request를 찾아서 주입해 줌
            this.timestamp = LocalDateTime.now()
            this.errors = errors
        }

        // 3. Return -> ResponseEntity
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse)
    }

//        if (true) {
//            throw RuntimeException("강제 exception 발생")   //hello method 실행되면 바로 Exception -> java.lang.RuntimeException: 강제 exception 발생
//        }

    // 컨트롤러 내부에 Exception Handler가 있다면 Advice타지 않고 이 곳을 탄다 , 각 장단점이 있다.
    // 클래스 내부의 경우는 해당 컨트롤러 안에서만 일어나는 예외 한번에 처리 가능
    // 너무 예외가 많아지면 가독성이 떨어지는 단점이 있다.
    @ExceptionHandler(value = [IndexOutOfBoundsException::class])
    fun indexOutOfBoundsException(e: IndexOutOfBoundsException): ResponseEntity<String> { // 200 OK, Catch 됨
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Index Error")
    }

}

결과 화면 예시(Response설계..)

data class ErrorResponse(

    @field:JsonProperty("result_code")  //snake pattern 으로 변환(실제 Response)에서는
    var resultCode: String? = null,

    @field:JsonProperty("http_status")
    var httpStatus: String? = null,

    @field:JsonProperty("http_method")
    var httpMethod: String? = null,

    var message: String? = null,
    var path: String? = null,
    var timestamp: LocalDateTime? = null,
    var errors: MutableList<Error>? = mutableListOf()
)

data class Error(
    var field: String? = null,
    var message: String? = null,
    var value: Any? = null
)

 


GitHub

GitHub - SeungyeonHwang/restapi-crud-test: RESTapi Test with Kotlin and Spring Boot

728x90

댓글

Designed by JB FACTORY