SpringBoot3.3.1에 gRPC를 적용시켜 보자
📌 서론
개인적인 공부를 하면서 MSA 프로젝트를 구성할 때 유저(Client)의 기본적인 요청에 대해서 먼저 http로 받아서 처리하고 만약 서버 간 소통이 필요하다면 그때는 gRPC를 사용하도록 하기 위해 gRPC를 도입해보고자 했다.
아무것도 모르는 상태에서 공부를 시작했고 일단 도입해 보면서 발전시켜 가자는 목표로 코드에 1차 적용을 시켰다.
막상 적용시켜 보니 버전에 대해서 이슈가 조금 있어서 생각보다 오래 걸렸지만 잘 동작하는 것을 보니 매우 뿌듯했다.
혹시나 gRPC를 도입하고자 하는 분들을 위해 적용방법을 남기고 가능하면 조금 더 좋은 방법이 있다면 도움을 받고 싶다.
예시 프로젝트는 아래의 github에 만들어 두었습니다. (편하게 사용해주시면 됩니다.)
1. build.gradle에 gRPC 관련 의존성 추가하기
프로젝트 설명
- grpc 버전은 maven repository에서 가장 최신 버전을 기준으로 가져왔다. (24.07.21 기준)
- SpringBoot 3.3.4, java 21, gRPC, JPA, h2, mapstruct, lombok을 사용한 간단한 예시 프로젝트이다.
전체 build.gradle
buildscript {
// 24.07.21 기준 최신 버전
ext {
protobufVersion = '4.27.2'
protobufPluginVersion = '0.9.4'
grpcVersion = '1.65.1'
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
// Protobuf 플러그인을 적용하여 .proto 파일을 컴파일할 수 있다. 여기서 버전은 ext에 정의된 protobufPluginVersion을 사용한다.
id 'com.google.protobuf' version "${protobufPluginVersion}"
}
group = 'com.demo'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// spring
implementation 'org.springframework.boot:spring-boot-starter-web'
// data jpa, h2
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// mapstruct 설정
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
// grpc 프로토콜 버터를 사용하기 위한 핵심 라이브러리 (Protobuf 메시지의 직렬화 및 역직렬화를 지원합니다.)
implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}"
implementation "com.google.protobuf:protobuf-java:${protobufVersion}"
// grpc 서버, 클라이언트 설정
implementation 'net.devh:grpc-spring-boot-starter:3.1.0.RELEASE' // Spring Boot와 gRPC의 통합을 간편하게 도와주는 스타터
implementation "io.grpc:grpc-netty-shaded:${grpcVersion}" // Netty Shaded 사용(gRPC 서버와 클라이언트의 Netty 전송 계층을 제공)
implementation "io.grpc:grpc-protobuf:${grpcVersion}" // Protobuf 메시지와 gRPC의 통합을 지원
implementation "io.grpc:grpc-stub:${grpcVersion}" // gRPC 클라이언트 스텁을 생성
compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // 이걸 추가해야 gRPC 컴파일시 javax 어노테이션 오류가 발생하지 않는다.
}
protobuf {
// Protobuf 컴파일러를 지정하여 .proto 파일을 컴파일합니다.
protoc {
artifact = "com.google.protobuf:protoc:${protobufVersion}"
}
// 생성된 파일을 정리합니다.
clean {
delete generatedFilesBaseDir
}
// gRPC 플러그인을 설정하여 Protobuf 파일로부터 gRPC 관련 코드를 생성합니다.
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
// 모든 프로토콜 버퍼 작업에 대해 gRPC 플러그인을 적용합니다.
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
tasks.named('test') {
useJUnitPlatform()
}
다음으로 application.yml에 서버 설정 코드를 작성한다.
- 스프링은 8090 포트로 실행하고 grpc는 50051번 포트를 사용한다.
# 스프링 서버 포트
server:
port: 8090
# gRPC 서버 포트
grpc:
server:
port: 50051
2. build.gradle - dependencies 이해하기
각각의 의존성 및 역할을 이해해 보자
1. protobuf-java-util & protobuf-java
- 역할: Protobuf 메시지의 직렬화 및 역직렬화를 지원한다. Protobuf는 데이터를 효율적으로 직렬화하기 위한 Google의 데이터 인터페이스 언어다.
- protobuf-java-util: Protobuf 메시지의 직렬화 및 역직렬화를 지원하는 유틸리티 라이브러리다.
- protobuf-java: Protobuf 메시지의 기본 직렬화 및 역직렬화를 지원하는 라이브러리다.
implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}"
implementation "com.google.protobuf:protobuf-java:${protobufVersion}"
2. grpc-spring-boot-starter
- 역할: Spring Boot와 gRPC의 통합을 간편하게 도와주는 starter 라이브러리다. 이 라이브러리는 gRPC 서버와 클라이언트를 쉽게 설정할 수 있도록 Spring Boot와 gRPC의 자동 구성을 제공한다.
implementation 'net.devh:grpc-spring-boot-starter:3.1.0.RELEASE'
3. grpc-netty-shaded
- 역할: gRPC 서버와 클라이언트의 Netty 전송 계층을 제공한다. Netty는 비동기 이벤트 주도 네트워크 애플리케이션 프레임워크로, 높은 성능과 유연성을 제공한다.
- 주의사항: 여기서 grpc-netty-shaded: 는 모든 종속성이 포함된 Netty 버전으로, 종속성 충돌 문제를 줄이는 데 도움이 된다. (최신 버전에서 shaded가 아닌 그냥 netty 의존성을 사용했더니 오류가 발생했다.)
implementation "io.grpc:grpc-netty-shaded:${grpcVersion}"
4. grpc-protobuf
- 역할: Protobuf 메시지와 gRPC의 통합을 지원한다. Protobuf를 사용하여 gRPC 서비스 정의 파일(.proto 파일)을 기반으로 하는 메시지와 서비스를 생성할 수 있다.
implementation "io.grpc:grpc-protobuf:${grpcVersion}"
5. grpc-stub
- 역할: gRPC 클라이언트 스텁을 생성한다. 클라이언트 스텁은 클라이언트가 서버의 gRPC 서비스를 호출할 수 있게 하는 간편한 인터페이스를 제공한다. (추후 스텁을 활용해서 편리하게 gRPC 클라이언트에서 요청을 보낼 수 있다.)
implementation "io.grpc:grpc-stub:${grpcVersion}"
6. annotations-api
- 역할: gRPC 컴파일 시 javax 어노테이션 오류를 방지한다. 일부 gRPC 및 Protobuf 코드 생성기는 javax 어노테이션을 사용하며, 이 의존성이 필요하다.
- 사용 이유: 컴파일 시 발생하는 어노테이션 관련 오류를 방지하기 위해 추가한다. compileOnly로 지정하여 컴파일 타임에만 필요하고, 런타임에는 포함되지 않도록 한다.
compileOnly 'org.apache.tomcat:annotations-api:6.0.53'
- 만약 위의 의존성을 받지 않고 빌드를 하면 이 오류가 발생한다. 사진을 살펴보면 @javax.annotation.Generated가 있는데 스프링 최신버전에서는 javax가 아니라 jakarta를 사용한다. 근데 아직 이게 지원되지 않아서 그런 것 같다. 그래서 가능하다면 위의 의존성을 build.gradle에 추가시켜 주면 잘 동작한다. (다른 방법이 있을 수도 있다.)
의존성 내용 요약
- protobuf-java-util & protobuf-java: Protobuf 메시지의 직렬화 및 역직렬화 지원.
- grpc-spring-boot-starter: Spring Boot와 gRPC의 간편한 통합을 지원하는 스타터.
- grpc-netty-shaded: Netty 전송 계층을 제공하며, 모든 종속성이 포함된 버전으로 종속성 충돌 문제를 줄임.
- grpc-protobuf: Protobuf와 gRPC의 통합을 지원.
- grpc-stub: gRPC 클라이언트 스텁을 생성.
- annotations-api: javax 어노테이션 관련 컴파일 오류 방지. (빌드시 컴파일 오류 해결)
3. build.gradle - Protobuf 이해하기
Protobuf 설정의 역할 이해하기
- build.gradle 파일의 protobuf 블록은 Protobuf 파일을 컴파일하고, gRPC 코드를 생성하는 작업을 정의한다.
- 이 작업은 IntelliJ IDEA의 Gradle Tasks에서 extractProto, generateProto 등과 관련이 있다.
- 이 설정은 Gradle 빌드 프로세스의 일부로, 빌드할 때 자동으로 실행된다.
protobuf {
// Protobuf 컴파일러를 지정하여 .proto 파일을 컴파일합니다.
protoc {
artifact = "com.google.protobuf:protoc:${protobufVersion}"
}
// 생성된 파일을 정리합니다.
clean {
delete generatedFilesBaseDir
}
// gRPC 플러그인을 설정하여 Protobuf 파일로부터 gRPC 관련 코드를 생성합니다.
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
// 모든 프로토콜 버퍼 작업에 대해 gRPC 플러그인을 적용합니다.
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
1. Protobuf 컴파일러 설정 (protoc)
- 역할: Protobuf 컴파일러를 지정한다. 이 컴파일러는 .proto 파일을 읽고 Java 소스 코드로 변환한다.
- 실행 시점: 빌드 스크립트가 로드될 때 설정된다. 이 설정은 컴파일러가 어떤 것인지 명시하는 단계다.
protoc {
artifact = "com.google.protobuf:protoc:${protobufVersion}"
}
2. 생성된 파일 정리 (clean)
- 역할: Protobuf 컴파일 시 생성된 파일을 정리한다. 이전 빌드에서 생성된 파일들을 삭제하여 빌드 환경을 깨끗하게 유지하기 위해 사용된다.
- 실행 시점: Gradle의 clean 작업이 실행될 때 작동한다. 즉, ./gradlew clean 명령이 실행될 때만 이 블록이 실행되어 생성된 파일을 삭제한다. 일반적인 빌드 작업(./gradlew build)에서는 실행되지 않는다.
clean {
delete generatedFilesBaseDir
}
3. gRPC 플러그인 설정 (plugins)
- 역할: .proto 파일로부터 gRPC 관련 코드를 생성할 gRPC 플러그인을 설정한다.
- 실행 시점: 빌드 스크립트가 로드될 때 설정된다. 이 설정은 어떤 플러그인을 사용할지 명시하는 단계다.
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
4. 모든 Protobuf 작업에 gRPC 플러그인 적용 (generateProtoTasks)
- 역할: 모든 Protobuf 컴파일 작업에 대해 gRPC 플러그인을 적용한다. 실제로 코드를 컴파일하여 생성하는 단계다.
- 실행 시점: ./gradlew build 명령이 실행될 때 실제로 .proto 파일을 컴파일하여 gRPC 관련 코드를 생성한다.
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
빌드 스크립트 실행 예시
1. 빌드만 실행 (./gradlew build)
- protoc 설정: 지정된 Protobuf 컴파일러 사용.
- plugins 설정: gRPC 플러그인 설정.
- generateProtoTasks: .proto 파일을 컴파일하여 gRPC 관련 코드를 생성.
2. 클린 후 빌드 실행 (./gradlew clean build)
- clean 블록: 생성된 파일을 삭제.
- protoc 설정: 지정된 Protobuf 컴파일러 사용.
- plugins 설정: gRPC 플러그인 설정.
- generateProtoTasks: .proto 파일을 컴파일하여 gRPC 관련 코드를 생성.
3. IntelliJ에서 Build 버튼을 누를 때
- Build 버튼: 기본적으로 ./gradlew build 명령을 실행한다. 이 경우 clean 블록은 실행되지 않는다.
- Rebuild Project: IntelliJ의 Build > Rebuild Project 옵션을 선택하면 ./gradlew clean build와 유사하게 동작하여 클린 작업 후 빌드가 수행된다.
내용 요약
- protoc 설정: 어떤 Protobuf 컴파일러를 사용할지 정의한다.
- clean 설정: Gradle의 clean 작업이 실행될 때만 작동하여 이전 빌드 결과물을 삭제한다.
- plugins 설정: gRPC 플러그인을 설정하여 .proto 파일로부터 gRPC 관련 코드를 생성할 준비를 한다.
- generateProtoTasks 설정: 실제로 .proto 파일을 컴파일하여 gRPC 관련 Java 코드를 생성한다.
4. proto파일 생성 및 이해하기
우리가 gRPC를 사용하려면 .proto 파일을 작성해줘야 한다.
- 일반적으로 우리가 SpringBoot 프로젝트를 생성하면 (start.spring.io 사용) src 패키지 하위에 main, resources가 만들어지는데 이 패키지들과 같은 경로에 proto패키지를 하나 만들어준다. (하단의 tree 확인)
- proto패키지 하위에는 member.proto라는 파일을 만들어준다. (파일 확장자가 .proto다.)
└── src
├── main
│ ├── java
│ ├── proto
│ └── member.proto
│ ├── resources
├── test
member.proto 파일 작성하기
- .proto 파일은 gRPC 및 Protobuf를 사용하여 서비스와 메시지 구조를 정의하는 데 사용된다.
syntax = "proto3";
// 여기에 내가 원하는 패키지명을 적는다. 아래는 예시이며 실제 프로젝트 패키지를 잘 보면서 수정하자.
option java_package = "com.test.member.grpc";
option java_outer_classname = "MemberProto";
service MemberService {
rpc CreateMember (MemberRequest) returns (MemberCreateResponse);
}
// 멤버 생성 요청 객체
message MemberRequest {
int64 id = 1;
string email = 2;
string password = 3;
string name = 4;
}
// 멤버 생성 응답
message MemberCreateResponse {
int64 id = 1;
string email = 2;
string password = 3;
string name = 4;
}
.proto 파일 구성 요소 설명
1. syntax = "proto3";
- Protobuf의 버전을 지정한다. 여기서는 proto3을 사용하고 있다. Protobuf 버전 3의 문법을 따르겠다는 의미다.
- 최신 버전에서는 edition 문법을 사용한다고 한다. 그러나 아직 나는 proto3을 사용 중이며 추후 업데이트를 해볼 생각이다.
2. option java_package = "com.test.member.grpc";
- Protobuf 컴파일러가 생성한 Java 클래스의 패키지명을 설정한다. 이를 통해 생성된 클래스가 특정 패키지에 포함되도록 한다.
3. option java_outer_classname = "MemberProto";
- 모든 Protobuf 메시지 및 서비스 정의를 포함하는 외부 클래스의 이름을 설정한다.
- 이 예제에서는 MemberProto라는 클래스 이름을 지정했다.
4. service MemberService { ... }
- MemberService라는 이름의 gRPC 서비스 인터페이스를 정의한다. 이 서비스에는 CreateMember라는 RPC 메서드가 포함되어 있다. 이 메서드는 MemberRequest 메시지를 받고 MemberCreateResponse 메시지를 반환한다.
5. rpc CreateMember (MemberRequest) returns (MemberCreateResponse);
- RPC 메서드를 정의한다. 여기서 정의한 CreateMember 메서드는 클라이언트가 MemberRequest 메시지를 서버로 보내면 서버는 MemberCreateResponse 메시지를 반환한다.
6. message MemberRequest { ... }
- 요청 메시지 구조를 정의한다. MemberRequest 메시지는 멤버 생성 요청에 필요한 데이터를 포함한다. 각 필드는 데이터 타입과 번호를 가지며, 이는 Protobuf 메시지의 기본 구성 요소다.
- int64 id = 1; id 필드는 64비트 정수형
- string email = 2; email 필드는 문자열
- string password = 3; password 필드는 문자열
- string name = 4; name 필드는 문자열
7. message MemberCreateResponse { ... }
- 응답 메시지 구조를 정의한다. MemberCreateResponse 메시지는 멤버 생성 응답 데이터를 포함한다. 각 필드는 요청 메시지와 동일한 구조를 가진다.
- int64 id = 1; 생성된 멤버의 id
- string email = 2; 생성된 멤버의 email
- string password = 3; 생성된 멤버의 password
- string name = 4; 생성된 멤버의 name
5. Request, Response에 대해서 조금 더 자세히 알아보자
- Protobuf 메시지 정의에서 필드 번호(예: 1, 2, 3, 4)는 필드의 고유 식별자로, Protobuf 메시지의 바이너리 형식에서 필드를 구별하는 데 사용된다. 이를 통해 Protobuf는 메시지를 효율적으로 직렬화하고 역직렬화할 수 있다.
// 멤버 생성 요청 객체
message MemberRequest {
int64 id = 1;
string email = 2;
string password = 3;
string name = 4;
}
// 멤버 생성 응답
message MemberCreateResponse {
int64 id = 1;
string email = 2;
string password = 3;
string name = 4;
}
필드 번호의 역할
1. 고유 식별자:
- 각 필드는 고유한 번호를 가져야 하며, 이 번호는 메시지 내에서 해당 필드를 고유하게 식별한다.
- 필드 번호는 유니크해야 한다. (같은 메시지 내에서 각 필드 번호는 고유해야 한다.)
- 예약된 필드 번호를 피해야 한다. (Protobuf는 19000에서 19999 사이의 필드 번호를 예약해 두었다. 이 범위는 사용자 정의 메시지에서 사용하지 않도록 한다.)
2. 효율적 인코딩:
- Protobuf는 필드 번호를 사용하여 데이터를 바이너리 형식으로 효율적으로 인코딩한다. 번호가 작을수록 데이터의 크기가 작아진다.
- 필드 번호가 작을수록 와이어 타입과 함께 효율적으로 인코딩 된다. 예를 들어, 필드 번호가 1부터 15 사이인 경우, 단일 바이트로 인코딩 된다.
3. 변경 가능성: 필드 번호는 변경하지 않는 한 메시지의 구조를 변경해도 호환성을 유지할 수 있다.
필드 번호 정리
- 필드 번호는 작게 설정하여 효율적인 인코딩을 도모하되, 실제 데이터 크기는 필드에 저장된 데이터 자체에 따라 결정된다. 필드 번호를 수정할 필요는 없지만, 새로운 필드를 추가할 때는 번호를 신중하게 선택하여 기존 메시지와의 호환성을 유지하는 것이 중요하다.
예시 설명
- 이 예제에서 필드 번호는 1에서 4로 작게 설정되어 있으며, 이는 Protobuf가 내부적으로 필드를 직렬화할 때 효율적으로 인코딩할 수 있도록 한다. 그러나 필드 내부에 저장된 데이터 자체(예: password 필드의 문자열 길이)의 크기는 데이터 자체 내용에 의해 결정된다. (필드 번호와 데이터 자체 크기의 관계는 X)
- 각 데이터 타입에 대해서는 검색해서 알아보도록 하자. (int64, string 등등)
message MemberRequest {
int64 id = 1; // 필드 번호 1: 작은 번호, 효율적 인코딩
string email = 2; // 필드 번호 2: 작은 번호, 효율적 인코딩
string password = 3; // 필드 번호 3: 작은 번호, 효율적 인코딩
string name = 4; // 필드 번호 4: 작은 번호, 효율적 인코딩
}
java 객체와 proto파일로 선언하는 DTO는 뭐가 다를까?
- 우리가 잘 알고 있는 자바의 DTO 객체는 다음과 같이 선언된다. (클래스로 생성)
/**
* 회원 생성 DTO
*/
@Data
public class MemberSignUpRequestDTO {
private Long id; // PK
private String email; // 이메일
private String password; // 소셜 로그인 사용자는 NULL일 수 있음
private String name; // 이름
}
- 하지만 proto파일에서는 자바의 객체처럼 선언하지 않고 DTO를 이렇게 정의한다. (proto 파일에 정의)
// 멤버 생성 요청 객체
message MemberRequest {
int64 id = 1;
string email = 2;
string password = 3;
string name = 4;
}
- 응답 DTO 또한 이렇게 선언한다.
// 멤버 생성 응답
message MemberCreateResponse {
int64 id = 1;
string email = 2;
string password = 3;
string name = 4;
}
6. gRPC서버 구현하기
.proto파일을 작성했다면 gradle을 통해 build를 한번 해줘야 한다.
- intelliJ gradle 선택 > Tasks > other > generateProto 이것을 실행해 주면 된다. (명령어도 가능하다.)
- 일반적으로 grpc 코드를 작성한 다음 (proto파일) 바로 generateProto를 해주는 게 좋다. 그래야 바로바로 grpc 서비스 코드도 상속을 받아서 구현이 가능하다.
유저 엔티티 생성하기
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(name = "password")
private String password;
@Column(name = "name")
private String name;
}
DTO 선언하기
import lombok.Data;
@Data
public class MemberSignUpRequestDTO {
private Long id;
private String email;
private String password;
private String name;
}
MapStruct를 사용하여 Mapper 인터페이스 선언하기
/**
* componentModel="spring"을 통해서 spring container에 Bean으로 등록 해 준다. (외부에서 주입받아서 사용하면 된다.)
* unmappedTargetPolicy IGNORE 만약, target class에 매핑되지 않는 필드가 있으면, null로 넣게 되고, 따로 report하지 않는다.
*/
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MemberMapper {
// gRPC request를 DTO로 변환
MemberSignUpRequestDTO requestProtoToDto(MemberProto.MemberRequest member);
// Entity를 gRPC response로 변환
MemberProto.MemberCreateResponse dtoToResponseProto(Member createdMember);
// DTO를 Entity로 변환
Member dtoToEntity(MemberSignUpRequestDTO memberDTO);
}
generateProto로 생성된 gRPC 클래스를 상속받아서 서버로직 구현하기
- 먼저 위에서 작성한 proto파일을 컴파일하면 (generateProto를 실행했다면) proto 파일 내부의 Service에 선언한 이름대로 MemberServiceGrpc라는 클래스가 만들어진다. 나는 이 클래스 안에 선언된 MemberServiceImplBase 클래스를 상속받아서 원하는 동작을 구현하면 된다. (@Override로 메서드를 구현할 수 있다.)
- 클래스 상단에는 @GrpcService를 적어준다.
@Slf4j
@RequiredArgsConstructor
@GrpcService
public class MemberServiceGrpcImpl extends MemberServiceGrpc.MemberServiceImplBase {
private final MemberService memberService;
private final MemberMapper memberMapper;
@Override
public void createMember(
MemberProto.MemberRequest request,
StreamObserver<MemberProto.MemberCreateResponse> responseObserver
) {
// 1. 클라이언트로부터 전달받은 request 데이터를 DTO로 변환한다.
MemberSignUpRequestDTO memberDTO = memberMapper.requestProtoToDto(request);
// 2. 서비스 레이어에서 request 데이터를 사용해서 RDB에 저장하는 로직을 수행하고 결과를 받는다.
Member createdMember = memberService.createMember(memberDTO);
// 3. RDB에 저장된 데이터를 gRPC response 데이터로 변환한다.
MemberProto.MemberCreateResponse response = memberMapper.dtoToResponseProto(createdMember);
// 4. 응답을 클라이언트에게 전달한다.
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
- 위의 createMember 메서드를 구현할 때 사용되는 첫 번째 매개변수 MemberProto.MemberRequest가 바로 .proto파일에서 작성해 줬었던 request다. (아래의 Request)
// 멤버 생성 요청 객체
message MemberRequest {
int64 id = 1;
string email = 2;
string password = 3;
string name = 4;
}
위에서 사용된 서비스 로직은 다음과 같다. (간단하게 h2 db에 유저 정보를 저장한다.)
@RequiredArgsConstructor
@Service
public class MemberService {
private final MemberRepository memberRepository;
private final MemberMapper memberMapper;
public Member createMember(MemberSignUpRequestDTO memberDTO) {
Member member = memberMapper.dtoToEntity(memberDTO);
return memberRepository.save(member);
}
}
repository는 Jpa를 선언한다.
package com.demo.grpc.repository;
import com.demo.grpc.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
}
7. gRPC 사용해 보기 (클라이언트를 사용한 요청)
아래와 같이 호출하면 된다. (protobufVersion을 최신(4.27.2)으로 사용했더니 뭔가 조합이 안 맞는지 grpcurl이나 intelliJ의 .http로 직접 호출했더니 오류가 발생하면서 gRPC가 동작하지 않았습니다. 해결방법을 알고 계신 분은 댓글 부탁드립니다 ㅠㅠㅠ)
intelliJ의 .http파일을 활용한다. (grpcurl을 사용해도 된다.)
### CreateMember
GRPC localhost:50051/MemberService/CreateMember
{
"email": "tes2t25s2@example.com",
"password": "1234",
"name": "Test User",
}
또는 gRPC 클라이언트 클래스를 작성하고 사용하고자 하는 로직에서 호출하면 된다. (이 방법으로 테스트 진행)
- 아래의 코드를 프로젝트에 작성한다. (msa 환경에서는 다른 서버에서 아래의 클라이언트 코드를 작성하고 호출한다.)
- 여기서 grpc의 stub이 사용된다. MemberServiceBlockingStub을 주입받아서 사용한다. (코드 내부에 들어가 보는것을 추천)
- 생성자를 직접 선언하고 NettyChannelBuilder를 통해 application.yml에 설정해 준 localhost:50051 포트에 채널을 연결시켜 주고 이것을 반환한 다음 stub에 연결시켜 준다.
/**
* gRPC 클라이언트
* GrpcClient 클래스는 gRPC 클라이언트를 구현한 것으로, 다른 서버 또는 같은 서버 내에서 gRPC 서버 메서드를 호출하는 데 사용됩니다.
* 이 클래스는 애플리케이션 내에서 gRPC 서버에 요청을 보내는 역할을 합니다.
*/
@Component
public class GrpcMemberClient {
private final MemberServiceGrpc.MemberServiceBlockingStub blockingStub;
// gRPC 서버에 연결 (생성자)
public GrpcMemberClient() {
ManagedChannel channel = NettyChannelBuilder.forAddress("localhost", 50051)
.usePlaintext()
.build();
blockingStub = MemberServiceGrpc.newBlockingStub(channel);
}
/**
* 회원 생성
*
* @param request
* @return
*/
public MemberProto.MemberCreateResponse createMember(MemberProto.MemberRequest request) {
return blockingStub.createMember(request);
}
}
사용하고자 하는 서버에서 GrpcMemberClient 클래스를 주입받아서 createMember() 메서드를 호출하면 된다.
- 예를 들면 나 같은 경우는 Kafka 메시지를 받으면 gRPC 클라이언트를 호출하도록 했다.
- feignClient를 호출하듯이 다른 서버에서 호출하는 방식으로 구현하면 된다.
예시를 통해 알아보자 (같은 프로젝트에서 컨트롤러를 만들고 gRPC 클라이언트를 호출하도록 했다.)
- 다른 서버에서 호출할 때도 아래 코드에서 사용하는 방식처럼 선언한 grpcMemberClient 클래스를 주입받아서 그 안에 선언한 메서드를 호출하면 된다. (Feign 호출과 비슷하다.)
- 꼭 컨트롤러에서 호출할 필요는 없고 서비스 로직에서 호출해서 데이터를 gRPC로 받아와서 처리하거나 이런 식으로 사용하면 된다.
@RequestMapping("/member")
@RequiredArgsConstructor
@RestController
public class MemberController {
private final GrpcMemberClient grpcMemberClient;
@PostMapping("/create")
public String createMember() {
MemberProto.MemberCreateResponse member = grpcMemberClient.createMember(
MemberProto.MemberRequest
.newBuilder()
.setEmail("test")
.setName("test")
.setPassword("test")
.build()
);
return member.toString();
}
}
이제는 intelliJ의 .http 파일이나 postman을 사용해서 요청을 진행한다.
- 내가 만든 예시 프로젝트의 경우 intelliJ의 .http 파일을 사용해서 요청을 보낸다. (아래의 예시)
### 멤버 조회
POST http://localhost:8090/member/create
만약 msa 서버에서 호출을 하고 싶다면?
- 다른 msa서버에서도 동일한 .proto 파일을 생성하고 generateProto로 컴파일을 한 다음 위와 같이 클라이언트를 선언하면 된다. (메인 서버에서는 gRPC Service(Impl)를 꼭 구현해야 하고 호출할 서버에서는 gRPC 클라이언트 (stub)을 사용하면 된다.)
- 참고로 gRPC 클라이언트를 작성하고 생성자를 만들 때 gRPC 서버에 연결해 줄 채널 설정만 잘해주면 되는데 이때 NettyChannelBuilder로 forAddress를 설정하는 과정에서 서버 역할을 하는 msa 서버에 적어준 application.yml의 설정 내용 (예를 들면 ip주소와 gRPC 서버의 포트는 50051번) 이것을 정확하게 channel 설정 코드에 적어줘야만 클라이언트가 gRPC 서버에 연결하여 호출하게 된다.
'gRPC' 카테고리의 다른 글
개발자를 위한 gRPC 기본 개념 (0) | 2024.10.31 |
---|---|
[gRPC] SpringBoot3 gRPC 예외 인터셉터 적용 (0) | 2024.07.27 |