Error Handling#
GraphQL에서의 오류 처리는 REST API와는 다른 접근 방식을 취한다. GraphQL은 단일 엔드포인트로 여러 리소스에 접근할 수 있기 때문에, 오류 처리도 더 복잡하고 구조화되어 있다.
GraphQL 응답은 기본적으로 다음과 같은 구조를 가진다:
1
2
3
4
| {
"data": { … }, // 요청한 데이터
"errors": [ … ] // 발생한 오류들
}
|
여기서 중요한 점은 GraphQL은 부분적 성공(partial success)을 허용한다는 것이다. 즉, 일부 필드에서 오류가 발생하더라도 나머지 필드에 대한 데이터는 정상적으로 반환될 수 있다.
GraphQL 오류의 종류#
GraphQL에서 오류는 크게 세 가지 범주로 나눌 수 있다:
- 문법 오류(Syntax Errors): 쿼리 구문이 잘못된 경우 발생한다.
- 검증 오류(Validation Errors): 쿼리가 스키마와 일치하지 않는 경우 발생한다.
- 실행 오류(Execution Errors): 쿼리 실행 중에 발생하는 오류이다.
표준 오류 형식#
GraphQL 명세에 따르면, 오류 객체는 다음과 같은 필드를 포함한다:
1
2
3
4
5
6
| {
"message": "오류 메시지",
"locations": [{ "line": 6, "column": 7 }], // 오류가 발생한 위치
"path": ["user", "email"], // 오류가 발생한 필드 경로
"extensions": { … } // 추가 정보
}
|
extensions
필드를 통해 구현에 따라 추가적인 정보를 제공할 수 있다.
실용적인 오류 처리 방법#
오류 유형 확장하기#
많은 GraphQL 구현체에서는 사용자 정의 오류 유형을 만들 수 있다. 예를 들어, Apollo Server에서는 다음과 같이 활용할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 사용자 정의 오류 클래스
class NotFoundError extends ApolloError {
constructor(message, properties) {
super(message, 'NOT_FOUND', properties);
}
}
// 리졸버에서 오류 발생시키기
const resolvers = {
Query: {
user: (parent, { id }) => {
const user = findUser(id);
if (!user) {
throw new NotFoundError(`ID가 ${id}인 사용자를 찾을 수 없습니다`);
}
return user;
}
}
};
|
비즈니스 로직 오류 처리하기#
비즈니스 로직에서 발생하는 오류(예: 유효성 검사 실패)는 GraphQL 응답의 data
필드 내에서 처리하는 것이 좋다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| type MutationResponse {
success: Boolean!
message: String
code: String
}
type CreateUserResponse implements MutationResponse {
success: Boolean!
message: String
code: String
user: User
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserResponse!
}
|
이 방식을 사용하면, HTTP 상태 코드를 항상 200으로 유지하면서도 클라이언트에 오류 정보를 명확하게 전달할 수 있다.
오류 마스킹(Error Masking)#
민감한 오류 정보가 클라이언트에 노출되지 않도록 오류 메시지를 마스킹하는 것이 중요하다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Apollo Server 예제
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (error) => {
// 내부 서버 오류 마스킹
if (error.originalError instanceof InternalServerError) {
return {
message: '내부 서버 오류가 발생했습니다',
code: 'INTERNAL_ERROR'
};
}
// 나머지 오류는 그대로 반환
return error;
}
});
|
클라이언트 측 오류 처리#
클라이언트에서는 다음과 같이 오류를 처리할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Apollo Client 예제
client.query({
query: GET_USER,
variables: { id: 1 }
})
.then(result => {
// 부분적 오류 확인
if (result.errors) {
console.error('GraphQL 오류:', result.errors);
}
// 데이터 처리
if (result.data) {
// …
}
})
.catch(error => {
// 네트워크 오류 등 처리
console.error('요청 오류:', error);
});
|
고급 오류 처리 패턴#
오류 코드 표준화#
오류 코드를 표준화하여 클라이언트가 쉽게 처리할 수 있도록 한다:
1
2
3
4
5
6
7
8
9
10
11
| {
"errors": [
{
"message": "인증 토큰이 유효하지 않습니다",
"extensions": {
"code": "UNAUTHENTICATED",
"statusCode": 401
}
}
]
}
|
컨텍스트 기반 오류 처리#
GraphQL 컨텍스트를 활용하여 오류 처리를 일관되게 관리할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
return {
throwError: (code, message) => {
throw new ApolloError(message, code);
}
};
}
});
// 리졸버에서 사용
const resolvers = {
Query: {
protectedResource: (_, __, context) => {
if (!context.user) {
context.throwError('UNAUTHENTICATED', '인증이 필요합니다');
}
// …
}
}
};
|
오류 그룹화#
복잡한 뮤테이션에서 여러 오류를 그룹화하여 반환할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| type FieldError {
field: String!
message: String!
}
type MutationResponse {
success: Boolean!
errors: [FieldError!]
data: User
}
type Mutation {
registerUser(input: RegisterInput!): MutationResponse!
}
|
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
| const resolvers = {
Mutation: {
registerUser: (_, { input }) => {
const errors = [];
if (input.password.length < 8) {
errors.push({
field: 'password',
message: '비밀번호는 최소 8자 이상이어야 합니다'
});
}
if (input.email && !isValidEmail(input.email)) {
errors.push({
field: 'email',
message: '유효한 이메일 주소를 입력해주세요'
});
}
if (errors.length > 0) {
return {
success: false,
errors,
data: null
};
}
// 사용자 등록 로직…
return {
success: true,
errors: [],
data: newUser
};
}
}
};
|
프레임워크별 오류 처리 방법#
Apollo Server#
Apollo Server는 가장 널리 사용되는 GraphQL 서버 중 하나로, 강력한 오류 처리 기능을 제공한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 사용자 정의 오류 클래스
import { ApolloError, UserInputError, ForbiddenError, AuthenticationError } from 'apollo-server';
// 리졸버에서 오류 발생시키기
if (!user) {
throw new AuthenticationError('로그인이 필요합니다');
}
if (!hasPermission(user, 'DELETE_ITEM')) {
throw new ForbiddenError('이 작업을 수행할 권한이 없습니다');
}
if (!isValidInput(input)) {
throw new UserInputError('입력 데이터가 올바르지 않습니다', {
invalidArgs: Object.keys(invalidFields)
});
}
|
GraphQL-Java#
Java 기반 GraphQL 구현에서는 다음과 같이 오류를 처리할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public DataFetcher<User> getUserById() {
return environment -> {
String id = environment.getArgument("id");
User user = userRepository.findById(id);
if (user == null) {
throw new GraphQLError("사용자를 찾을 수 없습니다") {
@Override
public Map<String, Object> getExtensions() {
Map<String, Object> extensions = new HashMap<>();
extensions.put("code", "NOT_FOUND");
return extensions;
}
@Override
public List<SourceLocation> getLocations() {
return null;
}
};
}
return user;
};
}
|
용어 정리#
참고 및 출처#