Go 마이크로 서비스에서 context.Context의 힘
Min-jun Kim
Dev Intern · Leapcell

최신 마이크로 서비스 아키텍처에서 단일 사용자 요청은 종종 여러 서비스에 걸쳐 호출 체인을 트리거합니다. 이 호출 체인의 라이프사이클을 효과적으로 제어하고, 공통 데이터를 전달하고, 적절한 시점에 "정상적으로" 종료하는 것은 시스템의 견고성, 응답성 및 리소스 효율성을 보장하는 데 핵심입니다. Go의 context.Context
패키지는 이러한 문제점을 해결하기 위해 특별히 설계된 표준 솔루션입니다.
이 기사에서는 context.Context
의 핵심 설계 원칙을 체계적으로 설명하고 마이크로 서비스 시나리오에 적용할 수 있는 모범 사례를 제공합니다.
마이크로 서비스에 Context가 필요한 이유: 문제의 근원
일반적인 전자 상거래 주문 시나리오를 상상해 보세요:
- API 게이트웨이는 사용자의 HTTP 주문 요청을 수신합니다.
- 게이트웨이는 Order Service를 호출하여 주문을 생성합니다.
- Order Service는 User Service를 호출하여 사용자의 ID와 잔액을 확인해야 합니다.
- Order Service는 또한 Inventory Service를 호출하여 제품 재고를 확보해야 합니다.
- 마지막으로 Order Service는 Reward Service를 호출하여 사용자 계정에 포인트를 추가할 수 있습니다.
이 과정에서 몇 가지 까다로운 문제가 발생합니다.
- Timeout Control: Inventory Service가 느린 데이터베이스 쿼리 때문에 멈추면 전체 주문 요청이 무기한으로 대기하는 것을 원하지 않습니다. 전체 요청에는 전체 제한 시간이 있어야 합니다(예: 5초).
- Request Cancellation: 사용자가 중간에 브라우저를 닫으면 API 게이트웨이는 클라이언트 연결 해제 신호를 수신합니다. 모든 다운스트림 서비스(Order, User, Inventory)에 "업스트림이 더 이상 기다리지 않는다"는 것을 알려서 데이터베이스 연결, CPU, 메모리 등의 리소스를 즉시 해제해야 할까요?
- Data Passing (Request-scoped Data): TraceID(분산 추적용), 사용자 ID 정보 또는 카나리아 릴리스 태그와 같이 이 요청과 강력하게 연결된 데이터를 호출 체인의 모든 서비스에 안전하고 비침해적으로 전달할 수 있는 방법은 무엇일까요?
context.Context
는 Go의 공식 솔루션입니다. 정보 제어 및 전달을 위해 요청 호출 체인 전체에서 "지휘관" 역할을 합니다.
context.Context의 핵심 개념
핵심적으로 Context는 네 가지 메서드를 정의하는 인터페이스입니다.
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any }
- Deadline(): 이 Context가 취소될 시간을 반환합니다. 만약 데드라인이 설정되지 않았다면,
ok
는false
가 될 것입니다. - Done(): 시스템의 핵심입니다. 채널을 반환합니다. 이 Context가 취소되거나 타임아웃되면 이 채널이 닫힙니다. 이 채널을 듣고 있는 모든 다운스트림 Goroutine은 즉시 신호를 수신합니다.
- Err():
Done()
채널이 닫힌 후,Err()
은 Context가 취소된 이유를 설명하는 nil이 아닌 오류를 반환합니다. 만약 타임아웃되었다면,context.DeadlineExceeded
를 반환하고, 만약 적극적으로 취소되었다면,context.Canceled
를 반환합니다. - Value(): Context에 첨부된 키-값 데이터를 검색하는 데 사용됩니다.
context
패키지는 Context를 생성하고 파생시키기 위한 몇 가지 중요한 함수를 제공합니다.
- context.Background(): 일반적으로 모든 Context의 루트로
main
, 초기화 및 테스트 코드에서 사용됩니다. 절대로 취소되지 않고, 값이 없고, 마감일이 없습니다. - context.TODO(): 어떤 Context를 사용해야 할지 확실하지 않거나 함수가 나중에 Context를 수락하도록 업데이트될 때 사용합니다. 의미상 코드를 읽는 사람에게 "해야 할 일"을 알립니다.
- context.WithCancel(parent): 부모 Context를 기반으로 새롭고 적극적으로 취소 가능한 Context를 만듭니다. 새로운
ctx
와cancel
함수를 반환합니다.cancel()
을 호출하면 이ctx
와 파생된 모든 자식 Context가 취소됩니다. - context.WithTimeout(parent, duration): 부모 Context를 기반으로 타임아웃이 있는 Context를 만듭니다.
- context.WithDeadline(parent, time): 부모 Context를 기반으로 특정 마감일이 있는 Context를 만듭니다.
- context.WithValue(parent, key, value): 부모 Context를 기반으로 키-값 쌍을 전달하는 Context를 만듭니다.
핵심 설계 아이디어: Context Tree
Context는 중첩될 수 있습니다. WithCancel
, WithTimeout
, WithValue
등을 사용하면 Context 트리가 형성됩니다. 부모 Context의 취소 신호는 자동으로 모든 자식 Context로 전파됩니다. 이렇게 하면 호출 체인의 모든 업스트림 노드가 Context를 취소할 수 있으며 모든 다운스트림 노드가 알림을 받게 됩니다.
마이크로 서비스에서 Context에 대한 모범 사례
Context를 첫 번째 매개변수로 전달하고 이름을 ctx
로 지정합니다.
이것은 Go 커뮤니티의 철통 규칙입니다. ctx
를 첫 번째 매개변수로 배치하면 함수가 호출자에 의해 제어되고 취소 신호에 응답할 수 있음을 명확하게 나타냅니다.
// Good func (s *Server) GetOrder(ctx context.Context, orderID string) (*Order, error) // Bad func (s *Server) GetOrder(orderID string, timeout time.Duration) (*Order, error)
절대 nil
Context를 전달하지 마세요
어떤 Context를 사용해야 할지 확실하지 않더라도 nil
대신 context.Background()
또는 context.TODO()
를 사용해야 합니다. nil
을 전달하면 다운스트림 코드에서 직접 패닉이 발생합니다.
context.Value
는 요청 범위 메타데이터에만 사용하세요
context.Value
는 선택적 매개변수가 아닌 API 경계를 넘어 요청 관련 메타데이터를 전달하기 위한 것입니다.
권장 사용:
- TraceID, SpanID: 분산 추적
- 사용자 인증 토큰 또는 사용자 ID
- API 버전, 카나리아 릴리스 플래그
권장하지 않음:
- 선택적 함수 매개변수(함수 서명이 명확하지 않음; 대신 명시적으로 전달)
- 종속성 주입의 일부여야 하는 데이터베이스 핸들 또는 Logger 인스턴스와 같은 무거운 객체
키 충돌을 피하기 위해 가장 좋은 방법은 사용자 지정 미공개 유형을 키로 사용하는 것입니다.
// mypackage/trace.go package mypackage type traceIDKey struct{} // key is a private type func WithTraceID(ctx context.Context, traceID string) context.Context { return context.WithValue(ctx, traceIDKey{}, traceID) } func GetTraceID(ctx context.Context) (string, bool) { id, ok := ctx.Value(traceIDKey{}).(string) return id, ok }
Context는 불변입니다. 파생된 새 Context를 전달하세요
WithCancel
, WithValue
등과 같은 함수는 새로운 Context 인스턴스를 반환합니다. 다운스트림 함수를 호출할 때는 원본 Context가 아닌 이 새로운 Context를 전달해야 합니다.
func handleRequest(ctx context.Context, req *http.Request) { // 다운스트림 호출에 대한 짧은 제한 시간 설정 ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() // 다운스트림 서비스를 호출할 때 새 ctx 전달 callDownstreamService(ctx, ...) }
항상 취소 함수를 호출하세요
context.WithCancel
, WithTimeout
및 WithDeadline
은 모두 취소 함수를 반환합니다. 작업이 완료되거나 함수가 Context와 관련된 리소스를 해제하기 위해 반환될 때 cancel()
을 호출해야 합니다. defer
를 사용하는 것이 가장 안전한 방법입니다.
func operation(parentCtx context.Context) { ctx, cancel := context.WithTimeout(parentCtx, 50*time.Millisecond) defer cancel() // 함수 반환에 관계없이 cancel이 호출되도록 보장 // ... 작업 수행 }
cancel()
을 호출하지 않으면 부모 Context가 여전히 살아 있는 동안 자식 Context 리소스(내부 고루틴 및 타이머와 같은)가 해제되지 않아 메모리 누수가 발생할 수 있습니다.
장기 실행 작업에서 항상 ctx.Done()
을 수신하세요
잠재적으로 차단되거나 장기 실행 작업(데이터베이스 쿼리, RPC 호출, 루프 등)의 경우 select
문을 사용하여 ctx.Done()
과 비즈니스 채널을 모두 수신합니다.
func slowOperation(ctx context.Context) error { select { case <-ctx.Done(): // 업스트림에서 취소되었습니다. 빠르게 로그를 기록하고 정리하고 반환합니다. log.Println("Operation canceled:", ctx.Err()) return ctx.Err() // 취소 오류 전파 case <-time.After(5 * time.Second): // 장기 실행 작업 완료 시뮬레이션 log.Println("Operation completed") return nil } }
서비스 경계를 넘어 Context 전달
Context 객체 자체는 직렬화되어 네트워크를 통해 전송될 수 없습니다. 따라서 마이크로 서비스 간에 Context를 전달할 때 다음을 수행해야 합니다.
- 발신자 측의
ctx
에서 필요한 메타데이터를 추출합니다(예: TraceID, Deadline). - 이 메타데이터를 RPC 또는 HTTP 헤더에 패키징합니다.
- 수신자 측의 헤더에서 이 메타데이터를 파싱합니다.
- 이 메타데이터를 사용하여
context.Background()
를 부모로 사용하여 새로운 Context를 만듭니다.
주류 RPC 프레임워크(gRPC, rpcx 등) 및 게이트웨이(Istio 등)는 일반적으로 OpenTelemetry 또는 OpenTracing 표준을 통해 Context 전파를 이미 지원합니다.
gRPC 예제(프레임워크에서 자동으로 처리):
// Client ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // gRPC는 ctx의 마감일을 HTTP/2 헤더로 자동 인코딩합니다. r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) // Server func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { // gRPC 프레임워크는 헤더에서 마감일을 파싱하고 ctx를 생성했습니다. // 이 ctx를 직접 사용할 수 있습니다. // 클라이언트가 시간 초과되면 ctx.Done()이 여기에서 닫힙니다. select { case <-ctx.Done(): return nil, status.Errorf(codes.Canceled, "client canceled request") case <-time.After(2 * time.Second): // 장기 실행 작업 시뮬레이션 return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } }
요약
context.Context
는 Go 마이크로 서비스 개발에서 없어서는 안 될 도구입니다. 선택적 라이브러리가 아니라 강력하고 유지 관리 가능한 시스템을 구축하기 위한 핵심 패턴입니다.
다음 규칙을 명심하세요.
- 항상 Context 전달: 함수 서명의 표준 부분으로 만드세요.
- 취소를 정상적으로 처리: 장기 실행 작업에서는
ctx.Done()
을 수신하고 업스트림 취소 신호에 즉시 응답합니다. defer cancel()
을 현명하게 사용: 리소스가 누출되지 않도록 합니다.WithValue
를 신중하게 사용: 진정으로 요청 관련 메타데이터만 전달하고 개인 유형을 키로 사용합니다.- 표준을 수용: gRPC와 같은 프레임워크에서 기본 Context 지원을 활용하여 서비스 간 전파를 간소화합니다.
context.Context
를 마스터하면 Go 마이크로 서비스에서 라이프사이클 제어 및 정보 전파를 제어할 수 있으므로 보다 효율적이고 탄력적인 분산 시스템을 구축할 수 있습니다.
저희는 Go 프로젝트 호스팅을 위한 최고의 선택인 Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하세요.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불 — 요청 없음, 요금 없음.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하세요.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
간편한 확장성 및 고성능
- 높은 동시성을 쉽게 처리하기 위한 자동 확장.
- 운영 오버헤드가 전혀 없습니다. 구축에만 집중하세요.
설명서에서 자세히 알아보세요!
X에서 저희를 팔로우하세요: @LeapcellHQ