현대 RPC와 기존 웹 프레임워크의 융합
Min-jun Kim
Dev Intern · Leapcell

소개
끊임없이 진화하는 백엔드 개발 환경에서 통신 프로토콜 선택은 종종 전략적 딜레마를 안겨줍니다. 수년간 Django 및 FastAPI와 같은 프레임워크로 구동되는 RESTful API는 단순성, 광범위한 도구, 인간 가독성으로 유명해지면서 사실상의 표준이 되었습니다. 그러나 고성능 마이크로서비스, 실시간 통신, 엄격한 타입 강제 요구는 gRPC와 같은 대안의 부상을 가져왔습니다. Google의 이 강력하고 고성능 RPC 프레임워크는 특정 시나리오, 특히 분산 시스템 내 서비스 간 통신에서 상당한 이점을 제공합니다.
많은 조직의 현실은 전면적인 마이그레이션이 아니라 점진적인 발전입니다. 많은 기존 시스템은 기존 RESTful API에 크게 의존하는 반면, 최신 서비스나 성능이 중요한 구성 요소는 gRPC를 통해 큰 이점을 얻을 수 있습니다. 이는 자연스럽게 중요한 질문으로 이어집니다. Django 또는 FastAPI와 같은 기존 RESTful API 프레임워크와 gRPC 서비스를 조화롭게 통합하는 방법은 무엇일까요? 이 기사에서는 이러한 공존을 달성하기 위한 실질적인 전략과 고려 사항을 탐구하여 개발자가 두 가지 장점을 모두 활용할 수 있도록 할 것입니다.
통합을 위한 핵심 개념
통합 전략을 자세히 살펴보기 전에 논의의 중심이 되는 핵심 기술과 개념을 간략하게 정의해 보겠습니다.
- RESTful API: 네트워크 하이퍼미디어 애플리케이션을 위한 아키텍처 스타일입니다. 상태 비저장, 클라이언트-서버 분리, 통합 인터페이스를 강조하며 일반적으로 HTTP 메서드(GET, POST, PUT, DELETE) 및 JSON 데이터 형식을 사용합니다.
- Django: 신속한 개발과 깔끔하고 실용적인 디자인을 장려하는 고수준 Python 웹 프레임워크입니다. "배터리 포함" 철학으로 유명하며 ORM, 관리자 패널, 강력한 템플릿을 제공합니다.
- FastAPI: Python 3.7+ 기반의 현대적이고 빠른(고성능) Python 웹 프레임워크로, 표준 Python 타입 힌트를 기반으로 API를 구축합니다. 대화형 API 문서(OpenAPI/Swagger UI)를 자동으로 생성하여 개발 및 소비를 용이하게 합니다.
- gRPC: 모든 환경에서 실행할 수 있는 고성능 오픈 소스 범용 RPC 프레임워크입니다. 인터페이스 정의 언어(IDL)로 Protocol Buffers를 사용하며 HTTP/2를 기반으로 구축되어 양방향 스트리밍, 효율적인 직렬화, 강력한 타입 계약과 같은 기능을 제공합니다.
- Protocol Buffers (Protobuf): 구조화된 데이터를 직렬화하는 Google의 언어 중립, 플랫폼 중립, 확장 가능한 메커니즘입니다. 많은 사용 사례, 특히 서비스 간 통신에서 XML 또는 JSON보다 작고 빠르고 간단합니다.
이러한 개념을 이해하는 것은 다양한 통합 패턴을 탐색하는 데 중요합니다.
조화로운 공존을 위한 전략
기존 RESTful API 프레임워크와 gRPC 서비스를 통합하는 방법은 여러 가지가 있으며, 각각 고유한 장점과 절충점이 있습니다. 선택은 종종 특정 아키텍처 요구 사항, 기존 인프라 및 원하는 커플링 수준에 따라 달라집니다.
1. API 게이트웨이를 사용한 서비스 분리 (마이크로서비스에 권장)
이것은 아마도 마이크로서비스 아키텍처에서 가장 일반적이고 강력한 접근 방식일 것입니다. Django/FastAPI 애플리케이션을 외부 RESTful API 요청(예: 웹 브라우저, 모바일 앱)을 처리하는 별도의 서비스로 실행하고, gRPC 서비스는 별도의 독립적인 서비스로 실행합니다. API 게이트웨이가 이러한 서비스 앞에 위치합니다.
API 게이트웨이는 모든 클라이언트 요청에 대한 단일 진입점 역할을 합니다. 인증, 권한 부여, 라우팅, 속도 제한과 같은 다양한 기능을 수행할 수 있으며, 우리 경우 가장 중요한 프로토콜 변환 기능을 수행할 수 있습니다.
메커니즘:
- 클라이언트는 REST/HTTP를 사용하여 API 게이트웨이와 상호 작용합니다. API 게이트웨이는 RESTful 인터페이스를 노출합니다.
- API 게이트웨이는 RESTful 요청을 백엔드 gRPC 서비스에 대한 gRPC 호출로 변환합니다.
- gRPC 서비스는 요청을 처리하고 gRPC 응답을 보냅니다.
- API 게이트웨이는 gRPC 응답을 클라이언트에 대한 RESTful 응답으로 다시 변환합니다.
인기 있는 API 게이트웨이 솔루션:
- Envoy Proxy: API 게이트웨이 역할을 할 수 있는 고성능 오픈 소스 엣지 및 서비스 프록시로, gRPC 트랜스코딩(HTTP/JSON을 gRPC로, 그 반대로 변환)을 지원합니다.
- NGINX: 주로 웹 서버이지만 NGINX는 모듈이나 스크립트와 함께 API 게이트웨이로 구성하여 요청을 전달할 수 있습니다. 그러나 직접적인 gRPC 트랜스코딩에는 더 많은 사용자 정의 작업이나 보조 도구가 필요할 수 있습니다.
- 사용자 정의 게이트웨이: API 게이트웨이 역할을 하는 작고 전용 서비스(예: FastAPI 자체 사용)를 구축하여 REST 엔드포인트를 노출하고 내부적으로 gRPC 서비스를 호출할 수 있습니다.
예제 (가상의 FastAPI 게이트웨이 및 gRPC 서비스를 사용):
GetUser(id)
메서드를 가진 UserService
라는 gRPC 서비스가 있다고 가정해 보겠습니다.
user_service.proto
:
syntax = "proto3"; package users; message GetUserRequest { string user_id = 1; } message User { string id = 1; string name = 2; string email = 3; } service UserService { rpc GetUser (GetUserRequest) returns (User); }
user_grpc_server.py
:
import grpc from concurrent import futures import users_pb2 import users_pb2_grpc class UserServicer(users_pb2_grpc.UserServiceServicer): def GetUser(self, request, context): if request.user_id == "1": return users_pb2.User(id="1", name="Alice", email="alice@example.com") context.set_details("User not found") context.set_code(grpc.StatusCode.NOT_FOUND) return users_pb2.User() def serve(): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) users_pb2_grpc.add_UserServiceServicer_to_server(UserServicer(), server) server.add_insecure_port('[::]:50051') server.start() server.wait_for_termination() if __name__ == '__main__': serve()
api_gateway_fastapi.py
:
from fastapi import FastAPI, HTTPException import grpc import users_pb2 import users_pb2_grpc app = FastAPI() USER_GRPC_SERVER = 'localhost:50051' @app.get("/users/{user_id}") async def get_user_rest(user_id: str): try: with grpc.insecure_channel(USER_GRPC_SERVER) as channel: stub = users_pb2_grpc.UserServiceStub(channel) request = users_pb2.GetUserRequest(user_id=user_id) response = stub.GetUser(request) return {"id": response.id, "name": response.name, "email": response.email} except grpc.RpcError as e: if e.code() == grpc.StatusCode.NOT_FOUND: raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=500, detail=f"gRPC error: {e.details()}") except Exception as e: raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
이 설정에서 FastAPI 애플리케이션은 게이트웨이 역할을 하여 HTTP 요청을 수신하고 이를 전용 UserService
에 대한 gRPC 호출로 전달합니다.
장점:
- 명확한 관심사 분리: 외부 클라이언트는 REST, 내부 서비스 간 통신은 gRPC.
- 확장성: 각 서비스를 독립적으로 확장할 수 있습니다.
- 성능: 내부 통신을 위한 gRPC 이점.
- 유연성: 다른 서비스가 가장 적절한 프로토콜을 사용할 수 있도록 합니다.
단점:
- 복잡성 증가: 추가 계층(API 게이트웨이)이 도입됩니다.
- 운영 오버헤드: 관리하고 배포할 서비스가 더 많습니다.
2. 내부 gRPC 클라이언트가 있는 모놀리식 애플리케이션
기존 Django 또는 FastAPI 애플리케이션이 있고 외부 gRPC 서비스(예: 타사 결제 게이트웨이, 내부 데이터 처리 서비스)와 상호 작용해야 하는 시나리오에서는 기존 웹 프레임워크가 gRPC 클라이언트 역할을 할 수 있습니다.
메커니즘:
- Django/FastAPI 애플리케이션은 평소와 같이 RESTful API를 계속 제공합니다.
- 특정 비즈니스 로직이 gRPC 서비스의 데이터 또는 작업을 요구할 때 Django/FastAPI 애플리케이션은 해당 서비스에 대한 gRPC 클라이언트 호출을 시작합니다.
- gRPC 응답은 처리되어 RESTful API 응답에 통합됩니다.
예제 (외부 gRPC 서비스를 사용하는 FastAPI 앱):
user_service.proto
및 user_grpc_server.py
를 재사용합니다.
fastapi_app_client.py
:
# users_pb2.py 및 users_pb2_grpc.py가 생성되어 사용 가능하다고 가정 from fastapi import FastAPI, HTTPException import grpc import users_pb2 import users_pb2_grpc app = FastAPI() USER_GRPC_SERVER = 'localhost:50051' @app.get("/users/{user_id}") async def read_user_from_grpc(user_id: str): """ 내부적으로 gRPC 서비스를 호출하는 REST 엔드포인트를 노출합니다. """ try: with grpc.insecure_channel(USER_GRPC_SERVER) as channel: stub = users_pb2_grpc.UserServiceStub(channel) request = users_pb2.GetUserRequest(user_id=user_id) # gRPC 호출 수행 response = stub.GetUser(request) return {"id": response.id, "name": response.name, "email": response.email} except grpc.RpcError as e: if e.code() == grpc.StatusCode.NOT_FOUND: raise HTTPException(status_code=404, detail="User not found from gRPC service") raise HTTPException(status_code=500, detail=f"gRPC service error: {e.details()}") except Exception as e: raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
이 패턴에서는 FastAPI 애플리케이션이 외부 클라이언트에 대한 HTTP 서버 역할을 하고 내부/외부 gRPC 서비스에 대한 gRPC 클라이언트 역할을 합니다. 클라이언트 관점에서는 RESTful 인터페이스만 보이기 때문에 통합이 원활합니다.
장점:
- 단순성: 웹 프레임워크가 외부 gRPC 서비스에 직접 연결하는 경우 추가 게이트웨이 계층이 필요하지 않습니다.
- 기존 인프라 활용: 클라이언트 대면 활동에 기본 웹 애플리케이션을 사용합니다.
- 직접 액세스: 애플리케이션 코드는 gRPC 서비스와 직접 상호 작용하여 세밀한 제어를 제공합니다.
단점:
- 잠재적 성능 병목 현상: 많은 REST 엔드포인트에서 gRPC 호출이 필요한 경우 웹 애플리케이션이 병목 현상이 될 수 있습니다.
- 종속성 증가: 웹 애플리케이션에는 이제 gRPC 클라이언트 종속성 및 protobuf 정의가 추가됩니다.
3. Django/FastAPI 내에서 gRPC 서버 실행 (덜 일반적, 특정 사용 사례)
전체 gRPC 서비스를 위해서는 덜 일반적이지만, 특히 비동기 프레임워크를 사용하면 동일한 Python 프로세스 내에서 gRPC 서버와 RESTful API 서버를 모두 실행하는 것이 기술적으로 가능합니다. 이는 긴밀한 결합과 공유 리소스가 필수적인 매우 특정 시나리오나 기존 모놀리식에 gRPC 구성 요소를 점진적으로 도입하는 경우 고려될 수 있습니다.
메커니즘:
- Django/FastAPI 애플리케이션은 HTTP 서버를 실행합니다.
- 동시에 gRPC 서버는 동일한 애플리케이션 프로세스 내에서, 일반적으로 별도의 스레드 또는 비동기 작업 실행기를 사용하여 시작됩니다.
- 두 서버 모두 다른 포트에서 수신 대기하거나 고급 멀티플렉싱을 활용합니다(다른 프로토콜의 경우 덜 일반적).
예제 (다른 포트에서 REST 및 gRPC 모두 제공하는 FastAPI):
이 패턴은 비동기 루프 및 잠재적으로 별도의 스레드에 대한 신중한 관리가 필요합니다. FastAPI에 대한 개념적 개요는 다음과 같습니다.
# 이것은 매우 개념적인 예제입니다. # 한 프로세스에서 두 개의 영구 서버를 실행하려면 신중한 비동기 처리가 필요합니다(예: AnyIO, asyncio.gather 사용). from fastapi import FastAPI import uvicorn import asyncio import grpc from concurrent import futures # users_pb2.py 및 users_pb2_grpc.py가 생성되었다고 가정 class UserServicer(users_pb2_grpc.UserServiceServicer): # ... (이전과 동일) def start_grpc_server_sync(): """별도의 스레드에서 실행될 gRPC 서버를 동기 방식으로 시작합니다.""" server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) users_pb2_grpc.add_UserServiceServicer_to_server(UserServicer(), server) server.add_insecure_port('[::]:50051') server.start() print("gRPC 서버가 포트 50051에서 시작되었습니다") server.wait_for_termination() # 스레드를 활성 상태로 유지 app = FastAPI() @app.get("/") async def read_root(): return {"message": "Hello from FastAPI REST!"} # 일반적으로 gRPC 서버는 별도의 프로세스 또는 전용 워커에서 실행됩니다. # 시연 목적으로, 어떻게 공존할 수 있는지 개념적으로 보여줍니다. # 실제 FastAPI 앱에서는 시스템d, Kubernetes 등과 같은 더 큰 배포 전략과 통합할 것입니다. class ServerManager: def __init__(self): self.grpc_server_future = None async def start_grpc_server_async(self): # 이것은 자리 표시자입니다. 실제 시나리오에서는 `asyncio.start_server`를 사용하거나 # 사용 가능한 적절한 비동기 gRPC 서버 라이브러리와 통합 것입니다. # Python의 공식 gRPC 라이브러리는 주로 스레드 기반입니다. # 간단하게 유지하기 위해 여기서 스레드를 실행합니다. loop = asyncio.get_running_loop() self.grpc_server_future = loop.run_in_executor(None, start_grpc_server_sync) async def shutdown(self): if self.grpc_server_future: # 실제 종료 시 gRPC 서버에 정상 종료를 알립니다. # 이 예시는 작업이 완료되면 실행자 작업을 기다립니다. # 적절한 gRPC 종료는 server.stop(grace_period)을 호출하는 것을 포함합니다. print("gRPC 서버 종료 시도 중...") server_manager = ServerManager() @app.on_event("startup") async def startup_event(): # 별도의 스레드 또는 프로세스에서 gRPC 서버 시작 await server_manager.start_grpc_server_async() @app.on_event("shutdown") async def shutdown_event(): await server_manager.shutdown()
장점:
- 매우 긴밀한 결합: 공유 메모리, 구성 등.
- 배포 단위 감소: 배포할 애플리케이션은 하나뿐입니다.
단점:
- 리소스 경합: 두 서버 모두 CPU, 메모리를 놓고 경쟁할 수 있습니다.
- 관리 복잡성: 단일 프로세스 내에서 두 개의 서로 다른 서버 유형을 관리하기가 더 어렵습니다.
- 디버깅 문제: 한 서버의 문제가 다른 서버에 영향을 줄 수 있습니다.
- 독립적 확장 불가: REST 엔드포인트와 gRPC 엔드포인트를 별도로 확장할 수 없습니다.
- 운영에서 비권장: 일반적으로 운영 복잡성과 독립적 확장성 부족으로 인해 권장되지 않습니다.
올바른 전략 선택
- 마이크로서비스/분산 시스템의 경우: API 게이트웨이를 사용한 서비스 분리가 최고 표준입니다. 명확한 분리, 확장성 및 내부 통신을 위한 성능 이점을 제공합니다.
- 외부 gRPC 데이터가 필요한 모놀리식 애플리케이션의 경우: 내부 gRPC 클라이언트가 있는 모놀리식 애플리케이션은 실용적인 선택입니다. 기존 애플리케이션이 주요 아키텍처 개편 없이 gRPC 서비스를 사용할 수 있도록 합니다.
- 틈새 또는 전환 시나리오의 경우: 전환 중 임시로 동일한 프로세스 내에서 gRPC 서버를 실행하는 것을 고려할 수 있지만, 결국에는 더 많은 문제를 야기할 수 있습니다.
결론
기존 RESTful API 프레임워크와 gRPC 서비스를 통합하는 것은 가능할 뿐만 아니라 종종 매우 유익한 전략입니다. 각 기술의 핵심 원칙을 이해하고 통합 패턴(주로 API 게이트웨이를 사용한 별도의 서비스 또는 모놀리식 내의 내부 gRPC 클라이언트)을 신중하게 선택함으로써 개발자는 REST의 광범위한 액세스 가능성과 사용 편의성을 gRPC의 성능 및 타입 안전성과 결합할 수 있습니다. 이 하이브리드 접근 방식은 조직이 기존 시스템 및 클라이언트 애플리케이션과의 호환성을 유지하면서 특정 사용 사례에 최적화된 인프라를 점진적으로 현대화할 수 있도록 합니다. 이러한 전략을 활용함으로써 개발자는 시간의 시험을 견딜 수 있는 강력하고 확장 가능하며 효율적인 백엔드 시스템을 구축할 수 있습니다.