Go에서 context timeout 제대로 다루기

문제 상황

외부 결제 API 호출 시 3초 timeout을 설정했는데, 실제로는 30초 가까이 대기하는 현상이 발생했다. 모니터링 지표에서 P99 latency가 급증하고 있었다.

func (s *PaymentService) ProcessPayment(userID string) error {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    
    // 문제: HTTP client가 context를 무시하고 있었음
    resp, err := http.Get(paymentURL)
    // ...
}

원인

http.Get은 context를 받지 않는다. http.NewRequestWithContext를 사용해야 했다.

해결

func (s *PaymentService) ProcessPayment(ctx context.Context, userID string) error {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
    
    req, err := http.NewRequestWithContext(ctx, "GET", paymentURL, nil)
    if err != nil {
        return err
    }
    
    resp, err := s.httpClient.Do(req)
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            return errors.New("payment timeout")
        }
        return err
    }
    defer resp.Body.Close()
    // ...
}

배운 점

  1. context는 첫 번째 인자로 전달: Go 컨벤션을 따라 함수 시그니처를 수정했다
  2. context.Background() 남용 금지: 상위 레이어에서 받은 context를 전파해야 한다
  3. 취소 여부 확인: ctx.Err()로 timeout인지 cancel인지 구분할 수 있다

외부 의존성이 있는 모든 HTTP, DB 호출에 동일한 패턴을 적용했고, P99가 3초 이하로 안정화되었다.

Go에서 context timeout 제대로 다루기