gRPC 통신 구현해보기 (서버 : Java / 클라이언트 : Go)

2025. 1. 23. 21:40·gRPC

 

환경 구성

gRPC의 원리를 이해하고 실습하기 위해 아래와 같은 방식으로 진행하였습니다.

서비스 정의(IDL) 기반 통신 흐름

 

gRPC 클라이언트에 해당하는 Go 언어 기반의 클라이언트의 경우,

특별한 웹 프레임워크(ex. Gin)을 사용하지 않고 순수하게 Go 언어의 기본 라이브러리와 gRPC 패키지를 사용해 코드를 작성하였습니다.

Go 언어를 사용한 이유는 기본적으로 다양한 프로그래밍 언어를 지원하기 때문에 Java로만 gRPC 서버와 클라이언트를 구성하기 보다는 다른 언어로도 통신 과정을 구현해보고 싶었습니다. (사실은 Go 언어와 친해지고 싶었음)

 

 

그리고 Java 기반의 gRPC 서버는 Spring Boot와 함께 환경을 구축하였습니다.

예제로 보고있는 교재처럼 별도의 프레임워크 없이 순수 Gradle Project로 gRPC 서버를 구성하는 방식도 좋지만

추후에 진행하는 프로젝트에서 필요하다면 적용할 때 참고할 겸 그리고 확장성 측면에서 좀 더 개발에 적합하고 다른 스프링 에코 시스템과 쉽게 통합이 가능하기 때문에 이러한 방식으로 진행하였습니다.

 

 

참고로

Spring Boot 애플리케이션에서 gRPC 서버와 클라이언트를 쉽게 설정하고 관리할 수 있도록 Spring gRPC 라이브러리가 존재하는데,

Spring Boot 3.4 버전부터 Spring gRPC 라이브러리가 호환된다고 합니다.

저는 3.3.8 버전을 사용했습니다.

 

Getting Started :: Spring gRPC Reference

This section offers jumping off points for how to get started using Spring gRPC. There is a simple sample project in the samples directory (e.g. grpc-server). You can run it with mvn spring-boot:run or gradle bootRun. You will see the following code in tha

docs.spring.io

 

 

다음은 상품 등록과 상품 검색에 대해서 gRPC 클라이언트와 서버간의 통신 패턴을 보여주는 시각화 자료입니다.

서버는 addProduct(product)와 getProduct(productId)라는 두 개의 원격 메서드를 제공하는 gRPC 서비스를 호스팅하고, 클라이언트는 이러한 원격 메서드들을 호출할 수 있습니다.

ProductInfo 서비스의 클라이언트-서버 상호작용

 

 

 

gRPC 서버 구성하기 (Java)

build.gradle 구성

grpc 버전은 글 작성일을 기준으로 최신 버전으로 구성하였습니다.

buildscript {
	ext {
		protobufVersion = '4.27.2'
		protobufPluginVersion = '0.9.4'
		grpcVersion = '1.70.0'
	}
}

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.8'
	id 'io.spring.dependency-management' version '1.1.7'
	id 'com.google.protobuf' version "${protobufPluginVersion}"
}

group = 'grpc'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	// spring boot
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-validation'

	// mysql
	runtimeOnly 'com.mysql:mysql-connector-j'

	// lombok
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	// grpc 프로토콜 버터를 사용하기 위한 핵심 라이브러리 (Protobuf 메시지의 직렬화 및 역직렬화를 지원)
	implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}"
	implementation "com.google.protobuf:protobuf-java:${protobufVersion}"

	// grpc 서버, 클라이언트 설정
	implementation 'net.devh:grpc-spring-boot-starter:3.1.0.RELEASE' // Spring Boot와 gRPC의 통합을 간편하게 도와주는 스타터
	implementation "io.grpc:grpc-netty-shaded:${grpcVersion}" // gRPC 서버와 클라이언트의 Netty 전송 계층을 제공
	implementation "io.grpc:grpc-protobuf:${grpcVersion}"     // Protobuf 메시지와 gRPC의 통합을 지원
	implementation "io.grpc:grpc-stub:${grpcVersion}"         // gRPC 클라이언트 스텁을 생성
	compileOnly 'org.apache.tomcat:annotations-api:6.0.53'

	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

