본문 바로가기

Lang

Armeria, gRPC, JPA 간단한 샘플

라인 엔지니어가 올려놓은 샘플 응용해서 Armeria, JPA 이용한 간단한 gRPC API 만들어본거 정리(아직 webflux + R2DBC 를 시도해보진 못했다)

Armeria

  • https://armeria.dev/
  • https://github.com/line/armeria : 사용자 친화적인 Java microservice framework.
    • asynchronous, reactive model 기반.
    • Netty, gRPC, Thrift에 기반한 오픈소스 비동기 HTTP/2 RPC/REST 클라이언트/서버 라이브러리.

gRPC 서버

  • 높은 생산성과 효율적인 유지보수
  • 다양한 언어와 플랫폼 지원 : C/C++, go, java, C#, python, node.js 등
  • HTTP/2 기반의 양방향 스트리밍
  • 높은 메시지 압축률과 성능

개발

설정

plugins {  
  id 'org.springframework.boot' version '2.3.2.RELEASE'  
  id 'io.spring.dependency-management' version '1.0.9.RELEASE'  
  id 'java'  
  id 'com.google.protobuf' version '0.8.12'  
}

group = 'com.test.dc_api'  
version = '0.0.1-SNAPSHOT'  
sourceCompatibility = '11'

configurations {  
  compileOnly {  
    extendsFrom annotationProcessor  
  }  
}

repositories {  
  mavenCentral()  
}

dependencies {  
  implementation 'org.springframework.boot:spring-boot-starter-webflux'  
  implementation 'com.linecorp.armeria:armeria-spring-boot2-webflux-starter'  
  implementation 'com.linecorp.armeria:armeria-grpc'  
  compileOnly 'org.projectlombok:lombok'  
  annotationProcessor 'org.projectlombok:lombok'  
  testImplementation('org.springframework.boot:spring-boot-starter-test') {  
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'  
  }  
  testImplementation 'io.projectreactor:reactor-test'  
  compile('org.springframework.boot:spring-boot-starter-data-jpa')
  runtime group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.4.1'
}

test {  
  useJUnitPlatform()  
}

dependencyManagement {  
  imports {  
    mavenBom 'com.linecorp.armeria:armeria-bom:0.99.9'  
    mavenBom 'io.netty:netty-bom:4.1.51.Final'  
  }  
}

protobuf {  
  protoc {  
    artifact = "com.google.protobuf:protoc:4.0.0-rc-2"  
  }  
  plugins {  
    grpc {  
      artifact = 'io.grpc:protoc-gen-grpc-java:1.31.0'  
    }  
  }  
  generateProtoTasks {  
    all()\*.plugins {  
      grpc {}  
    }  
    all().each { task ->  
      task.generateDescriptorSet = true  
      task.descriptorSetOptions.includeSourceInfo = true  
      task.descriptorSetOptions.includeImports = true  
      task.descriptorSetOptions.path =  
        "${buildDir}/resources/main/META-INF/armeria/grpc/service-name.dsc"  
    }  
  }  
}

sourceSets {  
  main {  
    java {  
      srcDir 'build/generated/source/proto/main/grpc'  
      srcDir 'build/generated/source/proto/main/java'  
    }  
  }  
}

서버 개발

proto

syntax = "proto3";

package com.test.dc_api.proto;  
option java_package = "com.test.dc_api.proto";  
option java_outer_classname = "TaskOrdGrpcProto";

import "google/protobuf/timestamp.proto";

service TaskOrd {  
    rpc GetTaskOrd (TaskOrdReq) returns (stream TaskOrdRes) {}  
}

message TaskOrdReq {  
    string taskOrdId = 1;  
}

message TaskOrdRes {  
    string taskOrdId = 1;  
    string taskActCd = 2;  
    string taskTpCd = 3;  
    google.protobuf.Timestamp taskStDt = 4;  
    google.protobuf.Timestamp taskCmpltDt = 5;  
    string twinTandCd = 6;  
}

controller

@Controller  
@AllArgsConstructor  
public class DcApiController {

    private final DcApiService dcApiService;

