작성일 댓글 남기기

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

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

작성일 댓글 남기기

GraphQL의 프래그먼트

GraphQL **프래그먼트(Fragment)**는 쿼리에서 공통 필드를 재사용할 수 있도록 도와주는 기능입니다. 큰 쿼리나 여러 쿼리에서 동일한 필드를 반복적으로 요청할 때 코드 중복을 줄이고, 유지보수성을 높일 수 있는 중요한 기능입니다.

프래그먼트를 사용하면 여러 쿼리나 뮤테이션에서 자주 사용하는 필드들을 하나의 프래그먼트로 정의하고, 그 프래그먼트를 다른 쿼리 내에서 호출함으로써 일관성을 유지할 수 있습니다. 특히 큰 애플리케이션에서 여러 곳에서 동일한 데이터를 요청할 때 매우 유용합니다.

1. 프래그먼트 기본 개념

프래그먼트는 GraphQL 쿼리나 뮤테이션에서 중복되는 필드를 하나로 묶어, 여러 곳에서 재사용할 수 있도록 하는 일종의 모듈화된 쿼리 블록입니다.

기본 구조:

fragment FragmentName on TypeName {
field1
field2
field3
}
  • FragmentName: 프래그먼트의 이름입니다.
  • on TypeName: 프래그먼트를 적용할 GraphQL 타입을 명시합니다. 예를 들어, 특정 객체 타입(User, Post 등)에만 해당 필드를 사용할 수 있도록 지정합니다.
  • field1, field2, field3: 재사용할 필드 목록입니다.

2. 프래그먼트 사용 예시

1) 기본 프래그먼트 정의

다음은 User 타입에 대한 필드를 프래그먼트로 정의하는 예시입니다. id, name, email 같은 필드가 여러 쿼리에서 공통으로 사용될 경우, 이를 프래그먼트로 정의하고 재사용할 수 있습니다.

fragment UserInfo on User {
id
name
email
}

2) 프래그먼트 사용하기

프래그먼트는 쿼리에서 재사용할 수 있습니다. 예를 들어, 사용자 목록을 요청하는 쿼리와 특정 사용자를 요청하는 쿼리에서 동일한 id, name, email 필드를 사용하고 싶다면, 프래그먼트를 다음과 같이 적용할 수 있습니다.

사용자 목록을 가져오는 쿼리:

query {
users {
...UserInfo
}
}

특정 사용자를 가져오는 쿼리:

query {
user(id: 1) {
...UserInfo
}
}

두 쿼리 모두 ...UserInfo를 통해 id, name, email 필드를 요청합니다. 프래그먼트를 사용하면 코드 중복을 줄일 수 있고, 필드가 변경되었을 때 프래그먼트만 수정하면 모든 쿼리가 함께 반영됩니다.


3. 중첩 프래그먼트

프래그먼트는 다른 프래그먼트 안에서 사용할 수도 있습니다. 이를 통해 더 복잡한 구조에서도 재사용성을 극대화할 수 있습니다.

예시:

사용자(User)와 그 사용자가 작성한 게시물(Post)를 요청한다고 가정해 보겠습니다. 각 사용자는 여러 개의 게시물을 가지고 있고, 게시물 정보에도 공통 필드가 있을 수 있습니다.

UserInfo 프래그먼트:

fragment UserInfo on User {
id
name
email
posts {
...PostInfo
}
}

PostInfo 프래그먼트:

fragment PostInfo on Post {
id
title
content
}

쿼리:

query {
users {
...UserInfo
}
}

이 경우, UserInfo 프래그먼트 안에 PostInfo 프래그먼트를 중첩하여, 사용자 정보와 함께 게시물 정보를 가져옵니다.


4. 프래그먼트의 장점

1) 코드 중복 제거

프래그먼트를 사용하면 쿼리에서 중복되는 필드를 하나로 정의해 재사용할 수 있습니다. 동일한 필드를 여러 쿼리에서 중복 작성하는 대신, 프래그먼트를 한 번 정의해두고 모든 곳에서 재사용할 수 있기 때문에 코드가 간결해집니다.