protobuf {
	// Protobuf 컴파일러를 지정하여 .proto 파일을 컴파일
	protoc {
		artifact = "com.google.protobuf:protoc:${protobufVersion}"
	}
	// 생성된 파일을 정리
	clean {
		delete generatedFilesBaseDir
	}
	// gRPC 플러그인을 설정하여 Protobuf 파일로부터 gRPC 관련 코드를 생성
	plugins {
		grpc {
			artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
		}
	}
	// 모든 프로토콜 버퍼 작업에 대해 gRPC 플러그인을 적용
	generateProtoTasks {
		all()*.plugins {
			grpc {}
		}
	}
}

tasks.named('test') {
	useJUnitPlatform()
}

 

의존성 설명

  • protobuf-java-util & protobuf-java: Protobuf 메시지의 직렬화 및 역직렬화 지원
  • grpc-spring-boot-starter: Spring Boot와 gRPC의 간편한 통합을 지원하는 스타터
  • grpc-netty-shaded: Netty 전송 계층을 제공하며, 모든 종속성이 포함된 버전으로 종속성 충돌 문제를 줄여줌
  • grpc-protobuf: Protobuf와 gRPC의 통합을 지원
  • grpc-stub: gRPC 클라이언트 스텁을 생성
  • annotations-api: javax 어노테이션 관련 컴파일 오류 방지. (빌드시 컴파일 오류 해결)
 

GitHub - grpc/grpc-java: The Java gRPC implementation. HTTP/2 based RPC

The Java gRPC implementation. HTTP/2 based RPC. Contribute to grpc/grpc-java development by creating an account on GitHub.

github.com

 

 

proto 파일 생성

.proto 파일을 작성하기 전에 src/main 디렉터리 밑에 proto 디렉터리를 생성하고,

└── src
    ├── main
    │   ├── java
    │   ├── proto
    │       └── productInfo.proto
    │   ├── resources
    ├── test

 

예시를 위해 만든 ProductInfo 서비스 정의 파일을 작성해줍니다.

아까 build.gradle에서 Protobuf 파일을 컴파일하고, gRPC 코드를 생성하는 작업을 하는 protobuf 블록을 추가했으므로

IntelliJ gradle 선택 > Tasks > other > generateProto를 통해 .proto 파일을 빌드할 수 있습니다.

또는 gradle build를 통해 애플리케이션 전체를 빌드하면서 .proto 파일도 자동으로 빌드되고, 필요한 코드가 생성되도록 할 수 있습니다.

syntax = "proto3";
package productInfo;

service ProductInfo {
  rpc addProduct(Product) returns (ProductID);
  rpc getProduct(ProductID) returns (Product);
}

message Product {
  int64 id = 1;
  string name = 2;
  string description = 3;
  double price = 4;
}

message ProductID {
  int64 id = 1;
}

 

 

상품 Entity 생성

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "product_id")
    private Long id;

    @NotNull
    private String name;

    @NotNull
    private String description;

    @NotNull
    private double price;

    @Builder
    private Product (@NonNull String name,
                     @NonNull String description,
                     double price) {
        this.name = name;
        this.description = description;
        this.price = price;
    }
}

 

 

상품 Repository, Service 생성

다음과 같이 간단하게 구현하였습니다.

public interface ProductRepository extends JpaRepository<Product, Long> {
}
@Service
@RequiredArgsConstructor
@Slf4j
public class ProductService {

    private final ProductRepository productRepository;

    @Transactional
    public ResponseIdDto createProduct(RequestProductDto dto) {
        Product product = Product.builder()
                .name(dto.getName())
                .description(dto.getDescription())
                .price(dto.getPrice())
                .build();
        productRepository.save(product);

        return new ResponseIdDto(product.getId());
    }

}

 

 

gRPC 관련 비즈니스 로직 구현

아래와 같이 프로토버프 플러그인에 의해 생성된 추상 클래스(ProductInfoGrpc.ProductInfoImplBase)를 확장합니다. 이를 통해서 서비스에 정의된 addProduct와 getProduct 메서드에 비즈니스 로직을 추가할 수 있습니다.

 

각 메서드에서 파라미터로 사용하고 있는 Product(ProductInfoOuterClass.Product)와 ProductID(ProductInfoOuterClass.Product)는 서비스 정의에 의해 생성된 ProductInfoOuterClass 클래스에 선언되어있습니다.

 

