gRPC + Spring Boot 마스터 노트 시리즈 7편. Interceptor가 인증·로깅·메트릭 같은 횡단 관심사를 분리하는 메커니즘, ServerInterceptor·ClientInterceptor 양쪽 구현, 체인 순서와 우선순위, 글로벌·서비스별 인터셉터 등록, MDC tracing·Micrometer 메트릭 통합, 자주 쓰는 인터셉터 패턴까지.
이 글은 gRPC + Spring Boot 마스터 노트 시리즈의 일곱 번째 편입니다. 1~6편이 RPC 모드였다면, 이번엔 그 위에 횡단 관심사 — Interceptor.
인증·로깅·메트릭·재시도·tracing을 RPC 코드에서 분리. Spring AOP의 gRPC 버전. 잘 쓰면 깔끔, 잘못 쓰면 복잡.
처음 Interceptor가 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, ServerInterceptor vs ClientInterceptor 어느 쪽인지 헷갈립니다. 둘째, 체인 순서가 막연합니다.
해결법은 한 가지예요. "Interceptor = AOP, 서버·클라이언트 양쪽". 서버 = 들어오는 요청, 클라이언트 = 나가는 요청. 양쪽 모두 가능. 체인은 등록 순서 (가장 먼저 등록 = 가장 바깥).
ServerInterceptor 기본
public class LoggingServerInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next
) {
log.info("Incoming: {}", call.getMethodDescriptor().getFullMethodName());
return next.startCall(call, headers);
}
}
핵심 — interceptCall 오버라이드. 메서드 이름·헤더 등 가로채기.
등록 — Spring Boot
글로벌 (모든 서비스)
@Configuration
public class GrpcConfig {
@Bean
@GrpcGlobalServerInterceptor
public ServerInterceptor loggingInterceptor() {
return new LoggingServerInterceptor();
}
}
@GrpcGlobalServerInterceptor — 모든 @GrpcService에 자동 적용.
서비스별
@GrpcService(interceptors = {LoggingServerInterceptor.class})
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase { ... }
특정 서비스만.
ClientInterceptor
public class LoggingClientInterceptor implements ClientInterceptor {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method,
CallOptions callOptions,
Channel next
) {
log.info("Outgoing: {}", method.getFullMethodName());
return next.newCall(method, callOptions);
}
}
@Bean
@GrpcGlobalClientInterceptor
public ClientInterceptor clientLogging() {
return new LoggingClientInterceptor();
}
또는 명시적:
@GrpcClient(value = "user-service", interceptors = {LoggingClientInterceptor.class})
private UserServiceGrpc.UserServiceBlockingStub stub;
인증 인터셉터
public class AuthServerInterceptor implements ServerInterceptor {
private static final Metadata.Key<String> TOKEN =
Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
@Autowired
private AuthService authService;
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next
) {
String token = headers.get(TOKEN);
if (token == null || !authService.verify(token)) {
call.close(Status.UNAUTHENTICATED.withDescription("Invalid token"), new Metadata());
return new ServerCall.Listener<ReqT>() {};
}
// 인증 성공 → Context에 사용자 정보 저장
Context ctx = Context.current().withValue(USER_KEY, authService.getUser(token));
return Contexts.interceptCall(ctx, call, headers, next);
}
}
핵심 — Metadata에서 토큰 추출 → 검증 → 실패 시 call.close(), 성공 시 Context에 사용자 저장.
서비스에서 인증 정보 사용
public static final Context.Key<User> USER_KEY = Context.key("user");
@Override
public void getProfile(Empty req, StreamObserver<Profile> observer) {
User user = USER_KEY.get();
Profile profile = profileService.getByUser(user.getId());
observer.onNext(profile);
observer.onCompleted();
}
Context.Key로 인터셉터 ↔ 서비스 통신.
로깅 + 시간 측정
public class TimingServerInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next
) {
long start = System.currentTimeMillis();
String method = call.getMethodDescriptor().getFullMethodName();
ServerCall<ReqT, RespT> wrappedCall = new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {
@Override
public void close(Status status, Metadata trailers) {
long duration = System.currentTimeMillis() - start;
log.info("{} {} {}ms", method, status.getCode(), duration);
super.close(status, trailers);
}
};
return next.startCall(wrappedCall, headers);
}
}
ForwardingServerCall로 응답 close 시점 후크.
Micrometer 메트릭
implementation 'net.devh:grpc-server-spring-boot-starter:3.x'
implementation 'io.micrometer:micrometer-registry-prometheus'
grpc:
server:
metric-server:
enabled: true
자동 메트릭:
grpc.server.calls.durationgrpc.server.calls.received- 서비스별·메서드별·status별
Grafana 대시보드 자동.
MDC Tracing
public class MDCInterceptor implements ServerInterceptor {
private static final Metadata.Key<String> TRACE_ID =
Metadata.Key.of("trace-id", Metadata.ASCII_STRING_MARSHALLER);
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next
) {
String traceId = headers.get(TRACE_ID);
if (traceId == null) {
traceId = UUID.randomUUID().toString();
}
MDC.put("trace-id", traceId);
try {
return next.startCall(call, headers);
} finally {
MDC.clear();
}
}
}
여기서 정말 중요한 시험 함정 — MDC는 ThreadLocal. gRPC는 비동기 처리 시 다른 스레드 → MDC 전파 X. Reactor Context 또는 명시적 전파 필요.
클라이언트 — 인증 토큰 자동 첨부
public class AuthClientInterceptor implements ClientInterceptor {
private static final Metadata.Key<String> TOKEN =
Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
@Autowired
private TokenProvider tokenProvider;
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method,
CallOptions options,
Channel next
) {
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
next.newCall(method, options)
) {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
String token = tokenProvider.getToken();
headers.put(TOKEN, "Bearer " + token);
super.start(responseListener, headers);
}
};
}
}
모든 클라이언트 호출에 자동 토큰 첨부.
재시도 인터셉터
public class RetryClientInterceptor implements ClientInterceptor {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method,
CallOptions options,
Channel next
) {
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
next.newCall(method, options)
) {
// 재시도 로직
};
}
}
또는 gRPC 내장 retry policy 사용 권장:
grpc:
client:
user-service:
address: static://localhost:9090
method-config:
- service: UserService
method: GetUser
retryPolicy:
maxAttempts: 3
initialBackoff: 1s
maxBackoff: 10s
backoffMultiplier: 2
retryableStatusCodes:
- UNAVAILABLE
- DEADLINE_EXCEEDED
인터셉터 체인
여러 인터셉터 등록 시:
요청 들어옴
↓
[ServerInterceptor 1: 인증]
↓
[ServerInterceptor 2: 로깅]
↓
[ServerInterceptor 3: 시간 측정]
↓
[실제 서비스 메서드]
↑
[3] → [2] → [1] → 응답
여기서 시험 함정이 하나 있어요. 인터셉터 순서. 등록 순서대로. @Order 또는 List 명시.
@GrpcGlobalServerInterceptor
@Order(1)
public ServerInterceptor authInterceptor() { ... }
@GrpcGlobalServerInterceptor
@Order(2)
public ServerInterceptor loggingInterceptor() { ... }
낮은 순서 = 먼저 실행 (가장 바깥).
자주 쓰는 인터셉터 패턴
1. 인증 (Authorization 헤더 검증)
2. 인가 (RBAC·라우트별 권한)
3. 로깅 (요청·응답·시간)
4. Tracing (trace-id 전파, OpenTelemetry)
5. 메트릭 (Micrometer·Prometheus)
6. Rate Limit
7. 재시도 (클라이언트)
8. Circuit Breaker
9. MDC 컨텍스트
10. 입력 검증
대부분 라이브러리·내장 기능 활용 권장. 직접 구현은 특수 경우만.
OpenTelemetry 통합
implementation 'io.opentelemetry:opentelemetry-api'
implementation 'io.opentelemetry.instrumentation:opentelemetry-grpc-1.6'
@Bean
public ServerInterceptor otelInterceptor(OpenTelemetry otel) {
return GrpcTelemetry.create(otel).newServerInterceptor();
}
자동 trace 전파. 분산 시스템 가시성.
디버깅
@Bean
public ServerInterceptor debugInterceptor() {
return new ServerInterceptor() {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next
) {
log.debug("Method: {}", call.getMethodDescriptor().getFullMethodName());
log.debug("Headers: {}", headers);
return next.startCall(call, headers);
}
};
}
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 7편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- Interceptor = AOP, 서버·클라이언트 양쪽
ServerInterceptor— 들어오는 요청ClientInterceptor— 나가는 요청- 등록 —
@GrpcGlobalServerInterceptor(모든) /@GrpcService(interceptors = ...)(특정) - 인증 — Metadata에서 토큰 → 검증 →
call.close(UNAUTHENTICATED)또는Context저장 Context.Key= 인터셉터 ↔ 서비스 통신- 로깅·시간 —
ForwardingServerCall.SimpleForwardingServerCall후크 - Micrometer 메트릭 — 자동 (
grpc-server-spring-boot-starter) - duration·received·status 자동 분리
- MDC 함정 — ThreadLocal, 비동기 시 전파 X
- Reactor Context 또는 명시적 전파
- 클라이언트 —
ForwardingClientCall+ headers.put(TOKEN) - 재시도 — gRPC 내장 retry policy 권장 (yaml)
- retryableStatusCodes — UNAVAILABLE·DEADLINE_EXCEEDED
- 인터셉터 체인 순서 — 등록 순서·
@Order - 낮은 순서 = 먼저 실행 (바깥)
- 자주 쓰는 — 인증·인가·로깅·tracing·메트릭·rate limit·재시도·MDC
- OpenTelemetry —
GrpcTelemetry.newServerInterceptor - 자동 trace 전파
시리즈 다른 편
- 1편 — 기본 개념·HTTP/2·4 RPC 모드
- 2편 — Protocol Buffers
- 3편 — Unary RPC
- 4편 — Server Streaming
- 5편 — Client Streaming
- 6편 — Bidirectional Streaming
- 7편 — Interceptors (현재 글)
- 8편 — Error Handling
- 9편 — Security
- 10편 — 고급 (Reflection·Health·LB·gRPC-Web)
공식 문서: gRPC Interceptors 에서 더 깊이.
다음 글(8편)에서는 Error Handling — Status Code 16종, 에러 전파, 클라이언트 처리, ExceptionHandler까지 풀어 갑니다.