요약 — DeployHub 업로드의 HTTP 400이 요청별이 아니라 JVM 기동 단위로 몰리는 현상을 단서로 삼았다. Java 17 Map.of의 순회 순서와 특정 외부 API의 본문 순서 요구가 맞물린 경로를 분석하고 순서를 고정한 본문 생성·직렬화 문자열 회귀 검사를 적용했다. 모든 불변 Map이나 JSON 일반 규칙의 문제로 확대하지 않는다.
배경
프로젝트/기능: DeployHub — 완성된 반입 아카이브를 문서 라이브러리에 올리는 업로드 단계
환경: Java 17, Spring Boot 3.5, 외부 문서 API
문제 (증상)
어떤 날은 업로드가 전부 성공한다
어떤 날은 9건이 전부 400으로 실패한다
그 사이 코드는 바뀌지 않았지만 JVM 재기동마다 결과가 달랐다. 요청별 실패율보다 같은 프로세스에서 전건 성공 또는 전건 실패한다는 분포가 원인 탐색의 단서였다.
원인 분석
당시 기록에서는 특정 키의 순서만 바꿨을 때 외부 API 응답이 200/400으로 나뉘었다. 이를 해당 API의 관측 결과로 한정한다. 실제 키·민감값을 제거한 요청/응답 재현 로그가 이 글에 없으므로 공식 API 계약이나 새 독립 재현 완료로 주장하지 않는다.
본문 생성에 사용한 Java 17 Map.of는 순회 순서를 보장하지 않으며 JVM 실행 간 순서가 달라질 수 있다. 특정 구현의 순회 특성과 직렬화 결과를 구분해야 하고, 모든 불변 Map이 같은 방식으로 순서를 정한다고 일반화하지 않는다.
이 순회 결과가 본문 직렬화 순서에 반영되면서 재기동 후 API 성공 여부가 달라졌다. 특정 외부 API에서 관측한 순서 의존성이지 JSON 객체의 일반적인 의미 규칙은 아니다.
가끔 한 요청만 실패하는 것이 아니라 기동 단위로 전건 성공 또는 전건 실패했다. 프로세스 수명 동안 유지되는 순회 순서를 의심하게 한 관측이며 요청마다 새로 무작위 순서를 뽑는다는 뜻은 아니다.
해결
직렬화할 키 순서를 고정하는 본문 생성기로 교체했다. 실제 전송 문자열의 순서가 해당 API에서 관측한 요구와 일치하는지를 검사하는 방식이며, Map 객체의 동등성 비교와 구분한다.
테스트는 객체 비교가 아니라 직렬화된 본문 문자열 자체를 고정했다 — 순서가 문제인데 객체로 비교하면 같다고 나와 잡지 못한다
문서 예시에 있는 일부 필드는 오히려 넣으면 400이 난다는 것도 실측으로 확인해 제외했다