GraphQL 서버의 오류 처리 (Apollo Server Error handling)

GraphQL 서버에서 클라이언트로 부터 요청받은 쿼리(Query)나 뮤테이션(Mutation)을 실행하다가 오류가 발생할 수 있습니다.
이런 경우, GraphQL 서버에서 해당 오류에 대한 정보를 응답해줘야 클라이언트에서도 그에 상응하는 화면 처리를 할 수가 있을 것입니다.

이번 포스트에서는 Apollo Server로 개발된 GraphQL 서버에서 어떻게 오류 처리를 해야하는지 알아보도록 하겠습니다.

오류 발생 시 응답 결과

GraphQL 서버에서 오류가 발생할 경우, Apollo Server는 HTTP 응답 바디의 errors 배열에 해당 오류에 대한 정보를 담아줍니다.
여기서 오류 정보를 담기 위해서 객체 대신에 배열이 사용된 이유는 GraphQL API는 Rest API와 달리 한 번의 HTTP 요청에 여러 개의 리소스(resource)에 대한 쿼리가 가능하기 때문입니다.

예를 들어, GraphQL 서버가 다음과 같은 쿼리를 요청받을 경우, allUsersuser 쿼리 둘 다 에서 에러가 발생할 수도 있고, 둘 중 하나에서만 에러가 발생할 수 있습니다.

1
2
3
4
5
6
7
8
query {
allUsers {
email
}
user(id: -1) {
email
}
}

만약에 allUsers 쿼리에서만 오류가 발생하고, user 쿼리는 정상적으로 실행된다면, HTTP 응답의 errors 배열은 allUsers 쿼리에 대한 오류 결과를 담고, data 객체는 user 쿼리에 대한 정상 결과만을 담습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"data": {
"allUsers": null,
"user": {
"email": "test@email.com"
}
},
"errors": [
{
"message": "allUsers query failed"
// 생략
}
]
}

만약에 두 개의 쿼리 모두 에러가 발생했다면, HTTP 응답의 errors 배열은 2개의 오류를 가질 것이고, data 객체는 2개의 쿼리에 대해 모두 null이 할당됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"data": {
"allUsers": null,
"user": null
},
"errors": [
{
"message": "allUsers query failed"
// 생략
},
{
"message": "user query failed"
// 생략
}
]
}

오류 객체의 내부 구조

다음으로 errors 배열에 들어가는 오류 객체 하나 하나는 어떠한 모습을 갖는지 살펴보겠습니다.
먼저, message 속성은 오류 메세지를 담고 있으며, locations은 클라이언트가 전송한 쿼리문 내에서 오류가 발생한 줄과 열을 나타냅니다.
path는 쿼리 경로를 나타내고, extensions는 에러 코드 및 그 외 부가적인 오류 정보를 담는 용도로 쓰입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"message": "user query failed",
"locations": [
{
"line": 5,
"column": 3
}
],
"path": ["user"],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"Error: user query failed",
" at user (/sandbox/index.js:31:13)",
" at field.resolve (/sandbox/node_modules/graphql-extensions/dist/index.js:133:26)",
" at resolveFieldValueOrError (/sandbox/node_modules/graphql/execution/execute.js:467:18)",
" at resolveField (/sandbox/node_modules/graphql/execution/execute.js:434:16)",
" at executeFields (/sandbox/node_modules/graphql/execution/execute.js:275:18)",
" at executeOperation (/sandbox/node_modules/graphql/execution/execute.js:219:122)",
" at executeImpl (/sandbox/node_modules/graphql/execution/execute.js:104:14)",
" at Object.execute (/sandbox/node_modules/graphql/execution/execute.js:64:35)",
" at /sandbox/node_modules/apollo-server-core/dist/requestPipeline.js:240:46",
" at Generator.next (<anonymous>)"
]
}
}
}

일반 오류 발생시키기

지금까지 GraphQL 서버에서 오류 정보가 어떤 모습으로 응답되는지에 대해서 살펴보았으니, 지금부터는 실습 프로젝트를 통해 서버에서 직접 오류를 한 번 발생시키고, 응답 결과를 확인해보겠습니다.

Apollo Server를 이용해서 GraphQL 서버를 개발해보지 않으신 분들은 아래 포스트를 먼저 보시고 기본 프로젝트 세팅을 하시고 돌아오시기 바랍니다.

