Record/Trubble Shooting

싱글톤 Bean 공유 상태로 동시 Ajax 응답이 섞인 사례

범데이 2026. 6. 16. 18:50

화면에서 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(...)직접 반환하도록 수정했다.

 

 

 

정리

  1. SQL 로그 정상 + API Response 비정상이면 DB가 아니라 응답 객체를 만드는 Java 레이어를 본다.
  2. 단독 호출 OK / 동시 호출 NG스레드 경쟁의 강한 힌트다.
  3. 싱글톤 Bean에 요청별 데이터를 인스턴스 필드로 두지 않는다 — 메서드 지역 변수로 생성 후 반환한다.
  4. Ajax를 2개 이상 동시에 쏘는 화면은, 공유 상태 버그가 간헐적으로만 터져 찾기 어렵다 — Network에서 URL별 Responsesql.log를 반드시 대조한다.

 

 

참고: API1 vs API2 (예시)

구분 역할 SQL 예상 건수
API1 드롭다운 목록 필터 적용 (예: 특정 타입만) 1건
API2 부가 정보·안내 필터 없음 (전체 트리) 3건 (예: 상위·중간·하위 코드)

 

두 API를 항상 동시 호출하는 구조에서, 수정 전에는 API1 응답에 API2 결과가 섞여 잘못된 option이 노출될 수 있었다.

728x90
반응형