Post

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 필드를 추가하면 더 좋지 않을까?

이는 불가능하다.

throw Exception을 return 처럼 정보 반환 용도로 사용해서는 안된다

This post is licensed under CC BY 4.0 by the author.