인턴 시작할 때 gRPC 문서를 읽고 블로그에 글로 작성한 적이 있다.
그 때는 gRPC를 전혀 몰랐기 때문에 읽으면서도 무슨 말인지 이해를 못했다.
그래서 포스팅이 "정리"가 아니라 "옮겨 적기" 였던 것 같다.
gRPC를 사용한지 두 달이 지난 지금 다시 문서를 읽고 "정리"를 해보려한다.
gRPC는 proto buf를 인터페이스 정의 언어와 메세지 교환 형식으로 사용하고 있다.
REST식으로 이야기하면 proto buf를 사용해서 API를 문서화할 수 있으며 이 API 문서 자체가 HTTP Body에 들어가는 JSON이 된다는 뜻이다.
그렇다면 proto buf는 무엇이고 이 녀석을 어떻게 사용한다는 걸까?
Protocol Buffers(proto buf)는 구글에서 개발한 데이터 직렬화 매커니즘이다.
proto buf는 우리가 사용하는 객체들을 binary로 직렬화 해준다.
만약 아래와 같은 객체가 있다고 생각해보자.
// kotlin
data class Hello(name: String)
Hello
라는 데이터 클래스는 String
타입의 name
프로퍼티를 가지고 있다.
REST에서는 이 Hello
를 다른 서버나 클라이언트에게 전송하고 싶을 때 객체를 JSON으로 파싱해서 HTTP Message body에 담아서 전송한다.
반면에 gRPC는 proto buf를 사용해서 객체를 JSON이 아닌 binary로 직렬화하여 전송한다.
proto buf는 binary 직렬화 될 뿐만 아니라 내부적으로 여러가지 최적화도 수행해준다.
그래서 같은 용량의 JSON보다 훨씬 많은 데이터를 전송할 수 있다.
만약 위에 작성한 Hello
클래스를 proto buf로 직렬화 하고 싶다면 아래와 같은 proto message를 작성해야 한다.
// Hello.proto
syntax = "proto3";
message Hello {
string hello = 1;
}
위와 같은 message를 작성하고 protoc 커맨드를 입력하거나 gradle과 같은 패키지 매니저에 플러그인을 등록하고 빌드를 하면 직렬화 된 데이터를 사용할 수 있는 코드를 생성해준다.
다음 포스팅에서 직접 실습할 예정이니 일단은 넘어가자.
proto buf는 우리가 사용하는 객체 (Entity 클래스 등)들을 압축, 최적화 하기위해서 사용한다고 생각하면 좋겠다.
더 자세한 내용을 알고 싶다면 공식문서를 참고하자
proto buf를 사용해서 통신할 데이터를 만들었다면 이제 통신을 할 차례이다.
REST에서는 클라이언트가 서버의 URL로 요청을 보내는 방식으로 통신을 했다.
그런데 gRPC는 URL로 요청을 보내지 않는다.
URL로 요청을 보내지 않기 때문에 URL을 설계할 필요도 없다.
그렇다면 어떻게 통신을 할까?
gRPC에서는 클라이언트가 서버의 메서드를 직접 호출함으로써 통신을 한다.
사실 이건 gRPC만의 특징은 아니다. RPC 통신의 특징이다.
gRPC는 google이 개발한 RPC 방식 중 하나일 뿐이다.
특징이라고 하면 proto buf를 사용한다는 사실이다.
그런데 서버와 클라이언트는 일반적으로 물리적으로 멀리 떨어져있다.
즉, 클라이언트의 소스코드와 서버의 소스코드가 분리되어 있는데 어떻게?
함수를 호출 하려면 로컬 컴퓨터가 호출하고자 하는 함수의 주소공간을 알고 있어야한다.
그런데 물리적으로 떨어진 서버와 클라이언트 간의 주소공간을 어떻게 알까?
간단하게 말하면,
proto buf에서 제공하는 protoc 라는 컴파일러가 원하는 언어들로 코드를 생성해준다.
생성된 코드는 언어만 다를 뿐, proto buf의 message를 보고 생성되기 때문에 서버와 클라이언트 언어만 다를 뿐 같은 클래스명, 함수 이름을 가지는 코드를 각자의 프로젝트 안에 포함 시킬 수 있다.
위에서 정의한 Hello.proto가 protoc를 통해 여러가지 언어로 컴파일 되는 모습을 그림으로 살펴보자.
우리는 proto 메세지만 정의를 해놓으면 protoc는 여러가지 언어의 코드를 생성해준다.
물론 어떤 언어를 생성할 것인지는 설정을 해줘야 한다.
그렇다면 protoc가 생성해주는 코드는 어떤 역할을 할까?
생성되는 코드는 세 가지 형태로 나눌 수 있을 것 같다.
각각을 현실세계에 비유해서 그림으로 살펴보자
service 인터페이스는 기차 역이다.
stub는 기차역으로 향하는 기차이다.
요청, 응답 모델은 기차가 싣고 가는 화물이다.
참고로 우리가 위에서 정의한 Hello.proto 만으로는 service, stub를 생성해주지 않는다.
다음 포스팅에서 직접 실습을 할 예정이니 일단 넘어가자.
위에서 생성된 코드들을 기반으로 서버와 클라이언트가 통신할 수 있다.
서버는 protc가 생성해준 인터페이스를 구현하고 gRPC 서버를 실행시키고 요청을 기다린다.
클라이언트는 protoc가 생성해준 stub을 사용해서 서버의 구현체 메서드들을 사용할 수 있다.
아래 그림을 보며 설명하면 좋을 것 같다.
하나씩 차근차근 살펴보자.
까만 네모 박스를 보자.
서로 다른 언어로 구현된 서로 다른 유형의 애플리케이션들이 통신하고 있는 것을 볼 수 있다.
이렇게 말하면 "REST도 가능한데?" 라고 할 수 있다.
하지만 아까 말했듯이 gRPC는 서버의 메서드를 호출함으로써 통신을 한다.
그런데 서로 다른 언어의 메서드를 호출한다? 처음엔 도무지 이해가 안됐다.
내가 영어를 못해서 문서를 오역한 줄 알았다. 그런데 이게 맞다.
protoc의 도움을 받아서 이것을 가능하게 한다. 다음 포스팅에서 직접 실습하며 살펴보자.
또 한가지 주목할 점은 모든 통신은 stub를 통해서 이뤄지고 있다는 점이다.
다음으로 빨간 네모 박스를 보면 통신 단위는 모두 Proto 이다.
처음에 언급했지만 gRPC에서는 API가 문서화 됨과 동시에 문서 자체가 BODY가 된다고 했다.
즉, Proto가 API이며 BODY가 된다.
마지막으로 노란 글씨를 보면 gRPC Server에 인터페이스 구현체가 있다는 것을 알 수 있다.
인터페이스를 구현한다는 것은 서버의 비즈니스 로직을 작성하는 것을 의미한다.
말 그대로 요청에 대한 응답 로직을 작성하면 된다.
클라이언트에서는 stub를 통해서 서버의 구현체 메서드를 호출하고, 호출당한 메서드는 그에 맞는 리턴값을 전송한다.
클라이언트는 함수의 리턴값으로 원하는 동작을 하면 된다.
이론적으로는 이게 전부다.
다음 포스팅에서는 gRPC가 어떻게 proto buf를 사용하고 있는지 코드로 알아보려고 한다.
실제로 코드를 작성하며 gRPC와 proto buf의 관계와 역할을 이해하고 더 나아가 뱅크샐러드의 포스팅 - 프로덕션 환경에서 사용하는 golang과 gRPC 에서 강조하는 source of truth를 체험하는 것을 목표로 한다.
스키마 주도 개발 알아보다가 여기까지 왔습니다. 너무고마워