Go의 비밀스러운 삶: Context 패키지

발행: (2026년 1월 20일 오후 01:08 GMT+9)
11 min read
원문: Dev.to

Source: Dev.to

번역을 진행하려면 번역이 필요한 실제 텍스트를 제공해 주시겠어요?
텍스트를 알려주시면 원본 형식과 마크다운을 그대로 유지하면서 한국어로 번역해 드리겠습니다.

런어웨이 고루틴을 중지하고 메모리 누수를 방지하는 방법

런어웨이 고루틴을 중지하고 메모리 누수를 방지하는 방법.

Ethan: “메모리 누수가 있어요.”
Eleanor: “그렇나요?” (not looking up)
Ethan: “음, 정확히 말하면 누수가 아니라요. 하지만 제 대시보드를 보세요.” (points to a monitor showing a jagged, climbing line) “이게 제 API입니다. 사용자가 제품을 검색할 때마다 데이터베이스와 추천 엔진을 조회하기 위해 고루틴을 생성합니다. 사용자가 지루해져서 브라우저를 닫아도 서버는 계속 작업을 수행합니다.”
Eleanor: “그것을 멈추라고 알려주지 않았기 때문이에요.” (walks over)
Ethan: “HTTP 핸들러가 그걸 처리한다고 생각했는데요?”
Eleanor: “핸들러는 연결을 처리할 뿐이에요. 고루틴은 독립적으로 동작합니다. 당신은 고루틴을 시작하고 떠났죠. 그 고루틴들은 여전히 실행 중이며, 이미 건물을 떠난 사용자를 위해 CPU 사이클을 소모하고 있습니다. 서버에 무례한 행동이에요.”
Ethan: “그럼 어떻게 사용자에게 떠났다고 알려줄 수 있나요?”
Eleanor:Context를 전달하면 됩니다.”

The First Argument

Eleanor pulled up a chair.

“In Go, context.Context is the standard way to carry deadlines, cancellation signals, and request‑scoped values across API boundaries. It is almost always the first argument of a function.”

그녀는 Ethan의 코드를 열었다.

// Bad: No way to stop this function once it starts
func SlowDatabaseQuery(id string) string {
    time.Sleep(5 * time.Second) // Simulate work
    return "Product Details for " + id
}

func HandleSearch(w http.ResponseWriter, r *http.Request) {
    // We ignore the request context!
    result := SlowDatabaseQuery("12345")
    fmt.Fprintln(w, result)
}

“If I hit this endpoint and cancel the request after one second, your SlowDatabaseQuery still sleeps for the full five seconds. Multiply that by a thousand users, and your server crashes.”

그녀는 코드를 리팩터링했다.

// Good: We accept a Context
func SlowDatabaseQuery(ctx context.Context, id string) (string, error) {
    // Use a select statement to listen for cancellation
    select {
    // …
    }
}

Look at the select block. We are racing two things: the work finishing or the context finishing. ctx.Done() is a channel that closes when the context is canceled. If the user disconnects, ctx.Done() fires immediately, and your function returns. You save four seconds of work.

The Timeout (Setting Boundaries)

“그게 취소를 처리해,” Ethan이 말했다. “하지만 데이터베이스가 고장 나서 영원히 멈춰버리면 어떡하지? 사용자는 기다리고, 연결은 열려 있어…”

“그럼 마감 시간을 설정하면 돼,” Eleanor가 답했다. “프로세스가 영원히 실행되지 않게 해야 해. 우리는 context.WithTimeout을 사용해.”

그녀는 새로운 예제를 만들었다.

func CallExternalAPI() error {
    // 1. Create a derived context that dies after 100 ms
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    // 2. ALWAYS defer the cancel function to release resources
    defer cancel()

    // 3. Pass this strict context to the worker
    req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://slow-api.com", nil)
    client := &http.Client{}

    _, err := client.Do(req)
    return err
}

“이건 과감하네.”
“외부 API가 그보다 느리면 우리는 답을 원하지 않아,” Eleanor가 단호히 말했다. “이게 시스템을 보호해. 그들의 서버가 멈추면, 우리 서버는 수천 개의 대기 연결이 쌓이지 않아. 빠르게 실패하고 넘어가는 거야.”

defer cancel()은요?”
“핵심이야. 작업이 10 ms 안에 끝나도 타이머는 백그라운드에서 계속 돌고 있어. cancel()을 호출하면 타이머가 멈추고 메모리가 즉시 해제돼. 항상 defer cancel()을 사용해.”

값 (주의해서 사용하세요)

“문서에서 context.WithValue를 봤어요. 그걸 사용해서 사용자 객체와 설정을 함수들에 전달할 수 있나요?”

엘리너는 찡그렸다.

