단일 백엔드 프레임워크에서 GraphQL과 REST를 원활하게 통합하기
Wenhao Wang
Dev Intern · Leapcell

소개
끊임없이 진화하는 백엔드 개발 환경에서 유연성과 적응성은 무엇보다 중요합니다. 현대 애플리케이션은 종종 기존 웹 브라우저부터 모바일 앱, IoT 장치, 심지어 다른 백엔드 서비스에 이르기까지 다양한 클라이언트를 지원합니다. 이러한 각 클라이언트는 고유한 데이터 가져오기 요구 사항과 선호도를 가질 수 있습니다. RESTful API가 오랫동안 사실상의 표준이었던 반면, GraphQL은 데이터 검색에서 더 큰 효율성과 유연성을 제공하는 강력한 대안으로 부상했습니다. 그러면 이러한 두 가지 진영 모두를 효과적으로 지원하고 REST의 익숙함과 광범위한 지원을 GraphQL의 혁신적인 데이터 쿼리 기능과 동시에 제공할 수 있도록 백엔드를 어떻게 강화할 수 있을지에 대한 과제가 발생합니다. 이것은 단순히 다른 클라이언트 요구를 충족시키는 것이 아닙니다. 개발 워크플로우를 최적화하고, 데이터 진화를 관리하며, 잠재적으로 점진적인 마이그레이션 전략을 촉진하는 것입니다. 이 글은 단일 백엔드 프레임워크 내에서 GraphQL과 REST API를 모두 성공적으로 제공하기 위한 전략을 탐구하며, 이점, 과제 및 실질적인 구현 방법을 살펴봅니다.
듀얼 API 전략 설명
"방법"을 자세히 살펴보기 전에, 우리의 논의를 뒷받침할 몇 가지 핵심 개념을 명확히 해보겠습니다.
핵심 용어
- REST (Representational State Transfer): 네트워크 하이퍼미디어 애플리케이션을 위한 아키텍처 스타일입니다. 상태 비저장 클라이언트-서버 통신, 통합 인터페이스 및 리소스 기반 상호 작용(예:
/users
,/products
)을 강조합니다. REST API는 일반적으로 표준 HTTP 메서드(GET, POST, PUT, DELETE)를 사용하고 고정된 데이터 구조를 반환합니다. - GraphQL: API를 위한 쿼리 언어이자 기존 데이터로 해당 쿼리를 실행하는 런타임입니다. 클라이언트는 필요한 데이터를 정확히, 그 이상도 이하도 아닌 것을 요청할 수 있습니다. GraphQL API는 단일 엔드포인트를 노출하며 데이터는 타입 시스템을 사용하여 설명됩니다.
- 스키마 (GraphQL): GraphQL API를 통해 사용 가능한 모든 데이터 타입과 작업(쿼리, 뮤테이션, 구독)의 중앙 정의입니다. 클라이언트와 서버 간의 계약 역할을 합니다.
- 리졸버 (GraphQL): GraphQL 스키마의 각 필드에 대한 실제 데이터를 가져오는 함수입니다. 스키마 정의를 백엔드의 데이터 소스(데이터베이스, 다른 서비스)에 연결합니다.
- 미들웨어: 운영 체제 또는 데이터베이스와 애플리케이션 사이에 위치한 소프트웨어로, 분산 애플리케이션의 통신 및 데이터 관리를 가능하게 합니다. 웹 프레임워크에서 미들웨어는 종종 인증, 로깅 및 라우팅과 같은 작업을 처리합니다.
듀얼 API의 근거
왜 두 가지 모두를 원할까요?
- 클라이언트 다양성: 일부 클라이언트는 REST의 단순성과 캐싱 이점을 선호할 수 있지만, 다른 클라이언트는 GraphQL의 정밀성과 효율성을 요구할 수 있습니다.
- 점진적 채택/마이그레이션: 기존 REST API가 있는 경우 전체 재작성 없이 GraphQL을 점진적으로 도입하여 클라이언트가 자신만의 속도로 마이그레이션할 수 있습니다.
- 특정 사용 사례: 복잡한 데이터 집계 또는 실시간 업데이트(GraphQL 구독 사용)와 같은 특정 작업은 GraphQL에서 더 깔끔하게 처리되는 경우가 많습니다. 개별 리소스에 대한 간단한 CRUD 작업은 REST에 완벽하게 적합할 수 있습니다.
- 개발자 선호도: 다른 팀이나 개발자는 하나에 대한 전문성과 선호도를 가질 수 있습니다.
구현 전략
핵심 아이디어는 백엔드 프레임워크의 기능을 활용하여 양쪽 API 스타일에 대한 요청을 독립적으로 라우팅하고 처리하는 것입니다. 동시에 가능한 한 많은 기본 비즈니스 로직 및 데이터 액세스를 공유하는 것입니다.
전략 1: 별도 엔드포인트, 공유 비즈니스 로직
이것이 가장 일반적이며 종종 권장되는 접근 방식입니다.
- REST 엔드포인트 계층 구조: REST API는 일반적으로 리소스 중심 구조를 따릅니다. 예:
/api/v1/users
,/api/v1/products/{id}
. 각 엔드포인트는 특정 컨트롤러 또는 뷰 함수에 매핑됩니다. - GraphQL 단일 엔드포인트: GraphQL API는 일반적으로 단일 진입점, 예를 들어
/graphql
을 노출합니다. 모든 GraphQL 쿼리와 뮤테이션은 이 엔드포인트를 통과합니다.
상호 작용 방식: REST 컨트롤러와 GraphQL 리졸버 모두 핵심 비즈니스 로직을 캡슐화하고 데이터베이스 또는 다른 외부 서비스와 상호 작용하는 일반적인 서비스 계층 또는 리포지토리 계층 함수를 호출해야 합니다. 이렇게 하면 코드 중복을 방지하고 양쪽 API 간의 데이터 일관성을 보장할 수 있습니다.
예시 (Python/Django 설정에서 개념적):
# --- api_project/core_app/services.py --- # 이것은 공유 비즈니스 로직 계층입니다. def get_all_users(): # DB에서 사용자 가져오기 로직 return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] def get_user_by_id(user_id): # 단일 사용자 가져오기 로직 return {"id": user_id, "name": f"User {user_id}"} def create_user(data): # 사용자 생성 로직 return {"id": 3, "name": data['name']} # --- api_project/rest_app/views.py (Django REST Framework) --- from rest_framework.views import APIView from rest_framework.response import Response from api_project.core_app.services import get_all_users, get_user_by_id, create_user class UserListAPIView(APIView): def get(self, request): users = get_all_users() return Response(users) def post(self, request): user = create_user(request.data) return Response(user, status=201) class UserDetailAPIView(APIView): def get(self, request, user_id): user = get_user_by_id(user_id) if not user: return Response({"error": "User not found"}, status=404) return Response(user) # --- api_project/graphql_app/schema.py (Graphene-Python) --- import graphene from api_project.core_app.services import get_all_users, get_user_by_id, create_user class UserType(graphene.ObjectType): id = graphene.Int() name = graphene.String() class Query(graphene.ObjectType): all_users = graphene.List(UserType) user = graphene.Field(UserType, user_id=graphene.Int(required=True)) def resolve_all_users(root, info): return get_all_users() def resolve_user(root, info, user_id): return get_user_by_id(user_id) class CreateUser(graphene.Mutation): class Arguments: name = graphene.String(required=True) Output = UserType def mutate(root, info, name): user = create_user({"name": name}) return UserType(**user) class Mutation(graphene.ObjectType): create_user = CreateUser.Field() schema = graphene.Schema(query=Query, mutation=Mutation) # --- api_project/urls.py (Django Routing) --- from django.urls import path, include from graphene_django.views import GraphQLView from api_project.rest_app.views import UserListAPIView, UserDetailAPIView urlpatterns = [ # REST API routes path('api/v1/users/', UserListAPIView.as_view(), name='user-list'), path('api/v1/users/<int:user_id>/', UserDetailAPIView.as_view(), name='user-detail'), # GraphQL API route path('graphql/', GraphQLView.as_view(graphiql=True, schema=schema)), ]
이 예시에서 UserListAPIView
/UserDetailAPIView
(REST)와 resolve_all_users
/resolve_user
/mutate
(GraphQL) 모두 실제 데이터 작업을 api_project.core_app.services
모듈에 위임합니다. 이것이 DRY(Don't Repeat Yourself) 코드를 유지하는 핵심입니다.
전략 2: REST를 위한 API 게이트웨이로서의 GraphQL
이 보다 고급 패턴에서는 GraphQL 서버 자체가 기존 REST 엔드포인트 또는 마이크로서비스에서 데이터를 가져오는 파사드 역할을 합니다.
- 고도로 구체적인 데이터가 필요한 프런트엔드: GraphQL 서버는 여러 REST 리소스의 데이터를 단일 최적화된 응답으로 "스트리칭"할 수 있습니다.
- 레거시 REST API 래핑: 원래 REST 서비스를 수정하지 않고 데이터 액세스를 점진적으로 현대화합니다.
- 마이크로서비스 아키텍처: GraphQL 게이트웨이는 다양한 마이크로서비스에 걸쳐 통합된 API를 제공할 수 있습니다.
구현: GraphQL 리졸버는 직접 서비스 계층 함수를 호출하는 대신 내부 또는 외부 REST 엔드포인트에 HTTP 호출을 하여 데이터를 처리하고 반환합니다.
예시 (Apollo Server를 사용하는 개념적 Node.js):
// --- graphql_server/datasources/UserService.js --- const { RESTDataSource } = require('apollo-datasource-rest'); class UsersAPI extends RESTDataSource { constructor() { super(); this.baseURL = 'http://localhost:3000/api/v1/'; // 귀하의 내부 REST API } async getAllUsers() { return this.get('users'); } async getUserById(id) { return this.get(`users/${id}`); } } // --- graphql_server/index.js --- const { ApolloServer, gql } = require('apollo-server'); const UsersAPI = require('./datasources/UserService'); const typeDefs = gql` type User { id: ID! name: String } type Query { users: [User] user(id: ID!): User } `; const resolvers = { Query: { users: (_, __, { dataSources }) => dataSources.usersAPI.getAllUsers(), user: (_, { id }, { dataSources }) => dataSources.usersAPI.getUserById(id), }, }; const server = new ApolloServer({ typeDefs, resolvers, dataSources: () => ({ usersAPI: new UsersAPI(), }), }); server.listen().then(({ url }) => { console.log(`🚀 GraphQL Server ready at ${url}`); });
이 Node.js 예시에서 GraphQL 서버 자체는 REST 서비스에서 데이터를 가져오는 별도의 애플리케이션입니다. 이것은 GraphQL이 집계 계층 역할을 할 때 일반적인 패턴입니다. 귀하의 기본 백엔드 프레임워크는 여전히 원래 REST 엔드포인트를 제공할 것이고, 이 GraphQL 서버는 이를 소비할 것입니다.
듀얼 API에 대한 고려 사항
- 인증/권한 부여: 두 API 모두에 걸쳐 일관되고 강력한 인증 및 권한 부여 메커니즘을 보장합니다. JWT(JSON Web Tokens)는 REST 엔드포인트와 GraphQL 요청 모두에 대해 검증할 수 있는 인기 있는 선택입니다.
- 오류 처리: 두 API 유형 모두에 대해 명확하고 일관된 오류 형식을 정의합니다. GraphQL에는 표준화된 오류 구조가 있지만, REST API도 예측 가능한 패턴(예: HTTP 상태 코드 및 JSON 오류 본문 사용)을 따라야 합니다.
- 데이터 유효성 검사: REST 본문 또는 GraphQL 입력 타입의 입력이든 모든 입력에 대해 서버 측 데이터 유효성 검사를 구현합니다.
- 도구 및 생태계: REST는 광범위한 도구(Postman, curl, OpenAPI/Swagger)의 이점을 누립니다. GraphQL은 자체적으로 훌륭한 도구(GraphiQL, Apollo Client, Relay)를 가지고 있습니다. 개발 및 클라이언트 팀이 각 API와 어떻게 상호 작용할지 염두에 두십시오.
- 캐싱: REST는 HTTP 캐싱을 효과적으로 활용할 수 있습니다. GraphQL은 일반적으로 클라이언트 측 캐싱(Apollo Client의 정규화된 캐시와 같은) 또는 애플리케이션 레벨 캐싱 전략이 필요합니다.
- 문서화: 두 API에 대해 별도의 명확한 문서를 유지합니다. REST에 대한 OpenAPI/Swagger 및 GraphQL 스키마 정의 언어(SDL)와 Apollo Studio와 같은 도구를 사용합니다.
애플리케이션 시나리오
- 새로운 애플리케이션과 레거시 통합 개발: 기존 내부 시스템과의 상호 작용을 위해 REST를 유지하면서 새로운 클라이언트 측 기능에 GraphQL을 사용합니다.
- 모바일 우선 개발: 모바일 앱의 대역폭과 네트워크 요청을 최적화하기 위해 GraphQL을 제공하고, 기존 웹 인터페이스는 REST를 계속 사용할 수 있습니다.
- API 진화: 기존 REST API에 대한 호환성을 중단하지 않고 GraphQL을 점진적으로 도입합니다.
- 마이크로서비스 오케스트레이션: GraphQL을 API 게이트웨이로 사용하여 REST 인터페이스만 노출하는 여러 마이크로서비스에서 데이터를 집계합니다.
결론
단일 백엔드 프레임워크에서 GraphQL과 REST API를 모두 제공하면 탁월한 유연성을 제공하여 다양한 클라이언트 요구 사항을 충족하고 전략적인 API 진화를 가능하게 합니다. 핵심 비즈니스 로직을 공유하고 요청을 지능적으로 라우팅함으로써 개발자는 두 아키텍처 스타일의 강점을 활용하는 강력하고 적응성 있는 시스템을 구축할 수 있습니다. 성공의 열쇠는 관심사의 명확한 분리, 일반적인 비즈니스 로직 공유, 각 API에 대한 인증, 오류 처리 및 문서화에 대한 신중한 고려에 있습니다. 이 이중 접근 방식은 백엔드 서비스가 효율적이고 광범위하게 액세스 가능하도록 합니다. 궁극적으로 이 전략은 서버의 중복 코드를 최소화하면서 소비 클라이언트에게 최대의 유연성을 제공합니다.