[Go] Context에 관한 고찰 - 1

2025. 7. 11. 14:14·Language/Go

~Go Context 시리즈~

[1] Context에 관한 고찰 - 1 : Context 란

[2] Context에 관한 고찰 - 2 : Context의 중요성과 고루틴 누수

[3] Context에 관한 고찰 - 3 : Graceful Shutdown의 미학


context란 무엇인가...!

Go 언어로 서버를 만들다보면 우리는 필연적으로 context라는 단어와 마주하게 된다.

나도 그랬듯이, 아마 많은 주니어 개발자들이 context를 단순히 타임 아웃이나 취소 신호를 전달하는 도구 정도로 이해하고 넘어가는 경우가 많다. 하지만 context의 본질은 그보다 훨씬 깊은 곳에 있으며, Go가 지향하는 동시성 철학의 핵심을 담고 있다.

이 시리즈에서는 context가 왜 그토록 중요한지, 그리고 실제 대규모 프로젝트에서 context 생명주기를 어떻게 관리해야 하고 있는지에 대해 심도 있게 고찰해보고자 한다.

또한 해당 시리즈는 많은 AI 에이전트와의 협업을 통해 작성 되었다. (개발자로서 작문의 한계를 느꼈기 때문..ㅎ)


context 란

Go의 context 패키지의 공식 문서에서는 아래와 같이설명하고 있다.

"Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes."
"Context는 API의 경계를 넘나드는 요청 범위의 값, 취소 신호, 그리고 마감일을 담고 있다."

 

이 문장은 context의 기능을 정확히 요약하지만, 그 이면에 숨겨진 설계 철학을 파악하기는 어렵다.

 

Go에서는 수많은 함수 호출과 고루틴(goroutine)에 걸쳐 진행되는 하나의 논리적인 '작업'이 존재한다.

예를 들어, 사용자로부터 들어온 HTTP Request 하나를 처리하는 과정은 handler -> middleware -> service -> repository -> DB driver 와 같이 여러 컴포넌트를 거친다. 이 모든 과정은 '하나의 작업'이라는 생명주기를 공유한다.

Context는 바로 이 생명주기 동안 필요한 제어 신호 (ex. 작업을 언제까지 끝내야 하는가? 작업이 중간에 취소되었는가?) 와 데이터를 전파하는 Go에서의 표준적인 방법이다. 따라서 나는 Context를 보다 구체적으로 아래와 같이 정의하고 싶다.

 

"작업의 생명 주기를 제어하기 위한 표준화된 계약(Standarized Contract)"

 

 

Context는 불변(immutable) 객체다. context 객체가 생성된 후에는 그 내용을 직접 수정할 수 없기 때문이다.

context.WithCancel, context.WithTimeout 같은 함수들은 기존 context를 수정하는 것이 아니라, 새로운 속성이 추가된 자식 context 를 생성하여 반환한다. 이 부모-자식 관계의 트리 구조는 context가 강력한 제어 흐름을 만들어내는 핵심원리다. 부모 context가 취소되면, 그로부터 파생된 모든 자식 context들 역시 연쇄적으로 취소된다. 이 단방향 취소 전파 모델은 복잡한 동시성 환경에서 작업의 흐름을 예측 가능하고 안정적으로 만들어준다.

 

모든 context의 시조는 context.Background() 인데, 이는 어떠한 값도, 데드라인도 없는 비어있는 context로, 보통 App의 최상위 레벨에서 메인 context를 생성할 때 사용된다. context.TODO() 역시 기능적으로는 동일하지만, '아직 어떤 context를 사용해야할지 정하지 못했으니 나중에 수정해야한다' 라는 기술 부채를 명시하는 용도로 사용된다.

 

Go 1.20 Update: 왜 취소되었는가?

Go 1.20 이전의 context는 작업이 취소되었다는 사실(Fact)만을 알려줄 뿐, 그 이유(Reason)는 알려주지 않았다. ctx.Err()의 결과는 context.Canceled 또는 context.DeadlineExceeded 뿐이었다. 이로 인해 여러 하위 작업이 얽힌 복잡한 동시성 패턴에서 특정 작업이 왜 취소되었는지 근본 원인을 추적하기 어려웠다.

 

이러한 한계를 극복하기 위해 go 1.20 에서 CancelCause 관련 기능이 도입되었다. 핵심 아이디어는 cancel() 함수를 호출할 때, 취소의 원인이 되는 error 객체를 함께 전달하는 것이다. 이를 위해 func(cause error) 시그니처를 갖는 CancelCauseFunc 타입과, 이를 반환하는 context.WithCancelCause 함수가 추가되었다.

+) go 1.21 버전부터는 기존 WithCancel, WithDeadline, WithTimeout과 거의 동일하지만, CancelFunc 대신 CancelCauseFunc를 반환하는 WithCancelCause, WithDeadlineCause, WithTimeoutCause 함수가 추가되었다.

// 취소의 원인을 명시적으로 전달
ctx, cancel := context.WithCancelCause(parentCtx)
cancel(errors.New("permission denied"))

// WithTimeoutCause (Go 1.21부터)
// 타임아웃 발생 시, 기본 원인 에러를 context.DeadlineExceeded가 아닌
// 우리가 지정한 에러로 설정할 수 있게 해준다.
ctx, cancel := context.WithTimeoutCause(parentCtx, 5*time.Second, errors.New("custom timeout error"))
defer cancel(nil) // 정상 종료 시에는 nil 전달

 

