Skip to main content

Command Palette

Search for a command to run...

REST, GraphQL, RPC 중 무엇을 쓸지 판단이 안 될 때

Updated
REST, GraphQL, RPC 중 무엇을 쓸지 판단이 안 될 때

원문: Ismayil Khayredinov, "REST, GraphQL or RPC — A Decision Paralysis"

약 20년 넘게 AJAX 기능이 브라우저에 탑재된 이래로 전 세계의 개발자들은 이 기술을 한계까지 끌어올리며 기존 인터넷 기반에 도전하고 정적 페이지의 웹을 동적으로 전환하기 위한 창의적 방법을 고안하느라 몹시 분주했습니다.

데이터 교환을 지원하는 API(Application Programming Interfaces)는 수많은 변화를 거쳐 왔고 지금까지도 많은 개발팀이 API 설계와 구현 결정에 많은 어려움을 겪는 것은 당연한 일입니다. 다양한 프로그래밍 언어, 전송 프로토콜, 인프라 아키텍처로 인하여 데이터를 안전히 전달하면서도 변하는 요구사항에 맞게 API를 구현하고 유지보수하는 방법을 찾는 것은 결코 쉽다고 할 수 없습니다.

매일 API를 다루고 API 설계에 대한 논의들을 지켜보면서 제 생각을 글로 남기는 것이 유용할 것이라는 생각이 들었습니다. REST, GraphQL, RPC가 가장 일반적으로 논의되는 API 설계 패턴이므로 이 세 가지에 초점을 맞춰 제 생각과 경험을 간추려 보고자 합니다. 이 모두를 써 본 결과 만병 통치약은 없으며 브라우저 기술의 표준인 W3C와 같은 공통 기반을 갖기에는 아직 멀었다는 생각이 듭니다.

시작하기 전에 몇 가지 가정을 명확히 해두겠습니다.

  • 어떤 면에서 이 세 가지 패러다임을 동일하게 취급하는 것은 오렌지와 사과를 동일하게 여기는 것과 같습니다. 결국 GraphQL은 단지 쿼리 언어 명세이기에 RPC 게이트웨이와 매우 유사한 방식으로 이용될 수 있습니다. 쿼리 측면에서도 자원에 중점을 둔다는 점이 REST와 유사하며 실제로 REST 호출의 일부로 GraphQL 문서를 전송하고 컨트롤러 구현의 일부로 리졸버를 사용하는 데 아무런 지장이 없습니다. 따라서 이 글에서는 GraphQL은 명세를 구현하는, 즉 HTTP를 통해 데이터를 송수신하는 Apollo와 유사한 서버로 생각하겠습니다.

  • RPC라고 하면 gRPC와 같은 특정 구현을 말하는 것이 아닙니다. RPC란 REST에서 일반적으로 사용되는 것처럼 네트워크에서 고유 주소를 가진 자원에서 작업하는 것이 아니라 프로시저 호출을 사용하는 패러다임을 의미합니다. 다른 전송 프로토콜과도 비교를 하겠지만 이 글의 목적상 API는 HTTP를 통한 데이터 전송을 주로 의미하게 될 것입니다. 네트워킹에 대한 고려 사항은 뒤로 미루고 유선으로 데이터가 이동하는 메커니즘보다는 실제 API 설계에 초점을 맞추겠습니다. 따라서 (바이너리 데이터의 양방향 스트리밍을 위해 HTTP/2를 사용하는) gRPC보다는 JSON-RPC 또는 tRPC와 유사하게 RPC 구현을 생각하는 것이 가장 좋습니다.

그럼 이제 의사결정을 할 때 고려할 API 설계의 다양한 측면을 살펴보고 세 가지 패턴이 다음의 요구사항들에 어떻게 부합하는지 알아봅시다.

스키마 정의

API 설계를 선택할 때 중요한 고려 사항 중 하나는 스키마 정의 언어(Schema Definition Language, SDL)를 사용하여 추상적인 용어로 API를 표현할 수 있는 능력입니다. SDL은 설계를 구현 세부 사항과 분리하여 데이터 모델에 집중하고 앞으로 다양한 클라이언트가 API와 상호작용할 수 있는 방식으로 규약을 표현하는 데 도움이 됩니다. SDL을 사용하면 클라이언트 SDK 또는 데이터 전송 객체의 타입을 생성하는 등 일반적으로 수작업이 필요한 일부 번거로운 프로세스를 자동화하는 것이 더 쉬워집니다.

SDL 없이 작업하면 임의의 형식으로, 즉 문서 집합을 최신 상태로 유지 및 발전시키고 클라이언트와 소통하기에 어려운 형식으로 유지하게 되므로 문제가 발생합니다.

SDL을 평가할 때는 개발 과정이 어떤 모습일지 고려하세요.

  • 스키마 우선 접근 방식은 조직의 복잡성을 다루는 대규모 팀에 실용적입니다. 실제로 구현하기 전에 여러 팀이 함께 규약을 정의할 수 있습니다. 이 접근 방식은 계획과 커뮤니케이션에 더 많은 시간이 필요하지만 결과적으로 팀들이 서로 다른 구현 일정을 처리할 필요 없이 독립적이고 병렬적으로 작업할 수 있다는 장점이 있습니다. 또한 이 방식 하에서는 모의 테스트와 테스트가 가능해야 하므로 코드 품질이 향상되며 심지어는 TDD로 개발 흐름을 이끌 수도 있습니다. 스키마 우선 접근 방식의 또 다른 장점은 실제 구현을 위한 세부 사항과는 관계없이 업무 진행 과정이나 클라이언트에 영향을 주지 않으면서 기반 기술을 언제든지 교체할 수 있다는 점입니다. 하지만 이 접근 방식을 사용하려면 모든 사람이 새로운 언어를 배워야 하므로 구문과 사용성이 매우 중요한 고려 사항입니다.

  • 코드 우선 접근 방식은 빠르게 유지보수하면서도 동시에 클라이언트 간에 규약 안전성을 어느 정도 확보하고 SDL 생태계 내 도구의 이점을 활용하려는 소규모 팀에 더 실용적입니다. 코드 우선 접근 방식은 개발을 위해 선택한 도구와 크게 결합되어 있기 때문에 향후에 도구 작성자가 더 이상 유지 보수하지 않거나 조직의 복잡성이 증가하여 팀이 효율적으로 협업하지 못하는 경우 스키마 우선 접근 방식으로 전환하는 데 많은 비용이 소요될 수 있는 위험이 있습니다.

