Skip to content

Feat/#143 응답 통일 - GlobalExceptionHandler 구체화#144

Merged
kingmingyu merged 8 commits into
developfrom
feat/#143
Jun 3, 2026
Merged

Feat/#143 응답 통일 - GlobalExceptionHandler 구체화#144
kingmingyu merged 8 commits into
developfrom
feat/#143

Conversation

@kingmingyu
Copy link
Copy Markdown
Collaborator

@kingmingyu kingmingyu commented Jun 1, 2026

📌 관련 이슈

🚀 개요

이번 PR에서 변경된 핵심 내용을 요약해주세요.

서버에서 내려주는 500 error를 잡기 위해 GlobalException을 구체화했습니다.

📄 작업 내용

구체적인 작업 내용을 설명해주세요.

  • Global Error Code 확장
  • Error Response에 errors 필드 추가
  • AppException에 bind필드 추가

📸 스크린샷 / 테스트 결과 (선택)

결과물 확인을 위한 사진이나 테스트 로그를 첨부해주세요.

@Valid DTO 유효성 검사 실패

  • 기존
image
  • 변경
image

필수 @RequestParam 누락

  • 기존
image
  • 변경
image

@RequestParam 타입 불일치

  • 기존
image
  • 변경
image

JSON 특정 필드 타입 변환 실패

  • 기존
image
  • 변경
image

JSON 문법 오류

  • 기존
image
  • 변경
image

Request Body 자체 누락

  • 기존
image
  • 변경
image

지원하지 않는 HTTP 메서드

  • 기존
image
  • 변경
image

존재하지 않는 URI

  • 기존
image
  • 변경
image

지원하지 않는 Content-Type

  • 기존
image
  • 변경
image

✅ 체크리스트

  • 브랜치 전략(GitHub Flow)을 준수했나요?
  • 메서드 단위로 코드가 잘 쪼개져 있나요?
  • 테스트 통과 확인
  • 서버 실행 확인
  • API 동작 확인

🔍 리뷰 포인트 (Review Points)

리뷰어가 중점적으로 확인했으면 하는 부분을 적어주세요. (P1~P4 적용 가이드)

  • (예: 이 로직이 최선일까요? P2)

  • (예: 예외 처리 누락 여부 확인 부탁드립니다. P1)

  • P1: 혹시 더 추가할 부분이 있을까요?

  • P1: 프론트 응답 쪽에서는 error필드만 추가되는 건데 괜찮겠죠..?

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

  • Bug Fixes

    • 예외 처리 범위 및 안정성 대폭 개선 — 다양한 요청 오류에 대한 일관된 응답 제공
    • 기존 에러 코드 포맷 및 매핑 정비로 진단 정확도 향상
  • New Features

    • 에러 응답에 상세한 필드별 정보(errors) 포함 — 문제 원인 파악 및 사용자 안내 강화
    • 파라미터 불일치·바디 파싱·미지원 메서드·미지원 컨텐츠 타입 등 세분화된 오류 구분 및 응답 추가

@kingmingyu kingmingyu requested review from jinnieusLab and ojy0903 June 1, 2026 09:21
@kingmingyu kingmingyu self-assigned this Jun 1, 2026
@kingmingyu kingmingyu added the ✨ Feature 새로운 기능 추가 label Jun 1, 2026
@kingmingyu kingmingyu linked an issue Jun 1, 2026 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Walkthrough

이 PR은 예외 처리 파이프라인을 통합하여 AppException의 bind 메타데이터, 세분화된 ErrorCode, ErrorResponse의 errors 필드, 그리고 GlobalExceptionHandler의 확장된 예외 핸들러들을 추가해 구조화된 에러 응답을 반환하도록 변경합니다.

Changes

예외 처리 파이프라인 통합

Layer / File(s) Summary
AppException 확장 - bind 필드 추가
src/main/java/com/whereyouad/WhereYouAd/global/exception/AppException.java
Map 기반의 bind 필드를 도입하고, 기존 생성자는 bind = null로 초기화하며 AppException(BaseErrorCode, Map<String,String>) 생성자를 추가했습니다.
ErrorCode enum 세분화
src/main/java/com/whereyouad/WhereYouAd/global/exception/ErrorCode.java
에러 코드를 HTTP 상태별로 세분화하고 code 문자열 포맷을 COMMON_<HTTP_STATUS>_<번호> 형태로 변경해 JSON 파싱 실패, 파라미터 불일치, 지원되지 않는 메서드/컨텐츠 등 세부 케이스를 표현합니다.
ErrorResponse 확장
src/main/java/com/whereyouad/WhereYouAd/global/response/ErrorResponse.java
응답에 포함할 errors(Object) 필드를 추가하고, of(BaseErrorCode, HttpServletRequest, Object) 정적 팩토리를 제공하며 기존 oferrors = null을 전달합니다.
GlobalExceptionHandler 통합
src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java
검증(Validation), 파싱(Json), HTTP 관련 예외들의 전용 핸들러를 추가/정리했습니다. 필드 검증은 Map<String,String>으로 수집하고, 타입 미스매치 등은 파라미터 맵을 만들어 ErrorResponse.of(..., errors)로 반환합니다. AppException 처리에 e.getBind()를 포함하도록 변경되었습니다.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Controller
  participant GlobalExceptionHandler
  participant ErrorResponse
  Client->>Controller: 요청 (검증/파싱 실패 발생)
  Controller->>GlobalExceptionHandler: 예외 전달
  GlobalExceptionHandler->>GlobalExceptionHandler: 오류 상세를 Map으로 수집 (필드/파라미터/바인드)
  GlobalExceptionHandler->>ErrorResponse: ErrorResponse.of(ErrorCode, request, errors)
  ErrorResponse-->>Client: 구조화된 에러 응답 반환
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested Labels

