작성일 댓글 남기기

GraphQL의 Federation에 대해서

GraphQL Federation은 여러 개의 마이크로서비스에서 제공하는 여러 개의 GraphQL API를 하나의 통합된 그래프처럼 사용할 수 있게 해주는 아키텍처 패턴입니다. 이를 통해 대규모 애플리케이션에서 서비스 간 분리데이터 통합을 유연하게 처리할 수 있습니다. Apollo Federation이 대표적인 도구이며, 주로 대규모 마이크로서비스 아키텍처에서 GraphQL API를 통합할 때 사용됩니다.

1. Federation이 필요한 이유

전통적인 GraphQL 서버에서는 하나의 서버에서 모든 데이터를 제공하는 방식이 일반적입니다. 하지만 대규모 시스템에서는 여러 서비스가 각각의 데이터 소스를 가지고 있고, 이러한 데이터를 하나의 GraphQL API로 통합해서 사용해야 하는 경우가 많습니다.

Federation은 이러한 문제를 해결하기 위해 설계되었습니다. Federation을 사용하면 각각의 **서브서비스(Subgraph)**가 독립적으로 자신의 데이터를 제공하면서도, Gateway를 통해 모든 데이터를 하나의 통합된 그래프처럼 사용할 수 있습니다.

Federation을 사용하는 이유:

  • 마이크로서비스 아키텍처 지원: 각 서비스는 독립적으로 개발 및 배포되며, GraphQL API도 각 서비스에 맞게 분리되어 있습니다.
  • 데이터 통합: Federation은 여러 서비스에서 제공하는 데이터를 한 곳에서 모아 사용할 수 있게 해줍니다.
  • 확장성: 서비스를 분리하고 독립적으로 확장할 수 있습니다. 서비스 간 의존성이 낮아지고, 각 서비스가 독립적으로 운영됩니다.

2. Federation의 구성 요소

Federation을 구성하는 핵심 요소는 두 가지입니다:

  1. Subgraph (서브그래프): 각 서비스가 자체적으로 제공하는 독립적인 GraphQL API입니다. 각 서비스는 자체적으로 스키마를 정의하고, 해당 데이터를 처리합니다.
  2. Apollo Gateway (게이트웨이): 여러 개의 서브그래프를 하나의 통합된 그래프로 결합하는 역할을 합니다. 클라이언트는 이 게이트웨이를 통해 각각의 서브그래프의 데이터를 요청할 수 있습니다.

그림으로 보면 다음과 같은 구조를 가집니다:

           클라이언트
|
[Apollo Gateway]
/ | \
[Subgraph 1] [Subgraph 2] [Subgraph 3]

게이트웨이가 각 서브그래프에 쿼리를 전달하고, 결과를 클라이언트에 반환하는 방식입니다.


3. Federation의 핵심 기능

1) Key and Extend (키와 확장)

Federation의 핵심 기능 중 하나는 확장성입니다. **@key**와 @extend 디렉티브를 사용해 각 서브그래프의 스키마를 결합할 수 있습니다.

  • @key: 각 서브그래프에서 엔터티를 식별하는 필드를 정의합니다.
  • @extend: 다른 서브그래프에서 정의한 엔터티를 확장할 때 사용합니다.

예시:

User 데이터를 사용자 서비스에서 정의하고, 주문 서비스에서 사용자의 주문 데이터를 확장하려고 할 때, 각 서브그래프는 다음과 같이 정의됩니다.

  1. User 서비스의 Subgraph:
type User @key(fields: "id") {
id: ID!
name: String
email: String
}
  • 여기서 @key(fields: "id")User 타입의 id 필드가 사용자 엔터티를 식별하는 고유 값임을 나타냅니다.
  1. Order 서비스의 Subgraph:
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order]
}

type Order {
id: ID!
product: String
price: Float
}
  • 여기서 **@extend**는 User 타입을 확장하고, @key를 통해 id로 사용자 엔터티를 식별합니다. **@external**은 id 필드가 다른 서브그래프(User 서비스)에서 제공된다는 것을 의미합니다.

2) Entities와 Reference Resolver

게이트웨이가 여러 서브그래프 간에 데이터를 결합할 때, Federation은 entitiesreference resolver를 사용합니다.

  • Entity: 각 서브그래프에서 @key로 정의된 엔터티는 게이트웨이를 통해 통합된 그래프에서 공유됩니다.
  • Reference Resolver: @key로 식별된 엔터티를 다른 서브그래프에서 참조할 수 있도록 리졸버를 제공합니다.

4. Apollo Federation 설정

다음은 Apollo Federation을 실제로 설정하는 과정입니다.

1) 각 서비스의 Subgraph 설정

