다 울었니? 이제 gRPC 제대로 해보자.

Sun choi·2024년 11월 21일

gRPC

목록 보기
4/4
post-thumbnail

🏁 문제 상황

  • 기존 REST API를 통해 티켓 서버가 결제 서버에서 결제 정보를 반환받는 기능 구현 → 성능 저하와 비효율적 직렬화 발생
  • 성능 저하: 기존 REST API는 HTTP/1.1 기반으로 요청-응답마다 연결을 반복 생성, 트래픽 증가 시 처리 성능이 저하됨
  • 비효율적 직렬화: JSON 직렬화 방식은 결제 정보와 같은 복잡한 데이터 처리 시 속도가 느리고 자원 소모가 큼
  • 높은 요청 처리 성능과 효율적인 데이터 전송 방식이 필요 → gRPC 도입 검토

💡 gRPC 도입 이유

  • 복잡하고 방대한 결제 데이터를 빠르고 가볍게 처리 → Protobuf 직렬화
  • 한 번의 연결로 여러 요청을 동시에 처리하며, 동기적 요청-응답 속도 극대화 → HTTP/2 멀티플렉싱
  • REST API보다 효율적이고, 메시지 큐보다 즉각적인 응답을 제공 → 실시간 요청을 처리하는 데 최적화하여 사용자 경험 개선
  • 브라우저에서 직접 호출하기 어렵다는 문제점
    -> Spring MVC 컨트롤러를 통해 HTTP 요청을 받고 내부적으로 gRPC 클라이언트를 호출하는 방식으로 극복
    -> 기존 REST API 기반 클라이언트와의 호환성을 유지하며 gRPC 도입의 장점을 극대화!

🪐 구현 순서도

  • 프로토콜 파일 작성 (.proto)
  • 컴파일 및 코드 생성
  • 티켓 서버 & 결제 서버에 gRPC 서버 구현
  • 클라이언트 설정 및 통신 테스트
  • 테스트 및 배포

🚀 내가 마구잡이로 적은 실제 구현 순서도

  • proto 파일 생성 & 빌드 설정(build.gradle에 플러그인을 추가해 .proto 파일을 자동으로 컴파일하게 설정)
  • proto 파일 작성
  • gRPC로 로직 서버구현
  • 각 서버의 .proto 파일을 공유(모듈로 만들어 관리 or 각각 복사 -> 일단 각각 복사하고, 수정할 때 양쪽 서버의 파일에 수정하는 방법 택)
  • 티켓서버쪽 서비스 구현 완료 / gRPC 포트번호 50051 로 설정
  • 페이먼트쪽 서비스구현 완료 / gRPC 포트번호 50052 로 설정
  • 양 서버 연결 (연결 에러 트러블슈팅 포스팅 트러블슈팅 -> 엘라스틱서치 문제였음ㅎ )
  • 통신 테스트

멀티모듈 방식으로 로직 변경 결정!

해당 의사결정 내용의 포스팅 - 멀티모듈 방식으로 proto repository를 따로 만들어 관리. 왜?

  • 프로토파일 하나로 합치기, 서비스 로직 수정
  • Spring MVC 컨트롤러를 통해 HTTP 요청을 받고 내부적으로 gRPC 클라이언트를 호출하여 응답을 처리하는 우회 방법 구현
  • grpc-proto 레포지토리 생성
  • Gradle Composite Build 사용해서 티켓, 결제서버에 연결. 프로토파일 공유
  • 위에서 프로토파일만 공유됨. 그걸 각 서버에서 자바파일로 만드는 로직 구현
  • 팀원들이 별도로 로컬 경로를 확인하거나 설정하지 않고도 프로젝트를 사용할 수 있게 빌드 자동화 스크립트를 사용
  • protobuf 최신버전으로 업그레이드
  • 결제서버 Impl 클래스, 티켓서버 유저 확인 예외처리
  • 성능테스트 준비
    • 직렬화 차이 더 보이도록 데이터구조 변경
    • 테스트 준비 갈 완료🙋🏻‍♀️

💥 간단 트러블슈팅

  • 포트 중복 문제 해결
    포트를 중복해서 쓰고있다는 문제 계속됨. 터미널 명령어로는 사용하고 있는 포트가 나오지 않았다.
    -> 해결 : 두 서버가 GrpcServerConfig와 grpc-server-spring-boot-starter를 동시에 사용하면서 포트 충돌이 발생한 것이었다. GrpcServerConfig를 삭제하니 정상 실행되었다.

  • UNIMPLEMENTED: Method not found: payment.PaymentService/GetPaymentInfo 에러 발생
    -> 해결 : PaymentServiceImpl에 어노테이션은 @Service에서 @GrpcService 로 변경


🤖 로직 구현

grpc-proto repo

  • grpc-proto 레포지토리의 .proto 파일
syntax = "proto3";

package payment;
option java_package = "com.example.payment.grpc";
option java_outer_classname = "PaymentServiceProto";

message PaymentRequest {
  int64 userId = 1;
  string orderId = 2;
  UserDetails user = 3;
  repeated EventDetails event = 4;
}

message UserDetails {
  string userRole = 1;
  string email = 2;
  Address address = 3;
}

message Address {
  string street = 1;
  string city = 2;
  string state = 3;
  string zip = 4;
}

message EventDetails {
  string eventId = 1;
  string title = 2;
  string category = 3;
  string place = 4;
}

message PaymentResponse {
  string orderId = 1;
  int64 userId = 2;
  int32 amount = 3;
  bool success = 4;
  string message = 5;
}

service PaymentService {
  rpc GetPaymentInfo(PaymentRequest) returns (PaymentResponse);  //실제 gRPC 서비스의 메서드를 정의
}
  • build.gradle
