Go Context 패키지 2026 완벽 가이드: 취소, 타임아웃, 면접 질문

Go 언어의 context 패키지를 상세히 해설합니다. 취소 처리, 타임아웃 설정, 요청 범위 값 활용법부터 기술 면접에서 자주 나오는 질문과 답변까지 실용적인 코드 예제와 함께 설명합니다.

Go Context 패키지 2026 완벽 가이드: 취소, 타임아웃, 면접 질문

Go 언어의 context 패키지는 데드라인, 취소 시그널, 요청 범위 값을 API 경계를 넘어 전달하기 위한 표준 메커니즘을 제공합니다. HTTP 핸들러, 데이터베이스 쿼리, gRPC 호출, 백그라운드 워커 등 모든 프로덕션 Go 애플리케이션이 그레이스풀 셧다운과 타임아웃 관리를 위해 context를 사용합니다.

면접 필수 지식

context는 Go 기술 면접에서 가장 자주 출제되는 주제 중 하나입니다. 면접관은 context 전파 설명, 적절한 취소 처리 구현, 구조체에 context 저장과 같은 일반적인 실수 회피에 대해 설명을 요청합니다.

Context 인터페이스와 4가지 메서드

Context 인터페이스는 모든 context 구현체가 충족해야 하는 4가지 메서드를 정의합니다. 이러한 메서드를 이해하는 것이 효과적인 context 사용의 기반이 됩니다.

context_interface.gogo
type Context interface {
    // Deadline returns the time when work should be canceled
    Deadline() (deadline time.Time, ok bool)
    
    // Done returns a channel that closes when the context is canceled
    Done() <-chan struct{}
    
    // Err returns the reason why Done was closed
    Err() error
    
    // Value returns the value associated with key, or nil
    Value(key any) any
}

Done()은 수신 전용 채널을 반환합니다. 이 채널이 닫히면 이를 감시하고 있는 모든 고루틴이 즉시 시그널을 받습니다. 패턴 <-ctx.Done()은 취소가 발생할 때까지 블록됩니다. Err()은 그 이유를 설명합니다: 명시적으로 취소된 경우 context.Canceled, 타임아웃 또는 데드라인이 지난 경우 context.DeadlineExceeded입니다.

Background와 TODO로 컨텍스트 생성하기

루트 컨텍스트를 생성하는 함수는 두 가지입니다: context.Background()context.TODO()입니다. 둘 다 nil이 아닌 빈 컨텍스트를 반환하며 취소되지 않습니다.

context_creation.gogo
package main

import (
    "context"
    "log"
    "net/http"
)

func main() {
    // Background: the root context for your application
    ctx := context.Background()
    
    // Use it as parent for derived contexts
    server := &http.Server{Addr: ":8080"}
    
    // TODO: placeholder when context source is unclear
    // Static analysis tools can flag context.TODO() for review
    processLegacyData(context.TODO())
}

func processLegacyData(ctx context.Context) {
    // Context parameter enables future cancellation support
    log.Println("Processing data...")
}

Background()는 수신 요청, main 함수, 초기화 코드의 부모로 사용됩니다. TODO()는 올바른 context 소스가 아직 결정되지 않은 리팩토링 중에 플레이스홀더로 사용됩니다. 정적 분석 도구는 TODO() 사용을 플래그하여 후속 검토를 유도합니다.

WithCancel과 WithCancelCause를 사용한 취소 처리

수동 취소를 통해 부모 고루틴은 자식에게 작업을 중단해야 함을 알릴 수 있습니다. 이 패턴은 워커 풀, 백그라운드 태스크, 그레이스풀 셧다운 구현에서 볼 수 있습니다.

cancellation.gogo
package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int, results chan<- int) {
    for {
        select {
        case <-ctx.Done():
            // Check why cancellation occurred
            if cause := context.Cause(ctx); cause != nil {
                fmt.Printf("Worker %d stopped: %v\n", id, cause)
            }
            return
        default:
            // Simulate work
            time.Sleep(100 * time.Millisecond)
            results <- id * 10
        }
    }
}

func main() {
    // WithCancelCause provides error context
    ctx, cancel := context.WithCancelCause(context.Background())
    results := make(chan int, 10)
    
    // Start 3 workers
    for i := 1; i <= 3; i++ {
        go worker(ctx, i, results)
    }
    
    // Collect some results
    for i := 0; i < 5; i++ {
        fmt.Println("Result:", <-results)
    }
    
    // Cancel with a specific reason
    cancel(errors.New("shutdown requested by user"))
    
    // context.Cause retrieves the cancellation reason
    time.Sleep(50 * time.Millisecond)
    fmt.Println("Cause:", context.Cause(ctx))
}

Go 1.20에서 도입된 WithCancelCause는 단순한 WithCancel보다 풍부한 에러 정보를 제공합니다. context.Cause() 함수는 취소 함수에 전달된 에러를 가져와 복잡한 시스템에서의 디버깅을 용이하게 합니다.

