From ea9388dc076a5f84afcc88e97f810c29b565b75b Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Wed, 9 Sep 2026 11:54:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC(GlobalExceptionHandler)=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BusinessException·Validation 예외·그 외 Exception을 ApiResponse 포맷으로 변환하는 GlobalExceptionHandler를 추가한다. MDC 로그 상관관계를 위한 MdcKeys(REQUEST_ID, USER_ID) 상수도 함께 추가한다. --- .../stream/GlobalExceptionHandler.java | 48 +++++++++++++++++++ .../kr/ac/kookmin/stream/logging/MdcKeys.java | 11 +++++ 2 files changed, 59 insertions(+) create mode 100644 api/common-api/src/main/java/kr/ac/kookmin/stream/GlobalExceptionHandler.java create mode 100644 gateway/logging/src/main/java/kr/ac/kookmin/stream/logging/MdcKeys.java diff --git a/api/common-api/src/main/java/kr/ac/kookmin/stream/GlobalExceptionHandler.java b/api/common-api/src/main/java/kr/ac/kookmin/stream/GlobalExceptionHandler.java new file mode 100644 index 0000000..de1e702 --- /dev/null +++ b/api/common-api/src/main/java/kr/ac/kookmin/stream/GlobalExceptionHandler.java @@ -0,0 +1,48 @@ +package kr.ac.kookmin.stream; + +import static kr.ac.kookmin.stream.logging.MdcKeys.REQUEST_ID; +import static kr.ac.kookmin.stream.logging.MdcKeys.USER_ID; + +import kr.ac.kookmin.stream.common.BusinessException; +import kr.ac.kookmin.stream.common.CommonErrorCode; +import kr.ac.kookmin.stream.common.ErrorStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(BusinessException.class) + public ResponseEntity> handleBusiness(BusinessException e) { + log.warn("[requestId={}, userId={}] ({}) {}", + MDC.get(REQUEST_ID), MDC.get(USER_ID), e.getErrorCode().name(), e.getMessage()); + return ResponseEntity.status(e.getErrorCode().status()).body(ApiResponse.error(e)); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .findFirst().map(FieldError::getDefaultMessage) + .orElse(CommonErrorCode.INVALID_INPUT.message()); + log.warn("[requestId={}, userId={}] (INVALID_INPUT) {}", + MDC.get(REQUEST_ID), MDC.get(USER_ID), message); + return ResponseEntity.status(CommonErrorCode.INVALID_INPUT.status()) + .body(ApiResponse.error(CommonErrorCode.INVALID_INPUT.name(), message)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleInternal(Exception e) { + log.error("[requestId={}, userId={}] {}", + MDC.get(REQUEST_ID), MDC.get(USER_ID), e.getMessage(), e); + return ResponseEntity.status(ErrorStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error(CommonErrorCode.INTERNAL_SERVER_ERROR)); + } +} diff --git a/gateway/logging/src/main/java/kr/ac/kookmin/stream/logging/MdcKeys.java b/gateway/logging/src/main/java/kr/ac/kookmin/stream/logging/MdcKeys.java new file mode 100644 index 0000000..661a38e --- /dev/null +++ b/gateway/logging/src/main/java/kr/ac/kookmin/stream/logging/MdcKeys.java @@ -0,0 +1,11 @@ +package kr.ac.kookmin.stream.logging; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class MdcKeys { + + public static final String REQUEST_ID = "requestId"; + public static final String USER_ID = "userId"; +}