GraphQL은 본질적으로 SDL 사양이므로 API를 표현하는 데 적합합니다. 프로시저 (쿼리 및 뮤테이션)와 입력 및 응답 객체 타입 정의를 위한 기본 지원 기능이 내장되어 있습니다.

RESTRPC는 추상적인 개념이며 표준 SDL은 없지만 HTTP 기반 API를 설명하는 데 사용할 수 있는 몇 가지 범용 프레임워크가 있습니다.

  • OpenAPI (이전의 Swagger)는 API 작성자가 HTTP 요청 및 응답을 YAML 또는 JSON으로 표현하도록 하는 가장 일반적인 SDL 사양입니다. OpenAPI 생태계에는 여러 가지 멋진 개발 도구가 있습니다.

  • 자원보다는 메시지 교환에 중점을 두는 RPC 및 기타 시스템에 대해 고려할 만한 또 다른 옵션은 AsyncAPI입니다. 구문은 OpenAPI와 비슷하지만 HTTP 프로토콜을 넘어서는 요구 사항을 더 잘 표현할 수 있습니다.

또한 SDL을 통하여 표현된 API는 타입 안전성과 직렬화 문제 해결을 도와줍니다. 규약들을 스택 전반에 걸쳐 더 강하게 만들어 주는 것이죠. 각자의 사양에 맞게 구축할 수 있는 여러 직렬화나 유효성 패턴이 있습니다.

  • 프로토콜 버퍼는 구조화된 데이터를 직렬화하기 위한 플랫폼 및 언어 중립적인 메커니즘입니다. OpenAPI로 표현된 모델을 gnostic을 사용하여 프로토콜 버퍼 형식으로 내보낼 수 있어 서버와 클라이언트 간에 데이터를 안정적으로 교환하기가 더 쉬워집니다.

  • JSON-Schema는 직렬화 가능한 데이터를 표현하는 또 다른 표준이며 이러한 데이터에 대한 유효성 검사 요건도 포함하고 있습니다. 변환기를 사용하여 OpenAPI 사양을 JSON-Schema로 변환할 수 있습니다. 요청 수명 주기의 다양한 단계에서 데이터의 유효성을 검사하려는 경우 흥미로운 접근 방식입니다.

  • Modelina 및 기타 도구를 사용하여 OpenAPI, AsyncAPI 및 JSON-Schema에서 모델을 생성할 수 있습니다.

스키마를 정의할 때 API 설계가 향후 통합하려는 다른 시스템과 최대한 호환되도록 보장하는 기존 규칙을 살펴볼 수도 있습니다.

  • JSON-RPC는 RPC 메시지를 구조화하기 위한 규칙입니다.

  • cloudevents는 다양한 형식의 이벤트를 문서화할 때 공통 언어를 찾기 위한 시도이며 다양한 RPC 고려 사항에 유용할 수 있습니다.

  • 이미 존재하는 다양한 필터링, 정렬 및 페이징 규칙 (예: SCIM, OData, CQL)을 고려하세요.

SDL을 다룰 때 염두에 두어야 할 기타 사항은 다음과 같습니다.

  • 모든 클라이언트가 로직, 자원, 관계를 쉽게 이해할 수 있는 방식으로 자원을 설명할 수 있나요?

  • 클라이언트의 역할이나 권한에 따라 인증 요구 사항과 자원에 대한 접근성을 표현할 수 있나요?

  • 애플리케이션에 필요한 모든 데이터 타입 (스칼라, 열거형, 복잡한 유니온)을 지원합니까?

  • HTTP (예: URL 쿼리 및 JSON) 및 기타 프로토콜에 적합한 형식으로 데이터를 직렬화하는 데 필요한 도구가 있습니까?

  • HTTP 프로토콜을 넘어 데이터 모델, 자원 및 절차를 표현할 수 있는지, 즉 동일한 SDL을 사용하여 다른 통신 프로토콜 (예: 메시지 큐 또는 명령줄 인터페이스)에서도 데이터 전송을 설명할 수 있나요?

SDL의 가장 큰 장점은 내외부에서 API 문서로서 기능할 수 있다는 것입니다. 클라이언트가 스키마에 접근할 수 있도록 하면 클라이언트 자체적으로 API 내에서 사용 가능한 자원을 발견할 수 있습니다. GraphQL (예: Apollo Studio) 및 OpenAPI (예: Swagger UI) 스키마를 탐색하고 상호작용할 수 있는 훌륭한 GUI playground가 있습니다. 물론 이러한 도구는 우리가 애용하는 Postman과 잘 통합되어 있으므로 스키마 정의에서 컬렉션을 생성하고 컬렉션을 일부 일반적인 SDL로 내보낼 수도 있습니다.

데이터 모델

기반 데이터 모델의 세부 사항과 복잡성, 클라이언트가 자원에 접근하는 데 필요한 요구 사항을 잘 이해하지 못하면 API 설계에 대한 결정을 내릴 수 없습니다.

관계형 모델의 복잡성

API 설계에서 관계형 모델 복잡성은 중요한 고려 사항이며 API를 통해 복잡한 관계형 모델을 탐색하는 것은 어려울 수 있습니다. 도메인 개체 간의 관계는 특정 접근 방식을 정하는 데 결정적인 요소가 될 수 있습니다.

