[Onboarding] : gRPC

문승현·2022년 7월 27일
0

BeDev_2

목록 보기
3/8
post-thumbnail

현재 회사의 제품은 MSA(Micro Service Architecture)로 이루어져있다.
각각의 서비스들은 원격 프로시저 호출(RPC, Remote Procedure Call)을 통해 통신한다.
원격 프로시저 호출이란 한 프로그램이 다른 컴퓨터에 위치한 프로그램의 서비스(프로시저)를
마치 로컬 서비스처럼 직접 호출하여 사용할 수 있도록 지원하는 기술을 의미한다.

원격 프로시저 호출은 일반적으로 서버-클라이언트 모델을 사용한다.
클라이언트에서 원격 프로시저 호출을 진행하면 프로시저의 매개 변수가 서버로 전달된다.
이후, 서버에서 해당 프로시저가 실행되고 그 반환 값이 다시 클라이언트로 전달된다.
마치 클라이언트 자신의 프로시저인 것처럼 서버의 프로시저를 사용할 수 있는 것이다.

클라이언트에서 서버로의 자세한 통신 과정은 아래와 같다.

  1. 클라이언트가 클라이언트 스텁을 호출한다.
  2. 호출은 매개 변수가 정상적인 방식으로 스택에 푸시되는 로컬 프로시저 호출과 같다.
  3. 클라이언트 스텁은 매개 변수를 메시지에 포장(마샬링)하고 시스템 콜을 수행하여 메시지를 보낸다.
  4. 클라이언트의 로컬 OS는 클라이언트 시스템에서 원격 서버 시스템으로 메시지를 보낸다.
  5. 서버 OS는 수신 패킷을 서버 스텁으로 전달한다.
  6. 서버 스텁은 메시지에서 매개변수의 압축을 푼다(언마샬링).
  7. 서버의 프로시저가 완료되면 반환 값을 메시지로 포장(마샬링)하여 서버 스텁으로 전달한다.
  8. 서버 스텁은 메시지를 전송 계층에 전달한다.
  9. 전송 계층은 결과 메시지를 클라이언트 전송 계층으로 다시 보낸다.
  10. 클라이언트 전송 계층은 메시지를 클라이언트 스텁에 다시 전달한다.
  11. 클라이언트 스텁은 반환 매개 변수의 압축을 풀고(언마샬링) 호출자에게 이를 반환한다.

스텁(Stub)이란?

이러한 RPC에 대한 이해를 높이고자 온보딩 과정에서 gRPC를 사용하는 과제가 주어졌다.
gRPC는 구글에서 만든 최신 오픈 소스 고성능 원격 프로시저 호출 프레임워크이다.

기타 RPC 시스템과 유사하게 gRPC는 원격으로 호출할 수 있는 프로시저를 정의한다.
나 역시 과제에서 아래와 같이 프로시저를 포함한 프로토 파일을 정의하였다.

syntax = "proto3";

option csharp_namespace = "WebTutorial.Protos.BlogPostProto";

package blogpost;

import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";

service GrpcBlogPost {
	rpc GrpcCreatePost (BlogPostRequest) returns (BlogPostResponse);
	rpc GrpcReadPost (BlogPostRequest) returns (BlogPostResponse);
	rpc GrpcUpdatePost (BlogPostRequest) returns (BlogPostResponse);
	rpc GrpcDeletePost (BlogPostRequest) returns (BlogPostResponse);
}

service GrpcBlogPostComment {
	rpc GrpcCreatePostComment (BlogPostCommentRequest) returns (BlogPostCommentResponse);
	rpc GrpcReadPostComment (BlogPostCommentRequest) returns (BlogPostCommentResponse);
	rpc GrpcUpdatePostComment (BlogPostCommentRequest) returns (BlogPostCommentResponse);
	rpc GrpcDeletePostComment (BlogPostCommentRequest) returns (BlogPostCommentResponse);
}

message BlogPostRequest{
	google.protobuf.StringValue pid = 1;
	google.protobuf.StringValue title = 2;
	google.protobuf.StringValue content = 3;
	google.protobuf.StringValue author = 4;
}

