Go 요청 처리에서 컨텍스트 생명주기 이해하기
Wenhao Wang
Dev Intern · Leapcell

소개
현대의 마이크로서비스 및 동시성 애플리케이션의 복잡한 세계에서 작업의 생명주기를 관리하는 것은 매우 중요합니다. 데이터베이스 쿼리, 외부 API 호출 또는 복잡한 계산과 같은 다양한 작업을 수행하는 여러 서비스를 거치는 사용자 요청을 상상해 보세요. 적절한 제어 없이는 느린 데이터베이스 쿼리나 응답 없는 외부 서비스가 리소스를 백로그시켜 성능 저하 또는 시스템 충돌까지 초래할 수 있습니다. 이때 Go의 context.Context
패키지가 강력하고 우아한 솔루션을 제공하여 작업 생명주기를 관리합니다. 이를 통해 API 경계와 고루틴 트리를 넘어 마감 시간, 취소 신호 및 요청 범위 값을 전파하여 리소스를 효율적으로 사용하고 작업을 우아하게 종료할 수 있습니다. 이 글에서는 context.Context
가 효율적인 요청 처리, 강력한 타임아웃 제어 및 원활한 취소를 어떻게 촉진하는지 탐구하여 궁극적으로 더 복원력 있고 성능이 뛰어난 Go 애플리케이션을 만들 수 있습니다.
컨텍스트와 그 역할 이해하기
세부 사항을 살펴보기 전에 context.Context
와 관련된 핵심 개념에 대한 기초적인 이해를 확립해 보겠습니다.
컨텍스트 인터페이스: 핵심적으로 context.Context
는 API 경계와 고루틴 트리를 넘어 마감 시간, 취소 신호 및 요청 범위 값을 전달하는 메서드를 제공하는 인터페이스입니다. 실제로 값을 저장하는 것이 아니라 이러한 신호를 위한 채널 역할을 합니다. context.Context
의 주요 특징은 불변이며 동시 사용에 안전하다는 것입니다.
취소: 작업을 중지하도록 신호를 보내는 기능입니다. 이는 특히 장기 실행 작업의 경우 리소스 누수를 방지하고 응답성을 향상시키는 데 중요합니다.
마감 시간/타임아웃: 작업이 자동으로 취소되어야 하는 특정 시점 또는 기간입니다. 이 메커니즘은 응답 없는 외부 서비스 또는 지나치게 긴 계산을 방지합니다.
요청 범위 값: 컨텍스트에 임의의 불변이며 스레드 안전한 값을 연결하는 기능입니다. 이 값은 해당 컨텍스트를 상속하는 모든 고루틴에서 액세스할 수 있으므로 인증 토큰, 추적 ID 또는 사용자 메타데이터를 요청 수명 주기 전체에 걸쳐 전달하는 데 이상적입니다.
고루틴 동기화: context.Context
는 종종 고루틴과 함께 사용하여 집단 생명주기를 관리합니다. 부모 컨텍스트가 취소되면 파생된 모든 컨텍스트와 이를 수신 대기하는 고루틴도 암시적으로 취소됩니다.
컨텍스트 생성의 시작
context.Context
객체는 일반적으로 context.Background()
또는 context.TODO()
를 사용하여 생성됩니다.
-
context.Background()
: 모든 작업의 루트 컨텍스트입니다. 취소되지 않으며 마감 시간이 없고 값을 전달하지 않습니다. 일반적으로main
함수,init
함수 및 테스트의 시작점입니다.package main import ( "context" "fmt" ) func main() { ctx := context.Background() fmt.Printf("Background Context: %+v\n", ctx) }
-
context.TODO()
: 적절한 컨텍스트를 아직 알 수 없거나 사용할 수 없을 때 사용됩니다. 나중에 컨텍스트를 추가해야 함을 나타냅니다.context.Background()
와 동일하게 작동하지만 가능한 경우 리팩터링하고 더 적절한 컨텍스트를 전달해야 함을 상기시키는 역할을 합니다.package main import ( "context" "fmt" ) func main() { ctx := context.TODO() fmt.Printf("TODO Context: %+v\n", ctx) }
취소를 통한 생명주기 관리
작업 생명주기를 관리하는 가장 일반적인 방법은 취소를 통한 것입니다. context.WithCancel()
은 반환된 cancel
함수를 호출하여 취소할 수 있는 새 컨텍스트를 생성합니다.
package main import ( "context" "fmt" "time" ) func longRunningOperation(ctx context.Context, id int) { select { case <-time.After(3 * time.Second): fmt.Printf("Operation %d completed successfully\n", id) case <-ctx.Done(): fmt.Printf("Operation %d canceled: %v\n", id, ctx.Err()) } } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // main이 조기에 종료될 경우 취소 보장 fmt.Println("Starting long running operations...") go longRunningOperation(ctx, 1) time.Sleep(1 * time.Second) fmt.Println("Canceling operation 1 after 1 second...") cancel() // 'ctx'와 그 모든 자식 컨텍스트를 취소합니다. // 취소 *이후*에 시작된 새 작업 // ctx에서 파생된 경우 취소된 상태를 상속합니다. go longRunningOperation(ctx, 2) // 즉시 취소됩니다. time.Sleep(2 * time.Second) // 메시지를 인쇄할 시간 허용 fmt.Println("Main function finished.") }
이 예제에서는 longRunningOperation
이 자체 완료 신호와 컨텍스트의 Done
채널 모두를 수신 대기합니다. cancel()
이 호출되면 ctx.Done()
이 닫히고 이를 수신 대기하는 모든 고루틴이 정상적으로 종료됩니다. longRunningOperation(ctx, 2)
는 이미 취소된 컨텍스트로 시작되었기 때문에 즉시