표준 REST 구현에서 각 자원에는 고유한 네트워크 주소가 있습니다. 예를 들어 GET /users/1과 같은 간단한 GET 요청으로 자원에 접근할 수 있지만 사용자의 친구 목록을 얻으려면 GET /users/1/friends로 이동해야 하는 등 컬렉션과 관계를 표현하려고 하면 상황이 더 복잡해집니다. GET /users/1/friends/friendship-1, GET /friendships/friendship-1, GET /users/2 중에서 무엇에 접근할지가 매우 불분명합니다. 사용자 주소가 사용자 자원의 일부로 반환되어야 하는지 아니면 GET /user/1/addresses 경계에 있는 자체 자원이어야 하는지 등 중첩된 문서를 어디에 넣어야 하는지도 불분명합니다. 이러한 모호성으로 인해 개발 팀은 도메인 경계와 REST 그래프에서의 표현에 대해 끝없는 결정을 내려야 합니다. 도메인 모델의 일관성과 성능 사이에서 타협점을 찾아야 하기 때문에 API 클라이언트는 어디를 봐야 할지 정확히 알기 어렵습니다.

그 외에도 REST는 그래프 탐색의 부담을 API 클라이언트에 떠넘깁니다. 2차 관계 목록을 해결하려고 시도해 본 적이 있다면 수천 개는 아니더라도 수백 개의 API 요청이 연쇄적으로 발생하는 것이 얼마나 악몽 같은 일인지 잘 알고 계실 것입니다. 게다가 양방향 관계를 다루는 경우 순환성뿐만 아니라 노드 반복도 고려해야 하므로 무한 루프를 피하기 위해 어떤 형태의 캐싱과 중복 제거를 구현하는 것은 클라이언트의 책임입니다.

이러한 어려움을 조금이라도 덜기 위해 REST API 작성자는 자원이 요청될 때 관계를 해결할 수 있는 창의적인 솔루션 (일명 해킹)을 사용합니다. 예를 들어 Stripe API는 expand 매개변수를 사용합니다. 이는 탐색 문제를 해결하지만 비결정적 응답 타입으로 이어지며 이 역시 API 클라이언트에서 처리해야 하므로 많은 타입 변환과 강제 형변환을 수행해야 합니다.

소셜 네트워크와 복잡한 소셜 그래프의 등장으로 REST는 관리하기가 상당히 어려워졌습니다. GraphQL은 이름에서 알 수 있듯이 여섯 단계로 분리된 인간 사회 전체를 나타내는 복잡한 소셜 그래프를 탐색하기 위한 방법으로 Facebook에서 만들었습니다. Facebook은 다수의 행위자와 그들 사이의 다방향 관계로 인한 사회적 복잡성에 내재된 문제를 해결하고자 했습니다. API 설계에 이점이 있는지 여부를 평가할 때 이를 염두에 두어야 합니다.

GraphQL은 뛰어난 단순성을 보입니다. 클라이언트가 필요한 데이터를 정확하게 지정하고 단일 요청으로 데이터를 가져올 수 있도록 함으로써 그래프를 탐색하고 연결하는 책임을 서버에 둡니다. 스키마 설계에 타입과 관계를 포함하면 필드 리졸버를 사용해 필요한 데이터를 추출하고 복잡한 쿼리를 순회할 수 있습니다. 이는 항상 저렴하지는 않으며 추가 문제와 도구 (예: n+1 문제를 해결하기 위한 데이터 로더)가 필요하지만 서버가 여러 데이터 저장소를 사용하여 속성과 관계를 집계할 수 있으므로 API 클라이언트는 이러한 구현 세부 사항과 무관하게 사용할 수 있습니다.

RESTGraphQL의 중심에는 모두 자원과 그 표현을 초점으로 둡니다. 이는 주로 데이터베이스에서 개체를 표현하는 방법과 밀접하게 연관되어 있습니다. 어떤 의미에서 이러한 개체는 관찰자가 누구인지에 관계없이 항상 플라토닉한 형태로 존재합니다. 이는 모든 클라이언트가 동일하게 생성되지 않는 특정 시스템에서 문제가 될 수 있습니다. 사용 사례와 요청자 역할을 고려하지 않는 정적 스키마나 그래프를 사용하면 설계에서 모든 종류의 문제가 발생할 수 있습니다. 직원 모델에 회계사 역할만 접근할 수 있는 급여 속성이 있다고 가정할 때 API 설계에서 이를 어떻게 표현하고 구현해야 할까요? 속성을 선택 사항으로 만들까요? 선택 사항인 경우 (저장소에 데이터가 없거나 이 요청자에 대해 접근할 수 있는 데이터가 없는 경우와 같이) 값이 없는 것이 무엇을 나타내는지 어떻게 알 수 있을까요? HTTP 오류를 던지는 또 다른 경계를 REST에 생성하나요? GraphQL에서 null 또는 오류를 전송할까요? 이러한 모호성은 클라이언트 측에서 추가적인 복잡성과 불확실성을 초래할 수 있으므로 API 설계 단계에서 이를 고려해야 합니다.

API 설계에 대한 자원 중심 접근 방식은 데이터 스토리지 모델 측면에서 API를 생각하게 만드는 경향이 있습니다. 어떤 경우에는 괜찮으나 API를 데이터베이스와 너무 결합하면 문제가 될 수 있습니다. 우리는 너무 자주 ORM 모델을 API 모델로 재사용하고 API를 통해 데이터베이스와 직접 인터페이스하기 위해 Hasura, Prisma GraphQL, PostgREST 등과 같은 도구에 의존합니다. 이러한 도구들은 많은 시간을 절약하도록 해주지만 아키텍처 계층 간의 경계를 모호하게 만들고 API 클라이언트가 변경 사항에 취약하게 만듭니다. 클라이언트의 사용 방법과 위치에 따라 이러한 접근 방식은 데이터 무결성에 위험을 초래할 수 있으며 데이터 및 스키마 마이그레이션 실행, 새 SDK 버전 생성 및 태그 지정, 모든 클라이언트에 변경 사항 전파의 경쟁 조건을 수정할 수 있는 솔루션이 필요합니다. 데이터 스토리지 엔진이 어떤 버전 관리도 지원하지 않는 경우 API 버전 관리는 문제가 됩니다. 그 외에도 복잡한 접근 제어는 역할과 권한을 관리하기 위해 이러한 도구에 연결해야 하는 골칫거리가 됩니다.