먼저 스키마를 작성합니다. allUsersuser 쿼리를 정의하고, 두 쿼리에서 필요로하는 User 타입을 정의합니다.

1
2
3
4
5
6
7
8
9
10
11
const typeDefs = gql`
type Query {
allUsers: [User]
user(id: Int): User
}

type User {
id: Int
email: String
}
`;

다음으로 리졸버를 작성합니다. allUsers 쿼리는 항상 오류를 던지고, user 쿼리는 id 입력 파라미터에 음수가 넘어왔을 때만 오류를 던지게 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const resolvers = {
Query: {
allUsers: () => {
throw new Error('allUsers query failed');
},
user: (_, { id }) => {
if (id < 0) throw new Error('id must be non-negative');
return {
id,
email: `test${id}@email.com`
};
}
}
};

이제 작성한 두 개의 쿼리를 함께 호출해보면,

  • 요청
1
2
3
4
5
6
7
8
query {
allUsers {
email
}
user(id: -1) {
email
}
}

아래와 같이 GraphQL 서버는 개의 2개의 쿼리에서 발생한 오류 정보를 errors 배열에 담아 응답합니다.

  • 응답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
{
"data": {
"allUsers": null,
"user": null
},
"errors": [
{
"message": "allUsers query failed",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["allUsers"],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"Error: allUsers query failed",
" at allUsers (/sandbox/index.js:18:13)",
" at field.resolve (/sandbox/node_modules/graphql-extensions/dist/index.js:133:26)",
" at resolveFieldValueOrError (/sandbox/node_modules/graphql/execution/execute.js:467:18)",
" at resolveField (/sandbox/node_modules/graphql/execution/execute.js:434:16)",
" at executeFields (/sandbox/node_modules/graphql/execution/execute.js:275:18)",
" at executeOperation (/sandbox/node_modules/graphql/execution/execute.js:219:122)",
" at executeImpl (/sandbox/node_modules/graphql/execution/execute.js:104:14)",
" at Object.execute (/sandbox/node_modules/graphql/execution/execute.js:64:35)",
" at /sandbox/node_modules/apollo-server-core/dist/requestPipeline.js:240:46",
" at Generator.next (<anonymous>)"
]
}
}
},
{
"message": "id must be non-negative",
"locations": [
{
"line": 5,
"column": 3
}
],
"path": ["user"],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"Error: id must be non-negative",
" at user (/sandbox/index.js:21:25)",
" at field.resolve (/sandbox/node_modules/graphql-extensions/dist/index.js:133:26)",
" at resolveFieldValueOrError (/sandbox/node_modules/graphql/execution/execute.js:467:18)",
" at resolveField (/sandbox/node_modules/graphql/execution/execute.js:434:16)",
" at executeFields (/sandbox/node_modules/graphql/execution/execute.js:275:18)",
" at executeOperation (/sandbox/node_modules/graphql/execution/execute.js:219:122)",
" at executeImpl (/sandbox/node_modules/graphql/execution/execute.js:104:14)",
" at Object.execute (/sandbox/node_modules/graphql/execution/execute.js:64:35)",
" at /sandbox/node_modules/apollo-server-core/dist/requestPipeline.js:240:46",
" at Generator.next (<anonymous>)"
]
}
}
}
]
}

ApolloError 사용하기

위와 같이 일반적인 에러 객체를 던질 경우에 Error 생성자로 넘기는 문자열이 오류 메세지가 되지만, 오류 코드는 항상 INTERNAL_SERVER_ERROR로 고정되게 됩니다.
오류 코드를 포함해서 좀 더 다양한 정보를 오류 응답의 extensions 속성에 추가하고 싶다면 Apollo Server에서 제공하는 ApolloError 클래스를 사용해야 합니다.

ApolloError 클래스는 인자로 messagecode, properties를 받습니다.
code에는 오류 코드로 사용할 문자열을 넘기고, properties에는 그 밖에 오류 관련 정보를 객체로 넘기면 됩니다.

이전 섹션에서 작성한 user 쿼리에 대한 리졸버를 ApolloError를 이용해서 다시 작성해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const resolvers = {
Query: {
allUsers: () => {
throw new Error('allUsers query failed');
},
user: (_, { id }) => {
if (id < 0)
throw new ApolloError('id must be non-negative', 'INVALID_ID', {
parameter: 'id'
});
return {
id,
email: `test${id}@email.com`
};
}
}
};

