📚 Spring Boot(Kotlin) Server Setup — Series Overview
- Why Multi-Module Architecture? (Architecture Philosophy & System Design)
- Designing the API Response Format
- Global Exception Handling (GlobalExceptionHandler) ← Current Episode
- Swagger (OpenAPI) Configuration
- Security (JWT) Fundamentals
- JWT TokenProvider
- Redis Configuration
- Validation Setup
- Logging + MDC(traceId)
- application.yml Profile Separation (local/dev/prod)
- Multi-Module + JPA Base Structure
- 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?
❌ 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
✅ 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.
- 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:
- 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 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)
}
✅ 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)
}
✅ 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)
}
✅ 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)
}
✅ 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
)
)
}
✅ 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)
}
✅ 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)
✅ 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)
✅ 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
)
✅ Supports multilingual messages via message keys
✅ Stores HTTP statuses for consistency
✅ Useful for collaboration across frontend/backend/QA
✅ Conclusion
✅ 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