왜 gRPC를 쓰려고 했나
내가 관리하는 서버에는 이커머스 플랫폼에서 발생한 데이터를 저장하고 관리하고 있다. 이 데이터는 여러 프로덕트들과 통신으로 주고받는다. 지금까지는 REST(흔히 쓰는 HTTP API 방식)로 이 데이터를 주고받았는데, gRPC라는 통신 방식으로 옮기면 다음과 같은 이득이 있을 거라 기대했다.
- 더 빠른 통신 → 리소스 부하와 비용 감소: 통신이 빨라지면 서버가 요청 하나를 처리하는 데 걸리는 시간이 줄고, 그만큼 같은 서버 자원으로 더 많은 요청을 처리할 수 있다.
- 내부 전용(private) 통신 구조로 인한 통신 비용 감소: gRPC는 외부에 노출되지 않는 private subnet을 이용한 VPC 내부망 통신으로 구현했기 때문에, 이 경로로 오가는 데이터에 대한 통신 비용이 줄어든다.
- 위성 서비스의 중복 리소스 관리 부담 감소: 1번의 효과로, 데이터를 가져다 쓰는 여러 위성 서비스(주문/리뷰/업셀/푸시 등)가 각자 캐싱이나 재시도 로직 같은 걸 두껍게 만들 필요가 줄어들 거라 예상했다.
기존 REST 통신들을 gRPC로 점차 옮겨가는 작업을 진행하면서, 정말 기대한 대로 빨라졌는지를 실제로 확인해보고 싶었다. 이 글은 그 확인 과정에서 벌어진 일을 정리한 것이다.
용어 먼저 정리: v1 · v2 · stub
이 글에 계속 나오는 세 단어를 먼저 정리한다.
| 용어 | 한 줄 설명 |
|---|---|
| v1 | 25년 5월에 최초로 만든 gRPC 방식. 모든 데이터 값을 문자열(“123”, “true”)로 주고받는다 |
| v2 | 26년 7월에 v1을 개선한 새 버전. 숫자는 숫자(int), 참/거짓은 boolean으로 원래 타입 그대로 주고받는다 |
| stub 직접호출 | v2에서 한 단계 더 나아가, 받은 데이터를 우리 코드가 쓰기 편한 형태(딕셔너리)로 변환하는 과정 자체를 생략하고 원본 그대로 쓰는 방식 |
1. HTTP(REST)와 gRPC(v1)를 비교해봤더니, 느렸다
기대와 확인해보고 싶은 마음을 가지고 실제로 REST와 gRPC(v1)의 속도를 비교했다. 결과는 예상과 정반대였다.
- 데이터 1
10건처럼 적을 때는 gRPC가 REST보다 3070% 빨랐다. 여기까지는 기대한 대로였다. - 하지만 100건 이상 대량으로 조회하면 상황이 뒤집혔다. gRPC가 REST보다 최대 2.5~2.7배 느렸다.
예를 들어 상품 1,000건(실제 603건) 조회 기준:
| 방식 | 걸린 시간(중간값) |
|---|---|
| REST | 159.1ms |
| gRPC v1 | 338.2ms (REST의 2.1배) |
“많은 데이터를 자주 오가는 이커머스 플랫폼에 유리할 것"이라는 도입 취지와, 실제로 대량 데이터일수록 더 느려진다는 결과가 정반대였다.
2. 왜 느렸을까 — 원인 분석
병목을 찾아보니 통신 자체가 아니라 데이터를 문자열로 바꾸고 다시 되돌리는 과정이 원인이었다. 모든 데이터를 문자열로 주고받아 이를 원래의 형식으로 바꿔줘야 했다. 예를 들어 가격이 데이터베이스에서는 숫자로 저장되어 있지만 gRPC 서버에서 이를 가져와 문자로 바꾼채 보내주고 있었다.
문제는 이 단순함이 대량 조회에서 비용으로 돌아왔다는 점이다.
- 서버는 응답을 만들 때 모든 필드를 문자열로 바꾸는 작업을 해야 했고,
- 클라이언트는 그 문자열 응답을 다시 JSON으로 감싸서 보내고 받는 쪽에서 파싱하고, 숫자·참거짓으로 되돌리는 작업을 매번 반복해야 했다.
데이터가 적을 땐 이 변환 비용이 미미했지만, 건수가 늘어날수록 변환해야 할 필드 수도 같이 늘어나 비용이 누적됐다. 즉, 도입을 단순하게 하기 위한 선택이 대량 조회 성능이라는 목표와 충돌한 것이다.
3. 그래서 만든 v2, 그리고 우연히 찾은 stub
원인이 타입 변환·문자열 왕복으로 좁혀지자, 이를 없애는 방향으로 새 버전(v2)을 만들었다.
- 필드마다 원래 타입(숫자는 int, 참/거짓은 bool)을 정확히 정의해서, 서버가 문자열로 바꾸는 과정과 클라이언트가 되돌리는 과정을 없앴다.
- 값이 없는 필드는 빈 문자열(
"") 대신 아예 키를 비워, 값이 없음인지 빈 값인지 구분할 수 있게 했다. - 기존 v1은 그대로 두고 v2를 병행 배포해서, 각 서비스가 준비되는 대로 점진적으로 옮겨갈 수 있게 했다.
v2를 만드는 과정에서 한 가지를 더 발견했다. v2는 원본 데이터를 받은 뒤 우리 코드가 쓰기 편한 딕셔너리로 한 번 더 변환하는데, 이 변환 과정 자체를 생략하고 원본(stub)을 그대로 쓰면 추가로 더 빨라진다는 것이었다. 대신 딕셔너리의 편리함(.get() 같은 접근)을 포기하고 원본 형식의 접근 방식에 맞춰야 하는 트레이드오프가 있다.
여기에 더해, 실험 중에 필요한 필드만 요청하면 어느 방식이든 훨씬 빨라진다는 것도 확인했다. 예를 들어 상품 조회 시 60여 개 필드 중 실제로 쓰는 6개만 요청하면 데이터량이 90% 이상 줄어든다.
4. 종합 실험 결과
앞서 나온 v1의 문제, v2의 개선, stub과 필드 축소까지 모두 넣어서 다시 측정했다. 상품 1,000건(603건) 조회 기준 핵심 결과:
| 방식 | 걸린 시간 | REST 대비 |
|---|---|---|
| REST (기존 방식) | 159.1ms | 기준 |
| gRPC v1 (개선 전) | 338.2ms | 2.1배 느림 |
| gRPC v2 (개선 후) | 194.6ms | 1.2배 느림 |
| gRPC v2 + stub 직접호출 | 145.0ms | 1.1배 빠름 |
| gRPC v2 + stub + 필요한 필드만 요청 | 126.7ms | 빠름 |
정리: v1은 REST보다 2배 이상 느렸지만, v2로 바꾸고 stub까지 쓰고 필요한 필드만 요청하면 REST보다 오히려 빠르다.
알아둘 예외
- 필드를 줄여서 요청하면서 동시에 수백 건 이상 대량 조회하는 극단적인 경우엔 REST가 근소하게(수십 ms 차이) 더 빠르다.
- 개발팀이 챙기기 가장 쉬운 습관은 stub 같은 고급 기법보다 필요한 필드만 요청하기다. 코드 변경이 적고 v1/v2/REST 어떤 방식을 쓰든 다 효과가 있다.
5. 결론
- gRPC 도입 취지(대량 데이터를 빠르게, 내부 통신 비용을 줄이려고)는 합리적이었지만, 처음 설계(v1, 전부 문자열)는 그 취지와 반대로 대량 조회에서 REST보다 느린 결과를 냈다.
- 원인을 좁혀서 v2(타입 복원)를 만들고, 그 과정에서 찾은 stub 직접호출과 필드 축소까지 더해 최종적으로 REST를 앞서는 결과를 만들었다.
- 습관으로 남길 것 하나만 고르라면: 필요한 필드만 요청하기 — 가장 적은 코드 변경으로 어떤 방식을 쓰든 효과를 본다.
부록: 상세 비교표
측정 조건: Test 서버, shop_id=1 (주문 304건 / 상품 603건 / 고객 98건), 각 케이스 30회 반복.
A. v1 → v2 개선 폭 (gRPC 내부 비교, embed 옵션 꺼짐 기준, 단위 ms)
| 대상 | 건수 | v1 | v2 | 개선율 |
|---|---|---|---|---|
| 주문 | 100건 | 41.9 | 26.4 | -37% |
| 주문 | 1,000건(304) | 103.3 | 67.5 | -35% |
| 상품 | 100건 | 59.6 | 38.6 | -35% |
| 상품 | 1,000건(603) | 332.5 | 194.5 | -41% |
| 고객 | 100건 | 28.4 | 21.2 | -25% |
B. v2 vs REST
| 대상·건수 | v2 | REST | 승자 |
|---|---|---|---|
| 주문 1건 | 7.4ms | 13.5ms | v2 |
| 주문 100건 | 26.4ms | 30.8ms | v2 |
| 주문 1,000건(304) | 67.5ms | 64.8ms | REST |
| 상품 100건 | 38.6ms | 42.4ms | v2 |
| 상품 1,000건(603) | 194.5ms | 161.8ms | REST |
→ 1회 100건 이하 조회는 v2가 REST보다 빠르거나 동급. 수백 건 이상 초대량에서만 REST가 근소하게 앞서는데, 이 구간을 stub + fields 조합으로 뒤집은 것이 이번 최적화의 핵심이다.
C. 필드 축소 효과 (전체 필드 → 필요한 필드만, p50 기준)
| 대상·건수 | 방식 | 전체 필드 | 필요한 필드만 | 시간 단축 | 데이터량 감소 |
|---|---|---|---|---|---|
| 주문 1,000건 | v1 | 102.7ms | 56.9ms | -45% | -86% |
| 주문 1,000건 | v2 | 64.2ms | 50.7ms | -21% | -86% |
| 주문 1,000건 | REST | 64.2ms | 43.8ms | -32% | -88% |
| 상품 1,000건 | v1 | 338.2ms | 150.9ms | -55% | -93% |
| 상품 1,000건 | v2 | 194.6ms | 130.8ms | -33% | -93% |
| 상품 1,000건 | REST | 159.1ms | 93.7ms | -41% | -94% |
주고받는 데이터량은 8694% 줄어드는데, 걸리는 시간은 1055%만 줄어든다. 필드를 줄여도 DB에서 데이터를 읽어오고 객체로 만드는 비용은 그대로이고, 줄어드는 건 전송·변환 비용뿐이기 때문이다.
D. stub 직접호출 효과 (v2 딕셔너리 변환 대비, 전체 필드 기준)
| 대상·건수 | v2 (변환 포함) | v2 + stub (변환 생략) | 절감 | REST | 결과 |
|---|---|---|---|---|---|
| 주문 100건 | 26.0ms | 23.1ms | -11% | 29.6ms | stub 우위 |
| 주문 1,000건 | 64.2ms | 54.1ms | -16% | 64.2ms | stub 우위 |
| 상품 1,000건 | 194.6ms | 145.0ms | -25% | 159.1ms | stub 우위 |