Go 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

요청 처리를 위한 타임아웃과 데드라인

HTTP 핸들러, 데이터베이스 호출, 외부 API 요청에는 항상 타임아웃을 설정해야 합니다. context 패키지는 이 목적을 위해 WithTimeoutWithDeadline을 제공합니다. 둘 다 시간이 경과하면 자동으로 취소되는 컨텍스트를 생성합니다.

timeouts.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

// fetchFromAPI simulates an HTTP call
func fetchFromAPI(ctx context.Context, endpoint string) (string, error) {
    // Simulate variable response time
    responseTime := time.Duration(100+endpoint[0]%150) * time.Millisecond
    
    select {
    case <-time.After(responseTime):
        return fmt.Sprintf("Response from %s", endpoint), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // WithTimeout: relative duration
    ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
    defer cancel() // Always call cancel to release resources
    
    result, err := fetchFromAPI(ctx, "api.example.com/users")
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("Request timed out")
        } else {
            fmt.Println("Request canceled")
        }
        return
    }
    fmt.Println(result)
    
    // WithDeadline: absolute time
    deadline := time.Now().Add(500 * time.Millisecond)
    ctx2, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()
    
    result2, _ := fetchFromAPI(ctx2, "api.example.com/orders")
    fmt.Println(result2)
}

defer cancel() 패턴은 작업이 타임아웃 전에 완료되더라도 리소스가 해제되도록 보장합니다. 이 호출을 건너뛰면 리소스 누수가 발생합니다: 컨텍스트의 타이머 고루틴은 타임아웃이 만료될 때까지 활성 상태로 유지됩니다.

WithValue를 사용한 요청 범위 값 전달

WithValue는 요청 범위 데이터를 컨텍스트에 첨부합니다. 일반적인 사용 사례로는 요청 ID, 인증 토큰, 트레이싱 스팬이 있습니다. 공식 문서에서는 충돌을 피하기 위해 커스텀 타입을 키로 사용할 것을 강조합니다.

context_values.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "time"
)

// Define custom key types to avoid collisions
type contextKey string

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

// middleware adds request ID to context
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 = generateRequestID()
        }
        
        // Create new context with request ID
        ctx := context.WithValue(r.Context(), requestIDKey, requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// handler retrieves values from context
func handler(w http.ResponseWriter, r *http.Request) {
    requestID, ok := r.Context().Value(requestIDKey).(string)
    if !ok {
        requestID = "unknown"
    }
    
    log.Printf("[%s] Processing request", requestID)
    fmt.Fprintf(w, "Request ID: %s", requestID)
}

func generateRequestID() string {
    return fmt.Sprintf("req-%d", time.Now().UnixNano())
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)
    
    http.ListenAndServe(":8080", requestIDMiddleware(mux))
}

컨텍스트 값은 불변 체인을 형성합니다: 각 WithValue 호출은 부모를 래핑하는 새 컨텍스트를 생성합니다. 조회는 이 체인을 순회하므로 깊이 중첩된 값에 대한 접근은 느려집니다. 요청 범위 데이터만 저장하고 애플리케이션 설정이나 옵션 파라미터는 저장하지 마십시오.

WithoutCancel과 AfterFunc 유틸리티

Go 1.21에서는 정리 태스크 등 부모의 취소와 관계없이 완료되어야 하는 작업을 위한 WithoutCancel이 추가되었습니다. AfterFunc는 컨텍스트 취소가 발생했을 때 콜백을 스케줄링합니다.

utilities.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "time"
)

