@RestControllerAdvice
, ExceptionHandler
, ApiResponse
로 예외를 구조화하기
→ 관련 PR 보러가기
코틀린(Kotlin)을 공부하면서 자주 부딪히게 되는 문제 중 하나는 바로 예외 처리(Exception Handling)입니다. 특히 웹 애플리케이션을 개발할 때, 사용자의 잘못된 요청이나 서버 내부 오류 등을 명확하게 처리하는 게 중요합니다.
이번 글에서는 코틀린 언어를 알고 있지만, 스프링(Spring) 프레임워크를 잘 모르는 분들을 위해, 주어진 예외 처리 코드를 꼼꼼하게 살펴보면서 설명해 드리겠습니다!
먼저, 코드가 있는 패키지를 볼까요?
package com.terning.server.kotlin.ui.api
위 패키지는 API 요청에 대한 응답과 관련된 코드를 담고 있습니다.
ApiResponse
먼저 ApiResponse
클래스부터 살펴볼게요.
data class ApiResponse<T>(
val message: String? = "",
val body: T? = null
) {
companion object {
fun error(message: String?): ApiResponse<Unit> = ApiResponse(message = message)
fun <T> success(body: T?): ApiResponse<T> = ApiResponse(body = body)
}
}
이 클래스는 API 요청의 응답을 일관된 형태로 전달하기 위한 것입니다. 일반적으로 웹 API는 성공하거나 실패했을 때 메시지와 데이터를 함께 응답으로 내려줍니다.
message
: 응답과 함께 내려가는 메시지body
: 실제 응답 데이터특히 중요한 건 companion object
로 선언된 두 메소드입니다.
error(message: String?)
: 오류가 발생했을 때 메시지를 담아 응답success(body: T?)
: 정상 처리되었을 때 데이터를 담아 응답이 구조를 통해 클라이언트는 항상 동일한 형식으로 응답을 받을 수 있습니다.
ExceptionHandler
)다음으로 살펴볼 클래스는 전역에서 발생하는 모든 예외를 관리하는 클래스입니다.
@RestControllerAdvice
class ExceptionHandler : ResponseEntityExceptionHandler() {
@RestControllerAdvice
: 모든 REST API 요청에서 발생하는 예외를 이 클래스에서 처리하겠다고 스프링에게 알립니다.ResponseEntityExceptionHandler
: 스프링이 제공하는 기본적인 예외 처리 기능을 확장하여 사용합니다.이 클래스는 여러 가지 예외를 처리하는 메소드로 구성되어 있습니다. 하나씩 살펴볼게요.
override fun handleHttpMessageNotReadable(
ex: HttpMessageNotReadableException,
headers: HttpHeaders,
status: HttpStatusCode,
request: WebRequest
): ResponseEntity<Any> {
JSON 데이터가 잘못된 형식으로 들어왔을 때 발생하는 예외입니다.
MismatchedInputException
: 필수 필드가 누락됐을 때InvalidFormatException
: 필드의 데이터 형식이 잘못됐을 때val message = when (val exception = ex.cause) {
is MismatchedInputException -> "${exception.path.lastOrNull()?.fieldName ?: "UnknownField"}: 널이어서는 안됩니다"
is InvalidFormatException -> "${exception.path.lastOrNull()?.fieldName ?: "UnknownField"}: 올바른 형식이어야 합니다"
else -> exception?.message.orEmpty()
}
이렇게 상세하게 메시지를 만들어 클라이언트에게 정확한 정보를 제공합니다.
override fun handleMethodArgumentNotValid(
ex: MethodArgumentNotValidException,
headers: HttpHeaders,
status: HttpStatusCode,
request: WebRequest
): ResponseEntity<Any> {
요청한 데이터가 검증 규칙(예: 이메일 형식, 필수 값 누락)을 통과하지 못했을 때 발생합니다.
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(ex.messages()))
실패한 검증 항목과 이유를 사용자에게 알려줍니다.
@ExceptionHandler(IllegalArgumentException::class, IllegalStateException::class)
fun handleBadRequestException(exception: RuntimeException): ResponseEntity<ApiResponse<Unit>> {
비즈니스 로직상 잘못된 값이나 상태에서 발생하는 예외를 처리합니다.
@ExceptionHandler(EntityNotFoundException::class)
fun handleNotFoundException(exception: EntityNotFoundException): ResponseEntity<ApiResponse<Unit>> {
요청된 데이터가 데이터베이스에 없을 때 발생하는 예외입니다.
@ExceptionHandler(Exception::class)
fun handleGlobalException(exception: Exception): ResponseEntity<ApiResponse<Unit>> {
앞에서 정의하지 않은 모든 예외를 처리하는 최후의 방어선입니다. 클라이언트에게는 내부 오류 메시지로 응답합니다.
이렇게 전역적으로 예외를 관리하면 다음과 같은 장점이 있습니다.
이제 코틀린만 알고 있던 여러분들도 스프링을 이용한 예외 처리의 기본 원리를 이해하셨을 겁니다. 앞으로도 쉽고 깔끔한 코드를 작성할 수 있도록, 스프링을 차근차근 배워나가길 응원합니다!