Error Handling

GraphQL에서의 오류 처리는 REST API와는 다른 접근 방식을 취한다. GraphQL은 단일 엔드포인트로 여러 리소스에 접근할 수 있기 때문에, 오류 처리도 더 복잡하고 구조화되어 있다.

GraphQL 응답은 기본적으로 다음과 같은 구조를 가진다:

1
2
3
4
{
  "data": {  },  // 요청한 데이터
  "errors": [  ] // 발생한 오류들
}

여기서 중요한 점은 GraphQL은 부분적 성공(partial success)을 허용한다는 것이다. 즉, 일부 필드에서 오류가 발생하더라도 나머지 필드에 대한 데이터는 정상적으로 반환될 수 있다.

GraphQL 오류의 종류

GraphQL에서 오류는 크게 세 가지 범주로 나눌 수 있다:

  1. 문법 오류(Syntax Errors): 쿼리 구문이 잘못된 경우 발생한다.
  2. 검증 오류(Validation Errors): 쿼리가 스키마와 일치하지 않는 경우 발생한다.
  3. 실행 오류(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;
  };
}

용어 정리

용어설명

참고 및 출처