본문 바로가기

GRPC

1) GRPC 란 ?

반응형

springboot를 이용해서 rest api로 backEnd를 개발하다 보면 성능에 대한 부분을 많이 고려하게 되고 생각하게 된다.

 

최근에 성능에 문제가 생겼을때 어떻게 하셨어요? 라고는 질문을 받았다. 

 

보통 지금까지는 성능에 문제가 생겼을때 infra를 확장하는 방식 즉. 쿠버네티스의 경우는 worker node를 늘리고 

 

해당 api에 대한 pod를 늘리는 방식으로 처리를 했다. 

 

rest api는 단일 Request 에 단일 Response만 주기 때문에 Client는 여러번의  Request를 보내 서버에 처리를 요청하게 되는 경우가 많다. 이렇게 되면 사실 불필요한 네트웍 비용이 발생하기도 하고 성능에도 크게 좋지 않은 결과를 가져 올 수 있다. 

 

이를 해결하기 위해 소켓통신이나 Soap , RPC 같은 기술등이 등장했다고 한다. 

 

여기서는 이런 문제를 해결하기 Google에서 만들어진 높은 성능의 RPC( Remote Procedure Calls ) 프레임워크인 

GRPC에 대해 공부해 보자.

 

GRPC는 왜 성능이 REST API보다 빠르다고 애기를 하는지 궁금했다. 이유를 알아 보자.

 

1) 통신프로토콜을 HTTP/2 를 사용한다.

   HTTP/2는 HTTP/1.1의 문제점인 하나의 연결에 하나의 요청과 응답만 처리 가능한 문제점 

  예를들어 HTTP/1.1 은 하나의 WEB Page를 요청하게 되면 거기에 필요한 script , css 등의 Data는 또 각각 요청을 보내

  야 하는 문제로 인해 성능 문제가 발생하게 됨

 

  HTTP/2는 하나의 연결에 여러개의 메세지를 주고 받을수 있는 양방향 통신이 가능한 프로토콜이므로 단순하게 생각

 해서 서버에 index.html을 요청을 하면 서버에서는 index.html 과 그에 필요한 js 내용까지 한번에 보내주면 클라이언트

 는 한번만 요청을 하고 그에 대한 Data만 받아서 처리가 가능하니 성능이 더 나아지는것은 당연하다.

 

2) protobuf 라는 데이터 포맷을 사용. protobuf는 역시 구글에서 만든 json 과 같은 데이터 포맷을 애기하는데 

   이는 xml 과 json는 비해 굉장히 적은 양으로 표현가능하고 처리속도도 몇십배가 빠르다고 한다.

 

기본적으로 RPC는 말 그대로 원격에 있는 서버의 함수를 호출한다로 애기할 수 있는데 그러면 아래와 같은 내용을 서버와 client가 알고 있어야 한다.

 

1) 서버에 있는 기능이 뭐가 있는지 

2) 서버에 있는 기능을 호출할때 parameter는 어떤 정보를 넘겨야 하는지

3) 결과값은 어떤 결과를 주는지

 

크게 저 3가지에 대한 약속은 서버와 client가 미리 약속을 하고 서버 알고 있어야 한다. 

 

REST API 경우는 저 부분에서 좀 더 자유롭게 되고 그런 자유도로 인해 OverHead가 발생하게 된다.

 

GRPC를 사용하기 위해 아래와 같은 순서가 필요하다.

 

1) .proto 라는 파일을 서버와 Client가 공유한다.

2) .proto라는 파일을 가지고 grpc build를 하여 .proto에 있는 내용대로 java 파일을 생성한다.

3) Server 는 생성된 Java 파일을 Override해서 기능을 구현.

4) Client는 생성된 파일을 기준으로 서버에 연결한 Stub로 함수를 호출결과를 받는다.

 

간단히 저런 순서가 필요하다. 

 

Server에서 보내는 방식및 Client에서 받는 방식 및 Request를 보내는 방식이 몇가지 있는데 이는 2부에서 다시 공부하기로 한다. 

 

먼저 간단히 서버를 띄우고 Client에서 호출하는것 까지 해보자.

 

서버 프로그램 소스를 만들어보자.

1) 이클립스에 gradle 프로젝트 생성.

 

2) build.gradle 파일을 아래와 같이 작성.

buildscript { 
	repositories { 
		mavenCentral() 
	} 
	
	dependencies { 
		classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8' 
	} 
} 

plugins {
	 id 'org.springframework.boot' version '2.2.6.RELEASE' 
	 id 'io.spring.dependency-management' version '1.0.9.RELEASE' 
	 id 'java' 
}

apply plugin: 'com.google.protobuf'
 
group = 'com.devracoon.grpc' 
version = '0.0.1-SNAPSHOT' 
sourceCompatibility = '1.8' 

configurations { 
	compileOnly { 
		extendsFrom annotationProcessor 
	} 
} 

repositories { 
	mavenCentral() 
} 

dependencies {
	 /** * gRPC */ 
	 compile group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.21.0' 
	 compile group: 'io.grpc', name: 'grpc-protobuf', version: '1.21.0' 
	 compile group: 'io.grpc', name: 'grpc-stub', version: '1.21.0' 
	 implementation "com.google.protobuf:protobuf-java-util:3.8.0" 
	 compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.8.0' 
	 implementation 'org.springframework.boot:spring-boot-starter-webflux' 
	 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' 
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.7.1"
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.21.0'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}


test { 
	useJUnitPlatform() 
} 

sourceSets {
	main {
		java {
			srcDirs += [ 'build/generated/source/proto/main/grpc', 'build/generated/source/proto/main/java' ] 
		} 
	} 
}

 

3) src/main/proto 폴더에 .proto파일 생성 및 작성