가능하지만 보통은 하지 않는 것이 좋습니다. WithValue는 요청‑스코프 데이터—예를 들어 요청 ID나 인증 토큰—에 사용됩니다. 타입이 없고 보이지 않기 때문이죠. 필수 함수 인자를 전달하기 위해 사용하면 코드가 불투명해집니다.”

“불투명해진다구요?”

“함수가 데이터베이스 연결이 필요하면 인자로 전달하세요: func(db *DB). 아무도 볼 수 없는 ctx 안에 숨겨두지 마세요. 명시적인 것이 암시적인 것보다 낫습니다.”

명령 체인

Ethan은 대시보드를 바라보았다. 메모리 사용량이 평탄해지고 있었다.

“전파되네,” 그가 깨달았다. “HandleSearch에서 부모 컨텍스트를 취소하면 SlowDatabaseQuery의 자식 컨텍스트가 취소되고, 그 결과 데이터베이스에 대한 HTTP 요청이 취소돼…”

“바로 그거야,” Eleanor가 말했다. “이건 명령 체인이다. 최상위 레벨이 ‘멈춰’라고 하면 그 명령이 트리 전체로 내려간다. 각 함수는 자신의 정리를 하고 반환한다.”

그녀는 일어서서 펀치 카드 더미를 집어 들고 말했다:

“서버는 쓰레기통이 아니야, Ethan. 버려진 프로세스로 채우지 마. 예의를 갖춰. 사용자가 떠나면 작업을 멈춰.”

Key Concepts from Chapter 16

ConceptWhat It Does
context.Context마감 시간, 취소 신호, 그리고 요청‑범위 값을 전달합니다.
First argument작업을 수행하는 함수는 첫 번째 매개변수로 Context를 받아야 합니다.
select on ctx.Done()컨텍스트가 취소될 때 작업을 조기에 중단할 수 있게 합니다.
context.WithTimeout / WithDeadline작업이 실행될 수 있는 최대 시간을 강제로 제한합니다.
defer cancel()작업이 끝나는 즉시 타이머와 goroutine 누수 등을 해제합니다.
context.WithValue메타데이터 용도로만 제한적으로 사용하고, 필수 인자로 사용하지 마세요.
Propagation부모 컨텍스트를 취소하면 파생된 모든 자식 컨텍스트가 자동으로 취소됩니다.

Context를 일관되게 전달하고 존중함으로써, 실행되지 않는 goroutine을 방지하고 메모리 누수를 피하며 서버를 반응성 있게 유지할 수 있습니다.

Go 컨텍스트 개요

컨텍스트는 데드라인, 취소 신호, 그리고 요청 범위 값을 전달합니다. 불변이며 스레드‑안전합니다.

ctx.Done()

  • 컨텍스트가 취소되거나 타임아웃될 때 닫히는 채널입니다.
  • 작업이 더 이상 필요 없을 때 조기에 반환하기 위해 select 문에서 사용합니다.

context.Background()

  • 최상위(root) 컨텍스트입니다.
  • 메인 함수나 최상위 프로세스를 시작하고, 상속받을 기존 컨텍스트가 없을 때 사용합니다.

context.WithCancel(parent)

ctx, cancel := context.WithCancel(parent)
  • 부모 컨텍스트의 복사본을 반환하며, 새로운 Done 채널을 가집니다.
  • 반환된 cancel() 함수를 호출하면 해당 채널이 닫힙니다.

context.WithTimeout(parent, duration)

ctx, cancel := context.WithTimeout(parent, duration)
  • 지정된 기간이 지나면 자동으로 취소되는 부모 컨텍스트의 복사본을 반환합니다.

베스트 프랙티스:
함수가 반환될 때 즉시 리소스를 해제하도록 항상 defer cancel()을 호출하세요. 타임아웃이 발생하지 않았더라도 마찬가지입니다.

context.TODO()

  • 자리표시자 컨텍스트입니다.
  • 리팩터링 중이며 아직 컨텍스트가 어디서 오는지 결정되지 않았을 때 사용합니다.

context.WithValue

ctx = context.WithValue(parent, key, value)
  • 요청 범위 데이터(예: Trace ID)를 전달할 때 사용합니다.
  • 옵션 매개변수나 핵심 의존성(예: 로거, 데이터베이스 핸들)에는 사용하지 마세요. 명시적인 인수가 더 명확합니다.

다음 장: JSON 및 태그. Ethan은 설정 파일을 파싱하려고 시도하면서 Go의 구조체 태그가 언어가 제공하는 마법에 가장 가까운 것임을 알게 됩니다.

Aaron Rose는 tech-reader.blog에서 소프트웨어 엔지니어이자 기술 작가이며, Think Like a Genius의 저자입니다.

Back to Blog

관련 글

더 보기 »

Go의 비밀스러운 삶: Concurrency

경쟁 조건(race condition)의 혼란에 질서를 부여한다. Chapter 15: Sharing by Communicating 아카이브는 그 화요일에 유난히 시끄러웠다. 목소리 때문이 아니라 t...