Spring Boot로 gRPC server client 구현해보자

Karim·2025년 7월 29일
3

SpringBoot

목록 보기
16/17

1. Version

💬

  • jdk : 21
  • Spring boot : 3.5.4
  • grpc-spring-boot-starter: 3.1.0.RELEASE
  • grpc 관련 : 1.64.0

2. build.gradle

📓 build.gredle dependencies

    // gRPC 서버 및 클라이언트 스타터
    implementation 'net.devh:grpc-server-spring-boot-starter:3.1.0.RELEASE'
    implementation 'net.devh:grpc-client-spring-boot-starter:3.1.0.RELEASE'
    
    // gRPC와 Protobuf
    implementation 'io.grpc:grpc-netty-shaded:1.64.0'
    implementation 'io.grpc:grpc-protobuf:1.64.0'
    implementation 'io.grpc:grpc-stub:1.64.0'
    
    // .proto build 시 사용
    implementation 'javax.annotation:javax.annotation-api:1.3.2'

📓 build.gredle protobuf 설정 및 classpath 설정

// Protobuf 설정
protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.25.3" // protoc 컴파일러 최신 버전
    }
    plugins {
        grpc {
            artifact = "io.grpc:protoc-gen-grpc-java:1.64.0" // gRPC 플러그인 최신 버전
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}

// 생성된 코드가 classpath에 포함되도록 설정
sourceSets {
    main {
        java {
            srcDirs 'build/generated/source/proto/main/grpc', 'build/generated/source/proto/main/java'
        }
    }
}

3. proto 파일

✒️ HelloService.proto

\src\main\proto

syntax = "proto3";

// Java 코드 생성 시 각 메시지 및 서비스에 대해 별도의 .java 파일을 생성하도록 합니다. (true: 각자 파일, false: 단일 파일 안에 중첩)
option java_multiple_files = true;
// 생성될 Java 클래스들이 위치할 패키지명을 지정합니다.
option java_package = "com.example.grpc.service";
// 생성될 Java 외부 클래스(Outer Class)의 이름을 지정합니다. (java_multiple_files가 false일 때 주로 사용)
option java_outer_classname = "HelloServiceProto";

// Protocol Buffers 내부에서 사용될 패키지명을 정의
package com.example.grpc;

// Greeting 기능을 제공하는 서비스 'HelloService'를 정의합니다.
service HelloService {
  // 단방향(Unary) RPC 메서드를 정의합니다.
  // 클라이언트가 'HelloRequest' 메시지를 보내면, 서버는 'HelloResponse' 메시지로 응답합니다.
   rpc requestMessage (HelloRequest) returns (HelloResponse);
}

// 클라이언트가 서버로 보낼 요청 메시지 'HelloRequest'의 구조를 정의합니다.
message HelloRequest {
  string jsonString = 1;
}

// 서버가 클라이언트에 보낼 응답 메시지 'HelloResponse'의 구조를 정의합니다.
message HelloResponse {
  string jsonString = 1;
}

4. grpc spring server

grpc client에 응답 받은 데이터를 로그로 적재 후 "요청 받은 메세지 : requestMessge" 로 응답해주는 코드

✒️ application.yml

server:
  port: 8081

# src/main/resources/application.yml
grpc:
  server:
    port: 9090 # gRPC 통신용 서버 포트

✒️ HelloServiceImpl

@Slf4j
@GrpcService
public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {

    @Override
    public void requestMessage(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {

        String requestMessage = request.getJsonString();

        log.info("클라이언트로부터 요청 수신: {}", requestMessage);

        // 응답 메시지 생성
        HelloResponse response = HelloResponse.newBuilder()
                .setJsonString("요청 받은 메세지 : " + requestMessage )
                .build();

        // 응답 전송
        // 생성된 응답 메시지를 클라이언트에게 전송합니다.
        // 'onNext()' 메서드는 응답 스트림에 하나의 메시지를 발행합니다.
        responseObserver.onNext(response);
        // 클라이언트에게 모든 응답 메시지 전송이 완료되었음을 알립니다.
        // 이 메서드를 호출하면 RPC 호출이 종료됩니다.
        responseObserver.onCompleted();
    }
}

5. grpc spring client

rest로 받은 데이터를 grpc 서버에 메세지를 보내 응답을 받는 코드

✒️ application.yml

grpc:
  client:
    local-grpc-server:
      address: 'static://localhost:9090'
      enableKeepAlive: true
      negotiationType: plaintext # 개발 환경에서만 사용, 실제 운영에서는 TLS 권장

✒️ GrpcClientService

@Slf4j
@Service
public class GrpcClientService {

    /**
     * 스텁은 클라이언트 측에서 원격 서버의 서비스를 로컬 객체처럼 호출할 수 있게 해주는 프록시(Proxy) 객체
     * 클라이언트 코드와 실제 서버 간의 중간 다리 역할
     */
    // gRPC 서버의 Stub을 주입. blockingStub은 동기 호출을 지원합니다.
    @GrpcClient("local-grpc-server")
    private HelloServiceGrpc.HelloServiceBlockingStub helloServiceStub;
    @GrpcClient("local-grpc-server")
    private HelloServiceGrpc.HelloServiceStub helloServiceAsyncStub;

    public String sendMessage(String requestMessage) {
        log.info("서버로 'requestMessage' 요청을 보냅니다.");

        HelloRequest request = HelloRequest.newBuilder()
                .setJsonString(requestMessage)
                .build();

        HelloResponse response = helloServiceStub.requestMessage(request);
        log.info("서버로부터 응답 수신: {}", response.getJsonString());

        return response.getJsonString();
    }
    
     public CompletableFuture<String> sendMessageAsync(String requestMessage) {
       
        log.info("[비동기 호출] 서버로 'requestMessage' 비동기 요청을 보냅니다. (요청 스레드: {})", Thread.currentThread().getName());

        CompletableFuture<String> future = new CompletableFuture<>();

        HelloRequest request = HelloRequest.newBuilder()
                .setJsonString(requestMessage)
                .build();

        helloServiceAsyncStub.requestMessage(request, new StreamObserver<HelloResponse>() {

            @Override
            public void onNext(HelloResponse response) {
                log.info("[비동기 콜백] 서버로부터 비동기 응답 수신: {} (콜백 스레드: {})", response.getJsonString(), Thread.currentThread().getName());
                future.complete(response.getJsonString());
            }

            @Override
            public void onError(Throwable t) {
                log.error("[비동기 콜백] 비동기 호출 중 오류 발생: {} (콜백 스레드: {})", t.getMessage(), Thread.currentThread().getName(), t);
                future.completeExceptionally(t);
            }

            @Override
            public void onCompleted() {
                log.info("[비동기 콜백] 비동기 호출 완료. (콜백 스레드: {})", Thread.currentThread().getName());
            }
        });

        log.info("[비동기 호출] sendMessageAsync 메서드 종료. CompletableFuture 반환. (요청 스레드: {})", Thread.currentThread().getName());

        return future;
    }
}

✒️ HelloController

@RestController
@RequiredArgsConstructor
public class HelloController {

    private final GrpcClientService grpcClientService;

    @GetMapping("/hello")
    public String requestMessage(@RequestParam(name = "message" ) String message) {
        return grpcClientService.sendMessage(message);
    }
    
    @GetMapping("/helloAsync")
    public CompletableFuture<String> requestMessageAsync(@RequestParam(name = "message" ) String message) {

        return grpcClientService.sendMessageAsync(message);
    }
}

6. runining log

💬

  • postmen

  • client log

2025-07-29T13:06:25.685+09:00  INFO 31092 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2025-07-29T13:06:25.685+09:00  INFO 31092 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2025-07-29T13:06:25.686+09:00  INFO 31092 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2025-07-29T13:06:25.735+09:00  INFO 31092 --- [nio-8080-exec-2] c.d.g.grpc.GrpcClientService             : 서버로 'requestMessage' 요청을 보냅니다.
2025-07-29T13:06:26.275+09:00  INFO 31092 --- [nio-8080-exec-2] c.d.g.grpc.GrpcClientService             : 서버로부터 응답 수신: 요청 받은 메세지 : karim
  • server log
2025-07-29T13:07:45.421+09:00  INFO 19324 --- [ault-executor-1] c.d.g.grpc.HelloServiceImpl              : 클라이언트로부터 요청 수신: karim

📌


📚 참고

profile
나도 보기 위해 정리해 놓은 벨로그

0개의 댓글