plugins {
    id 'java'
    id 'com.google.protobuf' version '0.9.2'
}

group = 'com.sparta'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:4.28.3" }
    plugins {
        grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.68.1" }
    }
    generateProtoTasks {
        all().forEach { task ->
            task.builtins {
                java {}
            }
            task.plugins {
                grpc {}
            }
        }
    }
}

dependencies {
    // gRPC
    implementation "io.grpc:grpc-netty-shaded:1.68.1"
    implementation "io.grpc:grpc-protobuf:1.68.1"
    implementation "io.grpc:grpc-stub:1.68.1"
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
    implementation "com.google.protobuf:protobuf-java:4.28.3"
}

tasks.named('test') {
    useJUnitPlatform()
}

.proto파일과 build.gradle를 작성하고 gradle build를 하면 아래 이미지처럼 java 파일이 생성된다.


그리고나서! 티켓서버와 결제서버에서 위 grpc-proto 레포지토리를 Gradle Composite Build 사용해 연결하여 프로토파일 공유한다.
또한, 팀원들이 별도로 로컬 경로를 확인하거나 설정하지 않고도 프로젝트를 사용할 수 있게 빌드 자동화 스크립트를 사용했다.

  • settings.gradle
  • build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.5'
    id 'io.spring.dependency-management' version '1.1.6'
    id 'com.google.protobuf' version '0.9.2'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

sourceSets {
    main {
        java {
            srcDirs += file('grpc-proto/build/generated/source/proto/main/java')
            srcDirs += file('grpc-proto/build/generated/source/proto/main/grpc')
        }
    }
}

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:4.28.3" }
    plugins {
        grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.68.1" }
    }
    generateProtoTasks {
        all().forEach { task ->
            task.builtins {
                java {}
            }
            task.plugins {
                grpc {}
            }
        }
    }
}


dependencies {
	//gRPC
    implementation "io.grpc:grpc-netty-shaded:1.68.1"
    implementation "io.grpc:grpc-protobuf:1.68.1"
    implementation "io.grpc:grpc-stub:1.68.1"
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
    implementation 'net.devh:grpc-spring-boot-starter:3.1.0.RELEASE'
    implementation 'com.google.protobuf:protobuf-java:4.28.3'
    
    ...
    
}

// 빌드 전에 grpc-proto의 Java 파일이 생성
tasks.named('compileJava') {
    dependsOn gradle.includedBuild('grpc-proto').task(':generateProto')
}

이렇게 티켓 서버와 결제 서버 각각 설정해준 뒤,

 ./gradlew clean build

명령어를 입력하면, 각 서버에서도 아래와 같이 grpc-proto 폴더 하위 java 파일이 생성된다.


ticket repo

이제 각 서버의 서비스 단에서 로직을 작성해주면 된다.
결제 정보를 요청하는 티켓 서버에서는 아래와 같이 PaymentServiceImpl, PaymentServiceClient 클래스를 작성한다.

  • PaymentServiceImpl 클래스

  • PaymentServiceClient 클래스(테스트를 위해 임의의 값을 넣어줌)

Spring MVC 컨트롤러를 통해 HTTP 요청을 받고 내부적으로 gRPC 클라이언트를 호출하여 응답을 처리한다.

  • PaymentInfoController 클래스

payment repo

티켓서버에서 받은 request로 조회한 결제정보 데이터를 반환하는 결제서버의 로직이다.

  • PaymentServiceImpl 클래스


📊 성능테스트 결과 비교

프로토콜요청 수총 소요 시간(s)평균응답시간(ms)평균 TPS(/sec)에러율(%)
REST10000393384325.50
gRPC10000180177055.50
REST10000030441504432.80
gRPC1000009904902101.10
  • 요청 수 1만건 → gRPC가 REST 대비 평균 응답시간 약 2.2배, 평균 TPS 약 2배 높은 성능
  • 요청 수 10만건 → gRPC가 REST 대비 평균 응답시간 약 3배, 평균 TPS 약 3배 높은 성능

📋 결론

  • gRPC를 통해 동기적 요청-응답 처리로 복잡하고 방대한 데이터를 더 빠르게 제공하며 사용자 경험을 대폭 개선
  • 요청 수가 증가할수록 REST와 gRPC의 성능 차이가 더 커짐
  • 대규모 요청 처리 시 REST는 병목 현상이 발생하지만, gRPC는 높은 Throughput을 유지하며 안정적인 성능을 제공
  • 대규모 트래픽 처리나 실시간 성능이 중요한 시스템에서는 gRPC를 사용하는 것이 더 적합



🙈 회고

눈물이 찔끔 날 만큼 말도 안되게 어려웠다. 엘라스틱서치를 공부하면서도 스프링과 다른 규모라서 신세계를 맛봤다고 생각했는데 gRPC 통신 개선은 정말 정말 정말 어려웠다.
레파토리도 많이 안나와있고, 정보가 많지 않아서 찾아보기도 어려웠다.
근데... 그래서.... 희열이 나.. 통신 공부라...? 호오... 너무 너무 너무 어려워서 희열이 나.....!
무엇보다 처음에 다른 기술 스택을 사용하는 것보다 gRPC 통신을 공부하고 개선하는 걸 마음먹는 것부터가 결정하기 힘들었는데, 성능테스트를 하며 잘 연결이 되다는 것 그리고 REST 대비 속도가 배수의 차이가 난다는 것을 보고 감격에 젖어 눈물이 (마음속에서) 났다. 이제 더 더 깊이 공부해야할 차례! 아자!


profile
풀스택 개발자의 공부기록 📖

0개의 댓글