반면에 RPC는 자원과 그 표현에 대한 의견이 없습니다. API 작성자는 데이터 모델을 재현하려는 시도에서 벗어나 사용 사례에 집중할 수 있으며 시스템 내 클라이언트와 행위자의 특정 요구사항에 맞게 절차를 조정할 수 있습니다. 클라이언트가 데이터에 대한 발언권이 거의 없는 REST와 (Cambridge Analytica처럼) 클라이언트가 원하는 만큼의 데이터를 무차별적으로 요청할 수 있는 GraphQL과 달리 RPC는 서로 다른 API 애플리케이션에서 발생하는 다양한 요구 사항을 수용할 수 있습니다. 그래프를 풀기 위한 리졸버부터 요청이나 응답 흐름을 관리하는 라우팅 미들웨어와 동형 경로 및 쿼리 수준 권한 제어에 이르기까지 모든 방면의 장점을 최대한 뽑아내는 방식으로 RPC 서버를 활용할 수 있습니다. RPC API는 변경 불가능한 요구 사항을 중심으로 구축될 수 있으며 이전 버전과 호환되는 방식으로 시간이 지남에 따라 변화하는 요구 사항을 처리하도록 발전할 수 있습니다. RPC를 사용하면 데이터 지속성이나 저장 문제 및 기타 구현 세부 사항에서 API 문제를 분리할 수 있으므로 데이터 스키마가 변경될 때마다 변경할 필요가 없는 보다 미래 지향적인 API 설계로 이어질 수 있습니다.

GraphQL은 클라이언트의 요구 사항을 예측하기 어려운 대규모 API에는 좋은 솔루션입니다. 반면 클라이언트가 쿼리를 작성하고 클라이언트에 반환해야 하는 모든 필드를 열거하는 대신에 응답 타입을 RPC 스키마의 일부로 정의할 수 있다는 점에서 RPC는 소규모 프로젝트에 좋은 대안이 될 수 있습니다. 본질적으로 REST도 비슷한 맥락이지만 이는 각 자원이 고유한 네트워크 로케이터에서 사용 가능해야 한다는 패러다임의 본성에 반하는 것입니다.

데이터 업데이트

솔직히 말해서 REST를 통해 데이터를 업데이트하는 것은 골치 아픈 일입니다. 투명하게 만들어야 할 다양한 HTTP 메서드는 오히려 온갖 종류의 의문을 불러일으킵니다.

  • soft-delete하거나 보관하려면 어떤 메서드를 사용해야 하나요? 어떤 플래그가 포함된 DELETE 요청이어야 할까요, 행동명이 포함된 POST 요청이어야 할까요 (사실상 RPC 요청이 되나요), 아니면 삭제되거나 보관된 객체의 속성을 업데이트하는 PATCH 요청이어야 할까요?

  • 부분 업데이트는 어떤가요? PUTPATCH 중에서 무엇을 사용해야 할까요? 계정 이메일 변경과 같이 특정한 부작용이 있는 업데이트는 어떻게 해야 하나요? 여전히 PATCH 요청으로 보내야 하나요 아니면 행동 이름과 함께 POST로 보내야 하나요?

  • 특정 행위자만 특정 변경을 할 수 있는 부분 업데이트의 세분화는 어떻게 해야 하나요? REST API 설계에서 이 로직을 어떻게 분할하고 설명할 수 있을까요?

  • 실제로 자원이 첨부되어 있지 않은 로직은 어떻게 해야 할까요? 예를 들어 비밀번호 재설정 흐름의 여러 단계는 어디에 위치해야 할까요? REST API의 루트에 있어야 할까요 아니면 API의 일부 경계에 있어야 할까요?

  • 중첩된 문서는 어떻게 되나요? 중첩된 업데이트에 트랜잭션 안정성이 필요한 경우에는 어떻게 해야 하나요?

사실 REST API를 중심으로 SDK를 구축할 때마다 get/post/put/patch/delete 메서드로 직접 작업하기보다는 deleteUser() 또는 capturePayment()와 같은 프로시저 호출을 사용하여 다양한 작업을 설명하게 됩니다. 개인적으로는 REST는 더 이상 오늘날 애플리케이션 설계에 적합하지 않다고 생각합니다. 자원 중심적인 API 설계에 포함하기에는 요구 사항이 너무 복잡합니다.

프로시저 호출은 현대 애플리케이션의 복잡성과 더 잘 어울리므로 API 설계자는 제한된 RESTful 엔드포인트 집합에서처럼 가능한 모든 시나리오를 수용하려고 하기보다는 특정 사용 사례를 처리할 수 있습니다. 사용자 비밀번호 변경은 사용자 이메일을 변경하거나 사용자를 뉴스레터에 가입시키는 것과는 다릅니다. 세 가지 모두 동일한 자원의 일부일 수 있지만 비밀번호를 업데이트하고 사용자를 뉴스레터에 가입시키는 것을 같은 요청으로 보는 경우는 거의 없습니다. 이 모든 절차에는 분명한 부작용이 있으며 사용자 지정 유효성 검사 및 권한 확인이 필요할 수 있습니다.

RPC는 CQRS 원칙과 잘 맞습니다. 프로시저 호출을 쿼리와 명령으로 생각하면 전송 프로토콜 고려 사항이 무의미해집니다. HTTP 컨트롤러는 페이로드를 수신하고 유효성을 검사한 다음 쿼리 및 명령 버스로 전달합니다. 따라서 명령줄, 메시지 대기열, 챗봇 등 프로시저 호출을 수신할 수 있는 모든 전송에 프로시저 호출을 쉽게 재사용할 수 있습니다.

