DEV/Spring

[Spring] GlobalExceptionHandler 만들기 (with. Kotlin)

2024. 11. 9. 00:03
목차
  1. 0.  개요
  2. 1. 구현
  3. 2. 마무리

0.  개요

 전역 예외처리에 관한 핸들러를 만들어서 관리하는 기능을 코틀린 베이스로 만들려 한다.

이전 RestControllerAdvice, ControllerAdvice 포스팅하면서 말했던 내용을 베이스로 하기 때문에 조금 더 상세한 내용은 아래 링크 참고 바란다.

 

* 이전 포스팅

2023.07.12 - [DEV/Spring] - [Spring] Exception Handler (with. Spring Data Rest)

 

1. 구현

구현하고자 하는 코드 구성은 아래와 같다.

Class (or File) Description
ErrorCode 에러 정의를 담은 Enum 클래스.
DomainException 예외 클래스, RuntimeException 상속.
GlobalRestExceptionHandler 예외를 클래스 별로 감지하여 Response 와 별도 로직을 수행하도록 하는 클래스.
CommonResponse 공용 Response 클래스.
PageMetadata 공용 Response에 속한 Page, Slice Response 클래스.

 

CommonResponse, PageMetadata는

공용 Response와 유사한 구조로 ErrorResponse도 내보내야 클라이언트에서 덜 귀찮으니 함께 포함했다.

(겹치는 부분이 있어 그냥 포함한 거니 개발 성향에 따라서 분리해서 객체를 구성하여도 무관하다.)

 

 

a. ErrorCode

import org.springframework.http.HttpStatus

enum class ErrorCode(val code: String, val message: String, val status: HttpStatus) {
    NOT_FOUND("NOT_FOUND", "The requested resource was not found", HttpStatus.NOT_FOUND),
    INVALID_INPUT("INVALID_INPUT", "Invalid input provided", HttpStatus.BAD_REQUEST),
    INTERNAL_ERROR("INTERNAL_ERROR", "An internal error occurred", HttpStatus.INTERNAL_SERVER_ERROR),
    UNKNOWN_ERROR("UNKNOWN_ERROR", "An unexpected error occurred", HttpStatus.INTERNAL_SERVER_ERROR) // 추가된 코드
}

에러 관련한 코드를 미리 정의하여 담고 있는 Enum Class이다.

 

예외를 발생시킬 때 미리 세팅해두고 사용하면 편하고 수정에 용이한 편이면서,

지금 있는 파라메터 외에 추가로 프로젝트 내에서 사용하는 에러코드를 담아둬도 유용한 편이기에 만들어서 사용한다.

 

b. DomainException

class DomainException(
    val errorCode: ErrorCode, // ErrorCode를 주 생성자로 받음
    message: String? = errorCode.message, // message는 기본값으로 ErrorCode의 detail을 사용
    cause: Throwable? = null // 원인 예외를 선택적으로 받을 수 있음
) : RuntimeException(message, cause) {}

예외 처리를 할 때 ErrorCode 에 대한 매핑과 ExceptionHandler에서 어떤 예외인지 지정할 때 쓰는 용도이다.

RuntimeException을 상속받아 사용한다.

 

왜 Exception이 아닌 RuntimeException을 상속 받아 사용하는지에 대해선

Checked Exception, Unchecked Exception에 대해서 검색해 보도록 하자!

(무조건 RuntimeException 이 맞다는 것보단 왜 사용하는지 알아보면 좋을 듯하다.)

 

c. GlobalRestExceptionHandler

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class GlobalRestExceptionHandler {

    private val logger: Logger = LoggerFactory.getLogger(GlobalRestExceptionHandler::class.java)


    @ExceptionHandler(CustomException::class)
    fun handleCustomException(ex: CustomException): ResponseEntity<CommonResponse<Unit>> {
        val errorCode = ex.errorCode
        val commonResponse = CommonResponse.error(errorCode)
        return ResponseEntity(commonResponse, errorCode.status)
    }

    @ExceptionHandler(Exception::class)
    fun handleGeneralException(ex: Exception): ResponseEntity<CommonResponse<Unit>> {
        // 로그 기록: 알 수 없는 Exception 발생 시
        logger.error("Unknown error occurred", ex)

        val commonResponse = CommonResponse.error(ErrorCode.UNKNOWN_ERROR)
        return ResponseEntity(commonResponse, HttpStatus.INTERNAL_SERVER_ERROR)
    }
}

전역 예외처리를 감지 및 반환 타입을 만드는 Class 파일이다.

@ExceptionHandler() Annotation에 감지하고자 하는 예외 클래스를 지정하고 메서드를 수행하는 방식으로 이해하면 된다.

 

d. CommonResponse

import com.fasterxml.jackson.annotation.JsonInclude
import java.time.LocalDateTime

