지난 글에서 이어서 이번 글에서 클라이언트에서의 인터셉터 구현을 살펴보겠습니다.
gRPC 인터셉터 – 클라이언트
클라이언트에서의 gRPC 인터셉터도 이전 글에서 살펴본 서버와 동작 원리 및 구현 방법이 비슷합니다. 마찬가지로 라이브러리의 구조를 살펴보면서 gRPC 인터셉터를 구현해보도록 하겠습니다.
(1) ClientCall.Listener<RespT> 정적 추상 클래스를 상속받는 클래스를 생성합니다.
ClientCall.Listener<RespT>는 ClientCall 추상 클래스 안에 선언된 정적 추상 이너 클래스로, ServerCall.Listener와 유사합니다. 라이브러리에 선언된 ClientCall.Listener<RespT> 추상 클래스는 다음과 같으며, 모두 4개의 메서드가 정의되어 있습니다. onHeaders 메서드는 모든 RPC에 대해 서버로부터 응답 메시지가 처음 수신할 때 한 번만 호출되며, onMessage 메서드는 Stream RPC인 경우에도 서버로부터 응답 메시지를 수신받을 때마다 호출됩니다.
public abstract class ClientCall<ReqT, RespT> {
public abstract static class Listener<T> {
public void onHeaders(Metadata headers) {}
public void onMessage(T message) {}
public void onClose(Status status, Metadata trailers) {}
public void onReady() {}
}
...
}
ClicentCall.Listener<RespT>는 이전에 살펴본 ServerCall.Listener<ReqT>와 같이 추상 클래스이므로 모든 메서드에 대한 오버라이딩이 필요합니다. 마찬가지로, 라이브러리에서는 delegate 패턴을 이용하여 다음과 같은 상속 관계를 가지는 클래스들을 제공합니다.
- (abstract) ClientCall.Listener<RespT>
- (abstract) PartialForwardingClientCallListener<RespT>
- (abstract) ForwardingClientCallListener<RespT>
- (abstract) ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT>
따라서 ClientCall.Listener<RespT> 추상 클래스 대신 ForwardingClientCallListener<RespT> 추상 클래스를 상속받아 구현하면 됩니다. 여기서는 ForwardingClientCallListener 클래스의 이너 클래스로 구현되어 있는 SimpleForwardingClientCallListener을 이용하여 간단하게 구현합니다.
이제 ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT> 추상 클래스를 상속받는 UserClientCallListener을 다음과 같이 구현합니다. onHeaders 메서드에 대해서 오버라이딩하고, delegate 패턴에 대한 구현도 추가합니다. 서버로부터 응답 메시지가 처음 도달하면 표준 출력이 콘솔에 나타나도록 구현했습니다.
// UserClientCallListener.java
package com.lattechiffon.grpc;
import io.grpc.ClientCall;
import io.grpc.ForwardingClientCallListener;
import io.grpc.Metadata;
public class UserClientCallListener<RespT> extends ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT> {
protected UserClientCallListener(ClientCall.Listener<RespT> delegate) {
super(delegate);
}
@Override
public void onHeaders(Metadata headers) {
System.out.println("[Client Call Listener] client header : " + headers);
super.onHeaders(headers);
}
}
(2) ClientCall<ReqT, RespT> 추상 클래스를 상속받는 클래스를 생성합니다.
ClientCall<ReqT, RespT> 추상 클래스에는 gRPC 클라이언트에서의 메시지 송수신을 처리하는 메서드들이 정의되어 있습니다. 과정 (1)에서 살펴본 ClientCall.Listener<RespT> 클래스도 ClientCall<ReqT, RespT> 추상 클래스의 이너 클래스입니다. ClientCall 클래스에 선언된 sendMessage 메서드를 이용하여 서버로 요청 메시지를 전송할 수 있으며, 이 메서드를 오버라이딩하여 요청 메시지를 서버로 전달하기 전에 수행해야 할 사전 작업을 정의할 수 있습니다.
package io.grpc;
import javax.annotation.Nullable;
public abstract class ClientCall<ReqT, RespT> {
public abstract static class Listener<T> { ... }
public abstract void start(Listener<RespT> responseListener, Metadata headers);
public abstract void request(int numMessages);
public abstract void cancel(@Nullable String message, @Nullable Throwable cause);
public abstract void halfClose();
public abstract void sendMessage(ReqT message);
public boolean isReady() {
return true;
}
...
}
ClientCall<ReqT, RespT>는 Listener와 마찬가지로 추상 클래스이므로, 모든 메서드에 대한 오버라이딩이 필요합니다. 라이브러리에서는 delegate 패턴을 이용하여 다음과 같은 상속 관계를 가지는 클래스들을 제공합니다.
- (abstract) ClientCall<ReqT, RespT>
- (abstract) PartialForwardingClientCall<ReqT, RespT>
- (abstract) ForwardingClientCall<ReqT, RespT>
- (abstract) ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>
따라서 ClientCall<ReqT, RespT> 추상 클래스 대신 ForwardingClientCall<ReqT, RespT> 추상 클래스를 상속받아 구현하면 됩니다. 여기서는 ForwardingClientCall 클래스의 이너 클래스로 구현되어 있는 SimpleForwardingClientCall을 이용하여 간단하게 구현합니다.
이제 ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT> 추상 클래스를 상속받는 UserClientCall을 다음과 같이 정의합니다. start와 sendMessage 메서드에 대해서 오버라이딩하고, delegate 패턴에 대한 구현도 추가합니다. 새로운 요청 메시지를 서버로 전달하기 전에 표준 출력이 콘솔에 나타나도록 구현했습니다.
// UserClientCall.java
package com.lattechiffon.grpc;
import io.grpc.ClientCall;
import io.grpc.ForwardingClientCall;
import io.grpc.Metadata;
public class UserClientCall<ReqT, RespT> extends ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT> {
protected UserClientCall(ClientCall<ReqT, RespT> delegate) {
super(delegate);
}
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
super.start(new UserClientCallListener<>(responseListener), headers);
}
@Override
public void sendMessage(ReqT message) {
System.out.println("[Client Call] Client Send Message : " + message);
super.sendMessage(message);
}
}
(3) ClientInterceptor 인터페이스의 구현 클래스를 생성합니다.
마지막으로 ClientInterceptor 인터페이스를 살펴보겠습니다. ServerInterceptor 인터페이스와 마찬가지로 인터페이스의 구조는 단순합니다. 구현 클래스는 interceptCall이라는 하나의 메서드만 구현하면 됩니다. 세 개의 파라미터와 하나의 반환은 다음의 역할을 가집니다. 서버와는 그 구성이 다소 다릅니다.
- MethodDescriptor<ReqT, RespT> method:
- CallOptions callOptions:
- Channel next:
- ClientCall<ReqT, RespT> (return):
package io.grpc;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
public interface ClientInterceptor {
<ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next);
}
// UserClientInterceptor.java
package com.lattechiffon.grpc;
import io.grpc.*;
public class UserClientInterceptor implements ClientInterceptor {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
return new UserClientCall<>(next.newCall(method, callOptions));
}
}
(4) gRPC 클라이언트 채널에 구현한 Interceptor 클래스를 인터셉터로 등록합니다.
마지막으로 구현한 인터셉터가 정상적으로 동작하도록 다음과 같이 gRPC 클라이언트에 등록해줍니다. 이제 서버로부터 응답이 도달하면 인터셉터에 의해 앞서 구현한 일련의 과정들이 먼저 수행됩니다.
// GrpcApplication.java
...
public static void main(String[] args) {
...
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080).usePlaintext()
.intercept(new UserClientInterceptor()).build();
...
}
지금까지 서버와 클라이언트에서의 gRPC 인터셉터 동작 원리와 Java를 이용한 구현 과정에 대해 살펴보았습니다. 다음 글에서는 gRPC에서 제공하는 데드라인 기능에 대해 살펴보고 마찬가지로 Java로 구현하는 내용을 다루도록 하겠습니다.
- (참고) JDK 9부터는 JAVA EE 모듈이 제거되었기 때문에 gRPC 컴파일 코드를 포함한 프로젝트 전체를 빌드하기 위해서는 다음과 같이 javax.annotation 의존성을 추가해야 합니다.
// build.gradle
dependencies {
...
implementation 'javax.annotation:javax.annotation-api:1.3.2'
...
}