GraphQL 뮤테이션은 사실상 RPC입니다. 예측할 수 없는 유전자 변형 같은 것을 떠올리게 하는 끔찍한 이름과는 달리 매우 유연합니다. 그러나 무엇이 뮤테이션으로 간주되어야 하는지에 대한 의문이 남습니다. 이메일을 보내거나 문서를 다운로드하는 것은 실제로 아무것도 변화를 일으키지 않는 절차인데 그래프에 포함해야 할까요?

API 설계 시 고려해야 할 또 다른 중요한 사항은 데이터 업데이트가 동기식인지 비동기식인지 여부입니다. 분산 시스템에서는 동기화가 항상 가능한 옵션은 아니므로 API 클라이언트에 지속성 이벤트를 전달하는 방법과 이를 API 스키마 및 문서에 반영할 방법을 미리 계획해야 합니다. 클라이언트의 위치에 따라 웹훅, 웹소켓, 서버 측 이벤트, 푸시 알림 등 어떤 기술을 사용할지 결정할 수 있습니다. 비동기 이벤트 중심 API에 대한 흥미로운 글을 읽어 보세요.

분산 시스템에 대해 말하자면 GraphQL을 사용하는 것이 어떤 의미를 가질 수 있는지도 고려해야 합니다. 한편으로 GraphQL은 다양한 소스의 데이터를 집계하는 데 적합하며 마이크로서비스의 게이트웨이로 사용할 수 있습니다. 그러나 다른 한편으로는 스택 전체에서 GraphQL을 다른 API 설계와 혼합하거나 여러 개의 GraphQL 서버를 사용하는 경우 관리가 어려울 수 있습니다. 그래프의 무결성을 보장하기 위해 서버를 연결하거나 연합하는 방법을 고려해야 합니다.

데이터 타입

API 설계와 관련하여 API가 전송하는 직렬화 가능한 데이터 타입과 관련하여 몇 가지 고려해야 할 사항이 있습니다.

  • 사용자 정의 스칼라 타입에 대한 직렬화 요구 사항을 어떻게 전달할 것인가?

  • 혼합 타입과 oneOf 로직을 어떻게 표현할 것이며 그러한 유니온 타입을 어떻게 구별할 것인가?

모든 스칼라가 똑같이 만들어지는 것은 아닙니다. 문자열과 숫자 타입으로 가득 찬 API 스키마는 악몽과 다름없습니다. 특히 해당 속성에 유효성 검사가 첨부된 경우 API 클라이언트가 { contact: string }이 무엇을 의미하는지 추측해서는 안 되므로 규약은 요구 사항을 명시해야 합니다.

  • 문자열 식별자 (예: UUID, Email, IPv6, ISBN 등)를 사용하고 사용자 지정 문자열 패턴을 명시하세요.

  • 정수 식별자 (예: Year, Timestamp 등)를 사용하고 사용자 지정 정수 범위를 명시하세요.

  • 부동 소수점 식별자 (예: Lat, Long 등)를 사용하고 사용자 지정 부동 소수점 범위와 정밀도를 명시하세요.

  • 열거형을 사용하여 스칼라 옵션의 유한 목록을 표현하세요.

  • 단일 날짜 형식 (예: ISO 날짜 문자열 또는 유닉스 타임스탬프)을 채택하세요.

GraphQL과 OpenAPI는 모두 스칼라 타입 세분화를 지원하지만 직렬화 문제는 서버와 클라이언트에 맡기므로 구현 시 사용자 정의 스칼라 타입 직렬화를 고려해야 합니다. GraphQL은 OpenAPI와 달리 스키마 수준에서 유효성 검사 요구 사항을 문서화하는 기본 접근 방식을 제공하지 않으므로 일부 런타임 오류 처리에 대비하세요. 비표준 지시문을 사용하여 유효성 검사 제약 조건서식 지정 요구 사항을 표현하려는 시도가 있지만 이를 직접 구현해야 할 겁니다.

API 설계에서 데이터 모델링의 또 다른 중요한 측면은 혼합 타입입니다. API가 라이브러리에서 사용 가능한 항목의 카탈로그를 제공한다고 가정해 보겠습니다. 책, 잡지, 영화, 카세트, CD 등을 모두 고유한 데이터 모델로 처리해야 할 수도 있습니다. 설계에 따라 API 클라이언트는 이러한 모델을 구분하여 올바른 타입을 추론하고 그에 대한 로직을 구축해야 할 수도 있습니다.

OpenAPI 사양은 모든 타입의 유니온을 내장 지원합니다. 반면에 GraphQL은 구별되는 유니온과 혼합 타입에 대해서는 그다지 유용하지 않습니다. 혼합 스칼라 타입은 사용할 수 없습니다. GraphQL은 유니온을 지원하지만 모두 동일한 인터페이스를 공유해야 하며 다양한 타입의 필드가 겹치지 않아야 합니다. 이러한 제약 조건을 해결해야 하며 항상 가능한 것은 아니므로 결국 타입 안전성에 그다지 좋지 않은 스칼라 JSON 타입을 사용하게 됩니다.

이러한 한계는 구조화되거나 리치 텍스트로 작업할 때 가장 분명하게 드러납니다. 구조화된 텍스트 (예: PortableTextSlate)를 제공하는 헤드리스 CMS로 작업하는 경우 API 스키마에서 이러한 콘텐츠 모델을 어떻게 표현할지 고민해야 합니다.

필터링, 정렬, 페이지네이션

API를 설계할 때 일반적으로 간과하는 것은 필터링, 정렬, 페이지네이션에 대한 표준화된 접근 방식입니다. 작은 데이터 집합으로 시작하면 수백만 개의 레코드가 있는 미래를 예측하지 못하고 이러한 문제를 무시하게 되며 나중에는 새로운 요구 사항을 수용하기 위해 API 스키마를 다시 구축하게 될 수도 있습니다.

이러한 문제에 대한 하나의 표준 접근 방식은 없으며 각기 다른 API가 서로 다른 방식으로 문제를 해결합니다. SCIM, OData, CQL에서 몇 가지 아이디어를 살펴보세요.

