본문 바로가기
Web Programming

gRPC - From Java(Spring) Client to Python Server 예제

by 맑은안개 2022. 9. 2.

Introduction

gRPC를 사용하여 Java(Client)와 Python(Server)간 서비스 호출하는 방법을 알아본다.
Client는 Spring boot를 사용하여 간단한 Rest server로 만든다. 사용자로 부터 Rest 호출을 받고 Python 서버와 gRPC를 통해 서비스 호출하는 프로세스를 구현한다.

 

예제에서 사용된 환경의 Visual Studio Code + Spring boot 실행방법은 전 블로그를 참고한다.

2021.04.27 - [Web/springboot] - VSCode에서 SpringBoot with gradle 프로젝트 생성 & 실행

 

VSCode에서 SpringBoot with gradle 프로젝트 생성 & 실행

2019년, Stack Overflow에서 개발자를 상대로 선호하는 프로그램 개발 툴을 조사한 결과, VS Code가 50.7%로 가장 선호되는 툴로 선정됐다. Electron Framework로 만들어졌으며 Java, Javascript, Python등 다양..

youngwonhan-family.tistory.com

Development Environment

  • IDE: Visual Studio Code
  • gRPC
    • proto3 (Syntax version)
    • gRPC 1.47.0
  • Client
    • Spring boot 2.7.1
    • java 1.8
    • build tool: Gradle
  • Server
    • Python 3.10.4

1. Protocol buffers

서버, 클라이언트간 서비스 호출은 Protocol buffer를 사용하여 입출력 메시지, 서비스를 명세화한다. 이를 통해 gRPC를 사용하는 도메인들은 해당 명세를 보고 독립적으로 분리되어 개발할 수 있다.

디렉토리 구조를 server / client / proto로 나눈다.

1.1. /proto/book.proto

syntax = "proto3";

option java_package = "com.example.client.rpcservice";
option java_outer_classname = "BookProto";

package book;

service BookService {
    rpc GetBooksByBookName(BookRequest) returns (BooksResponse) {}
    rpc GetBookById(BookRequest) returns (BookResponse) {}
}

message BookRequest {
    Book book = 1;
}

message BookResponse {
    Book book = 1;
    string response_code = 2;
    string response_msg = 3;
}

message BooksResponse {
    repeated Book books = 1;
    string response_code = 2;
    string response_msg = 3;
}

message Book {
    int32 id = 1;
    string name = 2;
    enum Genre {
        UNKOWN = 0;
        DETECTIVE = 1;
        MYSTERY = 2;
        ROMANCE = 3;
    }
    Genre genre = 3;
    repeated string tags = 4;
    Author author = 5;
}

message Author {
    int32 id = 1;
    string name = 2;
    string dob = 3;
}
  • option java_package 지정하여 Java 컴파일 후 build path를 지정한다.
  • 문법은 2, 3버전이 존재한다. 여기선 3버전을 사용.
  • Protocol Buffers는 대부분의 프로그래밍 언어에서 사용되는 스칼라 변수를 지원한다.

2. Client

Spring boot Initializer를 사용하여 아래의 build.gradle과 같이 dependencies, proto plug-in을 설정한다.
( Gradle plug-in을 사용하여 Java stub파일을 생성한다.)

2.1. build.gradle

buildscript {
    ext {
        protobufVersion = '3.19.2'
        protobufPluginVersion = '0.8.18'
        grpcVersion = '1.47.0'
    }
}

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

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    implementation "io.grpc:grpc-protobuf:${grpcVersion}"
    implementation "io.grpc:grpc-stub:${grpcVersion}"
    implementation "io.grpc:grpc-netty:${grpcVersion}"
    implementation "io.grpc:grpc-okhttp:${grpcVersion}"
    implementation 'com.googlecode.protobuf-java-format:protobuf-java-format:1.4'
}

// source 클래스패스가 java project에 인식되지 않는 경우, Clean workspace를 수행한다.
sourceSets {
    main {
        java {
            srcDirs 'build/generated/source/proto/main/grpc'
            srcDirs 'build/generated/source/proto/main/java'
        }
        proto {
            srcDirs '../proto'
        }
    }
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:${protobufVersion}"
    }
    plugins {
        grpc {
            artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc{}
        }
    }
}

tasks.named('test') {
    useJUnitPlatform()
}
  • sourceSets에 정의된 것처럼 proto파일 위치를 지정한다. ( 해당 디렉토리의 모든 proto파일을 컴파일하여 Java stub파일로 생성한다. )

2.2. Java stub 생성

Client root 디렉토리에서 다음 gradle task를 실행하여 Java stub파일을 생성한다.

./gradlew generateProto

Visual Studio Code - Java Project view

위 와 같이 build에 생성된 proto stub파일이 SourceSet으로 설정되어야 한다.

2.3. BookConfig.java

package com.example.client.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

@Configuration
public class BookConfig {

    @Bean
    public ManagedChannel getManagedChannel() {
        return ManagedChannelBuilder.forTarget("127.0.0.1:50051")
                .usePlaintext()
                .build();
    }

}
  • Python Server 채널을 생성한다. ( Python server는 로컬호스트에 50051포트로 서비스 된다 )

2.4. BookController

package com.example.client.controller;

@RestController
public class BookController {

