
Go 언어를 학습하면서 조금 익숙해졌다고 생각될 즈음, 갑자기 context
라는 개념이 등장하면서 다시 머리를 쥐어짜게 되는 순간이 찾아옵니다. 특히 웹 서버나 API 클라이언트처럼 네트워크 작업이 관련된 실무에서는 go context
가 필수 요소처럼 사용되곤 합니다.
이번 글에서는 go context
의 기본 개념부터 실무에서 어떻게 활용되는지까지, 예제를 중심으로 아주 상세히 설명드리겠습니다.
Go Context란 무엇인가요?
go context
는 Go의 context 패키지에서 제공되며 함수 간에 취소 신호, 마감 시간(Timeout), 키-값 데이터 등을 전달하기 위한 표준적인 방법입니다. 간단히 말하면, 함수 실행을 제어하거나 중단하기 위해 사용되는 구조입니다.
예를 들어 HTTP 서버에서 클라이언트가 요청을 보냈다가 갑자기 브라우저를 닫아버렸다면, 해당 요청에 관련된 처리는 더 이상 의미가 없습니다. 이때 context를 사용하면 해당 작업을 즉시 중단할 수 있습니다. 이는 CPU 자원을 아끼고, 서버의 안정성을 높이는 데 매우 중요합니다.
context의 네 가지 종류
Go에서 제공하는 기본 context 생성 방법은 아래 네 가지입니다:
context.Background()
context.TODO()
context.WithCancel(parent)
context.WithTimeout(parent, duration)
이들을 각각 하나씩 살펴보겠습니다.
1. context.Background()
이것은 빈 context를 생성할 때 사용합니다. 보통 최상위 context로 많이 쓰이며, 실무에서는 서버의 루트 context나 테스트의 기본 context로 많이 사용됩니다.
ctx := context.Background()
이 context는 절대 취소되지 않으며, timeout도 없습니다. 그래서 다른 context를 만들기 위한 기반으로 사용됩니다. 따라서, 단독으로 사용되어서는 안 된다고 보시면 되겠습니다.
2. context.TODO()
이것은 이름 그대로 아직 어떤 context를 써야 할지 모르겠을 때 임시로 쓰는 context입니다. 초기 개발 중이라서 나중에 바꿔야 할 수도 있을 때 사용됩니다.
ctx := context.TODO()
정식 서비스 코드에서는 가능하면 사용하지 않는 것이 좋습니다.
3. context.WithCancel()
WithCancel()
은 수동으로 취소할 수 있는 context를 생성합니다. 이 context는 cancel()
함수를 호출할 때 하위 context로 전달됩니다.
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(2 * time.Second)
cancel() // 2초 후 context 취소
}()
select {
case <-ctx.Done():
fmt.Println("취소되었습니다:", ctx.Err())
}
4. context.WithTimeout()
Timeout은 일정 시간이 지나면 자동으로 context를 종료하도록 설정할 수 있습니다. 예를 들어 HTTP 요청에 제한 시간을 두고 싶을 때 사용됩니다.
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
select {
case <-time.After(5 * time.Second):
fmt.Println("5초 작업 완료")
case <-ctx.Done():
fmt.Println("Timeout 발생:", ctx.Err())
}
3초 후 ctx.Done()
이 호출되며, 이로 인해 타임아웃이 발생합니다.
실무 예제: HTTP 요청 취소
다음은 실제 HTTP 요청 처리 시 클라이언트가 요청을 취소했을 경우, 서버 측에서 context를 이용해 처리를 중단하는 예제입니다.
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
select {
case <-time.After(5 * time.Second):
fmt.Fprintln(w, "완료되었습니다")
case <-ctx.Done():
fmt.Println("클라이언트 요청 취소됨:", ctx.Err())
}
}
위 코드에서 클라이언트가 요청을 취소하거나 브라우저를 닫으면 ctx.Done()
이 실행되어 서버는 처리를 중단합니다.
필자는 실무에서 context가 없는 코드는 반드시 의심받아야 된다고 생각한다.
특히 Go로 작성된 서버에서는 모든 요청 핸들러가context
를 받아야 한다는 것이 기본 원칙처럼 생각한다.
기본적으로 프로그래밍에서 종료되지 않는 무언가는 조심히 다루어야 한다.
게임으로 따지면 발적화 현상이 이러한 것을 조심히 다루지 않아서 그렇다.
context에서 값 전달하기
context는 단순히 취소와 시간 제한 외에도 값을 전달하는 기능이 있습니다. 예를 들어 사용자 ID나 트랜잭션 ID 같은 데이터를 함수 간에 전달할 수 있습니다.
ctx := context.WithValue(context.Background(), "userID", 42)
userID := ctx.Value("userID")
fmt.Println("userID:", userID)
단, WithValue()
는 남용해서는 안 됩니다. 문서에서도 가능한 한 로깅, 추적, 인증 정보 정도로만 사용하라고 명시하고 있습니다.
실무 경험 공유: context 누락으로 인한 문제
필자는 과거에 Go로 작성된 마이크로서비스에서 외부 API 호출 중 context를 설정하지 않고 요청을 보낸 적이 있었습니다. 외부 서비스가 응답을 주지 않자, 고루틴이 계속 살아 있는 상태로 유지되었고, 이로 인해 메모리 누수가 발생한 사례가 있습니다.
context 사용 시 주의사항
- cancel 호출은 꼭 defer로 처리하세요. 누락 시 리소스 누수가 발생할 수 있습니다.
- 함수 시그니처에서 context는 항상 첫 번째 인자로 넘깁니다.
func(ctx context.Context, ...)
- 값 전달은 최소화하세요. 오직 인증, 트레이싱, 로깅 정보만 전달하는 것이 좋습니다.
go context
는 Go 언어를 실무에서 안전하고 효율적으로 활용하기 위한 핵심 기능 중 하나입니다.
특히 네트워크나 I/O 작업을 수행할 때 취소와 시간 제한, 값 전달 기능은 필수적으로 고려해야 합니다.
초보자 입장에서는 처음에는 조금 어렵게 느껴질 수 있지만, 몇 가지 패턴과 예제를 이해하면 금세 익숙해질 수 있습니다.
실무에서는 다음과 같은 형태로 context를 거의 모든 코드에서 활용하게 됩니다:
- HTTP 서버 핸들러에서 요청 context 받아 사용
- 외부 API 호출 시 timeout을 설정
- 취소가 가능한 작업을 고루틴으로 처리할 때 context 전달
go context
는 단순한 개념이지만 그 영향력은 매우 큽니다. 이 글을 통해 context를 이해하고 실무에 자신 있게 적용하시기를 바랍니다.
이렇게 기본편은 모두 끝이 났습니다. 여기까지 달려오시느라 고생많으셨고요. 응용편에서 go 언어로 무엇을 할 수 있는지 살펴보겠습니다.
다음 글: Go 파일 입출력 기본 사용법 (os, ioutil, bufio) – 업무에 사용하는 Go 언어 응용편 1 →