필터링과 관련해서는 Prisma.io에서 영감을 얻었습니다. Prisma는 데이터베이스 쿼리에 대한 아름다운 접근 방식을 설계했지만 그 접근 방식은 API 설계에도 적용될 수 있습니다. 이러한 필터는 직렬화가 가능하기 때문에 HTTP 요청과 쉽게 통합될 수 있으며 대규모 데이터세트 필터링에 대한 광범위한 요구 사항을 충족할 수 있습니다.

페이지네이션 설계는 기반 데이터 모델과 데이터베이스가 수신하는 트래픽의 양에 따라 결정되는 경우가 많습니다. 자주 변경되는 데이터세트는 커서 기반 접근 방식이 유용할 수 있지만 그렇지 않은 데이터세트는 오프셋 접근 방식이 꽤 유용할 수 있습니다. 이 주제에 대한 흥미로운 글을 참고하세요.

여기서 한 가지 주목해야 할 점은 GET 요청과 POST 요청을 구분하는 방식입니다. REST와 초기 HTTP 원칙에 근거하여 GET 요청을 쿼리에 사용하고 POST 요청을 데이터 업데이트에 사용하는 이러한 독단적인 접근 방식은 대규모 필터를 다루는 것을 까다롭게 만듭니다. REST 접근 방식의 찬성 입장을 살펴보면 캐시 가능성을 자랑하는데 솔직히 말해서 마지막으로 API 요청을 브라우저 캐시나 CDN에 의존한 적이 언제였나요? 본문 페이로드 대신 URL 쿼리 매개변수를 사용하면 세상을 더 나은 곳으로 만들 수 있다는 생각은 솔직히 구시대적인 발상입니다. URL 직렬화는 형편없습니다. URL은 길이가 제한되어 있습니다. 따라서 이 교리를 폐기하고 모든 API 요청에 JSON 페이로드가 포함된 POST 요청을 사용합시다. 이것이 바로 URL 쿼리 길이 제한에 직면한 GraphQL 클라이언트가 결국 하게 되는 일입니다. 데이터를 가져오는 것이 아니라 서버에 프로시저를 게시하고 서버로부터 응답을 받는다고 RPC 관점에서 생각하면 됩니다.

환경설정 및 툴

API를 제공하는 데 어떤 서버 기술을 사용할지와 어떤 클라이언트를 사용하여 접근할지를 고려하지 않고는 API 설계를 결정할 수 없습니다.

REST는 HTTP 프로토콜과 함께 발전해 왔기 때문에 일련의 범용 표준을 기반으로 합니다. 모든 서버 및 클라이언트 기술은 HTTP를 통해 통신할 수 있으며 어떤 프로그래밍 언어나 서버 인프라를 선택하든 REST 서버를 오케스트레이션하는 도구는 매우 풍부하기 때문에 스택은 그렇게 관계가 없습니다.

RPC 구현은 다양합니다. 각기 다른 아이디어를 제공하는 다양한 프레임워크가 있습니다. gRPC는 환경에 구애받지 않지만 양방향 통신 및 프로토콜 버퍼 사용과 관련하여 미묘한 차이가 있습니다. tRPC는 REST와 GraphQL을 차용하여 만들어진 풀스택 타입스크립트 구현으로 가장 인기 있는 API 도구 중 하나입니다. ZeroCUCall 등도 있습니다. 궁극적으로 REST 툴을 사용하여 RPC API를 구축하는 데 방해가 되는 것은 아무것도 없습니다. REST가 가지고 있는 번거로움을 없애고 모든 쿼리와 명령에 적절한 이름을 부여하고 표준 HTTP 툴을 사용하여 유선으로 전송하기만 하면 됩니다. REST와 유사하게 URL 세그먼트로 프로시저의 네임스페이스를 지정할 수 있지만 자원 ID를 URL 구조에 구워 넣을 필요도 없고 복잡한 HTTP 메서드를 처리할 필요도 없습니다.

GraphQL의 아이디어는 놀라울 정도로 간단합니다.

  • 각 클라이언트는 그래프 노드를 매우 간단한 형태로 표현하기 위해 특별히 설계된 쿼리 언어를 사용하여 필요한 데이터를 정의합니다.

  • 서버는 쿼리를 구문 분석하며 루트 노드에 접근할 수 있고 반환 값을 계산하기 위하여 다양한 데이터 저장소 및 또는 외부 서비스에 접근할 수 있는 리졸버를 통해 요청된 각 필드를 전달합니다.

새로운 언어를 도입할 때 발생하는 문제 중 하나는 쿼리를 다양한 서버 기술에서 처리할 수 있는 표준 형식으로 구문 분석할 수 있는 렉서가 필요하다는 점입니다. 대부분의 API 클라이언트가 브라우저에서 작동하기 때문에 브라우저 호환성을 고려하여 자바스크립트 생태계에서 GraphQL 툴은 프런트엔드 애플리케이션의 필요에 따라 발전해 왔습니다. GraphQL은 프런트엔드에서 매우 쾌적한 경험을 제공하지만 클라이언트를 설정하고 캐싱 및 조각화와 같은 기타 뉘앙스를 이해하는 것부터 해당 클라이언트에서 사용할 수 있는 쿼리 작성 및 구문 분석에 이르기까지 서버 간 통신에 관한 도구는 아직 부족합니다. GraphQL은 표준 HTTP 프로토콜에 맞춰 설계되었지만 문제는 GraphQL 자체가 표준이 아니며 다목적 API 설계를 위한 최상의 선택이 아닐 수 있다는 것입니다.

가시성 및 확장성

API 상태와 성능은 API 설계에 대한 결론을 도출해 낼 중요한 지표입니다. 이러한 지표를 지속적으로 관찰하고 지연 시간 및 성능 문제와 런타임 오류에 대응하는 능력은 의사 결정의 동기를 부여하는 요인이 되어야 합니다.