그리고 context.Cause(ctx)라는 새로운 함수를 통해 취소된 Context에서 그 원인이 된 에러를 직접 추출할 수 있게 되었다. 만약 원인이 된 에러가 permission denied 였다면, context.Cause(ctx)는 context.Canceled가 아닌 해당 에러를 그대로 반환한다. 이를 통해 "작업이 단순히 취소됨"이 아니라, "작업이 permission denied 문제로 인해 취소됨" 이라는 훨씬 구체적이고 유용한 에러를 전파하고 로깅할 수 있게 되었고, 정교한 에러 처리가 가능해졌다. 복잡한 분산 시스템과 동시성 환경에서 문제의 근본 원인을 추적하고 디버깅하는 능력을 크게 향상시켜주는 강력한 도구가 생긴 것이다.

 

사용 예시:

import (
    "context"
    "errors"
    "fmt"
    "golang.org/x/sync/errgroup"
    "time"
)

func RunMultipleTasksWithCause(parentCtx context.Context) error {
    g, gCtx := errgroup.WithContext(parentCtx)

    g.Go(func() error {
        // TaskA는 1초 뒤 구체적인 에러를 반환한다.
        time.Sleep(1 * time.Second)
        return errors.New("permission denied")
    })

    g.Go(func() error {
        // TaskB는 10초가 걸리는 긴 작업이다.
        select {
        case <-time.After(10 * time.Second):
            fmt.Println("TaskB completed successfully")
            return nil
        case <-gCtx.Done():
            // gCtx가 취소되었을 때, 그 원인을 확인한다.
            cause := context.Cause(gCtx)
            fmt.Printf("TaskB was canceled because: %v\n", cause)
            return cause // 원인 에러를 그대로 반환한다.
        }
    })

    // g.Wait()는 TaskA가 반환한 "permission denied" 에러를 최종 반환한다.
    return g.Wait()
}

func main() {
    err := RunMultipleTasksWithCause(context.Background())
    if err != nil {
        // 최종적으로 잡히는 에러는 최초 원인이 된 에러이다.
        fmt.Printf("The root cause of failure was: %v\n", err)
    }
}

 

실행 결과:

TaskB was canceled because: permission denied
The root cause of failure was: permission denied

이후 내용은 길어져서 2편으로 나누었다.

'Language > Go' 카테고리의 다른 글

[Go] Context에 관한 고찰 - 2 : Context의 중요성과 메모리 누수  (0) 2025.07.24
[Go] WaitGroup vs ErrGroup 비교  (0) 2025.07.23
[Go] field 이름을 기준으로 field 초기화하기 with reflect  (0) 2024.11.08
[Go] Decimal decoder : Decimal을 MongoDB에 바로 String으로 저장  (1) 2024.06.10
[Go] encoding/json 패키지 - json.Decoder, Unmarshal & io.ReadAll  (0) 2023.10.31
'Language/Go' 카테고리의 다른 글
  • [Go] Context에 관한 고찰 - 2 : Context의 중요성과 메모리 누수
  • [Go] WaitGroup vs ErrGroup 비교
  • [Go] field 이름을 기준으로 field 초기화하기 with reflect
  • [Go] Decimal decoder : Decimal을 MongoDB에 바로 String으로 저장
빵빵0
빵빵0
(아직은) 공부하고 정리하는 블로그입니다.
  • 빵빵0
    Hack Your World
    빵빵0
  • 전체
    오늘
    어제
    • 분류 전체보기 (92)
      • Error Handling (7)
      • Project (5)
        • MEV (2)
      • Architecture (0)
        • API (0)
        • Cache (0)
        • 사소한 고민거리 (0)
      • Computer Science (4)
        • Data Structure (2)
        • Database (1)
        • Cloud (0)
        • OS (0)
        • Infra, Network (1)
        • AI (0)
      • Language (8)
        • Go (8)
        • Rust (0)
        • Python (0)
        • Java (0)
      • Algorithm (40)
        • BaekJoon (18)
        • Programmers (7)
        • LeetCode (6)
        • NeetCode (9)
      • SW Books (9)
        • gRPC Up & Running (1)
        • System Design Interview (2)
        • 스프링 입문을 위한 자바 객체지향의 원리와 이해 (6)
        • 블록체인 해설서 (0)
        • 후니의 쉽게 쓴 CISCO 네트워킹 (0)
      • BlockChain (4)
        • Issues (0)
        • Research (4)
        • Tech (0)
      • Own (8)
        • TIR(Today I Read) (3)
        • Personal (2)
        • Novel (0)
        • Memo (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    LeetCode
    Python
    KBW
    백준
    Programmers
    큐
    프로그래머스
    blockchain
    MongoDB
    BaekJoon
    goroutine
    2024
    ethereum
    블록체인
    golang
    MEV
    context
    Palindrome
    스택
    DP
    BFS
    Hash Table
    BEAKJOON
    Greedy
    EVM
    two pointer
    NeetCode
    candlechart
    chart
    go
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
빵빵0
[Go] Context에 관한 고찰 - 1
상단으로

티스토리툴바