🐛 Bug

Suggested Reviewers

  • jinnieusLab
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 핵심 변경 내용을 명확하게 요약하고 있습니다: GlobalExceptionHandler를 구체화하여 다양한 예외 상황을 처리하도록 개선했습니다.
Linked Issues check ✅ Passed 변경 사항이 issue #143의 요구 사항을 충족합니다: GlobalExceptionHandler의 구체화, Map 기반 오류 정보 추가, 500 에러를 구체적인 에러 코드로 처리하도록 개선되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 issue #143의 범위 내에 있습니다: AppException의 bind 필드 추가, ErrorCode 확장, GlobalExceptionHandler 개선, ErrorResponse 구조 확대가 모두 예외 처리 통일이라는 목표에 부합합니다.
Description check ✅ Passed PR 설명이 필수 섹션(관련 이슈, 개요, 작업 내용, 체크리스트, 리뷰 포인트)을 모두 포함하고 있으며, 구체적인 스크린샷을 통해 변경사항을 명확하게 보여주고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#143

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6a59971 and e3ab225.

📒 Files selected for processing (4)
  • src/main/java/com/whereyouad/WhereYouAd/global/exception/AppException.java
  • src/main/java/com/whereyouad/WhereYouAd/global/exception/ErrorCode.java
  • src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java
  • src/main/java/com/whereyouad/WhereYouAd/global/response/ErrorResponse.java

Comment thread src/main/java/com/whereyouad/WhereYouAd/global/exception/ErrorCode.java Outdated
Comment thread src/main/java/com/whereyouad/WhereYouAd/global/exception/ErrorCode.java Outdated
Comment on lines +42 to +43
log.error("NullPointerException 발생 — 파일: {}, 라인: {}",
e.getStackTrace()[0].getFileName(), e.getStackTrace()[0].getLineNumber());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +211 to +216
e.getParameterValidationResults().forEach(result ->
errors.put(
result.getMethodParameter().getParameterName(),
result.getResolvableErrors().get(0).getDefaultMessage()
)
);
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Jun 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 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" .
fi

Repository: 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 || true

Repository: 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)"
fi

Repository: 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 || true

Repository: 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 || true

Repository: 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 || true

Repository: 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:


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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

무슨 오류인지 설명해줘

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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 에러

( ・ᴗ・ )🐇 ← 이 부분만 조금 고치면 훨씬 안전해져요!

Comment on lines +290 to +298
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() + "'을(를) 변환할 수 없습니다.");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +309 to +316
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));
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Jun 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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:


🏁 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

무슨 오류인지 설명해줘

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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)로 응답하게 됨 ❌

✅ 더 안전한 방법 (제안)

예외의 타입으로 판단하는 거예요. 이미 InvalidFormatExceptioninstanceof로 체크하는 것처럼요:

// 변경 전 (문자열 의존 - 취약)
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)은 훨씬 안정적인 기준이에요! 🐇

Copy link
Copy Markdown
Collaborator

@ojy0903 ojy0903 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4: 헉 오류 발생할때마다 좀 더 구체화 하는게 좋을 것 같아서 한번 리팩터링 해야겠다고 생각은 하고있었는데 바로 해주셨네요...!! 👍
제가 보기엔 핸들러는 이정도면 충분한 것 같고 응답 필드에서도 기존 필드가 사라지는게 아니고 errors 필드 하나 추가되는거라 제 생각에는 괜찮은 것 같습니다!!

++
제꺼 플랫폼 연동 해제 API 도 작업은 해놨는데 제가 일이 몇가지 겹쳐서 좀 늦어지네요...ㅠㅠ 되는대로 PR 올리겠습니다.

@kingmingyu
Copy link
Copy Markdown
Collaborator Author

P4: 헉 오류 발생할때마다 좀 더 구체화 하는게 좋을 것 같아서 한번 리팩터링 해야겠다고 생각은 하고있었는데 바로 해주셨네요...!! 👍 제가 보기엔 핸들러는 이정도면 충분한 것 같고 응답 필드에서도 기존 필드가 사라지는게 아니고 errors 필드 하나 추가되는거라 제 생각에는 괜찮은 것 같습니다!!

++ 제꺼 플랫폼 연동 해제 API 도 작업은 해놨는데 제가 일이 몇가지 겹쳐서 좀 늦어지네요...ㅠㅠ 되는대로 PR 올리겠습니다.

좋습니다! 천천히 해주셔도 됩니다!

Copy link
Copy Markdown
Collaborator

@jinnieusLab jinnieusLab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4: 오 에러가 구체적으로 나와서 확인할 때 훨씬 편할 것 같네요..! 충분히 다 적어주셔서 더 추가할 에러는 일단 없어 보여요 프론트 쪽에도 문제는 없을 듯 합니다! 고생하셨습니다!!👍

@kingmingyu kingmingyu merged commit 9da4d60 into develop Jun 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 응답 통일 - GeneralExceptionAdvice 구체화

3 participants