Many Developers Donโt Understand the Use of Context in Golang. Hereโs What You Need to Know.
์ ๋ง๋ก Go์์์ context๋ฅผ ์ ๋๋ก ์ดํดํ๊ณ ๊ณ์ ๊ฐ์?
์ด ๊ธ์์๋ context์ ํต์ฌ ๊ฐ๋ ์ ์ค์ ์์ ์ ํจ๊ป ๋ช ํํ ์ ๋ฆฌํฉ๋๋ค.
ํนํ ์์ฒญ ID ๋ฏธ๋ค์จ์ด(Request ID Middleware)๋ฅผ ํ์ฉํ ํธ๋ ์ด์ฑ ์์ ๋ฅผ ํตํด, context๊ฐ ์ค์ ์๋น์ค์์ ์ด๋ป๊ฒ ํ์ฉ๋ ์ ์๋์ง๋ฅผ ๊ตฌ์ฒด์ ์ผ๋ก ๋ณด์ฌ๋๋ฆฝ๋๋ค.
Go์ context ํจํค์ง๋ API์ ๊ณ ๋ฃจํด ์ ๋ฐ์ ๊ฑธ์ณ ์ทจ์, ๋ฐ๋๋ผ์ธ, ์์ฒญ ๋จ์์ ๋ฐ์ดํฐ ์ ๋ฌ์ ๊ด๋ฆฌํ ์ ์๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ ๋๋ค.
ํ์ง๋ง ๋ง์ ๊ฐ๋ฐ์๋ค์ด ๊ทธ ์ ํํ ์ฌ์ฉ๋ฒ์ด๋, ํนํ ์์ฒญ ํธ๋ ์ด์ฑ๊ณผ ๊ฒฐํฉํ์ ๋์ ์ง์ ํ ์ฅ์ ์ ์ ์ดํดํ์ง ๋ชปํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.
์ด ๊ธ์์๋ ๋ค์ ๋ด์ฉ์ ์ค์ฌ์ผ๋ก context.Context์ ๋ณธ์ง์ ํํค์ณ๋ด ๋๋ค:
Go์์ context.Context๋ ์์ฒญ์ด๋ ์์ ์ ์์ ์ฃผ๊ธฐ ์ ๋ฐ์ ๊ฑธ์ณ ๋ฐ๋๋ผ์ธ, ์ทจ์ ์ ํธ, ๋ฉํ๋ฐ์ดํฐ(ํค-๊ฐ ์)๋ฅผ ์ ๋ฌํ๊ธฐ ์ํ ๊ตฌ์กฐ์ฒด์ ๋๋ค.
์ด ๊ตฌ์กฐ๋ฅผ ํตํด ๋ค์๊ณผ ๊ฐ์ ์์ ์ด ๊ฐ๋ฅํด์ง๋๋ค:
context ์์ด ์ด์ ๊ฐ์ ๊ธฐ๋ฅ์ ์ง์ ๊ตฌํํ๋ ค๋ฉด
์ทจ์ ๋ก์ง์ด ์ค๋ณต๋๊ณ ์ฝ๋๊ฐ ๋ณต์กํด์ง๋ฉฐ, ๋ฆฌ์์ค ๋ญ๋น๋ ์ฝ๊ฒ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
context๋ ์ด๋ฅผ ๊น๋ํ๊ณ ์์ ํ๊ฒ ์ฒ๋ฆฌํ ์ ์๋๋ก ๋๋ Go์ ํต์ฌ ๋ฉ์ปค๋์ฆ์ ๋๋ค.
Request ID๋ ๊ฐ ์์ฒญ์ ๊ณ ์ ํ๊ฒ ๋ถ์ฌ๋๋ ์๋ณ์์ ๋๋ค. ์ด๋ ๋ถ์ฐ ํธ๋ ์ด์ฑ(distributed tracing)์ ํต์ฌ ์์๋ก, ๋ค์๊ณผ ๊ฐ์ ์ญํ ์ ์ํํฉ๋๋ค:
๊ฐ์ฅ ์ข์ ๋ฐฉ๋ฒ์, Request ID๋ฅผ ๋ฏธ๋ค์จ์ด์์ ์์ฑํ๊ฑฐ๋ ์์๋ฐ์ context์ ์ ์ฅํ๋ ๊ฒ์ ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ํด๋น ์์ฒญ์ ์ฒ๋ฆฌํ๋ ์ ์์ญ์์ Request ID๋ฅผ ์์ฝ๊ฒ ์ฌ์ฉํ ์ ์๊ฒ ๋ฉ๋๋ค.
๋ค์์ Request ID ๋ฏธ๋ค์จ์ด๊ฐ ์ ์ฉ๋ ์ค์ API ์์ ์ ๋๋ค:
// Request ID๋ฅผ context์ ์ ์ฅํ๊ธฐ ์ํ ํค ์ ์
type key string
const requestIDKey key = "requestID"
func CustomMiddleware() mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = uuid.New().String() // ์์ผ๋ฉด ์๋ก ์์ฑ
}
ctx := context.WithValue(r.Context(), requestIDKey, reqID)
w.Header().Set("X-Request-ID", reqID) // ์๋ต์๋ ํค๋ ์ธํ
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
๐ก Request ID๋
์ง์ ์์ฑํ๊ฑฐ๋ ์์ ์์ฒญ์์ ๋ฐ์ ๊ฐ์ ๊ทธ๋๋ก ์ฌ์ฉ
์ถ์ ์ ์ํด ํด๋ผ์ด์ธํธ โ ์๋ฒ โ ํ์ ์๋น์ค๊น์ง ์ด์ด์ ธ์ผ ํจ๊ณผ์ ์ ๋๋ค.
func main() {
r := mux.NewRouter()
r.Use(CustomMiddleware())
r.HandleFunc("/request", handlerRequest).Methods("GET")
log.Println("Server running at http://localhost:8081")
log.Fatal(http.ListenAndServe(":8081", r))
}
func handlerRequest(w http.ResponseWriter, r *http.Request) {
reqID, ok := r.Context().Value(requestIDKey).(string)
if !ok {
http.Error(w, "Request ID not found in context", http.StatusInternalServerError)
return
}
err := externalAPIHandler(r.Context())
if err != nil {
log.Printf("request id: %v error message: %v", reqID, err.Error())
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(requestIDResponse{Message: "internal server error"})
return
}
json.NewEncoder(w).Encode(requestIDResponse{Message: "success"})
}
๐ง context์์ Request ID๋ฅผ ๊บผ๋ด ๋ก๊น ์ ํ์ฉํ๊ณ , ์ธ๋ถ API ํธ์ถ์ ๊ฐ์ context๋ฅผ ๋๊น๋๋ค.
func externalAPIHandler(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8082/external/user", nil)
if err != nil {
return err
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, err = io.ReadAll(resp.Body)
return err
}
| ํญ๋ชฉ | ์ค๋ช |
|---|---|
| โ Request ID ์์ฑ | UUID ๊ธฐ๋ฐ ์์ฑ or ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ์ ๋ฌ |
| โ Context ์ ์ฅ | context.WithValue๋ก Request ID๋ฅผ ์ ํ |
| โ Context ํ์ฉ | ์ธ๋ถ API ํธ์ถ ์ ํ์์์ ๋ฐ ์ทจ์ ์ ํธ ์ ๋ฌ |
| โ ์ถ์ ํธ์์ฑ | ๋ก๊ทธ๋ง๋ค Request ID๋ฅผ ๋จ๊ฒจ ๋๋ฒ๊น ์ฉ์ด |
์ด ์๋น์ค๋ ํฌํธ 8082์์ ๋์ํ๋ฉฐ, /external/user ๊ฒฝ๋ก๋ก ๋ค์ด์ค๋ ์์ฒญ์ ๋ํด 30์ด๊ฐ ์ฒ๋ฆฌ ์ง์ฐ์ ์๋ฎฌ๋ ์ด์ ํฉ๋๋ค.
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/gorilla/mux"
)
type jsonResponse struct {
Message string `json:"message"`
}
func handlerUser(w http.ResponseWriter, r *http.Request) {
time.Sleep(30 * time.Second) // 30์ด๊ฐ ์์
์ง์ฐ ์๋ฎฌ๋ ์ด์
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(jsonResponse{
Message: "Process completed successfully",
})
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/external/user", handlerUser)
fmt.Println("Server running on http://localhost:8082")
http.ListenAndServe(":8082", r)
}