message BlogPostResponse {
	string error = 1;
	int32 status = 2;
	GrpcBlogPostData data = 3;
}

message GrpcBlogPostData {
	string pid = 1;
	string title = 2;
	string content = 3;
	string author = 4;
	repeated GrpcBlogPostCommentData comments = 5;
	google.protobuf.Timestamp created_time = 6;
	google.protobuf.Timestamp updated_time = 7;
}

message BlogPostCommentRequest{
	google.protobuf.StringValue pid = 1;
	google.protobuf.StringValue cid = 2;
	google.protobuf.StringValue message = 3;
	google.protobuf.StringValue writer = 4;
	google.protobuf.StringValue parent_post_id = 5;
}

message BlogPostCommentResponse {
	string error = 1;
	int32 status = 2;
	GrpcBlogPostCommentData data = 3;
}

message GrpcBlogPostCommentData {
	string cid = 1;
	string message = 2;
	string writer = 3;
	string parent_post_id = 4;
	google.protobuf.Timestamp created_time = 5;
	google.protobuf.Timestamp updated_time = 6;
}

그리고 해당 정의를 서버와 클라이언트 모두 나누어 가졌다.
클라이언트는 해당 정의에 맞게 프로시저를 호출하고,
서버는 해당 프로시저에 맞게 세부사항을 구현하여 호출을 처리한다.
나는 과제에서 아래와 같이 프로시저를 구현하였다.

using Grpc.Core;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebTutorial.Model;
using WebTutorial.Services;
using WebTutorial.Protos.BlogPostProto;
using Newtonsoft.Json;
using Google.Protobuf.WellKnownTypes;
using Google.Protobuf.Collections;


namespace WebTutorial.GrpcServices
{
    public class GrpcBlogPostService : GrpcBlogPost.GrpcBlogPostBase
    {
        private readonly IBlogPostService<BlogPost> _blogPostService;
        public GrpcBlogPostService(IBlogPostService<BlogPost> blogPostService)
        {
            _blogPostService = blogPostService;
        }

        public async override Task<BlogPostResponse> GrpcCreatePost(BlogPostRequest request, ServerCallContext context)
        {
            var blogPostRequestData = new BlogPost(request);
            var post = await _blogPostService.CreatePostAsync(blogPostRequestData);     
            var response = GrpcBlogData<BlogPost>.PostResponse(post);
            
            return response;
        }

        public async override Task<BlogPostResponse> GrpcReadPost(BlogPostRequest request, ServerCallContext context)
        {
            var post = await _blogPostService.GetPostAsync(request.Pid);
            var response = GrpcBlogData<BlogPost>.PostResponse(post);

            return response;
        }

        public async override Task<BlogPostResponse> GrpcUpdatePost(BlogPostRequest request, ServerCallContext context)
        {
            var blogPostRequestData = new BlogPost(request);
            var post = await _blogPostService.PutPostAsync(request.Pid, blogPostRequestData);
            var response = GrpcBlogData<BlogPost>.PostResponse(post); 

            return response;
        }

        public async override Task<BlogPostResponse> GrpcDeletePost(BlogPostRequest request, ServerCallContext context)
        {
            var post = await _blogPostService.DeletePostAsync(request.Pid);
            var response = GrpcBlogData<BlogPost>.PostResponse(post);

            return response;
        }
    }
}

기존 서버-클라이언트 통신은 HTTP 메소드를 이용한 API 작성이 대부분이었다.
RPC라는 새로운 방식의 통신을 구현하는 것이 굉장히 흥미롭고 도전적이었던 과제였다.

참고 자료 1) - Remote Procedure Call (RPC)
참고 자료 2) - Introduction to gRPC
참고 자료 3) - gRPC services with ASP.NET Core

1개의 댓글

comment-user-thumbnail
2022년 8월 2일

저도 막 gRPC를 처음 접한 단계인데, 잘 읽고 갑니다!

답글 달기