먼저, 각각의 서브그래프(Subgraph)를 설정해야 합니다. 각 서브그래프는 독립적인 GraphQL 서버로 동작하며, 서로 다른 데이터를 제공합니다.

예시: User 서비스
const { ApolloServer, gql } = require('apollo-server');
const { buildFederatedSchema } = require('@apollo/federation');

const typeDefs = gql`
type User @key(fields: "id") {
id: ID!
name: String
email: String
}

type Query {
users: [User]
}
`;

const resolvers = {
Query: {
users() {
return [
{ id: '1', name: 'John Doe', email: '[email protected]' },
{ id: '2', name: 'Jane Doe', email: '[email protected]' }
];
}
}
};

const server = new ApolloServer({
schema: buildFederatedSchema([{ typeDefs, resolvers }])
});

server.listen({ port: 4001 }).then(({ url }) => {
console.log(`User service running at ${url}`);
});
예시: Order 서비스
const { ApolloServer, gql } = require('apollo-server');
const { buildFederatedSchema } = require('@apollo/federation');

const typeDefs = gql`
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order]
}

type Order {
id: ID!
product: String
price: Float
}

type Query {
orders: [Order]
}
`;

const resolvers = {
User: {
orders(user) {
return [
{ id: '1', product: 'Laptop', price: 1000.0 },
{ id: '2', product: 'Phone', price: 500.0 }
];
}
},
Query: {
orders() {
return [
{ id: '1', product: 'Laptop', price: 1000.0 },
{ id: '2', product: 'Phone', price: 500.0 }
];
}
}
};

const server = new ApolloServer({
schema: buildFederatedSchema([{ typeDefs, resolvers }])
});

server.listen({ port: 4002 }).then(({ url }) => {
console.log(`Order service running at ${url}`);
});

2) 게이트웨이 설정

이제 Apollo Gateway를 설정하여 각각의 서브그래프를 하나의 통합된 GraphQL API로 연결합니다.

Apollo Gateway 설정:
javascript코드 복사const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('@apollo/gateway');

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'users', url: 'http://localhost:4001' },
    { name: 'orders', url: 'http://localhost:4002' }
  ]
});

const server = new ApolloServer({
  gateway,
  subscriptions: false
});

server.listen({ port: 4000 }).then(({ url }) => {
  console.log(`Gateway running at ${url}`);
});

게이트웨이는 각 서브그래프(User 서비스와 Order 서비스)를 연결하고, 클라이언트로부터 요청을 받습니다.

3) 클라이언트에서 쿼리 실행

이제 클라이언트는 게이트웨이를 통해 통합된 GraphQL API를 사용할 수 있습니다.

query {
users {
id
name
orders {
product
price
}
}
}

이 쿼리는 users 필드를 통해 사용자 목록을 가져오고, 각 사용자의 주문 데이터를 가져옵니다. 게이트웨이가 각 서비스에 쿼리를 분배하고, 최종적으로 클라이언트에 데이터를 반환합니다.


5. Federation의 장점

  • 마이크로서비스 아키텍처와의 자연스러운 통합: 각 서비스는 독립적으로 관리되며, 이를 통합하여 하나의 그래프로 제공할 수 있습니다.
  • 확장성: 개별 서비스는 독립적으로 개발 및 배포될 수 있어, 시스템 전체의 확장성이 향상됩니다.
  • 일관된 GraphQL API 제공: 클라이언트는 여러 개의 서비스로 나뉘어 있는 데이터를 하나의 통합된 그래프에서 쿼리할 수 있습니다.

6. Federation의 단점 및 고려사항

  • 복잡성 증가: 서비스가 많아질수록 Federation 구조가 복잡해질 수 있습니다. 각 서브그래프 간의 의존성을 관리하고, 데이터를 효율적으로 결합해야 합니다.
  • 추가적인 네트워크 호출: 게이트웨이가 각 서브그래프에 쿼리를 전달하고 데이터를 모으는 과정에서 네트워크 호출이 증가할 수 있습니다. 이는 성능에 영향을 줄 수 있으므로 최적화가 필요합니다.
  • 추가적인 인프라 관리: 게이트웨이와 각 서브그래프를 배포하고 모니터링하는 추가적인 인프라 관리가 필요합니다.

결론

GraphQL Federation은 여러 마이크로서비스를 통합하여 하나의 일관된 GraphQL API를 제공할 수 있는 강력한 아키텍처 패턴입니다. 특히 대규모 시스템에서 서비스 간의 의존성을 줄이고, 독립적으로 개발 및 배포할 수 있는 환경을 제공합니다.

Federation을 효과적으로 사용하려면 각 서브그래프 간의 관계를 명확하게 정의하고, 게이트웨이를 통해 통합된 데이터 흐름을 관리해야 합니다.

