GraphQL에서의 **에러 처리(Error Handling)**는 클라이언트와 서버 간에 일어나는 문제를 효율적으로 처리하고, 클라이언트에게 유용한 정보를 전달하는 데 매우 중요한 역할을 합니다. GraphQL은 **부분 성공(Partial Success)**이라는 개념을 가지고 있어, 한 쿼리 내에서 일부 필드가 실패하더라도 나머지 필드의 데이터를 정상적으로 반환할 수 있는 유연한 에러 처리 방식을 제공합니다.
GraphQL에서 에러는 일반적으로 두 가지로 구분할 수 있습니다:
- GraphQL 레벨 에러: 쿼리 문법이나 스키마와 관련된 에러.
- 비즈니스 로직 에러: 데이터베이스에서 데이터를 찾지 못하거나, 권한이 부족할 때 발생하는 에러.
이제 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 에러 처리 패턴
- 부분 성공 허용: GraphQL의 특성상 여러 필드에서 일부 성공과 일부 실패가 발생할 수 있습니다. 이런 상황에서는 성공한 데이터는
data
에, 실패한 데이터는errors
필드에 각각 포함됩니다. - 사용자 정의 에러 핸들링: 도메인 로직에 따라 적절한 커스텀 에러를 정의하고 클라이언트에 더 의미 있는 정보를 전달할 수 있습니다. 예를 들어,
ValidationError
,AuthenticationError
,AuthorizationError
등의 커스텀 에러 클래스를 만들어 사용하는 것이 좋습니다. - 에러 코드 및 확장 정보 제공: 클라이언트가 단순한 에러 메시지 외에 에러 코드나 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에서의 에러 처리는 부분 성공을 허용하면서도, 클라이언트에게 유의미한 에러 정보를 제공할 수 있도록 설계되어 있습니다. 이를 통해 사용자는 쿼리의 일부만 실패하더라도 나머지 데이터는 정상적으로 사용할 수 있습니다.
또한, 확장 필드를 활용해 에러 코드나 상태 코드를 추가로 제공함으로써, 클라이언트는 더 구체적인 에러 처리를 할 수 있습니다. 전역 에러 핸들러나 커스텀 에러 클래스를 통해 서버의 에러 관리를 더욱 효율적으로 처리할 수 있습니다.