그리고 responseObserver 객체는 클라이언트에게 응답을 보내고 스트림을 닫는 데 사용됩니다.

@GrpcService
@RequiredArgsConstructor
@Slf4j
public class ProductInfoImpl extends ProductInfoGrpc.ProductInfoImplBase {

    private final ProductService productService;

    private final ProductRepository productRepository;

    @Override
    public void addProduct(ProductInfoOuterClass.Product request,
                           StreamObserver<ProductInfoOuterClass.ProductID> responseObserver) {
        RequestProductDto requestProductDto = RequestProductDto.builder()
                .name(request.getName())
                .description(request.getDescription())
                .price(request.getPrice())
                .build();

        ResponseIdDto responseIdDto = productService.createProduct(requestProductDto);

        ProductInfoOuterClass.ProductID id = ProductInfoOuterClass.ProductID.newBuilder()
                        .setId(responseIdDto.getId()).build();

        responseObserver.onNext(id);
        responseObserver.onCompleted();
    }

    @Override
    public void getProduct(ProductInfoOuterClass.ProductID request,
                           StreamObserver<ProductInfoOuterClass.Product> responseObserver) {
        Long id = request.getId();

        Optional<Product> product = productRepository.findById(id);

        if (product.isPresent()) {
            responseObserver.onNext(
                    ProductInfoOuterClass.Product.newBuilder()
                            .setId(product.get().getId())
                            .setName(product.get().getName())
                            .setDescription(product.get().getDescription())
                            .setPrice(product.get().getPrice())
                            .build()
            );
            responseObserver.onCompleted();
        } else {
            responseObserver.onError(new StatusException(Status.NOT_FOUND));
        }
    }
}

 

 

아래와 같이 패키지 구조와 구성을 가지고 gRPC 서비스를 구현해보았습니다.

└── src
    ├── main
    │   ├── java
    │   	├── grpc.server
    │   		├── global
    │   			├── base
    │       			└── BaseEntity.java
    │   			├── config
    │       			└── JpaConfig.java
    │   			├── model.dto
    │       			└── ResponseIdDto.java
    │   		├── product
    │   			├── model
    │   				├── dto.request
    │       				└── RequestProductDto.java
    │   				├── entity
    │       				└── Product.java
    │   			├── repository
    │       			└── ProductRepository.java
    │   			├── service
    │       			└── ProductService.java
    │       			└── ProductInfoImpl.java
    │   ├── proto
    │       └── productInfo.proto
    │   ├── resources
    ├── test

 

 

 

gRPC 클라이언트 구성하기 (Golang)

전체적인 구조는 아래와 같습니다.

(작성한 Golang 코드가 아직 부족한 점이 많을 수 있으니 가볍게 봐주시면 감사하겠습니다!)

└── client
   ├── config
   │   ├── config.go
   ├── proto
   │   ├── productInfo.pb.go
   │   ├── productInfo.proto
   │   ├── productInfo_grpc.pb.go
   ├── go.mod
   ├── main.go
   ├── config.toml

 

 

작성했던 .proto 파일을 그대로 가져와서 아래 명령어로 컴파일해줍니다.

그러면 productInfo.pb.go 파일과 productInfo_grpc.pb.go 파일이 생성될겁니다.

protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    proto/productInfo.proto

 

 

gRPC 서버의 포트는 50051이고 로컬환경에서 진행하므로, toml 파일에 아래와 같이 작성해주었습니다.

[grpc]
url = "localhost:50051"

 

 

다음과 같이 애플리케이션의 설정 파일(config.toml)을 읽고 Config 구조체에 매핑하도록 코드를 작성해보았습니다.

toml.NewDecoder(file).Decode(c)를 사용해 파일 내용을 Config 구조체에 디코딩하고 에러가 발생하면 panic(err)가 발생합니다.

package config

import (
	"github.com/naoina/toml"
	"os"
)

type Config struct {
	GRPC struct {
		URL string
	}
}

func NewConfig(path string) *Config {
	c := new(Config)

	if file, err := os.Open(path); err != nil {
		panic(err)
	} else {
		defer file.Close()

		if err = toml.NewDecoder(file).Decode(c); err != nil {
			panic(err)
		} else {
			return c
		}
	}
}

 

 

gRPC 서버와 연결되고 메서드를 호출하는 코드는 아래 main.go에 모두 작성해두었습니다.

