Feat/#143 응답 통일 - GlobalExceptionHandler 구체화#144
Conversation
Walkthrough이 PR은 예외 처리 파이프라인을 통합하여 AppException의 bind 메타데이터, 세분화된 ErrorCode, ErrorResponse의 errors 필드, 그리고 GlobalExceptionHandler의 확장된 예외 핸들러들을 추가해 구조화된 에러 응답을 반환하도록 변경합니다. Changes예외 처리 파이프라인 통합
Sequence DiagramsequenceDiagram
participant Client
participant Controller
participant GlobalExceptionHandler
participant ErrorResponse
Client->>Controller: 요청 (검증/파싱 실패 발생)
Controller->>GlobalExceptionHandler: 예외 전달
GlobalExceptionHandler->>GlobalExceptionHandler: 오류 상세를 Map으로 수집 (필드/파라미터/바인드)
GlobalExceptionHandler->>ErrorResponse: ErrorResponse.of(ErrorCode, request, errors)
ErrorResponse-->>Client: 구조화된 에러 응답 반환
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested Labels
Suggested Reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java (1)
171-172: 💤 Low value반환 타입을 다른 핸들러와 맞춰 주세요 (
ResponseEntity<Object>→ResponseEntity<ErrorResponse>).이 핸들러만
ResponseEntity<Object>를 반환하고 나머지는 전부ResponseEntity<ErrorResponse>예요. 실제로 두 분기 모두ErrorResponse만 내려보내니 타입을 좁혀두면 일관성도 좋고 컴파일 타임 안정성도 올라갑니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java` around lines 171 - 172, The MethodArgumentTypeMismatchException handler in GlobalExceptionHandler (handleMethodArgumentTypeMismatch) currently returns ResponseEntity<Object>; change its signature to return ResponseEntity<ErrorResponse> and ensure the ResponseEntity body creation (the return statements inside handleMethodArgumentTypeMismatch) constructs and returns an ErrorResponse instance (not Object), updating any local variable types if needed and adding/importing the ErrorResponse class if missing so the method matches other handlers and preserves compile-time type safety.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/java/com/whereyouad/WhereYouAd/global/exception/AppException.java`:
- Around line 19-22: The new AppException(BaseErrorCode errorCode, Map<String,
String> bind) constructor is missing a call to the superclass constructor so
e.getMessage() is null; update that constructor (AppException(BaseErrorCode,
Map<String,String>)) to call super(errorCode.getMessage()) before assigning
this.errorCode and this.bind so the RuntimeException message is preserved for
logs and downstream handlers.
In `@src/main/java/com/whereyouad/WhereYouAd/global/exception/ErrorCode.java`:
- Line 22: Change the enum constant NOT_FOUND_REQUEST_BODY in ErrorCode to use
HttpStatus.BAD_REQUEST instead of HttpStatus.NOT_FOUND so that missing request
body maps to 400; update the ErrorCode definition (NOT_FOUND_REQUEST_BODY) so
GlobalExceptionHandler.handleHttpMessageNotReadableException will return a 400
Bad Request via getHttpStatus().
- Line 16: Update the ErrorCode enum entry NOT_SUPPORT_HTTP_METHOD to map to the
correct HTTP status for unsupported methods: change its HttpStatus from
HttpStatus.BAD_REQUEST to HttpStatus.METHOD_NOT_ALLOWED (405) so the enum
(ErrorCode) aligns with Spring's HttpRequestMethodNotSupportedException and HTTP
semantics.
In
`@src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java`:
- Around line 42-43: The current log in GlobalExceptionHandler directly indexes
e.getStackTrace()[0], which can throw ArrayIndexOutOfBoundsException when the
JVM reuses exceptions with empty stack traces; change the handler to safely
access the top stack element by checking that e.getStackTrace() is non-null and
has length>0 before using [0], and if absent fall back to safe values (e.g.,
e.getClass().getName(), e.toString(), or "unknown file/line") so logging never
attempts an out-of-bounds access; update the NullPointerException
handling/logging logic in GlobalExceptionHandler accordingly.
- Around line 290-298: When handling InvalidFormatException in
GlobalExceptionHandler, add guards around cause.getPath() and cause.getValue():
check if cause.getPath() is empty before calling get(0) and choose a safe
default fieldName (e.g., "value" or "root") when empty; check if
cause.getValue() is null before calling toString() and branch the error message
accordingly (null -> treat as empty or use a "'null'을(를) 변환할 수 없습니다." message).
Update the block that computes fieldName and the errors.put(...) logic so it
never calls get(0) on an empty path or toString() on a null value, preserving
the existing messages but using the safe defaults.
- Around line 309-316: Replace the fragile string-match branch in
GlobalExceptionHandler's HttpMessageNotReadableException handling (the if that
checks e.getMessage().contains("JSON parse error")) with a check on the nested
cause type: traverse e.getCause() chain (or use the same root-cause helper used
for InvalidFormatException handling) and detect
com.fasterxml.jackson.core.JsonParseException (or its Jackson supertype) to
identify malformed JSON; when that cause is found, log the error and request
like the current block and return ErrorCode.JSON_PARSE_FAIL, keeping the
existing logging and response behavior but based on the cause type rather than
the message text.
- Around line 211-216: When extracting validation errors from
e.getParameterValidationResults() in the GlobalExceptionHandler, guard against
null/empty values: check that result.getResolvableErrors() is not empty before
calling get(0) and obtain the default message safely, and derive the parameter
name via result.getMethodParameter() and
result.getMethodParameter().getParameterName() but substitute a safe fallback
(e.g. "unknown") if the methodParameter or parameterName is null; then put the
safe key and message into the errors Map instead of directly calling get(0) or
using a potentially null key.
---
Nitpick comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java`:
- Around line 171-172: The MethodArgumentTypeMismatchException handler in
GlobalExceptionHandler (handleMethodArgumentTypeMismatch) currently returns
ResponseEntity<Object>; change its signature to return
ResponseEntity<ErrorResponse> and ensure the ResponseEntity body creation (the
return statements inside handleMethodArgumentTypeMismatch) constructs and
returns an ErrorResponse instance (not Object), updating any local variable
types if needed and adding/importing the ErrorResponse class if missing so the
method matches other handlers and preserves compile-time type safety.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 83f961cb-cd0b-40e8-bd7b-f46f18f2173a
📒 Files selected for processing (4)
src/main/java/com/whereyouad/WhereYouAd/global/exception/AppException.javasrc/main/java/com/whereyouad/WhereYouAd/global/exception/ErrorCode.javasrc/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.javasrc/main/java/com/whereyouad/WhereYouAd/global/response/ErrorResponse.java
| log.error("NullPointerException 발생 — 파일: {}, 라인: {}", | ||
| e.getStackTrace()[0].getFileName(), e.getStackTrace()[0].getLineNumber()); |
There was a problem hiding this comment.
getStackTrace()[0] 접근이 핸들러 안에서 또 다른 예외를 던질 수 있어요.
JVM(HotSpot)은 같은 위치에서 NullPointerException 같은 예외가 반복 발생하면 -XX:+OmitStackTraceInFastThrow 최적화로 스택트레이스가 비어 있는 예외를 재사용합니다. 이때 e.getStackTrace()가 빈 배열이라 [0] 접근에서 ArrayIndexOutOfBoundsException이 터지고, 결국 예외 핸들러가 깔끔한 500 대신 또 다른 예외를 내뱉게 돼요.
🛠️ 제안 수정
- log.error("NullPointerException 발생 — 파일: {}, 라인: {}",
- e.getStackTrace()[0].getFileName(), e.getStackTrace()[0].getLineNumber());
+ StackTraceElement[] trace = e.getStackTrace();
+ if (trace.length > 0) {
+ log.error("NullPointerException 발생 — 파일: {}, 라인: {}",
+ trace[0].getFileName(), trace[0].getLineNumber());
+ } else {
+ log.error("NullPointerException 발생 — 스택트레이스 없음", e);
+ }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java`
around lines 42 - 43, The current log in GlobalExceptionHandler directly indexes
e.getStackTrace()[0], which can throw ArrayIndexOutOfBoundsException when the
JVM reuses exceptions with empty stack traces; change the handler to safely
access the top stack element by checking that e.getStackTrace() is non-null and
has length>0 before using [0], and if absent fall back to safe values (e.g.,
e.getClass().getName(), e.toString(), or "unknown file/line") so logging never
attempts an out-of-bounds access; update the NullPointerException
handling/logging logic in GlobalExceptionHandler accordingly.
| e.getParameterValidationResults().forEach(result -> | ||
| errors.put( | ||
| result.getMethodParameter().getParameterName(), | ||
| result.getResolvableErrors().get(0).getDefaultMessage() | ||
| ) | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# -parameters 컴파일 플래그 사용 여부 확인
rg -nP -C3 '(\-parameters|parameters\s*=\s*true|compilerArgs)' --iglob '*.gradle*' --iglob 'pom.xml'Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 55
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Build files =="
find . -maxdepth 5 -type f \( -name "build.gradle" -o -name "build.gradle.kts" -o -name "pom.xml" \) | sed 's|^\./||'
echo
echo "== Search for -parameters / parameters=true / compilerArgs in build files =="
rg -n -i '(-parameters|\bparameters\s*=\s*true\b|compilerArgs)' --hidden --follow \
--iglob '*/build.gradle*' --iglob '*/pom.xml' || true
echo
echo "== Show GlobalExceptionHandler.java around lines 180-250 =="
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
if [ -f "$FILE" ]; then
nl -ba "$FILE" | sed -n '180,250p'
else
echo "File not found at expected path: $FILE"
echo "== Candidate files =="
fd -t f "GlobalExceptionHandler.java" .
fiRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 454
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Build files =="
mapfile -t BUILD_FILES < <(find . -type f \( -name "build.gradle" -o -name "build.gradle.kts" -o -name "pom.xml" \) -print | sed 's|^\./||')
printf "%s\n" "${BUILD_FILES[@]:-}"
echo
echo "== Search -parameters / parameters=true / compilerArgs in build files (explicit targets) =="
if [ "${`#BUILD_FILES`[@]}" -eq 0 ]; then
echo "No build files found."
else
rg -n -i '(-parameters|\bparameters\s*=\s*true\b|compilerArgs)' "${BUILD_FILES[@]}" || true
fi
echo
echo "== Show GlobalExceptionHandler.java around lines 180-250 (with line numbers) =="
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
if [ -f "$FILE" ]; then
awk 'NR>=180 && NR<=250 {printf "%d\t%s\n", NR, $0}' "$FILE"
else
echo "File not found at: $FILE"
echo "== Candidate files =="
fd -t f "GlobalExceptionHandler.java" .
fi
echo
echo "== Fallback: repo-wide search for -parameters / getParameterName usage =="
rg -n --hidden --follow '\-parameters' . || true
rg -n --hidden --follow 'getParameterName\(\)' src/main/java || true
rg -n --hidden --follow 'getResolvableErrors\(\)' src/main/java || trueRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 166
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Repo root listing =="
ls -la
echo
echo "== Build config candidates (build.gradle / build.gradle.kts / pom.xml) =="
find . -maxdepth 6 -type f \( -name "build.gradle" -o -name "build.gradle.kts" -o -name "pom.xml" \) -print | sed 's|^\./||' || true
echo
echo "== Search for -parameters / parameters=true / compilerArgs in build files =="
rg -n -i '(\-parameters|\bparameters\s*=\s*true\b|compilerArgs)' \
-g 'build.gradle*' -g 'pom.xml' . || true
echo
echo "== Locate GlobalExceptionHandler.java =="
FILE="$(fd -t f 'GlobalExceptionHandler\.java$' . | head -n 1 || true)"
if [ -z "$FILE" ]; then
echo "GlobalExceptionHandler.java not found."
exit 0
fi
echo "Found: $FILE"
echo
echo "== Show code around the reported lines (180-250) =="
awk 'NR>=180 && NR<=250 {printf "%d\t%s\n", NR, $0}' "$FILE"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 4202
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== build.gradle: lines containing -parameters / compilerArgs / parameters=true =="
rg -n --hidden --follow '(-parameters|\bcompilerArgs\b|\bparameters\s*=\s*true\b)' build.gradle || true
echo
echo "== build.gradle: java/toolchain/compileJava related sections (best-effort) =="
rg -n --hidden --follow '(toolchain|compileJava|options\.compilerArgs|JavaCompile|tasks\.withType\(JavaCompile\)|jvmTarget)' build.gradle || true
echo
echo "== build.gradle: print entire compileJava-related blocks (up to ~200 lines around hits) =="
# Show a small context window around any compilerArgs hit; otherwise show near tasks.withType
if rg -n 'compilerArgs' build.gradle >/dev/null 2>&1; then
rg -n -C15 'compilerArgs' build.gradle
elif rg -n 'compileJava' build.gradle >/dev/null 2>&1; then
rg -n -C15 'compileJava' build.gradle
else
echo "(no obvious compileJava/compilerArgs blocks found)"
fiRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 393
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== build.gradle: search broader for parameter-name / compilerArgs settings =="
rg -n --hidden --follow 'parameters\s*=\s*true|-\s*parameters|parameterName|parameter names|compilerArgs|options\.compilerArgs|compileJava|tasks\.withType\(JavaCompile\)|withType\(JavaCompile\)|all\s*JavaCompile|jvmTarget|options\s*\{' build.gradle || true
echo
echo "== build.gradle: show java/toolchain + compileJava/tasks blocks (context) =="
rg -n --hidden --follow -C5 'toolchain|compileJava|withType\(JavaCompile\)|tasks\.' build.gradle || true
echo
echo "== Locate ErrorResponse class =="
ERRFILE="$(fd -t f 'ErrorResponse\.java$' src | head -n 1 || true)"
echo "Found: ${ERRFILE:-none}"
if [ -n "${ERRFILE:-}" ]; then
awk 'NR>=1 && NR<=220 {print NR "\t" $0}' "$ERRFILE" | sed -n '1,220p'
fi
echo
echo "== Locate ErrorResponse.of(...) usage/implementation if multiple constructors =="
rg -n --hidden --follow 'class ErrorResponse|static\s+ErrorResponse\s+of' src/main/java || trueRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 2935
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== build.gradle: search for any JavaCompile/options.compilerArgs/compilerArgs.add patterns =="
rg -n --hidden --follow -i '(options\.compilerArgs|compilerArgs(\.add)?|java\.toolchain|tasks\.withType\(JavaCompile\)|withType\(JavaCompile\)|\-parameters|\bparameters\s*=\s*true\b|parameter\s*names)' build.gradle || true
echo
echo "== Inspect BaseResponse (to see how errors field is serialized) =="
BASEFILE="$(fd -t f 'BaseResponse\.java$' src/main/java | head -n 1 || true)"
echo "Found: ${BASEFILE:-none}"
if [ -n "${BASEFILE:-}" ]; then
awk 'NR>=1 && NR<=220 {print NR "\t" $0}' "$BASEFILE"
fi
echo
echo "== Inspect any Jackson configuration affecting Map null keys =="
# Best-effort: search for ObjectMapper or JsonInclude settings
rg -n --hidden --follow -i '(ObjectMapper|JsonInclude|SerializationFeature|null\s*key|Map<|FAIL_ON|`@Json`' src/main/java || trueRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 965
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for Jackson/ObjectMapper customizations =="
rg -n --hidden --follow -i 'ObjectMapper|JsonInclude|Jackson2ObjectMapperBuilderCustomizer|SerializationFeature|FAIL_ON|`@Json`|`@JsonInclude`' src/main/java || true
echo
echo "== Search where getResolvableErrors() is used =="
rg -n --hidden --follow 'getResolvableErrors\(\)' src/main/java || true
echo
echo "== Search for HandlerMethodValidationException usage/overrides =="
rg -n --hidden --follow 'HandlerMethodValidationException' src/main/java || trueRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 16873
🌐 Web query:
Spring Boot 3 HandlerMethodValidationException getParameterValidationResults ParameterValidationResult getResolvableErrors empty behavior getParameterName null when -parameters not enabled
💡 Result:
In Spring Boot 3 (Spring Framework 6.x and 7.x), the HandlerMethodValidationException is thrown when method-level validation fails [1][2][3]. The behavior you described—null parameter names and potential issues with empty error lists—is tied to how Spring resolves method parameter names [3]. Parameter Names and the -parameters Flag Spring Framework relies on the Java reflection API to determine method parameter names. If the Java compiler is not invoked with the -parameters flag, these names are not stored in the compiled class files. Without this information, Spring may default to generic names like arg0, arg1, or return null, which causes getParameterName on a MethodParameter to return null [3][4][5]. To ensure correct parameter names are available at runtime for validation reporting, you must compile your code with the -parameters compiler flag [3]. Behavior of getResolvableErrors The getResolvableErrors method in ParameterValidationResult returns a list of MessageSourceResolvable objects [6][7]. This list will be empty if there are no validation errors for that specific parameter, but it should not be empty when a HandlerMethodValidationException is thrown, as the exception is only raised when errors exist [3]. If you are encountering an empty list in your exception handler, it may indicate you are iterating over parameters that did not cause the validation failure or that the structure of the result is being misinterpreted [3][4][5]. Recommended Handling Approach To correctly handle these exceptions and extract validation details: 1. Use getParameterValidationResults to access the results [1][8]. 2. For each ParameterValidationResult, check if it contains errors before accessing them [3]. 3. Ensure the -parameters flag is enabled in your build tool (e.g., Maven or Gradle) to avoid null parameter names [3]. Example snippet for your ExceptionHandler: @ExceptionHandler(HandlerMethodValidationException.class) public ResponseEntity handleMethodValidation(HandlerMethodValidationException ex) { List<Map<String, String>> errors = ex.getParameterValidationResults.stream.map(result -> { String name = result.getMethodParameter.getParameterName; // Requires -parameters flag String message = result.getResolvableErrors.stream.map(MessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(", ")); return Map.of("parameter", name!= null? name: "unknown", "error", message); }).collect(Collectors.toList); return ResponseEntity.badRequest.body(errors); }
Citations:
- 1: https://docs.spring.io/spring-framework/docs/7.0.0-M3/javadoc-api/org/springframework/web/method/annotation/HandlerMethodValidationException.html
- 2: https://docs.spring.io/spring-framework/docs/7.0.0-M2/javadoc-api/org/springframework/web/method/annotation/HandlerMethodValidationException.html
- 3: https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidator.java
- 4: https://youbidan-project.tistory.com/388
- 5: https://medium.com/@youable.framios/%EC%98%A4%EB%8A%98-%ED%95%A0-validation-%EB%82%B4%EC%9D%BC%EB%A1%9C-%EB%AF%B8%EB%A3%A8%EC%A7%80-%EB%A7%90%EC%9E%90-spring-boot-custom-validation-annotation-c39b9e9a7bd4
- 6: https://docs.spring.io/spring-framework/docs/6.2.x/javadoc-api/org/springframework/validation/method/ParameterValidationResult.html
- 7: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/method/ParameterValidationResult.html
- 8: https://docs.spring.io/spring-framework/docs/6.2.14/javadoc-api/org/springframework/validation/method/MethodValidationResult.html
HandlerMethodValidationException 에러 추출 시 null/빈 값 방어 추가 필요 (211-216)
build.gradle에-parameters(또는compilerArgs) 설정이 없어result.getMethodParameter().getParameterName()이null일 수 있고, 이 값이 Map 키로 들어가 예외/직렬화 이슈 가능성이 있습니다.result.getResolvableErrors()가 빈 리스트인 경우get(0)에서IndexOutOfBoundsException이 날 수 있으니isEmpty()가드를 추가하고, 키(parameterName)도null이면 대체값("unknown"등)으로 매핑하는 편이 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java`
around lines 211 - 216, When extracting validation errors from
e.getParameterValidationResults() in the GlobalExceptionHandler, guard against
null/empty values: check that result.getResolvableErrors() is not empty before
calling get(0) and obtain the default message safely, and derive the parameter
name via result.getMethodParameter() and
result.getMethodParameter().getParameterName() but substitute a safe fallback
(e.g. "unknown") if the methodParameter or parameterName is null; then put the
safe key and message into the errors Map instead of directly calling get(0) or
using a potentially null key.
There was a problem hiding this comment.
@kingmingyu 좋은 질문이에요! 😊 제가 지적한 부분에는 두 가지 잠재적 버그가 있어요. 하나씩 쉽게 설명해 드릴게요.
문제가 있는 코드 (Lines 211-216)
e.getParameterValidationResults().forEach(result ->
errors.put(
result.getMethodParameter().getParameterName(), // ← 문제 1
result.getResolvableErrors().get(0).getDefaultMessage() // ← 문제 2
)
);🐛 문제 1: getParameterName()이 null을 반환할 수 있어요
Spring은 메서드 파라미터 이름을 알아내기 위해 컴파일 시 -parameters 옵션을 필요로 합니다. 이 옵션이 build.gradle에 없으면 파라미터 이름을 알 수 없어서 getParameterName()이 null을 반환합니다.
그러면 아래처럼 동작해요:
errors.put(null, "에러 메시지");
// → HashMap은 null 키를 허용하지만,
// JSON 직렬화 시 예외 발생 또는 이상한 응답이 나올 수 있어요!실제로 build.gradle에 아래 설정이 없다면 위험합니다:
tasks.withType(JavaCompile) {
options.compilerArgs << '-parameters'
}🐛 문제 2: getResolvableErrors().get(0) — 리스트가 비어 있으면 터져요
getResolvableErrors()가 반환하는 리스트가 비어 있을 때 .get(0)을 호출하면 IndexOutOfBoundsException이 발생합니다. 이게 터지면 오히려 500 에러가 나버려요! 😱
result.getResolvableErrors().get(0) // 리스트가 비어있으면 바로 예외 발생!✅ 안전하게 고치는 방법
e.getParameterValidationResults().forEach(result -> {
String paramName = result.getMethodParameter().getParameterName();
String key = (paramName != null) ? paramName : "unknown"; // null 방어
List<MessageSourceResolvable> resolvableErrors = result.getResolvableErrors();
if (!resolvableErrors.isEmpty()) { // 빈 리스트 방어
errors.put(key, resolvableErrors.get(0).getDefaultMessage());
}
});간단히 요약하면:
| 문제 | 발생 원인 | 결과 |
|---|---|---|
null 키 |
-parameters 컴파일 옵션 누락 |
JSON 직렬화 오류 또는 null 키 응답 |
get(0) |
에러 리스트가 비어 있는 경우 | IndexOutOfBoundsException → 500 에러 |
( ・ᴗ・ )🐇 ← 이 부분만 조금 고치면 훨씬 안전해져요!
| if (e.getCause() instanceof InvalidFormatException cause) { | ||
| Map<String, String> errors = new HashMap<>(); | ||
| String fieldName = cause.getPath().get(0).getFieldName(); | ||
|
|
||
| if (cause.getValue().toString().isEmpty()) { | ||
| errors.put(fieldName, "빈칸을 변환할 수 없습니다."); | ||
| } else { | ||
| errors.put(fieldName, "'" + cause.getValue() + "'을(를) 변환할 수 없습니다."); | ||
| } |
There was a problem hiding this comment.
InvalidFormatException 파싱 시 경계 조건을 확인해 주세요.
cause.getPath().get(0)은 path가 비어 있는 경우(예: 루트 레벨 변환 실패) IndexOutOfBoundsException을, cause.getValue().toString()은 값이 null일 때 NullPointerException을 던질 수 있어요. 드물지만 핸들러 안에서 터지면 통일된 응답 대신 500이 나가니, 가드를 한 줄 넣어두면 안전합니다.
🛠️ 방어 코드 예시
- String fieldName = cause.getPath().get(0).getFieldName();
-
- if (cause.getValue().toString().isEmpty()) {
+ String fieldName = cause.getPath().isEmpty() ? "unknown" : cause.getPath().get(0).getFieldName();
+ Object rawValue = cause.getValue();
+
+ if (rawValue == null || rawValue.toString().isEmpty()) {
errors.put(fieldName, "빈칸을 변환할 수 없습니다.");
} else {
- errors.put(fieldName, "'" + cause.getValue() + "'을(를) 변환할 수 없습니다.");
+ errors.put(fieldName, "'" + rawValue + "'을(를) 변환할 수 없습니다.");
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java`
around lines 290 - 298, When handling InvalidFormatException in
GlobalExceptionHandler, add guards around cause.getPath() and cause.getValue():
check if cause.getPath() is empty before calling get(0) and choose a safe
default fieldName (e.g., "value" or "root") when empty; check if
cause.getValue() is null before calling toString() and branch the error message
accordingly (null -> treat as empty or use a "'null'을(를) 변환할 수 없습니다." message).
Update the block that computes fieldName and the errors.put(...) logic so it
never calls get(0) on an empty path or toString() on a null value, preserving
the existing messages but using the safe defaults.
| if (e.getMessage() != null && e.getMessage().contains("JSON parse error")) { | ||
| log.error("HttpMessageNotReadableException 발생: JSON 파싱 실패 - {}", e.getMessage()); | ||
| log.error("에러가 발생한 지점 {}, {}", request.getMethod(), request.getRequestURI()); | ||
|
|
||
| return ResponseEntity | ||
| .status(ErrorCode.JSON_PARSE_FAIL.getHttpStatus()) | ||
| .body(ErrorResponse.of(ErrorCode.JSON_PARSE_FAIL, request)); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Spring Boot 3.x HttpMessageNotReadableException JSON parse error cause JsonParseException
💡 Result:
In Spring Boot 3.x, HttpMessageNotReadableException is a wrapper exception thrown by the framework when a registered HttpMessageConverter—typically the Jackson-based converter—fails to read or deserialize the incoming request body [1]. When the root cause is a JsonParseException (or other Jackson exceptions like JsonMappingException), it indicates that the provided request body is not valid JSON or does not conform to the expected structure of the target Java DTO [2][3]. Root Cause Analysis: The exception chain often contains the underlying cause within the nested exception [3]. You can access this via: - ex.getCause: The direct cause (e.g., JsonMappingException or JsonParseException) [1][3]. - ex.getMostSpecificCause: A convenient method (from the base class NestedRuntimeException) to reach the root of the error, such as the specific parsing issue identified by Jackson [3][4]. Common Triggers: 1. Malformed JSON (syntax errors) triggering a JsonParseException [5][3]. 2. Schema mismatches, such as missing required fields or type mismatches (e.g., sending a string to an integer field), often triggering a JsonMappingException or InvalidFormatException [2][6][3]. 3. Missing default constructors or inaccessible properties on the target DTO [6]. Recommended Handling Strategy: The most robust approach to handling this exception in Spring Boot 3.x is to use a @RestControllerAdvice combined with ResponseEntityExceptionHandler [7]. By overriding the handleHttpMessageNotReadable method, you can provide a cleaner, consistent API error response instead of the default stack trace: @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity handleHttpMessageNotReadable( HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { String errorMessage = "Malformed JSON request"; // Accessing the specific cause for a more helpful message Throwable mostSpecificCause = ex.getMostSpecificCause; if (mostSpecificCause!= null) { errorMessage = mostSpecificCause.getMessage; } // Return a custom error response DTO return handleExceptionInternal(ex, errorMessage, headers, status, request); } } This allows you to intercept the error before it propagates to the default error handling mechanism, ensuring that clients receive a structured 400 Bad Request response [5][8][4].
Citations:
- 1: https://docs.spring.io/spring-framework/docs/7.0.6/javadoc-api/org/springframework/http/converter/HttpMessageNotReadableException.html
- 2: https://oneuptime.com/blog/post/2025-12-22-httpmessagenotreadableexception-spring/view
- 3: Handle specific reasons of HttpMessageNotReadableException zalando/problem-spring-web#1
- 4: https://medium.com/@dasbabai2017/%EF%B8%8F-the-ultimate-guide-to-exception-handling-in-spring-boot-rest-apis-d4de5f830778
- 5: https://stackoverflow.com/questions/62666481/how-to-handle-org-springframework-http-converter-httpmessagenotreadableexceptio
- 6: spring boot admin throws JSON parse error, please help ! spring boot admin version is 1.5.7 codecentric/spring-boot-admin#892
- 7: https://stackoverflow.com/questions/75145749/exception-handlind-httpmessagenotreadableexception-on-springboot
- 8: https://docs.enterprise.spring.io/spring-boot/3.3/how-to/spring-mvc.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
echo "== Exists? =="; ls -l "$FILE"
echo
echo "== Lines 280-350 (context) =="
nl -ba "$FILE" | sed -n '280,350p'
echo
echo "== Search for JSON_PARSE_FAIL handling =="
rg -n "JSON_PARSE_FAIL|JSON parse error|HttpMessageNotReadableException" "$FILE"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 314
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
echo "== Lines 280-350 (context) =="
awk 'NR>=280 && NR<=350 {printf "%d:%s\n", NR, $0}' "$FILE"
echo
echo "== Search for JSON_PARSE_FAIL / JSON parse error / HttpMessageNotReadableException =="
rg -n "JSON_PARSE_FAIL|JSON parse error|HttpMessageNotReadableException|getMessage\\(\\)" "$FILE"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 5095
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
echo "== Imports (top 1-120) =="
awk 'NR>=1 && NR<=120 {print}' "$FILE"
echo
echo "== Check for JsonParseException usage =="
rg -n "JsonParseException|getMostSpecificCause|mostSpecificCause|JsonMappingException" "$FILE"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 5649
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
echo "== Search for JsonParseException (anywhere in file) =="
rg -n "JsonParseException|getMostSpecificCause|getRootCause" "$FILE" || true
echo
echo "== Lines 200-360 (exception handler area) =="
awk 'NR>=200 && NR<=360 {print NR ":" $0}' "$FILE"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 7736
HTTP JSON 파싱 실패 분기를 e.getMessage() 문자열이 아닌 예외 원인 타입으로 처리하기
e.getMessage().contains("JSON parse error")는 Spring/Jackson 버전(또는 메시지 포맷 변경)에서 조용히 빗나가고, 실제로는 JSON 문법 오류인데 “Request Body 없음” 분기로 떨어질 수 있어요.- Spring Boot 3.x에서 malformed JSON이면
HttpMessageNotReadableException의 nested cause(가장 구체적인 원인)가com.fasterxml.jackson.core.JsonParseException으로 내려오는 편이라, 그 타입으로 분기하는 게 더 안전합니다. (이미InvalidFormatException분기처럼 원인 타입 기반으로 처리하고 있어요.)
🛠️ 제안
- if (e.getMessage() != null && e.getMessage().contains("JSON parse error")) {
+ if (e.getMostSpecificCause() instanceof com.fasterxml.jackson.core.JsonParseException) {
log.error("HttpMessageNotReadableException 발생: JSON 파싱 실패 - {}", e.getMessage());
log.error("에러가 발생한 지점 {}, {}", request.getMethod(), request.getRequestURI());
return ResponseEntity
.status(ErrorCode.JSON_PARSE_FAIL.getHttpStatus())
.body(ErrorResponse.of(ErrorCode.JSON_PARSE_FAIL, request));
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java`
around lines 309 - 316, Replace the fragile string-match branch in
GlobalExceptionHandler's HttpMessageNotReadableException handling (the if that
checks e.getMessage().contains("JSON parse error")) with a check on the nested
cause type: traverse e.getCause() chain (or use the same root-cause helper used
for InvalidFormatException handling) and detect
com.fasterxml.jackson.core.JsonParseException (or its Jackson supertype) to
identify malformed JSON; when that cause is found, log the error and request
like the current block and return ErrorCode.JSON_PARSE_FAIL, keeping the
existing logging and response behavior but based on the cause type rather than
the message text.
There was a problem hiding this comment.
@kingmingyu 쉽게 설명해 드릴게요! 😊
🔍 문제가 된 코드 (현재)
if (e.getMessage() != null && e.getMessage().contains("JSON parse error")) {
// JSON 문법 오류 처리
}❓ 왜 문제인가?
e.getMessage()의 문자열에 의존하고 있기 때문이에요.
Spring/Jackson이 malformed JSON(문법적으로 잘못된 JSON)을 파싱하다 실패하면 HttpMessageNotReadableException을 던지는데, 이 예외의 메시지가 "JSON parse error: ..."라는 형태로 내려와요.
그런데 이 메시지 포맷은 공식 API가 아니에요. Spring Boot나 Jackson 버전이 업그레이드되면 메시지 형태가 달라질 수도 있어요.
💥 어떤 상황이 생기냐면?
예를 들어 프론트에서 이런 잘못된 JSON을 보냈다고 해봐요:
{ "name": "홍길동" ← 닫는 괄호 없음!- 현재 코드:
e.getMessage()에"JSON parse error"문자열이 없어지면 → 이 조건을 통과 못 하고 → 맨 아래 "Request Body 없음" 분기로 떨어짐 - 결과: 실제로는 JSON 문법 오류인데, 잘못된 에러 코드(
NOT_FOUND_REQUEST_BODY)로 응답하게 됨 ❌
✅ 더 안전한 방법 (제안)
예외의 타입으로 판단하는 거예요. 이미 InvalidFormatException을 instanceof로 체크하는 것처럼요:
// 변경 전 (문자열 의존 - 취약)
if (e.getMessage() != null && e.getMessage().contains("JSON parse error")) {
// 변경 후 (타입 기반 - 안전)
if (e.getMostSpecificCause() instanceof com.fasterxml.jackson.core.JsonParseException) {JsonParseException은 Jackson이 JSON 문법 자체가 깨졌을 때 던지는 정식 예외 클래스라, 버전이 바뀌어도 신뢰할 수 있어요.
요약하면: 문자열("JSON parse error")은 언제든 바뀔 수 있는 불안정한 기준이고, 예외 타입(JsonParseException)은 훨씬 안정적인 기준이에요! 🐇
ojy0903
left a comment
There was a problem hiding this comment.
P4: 헉 오류 발생할때마다 좀 더 구체화 하는게 좋을 것 같아서 한번 리팩터링 해야겠다고 생각은 하고있었는데 바로 해주셨네요...!! 👍
제가 보기엔 핸들러는 이정도면 충분한 것 같고 응답 필드에서도 기존 필드가 사라지는게 아니고 errors 필드 하나 추가되는거라 제 생각에는 괜찮은 것 같습니다!!
++
제꺼 플랫폼 연동 해제 API 도 작업은 해놨는데 제가 일이 몇가지 겹쳐서 좀 늦어지네요...ㅠㅠ 되는대로 PR 올리겠습니다.
좋습니다! 천천히 해주셔도 됩니다! |
📌 관련 이슈
🚀 개요
서버에서 내려주는 500 error를 잡기 위해 GlobalException을 구체화했습니다.
📄 작업 내용
📸 스크린샷 / 테스트 결과 (선택)
@Valid DTO 유효성 검사 실패
필수 @RequestParam 누락
@RequestParam 타입 불일치
JSON 특정 필드 타입 변환 실패
JSON 문법 오류
Request Body 자체 누락
지원하지 않는 HTTP 메서드
존재하지 않는 URI
지원하지 않는 Content-Type
✅ 체크리스트
🔍 리뷰 포인트 (Review Points)
(예: 이 로직이 최선일까요? P2)
(예: 예외 처리 누락 여부 확인 부탁드립니다. P1)
P1: 혹시 더 추가할 부분이 있을까요?
P1: 프론트 응답 쪽에서는 error필드만 추가되는 건데 괜찮겠죠..?
Summary by CodeRabbit
Bug Fixes
New Features