Go에서 Context Timeout으로 외부 API 호출 안정성 확보하기

문제 상황

결제 서비스에서 외부 PG사 API 호출 시 간헐적으로 응답이 오지 않는 문제가 발생했다. 타임아웃 설정 없이 http.Client를 사용하고 있었고, 이로 인해 goroutine이 계속 대기하면서 메모리 누수가 발생했다.

해결 방법

context.WithTimeout을 활용해 모든 외부 API 호출에 타임아웃을 강제했다.

func callPaymentAPI(orderID string) (*PaymentResponse, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "POST", apiURL, body)
    if err != nil {
        return nil, err
    }

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        // context deadline exceeded 처리
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("payment API timeout: %w", err)
        }
        return nil, err
    }
    defer resp.Body.Close()

    // response 처리
    return parseResponse(resp)
}

추가 개선사항

  1. Circuit Breaker 패턴 도입: sony/gobreaker 라이브러리를 사용해 연속 실패 시 빠르게 실패하도록 처리
  2. Retry 로직: 타임아웃 발생 시 exponential backoff로 재시도
  3. 모니터링: Prometheus로 타임아웃 발생률 추적
breaker := gobreaker.NewCircuitBreaker(gobreaker.Settings{
    Name:        "payment-api",
    MaxRequests: 3,
    Timeout:     30 * time.Second,
})

result, err := breaker.Execute(func() (interface{}, error) {
    return callPaymentAPI(orderID)
})

결과

타임아웃 도입 후 장애 시간이 평균 15분에서 5초 이내로 단축되었다. Circuit Breaker 패턴으로 연쇄 장애도 방지할 수 있었다. Context는 Go에서 타임아웃과 취소를 다루는 표준 방법이므로, 외부 호출 시 반드시 적용해야 한다.

Go에서 Context Timeout으로 외부 API 호출 안정성 확보하기