
Go 언어로 동시성 프로그래밍을 할 때 가장 중요한 패키지 중 하나가 바로 context
패키지입니다. Context는 고루틴 간의 작업 취소, 타임아웃 설정, 데이터 전달 등 다양한 기능을 제공하여 동시성 프로그램의 제어 흐름을 관리하는 데 필수적인 도구입니다. 이번 포스트에서는 Golang의 Context 패키지를 다양한 예제와 함께 자세히 알아보겠습니다.
Context란 무엇인가?
Context는 Go 언어에서 API 경계나 프로세스 간에 마감시간, 취소 신호, 그리고 기타 요청 범위의 값들을 전달하는 데 사용되는 인터페이스입니다. 주로 다음과 같은 상황에서 사용됩니다:
- 작업 취소 (Cancellation)
- 타임아웃 설정 (Timeout)
- 데드라인 설정 (Deadline)
- 요청 범위의 값 전달 (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")
}
}
}
위 예제에서:
context.WithCancel()
을 사용하여 취소 가능한 컨텍스트와 취소 함수(cancel
)를 생성합니다.PrintTick
고루틴을 시작하고, 이 고루틴은 매 초마다 “tick”을 출력합니다.- 메인 함수에서 5초 후에
cancel()
함수를 호출하여 컨텍스트를 취소합니다. 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")
}
}
}
위 예제에서:
context.WithDeadline()
을 사용하여 5초 후에 자동으로 취소되는 컨텍스트를 생성합니다.- 데드라인에 도달하면 컨텍스트는 자동으로 취소됩니다.
- 여기서도 5초 후에
cancel()
을 호출하지만, 이미 데드라인에 의해 컨텍스트가 취소되었을 가능성이 높습니다.
실행 결과는 WithCancel
과 유사하지만, 취소 이유가 “context deadline exceeded”로 나타날 수 있습니다.
WithTimeout – 시간 제한 설정
WithTimeout
은 WithDeadline
의 편의 버전으로, 지정된 시간이 지나면 자동으로 취소되는 컨텍스트를 생성합니다.
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")
}
}
}
위 예제에서:
context.WithTimeout()
을 사용하여 3초 후에 자동으로 취소되는 컨텍스트를 생성합니다.- 메인 함수에서는 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()
}
위 예제에서:
context.WithValue()
를 사용하여 “v”라는 키와 3이라는 값을 가진 컨텍스트를 생성합니다.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")
}
}
}
위 예제에서:
- 먼저
context.WithCancel()
로 취소 가능한 컨텍스트를 생성합니다. - 그 다음
context.WithValue()
를 연속해서 사용하여 “s”와 “t” 키에 각각 값을 저장합니다. PrintTick
고루틴에서는 “s” 키의 값을 사용하여 tick 간격을 조정합니다.
실행 결과는 2초마다 “tick”이 출력됩니다:
tick
tick
tick
Done: context canceled
Context 사용 시 주의사항
- 컨텍스트 값은 불변(immutable)이어야 합니다: 컨텍스트에 저장된 값을 변경하지 마세요. 새로운 값이 필요하면 새 컨텍스트를 생성하세요.
- 취소는 전파되지만 값은 전파되지 않습니다: 부모 컨텍스트가 취소되면 자식 컨텍스트도 자동으로 취소되지만, 자식 컨텍스트에 추가된 값은 부모 컨텍스트에서 접근할 수 없습니다.
- 컨텍스트 키는 고유해야 합니다: 패키지 간 충돌을 방지하기 위해 문자열 대신 사용자 정의 타입을 키로 사용하는 것이 좋습니다.
cancel()
함수는 항상 호출해야 합니다: 컨텍스트가 더 이상 필요하지 않을 때는defer cancel()
을 사용하여 리소스 누수를 방지하세요.
결론
Context 패키지는 Go 언어에서 동시성 프로그래밍을 할 때 필수적인 도구입니다. 작업 취소, 타임아웃 설정, 데이터 전달 등의 기능을 통해 고루틴의 생명주기와 데이터 흐름을 효과적으로 관리할 수 있게 해줍니다.
이 글에서 살펴본 다양한 컨텍스트 함수들(WithCancel
, WithDeadline
, WithTimeout
, WithValue
)을 적절히 활용하면, 더 안정적이고 제어 가능한 동시성 프로그램을 작성할 수 있습니다.
Go 언어의 Context 패키지를 마스터하면, API 서버, 웹 애플리케이션, 분산 시스템과 같은 복잡한 동시성 프로그램을 더 효과적으로 개발할 수 있을 것입니다.