본문 바로가기
개발 (ENG)

Spring Boot (Kotlin) — ep.3 Global Exception Handling (GlobalExceptionHandler)

by 새싹 아빠 2025. 11. 24.

📚 Spring Boot(Kotlin) Server Setup — Series Overview

  1. Why Multi-Module Architecture? (Architecture Philosophy & System Design)
  2. Designing the API Response Format
  3. Global Exception Handling (GlobalExceptionHandler) ← Current Episode
  4. Swagger (OpenAPI) Configuration
  5. Security (JWT) Fundamentals
  6. JWT TokenProvider
  7. Redis Configuration
  8. Validation Setup
  9. Logging + MDC(traceId)
  10. application.yml Profile Separation (local/dev/prod)
  11. Multi-Module + JPA Base Structure
  12. Final Project Template git share

📌 Why Do We Need Global Exception Handling?

In an API server, various types of errors naturally occur:

  • Invalid user input
  • Violation of domain rules
  • JSON parsing failures
  • Requests to non-existing resources
  • Internal server/system errors

If we try to handle these in each controller?

❌ Duplicate code
❌ Inconsistent response formats
❌ Higher maintenance cost

This is why we need a GlobalExceptionHandler.
In other words, it provides a “single centralized mechanism to handle all exceptions occurring in the server.”

 

📌 Core Responsibilities of GlobalExceptionHandler

✅ Handle all exceptions in one place
✅ Standardize API Response structure
✅ Standardize HTTP status codes
✅ Apply logging and security rules
✅ Improve debugging efficiency

In short, the GlobalExceptionHandler acts as the Final Gate of the API server.

 

📌 Separation of Responsibilities: API Module vs Application Module

Exception classes can exist in both the API module and the Application module.

However, in this project, we intentionally split their responsibilities. The reason is that the Application module represents the domain and business logic layer.

Some errors in domain logic are not system failures, but rather business rule violations.

Example: "You do not have permission to delete this."

Since these situations belong closer to domain logic, it is natural to define and throw them in the Application layer.

✅ Role of the Application Module
- Evaluate business rules
- Define rule violation exceptions
- Throw exceptions at the nearest point
- Example: "This action cannot be canceled in the current state."

Meanwhile, the API module has a different responsibility:

✅ Role of the API Module (GlobalExceptionHandler)
- Handle exceptions thrown by the Application layer
- Also handle unexpected system-wide errors
- Handles both “Business Exceptions + System Exceptions”
- Determines HTTP status and JSON response
- Applies logging and security
- Produces the final response format sent to the client

In summary:

The Application layer “decides and throws exceptions”
The API layer “catches all exceptions and converts them into responses”
The API module is the Final Gate of exception handling

 

📌 GlobalExceptionHandler Code Structure

Let’s walk through how each type of exception is handled. (The ApiResponse structure is covered in Episode 2.)

1️⃣ Business Exception Handling


@ExceptionHandler
fun handleBusiness(e: BusinessException): ResponseEntity<ApiResponse> {
    val body = ApiResponse.fail(
        code = e.errorCode.code,
        messageKey = e.customMessage ?: e.errorCode.messageKey,
        detail = e.detail
    )
    return ResponseEntity.status(e.errorCode.status).body(body)
}
✅ Handles developer-defined business failures
✅ Business failures are considered “valid failures”
✅ Logs only WARN level
✅ HTTP status comes from ErrorCode

 

2️⃣ @Valid Validation Errors


@ExceptionHandler
fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException): ResponseEntity<ApiResponse> {
    val fieldError = e.bindingResult.fieldErrors.firstOrNull()
    val detail = fieldError?.let {
        mapOf(
            "field" to it.field,
            "reason" to (it.defaultMessage ?: "Invalid value"),
            "rejectedValue" to it.rejectedValue
        )
    }

    val body = ApiResponse.fail(
        code = ErrorCode.INVALID_INPUT.code,
        messageKey = ErrorCode.INVALID_INPUT.messageKey,
        detail = detail
    )

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body)
}
✅ Handles @RequestBody validation failures
✅ Provides field-level error details
✅ Returns HTTP 400 Bad Request

 

3️⃣ @ModelAttribute Validation Errors