localhost:50051로 gRPC 서버와의 커넥션을 설정하는데, 여기서는 클라이언트와 서버 사이에 보안이 되지 않은(insecure.NewCredentials()) 커넥션을 만듭니다.

 

c := productInfo.NewProductInfoClient(conn)

  • 이 부분에서 커넥션을 전달해 스텁을 생성하게 됩니다. 이 스텁 인스턴스에 서버를 호출하는 모든 원격 메서드가 포함되어있습니다.

ctx, cancel := context.WithTimeout(context.Background(), time.Second)

  • 원격 호출과 함께 전달할 Context를 생성합니다.
  • Context 객체에는 최종 사용자 인증 토큰에 대한 ID와 요청 데드라인 같은 메타데이터가 포함되며 서비스 요청 동안 유지됩니다.
package main

import (
	"context"
	"flag"
	"grpc/client/config"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	productInfo "grpc/client/proto"
)

var configFlag = flag.String("config", "./config.toml", "config path")

func main() {
	// Config 초기화
	cfg := config.NewConfig(*configFlag)

	// GRPC 연결 생성
	conn, err := grpc.NewClient(cfg.GRPC.URL, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close() // 모든 작업이 끝나면 커넥션을 종료
	c := productInfo.NewProductInfoClient(conn)

	// 상품 정보 추가
	name := "Apple MacBook M3 Pro"
	description := "Meet Apple MacBook M3 Pro. Featuring the powerful M3 Pro chip for enhanced performance and efficiency."
	price := float64(1699.99)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	r, err := c.AddProduct(ctx, &productInfo.Product{Name: name, Description: description, Price: price})
	if err != nil {
		log.Fatalf("Could not add product: %v", err)
	}
	log.Printf("Product ID: %s added successfully", r.Id)

	// 상품 정보 조회
	product, err := c.GetProduct(ctx, &productInfo.ProductID{Id: r.Id})
	if err != nil {
		log.Fatalf("Could not get product: %v", err)
	}
	log.Printf("Product: %v", product.String())
}

 

 

실행 결과

gRPC 클라이언트 측

go run main.go 명령어를 통해 소스 코드 파일을 컴파일하고 실행하였습니다.

아래와 같이 로그가 성공적으로 뜨는것을 확인할 수 있었습니다.

클라이언트 실행시 콘솔 로그

 

 

gRPC 서버 측

addProduct()와 getProduct()가 각각 한 번씩 수행되므로,

아래와 같이 성공적으로 Insert와 Select 문이 한 번씩 수행된 것을 로그에서 확인할 수 있었습니다.

서버 실행시 콘솔 로그

 

addProduct 수행 후, 테이블에 정상적으로 저장됨

 

 

'gRPC' 카테고리의 다른 글

Protocol Buffer와 인코딩/디코딩 그리고 gRPC 내부 통신 동작 원리  (0) 2025.04.07
RPC 통신과 gRPC  (0) 2025.01.12
'gRPC' 카테고리의 다른 글
  • Protocol Buffer와 인코딩/디코딩 그리고 gRPC 내부 통신 동작 원리
  • RPC 통신과 gRPC
개발이조아용
개발이조아용
IT 개발에서 배운 성장의 기록을 작성합니다.
  • 개발이조아용
    계속 하다 보면?!
    개발이조아용
  • 전체
    오늘
    어제
    • 분류 전체보기 (67)
      • Tibero DB (Tmax AI Bigdata .. (7)
      • Git (2)
      • CI CD (2)
      • Redis (3)
      • SpringBoot (16)
      • SQL 문제 풀이 (8)
      • Apache Kafka (8)
        • 오류 해결 (3)
        • 개념 정리 (4)
        • 보안 (1)
      • Nginx (3)
      • SW마에스트로 (3)
      • Kubernetes (4)
      • AWS (5)
      • gRPC (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    leetcode
    KAFKA
    K8S
    SpringBoot
    Kafka SASL
    grpc
    sql 문제
    nginx
    Tibero
    소프트웨어 마에스트로
    MSA
    Kafka 개념
    Git
    Kafka 오류
    SQL
    Redis 개념
    SASL 인증
    DynamoDB 연동
    redis script
    redis
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
개발이조아용
gRPC 통신 구현해보기 (서버 : Java / 클라이언트 : Go)
상단으로

티스토리툴바