    @GetMapping("/task_ord")  
    @ResponseBody  
    public List getTaskOrd(String taskOrdId) {  
        return dcApiService.getTaskOrd(taskOrdId);  
    }

}

service

grpc service
@Service  
@AllArgsConstructor  
public class TaskOrdGrpcService extends TaskOrdGrpc.TaskOrdImplBase {

private final DcApiService dcApiService;

@Override  
public void getJobOrd(  
    TaskOrdGrpcProto.TaskOrdReq request,  
    StreamObserver<TaskOrdGrpcProto.TaskOrdRes> responseObserver) {  
        String taskOrdId = request.getTaskOrdId();

        dcApiService.getTaskOrd(taskOrdId)
            .forEach(e -> {
                responseObserver.onNext(e.toProto());
            });

    }

}
service
@Service  
@RequiredArgsConstructor  
public class DcApiService {  
    @Autowired  
    TaskOrdRepository taskOrdRepository;

    public List getTaskOrd(String taskOrdId) {  
        return taskOrdRepository.findByTaskOrdId(taskOrdId);  
    }
}
entity
@Entity  
@Data  
@Table(name = "task_ord")  
@JsonIgnoreProperties({ "id" })  
public class TaskOrd implements Persistable, Serializable {  
    @Id  
    @Column(name = "regSeq")  
    @GeneratedValue(strategy= GenerationType.IDENTITY)  
    private long id;  

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMddHHmmss")  
    private LocalDateTime evntDt;

    private String taskOrdId;  
    private String taskActCd;  
    private String taskTpCd;  

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMddHHmmss")  
    private LocalDateTime taskStDt;  
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMddHHmmss")  
    private LocalDateTime taskCmpltDt;

    public TaskOrdGrpcProto.TaskOrdRes toProto() {  
        return TaskOrdGrpcProto.TaskOrdRes.newBuilder()  
            .setTaskOrdId(getTaskOrdId())  
            .setTaskActCd(getTaskActCd())  
            .setTaskTpCd(getTaskTpCd())  
            ...  
            .build();  
    }

}
config
@Configuration  
@RequiredArgsConstructor  
public class ArmeriaServerConfiguration {

    private final DemoGrpcService demoGrpcService;  
    //private final AlertHstGrpcService alertHstGrpcService;  
    private final TaskOrdGrpcService taskOrdGrpcService;

    @Bean  
    public ArmeriaServerConfigurator armeriaServerConfigurator() {

        return serverBuilder -> {
            serverBuilder.decorator(LoggingService.newDecorator());
            serverBuilder.decorator(ContentPreviewingService.newDecorator(Integer.MAX_VALUE, StandardCharsets.UTF_8));
            serverBuilder.accessLogWriter(AccessLogWriter.combined(), false);

            serverBuilder.service(GrpcService.builder()
                .addService(demoGrpcService)
                //.addService(alertHstGrpcService)
                .addService(TaskOrdGrpcService)
                .supportedSerializationFormats(GrpcSerializationFormats.values())
                .enableUnframedRequests(true)
                .build());

            serverBuilder.serviceUnder("/docs", new DocService());

        };
    }

}

 

  • test

         % curl --http2 -v http://localhost:9991/task_ord?taskOrdId='5288717'

  • generate pb

         % python -m grpc_tools.protoc -I./protos --python_out=.. --grpc_python_out=.. ./protos/task_ord.proto

다음 두 파일 생성됨 : task_ord_pb2.py, task_ord_pb2_grpc.py

  • python client
from future import print_function
import logging

import grpc

import task_ord_pb2
import task_ord_pb2_grpc

def run():
    with grpc.insecure_channel('localhost:9991') as channel:
        stub = task_ord_pb2_grpc.TaskOrdStub(channel)
        response = stub.GetTaskOrd(task_ord_pb2.TaskOrdReq(taskOrdId='5288717'))
        # print(response)

        try:
            i = 1
            for res in response:
                print(i, res)
                i += 1
        except grpc._channel._Rendezvous as err:
            status_code = err.code()
            if grpc.StatusCode.CANCELLED == status_code:
                print("end")
                pass
            else:
                print(err)

if name == 'main':
    logging.basicConfig()
    run()