백엔드 엔지니어가 알아야 할 7가지 재시도 패턴
Wenhao Wang
Dev Intern · Leapcell

서문
비즈니스 시스템에서 실패는 일반적인 일입니다. 네트워크 변동, 서비스 과부하 또는 불안정한 타사 인터페이스로 인해 시스템은 때때로 발생하는 이상을 처리할 수 있는 "자체 복구" 기능을 갖추어야 합니다.
재시도 메커니즘은 시스템의 자체 복구 능력의 핵심 구성 요소 중 하나입니다.
그러나 재시도는 양날의 검입니다. 잘 설계되면 성공률을 높이고 사용자 경험을 향상시키지만, 잘못 설계되면 요청 폭주, 연쇄적인 실패를 초래하고 심지어 문제를 사건으로 확대시킬 수도 있습니다.
이 글에서는 일반적으로 사용되는 7가지 재시도 전략에 대해 논의합니다.
1. 무차별 대입 루핑
문제 시나리오
사용자 등록 SMS 전송 인터페이스가 while 루프에서 타사 SMS API를 반복적으로 호출합니다.
코드 예시:
public void sendSms(String phone) { int retry = 0; while (retry < 5) { // 무차별 대입 루프 try { smsClient.send(phone); break; } catch (Exception e) { retry++; Thread.sleep(1000); // 고정 1초 지연 } } }
사건
SMS 서버에 과부하가 발생하여 모든 요청이 3초 지연되었습니다.
이 무차별 대입 코드는 0.5초 이내에 수만 번의 재시도를 트리거하여 SMS 플랫폼을 압도하고 회로 차단기를 트리거하여 정상적인 요청까지 거부했습니다.
교훈:
- 지연 간격 조정 없음: 고정 지연으로 인해 재시도 폭주 발생
- 예외 유형 무시: 비 일시적인 오류(예: 잘못된 매개변수)에도 재시도
- 수정: 임의 지연을 도입하고 재시도할 수 없는 예외를 필터링
2. Spring Retry
사용 사례
Spring Retry는 소규모에서 중간 규모 프로젝트에 적합하며, 어노테이션을 통해 기본적인 재시도 및 회로 차단을 빠르게 구현할 수 있습니다(예: 주문 상태 쿼리 API).
@Retryable
어노테이션을 사용하여 재시도 로직을 구현합니다.
구성 예시
@Retryable( value = {TimeoutException.class}, // 타임아웃 시에만 재시도 maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2) // 1초 → 2초 → 4초 ) public boolean queryOrderStatus(String orderId) { return httpClient.get("/order/" + orderId); } @Recover // 대체 메서드 public boolean fallback() { return false; }
장점
- 선언적 어노테이션: 깔끔한 코드, 비즈니스 로직과 분리
- 지수 백오프: 자동으로 재시도 간격 증가
- 회로 차단기 통합:
@CircuitBreaker
와 결합하여 실패 트래픽을 빠르게 차단
3. Resilience4j
고급 시나리오
사용자 정의 백오프 알고리즘, 회로 차단기 전략 및 다층 보호(예: 핵심 결제 API)가 필요한 더 복잡한 시스템의 경우 Resilience4j를 권장합니다.
핵심 코드:
// 1. 재시도 구성: 지수 백오프 + 지터 RetryConfig retryConfig = RetryConfig.custom() .maxAttempts(3) .intervalFunction(IntervalFunction.ofExponentialRandomBackoff( 1000L, // 초기 1초 지연 2.0, // 지수 승수 0.3 // 지터 계수 )) .retryOnException(e -> e instanceof TimeoutException) .build(); // 2. 회로 차단기 구성: 실패율 > 50%인 경우 트리거 CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom() .slidingWindow(10, 10, CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) .failureRateThreshold(50) .build(); // 결합된 사용 Retry retry = Retry.of("payment", retryConfig); CircuitBreaker cb = CircuitBreaker.of("payment", cbConfig); // 비즈니스 로직 실행 Supplier<Boolean> supplier = () -> paymentService.pay(); Supplier<Boolean> decorated = Decorators.ofSupplier(supplier) .withRetry(retry) .withCircuitBreaker(cb) .decorate();
결과
이 솔루션을 배포한 후 한 회사의 결제 API 타임아웃 비율이 60% 감소했고, 회로 차단기 트리거 빈도가 거의 90% 감소했습니다.
4. MQ 큐
사용 사례
높은 동시성, 지연에 민감하지 않은 비동기 시나리오(예: 물류 상태 동기화).
구현 원리
- 초기 요청이 실패하면 메시지가 지연 큐로 전송됩니다.
- 큐는 사전 설정된 지연 후 메시지 소비를 재시도합니다(예: 5초, 30초, 1분).
- 최대 재시도 횟수에 도달하면 메시지가 수동 처리를 위해 데드 레터 큐로 이동됩니다.
RocketMQ 코드 스니펫:
// 생산자가 지연된 메시지 전송 Message<String> message = new Message(); message.setBody("주문 데이터"); message.setDelayTimeLevel(3); // RocketMQ 레벨 3 = 10초 지연 rocketMQTemplate.send(message); // 소비자 재시도 @RocketMQMessageListener(topic = "DELAY_TOPIC") public class DelayConsumer { @Override public void handleMessage(Message message) { try { syncLogistics(message); } catch (Exception e) { // 증가된 지연 레벨로 재전송 resendWithDelay(message, retryCount + 1); } } }
RocketMQ는 실패한 소비자 작업을 자동으로 재시도합니다.
5. 예약된 작업
사용 사례
실시간 응답이 필요하지 않고 일괄 처리를 허용하는 작업(예: 파일 가져오기)의 경우 예약된 작업을 사용할 수 있습니다.
Quartz를 사용한 예시:
@Scheduled(cron = "0 0/5 * * * ?") // 5분마다 실행 public void retryFailedTasks() { List<FailedTask> list = failedTaskDao.listUnprocessed(5); // 실패한 작업 쿼리 list.forEach(task -> { try { retryTask(task); task.markSuccess(); } catch (Exception e) { task.incrRetryCount(); } failedTaskDao.update(task); }); }
6. 2단계 커밋
사용 사례
엄격한 데이터 일관성이 필요한 시나리오(예: 자금 이체)의 경우 2단계 커밋 메커니즘을 사용할 수 있습니다.
핵심 구현
- 1단계: 데이터베이스에 트랜잭션 기록(상태를 "보류 중"으로 설정).
- 2단계: 원격 인터페이스를 호출하고 결과에 따라 트랜잭션 상태를 업데이트합니다.
- 보상 작업: 시간 초과된 "보류 중" 트랜잭션을 주기적으로 스캔하고 재시도합니다.
샘플 코드:
@Transactional public void transfer(TransferRequest req) { // 1. 트랜잭션 기록 transferRecordDao.create(req, PENDING); // 2. 은행 API 호출 boolean success = bankClient.transfer(req); // 3. 트랜잭션 상태 업데이트 transferRecordDao.updateStatus(req.getId(), success ? SUCCESS : FAILED); // 4. 실패 시 비동기적으로 재시도 if (!success) { mqTemplate.send("TRANSFER_RETRY_QUEUE", req); } }
7. 분산 잠금
사용 사례
멱등성이 중요한 여러 서비스 인스턴스 또는 다중 스레드 환경(예: 플래시 세일)과 관련된 시나리오에서는 분산 잠금을 사용할 수 있습니다.
Redis + Lua를 사용한 분산 잠금 예시:
public boolean retryWithLock(String key, int maxRetry) { String lockKey = "api_retry_lock:" + key; for (int i = 0; i < maxRetry; i++) { // 분산 잠금 획득 시도 if (redis.setnx(lockKey, "1", 30, TimeUnit.SECONDS)) { try { return callApi(); } finally { redis.delete(lockKey); } } Thread.sleep(1000 * (i + 1)); // 재시도 전에 대기 } return false; }
요약
재시도 메커니즘은 데이터 센터의 소화기와 같습니다. 사용하지 않기를 바라지만, 재해가 발생하면 최후의 방어선이 될 수 있습니다.
업무에서 어떤 솔루션을 선택해야 할까요?
최신 기술 트렌드를 맹목적으로 따르지 마십시오. 비즈니스에 필요한 공격과 방어의 균형에 따라 선택하십시오.
시스템 안정성의 핵심은 항상 재시도를 존중하는 데 있습니다.
Leapcell은 백엔드 프로젝트 호스팅을 위한 최고의 선택입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하십시오.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하고 요청이나 요금은 없습니다.
탁월한 비용 효율성
- 사용한 만큼 지불하며 유휴 요금이 없습니다.
- 예시: $25로 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
간편한 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있는 자동 확장.
- 운영 오버헤드가 없어 구축에만 집중할 수 있습니다.
설명서에서 자세히 알아보십시오!
X에서 팔로우하세요: @LeapcellHQ