TT Lab
블로그 시작하기
배우기 러닝패스 코스

gRPC 서비스를 직접 세우고 운영한다

인터셉터: 모든 호출이 지나는 자리

TT Lab 에서 이어서 보기

한 줄 요약

인터셉터는 모든 호출이 지나는 한 자리입니다. 기록·인증·추적·재시도처럼 모든 메서드에 똑같이 필요한 일을 그 자리에 두면 빠뜨릴 수 없고, 여러 개를 겹치면 그 순서가 곧 의미가 됩니다.

왜 이게 필요했나

메서드가 스무 개인 서비스에서 인증 검사를 메서드마다 적으면, 스물한 번째 메서드를 더하는 사람이 그 줄을 빠뜨리는 순간 구멍이 생깁니다. 접근 기록과 요청 번호도 같습니다. 웹 프레임워크의 미들웨어가 이 문제를 풀었고, gRPC 에서는 그 자리를 인터셉터라고 부릅니다.

어떻게 동작하나

서버 인터셉터는 호출이 들어와 처리기를 고르는 순간에 불립니다. 파이썬 grpcio 의 ServerInterceptor 는 intercept_service(continuation, handler_call_details) 하나를 구현하고, handler_call_details 에서 메서드 이름과 메타데이터를 봅니다. continuation(handler_call_details) 를 부르면 안쪽(다음 인터셉터나 서비스)이 고른 처리기가 돌아옵니다.

여기에 첫 번째 함정이 있습니다. 인터셉터가 불리는 순간에는 처리기가 아직 돌지 않았으므로 결과를 모릅니다. 상태 코드를 기록하려면 돌아온 처리기의 본체를 감싼 새 처리기를 만들어 돌려주고, 본체가 끝난 뒤 context.code() 를 읽어야 합니다. grpcio 1.84.0 에서 성공한 호출의 context.code() 는 OK 가 아니라 None 입니다(실측). 스트리밍 처리기는 제너레이터라서 스트림이 끝난 뒤에야 결과가 정해집니다.

거절도 처리기로 합니다. 인증에 실패하면 continuation 이 준 처리기와 같은 종류(단항·서버 스트리밍)의 처리기를 돌려주고, 그 본체에서 context.abort(UNAUTHENTICATED, ...) 를 부릅니다. 종류가 다르면 스트리밍 호출에서 엉뚱한 오류가 납니다.

순서는 이렇습니다. grpc.server(interceptors=[A, B]) 에서 A 가 바깥입니다. 바깥 인터셉터는 안쪽이 돌려준 처리기를 감싸므로 안쪽의 거절까지 봅니다.

interceptors=[AccessLog, Auth]   요청 → AccessLog → Auth → 서비스   거절된 호출도 기록된다
interceptors=[Auth, AccessLog]   요청 → Auth ─ 거절 ─▶ (AccessLog 에 닿지 않음)

클라이언트 쪽도 같은 모양입니다. UnaryUnaryClientInterceptor 와 UnaryStreamClientInterceptor 를 구현하고 grpc.intercept_channel(채널, A, B) 로 감쌉니다. 역시 A 가 바깥입니다. 호출 정보(client_call_details)는 바꿀 수 없는 객체라 메타데이터를 더하려면 같은 여섯 칸을 가진 새 객체를 만들어 넘깁니다.

메타데이터의 규칙도 알아 둡니다. 키는 소문자이고, -bin 으로 끝나는 키는 바이너리 값입니다. grpc- 로 시작하는 키는 라이브러리가 쓰는 자리입니다. 토큰을 비교할 때는 hmac.compare_digest 를 씁니다. 보통 비교는 첫 글자가 다르면 곧바로 끝나서, 걸린 시간을 재면 토큰을 한 글자씩 맞춰 볼 수 있습니다.

메타데이터는 두 번 오갑니다. 요청과 함께 가는 머리(invocation metadata)와, 응답 쪽의 처음 머리(initial metadata)와 끝 트레일러(trailing metadata)입니다. 오류로 끝난 호출에도 트레일러는 실려 가므로, 요청 번호처럼 클라이언트가 실패한 뒤에도 알아야 하는 값은 트레일러에 둡니다. 서버 스트리밍 호출은 첫 메시지 전에 머리가 나가고 마지막에 트레일러가 나가므로, 스트림 도중에 정한 값을 머리에 실을 수는 없습니다.

재시도 인터셉터는 실패한 호출을 continuation 으로 다시 보냅니다. 이때 요청 번호를 붙이는 인터셉터가 재시도보다 바깥에 있어야 다시 보낼 때도 같은 번호가 갑니다. 그래야 서버가 같은 요청인 줄 알고 한 번만 처리할 수 있습니다. 채널 설정의 retryPolicy(rt-grpc-control-lab 에서 다룸)와 재시도 인터셉터를 함께 켜면 시도 횟수가 곱해지므로 한쪽만 씁니다.

현장에서 만나는 모습

보안 사고 조사에서 「공격 시도가 접근 기록에 없다」 는 말이 나오면 기록 인터셉터가 인증 안쪽에 있는지 봅니다. 결제 서비스에서 「같은 주문이 세 번 들어왔다」 면 재시도마다 요청 번호가 새로 붙었는지 봅니다. 쿠버네티스의 gRPC 프로브는 토큰을 보내지 않으므로 헬스 체크 메서드는 인증에서 빼 둡니다. 빼지 않으면 파드가 영영 Ready 가 되지 않습니다.

OpenTelemetry 의 gRPC 계측도 사실은 인터셉터입니다. 추적 문맥(traceparent)을 메타데이터에 넣고 꺼내며 호출마다 구간을 만듭니다. Go 에서 인터셉터 사슬이 어디서 조립되는지는 블로그 글 grpc-go 코드 지도 에 있습니다.

다음 실습에서 할 것

주어진 재고 서비스 둘레에 접근 기록과 인증·인가 인터셉터를 만들고, 순서를 바꿔 거절된 호출이 기록에 남는지 봅니다. 요청 번호를 기록과 트레일러에 싣고, 클라이언트 쪽에는 토큰과 요청 번호를 붙이는 인터셉터와 같은 번호로 다시 보내는 재시도 인터셉터를 만듭니다.