GoRetryKit: 간결하고 유연한 Go 언어 재시도 유틸리티

GoRetryKit은 네트워크 호출, 외부 API 통신, 일시적 장애 대응 등에서 실패 시 자동 재시도를 제공하는 최소 의존성 Go 라이브러리입니다. 400여 줄의 순수 Go 코드로 구성되어 복잡한 외부 패키지 없이도 신뢰성 있는 재시도 로직을 구현할 수 있습니다.

핵심 설계 철학

재시도는 단순 반복이 아니라 전략 기반의 오류 회복 과정입니다. GoRetryKit은 지수 백오프, 임계값 기반 중단, 컨텍스트 기반 타임아웃, 오류 필터링 등을 내장하여 개발자가 비즈니스 로직에 집중할 수 있도록 합니다. 특히 context.Context와의 완전한 호환성을 통해 애플리케이션 전체의 생명주기 제어를 지원합니다.

사용 사례

  • 외부 REST API 호출 시 연결 거부 또는 5xx 응답 발생 시 재시도
  • Kafka 또는 RabbitMQ 메시지 전송 실패 후 임시 네트워크 두절 대응
  • 분산 캐시(Redis/Memcached) 접근 시 타임아웃 또는 클러스터 리밸런싱 중 일시적 장애 처리
  • DB 트랜잭션 중 SQLSTATE 40001(serialization failure) 같은 재시도 가능한 오류 대응

기능 요약

  • 컨텍스트 연동: context.WithTimeout, context.WithCancel와 자연스럽게 통합
  • 백오프 정책: 고정 간격, 지수 증가, Jitter 적용 가능
  • 오류 분류: 특정 에러 타입만 재시도하거나, 특정 에러는 즉시 전파하도록 설정 가능
  • 결과 추출: 반환 값과 오류를 동시에 캡처하는 DoWithResult 함수 제공
  • 테스트 친화적: 인터페이스 기반 설계로 모킹 및 단위 테스트 용이

설치 및 기본 사용법

go get -u github.com/yourorg/goretrykit

간단한 재시도 예제:

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/yourorg/goretrykit"
)

func fetchResource(ctx context.Context) error {
	resp, err := http.DefaultClient.Do(http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil))
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 400 {
		return fmt.Errorf("HTTP %d", resp.StatusCode)
	}
	return nil
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	err := goretrykit.Do(
		ctx,
		func() error { return fetchResource(ctx) },
		goretrykit.WithMaxAttempts(4),
		goretrykit.WithJitterBackoff(500*time.Millisecond, 2.0, 0.2),
		goretrykit.WithShouldRetry(func(err error) bool {
			return !errors.Is(err, context.DeadlineExceeded)
		}),
	)
	if err != nil {
		fmt.Printf("최종 실패: %v\n", err)
		return
	}
	fmt.Println("성공적으로 데이터 가져옴")
}

고급 활용: 결과 반환 및 조건부 재시도

다음은 JSON 응답을 파싱하고, 특정 상태 코드나 유효성 검사 실패 시만 재시도하는 예제입니다:

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

func getUser(ctx context.Context) (User, error) {
	var u User
	resp, err := http.DefaultClient.Get("https://api.example.com/user/123")
	if err != nil {
		return u, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return u, goretrykit.PermanentError(fmt.Errorf("user not found"))
	}
	if resp.StatusCode != http.StatusOK {
		return u, fmt.Errorf("unexpected status: %d", resp.StatusCode)
	}

	if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
		return u, err
	}
	return u, nil
}

// 실행
user, err := goretrykit.DoWithResult(
	ctx,
	getUser,
	goretrykit.WithMaxAttempts(3),
	goretrykit.WithFixedDelay(1 * time.Second),
)

태그: go retry backoff context http-client

9월 6일 07:27에 게시됨