Golang의 Context 패키지 완벽 가이드

Golang

Go 언어로 동시성 프로그래밍을 할 때 가장 중요한 패키지 중 하나가 바로 context 패키지입니다. Context는 고루틴 간의 작업 취소, 타임아웃 설정, 데이터 전달 등 다양한 기능을 제공하여 동시성 프로그램의 제어 흐름을 관리하는 데 필수적인 도구입니다. 이번 포스트에서는 Golang의 Context 패키지를 다양한 예제와 함께 자세히 알아보겠습니다.

Context란 무엇인가?

Context는 Go 언어에서 API 경계나 프로세스 간에 마감시간, 취소 신호, 그리고 기타 요청 범위의 값들을 전달하는 데 사용되는 인터페이스입니다. 주로 다음과 같은 상황에서 사용됩니다:

  1. 작업 취소 (Cancellation)
  2. 타임아웃 설정 (Timeout)
  3. 데드라인 설정 (Deadline)
  4. 요청 범위의 값 전달 (Request-scoped values)

이제 각각의 사용 사례에 대해 예제 코드와 함께 자세히 알아보겠습니다.

WithCancel – 작업 취소 기능

WithCancel 함수는 취소 가능한 컨텍스트를 만들어 줍니다. 이는 장시간 실행되는 작업을 명시적으로 취소해야 할 때 유용합니다.

package main

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

var wg sync.WaitGroup

func main() {
	// 웨이팅 그룹은 메인에서 서부 루틴이 졸료하는 시점까지 기다림.
	wg.Add(1)

	// context 생성.
	ctx, cancel := context.WithCancel(context.Background())

	// 무한 루프 시작...
	go PrintTick(ctx)

	// 5초 후에 cancel 실행.
	time.Sleep(5 * time.Second)
	cancel()

	wg.Wait()
}

func PrintTick(ctx context.Context) {
	tick := time.Tick(time.Second)

	for {
		select {
		case <-ctx.Done():
			fmt.Println("Done:", ctx.Err())
			// waitGroup을 이곳에서 종료...
			wg.Done()
			return
		case <-tick:
			fmt.Println("tick")
		}
	}
}

위 예제에서:

  1. context.WithCancel()을 사용하여 취소 가능한 컨텍스트와 취소 함수(cancel)를 생성합니다.
  2. PrintTick 고루틴을 시작하고, 이 고루틴은 매 초마다 “tick”을 출력합니다.
  3. 메인 함수에서 5초 후에 cancel() 함수를 호출하여 컨텍스트를 취소합니다.
  4. PrintTick 고루틴은 컨텍스트가 취소되면 종료됩니다.

실행 결과는 다음과 같습니다:

tick
tick
tick
tick
tick
Done: context canceled

WithDeadline – 특정 시간에 작업 취소

WithDeadline 함수는 특정 시간이 되면 자동으로 취소되는 컨텍스트를 생성합니다. 이는 작업이 특정 시간까지 완료되어야 할 때 유용합니다.

package main

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

var wg sync.WaitGroup

func main() {
	wg.Add(1)

	// 현제시간에 5초을 추가해서 데드라인으로 설정.
	d := time.Now().Add(5 * time.Second)
	ctx, cancel := context.WithDeadline(context.Background(), d)

	go PrintTick(ctx)

	time.Sleep(time.Second * 5)
	cancel()

	wg.Wait()
}

func PrintTick(ctx context.Context) {
	tick := time.Tick(time.Second)
	for {
		select {
		case <-ctx.Done():
			fmt.Println("Done:", ctx.Err())
			// waitGroup을 이곳에서 종료...
			wg.Done()
			return
		case <-tick:
			fmt.Println("tick")
		}
	}
}

위 예제에서:

  1. context.WithDeadline()을 사용하여 5초 후에 자동으로 취소되는 컨텍스트를 생성합니다.
  2. 데드라인에 도달하면 컨텍스트는 자동으로 취소됩니다.
  3. 여기서도 5초 후에 cancel()을 호출하지만, 이미 데드라인에 의해 컨텍스트가 취소되었을 가능성이 높습니다.

실행 결과는 WithCancel과 유사하지만, 취소 이유가 “context deadline exceeded”로 나타날 수 있습니다.

WithTimeout – 시간 제한 설정

WithTimeoutWithDeadline의 편의 버전으로, 지정된 시간이 지나면 자동으로 취소되는 컨텍스트를 생성합니다.

package main

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

var wg sync.WaitGroup

func main() {
	wg.Add(1)

	// 3초후 종료...
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

	go PrintTick(ctx)

	// 5초후 cancel이 예정되어 있어도 영향을 주지 않음.
	time.Sleep(time.Second * 5)
	cancel()

	wg.Wait()
}

func PrintTick(ctx context.Context) {
	tick := time.Tick(time.Second)
	for {
		select {
		case <-ctx.Done():
			fmt.Println("Done:", ctx.Err())
			wg.Done()
			return
		case <-tick:
			fmt.Println("tick")
		}
	}
}

