context 패키지 — 취소·타임아웃·값 전파
실제 서버 프로그램에서는 하나의 요청을 처리하기 위해 여러 고루틴이 협력합니다. 클라이언트가 연결을 끊거나 타임아웃이 발생하면 관련된 모든 고루틴을 취소해야 합니다. context 패키지는 이런 취소 신호, 타임아웃, 요청 범위 값 을 고루틴 트리 전체에 전파합니다.
context란?
type Context interface {
Deadline() (deadline time.Time, ok bool) // 마감 시간
Done() <-chan struct{} // 취소 시 닫히는 채널
Err() error // 취소 이유 (Canceled/DeadlineExceeded)
Value(key any) any // 컨텍스트에 저장된 값
}
context 생성 함수들
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 1. 루트 컨텍스트 (취소 불가)
ctx := context.Background() // 최상위, 취소 없음
// ctx := context.TODO() // 아직 결정되지 않은 경우
// 2. 취소 가능한 컨텍스트
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel() // 반드시 cancel 호출 (리소스 누수 방지)
// 3. 타임아웃 컨텍스트
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, 5*time.Second)
defer cancelTimeout()
// 4. 마감 시간 컨텍스트
deadline := time.Now().Add(10 * time.Second)
deadlineCtx, cancelDeadline := context.WithDeadline(ctx, deadline)
defer cancelDeadline()
// 5. 값을 가진 컨텍스트
valueCtx := context.WithValue(ctx, "userID", "user-123")
fmt.Println("cancelCtx:", cancelCtx)
fmt.Println("timeoutCtx:", timeoutCtx)
fmt.Println("deadlineCtx:", deadlineCtx)
fmt.Println("값:", valueCtx.Value("userID"))
}
WithCancel — 수동 취소
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d 취소됨: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d 작업 중...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
// 여러 워커 시작
for i := 1; i <= 3; i++ {
go doWork(ctx, i)
}
// 2초 후 모든 워커 취소
time.Sleep(2 * time.Second)
fmt.Println("취소 신호 전송")
cancel() // 모든 하위 컨텍스트에 취소 전파
time.Sleep(100 * time.Millisecond)
fmt.Println("메인 종료")
}
WithTimeout — 시간 제한
HTTP 요청, DB 쿼리 등 외부 작업에 시간 제한을 둘 때 사용합니다.
package main
import (
"context"
"fmt"
"time"
)
// DB 쿼리 시뮬레이션
func queryDatabase(ctx context.Context, query string) (string, error) {
// 실제 DB 쿼리는 ctx를 지원하는 드라이버를 통해 자동 취소됨
resultCh := make(chan string, 1)
go func() {
time.Sleep(3 * time.Second) // 느린 쿼리 시뮬레이션
resultCh <- "쿼리 결과"
}()
select {
case result := <-resultCh:
return result, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func handleRequest(timeout time.Duration) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
result, err := queryDatabase(ctx, "SELECT * FROM users")
if err != nil {
fmt.Printf("오류 (타임아웃 %v): %v\n", timeout, err)
return
}
fmt.Println("결과:", result)
}
func main() {
handleRequest(1 * time.Second) // 타임아웃
handleRequest(5 * time.Second) // 성공
}
// 출력:
// 오류 (타임아웃 1s): context deadline exceeded
// 결과: 쿼리 결과
WithDeadline — 절대 시간 지정
package main
import (
"context"
"fmt"
"time"
)
func processWithDeadline() {
// 내일 자정까지의 데드라인
tomorrow := time.Now().Add(24 * time.Hour)
ctx, cancel := context.WithDeadline(context.Background(), tomorrow)
defer cancel()
// 현재 데드라인 확인
if deadline, ok := ctx.Deadline(); ok {
fmt.Printf("데드라인: %v\n", deadline.Format("2006-01-02 15:04:05"))
fmt.Printf("남은 시간: %v\n", time.Until(deadline).Round(time.Second))
}
// 단기 타임아웃으로 실제 테스트
shortCtx, shortCancel := context.WithDeadline(context.Background(),
time.Now().Add(100*time.Millisecond))
defer shortCancel()
select {
case <-time.After(500 * time.Millisecond):
fmt.Println("완료")
case <-shortCtx.Done():
fmt.Println("데드라인 초과:", shortCtx.Err())
}
}
func main() {
processWithDeadline()
}
WithValue — 요청 범위 값 전파
HTTP 요청 ID, 사용자 인증 정보 등을 컨텍스트에 담아 전파합니다.
package main
import (
"context"
"fmt"
)
// 타입 안전한 키 정의 (문자열 키 충돌 방지)
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
)
func middleware(ctx context.Context, userID, requestID string) context.Context {
ctx = context.WithValue(ctx, userIDKey, userID)
ctx = context.WithValue(ctx, requestIDKey, requestID)
return ctx
}
func serviceLayer(ctx context.Context) {
userID, ok := ctx.Value(userIDKey).(string)
if !ok {
fmt.Println("사용자 ID 없음")
return
}
requestID := ctx.Value(requestIDKey).(string)
fmt.Printf("서비스 레이어 - 사용자: %s, 요청 ID: %s\n", userID, requestID)
}
func repositoryLayer(ctx context.Context) {
userID := ctx.Value(userIDKey).(string)
fmt.Printf("저장소 레이어 - 사용자 %s의 데이터 조회\n", userID)
}
func main() {
ctx := context.Background()
ctx = middleware(ctx, "user-123", "req-456")
// 컨텍스트가 자동으로 전파됨
serviceLayer(ctx)
repositoryLayer(ctx)
}
WithValue 주의사항:
- 요청 범위 데이터만 저장 (함수 인자로 전달 가능한 데이터는 인자로)
string타입 키 대신 커스텀 타입 키 사용 (충돌 방지)- 많은 값을 저장하지 말 것 (구조체로 묶어서 저장 권장)
컨텍스트 트리 — 취소 전파
컨텍스트는 트리 구조입니다. 부모가 취소되면 모든 자식도 취소됩니다.
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, name string, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("%s 종료: %v\n", name, ctx.Err())
return
case <-time.After(300 * time.Millisecond):
fmt.Printf("%s 작업 중...\n", name)
}
}
}
func main() {
// 루트 컨텍스트
rootCtx, rootCancel := context.WithCancel(context.Background())
defer rootCancel()
// 자식 컨텍스트들
child1Ctx, child1Cancel := context.WithCancel(rootCtx)
defer child1Cancel()
child2Ctx, _ := context.WithTimeout(rootCtx, 2*time.Second)
// child2는 2초 후 자동 취소
var wg sync.WaitGroup
wg.Add(3)
go worker(rootCtx, "root-worker", &wg)
go worker(child1Ctx, "child1-worker", &wg)
go worker(child2Ctx, "child2-worker", &wg)
time.Sleep(1 * time.Second)
fmt.Println("child1 취소")
child1Cancel() // child1만 취소
time.Sleep(1500 * time.Millisecond)
fmt.Println("root 취소 (모두 종료)")
rootCancel() // root 취소 → root-worker, child2-worker도 취소
wg.Wait()
}
실전 예제 — HTTP 서버 요청 처리
package main
import (
"context"
"fmt"
"net/http"
"time"
)
type contextKey string
const requestIDKey contextKey = "requestID"
// 요청 ID 미들웨어
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = fmt.Sprintf("req-%d", time.Now().UnixNano())
}
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func slowHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestID := ctx.Value(requestIDKey).(string)
select {
case <-time.After(5 * time.Second):
fmt.Fprintf(w, "완료! (요청 ID: %s)", requestID)
case <-ctx.Done():
// 클라이언트가 연결을 끊으면 자동으로 ctx.Done()이 닫힘
fmt.Printf("요청 취소됨 (ID: %s): %v\n", requestID, ctx.Err())
http.Error(w, "요청이 취소됨", http.StatusRequestTimeout)
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/slow", slowHandler)
handler := requestIDMiddleware(mux)
fmt.Println("서버 시작: :8080")
http.ListenAndServe(":8080", handler)
}
핵심 정리
| 함수 | 용도 |
|---|---|
context.Background() | 루트 컨텍스트 (최상위) |
context.WithCancel(ctx) | 수동 취소 |
context.WithTimeout(ctx, d) | 지속 시간 기반 타임아웃 |
context.WithDeadline(ctx, t) | 절대 시간 기반 타임아웃 |
context.WithValue(ctx, k, v) | 요청 범위 값 전파 |
- 항상
cancel()호출— defer cancel()로 리소스 누수 방지 - 첫 번째 인자로 ctx 전달— 관례적으로 함수의 첫 번째 파라미터
- 부모 취소 시 자식 자동 취소— 트리 구조
- WithValue 키는 커스텀 타입 사용— 문자열 충돌 방지