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

