Go context로 타임아웃·취소 처리하기 | 실전 패턴 가이드

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와의 조합, 고루틴 누수를 피하는 습관입니다.


목차

  1. 개념 설명
  2. 실전 구현 (단계별 코드)
  3. 고급 활용
  4. 성능·비교
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

개념 설명

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+바디) 상한이고, NewRequestWithContextctx취소·데드라인을 전달합니다. 둘을 함께 두면 “느린 업스트림”과 “클라이언트가 떠남”을 동시에 다루기 쉽습니다.

4) 서버 종료: 별도의 Shutdown 컨텍스트

운영에서는 요청 단위 ctx프로세스 종료 신호를 분리하는 경우가 많습니다. 우아한 종료 예시는 Go 심화 #09Shutdown 패턴을 참고하면 됩니다.


고급 활용: 전파·값·계층

  • 전파: 부모가 취소되면 자식도 취소됩니다. 자식만 취소하려면 그 가지에서 파생된 WithCancelcancel()만 호출하면 됩니다.
  • 값(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*Context API를 쓰면 동일한 모델을 유지할 수 있습니다.

트러블슈팅

증상: 타임아웃이 안 걸린다
→ 블로킹 호출이 컨텍스트를 무시하는지 확인하세요. 표준 라이브러리는 *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 프로젝트와 이어 읽으면 좋습니다.