Go context(컨텍스트) – 업무에 사용하는 Go 언어 20

Go context(컨텍스트)
Go context(컨텍스트)

Go 언어를 학습하면서 조금 익숙해졌다고 생각될 즈음, 갑자기 context라는 개념이 등장하면서 다시 머리를 쥐어짜게 되는 순간이 찾아옵니다. 특히 웹 서버나 API 클라이언트처럼 네트워크 작업이 관련된 실무에서는 go context가 필수 요소처럼 사용되곤 합니다.

이번 글에서는 go context의 기본 개념부터 실무에서 어떻게 활용되는지까지, 예제를 중심으로 아주 상세히 설명드리겠습니다.


Go Context란 무엇인가요?

go context는 Go의 context 패키지에서 제공되며 함수 간에 취소 신호, 마감 시간(Timeout), 키-값 데이터 등을 전달하기 위한 표준적인 방법입니다. 간단히 말하면, 함수 실행을 제어하거나 중단하기 위해 사용되는 구조입니다.

예를 들어 HTTP 서버에서 클라이언트가 요청을 보냈다가 갑자기 브라우저를 닫아버렸다면, 해당 요청에 관련된 처리는 더 이상 의미가 없습니다. 이때 context를 사용하면 해당 작업을 즉시 중단할 수 있습니다. 이는 CPU 자원을 아끼고, 서버의 안정성을 높이는 데 매우 중요합니다.


context의 네 가지 종류

Go에서 제공하는 기본 context 생성 방법은 아래 네 가지입니다:

  1. context.Background()
  2. context.TODO()
  3. context.WithCancel(parent)
  4. context.WithTimeout(parent, duration)

이들을 각각 하나씩 살펴보겠습니다.

1. context.Background()

이것은 빈 context를 생성할 때 사용합니다. 보통 최상위 context로 많이 쓰이며, 실무에서는 서버의 루트 context나 테스트의 기본 context로 많이 사용됩니다.

Go
ctx := context.Background()

이 context는 절대 취소되지 않으며, timeout도 없습니다. 그래서 다른 context를 만들기 위한 기반으로 사용됩니다. 따라서, 단독으로 사용되어서는 안 된다고 보시면 되겠습니다.

2. context.TODO()

이것은 이름 그대로 아직 어떤 context를 써야 할지 모르겠을 때 임시로 쓰는 context입니다. 초기 개발 중이라서 나중에 바꿔야 할 수도 있을 때 사용됩니다.

Go
ctx := context.TODO()

정식 서비스 코드에서는 가능하면 사용하지 않는 것이 좋습니다.

3. context.WithCancel()

WithCancel()수동으로 취소할 수 있는 context를 생성합니다. 이 context는 cancel() 함수를 호출할 때 하위 context로 전달됩니다.

Go
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 요청에 제한 시간을 두고 싶을 때 사용됩니다.

Go
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를 이용해 처리를 중단하는 예제입니다.

Go
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 같은 데이터를 함수 간에 전달할 수 있습니다.

Go
ctx := context.WithValue(context.Background(), "userID", 42)
userID := ctx.Value("userID")
fmt.Println("userID:", userID)

단, WithValue()는 남용해서는 안 됩니다. 문서에서도 가능한 한 로깅, 추적, 인증 정보 정도로만 사용하라고 명시하고 있습니다.


실무 경험 공유: context 누락으로 인한 문제

필자는 과거에 Go로 작성된 마이크로서비스에서 외부 API 호출 중 context를 설정하지 않고 요청을 보낸 적이 있었습니다. 외부 서비스가 응답을 주지 않자, 고루틴이 계속 살아 있는 상태로 유지되었고, 이로 인해 메모리 누수가 발생한 사례가 있습니다.


context 사용 시 주의사항

  1. cancel 호출은 꼭 defer로 처리하세요. 누락 시 리소스 누수가 발생할 수 있습니다.
  2. 함수 시그니처에서 context는 항상 첫 번째 인자로 넘깁니다. func(ctx context.Context, ...)
  3. 값 전달은 최소화하세요. 오직 인증, 트레이싱, 로깅 정보만 전달하는 것이 좋습니다.

go context는 Go 언어를 실무에서 안전하고 효율적으로 활용하기 위한 핵심 기능 중 하나입니다.
특히 네트워크나 I/O 작업을 수행할 때 취소와 시간 제한, 값 전달 기능은 필수적으로 고려해야 합니다.

초보자 입장에서는 처음에는 조금 어렵게 느껴질 수 있지만, 몇 가지 패턴과 예제를 이해하면 금세 익숙해질 수 있습니다.
실무에서는 다음과 같은 형태로 context를 거의 모든 코드에서 활용하게 됩니다:

  • HTTP 서버 핸들러에서 요청 context 받아 사용
  • 외부 API 호출 시 timeout을 설정
  • 취소가 가능한 작업을 고루틴으로 처리할 때 context 전달

go context는 단순한 개념이지만 그 영향력은 매우 큽니다. 이 글을 통해 context를 이해하고 실무에 자신 있게 적용하시기를 바랍니다.

이렇게 기본편은 모두 끝이 났습니다. 여기까지 달려오시느라 고생많으셨고요. 응용편에서 go 언어로 무엇을 할 수 있는지 살펴보겠습니다.