1. 개요

이 예제에서는 GraphQL 의 오류 처리 옵션에 대해 알아봅니다 . 오류 응답에 대한 GraphQL 사양의 내용을 살펴보겠습니다. 결과적으로 Spring Boot를 사용하여 GraphQL 오류 처리의 예를 개발할 것입니다.

2. GraphQL 사양에 따른 응답

GraphQL 사양에 따라 수신된 모든 요청은 올바른 형식의 응답을 반환해야 합니다. 이 잘 구성된 응답은 각각의 성공 또는 실패 요청 작업의 데이터 또는 오류 맵으로 구성됩니다. 또한 응답에는 일부 성공한 결과 데이터 및 필드 오류가 포함될 수 있습니다.

응답 맵의 핵심 구성 요소는 오류 , 데이터확장 입니다.

응답 의 오류  섹션은 요청된 작업 중 실패를 설명합니다. 오류가 발생 하지 않으면 응답에 오류  구성 요소가 없어야 합니다. 다음 섹션에서는 사양에 설명된 다양한 종류의 오류를 살펴보겠습니다.

데이터  섹션 은 요청된 작업의 성공적인 실행 결과를 설명합니다. 작업이 쿼리인 경우 이 구성 요소는 쿼리 루트 작업 유형의 개체입니다. 반면에 작업이 변형인 경우 이 구성 요소는 변형 루트 작업 유형의 개체입니다.

누락된 정보, 유효성 검사 오류 또는 구문 오류로 인해 실행 전에도 요청된 작업이 실패하는 경우 응답에 데이터  구성 요소가 없어야 합니다. 그리고 작업 실행 중에 작업이 실패하고 실패한 결과가 있는 경우 데이터 구성 요소는 null 이어야 합니다 .

응답 맵에는 맵 개체인 extensions 라는 추가 구성 요소가 포함될 수 있습니다 . 이 구성 요소는 구현자가 적절하다고 판단되는 응답에 다른 사용자 지정 콘텐츠를 제공할 수 있도록 도와줍니다. 따라서 콘텐츠 형식에 대한 추가 제한이 없습니다.

데이터 구성 요소가 응답에 없으면 오류 구성 요소가 있어야 하며 하나 이상의 오류를 포함해야 합니다. 또한 실패 이유를 표시해야 합니다.

다음은 GraphQL 오류의 예입니다.

mutation {
  addVehicle(vin: "NDXT155NDFTV59834", year: 2021, make: "Toyota", model: "Camry", trim: "XLE",
             location: {zipcode: "75024", city: "Dallas", state: "TX"}) {
    vin
    year
    make
    model
    trim
  }
}

고유 제약 조건을 위반한 경우 오류 응답은 다음과 같습니다.

{
  "data": null,
  "errors": [
    {
      "errorType": "DataFetchingException",
      "locations": [
        {
          "line": 2,
          "column": 5,
          "sourceName": null
        }
      ],
      "message": "Failed to add vehicle. Vehicle with vin NDXT155NDFTV59834 already present.",
      "path": [
        "addVehicle"
      ],
      "extensions": {
        "vin": "NDXT155NDFTV59834"
      }
    }
  ]
}

3. GraphQL 사양별 오류 응답 구성 요소

응답 의 오류 섹션은 비어 있지 않은 오류 List이며 각 오류는 맵입니다.

3.1. 요청 오류

이름에서 알 수 있듯이 요청 자체에 문제가 있는 경우 작업 실행 전에 요청 오류가 발생할 수 있습니다. 요청 데이터 구문 분석 실패, 요청 문서 유효성 검사, 지원되지 않는 작업 또는 잘못된 요청 값 때문일 수 있습니다.

요청 오류가 발생하면 실행이 시작되지 않았음을 나타냅니다. 즉, 응답의 데이터 섹션이 응답에 없어야 합니다. 즉, 응답에는 오류 섹션만 포함됩니다.

잘못된 입력 구문의 경우를 보여주는 예를 살펴보겠습니다.

query {
  searchByVin(vin: "error) {
    vin
    year
    make
    model
    trim
  }
}

구문 오류에 대한 요청 오류 응답은 다음과 같습니다. 이 경우에는 따옴표가 누락되었습니다.

{
  "data": null,
  "errors": [
    {
      "message": "Invalid Syntax",
      "locations": [
        {
          "line": 5,
          "column": 8,
          "sourceName": null
        }
      ],
      "errorType": "InvalidSyntax",
      "path": null,
      "extensions": null
    }
  ]
}

3.2. 필드 오류

이름에서 알 수 있듯이 필드 오류는 값을 예상 유형으로 강제 변환하지 못하거나 특정 필드의 값을 확인하는 동안 내부 오류로 인해 발생할 수 있습니다. 요청한 작업을 수행하는 동안 필드 오류가 발생했음을 의미합니다.

