Chapter 1: Retrying in Go 소개

1.1 다시 시도 메커니즘의 필요성 이해

많은 컴퓨팅 시나리오에서 특히 분산 시스템이나 네트워크 통신을 다룰 때 일시적 오류로 인해 작업이 실패할 수 있습니다. 이러한 오류는 주로 네트워크 불안정성, 서비스의 임시적 무능무재 또는 타임아웃과 같은 일시적 문제로 발생합니다. 시스템은 즉시 실패하는 대신 이러한 일시적 오류를 만난 작업을 다시 시도하도록 설계되어야 합니다. 이러한 접근 방식은 신뢰성과 탄력성을 향상시킵니다.

다시 시도 메커니즘은 작업의 일관성과 완결성이 필요한 응용 프로그램에서 중요할 수 있습니다. 또한 사용자가 경험하는 오류율을 줄일 수 있습니다. 그러나 다시 시도 메커니즘을 구현하는 것은 얼마나 자주, 얼마 동안 다시 시도할지 결정하는 것과 같은 도전과제가 있습니다. 이때 백오프 전략이 중요한 역할을 합니다.

1.2 go-retry 라이브러리 개요

Go 언어의 go-retry 라이브러리는 다양한 백오프 전략을 사용하여 응용 프로그램에 다시 시도 로직을 유연하게 추가할 수 있는 방법을 제공합니다. 주요 기능은 다음과 같습니다:

  • 확장성: Go 언어의 http 패키지와 마찬가지로 go-retry는 미들웨어와 함께 확장 가능하도록 설계되었습니다. 자체 백오프 함수를 작성하거나 기본으로 제공되는 편리한 필터를 사용할 수 있습니다.
  • 독립성: 이 라이브러리는 Go 표준 라이브러리에만 의존하며 외부 종속성을 피하여 프로젝트를 가볍게 유지합니다.
  • 동시성: 동시적 사용에 안전하며 추가적인 불편함 없이 고루틴과 함께 작동할 수 있습니다.
  • 컨텍스트 지원: Go 언어의 네이티브 컨텍스트를 지원하여 시간 제한 및 취소를 위해 고루틴과 통합할 수 있습니다.

Chapter 2: 라이브러리 가져오기

go-retry 라이브러리를 사용하기 전에 프로젝트에 가져와야 합니다. 이 작업은 모듈에 의존성을 추가하는 Go 명령인 go get을 사용하여 수행할 수 있습니다. 단순히 터미널을 열고 다음을 실행하면 됩니다:

go get github.com/sethvargo/go-retry

이 명령은 go-retry 라이브러리를 가져와 프로젝트의 종속성에 추가합니다. 그러고 나서 다른 Go 패키지와 마찬가지로 코드에 가져올 수 있습니다.

Chapter 3: 기본적인 다시 시도 로직 구현

3.1 상수 백오프를 사용한 간단한 다시 시도

가장 간단한 형태의 다시 시도 로직은 각 다시 시도 사이에 일정 기간을 대기하는 것입니다. go-retry를 사용하여 상수 백오프로 다시 시도를 수행할 수 있습니다.

다음은 go-retry를 사용하여 상수 백오프를 적용하는 예시입니다:

package main

import (
  "context"
  "time"
  "github.com/sethvargo/go-retry"
)

func main() {
    ctx := context.Background()
    
    // 새로운 상수 백오프를 생성합니다
    backoff := retry.NewConstant(1 * time.Second)

    // 다시 시도할 로직을 retry.Do에 전달할 함수로 래핑합니다
    operation := func(ctx context.Context) error {
        // 여기에 코드를 작성합니다. 다시 시도할 때는 retry.RetryableError(err)를 반환하고, 중단하려면 nil을 반환합니다.
        // 예시:
        // err := someOperation()
        // if err != nil {
        //   return retry.RetryableError(err)
        // }
        // return nil

        return nil
    }
    
    // 원하는 컨텍스트, 백오프 전략 및 작업을 사용하여 retry.Do를 사용합니다
    if err := retry.Do(ctx, backoff, operation); err != nil {
        // 오류 처리
    }
}

이 예시에서 retry.Do 함수는 성공할 때까지 또는 컨텍스트가 시간 초과되거나 취소될 때까지 operation 함수를 1초마다 계속 시도합니다.

3.2 지수 백오프 구현

지수 백오프는 재시도 사이의 대기 시간을 지수적으로 증가시킴으로써 시스템의 부하를 줄이는 전략입니다. 이 전략은 대규모 시스템이나 클라우드 서비스를 다룰 때 특히 유용합니다.

go-retry를 사용하여 지수 백오프를 사용하는 방법은 다음과 같습니다:

