등장배경
1. Server-Client Model
하나의 메인 프레임에서 동작하는 Monolothic 구조로 설계 시절 네트워크 통신 중요 X
고가인 메인 프레임워크 → 비교적 저가의 워크스테이션 서버로 대체하고 싶어함
But, 메인 프레임워크의 초고양 사양 그대로 제공하는데 한계
⇒ 메인 프레임워크 기능 → 워크스테이션 서버로 분산 + 네트워크 연결로 서비스하는 방식 채택
= Server-Client Model
⇒ 서버-서버, 서버-개인PC 간 네트워크 연결 및 통신 중요
→ OSI 7 layer, TCP/IP 등 네트워크 계층 구조 정의, 발전
2. IPC
프로세스는 기본적으로 상호독립적
메모리 공유 X → 각자 자신의 일만 하고 서로 간섭 X
But, 필요에 따라 프로세스 간 정보를 교환해야 하는 경우
→ 별도의 수단 이용하여 프로세스 통신하는 방법론 = IPC(Inter Process Communication)
1) Socket
OSI 7 layer 구조의 Application Layer(L7)에서 Transport Port(L4)의 TCP or UDP를 이용하기 위한 수단
일종의 창구 역할
일련의 통신 과정 직접 구현 → 통신 관련 장애 처리 ⇒ 개발자의 몫
서비스 고도화 → data formatting 하는 것 어려워짐 ⇒ RPC
2) RPC(Remote Procedure Call)
네트워크로 연결된 서버 상의 프로시저(함수, 메서드 등) 원격으로 호출할 수 있는 기능
네트워크 통신을 위한 작업 챙기지 X 통신이나 call 방식 신경 X 원격자의 자원을 내 것처럼 사용 가능
IDL(Interface Definication Language) 기반으로 다양한 언어를 가진 환경에서도 쉽게 확장 가능
인터페이스 협업에도 용이
지원 언어: C++, Java, Python, Ruby, Node.js, C#, Go, PHP, Objective-C…
서버-클라이언트는 서로 다른 주소 공간 사용 → 함수 호출에 사용된 매개 변수를 꼭 변환해줘야 함
→ 메모리 매개 변수에 대한 포인터가 다른 데이터를 가리키게 될 수 있기 때문
이러한 변환 담당: 스텁(Stub)
•
client stub
◦
함수 호출에 사용된 파라미터의 변환(Marshalling, 마샬링) 및 함수 실행 후 서버에서 전달된 결과 반환
•
server stub
◦
클라이언트가 전달한 매개 변수의 역변환(Unmarshalling, 언마샬링) 및 함수 실행 결과 변환 담당
RPC 통신 과정
1.
IDL(Interface Definition Language) 사용 → 호출 규약 정의
a.
함수명, 인자, 반환값에 대한 데이터형이 정의된 IDL 파일 → rpcgen으로 컴파일 ⇒ stub code가 자동으로 생성
2.
Stub Code에 명시된 함수: 원시코드의 형태, 상세 기능: server에서 구현됨
a.
만들어진 stub 코드 → 클라이언트/서버와 함께 빌드
3.
client에서 stub에 정의된 함수 사용시
4.
client stub) RPC runtime ~> 함수 호출
5.
server) 수신된 procedure 호출에 대한 처리 후 결과값 반환
6.
최종적으로 client는 server의 결과값 반환받고, 함수를 local에 있는 것처럼 사용 가능
⇒ 원격지의 것을 끌어다 로컬처럼 사용,
구현체 CORBA, RMI 등 여러가지
→ 로컬에서 제공하는 빠른 속도, 가용성 등 분산 프로그래밍에서도 제공
But, 구현의 어려움/지원 한계 등 → 제대로 활용 X
⇒ 데이터 통신을 익숙한 Web을 활용해보려는 시도 → REST
3) REST(REpresentational State Transfer)
HTTP/1.1 기반으로 URI ~> 자원(resource) 명시 + HTTP Method ~> 처리하는 아키텍처
자원 그 자체를 표현 ⇒ 직관적, HTTP 그대로 계승 → 별도 작업 X 쉽게 사용할 수 있음
But, 일종의 스타일이지 표준 X → parameter와 응답값 명시적 X,
HTTP 메소드 형태 제한적 → 세부 기능 구현에는 제약
+XML, JSON
html과 같이 tag 기반이지만 미리 정의된 태그 X(no pre-defined tags) → 높은 확장성 인정
But, 복잡, 비효율적인 데이터 구조 → 속도 느림
⇒ JSON이 간결한 key-value 구조 기반으로 해결하는 듯 했지만
제공되는 자료형의 한계로 파싱 후 추가 형변환이 필요한 경우 많아짐
두 타입 모두 string 기반이라 사람이 읽기 편하지만, 데이터 전송 및 처리 → 별도의 serialization 필요
gRPC(Google Remote Procedure Call)
goolge 사에서 개발한 오픈소스 RPC 프레임워크
이전까지는 RPC 기능 지원 X, 메시지(JSON 등)을 serialize할 수 있는 프레임워크인 PB(Protocol Buffer, 프로토콜 버퍼)만 제공
⇒ PB 기반 serializer + HTTP/2 결합
REST와 비교: 기반 기술 다름, HTTP/2 사용, 프로토콜 버퍼로 데이터 전송
→ Proto File만 배포하면 환경과 프로그램 언어에 구애 X 서로 간의 데이터 통신 가능
1. HTTP
•
HTTP/1.1
◦
클라이언트의 요청이 올 때만 서버가 응답을 하는 구조 → 매 요청마다 connection 생성해야 함
◦
cookie 등 많은 메타 정보를 저장하는 무거운 header 요청마다 중복 전달 ⇒ 비효율, 느린 속도
•
HTTP/2
◦
한 connection으로 동시에 여러 개 메시지 주고 받음
◦
header 압축 → 중복 제거 후 전달 ⇒ 효율적
◦
필요 시 클라이언트 요청 X 서버가 리소스를 전달 할 수 있음 ⇒ 클라이언트 요청 최소화
2. ProtoBuf(Protocol Buffer, 프로토콜 버퍼)
google 사에서 개발한 구조화된 데이터를 직렬화(serialization)하는 기법
직렬화: 데이터 표현을 바이트 단위로 변환하는 작업
•
text 기반인 json: 82 byte 소요
•
직렬화 된 protocol buffer
◦
필드 번호, 필드 유형 등 → 1 byte 받아서 식별, 주어진 length 만큼만 읽도록 해 33 byte 필요
3. Proto File
1) Message and Field
주고 받는 data → message라는 것으로 정의
메시지는 여러가지 타입의 필드로 구성
이름은 CamelCase 형태, field 이름은 under_bar 형태로 사용 권장
field 이름은 숫자로 시작 X
2) Package
message type 이름을 중첩없이 구분할 때 사용
3) Service
RPC ~> 서버가 클라이언트에게 제공할 함수의 형태를 정의
서비스명과 RPC 메소드명 모두 CamelCase 권장
옵션 X → 단일 요청/응답으로 동작, stream 옵션 → RPC 구현할 수 있음
gRPC와 서버 간 통신
고성능과 효율성 요구할 때 많이 사용 ⇒ 마이크로서비스 아키텍처 or 분산 시스템에서 많이 사용
•
바이너리 프로토콜
◦
protobuf라는 바이너리 직렬화 방식 사용 → JSON or XML 같은 텍스트 기반 포맷에 비해 데이터 전송 속도 빠름
•
HTTP/2
◦
다중 요청/응답 스트림, 멀티플렉싱, 헤더 압축 등 기능 제공 → 클라이언트-서버 효율적인 연결 가능하게 함
•
양방향 스트리밍
◦
서버와 클라이언트 동시에 메시지 주고받을 수 있어 실시간 데이터 통신이 필요한 서비스에 적합
•
엄격한 타입 검사
◦
proto 파일 ~> 명확한 스키마 정의 ⇒ 서버 간에 주고받는 데이터 타입을 컴파일 시점에 검증할 수 있음
⇒ 타입 안정성이 보장
사용 사례
•
마이크로서비스 간의 통신
◦
여러 개의 서비스가 상호 통신해야 하는 경우가 많은데 서버 간 데이터 전송 빈번하고, 속도와 성능 중요한 경우 사용
▪
ex. 추천 시스템에서 사용자 데이터를 수집하는 서비스, 이를 기반으로 분석을 수행하는 서비스 간 통신
•
실시간 스트리밍
◦
HTTP/2 기반의 스트리밍 지원 → 실시간으로 대규모 데이터를 주고받는 서비스에 적합
▪
ex. 비디오 스트리밍, 채팅 시스템, 실시간 게임 서버 간 통신
•
다중 요청/응답 처리
◦
HTTP/2 멀티플렉싱 기능 활용 → 다중 요청 동시에 처리할 수 있음
•
클라우드 서비스 간 통신
◦
서버 간 통신 최적화
▪
ex. Goole Cloud, Kubernetes 등 클라우드 네이티브 환경
사용 이유 | 적합한 상황 | |
gRPC | - 속도
- 스트리밍 지원
- 엄격한 타입 | - 고성능과 효율성 중요
- 실시간 스트리밍 필요한 경우
- 마이크로서비스 통신 |
HTTP | - 호환성
- 사용 용이성
- 텍스트 기반 | - 서로 다른 언어 or 플랫폼에서 접근해야 하는 경우
- 브라우저 기반 통신
(대부분 gRPC 지원 X) |
단점
•
브라우저 지원 제한
◦
gRPC-Web이라는 프로토콜 사용 or 프록시 서버(Nginx, Envoy 등) 사용
•
복잡성 증가
◦
바이너리 형식의 Protocol Buffers 사용 → 추가적인 설정과 학습 필요
•
텍스트 디버깅 어려움
◦
바이너리 기반 직렬화 사용 → 데이터를 사람이 쉽게 읽거나 디버깅하기 어려움
▪
protoc 도구 사용 → 메시지 디코딩
▪
Postman과 같은 gRPC 클라이언트 사용
•
호환성 문제
•
메시지 크기 제한
◦
기본 설정에서 최대 메시지 크기 제한적 → 대용량 데이터 전송시 값을 변경해야 함
◦
(해결) gRPC 스트리밍 기능 사용 → 데이터 분할
•
클라이언트-서버 강한 결합
◦
동일한 프로토콜 정의(.proto 파일) 사용해야 함
•
표준적인 인증 및 보안 모델 부족
◦
(해결) TLS(전송 계층 보안) ~> 보안 설정 or OAuth 같은 기존 인증 방식 추가로 적용
사용하는 기업
실습
파일 구조
# grpc-client
- src
- main
- java
- com.example.grpc.client
- TestGrpcClient.java
- ClientController.java
- resources
- application.yml
- proto
- pb
- svc
- test
- test.proto
- unit
- common
- msg.proto
- item
- item.proto
Markdown
복사
# grpc-server
- src
- main
- java
- com.example.grpc.server
- TestService.java
- resources
- application.yml
- proto
- pb
- svc
- test
- test.proto
Markdown
복사
build.gradle
buildscript {
ext {
protobufVersion = '3.25.1'
protobufPluginVersion = '0.8.14'
grpcVersion = '1.58.1'
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
id 'com.google.protobuf' version '0.9.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// gRPC 관련 의존성
implementation "com.google.protobuf:protobuf-java-util:3.25.1"
implementation 'com.google.protobuf:protobuf-java:3.25.1'
// grpc-client 프로젝트에서는 아래 server을 client로 수정
implementation 'net.devh:grpc-server-spring-boot-starter:2.15.0.RELEASE'
runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}"
implementation "io.grpc:grpc-protobuf:${grpcVersion}"
implementation "io.grpc:grpc-stub:${grpcVersion}"
compileOnly 'org.apache.tomcat:annotations-api:6.0.53'
}
//tasks.named('test') {
// useJUnitPlatform()
//}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufVersion}"
}
clean {
delete generatedFilesBaseDir
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
generateProtoTasks {
all()*.plugins {
grpc{}
}
}
}
Java
복사
Proto
•
.proto 파일은 gRPC 서비스의 계약을 정의하는 파일
•
클라이언트-서버: 어떤 서비스와 메시지가 사용되는지에 대한 명세 제공
test.proto
syntax = "proto3";
// .proto 파일의 패키지 네임스페이스 정의, 패키지 ~> 메시지와 서비스 구분
package pb.svc.test;
// Java 코드 생성 시, .proto 파일에서 생성된 클래스들이 패키지에 속하도록 설정
option java_package="com.example.pb.svc.test";
// message 및 service가 별도의 Java 파일로 생성되도록 함
option java_multiple_files=true;
// 외부의 .proto 파일(msg.proto) 가져와 사용할 수 있게 함
import "pb/unit/common/msg.proto";
// 클라이언트로부터 GreetingReq 메시지 받아 GreetingRes 메시지로 응답
service Test {
rpc Greeting(GreetingReq) returns (GreetingRes);
}
// 클라리언트 -> 서버: 요청 메시지
message GreetingReq {
string some = 1;
}
// 서버 -> 클라이언트: 응답 메시지
message GreetingRes {
unit.common.ReturnMsg result = 1;
}
Protobuf
복사
msg.proto
syntax = "proto3";
// .proto 파일의 패키지 정의, 다른 .proto 파일들에서 ReturnMsg 메시지 가져다 사용할 수 있음
package unit.common;
option java_package="com.example.pb.unit.common";
option java_multiple_files=true;
message ReturnMsg {
string message = 1; // 응답시 사용하는 메시지
int32 code = 2; // 응답의 상태 코드(200: 성공, 400: 실패)
}
Protobuf
복사
Client
동기 방식의 gRPC 호출을 통해 서버로부터 데이터를 수신한 후, 결과를 반환하는 방식
1.
@GrpcClient ~> 서버와 연결, 클라이언트 스텁(TestBlockingStub) → 서버로 요청 전송
2.
GreetingReq 메시지를 생성하고 서버로 보내면, GreetingRes 메시지 ~> 서버 응답 받음
// application.yml
spring:
application:
name: grpc-client
server:
port: 8080
grpc:
client:
test:
address: ${TEST_ADDR:localhost:9090}
enable-keep-alive: true
keep-alive-without-calls: true
negotiation-type: plaintext
Java
복사
TestController
package com.example.grpcclient;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
public class TestController {
private final TestService testService;
@GetMapping("/greet")
public String greet(@RequestParam String name) {
return testService.sendGreeting(name);
}
}
Java
복사
TestService
package com.example.grpcclient;
import com.example.pb.svc.test.*;
import lombok.RequiredArgsConstructor;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class TestService {
@GrpcClient("test")
private TestGrpc.TestBlockingStub testStub;
public String sendGreeting(String message) {
GreetingReq request = GreetingReq.newBuilder()
.setSome(message)
.build();
GreetingRes response = testStub.greeting(request);
return response.getResult().getMessage();
}
}
Java
복사
•
TestBlockingStub은 =동기식 호출을 지원하는 클라이언트 스텁
◦
서버로부터 응답을 받을 때까지 대기하는 방식
Server
•
클라이언트로부터 요청 받고, 응답 메시지 생성해 반환하는 역할 수행
•
@GrpcService 어노테이션 ~> gRPC 서버로서 동작
// application.yml
spring:
application:
name: grpc-server
main:
web-application-type: none
grpc:
server:
port: 9090
Java
복사
TestService
package com.example.grpc_server;
import com.example.pb.svc.test.*;
import com.example.pb.unit.common.ReturnMsg;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;
@GrpcService
public class TestService extends TestGrpc.TestImplBase{
@Override
public void greeting(GreetingReq request, StreamObserver<GreetingRes> responseObserver) {
String received = request.getSome();
System.out.println("Received: " + received);
// 응답 메시지 생성
ReturnMsg returnMsg = ReturnMsg.newBuilder()
.setMessage("Hello, " + received)
.setCode(200)
.build();
GreetingRes response = GreetingRes.newBuilder()
.setResult(returnMsg)
.build();
// 클라이언트로 응답 전송
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
Java
복사