작성일 댓글 남기기

GraphQL의 에러 처리

GraphQL에서의 **에러 처리(Error Handling)**는 클라이언트와 서버 간에 일어나는 문제를 효율적으로 처리하고, 클라이언트에게 유용한 정보를 전달하는 데 매우 중요한 역할을 합니다. GraphQL은 **부분 성공(Partial Success)**이라는 개념을 가지고 있어, 한 쿼리 내에서 일부 필드가 실패하더라도 나머지 필드의 데이터를 정상적으로 반환할 수 있는 유연한 에러 처리 방식을 제공합니다.

GraphQL에서 에러는 일반적으로 두 가지로 구분할 수 있습니다:

  1. GraphQL 레벨 에러: 쿼리 문법이나 스키마와 관련된 에러.
  2. 비즈니스 로직 에러: 데이터베이스에서 데이터를 찾지 못하거나, 권한이 부족할 때 발생하는 에러.

이제 GraphQL 에러 처리에 대해 차근차근 설명해드릴게요.


1. GraphQL 에러의 기본 구조

GraphQL의 응답은 JSON 형식으로 이루어지며, 성공한 쿼리와 함께 에러가 발생했을 경우 errors 필드에 에러 메시지가 포함됩니다. 기본적인 응답 형식은 다음과 같습니다.

성공적인 응답:

{
"data": {
"user": {
"id": "1",
"name": "John"
}
}
}

에러가 포함된 응답:

json코드 복사{
  "data": {
    "user": null
  },
  "errors": [
    {
      "message": "User not found",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["user"]
    }
  ]
}
  • data: 쿼리의 성공한 결과를 나타냅니다. 일부 필드가 실패하더라도 나머지 필드는 성공할 수 있습니다.
  • errors: 실패한 필드와 관련된 에러 메시지를 포함합니다.
    • message: 에러 메시지.
    • locations: 쿼리에서 에러가 발생한 위치(줄과 열).
    • path: 에러가 발생한 필드 경로.

2. GraphQL에서 에러 처리 방식

GraphQL에서는 각 리졸버가 독립적으로 실행되므로, 한 리졸버에서 에러가 발생하더라도 다른 리졸버는 정상적으로 동작할 수 있습니다. 이로 인해 부분 성공이라는 특징이 있습니다. 예를 들어, 하나의 필드에서 에러가 발생해도, 다른 필드는 정상적으로 데이터를 반환할 수 있습니다.


3. 에러 처리 예시

다음은 사용자 정보를 조회하는 GraphQL 리졸버에서 발생할 수 있는 여러 상황을 처리하는 예시입니다.

1) 기본적인 에러 처리

아래는 사용자를 찾을 수 없을 때 발생하는 에러를 처리하는 리졸버입니다.

const resolvers = {
Query: {
user: async (parent, { id }) => {
const user = await User.findById(id);
if (!user) {
throw new Error('User not found');
}
return user;
}
}
};

이 리졸버에서 User.findById()로 사용자를 조회하는데, 사용자가 존재하지 않으면 throw new Error('User not found')로 에러를 발생시킵니다. 이 에러는 GraphQL의 errors 필드에 포함되어 클라이언트에 전달됩니다.

2) 커스텀 에러 처리

단순한 에러 메시지 외에, 더 구체적인 에러 정보를 클라이언트에 제공하기 위해 커스텀 에러 클래스를 사용할 수 있습니다.

class UserNotFoundError extends Error {
constructor(message) {
super(message);
this.name = "UserNotFoundError";
}
}

const resolvers = {
Query: {
user: async (parent, { id }) => {
const user = await User.findById(id);
if (!user) {
throw new UserNotFoundError('The user with the given ID does not exist.');
}
return user;
}
}
};

클라이언트는 이 에러를 받아 에러의 이름메시지를 통해 더욱 구체적인 정보를 알 수 있습니다.

3) 비즈니스 로직 에러 처리

로그인 시스템이나 권한 관리가 필요한 경우, 다음과 같은 방식으로 인증 및 권한 에러를 처리할 수 있습니다.

const resolvers = {
Query: {
secretData: async (parent, args, context) => {
if (!context.user) {
throw new Error('Not authenticated');
}
// 인증된 사용자만 secret data를 조회할 수 있음
return "This is secret data!";
}
}
};

이 리졸버는 인증되지 않은 사용자가 접근하려고 할 때 **Not authenticated**라는 에러를 발생시킵니다. 클라이언트는 이 에러를 받아 처리할 수 있습니다.


4. 에러 확장(Extension) 필드 사용하기

