빠른 흐름
client := &http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()Go HTTP client 코드는 재사용할 client, 요청별 context, 반드시 닫을 body를 한 세트로 봅니다.
기본 흐름
http.Client는 재사용한다
client := &http.Client{
Timeout: 5 * time.Second,
}http.Client는 내부 transport와 연결 재사용을 품고 있습니다. 요청마다 새로 만들기보다 애플리케이션 경계에서 만들고 공유하는 쪽이 기본입니다.
요청 수명은 context로 제한한다
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return err
}요청별 timeout, 취소, trace 연결은 request context로 묶습니다. 상위 요청이 취소되면 외부 HTTP 호출도 같이 멈출 수 있어야 합니다.
응답 body는 항상 닫는다
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()body를 닫지 않으면 연결이 재사용되지 못하거나 리소스가 남을 수 있습니다. status code 검사는 body close와 별개로 처리합니다.
if resp.StatusCode >= 400 {
return fmt.Errorf("request failed: %s", resp.Status)
}타임아웃
Client.Timeout은 전체 요청 시간 제한이다
Client.Timeout은 연결, redirect, body 읽기까지 포함한 전체 요청의 상한으로 읽습니다. 단순한 서비스 호출에는 가장 먼저 잡기 좋은 안전장치입니다.
더 세밀한 제어는 transport에서 잡는다
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 3 * time.Second,
}).DialContext,
ResponseHeaderTimeout: 3 * time.Second,
}
client := &http.Client{Transport: transport}연결 수립, TLS handshake, response header 대기처럼 단계별 timeout이 필요하면 http.Transport를 직접 구성합니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 작은 내부 호출 | 재사용 http.Client + Timeout |
| 요청별 취소 전파 | NewRequestWithContext |
| 단계별 timeout | custom http.Transport |
| 응답 처리 | defer resp.Body.Close() |
| 운영 API 호출 | status code, body size, retry 정책 같이 설계 |
주의할 점
http.Get이나 기본 client를 timeout 없이 그대로 쓰면 외부 서버 문제에 오래 묶일 수 있습니다. 운영 코드에서는 timeout 없는 HTTP 호출을 기본값으로 두지 않는 편이 좋습니다. 또한 retry를 붙일 때는 context deadline과 멱등성까지 같이 확인해야 합니다.
참고 링크
2 sources