[Golang] Context 패키지
Golang을 쓰다 보면 몇 가지 특이한 미리 정의된 패키지/구조체들을 볼 수 있다. error.Error, testing.T 같은 것들...
그래도 저 개념들은 우리가 자주 봐왔기 때문에 익숙한 편이다. 그런데 context.Context는 자주 보이는데 그 개념이 생소하다. 특히나 저 객체는 함수 인자로 자꾸 넘어가곤 해서 사용 빈도도 굉장히 잦은 편인데, 대체 어디 쓰이는 놈인걸까?
Go에서 Context란?
공식 문서를 보면 아래와 같이 깔끔하게 한줄로 설명하고 있다.
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
요약하면,
- 함수 수행의 마감시간 (deadlock)
- 함수 수행 도중 취소 여부 (cancellation)
- 이외 다른 정보들 (request-scoped values)
정도의 정보를 담고 있다는 것이다.
구조체 이름이 꽤나 추상적이어서 무엇에 대한 context일까 궁금했는데, 실제로 실행중인 상황에 대한 범용적인 context였다 -_-;
context interface는 아래와 같이 정의되어 있다. 이 인터페이스가 알맞는 어떤 구현이든, context로 사용할 수 있는 것이다. 보면 알겠지만 정말 간단명료하게 생겼다.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
이외에도, context 패키지에서 제공하는 몇 가지 함수들이 있다.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val any) Context
그렇다면 이걸 어떻게 쓸 수 있을지, 각각의 경우에 대해서 하나하나 알아봤다.
Context로 cancellation 수행하기
위에서 사용한 WithCancel을 이용하여 구현할 수 있다. 예제 코드를 보면 아래와 같이 사용할 수 있다.
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers
go DoSomeWork(ctx)
여기서 한가지 주목할 점은, 동일 컨텍스트를 보는 여러 고루틴이 있다면, 그 컨텍스트를 취소함으로서 연관된 고루틴을 모두 취소할 수 있다는 점이다. 이는 개발의 용이성에서 큰 장점을 가지는데, 일례로 상당히 깊은 depth를 가지는 콜스택 상황에서 취소하는 상황이 일어났을 때, 리턴값에 “취소"에 해당하는 marking을 할 필요 없이 상위 함수들도 같은 콘텍스트를 참조하고 있기 때문에 “취소"라는 case를 바로 탈 수 있다. 다시 말하면, 깊은 콜스택 상황에서 취소가 발생하더라도 어떤 코드를 타게 되는지가 명확해진다.
내부 구현
참고로 WithCancel의 내부 구현을 보면, 기존 context의 값을 변경하는 것이 아니라, 기존 context를 parent로 하는 새로운 cancelContext를 생성하는 것을 확인할 수 있다. 그래서 context를 참조로 쓰는 게 아니라 그냥 값으로 전달하나 싶다...
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
Context로 deadline 설정하고 확인하기
위의 context cancellation과 거의 동일하다. cancel에 deadline만 새로 생긴 수준.
d := time.Now().Add(shortDuration)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
WithTimeout도 본질적으로는 동일하기 때문에 딱히 설명할 점은 없다.
WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
그리고 context.Deadline
call을 통해 정보를 가져올 수 있다. 정해진 Deadline이 지나면 cancel되도록 하고 싶을 때 쓴다거나... 정도의 용도로 쓸 수 있다.
struct MyContext {}
// ... do some implementation with mycontext ...
func main() {
ctx MyContext
DoSomeWork(ctx)
}
func DoSomeWork(ctx context.Context) {
if d, ok := ctx.Deadline(); ok && !d.IsZero() {
DoSomeOtherWorkWithDeadline(d)
}
// ...
}
Context로 실행에 필요한 정보들 전달하기
context.WithValue를 이용하여 정보를 context에 담을 수 있다. 이 또한 기존의 context값을 변경하지 않고, 새로운 Context를 생성하는 형태이다. 예시코드의 일부를 잘라오면...
ctx := context.WithValue(context.Background(), "key", "value")
DoSomething(ctx)
참고로, 내부 구현을 보면 정말 간단하게 생겼다... key/value를 통상적인 map을 써서 하는 게 아니라 composite 형태로 구현한 건 신기하네...
꼭 Context를 만들어야 하나요?
Context를 받도록 뚫어놓았지만 굳이 만들 필요성을 못 느낄 수 있을 것 같다.
이럴 때 사용 가능한 미리 정의된 context를 마련해 놓았는데, context.TODO
를 쓰면 된다고 한다.
Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
실제 구현을 보면, 그냥 텅텅 비어있다. 딱 코드 컴파일을 위한 interface만 구색이 갖춰져 있음. 그러니까, nil 특수 처리 같은 건 절대 하지마! 라는 의도로 만들어 놓은 것 같다. 좋네.
비슷한 물건으로 context.Background
도 있는데, 사실상 TODO랑 똑같은 물건이다. 자세한 정보는 문서와 구현 코드에서 찾아보실 수 있습니다.
물론, 필요하다면 Context를 새로 만들어서 쓸 수 있을 것이다. Context에 추가적인 상태를 담고, 이를 계속해서 전달해야 하거나 아니면 특수한 동작을 수행해야 하거나 등등...
참고
- https://jaehue.github.io/post/how-to-use-golang-context/
- http.Request를 이용하여 실제로 context를 사용하는 예제도 있으니 참고하면 좋을 듯.