package main

import (
  "context"
  "time"
  "github.com/sethvargo/go-retry"
)

func main() {
    ctx := context.Background()

    // 새로운 지수 백오프를 생성합니다
    backoff := retry.NewExponential(1 * time.Second)

    // 반복 작업을 제공합니다
    operation := func(ctx context.Context) error {
        // 이전에 표시한 작업을 구현합니다
        return nil
    }
    
    // 지수 백오프로 작업을 실행하기 위해 retry.Do를 사용합니다
    if err := retry.Do(ctx, backoff, operation); err != nil {
        // 오류 처리
    }
}

지수 백오프의 경우 초기 백오프가 1초로 설정되었을 때, 재시도는 1초, 2초, 4초 등으로 지수적으로 증가하는 대기 시간을 가집니다.

3.3 피보나치 백오프 전략

피보나치 백오프 전략은 피보나치 수열을 사용하여 재시도 간의 대기 시간을 결정합니다. 이는 서서히 증가하는 타임아웃이 유익한 네트워크 관련 문제에 대한 좋은 전략일 수 있습니다.

go-retry를 사용하여 피보나치 백오프를 구현하는 예제는 아래와 같습니다:

package main

import (
  "context"
  "time"
  "github.com/sethvargo/go-retry"
)

func main() {
    ctx := context.Background()

    // 새로운 피보나치 백오프 생성
    backoff := retry.NewFibonacci(1 * time.Second)

    // 재시도할 작업 정의
    operation := func(ctx context.Context) error {
        // 여기에는 실패할 수도 있고 재시도가 필요한 동작 로직이 들어갑니다.
        return nil
    }
    
    // retry.Do를 사용하여 피보나치 백오프로 작업 실행
    if err := retry.Do(ctx, backoff, operation); err != nil {
        // 에러 처리
    }
}

초깃값이 1초인 피보나치 백오프를 사용하면, 재시도는 1초, 1초, 2초, 3초, 5초 등 피보나치 수열에 따라 발생합니다.

4장: 고급 재시도 기술 및 미들웨어

4.1 재시도에서 Jitter 활용

재시도 로직을 구현할 때, 동시에 발생하는 재시도의 영향을 고려하는 것이 중요합니다. 이는 여러 클라이언트가 동시에 재시도하는 '천둥의 무리(Thundering Herd)' 문제로 이어질 수 있습니다. 이 문제를 완화하기 위해 백오프 간격에 임의의 Jitter를 추가할 수 있습니다. 이 기술은 재시도 시도를 격자 모양으로 배열하여 동시에 여러 클라이언트의 재시도 가능성을 낮출 수 있습니다.

Jitter 추가 예시:

b := retry.NewFibonacci(1 * time.Second)

// 다음 값을 ± 500ms 반환
b = retry.WithJitter(500 * time.Millisecond, b)

// 결과 값의 ± 5% 반환
b = retry.WithJitterPercent(5, b)

4.2 최대 재시도 횟수 설정

일부 시나리오에서는 장기간이나 효과가 없는 재시도를 방지하기 위해 재시도 횟수를 제한하는 것이 필요할 수 있습니다. 최대 재시도 횟수를 명시함으로써 운영이 포기하기 전의 시도 횟수를 제어할 수 있습니다.

최대 재시도 횟수 설정 예시:

b := retry.NewFibonacci(1 * time.Second)

// 5번의 시도가 실패하면 중단
b = retry.WithMaxRetries(4, b)

4.3 개별 백오프 기간 제한

개별 백오프 기간이 특정 임계값을 초과하지 않도록 하기 위해 CappedDuration 미들웨어를 사용할 수 있습니다. 이를 통해 지나치게 긴 백오프 간격이 계산되는 것을 방지하여 재시도 동작에 예측 가능성을 부여할 수 있습니다.

개별 백오프 기간 제한 예시:

b := retry.NewFibonacci(1 * time.Second)

// 최댓값을 2초로 제한
b = retry.WithCappedDuration(2 * time.Second, b)

4.4 총 재시도 기간 제어

전체 재시도 과정에 대한 총 기간을 제한해야 하는 시나리오에서 WithMaxDuration 미들웨어를 사용하여 최대 총 실행 시간을 지정할 수 있습니다. 이를 통해 재시도 과정이 무한히 계속되지 않고 재시도에 대한 시간 예산을 부여할 수 있습니다.

총 재시도 기간 제어 예시:

b := retry.NewFibonacci(1 * time.Second)

// 최대 총 재시도 시간이 5초가 되도록 설정
b = retry.WithMaxDuration(5 * time.Second, b)