Go context로 타임아웃·취소 처리하기 | 실전 패턴 가이드
이 글의 핵심
Go context 취소·타임아웃 패턴—WithTimeout·WithCancel·WithDeadline과 HTTP 서버·클라이언트에서 끊김·마감을 일관되게 다루는 실전 요약입니다.
들어가며
분산 시스템과 HTTP 서비스에서는 “언제까지 기다릴지”와 “상위 단계가 실패했을 때 하위 작업을 멈출지”가 곧 안정성입니다. Go는 이를 context.Context로 표준화했고, net/http·database/sql 등이 요청 단위로 컨텍스트를 받도록 설계되어 있습니다.
이 글은 타임아웃·취소·데드라인을 코드로 연결하는 패턴에 초점을 맞춥니다. Go context 취소·타임아웃 패턴을 한 곳에 모아 두면, 팀 코드 리뷰에서도 기준이 맞춰집니다. 시리즈의 context·우아한 종료 심화와 맞닿지만, 여기서는 API 선택과 HTTP 서버·클라이언트에 바로 붙이는 레시피를 압축해 정리합니다.
다루는 내용: WithCancel / WithTimeout / WithDeadline 차이, 취소 전파 규칙, http.Server·http.Client와의 조합, 고루틴 누수를 피하는 습관입니다.
목차
개념 설명
context.Context는 취소 신호(Done), 마감 시각(Deadline), **요청 범위 값(Value)**을 묶은 불변 인터페이스입니다. 하위 함수로 같은 줄기를 넘기면, 한곳에서 취소·타임아웃이 걸렸을 때 트리 전체에 일관되게 전달됩니다.
- 취소(
WithCancel): 사용자가 요청을 취소했거나, 상위 로직이 실패해 더 이상 의미 없는 작업을 멈출 때. - 상대 타임아웃(
WithTimeout): “지금부터 N초 안에” 같은 기간 제한. - 절대 데드라인(
WithDeadline): “오늘 23:59까지”처럼 시각이 정해진 경우.
취소 가능한 컨텍스트를 만들면 반드시 cancel 함수를 호출해야 합니다(리소스 누수 방지). defer cancel() 패턴이 사실상 표준입니다.
실전 구현 (단계별 코드)
1) WithTimeout: DB·외부 API 호출 상한
package main
import (
"context"
"database/sql"
"errors"
"time"
)
func UserByID(ctx context.Context, db *sql.DB, id int64) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer cancel()
var name string
err := db.QueryRowContext(ctx, `SELECT name FROM users WHERE id = ?`, id).Scan(&name)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return "", err
}
return "", err
}
return name, nil
}
호출부의 ctx는 보통 http.Request.Context()에서 온 요청 루트입니다.
2) WithCancel: 한 단계 실패 시 나머지 워커 중단
func work(ctx context.Context, id int) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
errs := make(chan error, 2)
go func() { errs <- fetchA(ctx, id) }()
go func() { errs <- fetchB(ctx, id) }()
for i := 0; i < 2; i++ {
if err := <-errs; err != nil {
cancel() // 실패 시 다른 고루틴에 취소 전파
return err
}
}
return nil
}
func fetchA(ctx context.Context, id int) error {
select {
case <-time.After(100 * time.Millisecond):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func fetchB(ctx context.Context, id int) error {
select {
case <-time.After(200 * time.Millisecond):
return errors.New("upstream")
case <-ctx.Done():
return ctx.Err()
}
}
3) HTTP 서버: 요청 컨텍스트와 클라이언트 타임아웃
package main
import (
"context"
"io"
"net/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/v1/x", nil)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
http.Error(w, err.Error(), 502)
return
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "application/json")
_, _ = io.Copy(w, resp.Body)
}
http.Client.Timeout은 전체 요청(연결+TLS+바디) 상한이고, NewRequestWithContext의 ctx는 취소·데드라인을 전달합니다. 둘을 함께 두면 “느린 업스트림”과 “클라이언트가 떠남”을 동시에 다루기 쉽습니다.
4) 서버 종료: 별도의 Shutdown 컨텍스트
운영에서는 요청 단위 ctx와 프로세스 종료 신호를 분리하는 경우가 많습니다. 우아한 종료 예시는 Go 심화 #09의 Shutdown 패턴을 참고하면 됩니다.
고급 활용: 전파·값·계층
- 전파: 부모가 취소되면 자식도 취소됩니다. 자식만 취소하려면 그 가지에서 파생된
WithCancel의cancel()만 호출하면 됩니다. - 값(
context.WithValue): 추적 ID·인증 주체처럼 요청 스코프 메타데이터만 넣고, 비즈니스 입력은 함수 인자로 두는 편이 테스트에 유리합니다. - 중첩 타임아웃: 바깥이 5초, 안쪽이 800ms처럼 더 짧은 쪽이 먼저 발화합니다. 각 계층이 자신의 SLA만 알면 됩니다.
성능·비교: 세 API를 언제 쓰나
| API | 용도 | 메모 |
|---|---|---|
WithCancel | 수동 취소, 파이프라인 조기 종료 | cancel() 호출 필수 |
WithTimeout(d) | 상대 기한(지금부터 d) | 내부적으로 WithDeadline |
WithDeadline(t) | 절대 시각 | 클럭 동기화가 중요한 배치·스케줄에 적합 |
비용 자체는 가볍지만, 컨텍스트 없이 블로킹 호출을 남겨 두면 고루틴·연결이 쌓입니다. 성능 이슈는 대부분 “취소가 안 닿는 I/O”에서 옵니다.
실무 사례
- 게이트웨이: 업스트림 호출마다
WithTimeout을 걸고, 클라이언트가 끊기면r.Context()로 빠져나오게 합니다. - 배치 작업: 한 건 실패 시 전체를 멈추려면
WithCancel루트 하나를 두고, 워커에 같은ctx를 넘깁니다. - gRPC/DB:
PingContext,QueryContext등*ContextAPI를 쓰면 동일한 모델을 유지할 수 있습니다.
트러블슈팅
증상: 타임아웃이 안 걸린다
→ 블로킹 호출이 컨텍스트를 무시하는지 확인하세요. 표준 라이브러리는 *Context 변형을 써야 합니다.
증상: cancel을 안 불러서 리소스 누수 경고
→ defer cancel()을 습관화하세요. WithTimeout도 동일합니다.
증상: Err()가 Canceled인지 DeadlineExceeded인지 헷갈린다
→ errors.Is(err, context.DeadlineExceeded)처럼 값으로 비교하세요.
증상: 테스트가 느리다
→ context.Background() 대신 짧은 WithTimeout을 쓰거나, 취소 가능한 ctx로 끊으세요.
마무리
context는 Go에서 동시성과 마감 정책을 팀 전체가 같은 방식으로 표현하게 해 주는 핵심 도구입니다. WithTimeout·WithCancel·WithDeadline을 상황에 맞게 고르고, HTTP에서는 요청 ctx + Client 타임아웃을 함께 설계하면 운영 환경에서 재현하기 어려운 “느린 누수”를 많이 줄일 수 있습니다. 고루틴과 채널 기본기는 고루틴·채널과 REST 실습 API 프로젝트와 이어 읽으면 좋습니다.