GraphQL에서는 에러 메시지 외에도 **확장 필드(extensions)**를 사용해 추가적인 정보를 클라이언트에 제공할 수 있습니다. 예를 들어, 에러 코드, 디버그 정보, 상태 코드 등을 포함할 수 있습니다.

1) 확장 필드 사용 예시:

const resolvers = {
Query: {
user: async (parent, { id }) => {
const user = await User.findById(id);
if (!user) {
const error = new Error('User not found');
error.extensions = {
code: 'USER_NOT_FOUND',
httpStatus: 404
};
throw error;
}
return user;
}
}
};

클라이언트는 이렇게 확장된 에러를 받아, 코드나 상태 코드를 통해 추가적인 정보를 활용할 수 있습니다.

클라이언트로의 응답 예시:

{
"data": {
"user": null
},
"errors": [
{
"message": "User not found",
"extensions": {
"code": "USER_NOT_FOUND",
"httpStatus": 404
}
}
]
}

2) GraphQL 에러 확장 필드의 장점:

  • 에러 코드: 클라이언트가 에러 메시지 외에, 에러의 종류나 상태를 코드로 처리할 수 있게 해줍니다.
  • HTTP 상태 코드: GraphQL은 기본적으로 HTTP 200 응답을 반환하지만, 확장 필드를 사용해 상태 코드와 함께 에러를 클라이언트에 제공할 수 있습니다.
  • 디버깅 정보: 개발 환경에서는 추가적인 디버깅 정보를 제공해 문제를 추적할 수 있습니다.

5. 전역 에러 처리

서버에서 전역적으로 에러를 처리하고, 클라이언트에 에러 메시지를 일관되게 전달하기 위해 전역 에러 처리 미들웨어를 사용할 수 있습니다.

전역 에러 처리 예시 (Express + GraphQL):

const { graphqlHTTP } = require('express-graphql');

const app = express();

app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
customFormatErrorFn: (err) => {
return {
message: err.message,
code: err.extensions?.code || 'INTERNAL_SERVER_ERROR',
locations: err.locations,
path: err.path
};
}
}));

위 예시에서 **customFormatErrorFn**을 사용하여 GraphQL의 에러를 커스터마이즈합니다. 이 함수는 에러가 발생했을 때 에러 메시지와 확장 필드, 그리고 에러 경로 정보를 클라이언트에 전달합니다.


6. GraphQL 에러 처리 패턴

  1. 부분 성공 허용: GraphQL의 특성상 여러 필드에서 일부 성공과 일부 실패가 발생할 수 있습니다. 이런 상황에서는 성공한 데이터는 data에, 실패한 데이터는 errors 필드에 각각 포함됩니다.
  2. 사용자 정의 에러 핸들링: 도메인 로직에 따라 적절한 커스텀 에러를 정의하고 클라이언트에 더 의미 있는 정보를 전달할 수 있습니다. 예를 들어, ValidationError, AuthenticationError, AuthorizationError 등의 커스텀 에러 클래스를 만들어 사용하는 것이 좋습니다.
  3. 에러 코드 및 확장 정보 제공: 클라이언트가 단순한 에러 메시지 외에 에러 코드나 HTTP 상태 코드를 통해 추가적인 조치를 취할 수 있도록 확장 필드를 활용해 에러를 제공해야 합니다.

7. GraphQL 클라이언트에서 에러 처리

Apollo Client와 같은 GraphQL 클라이언트에서는 서버에서 발생한 에러를 쉽게 처리할 수 있습니다. 클라이언트는 errors 필드를 확인하고, 각 에러에 대한 적절한 처리를 할 수 있습니다.

Apollo Client에서 에러 처리:

import { useQuery, gql } from '@apollo/client';

const GET_USER = gql`
query getUser($id: ID!) {
user(id: $id) {
id
name
}
}
`;

function User({ id }) {
const { loading, error, data } = useQuery(GET_USER, {
variables: { id }
});

if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;

return <p>{data.user.name}</p>;
}

에러가 발생했을 경우, error.message를 클라이언트에서 처리할 수 있습니다.


결론

GraphQL에서의 에러 처리는 부분 성공을 허용하면서도, 클라이언트에게 유의미한 에러 정보를 제공할 수 있도록 설계되어 있습니다. 이를 통해 사용자는 쿼리의 일부만 실패하더라도 나머지 데이터는 정상적으로 사용할 수 있습니다.

또한, 확장 필드를 활용해 에러 코드나 상태 코드를 추가로 제공함으로써, 클라이언트는 더 구체적인 에러 처리를 할 수 있습니다. 전역 에러 핸들러나 커스텀 에러 클래스를 통해 서버의 에러 관리를 더욱 효율적으로 처리할 수 있습니다.