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. 마무리
이렇게 예외를 감지해 처리하는 전역 예외 핸들러를 만들면 기능 별로 모으고 의존성을 낮추는 작업이 되는 것 같아 만드는 맛이 있는 것 같다.
사람마다 만드는 스타일이나 더 좋은 방향이 있을 수 있으니 참고 정도로만 봐주면 좋을 것 같다.
'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 |
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. 마무리
이렇게 예외를 감지해 처리하는 전역 예외 핸들러를 만들면 기능 별로 모으고 의존성을 낮추는 작업이 되는 것 같아 만드는 맛이 있는 것 같다.
사람마다 만드는 스타일이나 더 좋은 방향이 있을 수 있으니 참고 정도로만 봐주면 좋을 것 같다.
'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 |