단일 진입점 API는 단순하지만 통합 가시성 및 확장성 측면에서는 꽤 까다로운 문제를 일으킵니다. 추가적인 추적 도구 없이는 GraphQL 또는 JSON-RPC API의 상태를 파악하는 것이 상당히 어려울 수 있습니다. REST는 어떤 자원에 접근했는지, 몇 번이나 접근했는지, 요청을 처리하는 데 얼마나 시간이 걸렸는지에 대한 자세한 로그를 제공할 가능성이 높지만 엔드포인트가 하나뿐인 API에 관하여 동일한 통계를 얻는 것은 불가능합니다. 대신 동일한 엔드포인트에 대한 수백만 개의 로그 항목이 생성되며 작업이나 메서드 이름으로 추가 로깅을 설정하지 않으면 해당 요청이 무엇인지 알기 어렵습니다. HTTP 상태 코드를 믿지 않는 GraphQL에서는 상황이 더욱 악화되어 모든 요청이 200 상태 코드로 반환되기 때문에 추적과 디버깅이 매번 더 복잡해집니다.

단일 진입점을 사용하는 것도 DevOps의 문제입니다. 환경에 따라 지연 시간이 더 길거나 더 많은 계산 자원이 필요한 API 엔드포인트에 더 많은 자원을 할당하고 싶을 수도 있지만 대신 전체 API 서버의 종속성과 메모리 풋프린트의 영향을 고려하여 전부 확장해야 할 수 있습니다.

또한 단일 진입점으로 인해 특정 요청을 재라우팅하고 캐싱, 가시성, 부하 분산, 암호화, 접근 제어 및 기타 문제를 지원하는 역방향 프록시의 이점을 활용하기 어렵습니다.

오류 처리

HTTP 코드는 너무 일반적입니다. 몇 년 전에 쓴 글에서 언급했듯이 API는 클라이언트와 사용자에게 문제를 전달할 수 있어야 합니다. 어떤 API 설계를 선택하든 오류를 클라이언트에 전달하고 상황에 맞는 표준화된 접근 방식을 구축해야 합니다.

GraphQL의 오류 처리는 두 가지 면에서 축복이자 저주입니다. 한편으로는 매우 상세하고 개별 필드 리졸버의 오류를 보고할 수 있습니다. 그러나 성공과 오류 상태를 단일 응답으로 병합하면 클라이언트가 네트워크 오류, 불변성, 스키마 유효성 검사 문제, 실제 성공 응답을 구분하기가 훨씬 더 어려워지므로 디버깅 및 오류 처리를 번거롭게 합니다. GraphQL 사양에 따르면 개별 리졸버가 오류를 보고하더라도 (Apollo와 같은 GraphQL 서버가 부분 응답을 방지하지 않는 경우가 아니면) 부분 성공 응답이 사실상 가능하므로 오류와 함께 null 필드 값을 조정하는 것은 상당히 번거로운 작업입니다. 따라서 서버가 오류를 처리하는 방법과 클라이언트가 오류를 이해하는 방법을 고려해야 합니다.

앞서 말한 것처럼 단일 진입점이 있는 시스템에서 오류를 추적하는 데 도움이 되는 추가적인 도구를 설정할 수 있습니다.

버전 관리

API에 버전 관리가 필요한지 여부는 주로 사용상의 문제입니다. API를 사내에서 사용하고 클라이언트를 최신 상태로 유지하면서 API를 발전시킬 수 있다면 문제가 되지 않을 수 있습니다. API를 외부에 배포하는 경우에는 어떤 형태로든 버전 관리를 고려할 수 있습니다.

API가 주로 데이터베이스 모델의 프록시로 존재하는 경우 버전 관리가 다소 심각한 문제가 될 수 있습니다. 데이터베이스 스키마가 변경되면 이전 버전과의 호환성을 유지할 수 있는 방법이 딱히 없으며 특히 API가 데이터베이스 모델에서 자동 생성되는 경우에는 더욱 그렇습니다. 따라서 도구를 선택할 때 신중하게 고려하세요.

앞서 언급한 것처럼 RPC는 스토리지 모델을 API 모델에서 분리하여 더 많은 유연성을 제공하는 흥미로운 옵션입니다. 새로운 프로시저를 도입하고 이전 프로시저를 폐기함으로써 버전을 관리하지 않고도 API를 발전시킬 수 있습니다. GraphQL은 새 필드를 추가하고 이전 필드를 더 이상 사용하지 않는 동일한 접근 방식에 의존하여 버전 관리를 생략하는 경향이 있습니다. 그러나 RPC를 사용하면 요청 라우팅을 더 잘 제어할 수 있고 버전 관리를 여전히 도입할 수 있다는 차이가 있습니다. GraphQL을 사용하면 버전 관리 기능이 제한되어 이전 버전의 그래프를 제공하는 다른 컨테이너를 생성해야 할 수도 있습니다. 게다가 때때로 오래된 코드를 제거해야 하므로 클라이언트에 중요한 변경 사항을 전달하는 방법도 추가로 필요합니다.

접근 제어

접근 요구 사항을 고려하는 데 시간을 투자하는 것이 좋습니다. 경로나 프로세스 수준, 자원 수준, 필드 수준 접근이 필요한지 여부는 API 설계 선택에 큰 영향을 미칠 수 있습니다.

자원 수준 검사로 충분하더라도 모든 사용자에게 자원이 동일하게 표시되는지 여부를 고려해야 합니다. 접근 제어에 더 세분화된 수준이 필요한 경우에는 이러한 요구 사항을 클라이언트에 전달할 방법을 찾아야 할 수도 있습니다. 서버에서 어떤 경로 (전체 자원 접근 방지, 필드 값 무효화, 필드 접근 오류 전달 등)를 취하든 그 결과에 대한 대처는 클라이언트의 책임입니다.

GraphQL은 필드 수준 접근이 필요한 경우 매우 편리하지만 (비표준 지시문을 제외하고는) 접근 요구 사항을 문서화하기 위한 SDL 수준의 표준이 부족합니다. REST API는 자원 표현을 분기할 수 없기 때문에 높은 수준의 세분화가 필요한 애플리케이션에는 이상적인 솔루션이 아닐 수 있습니다. RPC는 특정 요구 사항을 염두에 두고 API를 설계할 때 어느 정도 유연성을 제공할 수 있습니다.

