REST API vs gRPC: 마이크로서비스 통신 방식 비교
gRPC란?
gRPC는 구글이 만든 고성능 오픈소스 RPC(Remote Procedure Call) 프레임워크이다. 다른 서버에 있는 함수를 마치 내 서버에 있는 함수처럼 호출해서 쓸 수 있게 해주는 기술이다.
REST API vs gRPC
마이크로서비스 환경에서 서비스 간 통신(Internal API Call)에 REST API와 gRPC는 다음 관점에서 다르다.
1. 데이터 형식
REST API는 JSON을 사용한다.
{"name": "홍길동", "age": 30}
텍스트 기반이라 컴퓨터가 이를 다시 해석(Parsing)해야 해서 상대적으로 느리다.
gRPC는 Protocol Buffers(Protobuf)라는 이진 데이터를 사용한다. 데이터를 아주 작게 압축된 이진 코드(Binary)로 보내기 때문에:
- 데이터 크기가 훨씬 작다
- 컴퓨터가 해석할 필요 없이 바로 처리한다
- 통신 속도가 획기적으로 빠르다
2. 통신 프로토콜
REST API
- 주로 HTTP/1.1 사용
- 요청-응답이 1:1로 매핑되어 단방향으로만 동작
- 요청 하나당 연결을 하나씩 맺고 끊어서 오버헤드가 크다
gRPC
- HTTP/2 사용
- 양방향 통신 가능
- 하나의 연결로 여러 요청을 동시에 처리(Multiplexing)
- 마이크로서비스끼리 수많은 데이터를 주고받을 때 유리하다
3. Type Safety
gRPC는 .proto라는 파일로 서로 주고받을 데이터 타입을 엄격하게 미리 정의한다. 코드를 짤 때부터 에러를 잡을 수 있어(Type-safe), 서비스 간의 통신 오류를 줄여준다.
gRPC 구현 방법
gRPC를 만들려면 코드를 짜기 전에 .proto라는 파일부터 먼저 만들어야 한다. 여기에는 IDL(Interface Definition Language)이라는 인터페이스 정의 언어가 사용된다.
단계 1: 명세서 작성 (user.proto)
syntax = "proto3";
// 서비스(함수) 정의
service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
}
// 주고받을 데이터 모양 정의
message UserRequest {
int32 user_id = 1;
}
message UserResponse {
int32 user_id = 1;
string name = 2;
}
user_id = 1처럼 숫자를 지정하는 것은 필드의 고유 태그이다. 확장할때 좋다는데.. 아직 잘 모르겠음
단계 2: 파이썬 코드 생성
다음 명령어를 실행하면 파이썬 파일 2개가 생성된다.
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. user.proto
user_pb2.py: 데이터 구조를 담고 있다user_pb2_grpc.py: 통신을 담당한다
두 개의 파일로 분리한 이유는 책임과 역할의 분리 때문이다. 어떤 시스템에서는 데이터 구조만 필요하고 서버 기능은 필요 없을 수 있다. 또한 의존성 관리도 더 쉬워진다.
단계 3: 서버 구현 (server.py)
생성된 파일을 import해서 빈칸 채우기를 한다.
import grpc
from concurrent import futures
import user_pb2 # 생성된 데이터 코드
import user_pb2_grpc # 생성된 통신 코드
# 생성된 부모 클래스(UserServiceServicer)를 상속받는다
class UserService(user_pb2_grpc.UserServiceServicer):
# 비즈니스 로직을 채워 넣는다
def GetUser(self, request, context):
# request.user_id처럼 점(.)으로 데이터에 접근한다
return user_pb2.UserResponse(
user_id=request.user_id,
name="김제미니"
)
# 서버 실행 코드
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServiceServicer_to_server(UserService(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()
if __name__ == '__main__':
serve()
단계 4: 클라이언트 구현 (client.py)
import grpc
import user_pb2
import user_pb2_grpc
def run():
# 서버와 연결하는 통로 생성
with grpc.insecure_channel('localhost:50051') as channel:
# 서버의 함수를 호출할 대리인(Stub) 생성
stub = user_pb2_grpc.UserServiceStub(channel)
# 실제 호출
response = stub.GetUser(user_pb2.UserRequest(user_id=123))
print(f"받은 데이터: {response.name} (ID: {response.user_id})")
if __name__ == '__main__':
run()