func saveAuditLog(ctx context.Context, message string) error {
    // Use WithoutCancel: audit logs must complete even if request canceled
    cleanCtx := context.WithoutCancel(ctx)
    
    // Simulate database write
    select {
    case <-time.After(50 * time.Millisecond):
        log.Printf("Audit: %s", message)
        return nil
    case <-cleanCtx.Done():
        // This branch never executes: WithoutCancel context is never canceled
        return cleanCtx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    
    // AfterFunc: register cleanup when context ends
    stop := context.AfterFunc(ctx, func() {
        fmt.Println("Context ended, running cleanup")
    })
    
    // Perform operation
    time.Sleep(50 * time.Millisecond)
    
    // Cancel stops the AfterFunc from running if called before context ends
    if stop() {
        fmt.Println("Cleanup prevented")
    }
    
    cancel()
    
    // Audit log completes despite parent cancellation
    saveAuditLog(ctx, "Operation completed")
}

WithoutCancel은 부모 컨텍스트의 값을 보존하지만 취소 시그널은 무시합니다. 이 패턴은 요청 결과와 관계없이 성공해야 하는 로깅, 메트릭 전송, 데이터베이스 커밋에 적합합니다.

면접 질문과 답변

Go 기술 면접을 준비하려면 context의 내부 구조와 모범 사례를 이해해야 합니다. 이러한 질문은 백엔드 서비스에 Go를 사용하는 기업의 면접에서 자주 출제됩니다. Go 면접 준비에 대한 자세한 내용은 Context 패키지 면접 문제 모듈을 참조하십시오.

context는 왜 첫 번째 파라미터여야 하는가?

Go 팀은 context 전파를 가시화하고 일관성을 유지하기 위해 이 관례를 확립했습니다. context를 첫 번째에 배치함으로써 함수가 취소를 존중한다는 것을 독자에게 알립니다. 표준 라이브러리는 이 패턴을 따릅니다: http.Request.Context(), database/sql.QueryContext(), grpc.UnaryInterceptor는 모두 첫 번째 인자로 context를 기대합니다.

구조체에 context를 저장하면 어떻게 되는가?

구조체에 context를 저장하면 요청 라이프사이클 모델이 깨집니다. 구조체는 생성된 요청보다 더 오래 존재할 수 있으며, 이로 인해 작업이 오래된 취소 시그널을 사용하거나 새로운 데드라인을 놓칠 수 있습니다. context 문서는 이 패턴에 대해 명시적으로 경고합니다.

go
// BAD: context stored in struct
type Service struct {
    ctx context.Context // Never do this
}

// GOOD: pass context to each method
type Service struct{}

func (s *Service) Process(ctx context.Context, data []byte) error {
    // Context flows through the call chain
    return s.save(ctx, data)
}

고루틴에서 context를 어떻게 처리해야 하는가?

고루틴은 메인 루프 또는 select 문에서 ctx.Done()을 체크해야 합니다. done 채널을 무시하면 고루틴 누수가 발생합니다: 부모 함수가 반환되어도 고루틴은 리소스를 계속 소비합니다.

go
// Correct pattern for long-running goroutines
func processStream(ctx context.Context, stream <-chan Data) {
    for {
        select {
        case <-ctx.Done():
            log.Println("Shutting down processor")
            return
        case data, ok := <-stream:
            if !ok {
                return
            }
            handle(data)
        }
    }
}

context.TODO()는 언제 사용해야 하는가?

레거시 코드에 context 지원을 추가하는 점진적 리팩토링 중에 TODO()를 사용합니다. 이는 적절한 context 전파가 필요한 위치를 표시합니다. 프로덕션 코드는 최종적으로 모든 TODO() 호출을 요청 핸들러 또는 애플리케이션 초기화의 실제 context로 대체해야 합니다. Go 동시성 패턴에 대한 자세한 내용은 Go 동시성: 고루틴과 채널을 참조하십시오.

피해야 할 일반적인 실수

context의 잘못된 사용은 부하 시 또는 셧다운 시퀀스 중에 나타나는 미묘한 버그로 이어집니다. 이러한 패턴을 인식하면 프로덕션 환경에서의 인시던트를 방지할 수 있습니다.

mistakes.gogo
package main

import (
    "context"
    "time"
)

// MISTAKE 1: Ignoring context cancellation
func badWorker(ctx context.Context) {
    for {
        // Missing select on ctx.Done()
        doExpensiveWork() // Never stops when context canceled
    }
}

// MISTAKE 2: Not calling cancel
func leakyTimeout() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second)
    // Timer goroutine leaks until timeout expires
    _ = ctx
}

// MISTAKE 3: Using string keys for values
func collisionProne(ctx context.Context) context.Context {
    // Different packages might use same string key
    return context.WithValue(ctx, "userID", 123) // Bad
}

// CORRECT versions
func goodWorker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            doExpensiveWork()
        }
    }
}

func properTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel() // Always release resources
    _ = ctx
}

type userIDKey struct{}

func collisionSafe(ctx context.Context) context.Context {
    return context.WithValue(ctx, userIDKey{}, 123) // Good
}

func doExpensiveWork() {}

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

프로덕션에서 Go Context 사용을 위한 핵심 포인트

  • context는 함수의 첫 번째 파라미터로 전달하고 구조체에는 절대 저장하지 않는다
  • 리소스 누수를 방지하기 위해 WithTimeout, WithDeadline, WithCancel 후에는 항상 cancel()을 defer한다
  • 복잡한 취소 체인에서의 디버깅을 개선하기 위해 WithCancelCausecontext.Cause()를 사용한다
  • 그레이스풀 셧다운을 활성화하고 누수를 방지하기 위해 고루틴 루프에서 <-ctx.Done()을 체크한다
  • 패키지 간 충돌을 피하기 위해 WithValue에는 커스텀 키 타입을 사용한다
  • 부모의 취소와 관계없이 완료되어야 하는 정리 작업에는 WithoutCancel을 적용한다
  • 애플리케이션 초기화에는 context.Background()를 우선하고 context.TODO()는 리팩토링 중에만 사용한다
오늘의 챌린지

Go 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 8월 29일 업데이트

태그

#go
#golang
#context
#concurrency
#interview

공유

관련 기사