필드 오류 의 경우 요청된 작업의 실행이 계속되고 부분 결과를 반환합니다 . 즉 , 오류  섹션 의 모든 필드 오류와 함께 응답 의 데이터  섹션이 있어야 합니다 .

다른 예를 살펴보겠습니다.

query {
  searchAll {
    vin
    year
    make
    model
    trim
  }
}

이번에는 GraphQL 스키마에 따라 null을 허용하지 않는 차량 트림 필드를 포함했습니다.

그러나 차량 정보 중 하나에 null 트림 값이 있으므로 부분 데이터( 트림 값이 null이 아닌 차량)만 오류와 함께 반환됩니다.

{
  "data": {
    "searchAll": [
      null,
      {
        "vin": "JTKKU4B41C1023346",
        "year": 2012,
        "make": "Toyota",
        "model": "Scion",
        "trim": "Xd"
      },
      {
        "vin": "1G1JC1444PZ215071",
        "year": 2000,
        "make": "Chevrolet",
        "model": "CAVALIER VL",
        "trim": "RS"
      }
    ]
  },
  "errors": [
    {
      "message": "Cannot return null for non-nullable type: 'String' within parent 'Vehicle' (/searchAll[0]/trim)",
      "path": [
        "searchAll",
        0,
        "trim"
      ],
      "errorType": "DataFetchingException",
      "locations": null,
      "extensions": null
    }
  ]
}

3.3. 오류 응답 형식

앞에서 본 것처럼 응답의 오류 는 하나 이상의 오류 모음입니다. 그리고 모든 오류에는  클라이언트 개발자가 오류를 방지하기 위해 필요한 수정을 할 수 있도록 실패 이유를 설명 하는 메시지 키가 포함되어야 합니다.

각 오류에는 오류 와 관련된 요청된 GraphQL 문서의 줄을 가리키는 위치 List인 locations 라는 키가 포함될 수도 있습니다 . 각 위치는 관련 요소의 줄 번호와 시작 열 번호를 제공하는 키가 각각 줄과 열인 맵입니다.

오류의 일부일 수 있는 다른 키는 path 라고 합니다 . 오류가 있는 응답의 특정 요소까지 추적된 루트 요소의 값 List을 제공합니다. 필드 값이 List인 경우 경로 값은 오류 요소의 필드 이름 또는 인덱스를 나타내는 문자열일 수 있습니다. 오류가 별칭 이름이 있는 필드와 관련된 경우 경로 의 값 은 별칭 이름이어야 합니다.

3.4. 필드 오류 처리

nullable 또는 non-nullable 필드에서 필드 오류가 발생했는지 여부에 관계없이 필드가 null 을 반환하고 오류를 오류 List 에 추가해야 하는 것처럼 처리 해야 합니다.

null 허용 필드의 경우 응답의 필드 값은 null 이지만 오류 에는 이전 섹션에서 본 것처럼 실패 이유 및 기타 정보를 설명하는 이 필드 오류가 포함되어야 합니다.

반면에 부모 필드는 null을 허용하지 않는 필드 오류를 처리합니다. 부모 필드가 null을 허용하지 않는 경우 null을 허용하는 부모 필드 또는 루트 요소에 도달할 때까지 오류 처리가 전파됩니다.

마찬가지로 List 필드에 null을 허용하지 않는 유형이 포함되어 있고 하나 이상의 List 요소가 null 을 반환하면 전체 List이 null 로 확인 됩니다. 또한 List 필드를 포함하는 부모 필드가 null을 허용하지 않는 경우 null을 허용하는 부모 또는 루트 요소에 도달할 때까지 오류 처리가 전파됩니다.

어떤 이유로든 해결 중에 동일한 필드에 대해 여러 오류가 발생하면 해당 필드에 대해 오류에 하나의 필드 오류만 추가해야 합니다 .

4. 스프링 부트 GraphQL 라이브러리

Spring Boot 애플리케이션 예제는 필요한 GraphQL 의존성을 가져오는 spring-boot-starter-graphql 모듈을 사용합니다. 

또한 관련 테스트를 위해 spring-graphql-test 모듈을 사용하고 있습니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.graphql</groupId>
    <artifactId>spring-graphql-test</artifactId>
    <scope>test</scope>
</dependency>

5. 스프링 부트 GraphQL 오류 처리

이 섹션에서는 주로 Spring Boot 애플리케이션 자체에서 GraphQL 오류 처리를 다룹니다. GraphQL JavaGraphQL Spring Boot 애플리케이션 개발 은 다루지 않습니다 .

