| 1 | package com.project.auth.presentation.support.exception; | |
| 2 | ||
| 3 | import com.project.auth.application.support.exception.BusinessException; | |
| 4 | import com.project.auth.application.support.exception.CommonErrorCode; | |
| 5 | import com.project.auth.application.support.logging.LogSanitizer; | |
| 6 | import com.project.auth.presentation.support.response.ApiResult; | |
| 7 | import com.project.auth.presentation.support.response.ApiResultFactory; | |
| 8 | import jakarta.servlet.http.HttpServletRequest; | |
| 9 | import jakarta.servlet.http.HttpServletResponse; | |
| 10 | import org.slf4j.Logger; | |
| 11 | import org.slf4j.LoggerFactory; | |
| 12 | import org.springframework.core.Ordered; | |
| 13 | import org.springframework.core.annotation.Order; | |
| 14 | import org.springframework.http.HttpStatus; | |
| 15 | import org.springframework.http.ResponseEntity; | |
| 16 | import org.springframework.http.converter.HttpMessageNotWritableException; | |
| 17 | import org.springframework.web.bind.annotation.ExceptionHandler; | |
| 18 | import org.springframework.web.bind.annotation.RestControllerAdvice; | |
| 19 | ||
| 20 | @RestControllerAdvice | |
| 21 | @Order(Ordered.LOWEST_PRECEDENCE) | |
| 22 | public class ApplicationExceptionHandler { | |
| 23 | ||
| 24 | private static final Logger log = LoggerFactory.getLogger(ApplicationExceptionHandler.class); | |
| 25 | ||
| 26 | private final ApiResultFactory apiResultFactory; | |
| 27 | ||
| 28 | public ApplicationExceptionHandler(ApiResultFactory apiResultFactory) { | |
| 29 | this.apiResultFactory = apiResultFactory; | |
| 30 | } | |
| 31 | ||
| 32 | @ExceptionHandler(BusinessException.class) | |
| 33 | public ResponseEntity<ApiResult<Void>> handleBusinessException( | |
| 34 | BusinessException exception, | |
| 35 | HttpServletRequest request | |
| 36 | ) { | |
| 37 | log.warn( | |
| 38 | "Business exception. exceptionType={} errorCode={} method={} requestPath={}", | |
| 39 | exception.getClass().getSimpleName(), | |
| 40 | exception.getErrorCode().code(), | |
| 41 | request.getMethod(), | |
| 42 | LogSanitizer.requestPath(request.getRequestURI()) | |
| 43 | ); | |
| 44 | ||
| 45 |
1
1. handleBusinessException : replaced return value with null for com/project/auth/presentation/support/exception/ApplicationExceptionHandler::handleBusinessException → NO_COVERAGE |
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(exception.getErrorCode())) |
| 46 | .body(apiResultFactory.failure(exception.getErrorCode().code(), exception.getErrorCode().message())); | |
| 47 | } | |
| 48 | ||
| 49 | /** | |
| 50 | * 응답 직렬화 실패에 대한 정직한 처리. | |
| 51 | * | |
| 52 | * 응답이 이미 커밋된 상태(이미 바이트가 wire에 나간 상태)라면 회복 경로가 없다. | |
| 53 | * 같은 직렬화 경로로 본문을 또 쓰려고 하면 동일한 실패가 재발하거나 무시된다. | |
| 54 | * 이때는 본문 없는 빈 ResponseEntity(500)를 반환한다 — @ExceptionHandler가 null을 | |
| 55 | * 반환하면 Spring은 "응답이 작성되지 않음"으로 해석해 리졸버 체인으로 재진입할 수 있고, | |
| 56 | * 우리가 막으려는 재귀 실패가 바로 그것이다. | |
| 57 | * | |
| 58 | * 응답이 아직 커밋되지 않았다면 표준 ApiResult 본문을 시도한다. 직렬화 실패가 | |
| 59 | * 구조적인 원인이면 2차 시도도 같은 이유로 실패할 수 있는데, 그때는 | |
| 60 | * @ExceptionHandler(Exception.class) 안전망이 다시 잡아낸다. | |
| 61 | */ | |
| 62 | @ExceptionHandler(HttpMessageNotWritableException.class) | |
| 63 | public ResponseEntity<ApiResult<Void>> handleMessageNotWritableException( | |
| 64 | HttpMessageNotWritableException exception, | |
| 65 | HttpServletRequest request, | |
| 66 | HttpServletResponse response | |
| 67 | ) { | |
| 68 | log.error( | |
| 69 | "Response body not writable. committed={} method={} requestPath={}", | |
| 70 | response.isCommitted(), | |
| 71 | request.getMethod(), | |
| 72 | LogSanitizer.requestPath(request.getRequestURI()), | |
| 73 | exception | |
| 74 | ); | |
| 75 | ||
| 76 |
1
1. handleMessageNotWritableException : negated conditional → NO_COVERAGE |
if (response.isCommitted()) { |
| 77 |
1
1. handleMessageNotWritableException : replaced return value with null for com/project/auth/presentation/support/exception/ApplicationExceptionHandler::handleMessageNotWritableException → NO_COVERAGE |
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); |
| 78 | } | |
| 79 | ||
| 80 |
1
1. handleMessageNotWritableException : replaced return value with null for com/project/auth/presentation/support/exception/ApplicationExceptionHandler::handleMessageNotWritableException → NO_COVERAGE |
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(PresentationErrorCode.MESSAGE_NOT_WRITABLE)) |
| 81 | .body(apiResultFactory.failure( | |
| 82 | PresentationErrorCode.MESSAGE_NOT_WRITABLE.code(), | |
| 83 | PresentationErrorCode.MESSAGE_NOT_WRITABLE.message() | |
| 84 | )); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * 더 구체적인 @ExceptionHandler에 매칭되지 않은 모든 예외에 대한 최후의 안전망. | |
| 89 | * | |
| 90 | * 이 핸들러가 없으면 매칭되지 않은 예외는 advice를 우회해 {@code /error}로 흘러가고, | |
| 91 | * Spring Boot 기본 응답 형태가 반환되어 {@link ApiResult} 계약이 깨진다. | |
| 92 | * | |
| 93 | * 항상 500 + COMMON-999를 반환하고, 알 수 없는 장애 모드를 운영이 식별할 수 있도록 | |
| 94 | * ERROR 레벨로 전체 스택트레이스를 남긴다. | |
| 95 | */ | |
| 96 | @ExceptionHandler(Exception.class) | |
| 97 | public ResponseEntity<ApiResult<Void>> handleUncaughtException( | |
| 98 | Exception exception, | |
| 99 | HttpServletRequest request | |
| 100 | ) { | |
| 101 | log.error( | |
| 102 | "Uncaught exception reached @ExceptionHandler safety net. exceptionType={} method={} requestPath={}", | |
| 103 | exception.getClass().getName(), | |
| 104 | request.getMethod(), | |
| 105 | LogSanitizer.requestPath(request.getRequestURI()), | |
| 106 | exception | |
| 107 | ); | |
| 108 | ||
| 109 |
1
1. handleUncaughtException : replaced return value with null for com/project/auth/presentation/support/exception/ApplicationExceptionHandler::handleUncaughtException → NO_COVERAGE |
return ResponseEntity.status(ApiErrorHttpStatusMapper.map(CommonErrorCode.INTERNAL_SERVER_ERROR)) |
| 110 | .body(apiResultFactory.failure( | |
| 111 | CommonErrorCode.INTERNAL_SERVER_ERROR.code(), | |
| 112 | CommonErrorCode.INTERNAL_SERVER_ERROR.message() | |
| 113 | )); | |
| 114 | } | |
| 115 | } | |
Mutations | ||
| 45 |
1.1 |
|
| 76 |
1.1 |
|
| 77 |
1.1 |
|
| 80 |
1.1 |
|
| 109 |
1.1 |