위 예제에서:

  1. context.WithTimeout()을 사용하여 3초 후에 자동으로 취소되는 컨텍스트를 생성합니다.
  2. 메인 함수에서는 5초 후에 cancel()을 호출하지만, 이미 3초 타임아웃에 의해 컨텍스트가 취소되었기 때문에 실제로는 영향이 없습니다.

실행 결과는 다음과 같습니다:

tick
tick
tick
Done: context deadline exceeded

WithValue – 컨텍스트에 값 저장하기

WithValue 함수는 컨텍스트에 키-값 쌍을 저장할 수 있게 해줍니다. 이는 요청 범위의 데이터를 컨텍스트 체인을 따라 전달할 때 유용합니다.

package main

import (
	"context"
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)

	ctx := context.WithValue(context.Background(), "v", 3)

	go square(ctx)

	wg.Wait()
}

// 데이터 전달을 위해 사용된다.
func square(ctx context.Context) {
	if v := ctx.Value("v"); v != nil {
		n := v.(int)
		fmt.Println("Square:", n*n)
	}
	wg.Done()
}

위 예제에서:

  1. context.WithValue()를 사용하여 “v”라는 키와 3이라는 값을 가진 컨텍스트를 생성합니다.
  2. square 고루틴에서 컨텍스트로부터 “v” 키에 해당하는 값을 가져와 제곱을 계산합니다.

실행 결과는 다음과 같습니다:

Square: 9

Context 래핑 (Wrapping) – 여러 값 전달하기

여러 값을 컨텍스트에 저장하려면 컨텍스트를 래핑(wrapping)하여 체인을 만들 수 있습니다.

package main

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

var wg sync.WaitGroup

func main() {
	wg.Add(1)

	// multi value을 전달하기 위해서는 wrapping이 사용 된다.
	ctx, cancel := context.WithCancel(context.Background())
	ctx = context.WithValue(ctx, "s", 2)
	ctx = context.WithValue(ctx, "t", 3)

	go PrintTick(ctx)

	// 5초후 종료...
	time.Sleep(5 * time.Second)
	cancel()

	wg.Wait()
}

func PrintTick(ctx context.Context) {
	tick := time.Tick(time.Second)

	// 어떤 값을 받을지는 선택하면 된다. "s", "t"...
	// any -> int 변경하는 부분 참조...
	if v := ctx.Value("s"); v != nil {
		s := v.(int)
		tick = time.Tick(time.Duration(s) * time.Second)
	}

	for {
		select {
		case <-ctx.Done():
			fmt.Println("Done:", ctx.Err())
			wg.Done()
			return
		case <-tick:
			fmt.Println("tick")
		}
	}
}

위 예제에서:

  1. 먼저 context.WithCancel()로 취소 가능한 컨텍스트를 생성합니다.
  2. 그 다음 context.WithValue()를 연속해서 사용하여 “s”와 “t” 키에 각각 값을 저장합니다.
  3. PrintTick 고루틴에서는 “s” 키의 값을 사용하여 tick 간격을 조정합니다.

실행 결과는 2초마다 “tick”이 출력됩니다:

tick
tick
tick
Done: context canceled

Context 사용 시 주의사항

  1. 컨텍스트 값은 불변(immutable)이어야 합니다: 컨텍스트에 저장된 값을 변경하지 마세요. 새로운 값이 필요하면 새 컨텍스트를 생성하세요.
  2. 취소는 전파되지만 값은 전파되지 않습니다: 부모 컨텍스트가 취소되면 자식 컨텍스트도 자동으로 취소되지만, 자식 컨텍스트에 추가된 값은 부모 컨텍스트에서 접근할 수 없습니다.
  3. 컨텍스트 키는 고유해야 합니다: 패키지 간 충돌을 방지하기 위해 문자열 대신 사용자 정의 타입을 키로 사용하는 것이 좋습니다.
  4. cancel() 함수는 항상 호출해야 합니다: 컨텍스트가 더 이상 필요하지 않을 때는 defer cancel()을 사용하여 리소스 누수를 방지하세요.

결론

Context 패키지는 Go 언어에서 동시성 프로그래밍을 할 때 필수적인 도구입니다. 작업 취소, 타임아웃 설정, 데이터 전달 등의 기능을 통해 고루틴의 생명주기와 데이터 흐름을 효과적으로 관리할 수 있게 해줍니다.

이 글에서 살펴본 다양한 컨텍스트 함수들(WithCancel, WithDeadline, WithTimeout, WithValue)을 적절히 활용하면, 더 안정적이고 제어 가능한 동시성 프로그램을 작성할 수 있습니다.

Go 언어의 Context 패키지를 마스터하면, API 서버, 웹 애플리케이션, 분산 시스템과 같은 복잡한 동시성 프로그램을 더 효과적으로 개발할 수 있을 것입니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