Search

When to use multiple endpoints in GraphQL

Tags
GraphQL
Last edited time
2025/02/23 14:00
2 more properties

1. 서론

GraphQL은 데이터를 조회하기 위해 단일 엔드포인트를 사용하는 것이 기본입니다. 그러나 각 커스텀 엔드포인트가 사용자 지정 스키마를 노출하도록 하여 여러 엔드포인트를 사용하는 것이 적절한 상황도 있습니다. 이를 통해 단순히 접근하는 엔드포인트만 변경함으로써 서로 다른 사용자나 애플리케이션에 대해 개별적인 동작을 제공할 수 있습니다.

2. GraphQL에서 여러 엔드포인트를 사용하는 경우

GraphQL에서 여러 엔드포인트를 노출하는 것은 REST를 구현하는 것과는 다릅니다. REST의 각 엔드포인트는 미리 정의된 리소스 또는 리소스 집합에 접근을 제공하지만, 여러 GraphQL 엔드포인트는 각 스키마 내의 모든 데이터에 접근할 수 있게 하여, 필요한 데이터를 정확하게 조회할 수 있도록 합니다.
이는 기본적으로 GraphQL의 동작 방식과 동일하지만, 이제는 서로 다른 스키마에서 데이터를 접근할 수 있는 능력을 제공합니다. 또한, 이 방식은 여러 데이터 소스를 하나의 통합 그래프로 결합하는 스키마 스티칭(schema stitching)이나 페더레이션(Federation)과도 다릅니다. 스티칭과 페더레이션은 여러 스키마를 하나의 큰 스키마로 통합하여, 서로 충돌할 경우(예: 타입이나 필드의 이름 충돌 시) 조정해야 할 수도 있지만, 여러 엔드포인트 방식을 사용하면 각각의 스키마는 독립적으로 접근할 수 있습니다.
서로 다른 스키마를 노출하면, 여러 개의 독립적인 그래프에 접근할 수 있습니다. GraphQL 창시자 리 버라(Lee Byron)가 설명한 바와 같이, 다음과 같은 경우에 유용할 수 있습니다:
예를 들어, 회사가 하나의 제품에 초점을 맞추어 해당 제품에 대한 GraphQL API를 구축한 후, 원래 제품과 관련 없는 새로운 비즈니스 도메인으로 확장할 경우, 두 제품이 단일 API를 공유하는 것은 부담이 될 수 있습니다. 이 경우 서로 다른 두 엔드포인트와 서로 다른 스키마가 더 적절할 수 있습니다.
또 다른 예로, 외부 GraphQL API의 서브셋으로 구성된 내부 전용 엔드포인트를 별도로 둘 수 있습니다. Facebook은 내부 도구가 제품 타입과 상호작용할 수 있도록 내부용과 외부용 두 개의 엔드포인트를 사용하는 패턴을 채택하고 있습니다.
이 글에서는 위와 같은 예시들을 확장하여, 여러 GraphQL 엔드포인트를 노출하는 것이 적절한 다양한 사용 사례를 살펴보겠습니다.

3. 여러 GraphQL 엔드포인트 노출 방법

