ApplicationExceptionHandler.java

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 → KILLED
        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 → KILLED
        if (response.isCommitted()) {
77 1 1. handleMessageNotWritableException : replaced return value with null for com/project/auth/presentation/support/exception/ApplicationExceptionHandler::handleMessageNotWritableException → KILLED
            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 → KILLED
        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 → KILLED
        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
Location : handleBusinessException
Killed by : com.project.auth.presentation.support.exception.ApplicationExceptionHandlerTest.[engine:junit-jupiter]/[class:com.project.auth.presentation.support.exception.ApplicationExceptionHandlerTest]/[method:handleBusinessException_maps_business_error_code_to_status_and_body()]
replaced return value with null for com/project/auth/presentation/support/exception/ApplicationExceptionHandler::handleBusinessException → KILLED

76

1.1
Location : handleMessageNotWritableException
Killed by : com.project.auth.presentation.support.exception.ApplicationExceptionHandlerTest.[engine:junit-jupiter]/[class:com.project.auth.presentation.support.exception.ApplicationExceptionHandlerTest]/[method:handleMessageNotWritableException_returns_500_when_response_not_committed()]
negated conditional → KILLED

77

1.1
Location : handleMessageNotWritableException
Killed by : com.project.auth.presentation.support.exception.ApplicationExceptionHandlerTest.[engine:junit-jupiter]/[class:com.project.auth.presentation.support.exception.ApplicationExceptionHandlerTest]/[method:handleMessageNotWritableException_returns_empty_500_when_response_committed()]
replaced return value with null for com/project/auth/presentation/support/exception/ApplicationExceptionHandler::handleMessageNotWritableException → KILLED

80

1.1
Location : handleMessageNotWritableException
Killed by : com.project.auth.presentation.support.exception.ApplicationExceptionHandlerTest.[engine:junit-jupiter]/[class:com.project.auth.presentation.support.exception.ApplicationExceptionHandlerTest]/[method:handleMessageNotWritableException_returns_500_when_response_not_committed()]
replaced return value with null for com/project/auth/presentation/support/exception/ApplicationExceptionHandler::handleMessageNotWritableException → KILLED

109

1.1
Location : handleUncaughtException
Killed by : com.project.auth.presentation.support.exception.ApplicationExceptionHandlerTest.[engine:junit-jupiter]/[class:com.project.auth.presentation.support.exception.ApplicationExceptionHandlerTest]/[method:handleUncaughtException_always_returns_500_common_999()]
replaced return value with null for com/project/auth/presentation/support/exception/ApplicationExceptionHandler::handleUncaughtException → KILLED

Active mutators

Tests examined


Report generated by PIT 1.19.1