syntax = "proto3";

option java_multiple_files = true;
option java_outer_classname = "SampleProto";
option java_package = "com.devracoon.grpc.proto";

package com.devracoon.grpc;

message SampleRequest {
	string userId = 1; string message = 2; 
}

message SampleResponse { 
	string message = 1; 
} 

service SampleService {
	rpc SampleCall (SampleRequest) returns (SampleResponse) {} 
}

4) Refresh Gradle Project 실행

 

5) gradle build 실행

 

위와 그림과 같이 소스가 자동 생성 된 것을 확인.

 

6) 다시 Refresh Gradle Project 실행하면 아래 그림과 같이 sourceSet이 project에 추가됨.

 

7) GRPC 서버 함수 작성

package com.devracoon.grpc.sevice;

import org.springframework.stereotype.Service;

import com.devracoon.grpc.proto.SampleRequest;
import com.devracoon.grpc.proto.SampleResponse;
import com.devracoon.grpc.proto.SampleServiceGrpc;

import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class SampleServiceImpl extends SampleServiceGrpc.SampleServiceImplBase {
	
	 @Override
	    public void sampleCall(SampleRequest request, StreamObserver<SampleResponse> responseObserver) {
	        log.info("SampleServiceImpl#sampleCall - {}, {}", request.getUserId(), request.getMessage());
	        SampleResponse sampleResponse = SampleResponse.newBuilder()
	                .setMessage("grpc service response")
	                .build();
	 
	        responseObserver.onNext(sampleResponse);
	        responseObserver.onCompleted();
	    }
	
}

8) 해당 서버 RPC로 서버를 GRPC 서버를 띄우기.

package com.devracoon.grpc;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import com.devracoon.grpc.sevice.SampleServiceImpl;

import io.grpc.Server;
import io.grpc.ServerBuilder;

@Component
public class GrpcServer implements ApplicationRunner {
	private static final int PORT = 3030;
    private static final Server SERVER = ServerBuilder.forPort(PORT)
            .addService(new SampleServiceImpl())
            .build();
    @Override
    public void run(ApplicationArguments args) throws Exception {
        SERVER.start();
    }
    
}

 이렇게 하면 Spring boot 실행을 하면 GrpcServer가 실행되고 작성한 sampleService를 원격에서 호출하게 된다.

 

다음은 Client 소스 작성해보자.

 

1) 서버쪽 1 ~ 6 번까지 했던 작업을 똑같이 해준다. 

   .proto 라고 작성한 파일이 서버와 클라이언트가 약속한 규약이기때문에 똑같은 파일을 Generate해서 사용한다.

 

2) 클라이어튼소스 작성

package com.devracoon.grpc;

import org.springframework.stereotype.Service;

import com.devracoon.grpc.proto.SampleRequest;
import com.devracoon.grpc.proto.SampleResponse;
import com.devracoon.grpc.proto.SampleServiceGrpc;

import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class GrpcClientServiceImpl  {

	private static final int PORT = 3030;
    public static final String HOST = "localhost";
    private final SampleServiceGrpc.SampleServiceStub asyncStub = SampleServiceGrpc.newStub(
            ManagedChannelBuilder.forAddress(HOST, PORT)
            .usePlaintext()
            .build()
    );
    
    public String asyncCall() {
        final SampleRequest sampleRequest = SampleRequest.newBuilder()
                .setUserId("devracoon")
                .setMessage("grpc async test")
                .build();
 
        asyncStub.sampleCall(sampleRequest, new StreamObserver<SampleResponse>() {
            @Override
            public void onNext(SampleResponse value) {
                log.info("asyncCall onNext - {}", value);
            }
 
            @Override
            public void onError(Throwable t) {
                log.error("asyncCall - onError");
            }
 
            @Override
            public void onCompleted() {
                log.info("asyncCall - onCompleted");
            }
        });
        return "string";
    }

}
package com.devracoon.grpc;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Mono;

@RestController
@RequestMapping("sample")
public class SampleController {
	
	@Autowired
	private GrpcClientServiceImpl clientService;
	
	@GetMapping("/asyncCall")
	public Mono<String> asyncCall(){
		
		clientService.asyncCall();
		
		return Mono.just("asyncCall return");
				
	}
}

 

이상태로 client 서버를 띄우고 실행하면 아래와 같이 서버 Log와 클라이언트 Log가 찍힌다.

 

1) 서버Log

위 결과에서 마지막에 보면 클라이언트에서 온 request 값이 찍혀 있는것이 보인다.

 

2) 클라이언트 Log

서버에 요청에 대한 결과값을 OnNext에 정상적으로 응답받은것을 확인했고 서버에서 요청에 대한 처리 완료에 대한 

응답까지 onCompleted 로 응답받은것을 확인했다.

 

간단히 보면 하나의 요청에 처리된 결과와 처리 완료 응답 두가지의 메세지를 받은것을 확인 할 수 있다.

 

간단하게 Async 처리되는 소스를 작성해 보왔다. 

 

2부에서는 좀 더 여러가지의 요청과 메세지 전달 방식에 대해 알아 보자 .

 

 

참고사이트 : https://coding-start.tistory.com/352 

 

gRPC - java gRPC 간단한 사용법

이번 포스팅은 gRPC의 세세한 기능을 다루기 이전에 간단하게 java로 gRPC 서버와 클라이언트 코드를 작성해보고 감을 익혀보는 포스팅이다. 오늘 구성할 프로젝트 구조는 아래와 같다. grpc-common : .

coding-start.tistory.com

 

 

'GRPC' 카테고리의 다른 글

2) GRPC Client - Server 양방향 통신  (0) 2021.01.20