gRPC 개요
gRPC는 HTTP/2와 Protocol Buffers를 기반으로 한 고성능 RPC 프레임워크로, 양방향 스트리밍과 다중 언어 코드 생성을 지원합니다. REST JSON 대비 3-5배 작은 직렬화 크기와 30% 이상의 지연 시간 감소로 마이크로서비스 통신에 적합합니다.
프로젝트 구조
grpc-example/ ├── grpc-api/ # Proto 정의 + 생성 코드 │ └── src/main/proto/ │ └── member.proto ├── grpc-server/ # 서버 └── grpc-client/ # 클라이언트
Proto 파일 정의
syntax = "proto3";
package com.example.grpc;
option java_package = "com.example.grpc.member";
option java_outer_classname = "MemberProto";
service MemberService {
rpc FetchMember (FetchMemberReq) returns (MemberRes);
rpc StreamMembers (MemberListReq) returns (stream MemberRes);
rpc BatchCreate (stream CreateMemberReq) returns (BatchCreateRes);
rpc LiveChat (stream ChatMsg) returns (stream ChatMsg);
}
message FetchMemberReq {
int32 member_id = 1;
}
message MemberRes {
int32 member_id = 1;
string full_name = 2;
string contact = 3;
int32 years = 4;
}
message MemberListReq {
int32 page_num = 1;
int32 page_size = 2;
}
message CreateMemberReq {
string full_name = 1;
string contact = 2;
int32 years = 3;
}
message BatchCreateRes {
int32 total_created = 1;
repeated MemberRes members = 2;
}
message ChatMsg {
string sender = 1;
string content = 2;
}
Maven 의존성 설정
<dependencies>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.62.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.25.3:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.62.2:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals><goal>compile</goal><goal>compile-custom</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
서버 구현
@GrpcService
public class MemberGrpcService extends MemberServiceGrpc.MemberServiceImplBase {
@Autowired
private MemberRepo repository;
@Override
public void fetchMember(FetchMemberReq req, StreamObserver<MemberRes> obs) {
Member data = repository.findById(req.getMemberId())
.orElseThrow(() -> new StatusRuntimeException(Status.NOT_FOUND));
obs.onNext(buildResponse(data));
obs.onCompleted();
}
@Override
public void streamMembers(MemberListReq req, StreamObserver<MemberRes> obs) {
repository.findAll(PageRequest.of(req.getPageNum(), req.getPageSize()))
.forEach(member -> obs.onNext(buildResponse(member)));
obs.onCompleted();
}
@Override
public StreamObserver<CreateMemberReq> batchCreate(StreamObserver<BatchCreateRes> obs) {
return new StreamObserver<CreateMemberReq>() {
List<Member> newMembers = new ArrayList<>();
public void onNext(CreateMemberReq req) {
newMembers.add(repository.save(
new Member(req.getFullName(), req.getContact(), req.getYears())
));
}
public void onError(Throwable t) { }
public void onCompleted() {
BatchCreateRes res = BatchCreateRes.newBuilder()
.setTotalCreated(newMembers.size())
.addAllMembers(newMembers.stream()
.map(this::buildResponse)
.collect(Collectors.toList()))
.build();
obs.onNext(res);
obs.onCompleted();
}
};
}
private MemberRes buildResponse(Member m) {
return MemberRes.newBuilder()
.setMemberId(m.getId())
.setFullName(m.getName())
.setContact(m.getEmail())
.setYears(m.getAge())
.build();
}
}
클라이언트 호출
@Service
public class MemberGrpcClient {
@GrpcClient("member-service")
private MemberServiceGrpc.MemberServiceBlockingStub syncStub;
@GrpcClient("member-service")
private MemberServiceGrpc.MemberServiceStub asyncStub;
public MemberRes fetchMember(int id) {
return syncStub.fetchMember(FetchMemberReq.newBuilder().setMemberId(id).build());
}
public List<MemberRes> streamMembers(int page, int size) {
Iterator<MemberRes> results = syncStub.streamMembers(
MemberListReq.newBuilder().setPageNum(page).setPageSize(size).build());
return StreamSupport.stream(
Spliterators.spliteratorUnknownSize(results, Spliterator.ORDERED), false)
.collect(Collectors.toList());
}
public void batchCreateMembers(List<CreateMemberReq> requests) {
StreamObserver<BatchCreateRes> resObs = new StreamObserver<>() {
public void onNext(BatchCreateRes res) {
System.out.println("생성된 회원 수: " + res.getTotalCreated());
}
public void onError(Throwable t) { t.printStackTrace(); }
public void onCompleted() { }
};
StreamObserver<CreateMemberReq> reqObs = asyncStub.batchCreate(resObs);
requests.forEach(reqObs::onNext);
reqObs.onCompleted();
}
}
클라이언트 구성
# application.yml
grpc:
client:
member-service:
address: static://localhost:9090
negotiationType: plaintext
keepAlive:
time: 30s
timeout: 10s
REST 게이트웨이 통합
@RestController
@RequestMapping("/api/members")
public class MemberApiController {
@Autowired
private MemberGrpcClient grpcClient;
@GetMapping("/{id}")
public Map<String, Object> getMember(@PathVariable int id) {
MemberRes res = grpcClient.fetchMember(id);
return Map.of("id", res.getMemberId(),
"name", res.getFullName(),
"contact", res.getContact(),
"age", res.getYears());
}
@GetMapping
public List<Map<String, Object>> listMembers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return grpcClient.streamMembers(page, size).stream()
.map(res -> Map.of("id", res.getMemberId(),
"name", res.getFullName(),
"contact", res.getContact(),
"age", res.getYears()))
.collect(Collectors.toList());
}
}
성능 비교
| 지표 | REST/JSON | gRPC/Protobuf |
|---|---|---|
| 직렬화 크기 | ~2.1KB | ~0.4KB |
| 평균 지연 | 45ms | 12ms |
| QPS (단일 연결) | ~3,200 | ~9,500 |
모범 사례
- Proto 버전 관리: buf 도구를 사용한 스키마 변경 관리
- 오류 처리: gRPC 상태 코드를 활용한 일관된 오류 전달
- 시간 제한: Deadline 설정을 통한 연쇄 타임아웃 방지
- 로드 밸런싱: 서비스 디스커버리와 클라이언트 측 로드 밸런싱 통합
- 인터셉터: 인증 및 모니터링을 위한 gRPC 인터셉터 활용