2) 유지보수성 향상

프래그먼트를 사용하면 특정 필드가 변경되었을 때, 각 쿼리에서 필드를 일일이 수정할 필요 없이 프래그먼트만 수정하면 됩니다. 필드가 변경되면 그 프래그먼트를 사용하는 모든 쿼리에 자동으로 반영됩니다.

3) 큰 쿼리의 가독성 개선

대규모 애플리케이션에서는 쿼리가 복잡하고 커질 수 있습니다. 이때 프래그먼트를 사용하면 필드들을 모듈화하여 쿼리의 가독성을 높일 수 있습니다. 필요한 부분을 프래그먼트로 분리해 관리함으로써, 쿼리의 길이가 길어지는 문제를 해결할 수 있습니다.


5. 프래그먼트의 단점 및 고려사항

1) 오버페칭 가능성

프래그먼트는 재사용성을 극대화하기 위한 도구이지만, 때로는 필요하지 않은 데이터를 포함할 수 있습니다. 즉, 쿼리에서 오버페칭(over-fetching) 문제가 발생할 수 있습니다. 프래그먼트가 포함된 모든 필드를 항상 요청하는 구조이기 때문에, 일부 쿼리에서 불필요한 필드를 가져올 수 있습니다.

이를 방지하기 위해, 필요한 필드만 정확하게 가져오는지를 신경 써서 프래그먼트를 설계해야 합니다. 너무 많은 필드를 프래그먼트로 묶어두면, 각 쿼리에서 실제 필요한 데이터보다 많은 데이터를 요청할 수 있기 때문입니다.

2) 타입 의존성

프래그먼트는 특정 GraphQL 타입에 종속적입니다. 즉, on TypeName으로 정의된 타입에서만 프래그먼트를 사용할 수 있습니다. 만약 타입이 변경되거나 다른 타입에서 동일한 필드를 사용해야 한다면, 새로운 프래그먼트를 정의해야 할 수 있습니다.


6. 실습: 프래그먼트 사용 예시

예시 시나리오

  1. 사용자 정보와 그 사용자가 작성한 게시물 정보를 가져오는 쿼리에서 프래그먼트를 사용합니다.
  2. 사용자는 id, name, email 정보를 가지고 있고, 게시물은 id, title, content를 가지고 있습니다.

프래그먼트 정의:

graphql코드 복사# User 정보를 위한 프래그먼트
fragment UserInfo on User {
  id
  name
  email
}

# Post 정보를 위한 프래그먼트
fragment PostInfo on Post {
  id
  title
  content
}

쿼리 사용 예시:

query {
users {
...UserInfo
posts {
...PostInfo
}
}
}

이 쿼리는 각 사용자와 그 사용자가 작성한 게시물을 프래그먼트를 사용해 가져옵니다. ...UserInfo...PostInfo는 각각의 프래그먼트를 사용하여 정보를 요청합니다.


7. 프래그먼트가 유용한 상황

  • 반복적인 데이터 구조: 동일한 데이터를 여러 쿼리에서 반복해서 요청해야 할 때.
  • 공통 필드 관리: 데이터 모델이 바뀌었을 때, 여러 쿼리에서 중복된 필드를 수정할 필요 없이 프래그먼트만 수정할 수 있어 일관성을 유지할 수 있습니다.
  • 모듈화 필요: 큰 쿼리를 여러 프래그먼트로 분리해 가독성을 개선하고, 모듈화된 쿼리 구조를 만들 때 유용합니다.

결론

프래그먼트는 GraphQL 쿼리에서 코드 중복을 줄이고, 일관성을 유지하며, 쿼리의 가독성과 유지보수성을 높이는 중요한 도구입니다. 그러나, 너무 광범위한 필드를 프래그먼트로 묶어버리면 오버페칭 문제가 발생할 수 있으니, 실제로 필요한 필드를 신중하게 선택해 프래그먼트를 설계하는 것이 중요합니다.