그 다음 다시 user 쿼리를 음수 id와 함께 호출을 해보면,

  • 요청
1
2
3
4
5
query {
user(id: -1) {
email
}
}

extensions 속성에 "code": "INVALID_ID""parameter": "id"이 세팅되어 응답되었음을 알 수 있습니다.

  • 응답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"data": {
"user": null
},
"errors": [
{
"message": "id must be non-negative",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["user"],
"extensions": {
"code": "INVALID_ID",
"exception": {
"parameter": "id",
"stacktrace": [
"Error: id must be non-negative",
" at user (/sandbox/index.js:22:15)",
" at field.resolve (/sandbox/node_modules/graphql-extensions/dist/index.js:133:26)",
" at resolveFieldValueOrError (/sandbox/node_modules/graphql/execution/execute.js:467:18)",
" at resolveField (/sandbox/node_modules/graphql/execution/execute.js:434:16)",
" at executeFields (/sandbox/node_modules/graphql/execution/execute.js:275:18)",
" at executeOperation (/sandbox/node_modules/graphql/execution/execute.js:219:122)",
" at executeImpl (/sandbox/node_modules/graphql/execution/execute.js:104:14)",
" at Object.execute (/sandbox/node_modules/graphql/execution/execute.js:64:35)",
" at /sandbox/node_modules/apollo-server-core/dist/requestPipeline.js:240:46",
" at Generator.next (<anonymous>)"
]
}
}
}
]
}

에러 로그 남기기

GraphQL서버에서 에러가 발생했을 때 클라이언트에 알려주는 것도 중요하지만, 서버 내부적으로도 에러 로그를 남겨놔야 나중에 디버깅이 용이할 것입니다.
이를 지원하기 위해서 ApolloServer 생성자는 formatError 속성을 통해, 함수를 하나 받습니다.
이 함수에는 에러 객체가 인자로 넘어오기 때문에, 에러 객체에 접근하여 에러 내용을 콘솔이나 파일에 출력하는 코드를 작성할 수 있습니다.
여기서 반드시 인자로 받은 에러 객체를 다시 리턴을 해줘야 클라이언트까지 에러 정보가 전달된다는 점 주의 바랍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const formatError = err => {
console.error('--- GraphQL Error ---');
console.error('Path:', err.path);
console.error('Message:', err.message);
console.error('Code:', err.extensions.code);
console.error('Original Error', err.originalError);
return err;
};

const server = new ApolloServer({
typeDefs,
resolvers,
formatError
});

이제 GraphQL 서버에서 오류가 발생하면 다음과 같이 서버 콘솔에 오류 내용이 출력될 것 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--- GraphQL Error ---
Path: [ 'user' ]
Message: id must be non-negative
Code: INVALID_ID
Original Error { Error: id must be non-negative
at user (/sandbox/index.js:22:15)
at field.resolve (/sandbox/node_modules/graphql-extensions/dist/index.js:133:26)
at resolveFieldValueOrError (/sandbox/node_modules/graphql/execution/execute.js:467:18)
at resolveField (/sandbox/node_modules/graphql/execution/execute.js:434:16)
at executeFields (/sandbox/node_modules/graphql/execution/execute.js:275:18)
at executeOperation (/sandbox/node_modules/graphql/execution/execute.js:219:122)
at executeImpl (/sandbox/node_modules/graphql/execution/execute.js:104:14)
at Object.execute (/sandbox/node_modules/graphql/execution/execute.js:64:35)
at /sandbox/node_modules/apollo-server-core/dist/requestPipeline.js:240:46
at Generator.next (<anonymous>) parameter: 'id', extensions: { code: 'INVALID_ID' } }

스텍트레이스 숨기기

서버에서 에러가 발생했을 때 클리이언트에게 스텍트레이스(stacktrace)까지 제공하는 것은 보안 상의 이유로 일반적으로는 권장되지 않습니다.
응답 결과에서 에러 스텍트레이스 정보를 제외시키고 싶은 경우에는 ApolloServer 생성자의 debug 속성을 false로 세팅하면 됩니다.

1
2
3
4
5
6
const server = new ApolloServer({
typeDefs,
resolvers,
formatError,
debug: false
});

전체 코드

마치면서

이상으로 Apollo Server를 이용하여 GraphQL 서버에서 발생하는 오류를 처리하는 방법에 대해서 살펴보았습니다.

관련 포스트

공유하기