Go 프로젝트에 Context Timeout 적용하며 배운 것들

문제 상황

외부 결제 API를 호출하는 서비스에서 간헐적으로 응답이 오지 않아 고루틴이 계속 대기하는 문제가 발생했다. 타임아웃 설정 없이 http.Get()을 사용하고 있었던 게 원인이었다.

// 문제가 있던 코드
resp, err := http.Get(apiURL)
if err != nil {
    return err
}

해결 방법

Context를 활용해 타임아웃을 설정했다. http.Client를 직접 생성하고 Request에 Context를 연결하는 방식으로 수정했다.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

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

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
    // context deadline exceeded 에러 포함
    return err
}

추가로 적용한 것들

  1. DB 쿼리에도 Context 전파: 기존에는 db.Query()를 사용했는데, db.QueryContext()로 변경해서 타임아웃을 일관되게 적용했다.

  2. Graceful Shutdown: 서버 종료 시 처리 중인 요청들이 완료될 수 있도록 context를 활용한 shutdown 로직을 추가했다.

srv := &http.Server{Addr: ":8080"}

go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
}()

<-stopChan

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

srv.Shutdown(ctx)
  1. 모니터링 지표 추가: context deadline exceeded 에러를 별도로 집계해서 외부 API 응답 지연을 감지할 수 있도록 했다.

배운 점

Go에서 Context는 단순한 타임아웃 도구가 아니라 요청의 생명주기를 관리하는 핵심 패턴이다. 외부 호출이 있는 모든 곳에 Context를 전파하는 습관을 들이는 게 중요하다.

Go 프로젝트에 Context Timeout 적용하며 배운 것들