
시스템 개발 및 운영을 하다보면 외부 API 호출이나 DB 작업 중 여러 에러들을 마주하게 됩니다.
발생하는 에러들을 파악하고, 재시도를 해야하는 에러인지 아닌지를 판단하고, 다시 재시도하는 로직을 함께 구현해보겠습니다.
1. 에러 재시도
모든 에러는 각 성격에 맞춰 명확히 구분하고, 그 구분된 기준으로 에러를 재시도할지 말지를 판단하는 것이 중요합니다.
- 재시도 : 503(Service Unavailable), 429(Too Many Requests),
데이터베이스 연결 끊김 등과 같이 일시적인 네트워크 오류같이 재시도 할 경우 성공할 수 있는 경우. - 재시도 불가 : 400(Bad Request), 401(Unauthorized) 등 같은 조건에서 시도해도 동일한 결과가 나올 경우
2. 예제 코드
package main
import (
"context"
"errors"
"fmt"
"time"
)
var ErrRateLimited = errors.New("rate limited")
type UpstreamError struct {
Code int
Err error
}
func (e *UpstreamError) Error() string { return fmt.Sprintf("upstream code=%d: %v", e.Code, e.Err) }
func (e *UpstreamError) Unwrap() error { return e.Err }
func IsRetryable(err error) bool {
if err == nil {
return false
}
// context error
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return false
}
// sentinel error
if errors.Is(err, ErrRateLimited) {
return true
}
// typed error
var ue *UpstreamError
if errors.As(err, &ue) {
return ue.Code >= 500 && ue.Code <= 504
}
return false
}
func CallWithRetry(ctx context.Context) error {
maxRetries := 3
backoff := 100 * time.Millisecond // default waiting time
for i := 0; i < maxRetries; i++ {
err := callUpstream(ctx)
if err == nil {
return nil // 성공 시 즉시 반환
}
// 재시도 가능한 에러인지 확인
if !IsRetryable(err) {
return fmt.Errorf("non-retryable error: %w", err)
}
fmt.Printf("retry... (%d/%d): %v\n", i+1, maxRetries, err)
// 다음 시도 전 대기
select {
case <-time.After(backoff):
backoff *= 2 // 대기 시간을 늘려 서버 부담을 줄임
case <-ctx.Done():
return ctx.Err()
}
}
return errors.New("max retries exceeded")
}
// error func
func callUpstream(ctx context.Context) error {
return &UpstreamError{Code: 503, Err: errors.New("service unavailable")}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := CallWithRetry(ctx); err != nil {
fmt.Printf("result error : %v \n", err)
}
}
3. 핵심 정리
- Context
- context 관련 에러체크를 우선 진행합니다.
클라이언트가 요청을 중지하거나 주어진 시간을 초과했다면 더 이상 수행할 이유가 없음으로 우선적으로 확인합니다.
- context 관련 에러체크를 우선 진행합니다.
- Backoff
- 실패 직후 바로 재시도를 하기보다는 backoff을 줌으로써 대기시간을 점차 증가시켜 줍니다.
이는 리소스 낭비 방지 뿐만 아니라 상대 서버가 리커버리 될 수 있는 시간을 줍니다
- 실패 직후 바로 재시도를 하기보다는 backoff을 줌으로써 대기시간을 점차 증가시켜 줍니다.
4. 전체 코드
https://github.com/reochoi109/go-handbook/blob/main/error/advanced/retryable/main.go
go-handbook/error/advanced/retryable/main.go at main · reochoi109/go-handbook
A personal handbook of Go patterns and best practices. Lightweight, practical code snippets for real-world backend development. - reochoi109/go-handbook
github.com
'프로그래밍 > golang' 카테고리의 다른 글
| [Golang] errgroup 이란? (0) | 2026.03.18 |
|---|---|
| [Golang] Close() 에러를 무시하면 안 되는 이유 (0) | 2026.03.18 |
| [Golang] errors.Is와 As를 활용한 에러 핸들링 (0) | 2026.03.18 |
| [Golang] Slog를 활용한 로그 라우팅 설계 (0) | 2026.03.18 |
| [Golang] Slog를 활용한 로그 샘플링 구현하기 (0) | 2026.03.18 |