Table of Contents

Golang - Context 개념 및 기본 예제

HTTP 서버는 마이크로 서비스의 요청 체인을 식별하기 위해 추적 ID를 원하거나 너무 오래 걸린다면 요청을 중단하는 타이머를 설정할 수 있다. 하지만 Go에서는 고루틴이 고유한 식별자를 가지고 있지 않기 때문에 요청 메타 데이터를 처리하기 힘들다. 이에 Go는 컨텍스트라는 구성을 통해 요청 메타 데이터 문제를 해결했다.

컨텍스트는 요청 메타 데이터와 함께 고루틴 간의 작업 취소, 타임아웃, 값 전달 등과 같은 고루틴의 문맥관리를 제공하는 인터페이스이다.


기본 예제

컨텍스트는 단순히 context 패키지에 정의된 Context 인터페이스를 만족하는 인스턴스이다.

1func logic(ctx context.Context, info string) (string, error) {
2  // do something
3  return "", nil
4}

함수에서 마지막에 반환하는 것은 오류라는 관례가 있는 것처럼, 프로그램을 통해 명시적으로 전달되는 함수의 첫 번째 파라미터로써 컨텍스트를 사용한다.


context 패키지에는 컨텍스트를 생성하고 래핑하기 위한 여러 팩토리 함수가 포함된다. 기존 컨텍스트가 없는 경우에는 context.Background 함수를 사용하여 빈 초기 컨텍스트를 만든다.

1ctx := context.Background()
2result, err := logic(ctx, "a string")

빈 컨텍스트는 기존 컨텍스트를 래핑하는 시작점이 되고, 메타 데이터를 추가하기 위해서는 팩토리 함수를 사용해 기존 컨텍스트를 래핑하여 새로운 컨텍스트를 생성해야 한다.


HTTP 서버를 작성할 때, 미들웨어 계층에서 최상위 http.Handler로 컨텍스트를 전달하거나 획득하는 예시이다.

1func Middleware(handler http.Handler) http.Handler {
2  return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
3    ctx := req.Context()  // 1
4    // 컨텍스트로 작업을 감싼다.
5    req = req.WithContext(ctx) // 2
6    handler.ServeHTTP(rw, req)
7  })
8}
  1. Context는 요청과 연관된 context.Context를 반환한다.
  2. WithContext는 생성된 context.Context와 결합된 request 와 함께 새로운 http.Request를 반환한다.

HTTP 서버의 핸들러 함수이다.

 1func handler(rw http.ResponseWriter, req *http.Request) {
 2    ctx := req.Context()
 3    err := req.ParseForm()
 4    if err != nil {
 5        rw.WriteHeader(http.StatusInternalServerError)
 6        rw.Write([]byte(err.Error()))
 7        return
 8    }
 9    data := req.FormValue("data")
10    result, err := logic(ctx, data) // 1
11    if err != nil {
12        rw.WriteHeader(http.StatusInternalServerError)
13        rw.Write([]byte(err.Error()))
14        return
15    }
16    rw.Write([]byte(result))
17}

요청에서 Context 메서드를 사용하여 컨텍스트를 추출하고 1) 첫 번째 파라미터로 컨텍스트와 함께 비즈니스 로직(Logic 메서드)를 호출한다.


WithContet 메서드를 다음과 같은 상황에서도 사용할 수 있다.

 1type ServiceCaller struct {
 2    client *http.Client
 3}
 4
 5func (sc ServiceCaller) callAnotherService(ctx context.Context, data string) (string, error) {
 6    req, err := http.NewRequest(http.MethodGet, "<http://example.com?data=>"+data, nil)
 7
 8    if err != nil {
 9        return "", err
10    }
11    req = req.WithContext(ctx)
12    resp, err := sc.client.Do(req)
13    if err != nil {
14        return "", err
15    }
16    defer resp.Body.Close()
17    if resp.StatusCode != http.StatusOK {
18        return "", fmt.Errorf("unexpected status code %d", resp.StatusCode)
19    }
20
21    // 응답 처리
22    id, err := processResponse(resp.Body)
23    return id, err
24}

다른 HTTP 서비스로 HTTP 호출을 만드는 예시이다. client.Do(req) 를 사용하여 요청을 전송하고, 서버로부터의 응답을 수신한다. 미들웨어를 통해 컨텍스트를 전달하는 경우와 같이 WithContext를 사용하여 외부로 나가는 요청에 컨텍스트를 설정할 수 있다.


Cancel

여러 개의 고루틴이 동시에 실행되고 있을 때, 하나의 서비스가 유효한 결과를 반환하지 않는다면 다른 고루틴을 처리할 필요가 없다. 이런 상황에서 컨텍스트를 사용하여 작업을 취소할 수 있다.

취소 가능한 컨텍스트를 생성하려면 WithCancel 함수를 사용한다.

1ctx, cancel := context.WithCancel(context.Background())

파라미터로 context.Context를 받고 context.Context와 context.CancelFunc를 반환한다.

  • context.Context : 함수로 전달된 컨텍스트와 동일하지 않는 컨텍스트를 반환받는다.
  • context.CancelFunc : 컨텍스트를 취소하여 잠재적으로 취소를 대기하는 모든 코드에 중지할 시점을 알려준다.
 1func slowServer() *httptest.Server {
 2    s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 3        time.Sleep(2 * time.Second)
 4        w.Write([]byte("Slow response"))
 5    }))
 6    return s
 7}
 8
 9func fastServer() *httptest.Server {
10    s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
11        if r.URL.Query().Get("error") == "true" {
12            w.Write([]byte("error"))
13            return
14        }
15        w.Write([]byte("ok"))
16    }))
17    return s
18}
 1var client = http.Client{}
 2
 3func callBoth(ctx context.Context, errVal string, slowURL string, fastURL string) {
 4    ctx, cancel := context.WithCancel(ctx)
 5    defer cancel()
 6    var wg sync.WaitGroup
 7    wg.Add(2)
 8    go func() {
 9        defer wg.Done()
10        err := callServer(ctx, "slow", slowURL)
11        if err != nil {
12            cancel()
13        }
14    }()
15    go func() {
16        defer wg.Done()
17        err := callServer(ctx, "fast", fastURL+"?error="+errVal)
18        if err != nil {
19            cancel()
20        }
21    }()
22    wg.Wait()
23    fmt.Println("done with both")
24}
25
26func callServer(ctx context.Context, label string, url string) error {
27    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
28    if err != nil {
29        fmt.Println(label, "request err:", err)
30        return err
31    }
32    resp, err := client.Do(req)
33    if err != nil {
34        fmt.Println(label, "response err:", err)
35        return err
36    }
37    data, err := ioutil.ReadAll(resp.Body)
38    if err != nil {
39        fmt.Println(label, "read err:", err)
40        return err
41    }
42    result := string(data)
43    if result != "" {
44        fmt.Println(label, "result:", result)
45    }
46    if result == "error" {
47        fmt.Println("cancelling from", label)
48        return errors.New("error happened")
49    }
50    return nil
51}