개발일지

[개발일지_safeHome] 8. Custom Exception 개발

woopii 2025. 12. 28. 13:10

 

비지니스 로직 처리 중 발생하는 엣지케이스들은 왠만하면 예외로 처리할려고 한다.

그래서 Custom Exception을 만들것이다.

 

1. ErrorCode 정의

예외 응답을 위한 ErrorCode는 아래와 같이 작성함

너무 상세하게 원인을 적지는 않고, 예외 타입 정도만 작성함

 

package com.woopi.safehome.global.exception

import org.springframework.http.HttpStatus

enum class ErrorCode(
    val code: String,
    val httpStatus: HttpStatus,
    val defaultMessage: String
) {
    NOT_FOUND("NOT_FOUND", HttpStatus.NOT_FOUND, "데이터를 찾을 수 없습니다."),
    VALIDATION_FAILED("VALIDATION_FAILED", HttpStatus.BAD_REQUEST, "요청 값이 유효하지 않습니다."),
    CONSTRAINT_VIOLATION("CONSTRAINT_VIOLATION", HttpStatus.BAD_REQUEST, "요청 파라미터가 유효하지 않습니다."),
    INTERNAL_SERVER_ERROR("INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다.")
}

 

 

2. BusinessException 정의

커스텀 예외를 처리할 BusinessException을 정의한다.

 

package com.woopi.safehome.global.exception

class BusinessException(
    val errorCode: ErrorCode,
    override val message: String = errorCode.defaultMessage,
    val details: Any? = null
) : RuntimeException(message) {

    // 하위 호환성을 위한 프로퍼티
    val code: String get() = errorCode.code

}

 

 

3. Exception Handler 추가

 

package com.woopi.safehome.global.exception.handler

import com.woopi.safehome.global.exception.BusinessException
import com.woopi.safehome.global.exception.ErrorCode
import com.woopi.safehome.global.response.ApiResponse
import jakarta.validation.ConstraintViolationException
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler

@ControllerAdvice
class GlobalExceptionHandler {

    /**
     * BusinessException 처리
     */
    @ExceptionHandler(BusinessException::class)
    fun handleBusinessException(e: BusinessException): ResponseEntity<ApiResponse<Nothing?>> {
        return createErrorResponse(e.errorCode, e.details)
    }

    /**
     * DTO @Valid 검증 실패 (RequestBody)
     */
    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity<ApiResponse<Nothing?>> {
        val errorDetails = ex.bindingResult.fieldErrors.associate { fieldError ->
            fieldError.field to (fieldError.defaultMessage ?: "잘못된 입력입니다.")
        }

        return createErrorResponse(ErrorCode.VALIDATION_FAILED, errorDetails)
    }

    /**
     * 파라미터 제약조건 실패 (RequestParam, PathVariable)
     */
    @ExceptionHandler(ConstraintViolationException::class)
    fun handleConstraintViolation(ex: ConstraintViolationException): ResponseEntity<ApiResponse<Nothing?>> {
        val errorDetails = ex.constraintViolations.associate { violation ->
            violation.propertyPath.toString() to violation.message
        }

        return createErrorResponse(ErrorCode.CONSTRAINT_VIOLATION, errorDetails)
    }

    /**
     * 일반적인 Exception 처리
     */
    @ExceptionHandler(Exception::class)
    fun handleGenericException(ex: Exception): ResponseEntity<ApiResponse<Nothing?>> {
        return createErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, ex.localizedMessage)
    }

    private fun createErrorResponse(
        errorCode: ErrorCode,
        details: Any? = null
    ): ResponseEntity<ApiResponse<Nothing?>> {
        val response = ApiResponse.error(
            code = errorCode.code,
            message = errorCode.defaultMessage,
            details = details
        )
        return ResponseEntity.status(errorCode.httpStatus).body(response)
    }

}