화면에서 API1은 1건, API2는 3건이 내려와야 하는데, Network 탭에서 둘 다 3건으로 보이는 현상이 간헐적으로 발생했다. SQL·파라미터·배포를 의심했지만, 최종 원인은 Spring 싱글톤 Bean이 공유 응답 객체를 동시 요청이 덮어쓴 것이었다.
증상
- 팝업(또는 특정 화면) 로드 시 Ajax 2개가 동시에 호출된다
- API1 응답은 1건이어야 정상인데, Network에서 3건으로 보이는 경우가 있다
- API2 응답도 3건이며, API1과 JSON 내용이 완전히 동일하다
- 드롭다운 등 UI에 같은 라벨·다른 코드값이 여러 건 노출되고, 잘못된 값이 저장될 수 있다
- API1·SQL을 단독 호출하면 항상 1건 → DB·SQL·파라미터 문제는 아닌 것으로 보였다
어떻게 좁혔나
1) SQL·파라미터 소거
| 검증 | 결과 |
| API1 SQL 직접 실행 | 항상 1건 |
| curl 등으로 API1 단독 호출 | 항상 1건 |
| 서버 sql.log의 API1 실행 SQL | 필터 조건 정상 포함 |
| 요청 파라미터 (예: parentId) | 정상 전달 |
2) 결정적 로그
[시각] POST .../api1.json → SQL 필터 O, DB 1건
[시각] POST .../api2.json → SQL 필터 X, DB 3건 (수 ms 차이)
SQL은 각각 정상인데 API1 Response만 3건 → DB 이후 레이어를 의심했다.
3) 코드 추적
Service·Controller에 인스턴스 필드로 응답 객체를 하나만 두고, API1·API2가 거의 동시에 setResult 하는 구조였다.
원인
싱글톤 + 공유 가변 상태
- @Service·@Controller는 기본 싱글톤 → 모든 HTTP 요청이 같은 Bean 인스턴스를 쓴다
- 요청마다 새 응답 객체를 만들지 않고, 필드 하나에 결과를 넣고 그 참조를 반환했다
@Service
public class SomeService {
private ApiResponse response = new ApiResponse(); // 모든 요청이 공유
public ApiResponse getListA(String parentId) {
List<Item> list = dao.queryA(parentId); // 예: 3건
response.setResult(list);
return response;
}
public ApiResponse getListB(String parentId) {
List<Item> list = dao.queryB(parentId); // 예: 1건 (필터 적용)
response.setResult(list); // 같은 객체를 덮어씀
return response;
}
}
경쟁 시나리오 (Race Condition)
Thread-A (API1) Thread-B (API2)
─────────────── ───────────────
DB 조회 → 1건
DB 조회 → 3건
response.setResult(1건)
response.setResult(3건) ← 덮어씀
return → 클라이언트에 3건 return → 3건
- 마지막에 setResult한 쪽이 양쪽 HTTP 응답 모두에 반영될 수 있다
- API2가 나중이면 API1 Response에도 3건이 나간다 → 두 JSON이 완전히 동일해진다
- 스레드 스케줄링에 따라 1건/3건이 간헐적으로 바뀌어 보인다
왜 단독 호출은 항상 정상인가
- curl·SQL 직접 실행 = 스레드 1개 → 경쟁 없음
- 화면 로드 = API1·API2 동시 Ajax → 재현 가능
해결
요청마다 새 응답 객체를 생성하도록 변경했다.
private ApiResponse successResult(Object data) {
ApiResponse response = new ApiResponse();
response.setResult(data);
response.setResultCode("OK");
return response;
}
public ApiResponse getListB(String parentId) {
List<Item> list = dao.queryB(parentId);
return successResult(list);
}
Controller의 공유 응답 필드도 제거하고, return service.method(...) 로 직접 반환하도록 수정했다.
정리
- SQL 로그 정상 + API Response 비정상이면 DB가 아니라 응답 객체를 만드는 Java 레이어를 본다.
- 단독 호출 OK / 동시 호출 NG는 스레드 경쟁의 강한 힌트다.
- 싱글톤 Bean에 요청별 데이터를 인스턴스 필드로 두지 않는다 — 메서드 지역 변수로 생성 후 반환한다.
- Ajax를 2개 이상 동시에 쏘는 화면은, 공유 상태 버그가 간헐적으로만 터져 찾기 어렵다 — Network에서 URL별 Response와 sql.log를 반드시 대조한다.
참고: API1 vs API2 (예시)
| 구분 | 역할 | SQL | 예상 건수 |
| API1 | 드롭다운 목록 | 필터 적용 (예: 특정 타입만) | 1건 |
| API2 | 부가 정보·안내 | 필터 없음 (전체 트리) | 3건 (예: 상위·중간·하위 코드) |
두 API를 항상 동시 호출하는 구조에서, 수정 전에는 API1 응답에 API2 결과가 섞여 잘못된 option이 노출될 수 있었다.
728x90
반응형
'Record > Trubble Shooting' 카테고리의 다른 글
| Windows Cursor 터미널 한글 깨짐, PowerShell 프로필로 해결하는 방법 (0) | 2026.06.21 |
|---|---|
| 외부망에서만 API가 Empty Response로 끊긴 사례 해결 (0) | 2026.06.16 |
| [Trouble Shooting] 지도 포함 리포트 이미지 삽입 문제 해결 (0) | 2026.02.18 |
| IIS API 요청 실패 및 DB 연결 문제 해결 (0) | 2025.01.26 |
| [Python] pip 명령어 문제 해결: Fatal error in launcher: Unable to create process using '"' (2) | 2023.10.07 |