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 프로젝트 생성 & 실행
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
위 와 같이 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
4.3. Postman 요청
'Web Programming' 카테고리의 다른 글
Java Map객체를 Pojo(Model) class로 변환 ( JsonSetter, ObjectMapper 사용법 ) (0) | 2022.11.24 |
---|---|
Java - Lombok활용법, 쉽게 Builder class 만들기 (0) | 2022.10.14 |
문제해결: No ParameterResolver registered for parameter (0) | 2022.06.07 |
카프카(kafka) - VSCode에서 Java Producer/Consumer 생성 with Gradle (0) | 2021.12.31 |
카프카(kafka) - Docker + 카프카 클러스터 구축 및 토픽생성, 메시지 전송 (0) | 2021.12.31 |