ExceptionHandling (kotlin X Spring)

Jayson·2025년 4월 21일
0
post-thumbnail

@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: 스프링이 제공하는 기본적인 예외 처리 기능을 확장하여 사용합니다.

이 클래스는 여러 가지 예외를 처리하는 메소드로 구성되어 있습니다. 하나씩 살펴볼게요.

1️⃣ JSON 파싱 오류 처리

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()
}

이렇게 상세하게 메시지를 만들어 클라이언트에게 정확한 정보를 제공합니다.


2️⃣ 요청 값 검증 실패 처리

override fun handleMethodArgumentNotValid(
    ex: MethodArgumentNotValidException,
    headers: HttpHeaders,
    status: HttpStatusCode,
    request: WebRequest
): ResponseEntity<Any> {

요청한 데이터가 검증 규칙(예: 이메일 형식, 필수 값 누락)을 통과하지 못했을 때 발생합니다.

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
    .body(ApiResponse.error(ex.messages()))

실패한 검증 항목과 이유를 사용자에게 알려줍니다.


3️⃣ 잘못된 인수나 상태 예외 처리

@ExceptionHandler(IllegalArgumentException::class, IllegalStateException::class)
fun handleBadRequestException(exception: RuntimeException): ResponseEntity<ApiResponse<Unit>> {

비즈니스 로직상 잘못된 값이나 상태에서 발생하는 예외를 처리합니다.


4️⃣ 엔티티 미발견 예외 처리

@ExceptionHandler(EntityNotFoundException::class)
fun handleNotFoundException(exception: EntityNotFoundException): ResponseEntity<ApiResponse<Unit>> {

요청된 데이터가 데이터베이스에 없을 때 발생하는 예외입니다.


5️⃣ 전역 예외 처리

@ExceptionHandler(Exception::class)
fun handleGlobalException(exception: Exception): ResponseEntity<ApiResponse<Unit>> {

앞에서 정의하지 않은 모든 예외를 처리하는 최후의 방어선입니다. 클라이언트에게는 내부 오류 메시지로 응답합니다.


🎯 예외 처리 클래스가 중요한 이유

이렇게 전역적으로 예외를 관리하면 다음과 같은 장점이 있습니다.

  • 일관성 있는 응답 구조: 클라이언트는 항상 일정한 포맷으로 응답을 받으므로, 에러 핸들링이 간편합니다.
  • 명확한 오류 메시지 제공: 개발자와 사용자 모두 어떤 문제인지 명확하게 알 수 있어 문제 해결에 유리합니다.

이제 코틀린만 알고 있던 여러분들도 스프링을 이용한 예외 처리의 기본 원리를 이해하셨을 겁니다. 앞으로도 쉽고 깔끔한 코드를 작성할 수 있도록, 스프링을 차근차근 배워나가길 응원합니다!

profile
Small Big Cycle

0개의 댓글