Golang Context 딥 다이브: 제로에서 히로까지
Olivia Novak
Dev Intern · Leapcell

1. Context란 무엇인가?
간단히 말해서, Context는 Go 버전 1.7에 도입된 표준 라이브러리의 인터페이스입니다. 그 정의는 다음과 같습니다.
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
이 인터페이스는 네 가지 메서드를 정의합니다.
- Deadline:
context.Context
가 취소되는 시간, 즉 마감일을 설정합니다. - Done: 읽기 전용 채널을 반환합니다. Context가 취소되거나 마감일에 도달하면 이 채널이 닫혀 Context 체인의 끝을 나타냅니다.
Done
메서드를 여러 번 호출해도 동일한 채널이 반환됩니다. - Err:
context.Context
종료 이유를 반환합니다.Done
에서 반환된 채널이 닫힐 때만 null이 아닌 값을 반환합니다. 반환 값에는 두 가지 경우가 있습니다.context.Context
가 취소되면Canceled
를 반환합니다.context.Context
가 시간 초과되면DeadlineExceeded
를 반환합니다.
- Value:
context.Context
에서 키에 해당하는 값을 검색합니다. 이는 맵의get
메서드와 유사합니다. 동일한 컨텍스트에 대해 동일한 키로Value
를 여러 번 호출하면 동일한 결과가 반환됩니다. 해당하는 키가 없으면nil
을 반환합니다. 키-값 쌍은WithValue
메서드를 통해 작성됩니다.
2. Context 생성
2.1 루트 Context 생성
루트 컨텍스트를 생성하는 방법에는 주로 두 가지가 있습니다.
context.Background() context.TODO()
소스 코드를 분석해 보면 context.Background
와 context.TODO
사이에는 큰 차이가 없습니다. 둘 다 기능이 없는 빈 컨텍스트인 루트 컨텍스트를 만드는 데 사용됩니다. 그러나 일반적으로 현재 함수에 입력 매개변수로 컨텍스트가 없으면 일반적으로 context.Background
를 사용하여 아래로 전달할 시작 컨텍스트로 루트 컨텍스트를 만듭니다.
2.2 자식 Context 생성
루트 컨텍스트가 생성된 후에는 아무런 기능이 없습니다. 컨텍스트를 프로그램에서 유용하게 만들려면 context
패키지에서 제공하는 With
계열 함수를 사용하여 파생해야 합니다.
주로 다음과 같은 파생 함수가 있습니다.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context
현재 컨텍스트를 기반으로 각 With
함수는 새 컨텍스트를 만듭니다. 이는 우리가 익숙한 트리 구조와 유사합니다. 현재 컨텍스트를 부모 컨텍스트라고 하고, 새로 파생된 컨텍스트를 자식 컨텍스트라고 합니다. 루트 컨텍스트를 통해 네 가지 유형의 컨텍스트를 네 가지 With
계열 메서드를 사용하여 파생할 수 있습니다. 각 컨텍스트는 동일한 방식으로 With
계열 메서드를 호출하여 새로운 자식 컨텍스트를 계속 파생할 수 있으므로 전체 구조가 트리처럼 보입니다.
3. Context의 용도는 무엇입니까?
Context는 주로 두 가지 용도로 사용되며, 이는 프로젝트에서도 일반적으로 사용됩니다.
- 동시성 제어를 위해 고루틴을 정상적으로 종료합니다.
- 컨텍스트 정보 전달을 위해.
일반적으로 Context는 부모 및 자식 고루틴 간에 값을 전달하고 취소 신호를 보내는 메커니즘입니다.
3.1 동시성 제어
일반적인 서버의 경우 클라이언트 또는 브라우저로부터 요청을 수신하고 응답하기 위해 계속 실행됩니다. 백엔드 마이크로서비스 아키텍처에서 서버가 요청을 수신할 때 로직이 복잡하면 단일 고루틴에서 작업을 완료하지 않습니다. 대신 여러 고루틴을 만들어 함께 작업을 처리합니다. 요청이 들어오면 먼저 RPC1 호출을 거쳐 RPC2로 이동한 다음 두 개의 RPC가 더 생성되어 실행됩니다. RPC4 내부에는 또 다른 RPC 호출(RPC5)이 있습니다. 모든 RPC 호출이 성공한 후 결과가 반환됩니다. 전체 호출 프로세스 중에 RPC1에서 오류가 발생했다고 가정합니다. 컨텍스트가 없으면 모든 RPC가 완료될 때까지 기다린 후 결과를 반환해야 하는데, 이는 실제로 많은 시간을 낭비합니다. 오류가 발생하면 후속 RPC가 완료될 때까지 기다리지 않고 RPC1에서 직접 결과를 반환할 수 있기 때문입니다. RPC1에서 직접 실패를 반환하고 후속 RPC가 계속될 때까지 기다리지 않으면 후속 RPC의 실행은 실제로 의미가 없으며 컴퓨팅 및 I/O 리소스만 낭비합니다. 컨텍스트를 도입한 후에는 이 문제를 잘 처리할 수 있습니다. 자식 고루틴이 더 이상 필요하지 않으면 컨텍스트를 통해 정상적으로 닫도록 알릴 수 있습니다.
3.1.1 context.WithCancel
메서드는 다음과 같이 정의됩니다.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
context.WithCancel
함수는 취소 제어 함수입니다. 컨텍스트를 매개변수로 사용하고 context.Context
에서 새 자식 컨텍스트와 취소 함수 CancelFunc
를 파생할 수 있습니다. 이 자식 컨텍스트를 새 고루틴에 전달하면 이러한 고루틴의 종료를 제어할 수 있습니다. 반환된 취소 함수 CancelFunc
를 실행하면 현재 컨텍스트와 해당 자식 컨텍스트가 취소되고 모든 고루틴은 취소 신호를 동기적으로 수신합니다.
사용 예:
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) go Watch(ctx, "goroutine1") go Watch(ctx, "goroutine2") time.Sleep(6 * time.Second) // 고루틴1과 고루틴2가 6초 동안 실행되도록 합니다. fmt.Println("작업 종료!!!") cancel() // 고루틴1과 고루틴2에 닫도록 알립니다. time.Sleep(1 * time.Second) } func Watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s 종료!\n", name) // 기본 고루틴이 cancel을 호출하면 신호가 ctx.Done() 채널로 전송되고 이 부분이 메시지를 수신합니다. return default: fmt.Printf("%s 작업 중...\n", name) time.Sleep(time.Second) } } }
실행 결과:
goroutine2 작업 중...
goroutine1 작업 중...
goroutine1 작업 중...
goroutine2 작업 중...
goroutine2 작업 중...
goroutine1 작업 중...
goroutine1 작업 중...
goroutine2 작업 중...
goroutine2 작업 중...
goroutine1 작업 중...
goroutine1 작업 중...
goroutine2 작업 중...
작업 종료!!!
goroutine1 종료!
goroutine2 종료!
ctx, cancel := context.WithCancel(context.Background())
는 반환 함수 cancel
이 있는 ctx
를 파생하고 자식 고루틴에 전달합니다. 다음 6초 동안 cancel
함수가 실행되지 않으므로 자식 고루틴은 항상 default
문을 실행하고 모니터링 정보를 인쇄합니다. 6초 후 cancel
이 호출됩니다. 이때 자식 고루틴은 ctx.Done()
채널에서 메시지를 수신하고 return
을 실행하여 종료합니다.
3.1.2 context.WithDeadline
메서드는 다음과 같이 정의됩니다.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
context.WithDeadline
도 취소 제어 함수입니다. 이 메서드에는 두 개의 매개변수가 있습니다. 첫 번째 매개변수는 컨텍스트이고 두 번째 매개변수는 마감일입니다. 또한 자식 컨텍스트와 취소 함수 CancelFunc
를 반환합니다. 이를 사용할 때 마감일 전에 CancelFunc
를 수동으로 호출하여 자식 컨텍스트를 취소하고 자식 고루틴의 종료를 제어할 수 있습니다. 마감일까지 CancelFunc
를 호출하지 않은 경우 자식 컨텍스트의 Done()
채널도 자식 고루틴의 종료를 제어하는 취소 신호를 수신합니다.
사용 예:
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(4*time.Second)) // 현재 시간으로부터 4초 후로 제한 시간을 설정합니다. defer cancel() go Watch(ctx, "goroutine1") go Watch(ctx, "goroutine2") time.Sleep(6 * time.Second) // 고루틴1과 고루틴2가 6초 동안 실행되도록 합니다. fmt.Println("작업 종료!!!") } func Watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s 종료!\n", name) // 4초 후에 신호를 수신합니다. return default: fmt.Printf("%s 작업 중...\n", name) time.Sleep(time.Second) } } }
실행 결과:
goroutine1 작업 중...
goroutine2 작업 중...
goroutine2 작업 중...
goroutine1 작업 중...
goroutine1 작업 중...
goroutine2 작업 중...
goroutine1 종료!
goroutine2 종료!
작업 종료!!!
cancel
함수를 호출하지 않았지만 4초 후 자식 고루틴의 ctx.Done()
이 신호를 수신하고 exit
를 인쇄하고 자식 고루틴이 종료되었습니다. 이것이 WithDeadline
을 사용하여 자식 컨텍스트를 파생하는 방법입니다.
3.1.3 context.WithTimeout
메서드는 다음과 같이 정의됩니다.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
context.WithTimeout
은 기능 면에서 context.WithDeadline
과 유사합니다. 둘 다 시간 초과로 인해 자식 컨텍스트를 취소하는 데 사용됩니다. 유일한 차이점은 전달된 두 번째 매개변수에 있습니다. context.WithTimeout
에 의해 전달된 두 번째 매개변수는 특정 시간이 아니라 시간 지속 시간입니다.
사용 예:
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() go Watch(ctx, "goroutine1") go Watch(ctx, "goroutine2") time.Sleep(6 * time.Second) // 고루틴1과 고루틴2가 6초 동안 실행되도록 합니다. fmt.Println("작업 종료!!!") } func Watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s 종료!\n", name) // 기본 고루틴이 cancel을 호출하면 신호가 ctx.Done() 채널로 전송되고 이 부분이 메시지를 수신합니다. return default: fmt.Printf("%s 작업 중...\n", name) time.Sleep(time.Second) } } }
실행 결과:
goroutine2 작업 중...
goroutine1 작업 중...
goroutine1 작업 중...
goroutine2 작업 중...
goroutine2 작업 중...
goroutine1 작업 중...
goroutine1 작업 중...
goroutine2 작업 중...
goroutine1 종료!
goroutine2 종료!
작업 종료!!!
프로그램은 매우 간단합니다. 컨텍스트 파생 방법이 context.WithTimeout
으로 변경된 것 외에는 기본적으로 이전 context.WithDeadline
예제 코드와 동일합니다. 특히 두 번째 매개변수는 더 이상 특정 시간이 아니라 4초의 특정 시간 지속 시간입니다. 실행 결과도 동일합니다.
3.1.4 context.WithValue
메서드는 다음과 같이 정의됩니다.
func WithValue(parent Context, key, val interface{}) Context
context.WithValue
함수는 값 전달을 위해 부모 컨텍스트에서 자식 컨텍스트를 만듭니다. 함수 매개변수는 부모 컨텍스트, 키-값 쌍(키, 값)입니다. 컨텍스트를 반환합니다. 프로젝트에서 이 메서드는 일반적으로 링크 추적 및 구성 전달을 위해 고유한 요청 ID 및 추적 ID와 같은 컨텍스트 정보를 전달하는 데 사용됩니다.
사용 예:
package main import ( "context" "fmt" "time" ) func func1(ctx context.Context) { fmt.Printf("이름은: %s", ctx.Value("name").(string)) } func main() { ctx := context.WithValue(context.Background(), "name", "leapcell") go func1(ctx) time.Sleep(time.Second) }
실행 결과:
이름은: leapcell
Leapcell: Golang 앱 호스팅을 위한 최고의 서버리스 플랫폼
마지막으로 Golang 서비스를 배포하는 데 가장 적합한 플랫폼인 Leapcell을 추천합니다.
1. 다중 언어 지원
- JavaScript, Python, Go 또는 Rust로 개발합니다.
2. 무제한 프로젝트 무료 배포
- 사용량에 대해서만 지불하세요. 요청도, 청구도 없습니다.
3. 최고의 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불합니다.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
4. 간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
5. 손쉬운 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 크기 조정.
- 운영 오버헤드가 전혀 없습니다. 구축에만 집중하세요.
Leapcell Twitter: https://x.com/LeapcellHQ