
gRPC-Web은 웹 클라이언트에서 gRPC 서비스를 직접 호출할 수 있게 해주는 JavaScript 기반 라이브러리입니다.
이를 통해 브라우저에서 바로 gRPC 요청이 가능해져 웹 개발에서 통신을 단순화하고, 서버 리소스를 효율적으로 사용할 수 있습니다.
┌─────────────┐ HTTP/1.1 ┌─────────────┐ HTTP/2 ┌─────────────┐
│ 브라우저 │ ──────────────────▶│ Proxy 서버 │ ───────────────▶│ gRPC 서버 │
│ (gRPC-Web) │ ◀──────────────── │ (Envoy) │ ◀────────────── │ │
└─────────────┘ └─────────────┘ └─────────────┘
syntax = "proto3";
package helloworld;
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
service Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
protoc -I=$DIR helloworld.proto \
--js_out=import_style=commonjs:$OUT_DIR \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:$OUT_DIR
var helloService = new proto.helloworld.GreeterServiceClient("http://localhost:8080");
var request = new proto.helloworld.HelloRequest();
request.setMessage(msg);
var metadata = {"custom-header-1": "value1"};
helloService.SayHello(request, metadata, function(err, response) {
if (err) {
console.log(err.code);
console.log(err.message);
} else {
console.log(response.getMessage());
}
});
protoc --java_out=generated src/main/proto/helloworld.proto
public class HelloWorldServer {
public static void main(String[] args) throws Exception {
Server server = ServerBuilder.forPort(8080)
.addService(new GreeterImpl())
.build();
server.start();
server.awaitTermination();
}
static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
String name = request.getName();
HelloResponse response = HelloResponse.newBuilder()
.setMessage("Hello, " + name + "!")
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
}
docker-compose up -d node-server envoy commonjs-client
gRPC-Web 클라이언트가 보낸 HTTP/1.1 요청을 gRPC 서버가 이해할 수 있는 HTTP/2 gRPC 요청으로 변환합니다.
일반적인 Java gRPC 서버는 gRPC-Web 프로토콜을 직접 지원하지 않기 때문에, Envoy 프록시를 사용하거나 Armeria(JVM) 기반의 서버를 사용하는 것이 필요합니다.
Google에서 제공하는 REST API 자동 변환 도구를 활용합니다.
syntax = "proto3";
import "google/api/annotations.proto";
service MyService {
rpc GetItem(GetItemRequest) returns (GetItemResponse) {
option (google.api.http) = {
get: "/v1/items/{id}"
};
}
}
message GetItemRequest {
string id = 1;
}
message GetItemResponse {
string name = 1;
string description = 2;
}
// java gRPC 서버 생성
protoc --java_out=${output_path} \
--grpc-java_out=${output_path} \
--proto_path=${proto_file_path} \
your_service.proto
// gRPC Gateway 생성
protoc --go_out=${output_path} \
--go-grpc_out=${output_path} \
--grpc-gateway_out=${output_path} \
--proto_path=${proto_file_path} \
your_service.proto
@Configuration
public class GrpcClientConfig {
@Bean
public ItemServiceGrpc.ItemServiceBlockingStub itemServiceStub() {
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 9090)
.usePlaintext()
.build();
return ItemServiceGrpc.newBlockingStub(channel);
}
}
@RestController
@RequestMapping("/api/items")
public class ItemController {
private final ItemServiceGrpc.ItemServiceBlockingStub itemServiceStub;
public ItemController(ItemServiceGrpc.ItemServiceBlockingStub itemServiceStub) {
this.itemServiceStub = itemServiceStub;
}
@GetMapping("/{id}")
public ResponseEntity<?> getItem(@PathVariable String id) {
GetItemRequest request = GetItemRequest.newBuilder()
.setId(id)
.build();
try {
GetItemResponse response = itemServiceStub.getItem(request);
return ResponseEntity.ok(
Map.of("name", response.getName(),
"description", response.getDescription())
);
} catch (StatusRuntimeException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(e.getStatus().getDescription());
}
}
}
Armeria는 다음과 같은 장점을 제공합니다:
| 방법 | 장점 | 단점 |
|---|---|---|
| google.api.http | Google에서 공식 지원 자동화된 변환 | Java에서 사용하려면 gRPC Gateway 필요 설정 복잡 |
| Spring Gateway | 자바 개발자에게 친숙 높은 자유도 기존 Spring 생태계 활용 | 직접 구현 필요 추가 코드 작성 |
| Armeria | JVM 기반에서 gRPC 프로토콜을 자연스럽게 지원 HTTP/1.x, HTTP/2, gRPC, Thrift 등 다양한 프로토콜 동시 지원 | 새로운 프레임워크 학습 필요 |
// gRPC-Web 클라이언트 생성
const {HelloRequest, HelloResponse} = require('./helloworld_pb.js');
const {GreeterClient} = require('./helloworld_grpc_web_pb.js');
// 클라이언트 인스턴스 생성
const client = new GreeterClient('http://localhost:8080');
// 요청 메시지 생성
const request = new HelloRequest();
request.setName('Dobby');
// 메타데이터 설정 (선택사항)
const metadata = {'custom-header': 'value'};
// 서버에 요청 보내기
client.sayHello(request, metadata, (err, response) => {
if (err) {
console.error(err);
return;
}
console.log('Response:', response.getMessage());
});
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
public class HelloGrpcServer {
public static void main(String[] args) throws Exception {
// 서버 인스턴스 생성 및 포트 설정
Server server = ServerBuilder.forPort(9090)
.addService(new GreeterImpl())
.build();
// 서버 시작
server.start();
System.out.println("Server started on port 9090");
// 서버 종료 전까지 대기
server.awaitTermination();
}
// 서비스 구현
static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
// 요청 처리
String name = request.getName();
System.out.println("Received request from: " + name);
// 응답 생성
HelloResponse response = HelloResponse.newBuilder()
.setMessage("Hello, " + name + "!")
.build();
// 응답 전송
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
}
import com.linecorp.armeria.common.grpc.GrpcSerializationFormats;
import com.linecorp.armeria.server.Server;
import com.linecorp.armeria.server.docs.DocService;
import com.linecorp.armeria.server.grpc.GrpcService;
public class ArmeriaGrpcServer {
public static void main(String[] args) {
// gRPC 서비스 생성
GrpcService grpcService = GrpcService.builder()
.addService(new GreeterImpl())
.supportedSerializationFormats(GrpcSerializationFormats.values())
.enableUnframedRequests(true)
.build();
// Armeria 서버 생성
Server server = Server.builder()
.http(8080)
.service(grpcService)
.service("/docs", DocService.builder().build())
.build();
// 서버 시작
server.start().join();
System.out.println("Server started on port 8080");
// 서버 종료 전까지 대기
server.closeOnJvmShutdown();
}
// 서비스 구현 (위의 GreeterImpl과 동일)
}
service WeatherService {
rpc GetWeatherUpdates(LocationRequest) returns (stream WeatherUpdate);
}
@Override
public void getWeatherUpdates(LocationRequest request,
StreamObserver<WeatherUpdate> responseObserver) {
String location = request.getLocation();
// 여러 개의 응답을 스트리밍
for (int i = 0; i < 10; i++) {
WeatherUpdate update = WeatherUpdate.newBuilder()
.setLocation(location)
.setTemperature(20 + new Random().nextInt(10))
.setTimestamp(System.currentTimeMillis())
.build();
responseObserver.onNext(update);
try {
Thread.sleep(1000); // 1초마다 업데이트
} catch (InterruptedException e) {
e.printStackTrace();
}
}
responseObserver.onCompleted();
}
const stream = client.getWeatherUpdates(request);
stream.on('data', (response) => {
console.log('Update:', response.getTemperature());
});
stream.on('end', () => {
console.log('Stream ended');
});
stream.on('error', (err) => {
console.error('Error:', err);
});
Server server = ServerBuilder.forPort(9090)
.addService(new GreeterImpl())
.useTransportSecurity(certChainFile, privateKeyFile)
.build();
// 인증 인터셉터
public class AuthInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
String token = headers.get(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER));
if (token == null || !validateToken(token)) {
call.close(Status.UNAUTHENTICATED.withDescription("Invalid token"), new Metadata());
return new ServerCall.Listener<ReqT>() {};
}
return next.startCall(call, headers);
}
private boolean validateToken(String token) {
// JWT 토큰 검증 로직
return true; // 실제 구현에서는 토큰 검증
}
}
// 서버에 인터셉터 추가
Server server = ServerBuilder.forPort(9090)
.addService(new GreeterImpl())
.intercept(new AuthInterceptor())
.build();
@Override
public void getItem(GetItemRequest request, StreamObserver<GetItemResponse> responseObserver) {
String id = request.getId();
// 검증
if (id == null || id.isEmpty()) {
responseObserver.onError(
Status.INVALID_ARGUMENT
.withDescription("Item ID cannot be empty")
.asRuntimeException());
return;
}
// 아이템 조회
Item item = itemRepository.findById(id);
if (item == null) {
responseObserver.onError(
Status.NOT_FOUND
.withDescription("Item not found: " + id)
.asRuntimeException());
return;
}
// 정상 응답
GetItemResponse response = GetItemResponse.newBuilder()
.setName(item.getName())
.setDescription(item.getDescription())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
gRPC와 gRPC-Web은 현대적인 분산 시스템 개발에 강력한 도구입니다.
Protocol Buffers를 통한 명확한 API 계약, 효율적인 바이너리 직렬화, 그리고 HTTP/2의 장점을 활용할 수 있습니다.
웹 환경에서는 gRPC-Web을 통해 브라우저에서도 이러한 이점을 활용할 수 있으며, 기존 REST API와의 통합도 다양한 방법으로 가능합니다.
각 시스템의 특성과 팀의 익숙함에 따라 적절한 구현 방법을 선택하는 것이 중요합니다.
마이크로서비스 아키텍처, 실시간 애플리케이션, 멀티 플랫폼 환경에서 특히 gRPC의 장점이 두드러집니다.