Quick Flow
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)
}Close는 항상 호출해야 하지만, 연결 재사용은 response body를 어디까지 읽었는지와 transport 상태에도 좌우됩니다. 재사용만 위해 크기를 모르는 error body를 무제한으로 drain하지 말고, 필요한 오류 본문은 io.LimitReader로 제한해 읽은 뒤 닫습니다.
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
return fmt.Errorf("request failed: %s: %s", resp.Status, body)
}타임아웃
Client.Timeout은 전체 요청 시간 제한이다
Client.Timeout은 연결, redirect, body 읽기까지 포함한 전체 요청의 상한으로 읽습니다. 단순한 서비스 호출에는 가장 먼저 잡기 좋은 안전장치입니다.
request context deadline도 동시에 있으면 더 이른 deadline이 요청을 끝냅니다. 호출자 수명을 따르는 API에는 context를 우선으로 두고, 공용 client의 Timeout은 context가 빠진 호출을 막는 상한으로 정합니다.
더 세밀한 제어는 transport에서 잡는다
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.ResponseHeaderTimeout = 3 * time.Second
client := &http.Client{Transport: transport}연결 수립, TLS handshake, response header 대기처럼 단계별 timeout이 필요하면 http.Transport를 직접 구성합니다. 전역 http.DefaultTransport를 직접 수정하면 다른 client에도 설정이 번질 수 있으므로 clone한 transport를 씁니다. retry는 멱등한 요청인지, request body를 다시 보낼 수 있는지, 남은 context deadline 안에 끝나는지를 함께 확인합니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 작은 내부 호출 | 재사용 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