Skip to content

fix(backend): 잘못된 요청을 500 대신 4xx 로 응답 - #250

Merged
i3months merged 1 commit into
devfrom
fix/client-errors-as-4xx
Sep 27, 2026
Merged

i3months merged 1 commit into
devfrom
fix/client-errors-as-4xx

Conversation

@i3months

Copy link
Copy Markdown
Member

발견 경위

인증·권한 경계를 점검하다가 본문 없는 PUT /api/sessions/8/messages/1/bookmark 가 404 가 아니라 500 을 내는 걸 봤습니다. 권한 우회인가 싶어 파봤더니 권한 문제가 아니라 전역 문제였습니다.

GlobalExceptionHandler 가 처리하는 예외는 6종뿐이고, 아래 네 가지가 빠져 catch-all 로 떨어지고 있었습니다.

왜 고쳐야 하는가

운영 로그의 SYS_INTERNAL_ERROR 를 전수 집계했습니다:

11건 전체
   5  HttpMessageNotReadableException
   4  NoResourceFoundException
   1  MethodArgumentTypeMismatchException
   1  HttpRequestMethodNotSupportedException
──────────────────────────────────────────
   0  진짜 서버 버그

500 의 100% 가 클라이언트 오류입니다. 지금 실제 장애가 나면 오타 URL과 구분되지 않습니다. 특히 존재하지 않는 경로(/api/nope)가 500 이라 스캐너 한 대면 에러 로그가 통째로 오염됩니다.

부수적으로 클라이언트 재시도 로직도 망가집니다 — 5xx 는 보통 재시도 대상이라, 영원히 성공하지 못할 요청을 반복하게 됩니다.

변경 내용

상황 이전 이후
본문 누락·깨진 JSON·필드 타입 불일치 500 400 MALFORMED_REQUEST
경로변수 변환 실패 (/api/sessions/abc) 500 400 MALFORMED_REQUEST
존재하지 않는 경로 500 404 ENDPOINT_NOT_FOUND
지원하지 않는 메서드 500 405 METHOD_NOT_ALLOWED

로그 레벨도 낮췄습니다 — NoResourceFoundException 은 debug(봇 스캔이 상시 들어옵니다), 나머지는 warn. ERROR 에는 진짜 장애만 남습니다.

파싱 실패 응답에 원문 예외 메시지를 싣지 않습니다. HttpMessageNotReadableException 의 메시지에는 깨진 본문 조각(= 사용자 입력)이 들어오기 때문에 그대로 돌려주면 입력 반사가 됩니다. 고정 문구만 내보내고 원인은 예외 타입으로만 로그에 남깁니다. 회귀 테스트로 고정했습니다.

권한 경계 점검 결과 (이 PR 의 계기, 문제 없음)

user3 로그인 상태에서 다른 사용자(1·2·4·5) 리소스에 읽기만 시도했습니다:

대상 결과
타 사용자 세션 상세·전사·피드백·리포트MD (7종) 전부 404 SESSION_NOT_FOUND
타 사용자 문서·원문 (4종) 전부 404 DOC_NOT_FOUND
타 사용자 음성 오디오 404
/api/internal/* (JWT 사용자로) 전부 403 ACCESS_DENIED

403 이 아니라 404 로 존재 여부조차 흘리지 않는 점이 좋았습니다. /actuator/** 는 Spring 설정상 permitAll 이지만 nginx 가 백엔드로 프록시하지 않아 외부 노출은 없고, 노출 대상도 health,info 로 제한돼 있습니다.

테스트

  • GlobalExceptionHandlerClientErrorTest 6건 — 네 예외의 상태·코드, 입력 반사 방지, 전 코드 4xx 보장
  • ./gradlew test — 실패는 Testcontainers 5개 클래스뿐(로컬 Docker 없음, 이 변경과 무관, CI 에서 검증)

영향 범위

  • DB 마이그레이션: 없음
  • API contract: 상태 코드 변경 — 기존에 500 을 받던 잘못된 요청이 400/404/405 로 바뀝니다. 정상 요청 경로는 영향 없습니다.
  • 환경변수: 없음

리뷰어 체크포인트

  • NoResourceFoundException 을 debug 로 낮췄습니다. 404 를 아예 안 보고 싶지 않다면 info 가 나을 수 있습니다.
  • 프론트가 500 을 보고 분기하는 코드가 있는지 — 확인은 못 했습니다. 잘못된 요청을 보내는 경로가 있었다면 이제 400 을 받습니다.

🤖 Generated with Claude Code

권한 경계 점검 중, 본문 없는 PUT 이 404 가 아니라 500 을 내는 걸 발견했다. 파보니
권한 문제가 아니라 전역 문제였다 — GlobalExceptionHandler 에 핸들러가 없어서 네 종류의
클라이언트 오류가 전부 catch-all 로 떨어져 500 + ERROR 로그 + 스택트레이스를 남기고
있었다.

운영 로그의 SYS_INTERNAL_ERROR 11건을 세어보니 **전부 이 네 종류**였고 진짜 서버
버그는 0건이었다. 즉 실제 장애가 나도 오타 URL·봇 스캔에 묻혀 구분할 수 없는 상태다.
특히 존재하지 않는 경로(/api/nope)가 500 이라, 스캐너 한 대면 에러 로그가 오염된다.

- HttpMessageNotReadableException  → 400 MALFORMED_REQUEST (본문 누락·깨진 JSON)
- MethodArgumentTypeMismatchException → 400 MALFORMED_REQUEST (/api/sessions/abc)
- NoResourceFoundException → 404 ENDPOINT_NOT_FOUND (로그는 debug — 봇 스캔 상시)
- HttpRequestMethodNotSupportedException → 405 METHOD_NOT_ALLOWED
- 전부 WARN 이하로 낮춰 ERROR 는 진짜 장애만 남게 했다.

파싱 실패 응답에는 원문 예외 메시지를 싣지 않는다 — 깨진 본문 조각(사용자 입력)이
그대로 반사된다. 고정 문구만 내보내고 원인은 예외 타입으로만 로그에 남긴다.

권한 경계 자체는 이상 없었다: 타 사용자 세션·전사·피드백·문서·오디오 GET 12종이
전부 404(존재 여부도 흘리지 않음), 내부 API 는 JWT 사용자에게 403.
@i3months
i3months merged commit ffac121 into dev Sep 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant