지난 글에서 이어서 이번 글에서 클라이언트에서의 인터셉터 구현을 살펴보겠습니다.
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>
- (abstract) ForwardingClientCallListener<RespT>
- (abstract) PartialForwardingClientCallListener<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>
- (abstract) ForwardingClientCall<ReqT, RespT>
- (abstract) PartialForwardingClientCall<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로 구현하는 내용을 다루도록 하겠습니다.
- GitHub Repository: https://github.com/lattechiffon/grpc/tree/6212495b8d9b738e76aa3e516ea974c43baa1425
- (참고) JDK 9부터는 JAVA EE 모듈이 제거되었기 때문에 gRPC 컴파일 코드를 포함한 프로젝트 전체를 빌드하기 위해서는 다음과 같이 javax.annotation 의존성을 추가해야 합니다.
// build.gradle dependencies { ... implementation 'javax.annotation:javax.annotation-api:1.3.2' ... }