@ExceptionHandler
fun handleBindException(e: BindException): ResponseEntity<ApiResponse> {
    val fieldError = e.bindingResult.fieldErrors.firstOrNull()
    val detail = fieldError?.let {
        mapOf(
            "field" to it.field,
            "reason" to (it.defaultMessage ?: "Invalid value"),
            "rejectedValue" to it.rejectedValue
        )
    }

    val body = ApiResponse.fail(
        code = ErrorCode.INVALID_INPUT.code,
        messageKey = ErrorCode.INVALID_INPUT.messageKey,
        detail = detail
    )

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body)
}
✅ Handles query/form validation failures
✅ Same structure as @Valid failures
✅ Returns HTTP 400 Bad Request

 

4️⃣ JSON Parsing Errors


@ExceptionHandler
fun handleJsonParse(e: HttpMessageNotReadableException):
        ResponseEntity<ApiResponse> {

    val body = ApiResponse.fail(
        code = ErrorCode.INVALID_JSON.code,
        messageKey = ErrorCode.INVALID_JSON.messageKey,
        detail = mapOf(
            "reason" to "Invalid JSON format"
        )
    )

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body)
}
✅ Handles malformed JSON or invalid types
✅ Returns HTTP 400 Bad Request
✅ Provides helpful detail messages

 

5️⃣ 404 Not Found


@ExceptionHandler
fun handleNotFound(e: NoResourceFoundException): ResponseEntity<ApiResponse> {
    return ResponseEntity.status(HttpStatus.NOT_FOUND)
        .body(
            ApiResponse.fail(
                code = ErrorCode.RESOURCE_NOT_FOUND.code,
                messageKey = ErrorCode.RESOURCE_NOT_FOUND.messageKey,
                detail = e.message
            )
        )
}
✅ Handles requests to non-existing resources
✅ Returns HTTP 404
✅ Does not log as a business error

 

6️⃣ Unexpected System Errors


@ExceptionHandler(Exception::class)
fun handleException(e: Exception): ResponseEntity<ApiResponse> {

    val body = ApiResponse.fail(
        code = ErrorCode.INTERNAL_ERROR.code,
        messageKey = ErrorCode.INTERNAL_ERROR.messageKey
    )

    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
        .body(body)
}
✅ Final handler for all unanticipated errors
✅ Returns HTTP 500 Internal Server Error
✅ The API module becomes the final safeguard

 

📌 Why Do We Need BusinessException?


abstract class BusinessException(
    val errorCode: ErrorCode,
    val detail: Any? = null,
    val customMessage: String? = null
) : ApplicationException(customMessage ?: errorCode.messageKey)
✅ Represents business rule violations
✅ Not a system failure, but a contextual failure
✅ Thrown near domain logic
✅ Enables unified responses using ErrorCode

 

📌 The Role of ApplicationException


abstract class ApplicationException(
    override val message: String
) : RuntimeException(message)
✅ A base abstraction for application-layer exceptions
✅ Groups business exceptions under a single hierarchy
✅ Separates domain failures from system-level exceptions

 

📌 Why ErrorCode Exists


enum class ErrorCode(
    val code: String,
    val messageKey: String,
    val status: Int
)
✅ Identifies every failure via code
✅ Supports multilingual messages via message keys
✅ Stores HTTP statuses for consistency
✅ Useful for collaboration across frontend/backend/QA

 

✅ Conclusion

📌 Key Takeaways
✅ Application Layer: decides & throws
✅ API Layer (GlobalExceptionHandler): handles & converts to responses
✅ Final safeguard for all system exceptions
✅ Increases standardization, scalability, and maintainability

As the project grows, exception handling becomes a “philosophy.”
And the principle is simple:
“Let the domain stay as domain logic, and let the API focus on delivery.”

 

📌 Next Episode

Episode 4 — Swagger (OpenAPI) Configuration
📌 We’ll explore how to automate API documentation and improve collaboration.

 

https://jaemoi8.tistory.com/35

 

Spring Boot (Kotlin) — ep.4 Swagger (OpenAPI) Configuration

📚 Spring Boot(Kotlin) Server Setup — Series OverviewWhy Multi-Module Architecture? (Architecture Philosophy & System Design)API Response Format DesignGlobal Exception Handling (GlobalExceptionHandler)Swagger(OpenAPI) Configuration ← You are hereSecu

jaemoi8.tistory.com