Spring Boot와 gRPC를 활용한 마이크로서비스 고성능 통신 구현

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

모범 사례

  1. Proto 버전 관리: buf 도구를 사용한 스키마 변경 관리
  2. 오류 처리: gRPC 상태 코드를 활용한 일관된 오류 전달
  3. 시간 제한: Deadline 설정을 통한 연쇄 타임아웃 방지
  4. 로드 밸런싱: 서비스 디스커버리와 클라이언트 측 로드 밸런싱 통합
  5. 인터셉터: 인증 및 모니터링을 위한 gRPC 인터셉터 활용

태그: gRPC Spring Boot Protocol Buffers 마이크로서비스 HTTP/2

5월 30일 12:23에 게시됨