부업 프로젝트에서 재시도를 관리하는 4가지 스마트한 방법
출처: Dev.to
소개: 나만의 프로젝트가 가르쳐 준 숨은 스승들
내가 스스로 시작한 사이드 프로젝트들은 내 커리어에서 가장 큰 학습장이 되었다. 대기업 프로젝트에서 요구되는 복잡한 프로세스를 벗어나 직접 기술과 마주할 수 있는 순간들이다. 하지만 이 자유에는 자체적인 도전 과제도 따른다. 가장 큰 문제 중 하나는 특히 오류 상황에서 재시도를 체계적으로 관리하는 능력이다. 지난 10년간 개인 재무 계산기부터 모바일 스팸 차단기까지 수많은 사이드 프로젝트를 개발하면서, 실수를 교훈으로 삼고 앞으로 나아가기 위한 네 가지 기본 전략을 만들었다. 이 글에서는 그 전략들을 실제 예시와 함께 설명한다.
이 사이드 프로젝트들은 단순히 코드 조각이 아니라 지속적인 성장의 플랫폼이다. 기업에서 ERP 시스템이나 내부 은행 플랫폼을 운영하며 얻는 경험과는 별도로, 개인 프로젝트를 통해 다른 시각을 얻고 실험적인 접근 방식을 채택할 수 있다. 예를 들어, 내 개인 재무 계산기를 내 VPS에서 최적화하면서 얻은 인사이트가 메인 프로젝트의 데이터베이스 성능 문제를 해결하는 데 도움이 되기도 한다. 이번 글에서는 이러한 “숨은 스승”들이 알려준, 재시도를 보다 지능적으로 관리하는 방법을 공유한다.
사이드 프로젝트에서 가장 크게 마주친 문제 중 하나는 오류가 발생한 원인을 파악할 충분한 데이터가 부족하다는 점이었다. 일회성 스크립트나 일시적인 서비스에서는 오류가 발생한 순간 시스템에서 무슨 일이 일어나고 있었는지를 이해하는 것이 매우 중요하다. 이때 나는 오랫동안 SystemD를 사용해 온 Linux 시스템의 journald 기능을 활용하기 시작했다.
Journald는 단순히 로그를 수집할 뿐만 아니라 로그 레벨과 레이트 제한도 관리한다. 내 사이드 프로젝트에서는 일반적으로 다음과 같은 전략을 따른다: 치명적인 오류는 syslog 혹은 전용 파일에 상세히 기록하고, 일상적인 운영 로그는 더 간결하게 유지한다. 이렇게 하면 디스크 공간을 절약하면서도 오류 발생 시 필요한 모든 정보를 확보할 수 있다. 예를 들어, 한 번만 실행되는 데이터 추출 스크립트가 특정 API에서 데이터를 가져오다 가끔 HTTP 500 Internal Server Error를 반환했다. 처음엔 스크립트 안에 print() 문을 삽입해 디버깅했지만, 이는 느리고 오류가 발생한 순간을 정확히 포착하기 어려웠다.
Journald가 제공하는 레이트 제한 기능도 여기서 큰 도움이 된다. 특히 트래픽이 많은 서비스에서는 같은 오류가 반복적으로 기록돼 디스크를 가득 채우고 로그를 읽기 어렵게 만든다. SystemD 유닛 파일에 RateLimitIntervalSec와 RateLimitBurst 같은 파라미터를 추가하면 특정 시간 내에 동일 소스에서 발생하는 로그 수를 제한할 수 있다. 이는 고트래픽 API 게이트웨이에서 429 Too Many Requests 오류를 분석할 때 유용했다. 오류의 근본 원인을 파악하기 위한 첫 번째 단계는 오류를 얼마나 효과적이고 효율적으로 기록할 수 있는지를 자동화하는 것이다. 이는 오류를 감지하는 데 도움을 줄 뿐만 아니라 원인 분석 속도를 크게 높인다.
ℹ️ 로그 및 오류 분석
내 프로젝트에서는 중요한 서비스에 대해 journalctl -f 로 실시간 로그를 모니터링하고, 특정 오류에 대해서는 journalctl -xe 로 보다 상세한 오류 덤프를 확인한다. 이렇게 하면 잠재적인 문제를 놓치지 않을 수 있다.
오류가 발생했을 때 가장 먼저 떠오르는 생각은 “잠시 기다렸다가 다시 시도한다”는 것이다. 하지만 이 “잠시”의 길이는 시스템 안정성과 사용자 경험에 결정적인 영향을 미친다. 네트워크 오류나 일시적인 서비스 중단 상황에서는 즉시 재시도하기보다 스마트한 대기 전략을 따르는 것이 중요하다. 여기서 “지수 백오프(exponential backoff)” 개념이 등장한다.
내 사이드 프로젝트에서는 외부 API나 서비스에 접근하는 코드에서 이 메커니즘을 자주 사용한다. 예를 들어, 개인 재무 계산기 프로젝트에서 데이터 제공자 API에 연결할 때 가끔 네트워크 연결 문제가 발생했다. 첫 번째 시도에서 오류가 발생하면 바로 다음 초에 재시도하지 않고 1초를 기다렸다가 다시 시도한다. 여전히 오류가 나오면 대기 시간을 2초, 4초, 8초 … 식으로 두 배씩 늘린다. 이 전략의 가장 큰 장점은 대상 서비스를 과부하 시키지 않으면서 일시적인 문제 해결을 위한 충분한 시간을 제공한다는 점이다.
이 전략을 구현할 때 주의하는 또 다른 포인트는 최대 재시도 횟수와 총 대기 시간이다. 무한 루프에 빠지는 것을 원하지 않는다. 일반적으로 3~5회 정도의 재시도가 충분하다. 이 시도들 이후에도 오류가 지속되면 더 이상 일시적인 문제가 아니며 수동 조사가 필요하다는 신호다. 여기서 중요한 파라미터가 바로 “지터(jitter)”이다. 이는 정확한 대기 시간을 계산한 뒤에 작은 무작위성을 추가하는 것을 의미한다. 여러 클라이언트가 동시에 재시도하는 “천둥 무리(thundering herd)” 문제를 방지한다.
💡 지터의 중요성
지터를 추가하면 여러 클라이언트가 동시에 서비스를 재시도하는 상황을 방지해 급격한 부하 급증을 막을 수 있다. 내 프로젝트에서는 계산된 대기 시간에 10~20% 정도의 무작위 시간을 더해 이 효과를 만든다.
아래는 Astro의 fetch API를 사용해 위 전략을 구현한 예시이다:
async function fetchDataWithRetry(url, maxRetries = 3, initialDelay = 1000) {
let delay = initialDelay;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
// 서버 오류(5xx)인 경우 재시도
if (response.status >= 500 && response.status < 600) {
// 500~599 사이의 상태 코드는 재시도 대상
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // 지수 백오프
// 지터 추가
delay += Math.random() * initialDelay;
} else {
// 클라이언트 오류 또는 재시도 불가 오류
throw new Error(`HTTP error! status: ${response.status}`);
}
} else {
// 성공 시 데이터 반환
return await response.json();
}
} catch (error) {
console.error(`Attempt ${i + 1} failed: ${error.message}`);
if (i === maxRetries - 1) {
throw error; // 마지막 시도라면 오류를 전파
}
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2;
delay += Math.random() * initialDelay;
}
}
}
// 사용 예시:
// fetchDataWithRetry('https://api.example.com/data')
// .then(data => console.log('Data received:', data))
// .catch(error => console.error('Failed to fetch data after multiple retries:', error));
위 코드 조각은 외부 서비스에서 데이터를 가져올 때 발생할 수 있는 네트워크 문제를 대비해 지수 백오프와 지터 메커니즘을 포함하고 있다. 이렇게 간단하면서도 효과적인 전략을 적용하면 사이드 프로젝트를 보다 안정적으로 운영할 수 있다.
재시도 메커니즘을 구현할 때 가장 중요한 고려 사항 중 하나는 작업이 “멱등(idempotent)”인지 여부다. 멱등성은 같은 작업을 여러 번 수행해도 결과가 한 번 수행한 것과 동일하다는 뜻이다. 작업이 멱등하지 않다면 재시도가 예상치 못한 부작용을 초래할 수 있다.
예를 들어, 사용자의 이메일을 발송하는 함수를 생각해 보자. 이 함수가 멱등하지 않고 네트워크 오류 때문에 재시도된다면 같은 이메일이 두 번 전송될 수 있다. 이는 사용자 경험 측면에서 매우 불쾌한 상황이다. 내 프로젝트에서는 특히 금융 거래나 데이터 업데이트와 같이 민감한 작업에 대해 멱등성을 보장하기 위한 다양한 방법을 사용한다.
한 가지 방법은 각 작업에 고유한 트랜잭션 ID를 부여하는 것이다. 요청을 보낼 때 이 ID를 함께 전송하고, 서버 측에서는 해당 ID가 이미 처리된 적이 있는지 확인한다. 이미 처리된 경우 동일 작업을 다시 수행하지 않는다. 이렇게 하면 네트워크 오류 등으로 인한 재시도에도 중복 실행을 방지할 수 있다.