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 프로젝트와 이어 읽으면 좋습니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Go에서 context.WithTimeout·WithCancel·WithDeadline으로 작업 한계를 걸고 취소를 전파하는 법. HTTP 서버·클라이언트 예제와 흔한 실수까지 정리합니다. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [Go 심화 #09] context.Context로 타임아웃·취소·우아한 종료 다루기 — C++와의 비교
- [Go 2주 완성 #08] Day 14: 실전 미니 프로젝트 - REST API 서버 구축
- C++ HTTP 클라이언트 완벽 가이드 | REST API 호출·연결 풀·타임아웃·프로덕션 패턴
이 글에서 다루는 키워드 (관련 검색어)
Go, context, 타임아웃, 취소, 동시성, HTTP, goroutine 등으로 검색하시면 이 글이 도움이 됩니다.