외부 API나 서버와 통신할 때 항상 성공적으로 응답을 받을 수 있다면 좋겠지만, 네트워크 환경에서는 다양한 이유로 오류가 발생할 수 있습니다. 일시적인 네트워크 장애나 서버 과부하 상태에서는 단순히 한 번 더 요청을 보내는 것으로 문제를 해결할 수 있습니다. 하지만 이런 단순한 재시도 방식에는 문제가 있습니다.
이번 글에서는 효율적인 재시도 전략인 지수 백오프(Exponential Backoff) 알고리즘에 대해 알아보고, 실제 프로젝트에서 어떻게 활용했는지 소개하겠습니다.
지수 백오프 알고리즘은 네트워크 통신에서 오류가 발생했을 때 재시도 간격을 점진적으로 늘려가며 재시도하는 전략입니다. 이름 그대로 '지수적으로(exponentially)' 대기 시간을 증가시키는 방식으로 동작합니다.
예를 들어, 처음 실패 시 2초 후에 재시도하고, 두 번째 실패 시 4초, 세 번째 실패 시 8초... 이런 식으로 대기 시간이 2의 거듭제곱으로 증가합니다.
이 알고리즘의 핵심 공식은 다음과 같습니다:
대기 시간 = 초기 대기 시간 × 2^(재시도 횟수)
단순히 고정된 간격으로 재시도를 하는 것보다 지수 백오프 방식이 더 효과적인 이유는 다음과 같습니다:
서버 부하 감소: 모든 클라이언트가 동시에 같은 간격으로 재시도할 경우, 서버에 트래픽이 한꺼번에 몰려 과부하를 일으킬 수 있습니다. 지수적으로 대기 시간이 증가하면 재시도 요청이 시간에 따라 분산됩니다.
네트워크 자원 효율화: 문제가 일시적인 경우 짧은 대기 시간으로 빠르게 해결하고, 지속적인 문제일 경우 불필요한 요청을 줄일 수 있습니다.
자동 조절: 시스템 장애의 심각성에 따라 자연스럽게 재시도 빈도가 조절됩니다.
API 제한(Rate Limit) 준수: 많은 외부 API는 요청 횟수를 제한하는데, 지수 백오프를 사용하면 이러한 제한을 더 효과적으로 관리할 수 있습니다.
제 AI 블로그 프로젝트에서는 Google의 Gemini AI API를 사용해 블로그 컨텐츠를 생성합니다. API 호출 과정에서 일시적인 오류나 요청 제한(Rate Limit)에 도달하는 경우가 있어, 이를 효과적으로 처리하기 위해 지수 백오프 알고리즘을 구현했습니다.
특히 Gemini API는 429 오류(Too Many Requests)를 반환할 때가 있는데, 이는 단기간에 너무 많은 요청을 보냈다는 의미입니다. 이런 상황에서 지수 백오프 전략은 API 제한을 준수하면서도 필요한 작업을 완료할 수 있게 해줍니다.
프로젝트의 src/app/api/integrations/gemini/route.ts
파일에서 구현한 지수 백오프 알고리즘을 살펴보겠습니다:
// 재시도 설정
const MAX_RETRIES = 5;
const INITIAL_RETRY_DELAY = 2000; // 2초 (초기 대기 시간)
// 지수 백오프 함수 - 재시도할 때마다 대기 시간이 늘어남
const getRetryDelay = (attempt: number): number => {
return INITIAL_RETRY_DELAY * Math.pow(2, attempt);
};
// 지연 함수 - 지정된 시간(ms)만큼 대기
const delay = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
위 코드에서는 최대 5번의 재시도와 초기 대기 시간 2초를 설정했습니다. getRetryDelay
함수는 재시도 횟수에 따라 2초, 4초, 8초, 16초, 32초로 대기 시간을 지수적으로 증가시킵니다.
이 함수들을 실제 API 호출에 적용한 코드는 다음과 같습니다:
async function callGeminiAPI(prompt: string, apiKey: string): Promise<string> {
let attempt = 0;
let lastError: Error | null = null;
while (attempt < MAX_RETRIES) {
try {
// API 요청 시도
const response = await fetch(API_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-goog-api-key": apiKey,
},
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
temperature: 1.0,
maxOutputTokens: 2048,
},
}),
});
// 성공한 경우
if (response.ok) {
const data = await response.json();
return data.candidates?.[0]?.content?.parts?.[0]?.text || "";
}
// 429 에러(요청 제한)인 경우 재시도
if (response.status === 429) {
// 에러 응답 정보 확인
const errorBody = await response.text();
console.warn(
`API 요청 제한 도달 (시도 ${attempt + 1}/${MAX_RETRIES}):`,
errorBody,
);
// 다음 시도 전 지연 시간 계산
const retryDelay = getRetryDelay(attempt);
console.log(`${retryDelay / 1000}초 후 재시도...`);
await delay(retryDelay);
attempt++;
continue;
}
// 다른 HTTP 에러인 경우
throw new Error(
`Gemini API 오류: ${response.status} - ${await response.text()}`,
);
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
console.error(
`Gemini API 호출 실패 (시도 ${attempt + 1}/${MAX_RETRIES}):`,
lastError.message,
);
// 다음 시도 전 지연
const retryDelay = getRetryDelay(attempt);
console.log(`${retryDelay / 1000}초 후 재시도...`);
await delay(retryDelay);
attempt++;
}
}
// 모든 재시도 실패 후
throw lastError || new Error("알 수 없는 오류로 Gemini API 호출 실패");
}
이 코드는 다음과 같이 동작합니다:
기본적인 지수 백오프에서 한 걸음 더 나아가, 많은 구현에서는 Jitter(무작위성)를 추가합니다. Jitter는 대기 시간에 약간의 무작위성을 더해 여러 클라이언트가 동시에 재시도하는 것을 방지합니다.
예를 들어, 다음과 같이 구현할 수 있습니다:
// Jitter를 추가한 지수 백오프 함수
const getRetryDelayWithJitter = (attempt: number): number => {
const baseDelay = INITIAL_RETRY_DELAY * Math.pow(2, attempt);
const jitter = baseDelay * 0.3 * Math.random(); // 30%까지의 무작위 변동
return baseDelay + jitter;
};
이렇게 하면 여러 사용자가 동시에 API 제한에 도달했을 때 재시도 요청이 시간에 따라 더 고르게 분산되어, 재시도 요청의 '폭풍'을 방지할 수 있습니다.
지수 백오프 알고리즘을 구현할 때는 다음 사항을 고려해야 합니다:
최대 대기 시간 설정: 지수적 증가는 빠르게 매우 긴 대기 시간으로 이어질 수 있으므로, 일반적으로 최대 대기 시간을 설정합니다.
최대 재시도 횟수: 영원히 재시도하는 것보다는 적절한 시점에 실패를 인정하고 사용자에게 알려주는 것이 좋습니다.
오류 유형 분류: 모든 오류에 대해 재시도하는 것보다, 일시적인 오류(네트워크 장애, 서버 과부하 등)와 영구적인 오류(인증 실패, 리소스 없음 등)를 구분하여 처리하는 것이 효율적입니다.
로깅과 모니터링: 재시도 패턴을 모니터링하여 시스템 문제를 조기에 감지할 수 있도록 합니다.
지수 백오프 알고리즘은 네트워크 통신에서 발생하는 일시적인 오류를 효과적으로 처리하는 강력한 방법입니다. 특히 외부 API와 통신하는 애플리케이션에서는 필수적인 전략이라 할 수 있습니다.
제 AI 블로그 프로젝트에서는 Gemini API 호출 과정에 지수 백오프를 적용함으로써, API 제한 준수와 안정적인 콘텐츠 생성이라는 두 마리 토끼를 모두 잡을 수 있었습니다.