Spring Boot 3와 Kotlin 환경에서 MVC와 WebFlux 각각의 글로벌(중앙) 에러 핸들링 방법을 총정리합니다. 실무에서 자주 마주치는 에러 처리 패턴과 실전 적용 방법을 초보자도 쉽게 이해할 수 있도록 설명합니다. MVC와 WebFlux의 차이점, 각 방식의 장단점, 그리고 코드 예시까지 한 번에 정리합니다.

들어가며

Spring Boot 3는 최신 자바와 코틀린 환경에서 더욱 강력한 기능을 제공합니다. 웹 애플리케이션을 개발하다 보면 예외 상황을 효과적으로 처리하는 것이 매우 중요합니다. 특히, REST API나 웹 서비스에서 일관된 에러 응답을 제공하는 것은 사용자 경험과 유지보수성에 큰 영향을 미칩니다.

Spring Boot에서는 크게 두 가지 웹 프레임워크(MVC, WebFlux)를 지원하며, 각각의 에러 핸들링 방식이 다릅니다. 본 포스트에서는 Spring Boot 3 + Kotlin 환경에서 MVC와 WebFlux 각각의 글로벌 에러 핸들링 방법을 상세히 소개합니다.


1. Spring Boot 3 + Kotlin 환경 준비

Spring Boot 3와 Kotlin을 함께 사용하는 환경은 build.gradle.kts 또는 pom.xml에 spring-boot-starter-web, spring-boot-starter-webflux, kotlin 관련 의존성을 추가하면 쉽게 구성할 수 있습니다.

// build.gradle.kts 예시
plugins {
    id("org.springframework.boot") version "3.0.0"
    id("io.spring.dependency-management") version "1.0.15.RELEASE"
    kotlin("jvm") version "1.9.0"
    kotlin("plugin.spring") version "1.9.0"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web") // MVC
    implementation("org.springframework.boot:spring-boot-starter-webflux") // WebFlux
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

2. Spring MVC에서의 글로벌 에러 핸들링

2-1. @ControllerAdvice와 @ExceptionHandler

Spring MVC에서는 @ControllerAdvice@ExceptionHandler를 활용해 전역적으로 예외를 처리할 수 있습니다. 이 방식은 동기 방식의 전통적인 웹 애플리케이션에 적합합니다.

@RestControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(CustomException::class)
    fun handleCustomException(ex: CustomException): ResponseEntity<ErrorResponse> {
        val response = ErrorResponse(
            code = ex.errorCode,
            message = ex.message ?: "알 수 없는 오류가 발생했습니다."
        )
        return ResponseEntity.status(ex.status).body(response)
    }

    @ExceptionHandler(Exception::class)
    fun handleException(ex: Exception): ResponseEntity<ErrorResponse> {
        val response = ErrorResponse(
            code = "INTERNAL_ERROR",
            message = ex.localizedMessage ?: "서버 내부 오류가 발생했습니다."
        )
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response)
    }
}

// 예시용 커스텀 예외 및 응답 클래스
class CustomException(val errorCode: String, val status: HttpStatus, message: String): RuntimeException(message)
data class ErrorResponse(val code: String, val message: String)
2-2. HandlerExceptionResolver

보다 세밀한 제어가 필요하다면 HandlerExceptionResolver를 구현할 수도 있습니다. 하지만 실무에서는 대부분 @ControllerAdvice를 활용하는 것이 유지보수에 유리합니다.


3. Spring WebFlux에서의 글로벌 에러 핸들링

WebFlux는 비동기/논블로킹 환경을 지원하는 리액티브 웹 프레임워크입니다. MVC와는 다른 방식으로 예외를 처리해야 하며, 대표적으로 @ControllerAdvice@ExceptionHandler도 지원하지만, 내부적으로는 Mono/Flux 기반의 리턴 타입을 사용해야 합니다.

3-1. @ControllerAdvice + Mono/Flux
@RestControllerAdvice
class GlobalWebFluxExceptionHandler {
    @ExceptionHandler(CustomException::class)
    fun handleCustomException(ex: CustomException): Mono<ResponseEntity<ErrorResponse>> {
        val response = ErrorResponse(
            code = ex.errorCode,
            message = ex.message ?: "알 수 없는 오류가 발생했습니다."
        )
        return Mono.just(ResponseEntity.status(ex.status).body(response))
    }

    @ExceptionHandler(Exception::class)
    fun handleException(ex: Exception): Mono<ResponseEntity<ErrorResponse>> {
        val response = ErrorResponse(
            code = "INTERNAL_ERROR",
            message = ex.localizedMessage ?: "서버 내부 오류가 발생했습니다."
        )
        return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response))
    }
}
3-2. WebExceptionHandler 구현

WebFlux에서 더 낮은 레벨의 글로벌 에러 핸들링이 필요하다면 WebExceptionHandler를 직접 구현할 수 있습니다.

@Component
class CustomWebExceptionHandler : WebExceptionHandler {
    override fun handle(
        exchange: ServerWebExchange,
        ex: Throwable
    ): Mono<Void> {
        val response = exchange.response
        response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR
        response.headers.contentType = MediaType.APPLICATION_JSON

        val errorBody = ErrorResponse(
            code = "INTERNAL_ERROR",
            message = ex.localizedMessage ?: "WebFlux 서버 오류 발생"
        )
        val buffer = response.bufferFactory().wrap(
            ObjectMapper().writeValueAsBytes(errorBody)
        )
        return response.writeWith(Mono.just(buffer))
    }
}

4. MVC vs WebFlux 에러 핸들링 차이점 정리

구분 Spring MVC Spring WebFlux
주요 어노테이션 @ControllerAdvice, @ExceptionHandler @ControllerAdvice, @ExceptionHandler, WebExceptionHandler
리턴 타입 ResponseEntity Mono<ResponseEntity> 또는 Mono
동작 방식 동기 비동기/논블로킹
커스텀 핸들러 HandlerExceptionResolver WebExceptionHandler
활용 예 REST API, 웹사이트 실시간 데이터 처리, 스트리밍
  • MVC는 동기 방식이므로 예외 처리 로직이 직관적이고 단순합니다.
  • WebFlux는 비동기/논블로킹 특성 때문에 Mono/Flux를 리턴해야 하며, 커스텀 핸들러 구현이 더 자주 필요할 수 있습니다.

5. 실전 적용 팁 및 베스트 프랙티스

  • 공통 응답 포맷을 정의하여 일관성 있는 에러 메시지 제공
  • 커스텀 예외 계층을 설계하여 다양한 비즈니스 에러를 구분
  • 로그를 남길 때는 민감 정보가 노출되지 않도록 주의
  • WebFlux에서는 Mono/Flux 리턴을 잊지 말 것
  • 테스트 코드로 에러 상황을 반드시 검증
  • 필요하다면 AOP로 로깅/트래킹 기능 추가

6. 참고 코드 저장소 및 공식 문서


Spring Boot 3 + Kotlin 환경에서 MVC와 WebFlux 모두 글로벌 에러 핸들링을 통해 일관된 API 응답과 유지보수성을 확보할 수 있습니다. 각 방식의 특성을 이해하고, 실무에 맞게 적용해보세요!