CommonMessageException 정의하기
요구사항
- HTTP 요청을 처리하다 발생한 일반적인 에러 상황에서 클라이언트에게 노출할 (code, message)를 가지고 GlobalExceptionHandler까지 빠르게 탈출 하기 위한 커스텀 Exception이 필요하다.
- code : 클라이언트에서 코드 별 분기 처리 하기 위함.
- message : 실패 상세 정보를 전달하기 위함.
- 클라이언트가 수신하는 code, message의 일관성을 유지하기 위해 공통 응답 코드 Enum을 사용하고 있다.
- 특정 응답 코드(e.g., 2999, 3999)들은 클라이언트 노출 message를 매번 다르게 설정 할 수 있어야 한다.
- e.g., external API 응답 message를 시스템 내 매핑 관리하기에는 너무 많아서, 그대로 bypass해야 하는 경우.
- e.g., 다건 업데이트 작업 도중 실패가 발생한 경우, 어느 항목에서 실패가 발생했는지를 클라이언트 쪽에 내려주어야 함.
- 클라이언트에게 노출할 presentation message와 내부 추적을 위한 logging message는 다를 수 있다.
- 로깅 누락 방지를 위해 GlobalExceptionHandler에서 로깅해야 한다. 이 때, 에러의 중요도에 따라 로깅 레벨이나 알림 동작을 다르게 설정할 수 있어야 한다. (중요하지 않은 에러는 WARN)
해결방안
ResponseCodeMessageException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CommonException(
val level: Level,
val code: String,
val presentationMessage: String,
val loggingMessage: String = presentationMessage,
override val cause: Throwable? = null
) : RuntimeException(loggingMessage, cause) {
constructor(
level: Level,
responseCodeMessageEnum: ResponseCodeMessageEnum,
loggingMessage: String = responseCodeMessageEnum.message,
cause: Throwable? = null,
): this(level, responseCodeMessageEnum.code, responseCodeMessageEnum.message, loggingMessage, cause)
override fun toString(): String {...}
}
Enum
1
2
3
4
5
enum class ResponseCodeMessageEnum(val code: String, val message: String) {
Success("1000", "성공"),
NotFound("4000", "요청한 자원이 존재하지 않습니다."),
...
}
GlobalExcepionHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ExceptionHandler(CommonException::class)
fun handleCommonException(e: CommonException, request: HttpServletRequest): ResponseEntity<Any> {
// request.requestURI 관련 로깅 prefix 추가
when (e.level) {
Level.TRACE -> logger.trace("### $e", e.cause)
Level.DEBUG -> logger.debug("### $e", e.cause)
Level.INFO -> logger.info("### $e", e.cause)
Level.WARN -> logger.warn("### $e", e.cause)
Level.ERROR -> logger.error("### $e", e.cause)
}
if (e.level.toInt() > Level.INFO.toInt()) {
notifier.notify(e.loggingMessage)
}
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_JSON)
.body("[${e.level}] ${e.presentationMessage}");
}
- 분명 이건 오류가 맞긴 맞으니 ExceptionHandler에서 처리하고 싶은데, 심각하지 않은 오류라서 Level.INFO로 기록하고 Notify 하고 싶지 않을 수도 있다.
- 이런 세부적인 처리를 하기 위한 별도 CommonException을 정의하고, 기타 Exception(e.g., WebClientRequestException) 발생 시 CommonException으로 변환해서 던지는 방법을 사용.
- WebClientRequestException 전체에 대해 일괄 처리를 적용하는 것 보다 나은 경우가 많음.
- CommonException은 다음과 같은 상황에서 의미가 있음.
- 위 예제는 단순 log, notify만 하기 때문에 각 사용처에서 직접 찍어주어도 되겠으나 이 보다 처리가 복잡한 경우. (확장성 고려)
- return…return 이 아니라 한 번에 GlobalExceptionHandler 까지 탈출하는 용도 (그 밖에 orElseThrow 등)
CommonMessageException에 data: T 필드를 추가하면 더 좋지 않을까?
이는 불가능하다.
This post is licensed under CC BY 4.0 by the author.