@JsonInclude(JsonInclude.Include.NON_NULL)
data class CommonResponse<T>(
    val success: Boolean,
    val code: String,
    val message: String,
    val data: T? = null,
    val page: PageMetadata? = null,
    val timestamp: LocalDateTime = LocalDateTime.now()
) {
    companion object {
        fun error(errorCode: ErrorCode): CommonResponse<Unit> {
            return CommonResponse(
                success = false,
                code = errorCode.code,
                message = errorCode.message
            )
        }

        fun <T> successWithPage(data: List<T>, pageMetadata: PageMetadata): CommonResponse<List<T>> {
            return CommonResponse(
                success = true,
                code = "SUCCESS",
                message = "Operation completed successfully",
                data = data,
                page = pageMetadata
            )
        }

        fun <T> success(data: T): CommonResponse<T> {
            return CommonResponse(
                success = true,
                code = "SUCCESS",
                message = "Operation completed successfully",
                data = data
            )
        }
    }
}

공용 Response Class이고 로직 상 크게 영향을 끼치지 않기 때문에 편하게 변경해도 상관없다.

 

e. PageMetadata

import com.fasterxml.jackson.annotation.JsonInclude
import org.springframework.data.domain.Page
import org.springframework.data.domain.Slice

@JsonInclude(JsonInclude.Include.NON_NULL)
data class PageMetadata(
    val pageNumber: Int,
    val pageSize: Int,
    val totalPages: Int? = null,
    val totalElements: Long? = null,
    val isLast: Boolean
) {
    companion object {
        fun from(page: Page<*>): PageMetadata {
            return PageMetadata(
                pageNumber = page.number,
                pageSize = page.size,
                totalPages = page.totalPages,
                totalElements = page.totalElements,
                isLast = page.isLast
            )
        }

        fun from(slice: Slice<*>): PageMetadata {
            return PageMetadata(
                pageNumber = slice.number,
                pageSize = slice.size,
                totalPages = null, // Slice에는 totalPages 정보가 없음
                totalElements = null, // Slice에는 totalElements 정보가 없음
                isLast = slice.isLast
            )
        }
    }
}

 

페이징 정보를 담기 위한 Class.

위 공용 Response와 크게 다르지 않기 때문에 없어도 무관한 Class!

 

2. 마무리

이렇게 예외를 감지해 처리하는 전역 예외 핸들러를 만들면 기능 별로 모으고 의존성을 낮추는 작업이 되는 것 같아 만드는 맛이 있는 것 같다.

사람마다 만드는 스타일이나 더 좋은 방향이 있을 수 있으니 참고 정도로만 봐주면 좋을 것 같다.

728x90

'DEV > Spring' 카테고리의 다른 글

[Spring] Slack 메세지 Custom Annotation 만들기  (0) 2023.12.15
[JPA] QueryDSL 페이징 적용  (0) 2023.11.11
[JPA] QueryDSL 사용자 정의 레포지토리 적용  (2) 2023.11.06
[JPA] QueryDSL 동적 쿼리 적용  (0) 2023.10.26
[JPA] QueryDSL DTO 반환 방법  (0) 2023.10.23
  1. 0.  개요
  2. 1. 구현
  3. 2. 마무리
'DEV/Spring' 카테고리의 다른 글
  • [Spring] Slack 메세지 Custom Annotation 만들기
  • [JPA] QueryDSL 페이징 적용
  • [JPA] QueryDSL 사용자 정의 레포지토리 적용
  • [JPA] QueryDSL 동적 쿼리 적용
l-eazzy
l-eazzy
귀찮고 귀찮은데 귀찮기 싫어서 개발하는 귀찮은 개발자.
귀찮은 개발자의 블로그귀찮고 귀찮은데 귀찮기 싫어서 개발하는 귀찮은 개발자.
l-eazzy
귀찮은 개발자의 블로그
l-eazzy
전체
오늘
어제
  • 분류 전체보기 (86)
    • DEV (66)
      • Language (3)
      • Spring (22)
      • Node (4)
      • Flutter (1)
      • Container (7)
      • Cloud (5)
      • DataBase (2)
      • Airflow (2)
      • On-premise (1)
      • ErorrBox (8)
      • Etc (11)
    • REVIEW (7)
      • IT (7)
    • Daily (10)
      • 출사 (1)
      • 그냥 (1)
      • 오늘의 점심 (7)
    • 아무거나 (2)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • docker
  • JPA
  • springboot
  • AWS
  • Airflow
  • 강남
  • 오블완
  • QueryDSL
  • Spring
  • 티스토리챌린지

최근 댓글

최근 글

hELLO · Designed By 정상우.
l-eazzy
[Spring] GlobalExceptionHandler 만들기 (with. Kotlin)
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.