    private ManagedChannel channelToPythonServer;
    private final BookServiceGrpc.BookServiceBlockingStub bookStub;

    BookController(ManagedChannel managedChannel) {
        channelToPythonServer = managedChannel;
        bookStub =  BookServiceGrpc.newBlockingStub(channelToPythonServer);
    }

    @GetMapping(value="/book/id/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public String selectBookInfoById(@PathVariable int id) {
        Book book = Book.newBuilder().setId(id).build();

        BookResponse bookResponse = bookStub.getBookById(
            BookRequest.newBuilder()
                .setBook(book)
                .build()
        );

        return new JsonFormat().printToString(bookResponse);
    }

    @GetMapping(value="/book/name/{name}", produces = MediaType.APPLICATION_JSON_VALUE)
    public String selectBooksByBookName(@PathVariable String name) {
        Book book = Book.newBuilder().setName(name).build();

        BooksResponse booksResponse = bookStub.getBooksByBookName(
            BookRequest.newBuilder()
                .setBook(book)
                .build()
        );

        return new JsonFormat().printToString(booksResponse);
    }

}
  • Bean으로 설정한 ManagedChannel을 DI하고 stub을 생성한다.
  • stub을 사용하여 .proto에 정의되어 있는 서비스에 접근한다.
  • Java에서는 Proto파일에 message로 정의된 객체를 Build pattern을 통해 생성할 수 있다.
  • 아래와 같이 Proto에 정의된 service를 stub 클래스로 접근하여 호출한다.
        BookResponse bookResponse = bookStub.getBookById(
            BookRequest.newBuilder()
                .setBook(book)
                .build()
        );

3. Server

Server측에서도 Proto명세로 만들어진 stub파일이 필요하다. Python에는 grpcio-tools 모듈을 사용하여 생성한다.

3.1. gRPC 관련 모듈 설치

pip install grpcio

pip install grpcio-tools

3.2. Python Stub 생성

Stub파일을 생성하는 쉘 스크립트를 작성한다. ( Proto 명세가 바뀔 때마다 간편히 실행하기 위함 )

# /proto/genproto.sh

python -m grpc_tools.protoc -I. --python_out=../server --grpc_python_out=../server book.proto
  • 스크립트 생성 후 /server 디렉토리에 stub파일이 정상적으로 생성되었는지 확인한다.

3.3. /server/server.py

import book_service

from concurrent import futures
import logging

import grpc
import book_pb2_grpc


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    book_pb2_grpc.add_BookServiceServicer_to_server(
        book_service.BookService(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()


if __name__ == '__main__':
    logging.basicConfig()
    serve()
  • Python server 실행 파일

3.4. /server/data_storage.py

가상의 데이터 스토어를 만들어 요청 값에 따라 리턴한다.

import book_pb2


def get_book_data():
    return {
        1: {
            "id": 1,
            "name": "Who moved my cheese",
            "genre": book_pb2.Book.ROMANCE,
            "tags": ["motivational fable"],
            "author": {
                "id": 2,
                "name": "Cheese",
                "dob": "19800502"
            }
        },
        2: {
            "id": 2,
            "name": "The Giver",
            "genre": book_pb2.Book.MYSTERY,
            "tags": ["newbery", "science"],
            "author": {
                "id": 1,
                "name": "Louis",
                "dob": "19760502"
            }
        },
        3: {
            "id": 3,
            "name": "Who are you",
            "tags": ["netflix", "movie", "novel"],
            "author": {
                "id": 1,
                "name": "Louis",
                "dob": "19760502"
            }
        }
    }

3.5. book_service.py

gRPC Service 요청에 대한 처리 프로세스를 담당하는 파일

import data_storage
import asyncio

import book_pb2
import book_pb2_grpc

import time

book_db = data_storage.get_book_data()

success_message = {
    "response_code": "200",
    "response_msg": "it has been successfully done"
}

not_found_message = {
    "response_code": "404",
    "response_msg": "No data found"
}

fail_message = {
    "response_code": "500",
    "response_msg": "Occured a server error"
}

class BookService(book_pb2_grpc.BookServiceServicer):

    def GetBookById(self, request, context):
        try:
            res = book_pb2.BookResponse(
                book=book_pb2.Book(**book_db[request.book.id]),
                **success_message
            )
        except KeyError:
            res = book_pb2.BookResponse(
                **not_found_message
            )
        except:
            res = book_pb2.BookResponse(
                **fail_message
            )

        return res

    def GetBooksByBookName(self, request, context):

        book_name = request.book.name.lower()

        if len(book_name) < 2:
            return book_pb2.BooksResponse(
                response_code="500",
                response_msg="Please put name at least 3 characters"
            )

        res = book_pb2.BooksResponse(
            books=[
                book_db[i] for i in book_db if book_db[i]["name"].lower().find(book_name) != -1
            ],
            **success_message
        )
        return res
  • book_pb2_grpc.BookServiceServicer 를 상속한다.
  • Proto Service에 정의한 함수를 구현한다. ( GetBookById, GetBooksByBookName )

4. 예제 환경 실행

Client, Server를 실행하고 Postman에서 요청 처리 한다.

4.1. Client

root_project/client> ./gradlew bootRun

4.2. Server

root_project/server> python server.py

Client + Server 실행

4.3. Postman 요청

 

반응형