Go의 비밀스러운 삶: Context 패키지
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.Contextis 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
SlowDatabaseQuerystill 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
selectblock. 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
| Concept | What 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의 저자입니다.