결론

API 설계는 애플리케이션의 고유 특성과 시스템의 다른 참여자를 이해하는 제품 발견 단계에서 주도해야 하는 어려운 선택입니다.

여러분의 상황에 따라 판단하세요.

REST를 사용하세요.

  • CRUD 작업을 사용하여 데이터베이스와 상호작용하는 API를 구축하는 경우

  • 데이터 모델이 관계에 대한 부담이 적은 경우

  • 사용자 역할 및 접근 요구 사항이 제한되어 있는 경우

GraphQL을 사용하세요.

  • 여러 데이터 소스나 서비스를 연결하기 위한 게이트웨이를 구축하는 경우

  • 데이터 모델이 관계가 많고 그래프를 나타내는 경우

  • 고도로 세분화된 접근 요구 사항이 있는 경우

  • 주로 프런트엔드 애플리케이션을 대상으로 하는 API를 구축하는 경우

  • 다양한 데이터 요구 사항을 가진 많은 수의 API 클라이언트가 있는 경우

다음과 같은 경우 RPC를 사용하세요.

  • 이름 지정 및 라우팅 규칙을 유연하게 사용하려는 경우

  • 의미론적으로 읽기 쉬운 API를 구축하려는 경우

  • 다양하고 변동성이 크거나 복잡한 사용 사례를 처리해야 하는 경우

  • 서비스가 HTTP 프로토콜 외부에서 상호작용하는 경우

  • 구현 및 데이터 저장 문제와 분리된 강력한 미래 지향적 API를 구축하고자 하는 경우

제 개인적인 의견을 묻는다면 미래는 RPC라고 생각합니다. 저는 프런트엔드 개발에는 GraphQL을 좋아하지만 서비스 간 커뮤니케이션에는 이상적인 솔루션이 아니라고 생각하며 일단 발을 들여놓으면 발목을 잡아서 추론하기가 훨씬 더 어려워집니다. 반드시 GraphQL을 읽기 모델의 프록시로 사용하되 내부 서비스의 일부로 혼합하여 사용하지 마세요. REST는 그냥 쉬게 내버려 두세요.

저와 의견이 맞지 않는다면 댓글로 남겨 주세요. 제 글로 누군가가 행동하도록 이끄는 것은 언제나 기쁜 일입니다.

More from this blog

나의 오픈 소스 시작 이야기

원문: TkDoDo, “My Open Source Origin Story“ 가끔씩 제가 받는 질문이 하나 있는데, 바로 오픈 소스와 리액트 쿼리(React Query)를 어떻게 시작하게 되었는지입니다. 저의 기본 원칙은 어떤 질문을 세 번 받으면 더 이상 답변할 필요가 없도록 질문에 대해 글로 쓴다는 것입니다. 하지만 이 질문은 주로 직접 만났을 때 받는 질문이라 글로 작성할 생각을 한 적이 없었습니다. 최근에 오프라인 컨퍼런스에 더 많이 참...

Jul 30, 2025
나의 오픈 소스 시작 이야기

이더넷이란?

원문: baeldung, “What Is Ethernet?“ 1. 소개 이 튜토리얼에서는 이더넷(Ethernet)과 이를 통해 이루어지는 데이터 전송에 대해 알아보겠습니다. 2. 이더넷이란? 이더넷은 근거리 통신망(LAN) 또는 광역 네트워크(WAN) 내에서 장치들이 데이터를 주고받고 통신하기 쉽게 만들어 주는 널리 사용되는 기술입니다. 컴퓨터, 프린터, 서버는 물론 스마트 홈 기기까지도 이더넷으로 연결됩니다. 가정이나 사무실처럼 제한된 공간...

Jul 20, 2025
이더넷이란?

포스트 개발자 시대

원문: Josh W. Comeau, "The Post-Developer Era" 2년 전 2023년 3월, "프런트엔드 개발의 종말"이라는 제목의 블로그 글을 발행했습니다. 이는 OpenAI가 GPT-4 쇼케이스를 발표한 직후였고, 당시 업계 분위기는 머지않아 인간 소프트웨어 개발자는 필요 없어지고 앞으로는 소프트웨어 개발을 AI가 전담하게 될 것이라는 전망이 지배적이었습니다. 저는 이런 주장에 회의적이었고 그 블로그 글에서 소프트웨어 개발...

Jul 10, 2025
포스트 개발자 시대

널리 사용되는 네트워크 프로토콜

원문: Subham Datta, "Popular Network Protocols" 1. 개요 이 튜토리얼에서는 가장 널리 사용되고 인기 있는 네트워크 프로토콜들을 소개합니다. 2. 네트워크 프로토콜 소개 의사소통과 정보 교환은 현대 사회에서 가장 중요하고 강력한 역량입니다. 컴퓨터 네트워킹이란 여러 대의 컴퓨터와 장치를 케이블이나 위성을 통해 서로 연결하여, 거리와 상관없이 정보·자원·데이터베이스 등을 공유할 수 있게 하는 것을 말합니다. 네...

Jun 20, 2025
널리 사용되는 네트워크 프로토콜

커맨드 라인에 편해지는 법

원문: Julia Evans, "What helps people get comfortable on the command line?" 가끔 커맨드 라인을 써야 하는 친구들과 이야기하다 보면 많은 이들이 여전히 터미널을 두려워하고 있다는 걸 느낍니다. 그럴 때마다 어떤 조언을 할지 잘 모르겠더라고요. 저는 워낙 오래전부터 터미널을 써왔기 때문이죠. 그래서 Mastodon에 이렇게 물어봤습니다. 최근 1~3년 사이에 터미널 공포(?)를 극복한 분...

Jun 10, 2025
커맨드 라인에 편해지는 법
C

CodeSnap

84 posts

한국어로 전달하는 웹 개발 번역 매거진