Spring Boot 애플리케이션 예제에서는 위치 또는 VIN(Vehicle Identification Number)을 기반으로 차량을 변경하거나 쿼리합니다. 이 예제를 사용하여 오류 처리를 구현하는 다양한 방법을 살펴보겠습니다.

다음 하위 섹션에서는 Spring Boot 모듈이 예외 또는 오류를 처리하는 방법을 살펴봅니다.

5.1. 표준 예외가 있는 GraphQL 응답

일반적으로 REST 애플리케이션에서는 RuntimeException 또는 Throwable 을 확장하여 사용자 정의 런타임 예외 클래스를 생성합니다 .

public class InvalidInputException extends RuntimeException {
    public InvalidInputException(String message) {
        super(message);
    }
}

이 접근 방식을 사용하면 GraphQL 엔진이 다음 응답을 반환하는 것을 볼 수 있습니다.

{
  "errors": [
    {
      "message": "INTERNAL_ERROR for 2c69042a-e7e6-c0c7-03cf-6026b1bbe559",
      "locations": [
        {
          "line": 2,
          "column": 5
        }
      ],
      "path": [
        "searchByLocation"
      ],
      "extensions": {
        "classification": "INTERNAL_ERROR"
      }
    }
  ],
  "data": null
}

위의 오류 응답에서 오류에 대한 세부 정보가 포함되어 있지 않음을 알 수 있습니다.

기본적으로 요청 처리 중 모든 예외 는 GraphQL API 에서 DataFetcherExceptionHandler 인터페이스를 구현하는 ExceptionResolversExceptionHandler  클래스 에 의해 처리됩니다 . 응용 프로그램이 하나 이상의 DataFetcherExceptionResolver 구성 요소를 등록할 수 있습니다.

이러한 리졸버는 그 중 하나가 예외를 처리하고 이를 GraphQLError 로 해결할 수 있을 때까지 순차적으로 호출됩니다 . 해석기가 예외를 처리할 수 없는 경우 예외는 INTERNAL_ERROR로 분류됩니다.  위에 표시된 것처럼 실행 ID와 일반 오류 메시Map 포함됩니다.

5.2. 처리된 예외가 있는 GraphQL 응답

이제 사용자 지정 예외 처리를 구현하면 응답이 어떻게 표시되는지 살펴보겠습니다 .

먼저, 또 다른 사용자 지정 예외가 있습니다.

public class VehicleNotFoundException extends RuntimeException {
    public VehicleNotFoundException(String message) {
        super(message);
    }
}

DataFetcherExceptionResolver 는 비동기 계약을 제공합니다. 그러나 대부분의 경우 DataFetcherExceptionResolverAdapter 를 확장 하고 동기식으로 예외를 해결하는 resolveToSingleError 또는 resolveToMultipleErrors 메서드 중 하나를 재정의 하는 것으로 충분합니다.

이제 이 구성 요소를 구현하고 일반 오류 대신 예외 메시지와 함께 NOT_FOUND 분류를 반환할 수 있습니다.

@Component
public class CustomExceptionResolver extends DataFetcherExceptionResolverAdapter {

    @Override
    protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
        if (ex instanceof VehicleNotFoundException) {
            return GraphqlErrorBuilder.newError()
              .errorType(ErrorType.NOT_FOUND)
              .message(ex.getMessage())
              .path(env.getExecutionStepInfo().getPath())
              .location(env.getField().getSourceLocation())
              .build();
        } else {
            return null;
        }
    }
}

여기서는 JSON 응답 의 오류 섹션 에서 보다 유용한 응답을 생성하기 위해 적절한 분류 및 기타 오류 세부 정보를 사용하여 GraphQLError 를 생성했습니다.

{
  "errors": [
    {
      "message": "Vehicle with vin: 123 not found.",
      "locations": [
        {
          "line": 2,
          "column": 5
        }
      ],
      "path": [
        "searchByVin"
      ],
      "extensions": {
        "classification": "NOT_FOUND"
      }
    }
  ],
  "data": {
    "searchByVin": null
  }
}

이 오류 처리 메커니즘의 중요한 세부 사항은 해결되지 않은 예외가 클라이언트에 전송된 오류와 상관 관계가 있는 executionId 와 함께 ERROR 수준에서 기록 된다는 것입니다. 위와 같이 해결된 모든 예외는 로그의 DEBUG 수준에서 기록됩니다.

6. 결론

이 예제에서는 다양한 유형의 GraphQL 오류를 배웠습니다. 또한 사양에 따라 GraphQL 오류의 형식을 지정하는 방법도 살펴보았습니다. 나중에 Spring Boot 애플리케이션에서 오류 처리를 구현했습니다.

항상 그렇듯이 전체 소스 코드는 GitHub에서 사용할 수 있습니다 .

Generic footer banner