여러 GraphQL 엔드포인트를 노출하기 전에, GraphQL 서버가 여러 엔드포인트를 어떻게 노출할 수 있는지 살펴보겠습니다.
이미 몇몇 GraphQL 서버에서는 다음과 같이 이 기능을 기본 제공하고 있습니다:
WordPress용 GraphQL API의 커스텀 엔드포인트
만약 사용 중인 GraphQL 서버에서 여러 엔드포인트 기능을 기본 제공하지 않는다면, 애플리케이션 내에서 이를 직접 구현할 수도 있습니다. 기본 아이디어는 여러 GraphQL 스키마를 정의한 후, 요청된 엔드포인트에 따라 서버가 어떤 스키마를 사용할지 런타임에 결정하는 것입니다.
JavaScript 서버를 사용하는 경우, HTTP 요청 처리와 GraphQL 서버를 분리하는 GraphQL Helix를 사용하는 것이 편리합니다. Helix를 사용하면, Node.js 웹 프레임워크(예: Express.js나 Fastify)로 라우팅 로직을 처리한 후, 요청 경로(즉, 요청된 엔드포인트)에 따라 해당하는 스키마를 GraphQL 서버에 제공할 수 있습니다.
Helix의 기본 예제(Express 기반)를 단일 엔드포인트 /graphql에서 다음과 같이 구현했다고 가정해봅시다:
import express from "express"; import { schema } from "./my-awesome-schema"; const app = express(); app.use(express.json()); app.use("/graphql", async (req, res) => { // ... }); app.listen(8000);
JavaScript
복사
여러 엔드포인트를 처리하기 위해 URL 패턴을 /graphql/${customEndpoint}로 노출하고, 라우트 매개변수를 통해 customEndpoint 값을 얻습니다. 그런 다음 요청된 customEndpoint 값에 따라 사용할 스키마를 결정할 수 있습니다. 예를 들어, /graphql/clients, /graphql/providers, /graphql/internal과 같이 사용 가능합니다.
import { clientSchema } from "./schemas/clients"; import { providerSchema } from "./schemas/providers"; import { internalSchema } from "./schemas/internal"; // ... app.use("/graphql/:customEndpoint", async (req, res) => { let schema = {}; if (req.params.customEndpoint === 'clients') { schema = clientSchema; } else if (req.params.customEndpoint === 'providers') { schema = providerSchema; } else if (req.params.customEndpoint === 'internal') { schema = internalSchema; } else { throw new Error('지원하지 않는 엔드포인트입니다'); } // ... });
JavaScript
복사
스키마를 결정한 후, 이를 Helix가 기대하는 방식으로 GraphQL 서버에 주입합니다:
const request = { body: req.body, headers: req.headers, method: req.method, query: req.query, }; const { query, variables, operationName } = getGraphQLParameters(request); const result = await processRequest({ schema, query, variables, operationName, request, }); if (result.type === "RESPONSE") { result.headers.forEach(({ name, value }) => { res.setHeader(name, value) }); res.status(result.status); res.json(result.payload); } else { // ... }
JavaScript
복사
서로 다른 스키마는 코드의 일부를 공유할 수도 있으므로, 공통 필드를 중복 작성할 필요는 없습니다.
예를 들어, /graphql/clients는 기본 스키마를 노출하며 다음과 같이 요소를 내보낼 수 있습니다:
// File: schemas/clients.ts export const clientSchemaQueryFields = { // ... }; export const clientSchema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields: clientSchemaQueryFields, }), });
JavaScript
복사
그리고 이 요소들을 /graphql/providers의 스키마에 다음과 같이 가져와서 사용할 수 있습니다:
// File: schemas/providers.ts import { clientSchemaQueryFields } from "./clients"; export const providerSchemaQueryFields = { // ... }; export const providerSchema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields: { ...clientSchemaQueryFields, ...providerSchemaQueryFields }, }), });
JavaScript
복사

4. 여러 GraphQL 엔드포인트 사용 사례

이제 여러 GraphQL 엔드포인트를 사용하는 다양한 사례들을 살펴보겠습니다. 아래와 같은 사용 사례에 대해 알아보겠습니다:
관리자와 공개 엔드포인트를 별도로 노출하기
민감한 정보에 대한 접근을 보다 안전하게 제한하기
서로 다른 애플리케이션에 대해 다른 동작을 제공하기
여러 언어로 사이트 생성하기
프로덕션 릴리즈 전 업그레이드된 스키마를 테스트하기
Backend-For-Frontends(BFF) 접근 방식 지원하기

4.1. 관리자와 공개 엔드포인트를 별도로 노출하기

회사의 모든 데이터를 하나의 그래프로 사용하는 경우, GraphQL 스키마 내 각 필드에 대해 접근 제어 정책을 설정하여 누가 어떤 필드에 접근할 수 있는지 검증할 수 있습니다. 예를 들어, 로그인한 사용자만 접근할 수 있도록 @auth 지시어를 사용하거나, 특정 역할을 가진 사용자에게만 접근을 허용하는 @protect(role: "EDITOR") 지시어를 사용할 수 있습니다.
그러나 이러한 메커니즘은 소프트웨어 버그나 팀의 부주의로 인해 안전하지 않을 수 있습니다. 예를 들어, 개발자가 필드에 지시어를 추가하는 것을 잊거나 DEV 환경에는 추가했지만 PROD 환경에는 추가하지 않는다면, 해당 필드가 모든 사용자에게 노출되어 보안 위험을 초래할 수 있습니다.
민감하거나 기밀 정보 — 의도하지 않은 사용자가 절대 접근해서는 안 되는 정보 — 의 경우, 해당 필드를 공개 스키마에 노출하지 않고 팀만 접근할 수 있는 비공개 스키마에 포함시키는 것이 바람직합니다. 이러한 전략은 버그나 부주의로 인한 위험으로부터 민감한 데이터를 보호할 수 있습니다.
따라서, 관리자 전용(Admin)과 공개(Public) 스키마를 별도로 생성하여 각각 /graphql/admin/graphql 엔드포인트 아래에 노출할 수 있습니다.

4.2. 민감한 정보 접근을 보다 안전하게 제한하기

앞서 설명한 관리자와 공개 엔드포인트의 예시는, 특정 사용자 그룹이 절대로 다른 사용자 그룹의 정보를 접근할 수 없어야 하는 상황에 일반화할 수 있습니다.
예를 들어, 서로 다른 클라이언트를 위해 커스텀 스키마를 만들어야 할 경우, 각 클라이언트에 대해 /graphql/some-client, /graphql/another-client와 같이 커스텀 엔드포인트를 노출하면, 하나의 통합 스키마에 모두 접근 권한을 주고 접근 제어를 적용하는 것보다 더 안전할 수 있습니다.
또한, 이러한 엔드포인트에 대해 IP 주소로 접근을 검증할 수 있으므로, 클라이언트 측에서는 자신의 데이터에 대해서만 접근할 수 있음을 확신할 수 있습니다. 아래 코드는 Helix와 Express를 사용하여 /graphql/star-client 엔드포인트에 대해 특정 IP 주소만 접근할 수 있도록 하는 예시입니다:
import { starClientSchema } from "./schemas/star-client"; // 클라이언트의 IP 정의 const starClientIP = "99.88.77.66"; app.use("/graphql/:customEndpoint", async (req, res) => { let schema = {}; const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.socket.remoteAddress || req.connection.socket.remoteAddress; if (req.params.customEndpoint === 'star-client') { if (ip !== starClientIP) { throw new Error('잘못된 IP입니다'); } schema = starClientSchema; } // ... });
JavaScript
복사
클라이언트는 자신의 데이터에 대해 자신들의 IP 주소에서만 엔드포인트에 접근할 수 있다는 사실을 알고, 데이터가 안전하게 보호되고 있다는 확신을 가질 수 있습니다.

4.3. 서로 다른 애플리케이션에 다른 동작 제공하기

동일한 데이터 소스에 접근하는 서로 다른 애플리케이션에 대해 서로 다른 동작을 제공할 수 있습니다.
예를 들어, Reddit의 경우 데스크탑 브라우저와 모바일 브라우저에서 접근할 때 서로 다른 응답을 생성합니다. 데스크탑 브라우저에서는 로그인 여부와 상관없이 바로 콘텐츠를 볼 수 있지만, 모바일 브라우저에서는 콘텐츠에 접근하기 위해 로그인이 필요하며, 앱 사용을 권장합니다.
데스크탑 브라우저로 접속한 레딧 홈페이지
모바일 브라우저로 접속한 레딧 홈페이지
이와 같은 서로 다른 동작을 제공하기 위해 데스크탑과 모바일 각각의 스키마를 생성하여 /graphql/desktop/graphql/mobile 같은 엔드포인트로 노출할 수 있습니다.

4.4. 여러 언어로 사이트 생성하기

GraphQL을 유일한 데이터 소스(예: Gatsby로 정적 사이트 생성)에 사용하는 경우, 데이터가 전송되는 동안 데이터를 번역하여 같은 사이트를 여러 언어로 생성할 수 있습니다.
실제로 이 목표를 달성하기 위해 반드시 여러 엔드포인트가 필요한 것은 아닙니다. 예를 들어, 환경 변수 LANGUAGE_CODE로부터 언어 코드를 가져와 GraphQL 변수 $lang에 주입하고, 게시물의 제목과 본문 필드를 translateTo 인자를 통해 번역할 수 있습니다:
query GetTranslatedPost($lang: String!) { post(id: 1) { title(translateTo: $lang) content(translateTo: $lang) } }
GraphQL
복사
그러나 번역은 횡단 관심사(cross-cutting concern)이므로, 지시어(directive)를 사용하는 것이 더 적절할 수도 있습니다. 스키마 타입 지시어를 사용하면 쿼리는 번역되고 있다는 사실을 인지하지 않아도 됩니다:
{ post(id: 1) { title content } }
GraphQL
복사
그리고 스키마 내 필드에 @translate 지시어를 추가하여 번역 로직을 적용합니다:
directive @translate(translateTo: String) on FIELD type Post { title @translate(translateTo: "fr") content @translate(translateTo: "fr") }
GraphQL
복사
(지시어 인자 translateTo는 선택 사항으로, 제공되지 않으면 환경 변수 LANGUAGE_CODE에 설정된 기본 값을 사용합니다.)
이렇게 언어 정보가 스키마에 주입되면, 영어용 /graphql/en과 프랑스어용 /graphql/fr 등 언어별로 서로 다른 스키마를 생성할 수 있습니다.
최종적으로, 애플리케이션에서 해당 엔드포인트를 지정하여 원하는 언어의 사이트를 생성할 수 있습니다.

4.5. 프로덕션 릴리즈 전 업그레이드된 스키마 테스트하기

GraphQL 스키마를 업그레이드하고 일부 사용자가 사전에 테스트해보게 하려면, 새로운 스키마를 /graphql/upcoming 엔드포인트를 통해 노출할 수 있습니다. 더 나아가, DEV 환경에서 지속적으로 배포되는 스키마를 /graphql/bleeding-edge와 같은 엔드포인트로 노출할 수도 있습니다.

4.6. Backend-For-Frontends(BFF) 접근 방식 지원하기

Backend-For-Frontends(BFF)는 클라이언트별로 최적화된 API를 제공하기 위해 각 클라이언트가 자체 API를 "소유"하도록 하는 접근 방식입니다. 이를 통해 각 클라이언트는 자신의 요구 사항에 맞게 가장 최적화된 버전을 제공할 수 있습니다.
이 모델은 GraphQL 서버 내에서 여러 엔드포인트를 사용하여 모든 BfF를 한 곳에서 처리함으로써 구현할 수 있습니다.
예를 들어, /graphql/mobile/graphql/web 같은 엔드포인트를 만들어, 각 엔드포인트가 특정 BFF/클라이언트에 대응하도록 할 수 있습니다.

5. 결론

GraphQL은 REST의 대안으로 탄생했으며, 과도한 또는 부족한 데이터를 가져오지 않고 필요한 데이터를 효율적으로 조회할 수 있도록 설계되었습니다. 이를 위해 단일 엔드포인트를 노출하고, 쿼리를 통해 데이터를 가져오는 방식이 주로 사용됩니다.
단일 엔드포인트 방식은 대부분의 경우에 효과적이지만, 서로 완전히 다른 클라이언트나 애플리케이션에 대해 사용자 지정된 스키마가 필요할 때는 단일 API로는 한계가 있을 수 있습니다. 이 경우, 스키마마다 개별 엔드포인트를 노출하는 방식이 더 적절할 수 있습니다.