Context?

Go 1.7에서 추가된 context 패키지는 중첩된 구조의 API에서 문맥을 공유하기 위해 사용됩니다. context 패키지는 context.WithCancel()이나 context.WithTimeout() 등을 통해 API의 종료를 처리하기도 하고, context.WithValue()를 값을 공유하기도 합니다. 오늘은 context.WithValue()를 통해 값을 보다 안전하게 공유할 수 있는 팁을 알아보도록 하겠습니다. context 패키지에 대해 아직 이해가 잘 되지 않으신다면 Go Concurrency Patterns: Context를 먼저 보고 오시는 것을 추천드립니다.

Problem

package main

import (
    "context"
    "fmt"
)

func main() {
    ctx := context.Background() // 새로운 context를 생성합니다.

    ctx = context.WithValue(ctx, "key", "value") // 첫 번째 인자로 받은 ctx룰 부모로하고 key에 따른 value를 얻을 수 있는 context를 반환합니다.

    fmt.Println(ctx.Value("key")) // value

    ctx = context.WithValue(ctx, "key", "what the...") // 값이 덮어 씌워집니다.

    fmt.Println(ctx.Value("key")) // what the...    : 예상치 못한 결과가 나옵니다.
}

위 코드는 context.WithValue()를 통해 값을 저장하고, Context.Value() 메서드를 통해 값을 읽어오는 아주 간단한 예제입니다. context.WithValue()의 두 번째 인자는 interface{} 타입으로 모든 타입이 올 수 있고, 대부분의 경우 key에 string 타입을 많이 사용합니다.

그러다 보니 자주 사용하는 특정 키워드가 key인 경우 중복되는 일이 생길 수 있고, 기존 값이 덮어 씌워지며 전역변수가 오염되는 것 처럼 큰 문제를 불러 일으키기도 합니다.

위 코드와 같이 간단한 구조라면 key가 중복되는 일이 거의 생기지 않고, 만약 생긴다 하더라도 문제를 어렵지 않게 찾을 수 있겠지만, 중첩된 구조에서 다양한 함수를 넘나들며 많이 사용되는 context의 특성상 디버깅이 쉽지 않을 때도 있습니다. 그렇다면 이 문제를 어떻게 해결할 수 있을까요?

Solution

외부 패키지에서의 key와 중복되지 않도록 하기위한 몇 가지 방법에 대해 알아보도록 하겠습니다.

첫 번째 방법은 key에 특정 값을 추가하는 함수를 만들어 사용하는 것입니다.

package main

import (
    "context"
    "fmt"
)

func createContextKey(key string) string { // 함수를 소문자로 시작하여 패키지 내부에서만 사용할 수 있도록 합니다.
    return "specific value" + key // key에 특정 문자열을 더해 key의 중복을 막습니다.
}

func main() {
    ctx := context.Background() // 새로운 context를 생성합니다.

    ctx = context.WithValue(ctx, createContextKey("key"), "value") // createkey()를 사용해 key를 만들어 사용합니다.

    fmt.Println(ctx.Value("key")) // <nil>
    fmt.Println(ctx.Value(createContextKey("key"))) // value
}

위와 같이 key 값에 특정 문자열을 붙이는 함수를 만들어 다른 key와의 중복을 방지할 수 있습니다. 이 방법이 key의 중복을 해결할 수 있는 완벽한 해결책은 아니지만, 중복을 어느정돈 피할 수 있습니다.

두 번째 방법은 string 타입을 새로운 타입으로 정의하는 것입니다. Go에서의 타입은 패키지에 종속적이므로, 외부 패키지의 타입과 겹치지 않습니다.

package main

import (
    "context"
    "fmt"
)

type contextKey string // 새로운 타입을 정의합니다.

func main() {
    ctx := context.Background()

    ctx = context.WithValue(ctx, contextKey("key"), "value") // string 타입을 contextKey 타입으로 캐스팅하여 key로 사용합니다.

    fmt.Println(ctx.Value("key")) // <nil>
    fmt.Println(ctx.Value(contextKey("key"))) // value
}

위를 응용하여, struct{} 타입을 통해 key 하나당 타입 하나를 정의해서 사용할 수 있습니다.

package main

import (
    "context"
    "fmt"
)

type contextKey struct{}

func main() {
    ctx := context.Background()

    ctx = context.WithValue(ctx, contextKey{}, "value") // contextKey{}를 key로 사용합니다.

    fmt.Println(ctx.Value(contextKey{})) // value
}