
API가 단 한 번 호출됐다고 믿었는데, 실제로는 서버 2대에서 중복으로 처리되고 있었다.
어디서부터 잘못된 걸까? 실무에서 마주한 이슈를 기준으로 RestTemplate의 동작 방식, 로드밸런서 설정, 그리고 재시도 정책까지 정리했다.
크론탭에서 아래와 같이 RestTemplate을 통해 외부 API를 호출하는 코드가 있었다.
ResponseEntity<String> responseEntity = restTemplate.exchange(
url, HttpMethod.GET, requestEntity, String.class
);
관리자 서버에서는 분명 1회만 요청을 보냈는데,
Datadog 로그에는 백엔드 서버 2대(A서버, B서버)에서 각각 1건씩 처리한 로그가 찍혔다.
즉, 다음과 같은 구조로 동작했을 가능성이 높다:
1. Admin 서버에서 1회 요청
2. L4 로드밸런서가 요청을 A서버로 전달
3. A서버 응답 지연 또는 실패 감지 → 로드밸런서가 B서버로 같은 요청 재전송
4. 결과적으로 같은 요청이 두 번 처리됨
많은 개발자들이 오해하는 부분 중 하나는 RestTemplate 자체에 자동 재시도 기능이 있다고 착각하는 것이다.
하지만 Spring 공식 문서 기준, RestTemplate은 재시도 로직을 내장하고 있지 않다.
즉, 클라이언트 쪽에서 재시도가 발생한 것은 아니다.
이번 케이스에서 가장 가능성이 높았던 원인은 IDC 환경의 L4 로드밸런서가 재시도를 수행했을 가능성이다.
네트워크 레벨에서 L4는 다음 조건에서 자동 재전송을 할 수 있다:
• A서버로 요청을 보냈으나 응답이 지연되거나 실패함
• L4가 실패로 판단하고 동일한 요청을 B서버로 다시 전달함
즉, 클라이언트(RestTemplate)는 1회 호출했지만, L4가 내부적으로 2회 이상 요청을 유발할 수 있다.
RestTemplate의 재시도 정책을 구성하려면 기본적으로 다음과 같은 방법을 사용한다.
@Retryable(
value = { ResourceAccessException.class },
maxAttempts = 3,
backoff = @Backoff(delay = 1000)
)
public String callExternalApi() {
return restTemplate.getForObject(url, String.class);
}
ClientHttpRequestInterceptor retryInterceptor = (request, body, execution) -> {
int attempt = 0;
while (attempt++ < 3) {
try {
return execution.execute(request, body);
} catch (IOException ex) {
if (attempt == 3) throw ex;
Thread.sleep(1000);
}
}
return null;
};
하지만 위 코드들은 재시도가 필요할 때에만 구현하는 것이고,
이번 상황처럼 원치 않게 재시도가 발생했다면, 클라이언트보다는 인프라 측 설정을 먼저 의심해야 한다.
IDC 환경에서는 직접 로드밸런서 설정을 확인하기 어려운 경우가 많다.
이럴 땐 로그 기반 분석으로 접근한다.
분산 환경에서는 크론탭이나 스케줄러가 중복 실행되는 경우가 많다.
이럴 땐 DB Lock, Redis Lock, 혹은 ShedLock 같은 라이브러리를 사용해서 스케줄러 락을 걸어준다.
@Scheduled(cron = "0 0 8 * * MON")
@SchedulerLock(name = "newsletterJob", lockAtLeastFor = "PT10M")
public void sendNewsletter() {
...
}
라이브러리: ShedLock – https://github.com/lukas-krecan/ShedLock
RestTemplate 호출 시 X-Request-ID, taskId, timestamp 등을 함께 넘겨주면
서버에서 중복 호출을 감지하여 무시하거나 기록을 덮어쓸 수 있도록 처리할 수 있다.
1. RestTemplate은 자동으로 재시도하지 않는다. 재시도가 발생했다면 로드밸런서나 네트워크 계층을 의심해야 한다.
2. 인프라 설정을 볼 수 없을 경우, 로그 기반의 분석으로 원인을 추적할 수 있다.
3. 분산 환경에서는 항상 중복 실행 방지 로직을 고려해서 설계해야 한다.
이 경험을 통해, 단순한 API 호출이라도 인프라 구성과 운영환경을 함께 고려해야 안전하게 동작할 수 있다는 걸 다시 한 번 느꼈다.
내 코드만 잘 짜는 걸로는 부족하다. 실제 배포 구조를 이해하고 거기에 맞춘 설계가 필요하다.
티스토리