๐ย 30์ด ๋๊ธฐํ๋ ์ธ๋ถ API์ 5์ด ํ์์์ ์ค์ ์ ์กฐํฉ
context.WithTimeout()์ด ๋ฆฌ์์ค ๋ญ๋น๋ฅผ ๋ง๊ณ , ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์
curl -ik http://localhost:8081/request
HTTP/1.1 500 Internal Server Error
X-Request-ID: 6a1b1a54-6e7c-4ae0-8a2c-a7e376a73f28
Content-Type: application/json
{
"message": "internal server error"
}
๐ ๋ก๊ทธ ์ถ๋ ฅ ์์:
request id: 6a1b1a54-6e7c-4ae0-8a2c-a7e376a73f28 error message: context deadline exceeded
โ ์ด์ฒ๋ผ Request ID๊ฐ ์๋ค๋ฉด,
๋ถ์ฐ๋ ์ฌ๋ฌ ์๋น์ค์ ๋ก๊ทธ๋ฅผ ํ๋์ ํ๋ฆ์ผ๋ก ์ถ์ ํ ์ ์์ต๋๋ค.
curl -ik http://localhost:8081/request
# ์์ฒญ ํ 5์ด๊ฐ ๋๊ธฐ ์ ์ Ctrl + C๋ก ์ค๋จ
๐ ๋ก๊ทธ ์ถ๋ ฅ ์์:
request id: d3c9e401-b1aa-4410-8b65-b2eabffccf1e error message: context canceled
โ ๏ธ context deadline exceeded ์ context canceled์ ์๋ฏธ๊ฐ ๋ค๋ฆ ๋๋ค. ์ ์๋ ์์คํ ์ด ํ์์์๋ ๊ฒฝ์ฐ, ํ์๋ ํด๋ผ์ด์ธํธ๊ฐ ์ทจ์ํ ๊ฒฝ์ฐ ๋ฐ๋ผ์ ์๋น์ค์์๋ ์ด ์ฐจ์ด๋ฅผ ๋ช ํํ ๊ตฌ๋ถํ๊ณ ์ฒ๋ฆฌํ๋ ๋ก์ง์ด ํ์ํฉ๋๋ค.
ํ๋์ ๋ง์ดํฌ๋ก์๋น์ค ์์คํ ์์๋ ํ๋์ ์์ฒญ์ด ์ฌ๋ฌ ๊ณ์ธต๊ณผ ์๋น์ค๋ก ์ ํ๋ฉ๋๋ค.
์ด๋ฐ ๋ถ์ฐ ํ๊ฒฝ์์ Request ID๊ฐ ์๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค:
๐ก ๋ฏธ๋ค์จ์ด์์ Request ID๋ฅผ ์์ฑ/์ ํํ๊ณ ์ด๋ฅผ context.Context์ ์ ์ฅํจ์ผ๋ก์จ, ์๋น์ค ์ ๋ฐ์ ๊ฑธ์ณ ์ผ๊ด๋ ํธ๋ ์ด์ฑ ๊ธฐ๋ฐ์ ๋ง๋ จํ ์ ์์ต๋๋ค.
์ด ๊ธ์์ ๋ค๋ฃฌ ์์ ๋ฅผ ํตํด ๋ค์ ๋ด์ฉ์ ๋ฐฐ์ธ ์ ์์์ต๋๋ค:
์ด๋ฌํ ํจํด์ ์ตํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ด์ ์ ์ป๊ฒ ๋ฉ๋๋ค:
์ค์ ์๋น์ค ํ๊ฒฝ์์ context์ Request ID๋ฅผ ์ ๋๋ก ํ์ฉํ๋ ๊ฒ์
ํ๋ก๋์ ๊ธ Go ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋๋ ํต์ฌ ์ญ๋ ์ค ํ๋์ ๋๋ค.
์ ์ค์ํ๊ฐ?
context๋ ๋จ์ํ ํ์์์/์ทจ์ ์๊ทธ๋๋ง ์ ๋ฌํ๋ ๋๊ตฌ๊ฐ ์๋๋๋ค.
์ง์ง ์ค์ํ ๊ฑด, ๊ณ ๋ฃจํด ๋๋ ๋ฆฌ์์ค๋ฅผ ์ ๊ทน์ ์ผ๋ก ์ ๋ฆฌ(cleanup) ํ๋ ๋ก์ง์ ์ถ๊ฐํ๋ ๊ฒ์ ๋๋ค.
select {
case <-ctx.Done():
log.Println("context canceled:", ctx.Err())
// ์: ์ฐ๊ฒฐ ์ข
๋ฃ, ์ฑ๋ ๋ซ๊ธฐ, ์บ์ ๋กค๋ฐฑ ๋ฑ ๋ฆฌ์์ค ์ ๋ฆฌ
return ctx.Err()
case result := <-someProcess:
return result
}
๐ฏย ์ ๋ฆฌ ๋ก์ง์ด ์๋ค๋ฉด context๋ ๋ฌด์๋ฏธํฉ๋๋ค.
์ ์ค์ํ๊ฐ?
ํธ๋ค๋ฌ ๋ด๋ถ์์ ๋งค๋ฒ ctx.Value()๋ก Request ID๋ฅผ ๊บผ๋ด๋ ๊ฑด ๋ฒ๊ฑฐ๋กญ๊ณ ์ค์ํ๊ธฐ ์ฝ์ต๋๋ค.
๋ฐ๋ผ์ ๋ก๊ทธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ฐ์ธ์ context์์ ์๋์ผ๋ก Request ID๋ฅผ ๊บผ๋ด ๋ก๊น ์ ํฌํจ
func LogWithContext(ctx context.Context, msg string) {
reqID, _ := ctx.Value(requestIDKey).(string)
log.Printf("[reqID:%s] %s", reqID, msg)
}
๐งฉ ํ์์ ๊ณตํต Logger ์ ํธ๋ก ๋ง๋ค์ด ๋๋ฉด ๋งค์ฐ ํธ๋ฆฌํฉ๋๋ค.
์ ์ค์ํ๊ฐ?
๋จ์ผ ์๋น์ค ๋ด์์๋ง Request ID๋ฅผ ์ฐ๋ ๊ฑด ๋ฐ์ชฝ์ง๋ฆฌ์ ๋๋ค.
์ธ๋ถ API ํธ์ถ ์์๋ ๋ฐ๋์ X-Request-ID๋ฅผ ํค๋๋ก ์ถ๊ฐํด ์ฃผ๋ ๊ฒ์ด ๋ถ์ฐ ํธ๋ ์ด์ฑ์ ํต์ฌ์ ๋๋ค.
req.Header.Set("X-Request-ID", reqID)
๊ทธ๋ฆฌ๊ณ ํธ์ถ๋ฐ๋ ์ธ๋ถ ์๋น์ค๋ ๊ทธ ๊ฐ์ context์ ์ ์ฅํด์ ๋ค์ DB, ๋ก๊ทธ, ๋ค๋ฅธ API์ ์ ํํด์ผ ํธ๋ ์ด์ฑ์ ์ฐ์์ฑ์ด ์ ์ง๋ฉ๋๋ค.