InfoGrab DocsInfoGrab Docs

백엔드 GraphQL API 가이드

요약

GraphQL 및 REST API 섹션을 참조하세요. GraphQL API는 버전이 없습니다(versionless). GraphQL API는 버전이 없지만, 업데이트 간 하위 호환성을 고려해야 하며, 이것이 일부 사용자에게 사이드바가 로드되지 않은 문제와 같은 인시던트를 유발할 수 있습니다.


백엔드 GraphQL API 가이드#

  이 문서는 [GitLab GraphQL API](/19.1/api/graphql/)의 백엔드를 구현하는 엔지니어를 위한 스타일 및 기술 지침을 담고 있습니다.

REST API와의 관계#

GraphQL 및 REST API 섹션을 참조하세요.

버전 관리#

GraphQL API는 버전이 없습니다(versionless).

다중 버전 호환성#

GraphQL API는 버전이 없지만, 업데이트 간 하위 호환성을 고려해야 하며, 이것이 일부 사용자에게 사이드바가 로드되지 않은 문제와 같은 인시던트를 유발할 수 있습니다.

완화 방법#

인시던트의 위험을 줄이기 위해, GitLab Self-Managed 및 GitLab Dedicated에서는 @gl_introduced 디렉티브를 사용하여 노드가 도입된 GitLab 버전을 백엔드에 알릴 수 있습니다. 이렇게 하면 쿼리가 이전 버전의 백엔드에 도달했을 때, 해당 미래 노드가 쿼리에서 제거됩니다.

이 방법은 GitLab.com에서 발생하는 문제를 완화하지는 못합니다. 새로운 GraphQL 필드는 프론트엔드 이전에 백엔드가 먼저 GitLab.com에 배포되어야 합니다.

예를 들어 다음과 같이 임의의 필드에 @gl_introduced 디렉티브를 사용할 수 있습니다:

fragment otherFieldsWithFuture on Namespace {
  webUrl
  otherFutureField @gl_introduced(version: "99.9.9")
}

query namespaceWithFutureFields {
  futureField @gl_introduced(version: "99.9.9")
  namespace(fullPath: "gitlab-org") {
    name
    futureField @gl_introduced(version: "99.9.9")
    ...otherFieldsWithFuture
  }
}

응답:

{
  "data": {
    "futureField": null,
    "namespace": {
      "name": "Gitlab Org",
      "futureField": null,
      "webUrl": "http://gdk.test:3000/groups/gitlab-org",
      "otherFutureField": null
    }
  }
}

다음의 경우에는 이 디렉티브를 사용하지 않아야 합니다:

인수(Arguments): 실행 가능한 디렉티브는 인수를 지원하지 않습니다.

프래그먼트(Fragment): 대신 프래그먼트 노드에서 디렉티브를 사용하세요.

쿼리 또는 객체 내의 단독 미래 필드, 예를 들어:

query fetchData {
  futureField @gl_introduced(version: "99.9.9")
}

응답:

{
  "errors": [
    {
      "graphQLErrors": [
        {
          "message": "Field must have selections (query 'fetchData' returns Query but has no selections. Did you mean 'fetchData { ... }'?)",
          "locations": [
            {
              "line": 1,
              "column": 1
            }
          ],
          "path": [
            "query fetchData"
          ],
          "extensions": {
            "code": "selectionMismatch",
            "nodeName": "query 'fetchData'",
            "typeName": "Query"
          }
        }
      ],
      "clientErrors": [],
      "networkError": null,
      "message": "Field must have selections (query 'fetchData' returns Query but has no selections. Did you mean 'fetchData { ... }'?)",
      "stack": ""
    }
  ]
}

Query:

query fetchData {
  futureField @gl_introduced(version: "99.9.9") {
    id
  }
}

Response:

{
  "errors": [
    {
      "graphQLErrors": [
        {
          "message": "Field must have selections (query 'fetchData' returns Query but has no selections. Did you mean 'fetchData { ... }'?)",
          "locations": [
            {
              "line": 1,
              "column": 1
            }
          ],
          "path": [
            "query fetchData"
          ],
          "extensions": {
            "code": "selectionMismatch",
            "nodeName": "query 'fetchData'",
            "typeName": "Query"
          }
        }
      ],
      "clientErrors": [],
      "networkError": null,
      "message": "Field must have selections (query 'fetchData' returns Query but has no selections. Did you mean 'fetchData { ... }'?)",
      "stack": ""
    }
  ]
}

Query:

query fetchData {
  project(fullPath: "gitlab-org/gitlab") {
    futureField @gl_introduced(version: "99.9.9")
  }
}

Response:

{
  "errors": [
    {
      "graphQLErrors": [
        {
          "message": "Field must have selections (field 'project' returns Project but has no selections. Did you mean 'project { ... }'?)",
          "locations": [
            {
              "line": 2,
              "column": 3
            }
          ],
          "path": [
            "query fetchData",
            "project"
          ],
          "extensions": {
            "code": "selectionMismatch",
            "nodeName": "field 'project'",
            "typeName": "Project"
          }
        }
      ],
      "clientErrors": [],
      "networkError": null,
      "message": "Field must have selections (field 'project' returns Project but has no selections. Did you mean 'project { ... }'?)",
      "stack": ""
    }
  ]
}
Non-nullable fields#

@gl_introduced 디렉티브가 적용된 미래 필드는 백엔드에 존재하지 않을 때 null로 폴백됩니다. 따라서 non-nullable 필드라도 @gl_introduced 디렉티브가 있는 경우 프론트엔드에서 null 체크가 필요합니다.

GitLab에서 GraphQL 학습하기#

GitLab에서 GraphQL을 배우고자 하는 백엔드 엔지니어는 이 가이드를 GraphQL Ruby gem 가이드와 함께 읽어야 합니다. 해당 가이드에서는 gem의 기능을 설명하며, 그 내용은 일반적으로 이 문서에서 중복 기술하지 않습니다.

GraphQL 자체의 설계와 기능에 대해서는 GraphQL spec의 내용을 쉽게 정리한 graphql.org 가이드를 읽어보세요.

Deep Dive#

2019년 3월, Nick Thomas가 GitLab GraphQL API에 관한 Deep Dive(GitLab 팀원 전용: https://gitlab.com/gitlab-org/create-stage/issues/1)를 진행했습니다. 이 세션은 향후 코드베이스의 해당 영역을 담당할 수 있는 모든 구성원과 도메인 지식을 공유하기 위한 것이었습니다. 다음 링크에서 관련 자료를 확인할 수 있습니다:

YouTube 녹화본, 슬라이드는 Google SlidesPDF에서 제공됩니다. 그 이후로 세부 사항이 일부 변경되었지만, 좋은 입문 자료로 활용할 수 있습니다.

GitLab의 GraphQL 구현 방식#

Robert Mosolgo가 작성한 GraphQL Ruby gem을 사용합니다. 또한 GraphQL Pro 구독을 보유하고 있습니다. 자세한 내용은 GraphQL Pro 구독을 참고하세요.

모든 GraphQL 쿼리는 단일 엔드포인트 (app/controllers/graphql_controller.rb#execute)로 전달되며, /api/graphql에서 API 엔드포인트로 노출됩니다.

GraphiQL#

GraphiQL은 기존 쿼리를 실험해볼 수 있는 인터랙티브 GraphQL API 탐색기입니다. https://<your-gitlab-site.com>/-/graphql-explorer에서 모든 GitLab 환경에 접근할 수 있습니다. 예를 들어, GitLab.com의 탐색기를 사용할 수 있습니다.

GraphQL 변경 사항이 포함된 머지 리퀘스트 리뷰#

GraphQL 프레임워크에는 주의해야 할 몇 가지 특이 사항이 있으며, 이를 충족시키기 위해 도메인 전문 지식이 필요합니다.

GraphQL 파일을 수정하거나 엔드포인트를 추가하는 머지 리퀘스트를 리뷰해야 하는 경우, GraphQL 리뷰 가이드를 참고하세요.

GraphQL 로그 읽기#

GraphQL 요청 로그를 검사하고 GraphQL 쿼리의 성능을 모니터링하는 방법에 대한 팁은 GraphQL 로그 읽기 가이드를 참고하세요.

해당 페이지에는 다음과 같은 방법에 대한 팁이 있습니다:

  • 더 이상 사용되지 않는 필드의 사용 현황 확인.

  • 쿼리가 프론트엔드에서 온 것인지 여부 확인.

인증#

인증은 GraphqlController를 통해 이루어지며, 현재는 Rails 애플리케이션과 동일한 인증 방식을 사용합니다. 따라서 세션을 공유할 수 있습니다.

쿼리 문자열에 private_token을 추가하거나, HTTP_PRIVATE_TOKEN 헤더를 추가하는 것도 가능합니다.

제한 사항#

GraphQL API에는 여러 제한 사항이 적용되며, 그 중 일부는 개발자가 재정의할 수 있습니다.

최대 페이지 크기#

기본적으로 연결(connection)app/graphql/gitlab_schema.rb에서 정의된 최대 레코드 수만큼만 페이지당 반환할 수 있습니다.

개발자는 연결을 정의할 때 커스텀 최대 페이지 크기를 지정할 수 있습니다.

최대 복잡도#

복잡도에 대한 설명은 클라이언트용 API 페이지에서 확인할 수 있습니다.

필드는 기본적으로 쿼리의 복잡도 점수에 1을 추가하지만, 개발자는 필드를 정의할 때 커스텀 복잡도를 지정할 수 있습니다.

쿼리의 복잡도 점수는 직접 쿼리할 수도 있습니다.

요청 타임아웃#

요청은 30초 후 타임아웃됩니다.

최대 필드 호출 횟수 제한#

경우에 따라, 특정 필드가 여러 상위 노드에서 평가되는 것을 방지해야 할 수 있습니다. 이는 N+1 쿼리 문제를 야기하지만 최적의 해결책이 없는 경우입니다. 이 방법은 연관 관계 미리 로드를 위한 lookahead 또는 배치 처리 사용과 같은 방법을 먼저 고려한 후 사용하는 최후의 수단으로만 활용해야 합니다.

예를 들어:

# This usage is expected.
query {
  project {
    environments
  }
}

# This usage is NOT expected.
# It results in N+1 query problem. EnvironmentsResolver can't use GraphQL batch loader in favor of GraphQL pagination.
query {
  projects {
    nodes {
      environments
    }
  }
}

이를 방지하려면 필드에 Gitlab::Graphql::Limit::FieldCallCount 확장을 사용할 수 있습니다:

# This allows maximum 1 call to the `environments` field. If the field is evaluated on more than one node,
# it raises an error.
field :environments do
        extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
      end

또는 리졸버 클래스에 확장을 적용할 수 있습니다:

module Resolvers
  class EnvironmentsResolver < BaseResolver
    extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
    # ...
  end
end

이 제한을 추가할 때는 영향을 받는 필드의 description도 그에 맞게 업데이트해야 합니다. 예를 들면,

field :environments,
      description: 'Environments of the project. This field can only be resolved for one project in any single request.'

브레이킹 체인지(Breaking Changes)#

GitLab GraphQL API는 버전이 없는(versionless) 방식이므로 개발자는 더 이상 사용되지 않음(Deprecation) 및 제거 프로세스를 숙지해야 합니다.

브레이킹 체인지는 다음과 같습니다:

  • 필드, 인수, 열거형 값, 또는 뮤테이션의 제거 또는 이름 변경.

  • 인수의 타입 또는 타입 이름 변경. 인수의 타입은 클라이언트가 변수를 사용할 때 선언하며, 변경하면 기존 타입 이름을 사용하는 쿼리가 API에서 거부됩니다.

  • 필드 또는 열거형 값의 스칼라 타입(scalar type) 변경으로 인해 값이 JSON으로 직렬화되는 방식이 달라지는 경우. 예를 들어 JSON String에서 JSON Number로의 변경, 또는 String 형식이 변경되는 경우. 오브젝트 타입(object type)으로의 변경은 오브젝트의 모든 스칼라 타입 필드가 동일한 방식으로 계속 직렬화되는 경우에 한해 허용될 수 있습니다.

  • 필드의 복잡도(complexity) 또는 리졸버의 복잡도 배수 증가.

  • Nullable 필드에서 설명한 바와 같이, 필드를 nullable이 아닌(null: false) 상태에서 nullable(null: true)로 변경.

  • 인수를 선택 사항(required: false)에서 필수(required: true)로 변경.

  • 연결(connection)의 최대 페이지 크기 변경.

  • 쿼리 복잡도 및 깊이에 대한 전역 제한 낮추기.

  • 이전에 허용되던 제한에 쿼리가 걸리는 결과를 초래하는 기타 모든 변경 사항.

항목을 더 이상 사용하지 않음으로 표시하는 방법은 스키마 항목 더 이상 사용되지 않음 처리 섹션을 참조하세요.

브레이킹 체인지 예외 사항#

GraphQL API 브레이킹 체인지 예외 사항 문서를 참조하세요.

Global ID#

GitLab GraphQL API는 Global ID(예: "gid://gitlab/MyObject/123")를 사용하며 데이터베이스 기본 키 ID는 절대 사용하지 않습니다.

Global ID는 클라이언트 사이드 라이브러리에서 캐싱 및 페칭에 사용되는 컨벤션입니다.

참고:

값이 GlobalID인 경우 입력 및 출력 인수의 타입으로 커스텀 스칼라 타입(Types::GlobalIDType)을 사용해야 합니다. 이 타입을 ID 대신 사용하면 다음과 같은 이점이 있습니다:

  • 값이 GlobalID인지 유효성을 검사합니다.

  • 사용자 코드에 전달하기 전에 GlobalID로 파싱합니다.

  • 오브젝트의 타입으로 파라미터화할 수 있어(예: GlobalIDType[Project]) 더욱 강력한 유효성 검사와 보안을 제공합니다.

모든 새 인수 및 결과 타입에 이 타입을 사용하는 것을 고려하세요. 더 넓은 범위의 오브젝트를 허용하려면(예: GlobalIDType[Issue] 대신 GlobalIDType[Issuable]) concern 또는 상위 타입(supertype)으로 이 타입을 파라미터화하는 것도 완전히 가능합니다.

최적화#

기본적으로 GraphQL은 적극적으로 최소화하려는 노력을 기울이지 않으면 N+1 문제가 발생하기 쉽습니다.

안정성과 확장성을 위해 쿼리에 N+1 성능 문제가 발생하지 않도록 해야 합니다.

다음은 GraphQL 코드를 최적화하는 데 도움이 되는 도구 목록입니다:

개발 중 N+1 문제 확인 방법#

개발 중에 N+1 문제는 다음 방법으로 발견할 수 있습니다:

  • 컬렉션 데이터를 반환하는 GraphQL 쿼리를 실행하면서 development.log를 추적(tail)합니다. Bullet이 도움이 될 수 있습니다.

  • GitLab UI에서 쿼리를 실행하는 경우 성능 표시줄을 관찰합니다.

  • 기능의 N+1 문제가 없거나 제한적임을 검증하는 request spec을 추가합니다.

필드#

타입#

우리는 코드 우선(code-first) 스키마를 사용하며, Ruby에서 모든 타입을 선언합니다.

예를 들어, app/graphql/types/project_type.rb:

graphql_name 'Project'

field :full_path, GraphQL::Types::ID, null: true
field :name, GraphQL::Types::String, null: true

각 타입에는 이름을 부여합니다(이 경우 Project).

full_pathname스칼라(scalar) GraphQL 타입입니다. full_pathGraphQL::Types::ID 타입입니다 (GraphQL::Types::ID 사용 시기 참조). name은 일반적인 GraphQL::Types::String 타입입니다. 스칼라 데이터 타입을 위해 커스텀 GraphQL 데이터 타입을 선언할 수도 있습니다(예: TimeType).

GraphQL API를 통해 모델을 노출할 때는 app/graphql/types에 새 타입을 생성하는 방식으로 수행합니다.

타입에서 속성을 노출할 때는 정의 내부의 로직을 최대한 간결하게 유지해야 합니다. 그 대신, 로직을 프레젠터(presenter)로 이동하는 것을 고려하세요:

class Types::MergeRequestType < BaseObject
  present_using MergeRequestPresenter

  name 'MergeRequest'
end

기존 프레젠터를 사용할 수도 있지만, GraphQL 전용 새 프레젠터를 생성하는 것도 가능합니다.

프레젠터는 필드에 의해 리졸브된 객체와 컨텍스트를 사용하여 초기화됩니다.

Nullable 필드#

GraphQL에서는 필드를 “nullable” 또는 “non-nullable”로 지정할 수 있습니다. 전자는 지정된 타입의 값 대신 null이 반환될 수 있음을 의미합니다. 일반적으로 다음과 같은 이유로 non-nullable 필드보다 nullable 필드를 사용하는 것이 좋습니다:

  • 데이터가 필수에서 선택으로, 또는 그 반대로 전환되는 경우가 흔합니다

  • 필드가 선택적으로 변할 가능성이 없더라도, 쿼리 시점에 해당 필드가 사용 가능하지 않을 수 있습니다

예를 들어, blob의 content를 Gitaly에서 조회해야 할 수 있습니다

  • content가 nullable이라면, 전체 쿼리를 실패시키는 대신 부분적인 응답을 반환할 수 있습니다

  • 버전 없는 스키마에서 non-nullable 필드를 nullable 필드로 변경하는 것은 어렵습니다

Non-nullable 필드는 필드가 필수이고, 향후 선택적으로 변할 가능성이 매우 낮으며, 계산이 간단한 경우에만 사용해야 합니다. id 필드가 그 예시입니다.

Non-nullable GraphQL 스키마 필드는 오브젝트 타입 뒤에 느낌표(bang) !가 붙습니다. 다음은 gitlab_schema.graphql 파일의 예시입니다:

  id: ProjectID!

다음은 non-nullable GraphQL 배열의 예시입니다:


  errors: [String!]!

추가 참고 자료:

Global ID 노출#

GitLab의 Global ID 사용 방침에 따라, 노출 시 항상 데이터베이스 기본 키 ID를 Global ID로 변환해야 합니다.

id라는 이름의 모든 필드는 자동으로 변환됩니다.

id라는 이름이 아닌 필드는 수동으로 변환해야 합니다. 이를 위해 Gitlab::GlobalID.build를 사용하거나, GlobalID::Identification 모듈이 믹스인된 객체에서 #to_global_id를 호출하면 됩니다.

Types::Notes::DiscussionType의 예시:

field :reply_id, Types::GlobalIDType[Discussion]

def reply_id
  Gitlab::GlobalId.build(object, id: object.reply_id)
end

GraphQL::Types::ID 사용 시기#

GraphQL::Types::ID를 사용하면 해당 필드는 GraphQL ID 타입이 되며, JSON 문자열로 직렬화됩니다. 그러나 ID는 클라이언트에게 특별한 의미를 가집니다. GraphQL 스펙에서는 다음과 같이 말합니다:

ID 스칼라 타입은 고유 식별자를 나타내며, 주로 객체를 다시 가져오거나 캐시의 키로 사용됩니다.

GraphQL 명세는 ID의 고유성 범위를 명확히 정의하지 않습니다. GitLab에서는 ID가 최소한 타입 이름 기준으로 고유해야 한다고 결정했습니다. 타입 이름은 Types:: 클래스 중 하나의 graphql_name으로, 예를 들어 Project 또는 Issue입니다.

이를 바탕으로:

  • Project.fullPathID여야 합니다. API 전체에서 동일한 fullPath를 가진 다른 Project가 없으며, 해당 필드가 식별자이기도 하기 때문입니다.

  • Issue.iidID되어서는 안 됩니다. API 전체에서 동일한 iid를 가진 Issue 타입이 여러 개 존재할 수 있기 때문입니다. 서로 다른 프로젝트의 Issue를 캐싱하는 클라이언트가 있다면 이를 ID로 취급하는 것은 문제가 됩니다.

  • Project.id는 일반적으로 ID가 될 자격이 있습니다. 해당 ID 값을 가진 Project는 하나뿐이기 때문입니다. 단, 데이터베이스(DB) ID 값에는 ID 타입 대신 Global ID 타입을 사용하므로 Global ID로 타입을 지정하게 됩니다.

다음 표에 이를 정리했습니다:

필드 목적 GraphQL::Types::ID 사용 여부
Full path check-circle 예
데이터베이스 ID dotted-circle 아니오
IID dotted-circle 아니오

markdown_field#

markdown_fieldfield를 감싸는 헬퍼 메서드로, 렌더링된 Markdown을 반환하는 필드에는 항상 이 메서드를 사용해야 합니다.

이 헬퍼는 GraphQL 쿼리 컨텍스트를 헬퍼에서 사용할 수 있도록 하여 기존 MarkupHelper로 모델의 Markdown 필드를 렌더링합니다.

헬퍼에서 컨텍스트를 사용할 수 있어야 하는 이유는 현재 사용자가 볼 수 없는 리소스에 대한 링크를 삭제하기 위해서입니다.

HTML 렌더링 시 쿼리가 발생할 수 있으므로, 이러한 필드의 복잡도는 기본값보다 5 높게 설정됩니다.

Markdown 필드 헬퍼는 다음과 같이 사용할 수 있습니다:

markdown_field :note_html, null: false

이 코드는 모델의 Markdown 필드 note를 렌더링하는 필드를 생성합니다. method: 인수를 추가하여 이를 재정의할 수 있습니다.

markdown_field :body_html, null: false, method: :note

이 필드에는 기본적으로 다음 설명이 제공됩니다:

note의 GitLab Flavored Markdown 렌더링 결과

description: 인수를 전달하여 이를 재정의할 수 있습니다.

Connection 타입#

구현에 대한 자세한 내용은 [페이지네이션 구현](/19.1/development/api_graphql_styleguide/#pagination-implementation)을 참조하세요.

GraphQL은 커서 기반 페이지네이션을 사용하여 항목 컬렉션을 노출합니다. 이를 통해 클라이언트에 많은 유연성을 제공하면서 백엔드에서 다양한 페이지네이션 모델을 사용할 수 있습니다.

CountableConnectionType과 LimitedCountableConnectionType 중 선택하기#

GitLab은 카운팅을 지원하는 컬렉션을 위한 두 가지 connection 타입을 제공합니다:

  • CountableConnectionType - 기본적으로 정확한 카운트를 반환하며, 성능 최적화를 위한 선택적 limit 인수를 제공합니다.

  • LimitedCountableConnectionType - 항상 제한된 카운트를 반환합니다(기본 제한: 1000).

CountableConnectionType 사용 시기:

  • 정확한 카운트가 사용자 경험에 중요한 경우(예: 이슈 총 수 표시)

  • 컬렉션 크기가 일반적으로 소규모에서 중간 규모인 경우

  • 정확한 카운트가 필요하지 않을 때 클라이언트가 limit 인수를 제공하여 성능 최적화를 선택할 수 있는 경우

LimitedCountableConnectionType 사용 시기:

  • 정확한 카운트가 사용자 경험에 필수적이지 않은 경우

  • 컬렉션이 매우 크고 모든 항목을 카운팅하는 데 비용이 많이 드는 경우

  • 기본적으로 성능 제한을 적용하고자 하는 경우

두 connection 타입은 제한이 적용될 때 동일한 제한된 카운팅 로직을 사용하며, CountableConnectionHelper 모듈을 통해 구현을 공유합니다.

리소스 컬렉션을 노출하려면 connection 타입을 사용할 수 있습니다. 이는 배열을 기본 페이지네이션 필드로 감쌉니다. 예를 들어 프로젝트 파이프라인에 대한 쿼리는 다음과 같을 수 있습니다:

query($project_path: ID!) {
  project(fullPath: $project_path) {
    pipelines(first: 2) {
      pageInfo {
        hasNextPage
        hasPreviousPage
      }
      edges {
        cursor
        node {
          id
          status
        }
      }
    }
  }
}

이 쿼리는 프로젝트의 첫 번째 파이프라인 2개와 관련 페이지네이션 정보를 ID 내림차순으로 반환합니다. 반환되는 데이터는 다음과 같습니다:

{
  "data": {
    "project": {
      "pipelines": {
        "pageInfo": {
          "hasNextPage": true,
          "hasPreviousPage": false
        },
        "edges": [
          {
            "cursor": "Nzc=",
            "node": {
              "id": "gid://gitlab/Pipeline/77",
              "status": "FAILED"
            }
          },
          {
            "cursor": "Njc=",
            "node": {
              "id": "gid://gitlab/Pipeline/67",
              "status": "FAILED"
            }
          }
        ]
      }
    }
  }
}

다음 페이지를 가져오려면 마지막으로 알려진 요소의 커서를 전달할 수 있습니다:

query($project_path: ID!) {
  project(fullPath: $project_path) {
    pipelines(first: 2, after: "Njc=") {
      pageInfo {
        hasNextPage
        hasPreviousPage
      }
      edges {
        cursor
        node {
          id
          status
        }
      }
    }
  }
}

일관된 정렬을 보장하기 위해 기본 키에 대한 정렬을 내림차순으로 추가합니다. 기본 키는 일반적으로 id이므로, 관계 끝에 order(id: :desc)를 추가합니다. 기본 키는 기반 테이블에서 반드시 사용 가능해야 합니다.

단축 필드(Shortcut fields)#

때로는 파라미터가 전달되지 않을 경우 컬렉션의 첫 번째 항목을 반환하는 "단축 필드"를 구현하는 것이 간단해 보일 수 있습니다. 이러한 "단축 필드"는 유지 관리 부담을 증가시키기 때문에 권장하지 않습니다. 단축 필드는 정식 필드와 동기화 상태를 유지해야 하며, 정식 필드가 변경되면 deprecated 처리하거나 수정해야 합니다. 특별히 다르게 해야 할 이유가 없는 한 프레임워크에서 제공하는 기능을 사용하세요.

예를 들어, latest_pipeline 대신 pipelines(last: 1)을 사용하세요.

페이지 크기 제한#

기본적으로 API는 연결(connection)에서 페이지당 최대 레코드 수를 반환하며, 이 값은 app/graphql/gitlab_schema.rb에 정의되어 있습니다. 또한 클라이언트가 제한 인수(first: 또는 last:)를 제공하지 않을 경우 페이지당 반환되는 기본 레코드 수이기도 합니다.

max_page_size 인수를 사용하면 연결에 대해 다른 페이지 크기 제한을 지정할 수 있습니다.

기본값은 GraphQL API의 성능을 보장하기 위해 설정된 것이므로, `max_page_size`를 높이는 것보다 프런트엔드 클라이언트나 제품 요구 사항을 페이지당 많은 레코드가 필요하지 않도록 변경하는 것이 더 좋습니다.

예를 들어:

field :tags,
  Types::ContainerRegistry::ContainerRepositoryTagType.connection_type,
  null: true,
  description: 'Tags of the container repository',
  max_page_size: 20

필드 복잡도(Field complexity)#

GitLab GraphQL API는 복잡도(complexity) 점수를 사용하여 과도하게 복잡한 쿼리의 실행을 제한합니다. 복잡도에 대한 설명은 해당 주제에 관한 클라이언트 문서에서 확인할 수 있습니다.

복잡도 제한은 app/graphql/gitlab_schema.rb에 정의되어 있습니다.

기본적으로 필드는 쿼리의 복잡도 점수에 1을 추가합니다. 필드에 대한 커스텀 complexity 값 제공을 통해 이를 재정의할 수 있습니다.

개발자는 서버가 데이터를 반환하기 위해 더 많은 작업을 수행하는 필드에 더 높은 복잡도를 지정해야 합니다. 대부분의 경우 idtitle처럼 거의 또는 전혀 작업 없이 반환할 수 있는 데이터를 나타내는 필드에는 복잡도 0을 부여할 수 있습니다.

calls_gitaly#

리졸빙 시 Gitaly 호출을 수행할 가능성이 있는 필드는 해당 필드를 정의할 때 fieldcalls_gitaly: true를 전달하여 반드시 표시해야 합니다.

예를 들어:

field :blob, type: Types::Snippets::BlobType,
      description: 'Snippet blob',
      null: false,
      calls_gitaly: true

이렇게 하면 해당 필드의 complexity 점수1 증가합니다.

리졸버가 Gitaly를 호출하는 경우, BaseResolver.calls_gitaly!로 어노테이션할 수 있습니다. 이렇게 하면 해당 리졸버를 사용하는 모든 필드에 calls_gitaly: true가 전달됩니다.

예를 들면 다음과 같습니다:

class BranchResolver < BaseResolver
  type ::Types::BranchType, null: true
  calls_gitaly!

  argument name: ::GraphQL::Types::String, required: true

  def resolve(name:)
    object.branch(name)
  end
end

이후 이를 사용할 때, BranchResolver를 사용하는 모든 필드에 calls_gitaly:에 대한 올바른 값이 설정됩니다.

타입에 대한 권한 노출#

현재 사용자가 리소스에 대해 가진 권한을 노출하려면, 해당 리소스에 대한 권한을 나타내는 별도의 타입을 expose_permissions에 전달하여 호출할 수 있습니다.

예를 들면 다음과 같습니다:

module Types
  class MergeRequestType < BaseObject
    expose_permissions Types::MergeRequestPermissionsType
  end
end

권한 타입은 BasePermissionType을 상속하며, 이 클래스에는 권한을 null이 아닌 불리언으로 노출할 수 있는 몇 가지 헬퍼 메서드가 포함되어 있습니다:

class MergeRequestPermissionsType < BasePermissionType
  graphql_name 'MergeRequestPermissions'

  present_using MergeRequestPresenter

  abilities :admin_merge_request, :update_merge_request, :create_note

  ability_field :resolve_note,
                description: 'Indicates the user can resolve discussions on the merge request.'
  permission_field :push_to_source_branch, method: :can_push_to_source_branch?
end
  • permission_field: graphql-rubyfield 메서드와 동일하게 동작하지만, 기본 설명과 타입을 설정하고 null이 아닌 값으로 만듭니다. 이러한 옵션은 인수로 추가하여 재정의할 수 있습니다.

  • ability_field: 정책에 정의된 ability를 노출합니다. permission_field와 동일한 방식으로 동작하며, 동일한 인수를 재정의할 수 있습니다.

  • abilities: 정책에 정의된 여러 ability를 한 번에 노출할 수 있습니다. 이 필드들은 모두 기본 설명이 있는 null이 아닌 불리언이어야 합니다.

피처 플래그#

GraphQL에서 피처 플래그를 구현하여 다음을 토글할 수 있습니다:

  • 필드의 반환 값.

  • 인수 또는 뮤테이션의 동작.

이는 리졸버, 타입, 또는 모델 메서드에서도 구현할 수 있으며, 선호도와 상황에 따라 선택하면 됩니다.

피처 플래그 뒤에 있는 동안에는 [해당 항목을 실험으로 표시](/19.1/development/api_graphql_styleguide/#mark-schema-items-as-experiments)하는 것도 권장합니다.

이는 공개 GraphQL API의 소비자에게 해당 필드가 아직 사용하기 위한 것이 아님을 알립니다. 또한 더 이상 사용 중단(deprecate) 처리 없이도 언제든지 실험적 항목을 변경하거나 제거할 수 있습니다. 플래그가 제거되면, 스키마 항목의 experiment 속성을 제거하여 항목을 "릴리즈"하고 공개 상태로 만드세요.

피처 플래그가 적용된 항목의 설명#

피처 플래그를 사용하여 스키마 항목의 값 또는 동작을 토글하는 경우, 해당 항목의 description은 반드시:

  • 값 또는 동작이 피처 플래그로 토글될 수 있음을 명시해야 합니다.

  • 피처 플래그 이름을 명시해야 합니다.

  • 피처 플래그가 비활성화(또는 활성화가 더 적절한 경우 활성화)되었을 때 필드가 반환하는 값 또는 동작이 무엇인지 명시해야 합니다.

피처 플래그 사용 예시#

피처 플래그가 적용된 필드#

필드 값은 피처 플래그 상태에 따라 토글됩니다. 일반적인 사용 예로는 피처 플래그가 비활성화된 경우 null을 반환하는 것이 있습니다:

field :foo, GraphQL::Types::String, null: true,
      experiment: { milestone: '10.0' },
      description: 'Some test field. Returns `null`' \
                   'if `my_feature_flag` feature flag is disabled.'

def foo
  object.foo if Feature.enabled?(:my_feature_flag, object)
end

피처 플래그가 적용된 argument#

argument는 피처 플래그 상태에 따라 무시되거나 값이 변경될 수 있습니다. 일반적인 사용 사례는 피처 플래그가 비활성화된 경우 argument를 무시하는 것입니다:

argument :foo, type: GraphQL::Types::String, required: false,
         experiment: { milestone: '10.0' },
         description: 'Some test argument. Is ignored if ' \
                      '`my_feature_flag` feature flag is disabled.'

def resolve(args)
  args.delete(:foo) unless Feature.enabled?(:my_feature_flag, object)
  # ...
end

피처 플래그가 적용된 mutation#

피처 플래그 상태로 인해 수행할 수 없는 mutation은 복구 불가능한 mutation 오류로 처리됩니다. 오류는 최상위 레벨에서 반환됩니다:

description 'Mutates an object. Does not mutate the object if ' \
            '`my_feature_flag` feature flag is disabled.'

def resolve(id: )
  object = authorized_find!(id: id)

  raise_resource_not_available_error! '`my_feature_flag` feature flag is disabled.' \
    if Feature.disabled?(:my_feature_flag, object)
  # ...
end

스키마 항목 deprecated 처리#

GitLab GraphQL API는 버전이 없으므로, 모든 변경 사항에서 API의 이전 버전과의 하위 호환성을 유지합니다.

필드, argument, enum 값, 또는 mutation을 제거하는 대신, 반드시 deprecated 처리해야 합니다.

deprecated 처리된 스키마 항목들은 이후 GitLab deprecation 프로세스에 따라 향후 릴리즈에서 제거될 수 있습니다.

GraphQL에서 스키마 항목을 deprecated 처리하려면:

다음도 참조하세요:

deprecation 이슈 생성#

모든 GraphQL deprecation에는 deprecation 및 제거를 추적하기 위해 Deprecations 이슈 템플릿을 사용하여 생성된 deprecation 이슈가 있어야 합니다.

deprecation 이슈에 다음 두 라벨을 적용하세요:

  • ~GraphQL

  • ~deprecation

항목을 deprecated로 표시#

필드, argument, enum 값, mutation은 deprecated 속성을 사용하여 deprecated 처리합니다. 속성의 값은 다음으로 구성된 Hash입니다:

  • reason - deprecated 처리 이유.

  • milestone - 필드가 deprecated 처리된 마일스톤.

예시:

field :token, GraphQL::Types::String, null: true,
      deprecated: { reason: 'Login via token has been removed', milestone: '10.0' },
      description: 'Token for login.'

deprecated 처리되는 항목의 원래 description은 유지되어야 하며, deprecated 처리를 언급하도록 업데이트해서는 안 됩니다. 대신, reasondescription에 추가됩니다.

deprecation reason 스타일 가이드#

필드, argument, 또는 enum 값이 교체됨으로 인한 deprecated 처리인 경우, reason은 대체 항목을 명시해야 합니다. 예를 들어, 다음은 교체된 필드에 대한 reason입니다:

Use `otherFieldName`

예시:

field :designs, ::Types::DesignManagement::DesignCollectionType, null: true,
      deprecated: { reason: 'Use `designCollection`', milestone: '10.0' },
      description: 'The designs associated with this issue.',
module Types
  class TodoStateEnum < BaseEnum
    value 'pending', deprecated: { reason: 'Use PENDING', milestone: '10.0' }
    value 'done', deprecated: { reason: 'Use DONE', milestone: '10.0' }
    value 'PENDING', value: 'pending'
    value 'DONE', value: 'done'
  end
end

더 이상 사용되지 않는 필드, 인수, 또는 열거형 값이 대체 항목 없이 폐기되는 경우, 설명이 포함된 폐기 reason을 제공해야 합니다.

Global ID 폐기#

Global ID를 생성하고 파싱하기 위해 rails/globalid gem을 사용하며, 이로 인해 Global ID는 모델 이름과 결합됩니다. 모델 이름을 변경하면 Global ID도 변경됩니다.

Global ID가 스키마 어딘가에서 인수 타입으로 사용되는 경우, Global ID 변경은 일반적으로 하위 호환성을 깨는 변경에 해당합니다.

이전 Global ID 인수를 사용하는 클라이언트를 계속 지원하기 위해, Gitlab::GlobalId::Deprecations에 폐기 항목을 추가합니다.

Global ID가 *오직* [필드로만 노출](/19.1/development/api_graphql_styleguide/#exposing-global-ids)되는 경우에는

폐기 처리가 필요하지 않습니다. 필드에서 Global ID가 표현되는 방식의 변경은 하위 호환성이 있다고 간주합니다. 클라이언트는 이러한 값을 파싱하지 않을 것으로 기대하며, 이 값들은 불투명한 토큰으로 취급되어야 합니다. 값에 내재된 구조는 우연한 것이며 의존해서는 안 됩니다.

예시 시나리오:

이 예시 시나리오는 다음 머지 리퀘스트를 기반으로 합니다.

PrometheusService라는 모델이 Integrations::Prometheus로 이름이 변경됩니다. 기존 모델 이름은 뮤테이션의 인수로 사용되는 Global ID 타입을 만드는 데 사용됩니다:

# Mutations::UpdatePrometheus:

argument :id, Types::GlobalIDType[::PrometheusService],
              required: true,
              description: "The ID of the integration to mutate."

클라이언트는 "gid://gitlab/PrometheusService/1" 형식의 Global ID 문자열을 PrometheusServiceID라는 이름으로, input.id 인수로 전달하여 뮤테이션을 호출합니다:

mutation updatePrometheus($id: PrometheusServiceID!, $active: Boolean!) {
  prometheusIntegrationUpdate(input: { id: $id, active: $active }) {
    errors
    integration {
      active
    }
  }
}

모델을 Integrations::Prometheus로 이름 변경하고 코드베이스 전체를 새 이름으로 업데이트합니다. 뮤테이션을 업데이트할 때 Types::GlobalIDType[]에 이름이 변경된 모델을 전달합니다:

# Mutations::UpdatePrometheus:

argument :id, Types::GlobalIDType[::Integrations::Prometheus],
              required: true,
              description: "The ID of the integration to mutate."

이로 인해 뮤테이션에 하위 호환성을 깨는 변경이 발생합니다. API가 이제 id 인수를 "gid://gitlab/PrometheusService/1"로 전달하거나, 쿼리 시그니처에서 인수 타입을 PrometheusServiceID로 지정하는 클라이언트를 거부하기 때문입니다.

클라이언트가 변경 없이 뮤테이션을 계속 사용할 수 있도록 하려면, Gitlab::GlobalId::DeprecationsDEPRECATIONS 상수를 수정하여 배열에 새 Deprecation을 추가합니다:

DEPRECATIONS = [
  Gitlab::Graphql::DeprecationsBase::NameDeprecation.new(old_name: 'PrometheusService', new_name: 'Integrations::Prometheus', milestone: '14.0')
].freeze

그런 다음 일반적인 폐기 프로세스를 따릅니다. 이전 인수 스타일에 대한 지원을 나중에 제거하려면 Deprecation을 삭제합니다:

DEPRECATIONS = [].freeze

폐기 기간 동안 API는 인수 값에 대해 다음 형식 중 하나를 허용합니다:

  • "gid://gitlab/PrometheusService/1"

  • "gid://gitlab/Integrations::Prometheus/1"

API는 또한 인수의 쿼리 시그니처에서 다음 타입을 허용합니다:

  • PrometheusServiceID

  • IntegrationsPrometheusID

    이전 타입(이 예시에서는 PrometheusServiceID)을 사용하는 쿼리는 API에서 유효하고 실행 가능한 것으로 간주되지만, 유효성 검사 도구는 이를 유효하지 않은 것으로 간주합니다. 이는 GraphQL 표준 메커니즘 외부의 맞춤형 방법을 사용하여 폐기를 처리하고 있기 때문에 유효하지 않은 것으로 간주됩니다.

@deprecated 지시어이므로 유효성 검사기는 지원 여부를 인식하지 못합니다.

이 문서에서는 기존 Global ID 스타일이 이제 더 이상 사용되지 않는다고 언급합니다.

스키마 항목을 실험 기능으로 표시#

GraphQL 스키마 항목(필드, 인수, enum 값, mutation)을 실험 기능으로 표시할 수 있습니다.

실험 기능으로 표시된 항목은 사용 중단 프로세스에서 면제되며, 예고 없이 언제든지 제거될 수 있습니다. 변경 가능성이 있고 공개 사용 준비가 되지 않은 항목은 실험 기능으로 표시하세요.

새 항목만 실험 기능으로 표시하세요. 기존 항목은 이미 공개되었으므로

실험 기능으로 표시하지 마세요.

스키마 항목을 실험 기능으로 표시하려면 experiment: 키워드를 사용하세요. 실험적 항목이 도입된 milestone:을 반드시 제공해야 합니다.

예시:

field :token, GraphQL::Types::String, null: true,
      experiment: { milestone: '10.0' },
      description: 'Token for login.'

마찬가지로, app/graphql/types/mutation_type.rb에서 mutation이 마운트되는 위치를 업데이트하여 전체 mutation을 실험 기능으로 표시할 수도 있습니다:

mount_mutation Mutations::Ci::JobArtifact::BulkDestroy, experiment: { milestone: '15.10' }

실험적 GraphQL 항목은 GraphQL 사용 중단을 활용하는 GitLab 커스텀 기능입니다. 실험적 항목은 GraphQL 스키마에서 사용 중단된 것으로 표시됩니다. 사용 중단된 모든 스키마 항목과 마찬가지로, 대화형 GraphQL 탐색기(GraphiQL)에서 실험적 필드를 테스트할 수 있습니다. 다만, GraphiQL 자동 완성 편집기는 사용 중단된 필드를 제안하지 않으므로 주의하세요.

생성된 GraphQL 문서 및 GraphQL 스키마 설명에서 해당 항목은 experiment로 표시됩니다.

Enum#

GitLab GraphQL enum은 app/graphql/types에 정의됩니다. 새 enum을 정의할 때는 다음 규칙이 적용됩니다:

  • 값은 대문자여야 합니다.

  • 클래스 이름은 반드시 Enum 문자열로 끝나야 합니다.

  • graphql_name에는 Enum 문자열이 포함되어서는 안 됩니다.

예시:

module Types
  class TrafficLightStateEnum < BaseEnum
    graphql_name 'TrafficLightState'
    description 'State of a traffic light'

    value 'RED', description: 'Drivers must stop.'
    value 'YELLOW', description: 'Drivers must stop when it is safe to.'
    value 'GREEN', description: 'Drivers can start or keep driving.'
  end
end

enum이 대문자 문자열이 아닌 Ruby 클래스 프로퍼티에 사용되는 경우, 대문자 값을 조정하는 value: 옵션을 제공할 수 있습니다.

다음 예시에서:

  • GraphQL 입력값 OPENED'opened'로 변환됩니다.

  • Ruby 값 'opened'는 GraphQL 응답에서 "OPENED"로 변환됩니다.

module Types
  class EpicStateEnum < BaseEnum
    graphql_name 'EpicState'
    description 'State of a GitLab epic'

    value 'OPENED', value: 'opened', description: 'An open Epic.'
    value 'CLOSED', value: 'closed', description: 'A closed Epic.'
  end
end

Enum 값은 deprecated 키워드를 사용하여 사용 중단으로 표시할 수 있습니다.

Rails enum에서 동적으로 GraphQL enum 정의하기#

GraphQL enum이 Rails enum을 기반으로 하는 경우, Rails enum을 사용하여 GraphQL enum 값을 동적으로 정의하는 것을 고려하세요. 이렇게 하면 GraphQL enum 값이 Rails enum 정의에 바인딩되므로, Rails enum에 값이 추가될 경우 GraphQL enum에 자동으로 반영됩니다.

예시:

module Types
  class IssuableSeverityEnum < BaseEnum
    graphql_name 'IssuableSeverity'
    description 'Incident severity'

    ::IssuableSeverity.severities.each_key do |severity|
      value severity.upcase, value: severity, description: "#{severity.titleize} severity."
    end
  end
end

JSON#

GraphQL이 반환할 데이터가 JSON으로 저장되어 있는 경우, 가능한 한 GraphQL 타입을 계속 사용해야 합니다. 반환되는 JSON 데이터가 진정으로 비구조적인 경우가 아니라면 GraphQL::Types::JSON 타입 사용을 피하세요.

JSON 데이터의 구조가 다양하지만 알려진 가능한 구조 집합 중 하나인 경우, union을 사용하세요. 이 목적으로 union을 사용하는 예시는 !30129에서 확인할 수 있습니다.

필요한 경우 hash_key: 키워드를 사용하여 필드 이름을 해시 데이터 키에 매핑할 수 있습니다.

예를 들어, 다음과 같은 JSON 데이터가 있을 때:

{
  "title": "My chart",
  "data": [
    { "x": 0, "y": 1 },
    { "x": 1, "y": 1 },
    { "x": 2, "y": 2 }
  ]
}

다음과 같이 GraphQL 타입을 사용할 수 있습니다:

module Types
  class ChartType < BaseObject
    field :title, GraphQL::Types::String, null: true, description: 'Title of the chart.'
    field :data, [Types::ChartDatumType], null: true, description: 'Data of the chart.'
  end
end

module Types
  class ChartDatumType < BaseObject
    field :x, GraphQL::Types::Int, null: true, description: 'X-axis value of the chart datum.'
    field :y, GraphQL::Types::Int, null: true, description: 'Y-axis value of the chart datum.'
  end
end

Descriptions#

모든 필드와 인수에는 반드시 설명이 있어야 합니다.

필드 또는 인수에 대한 설명은 description: 키워드를 사용하여 제공합니다. 예를 들어:

field :id, GraphQL::Types::ID, description: 'ID of the issue.'
field :confidential, GraphQL::Types::Boolean, description: 'Indicates the issue is confidential.'
field :closed_at, Types::TimeType, description: 'Timestamp of when the issue was closed.'

필드 및 인수의 설명은 다음에서 확인할 수 있습니다:

Description 스타일 가이드#

언어 및 구두점#

필드와 인수를 설명할 때는 가능하면 {x} of the {y} 형식을 사용하세요. 여기서 {x}는 설명하려는 항목이고 {y}는 해당 항목이 적용되는 리소스입니다. 예를 들어:

ID of the issue.
Author of the epics.

정렬하거나 검색하는 인수에는 적절한 동사로 시작하세요. 지정된 값을 나타낼 때는 간결함을 위해 the given 또는 the specified 대신 this를 사용할 수 있습니다. 예를 들어:

Sort issues by this criteria.

일관성과 간결함을 위해 설명을 The 또는 A로 시작하지 마세요.

모든 설명은 마침표(.)로 끝내야 합니다.

Boolean#

boolean 필드(GraphQL::Types::Boolean)에는 해당 필드가 무엇을 하는지 설명하는 동사로 시작하세요. 예를 들어:

Indicates the issue is confidential.

필요한 경우 기본값을 제공하세요. 예를 들어:

Sets the issue to confidential. Default is false.

Sort enum#

정렬용 Enum의 설명은 'Values for sorting {x}.' 형식으로 작성해야 합니다. 예를 들어:

Values for sorting container repositories.

Types::TimeType 필드 설명#

Types::TimeType GraphQL 필드에는 timestamp라는 단어를 포함하세요. 이렇게 하면 독자가 해당 속성의 형식이 단순히 Date가 아닌 Time임을 알 수 있습니다.

예시:

field :closed_at, Types::TimeType, description: 'Timestamp of when the issue was closed.'

copy_field_description 헬퍼#

두 설명이 항상 동일하게 유지되도록 하려는 경우가 있습니다. 예를 들어, 타입 필드 설명과 뮤테이션 인수가 동일한 속성을 나타낼 때 두 설명을 동일하게 유지하려는 경우입니다.

설명을 직접 제공하는 대신 copy_field_description 헬퍼를 사용할 수 있습니다. 타입과 복사할 필드 이름을 인수로 전달합니다.

예시:

argument :title, GraphQL::Types::String,
          required: false,
          description: copy_field_description(Types::MergeRequestType, :title)

문서 참조#

설명에 외부 URL을 참조하고 싶을 때가 있습니다. 이를 쉽게 처리하고 생성된 레퍼런스 문서에 적절한 마크업을 제공하기 위해 필드에 see 속성을 제공합니다. 예시:

field :genus,
      type: GraphQL::Types::String,
      null: true,
      description: 'A taxonomic genus.'
      see: { 'Wikipedia page on genera' => 'https://wikipedia.org/wiki/Genus' }

이 내용은 문서에서 다음과 같이 렌더링됩니다:

A taxonomic genus. See: [Wikipedia page on genera](https://wikipedia.org/wiki/Genus)

여러 문서 참조를 제공할 수 있습니다. 이 속성의 문법은 키가 텍스트 설명이고 값이 URL인 HashMap입니다.

구독 티어 배지#

필드나 인수가 다른 필드보다 더 높은 구독 티어에서만 사용 가능한 경우, 인라인 가용성 세부 정보를 추가하세요.

예시:

description: 'Full path of a custom template. Premium and Ultimate only.'

인가(Authorization)#

참조: GraphQL 인가

리졸버(Resolvers)#

애플리케이션이 응답을 제공하는 방법은 app/graphql/resolvers 디렉터리에 저장된 리졸버를 사용하여 정의합니다. 리졸버는 해당 객체를 조회하는 실제 구현 로직을 제공합니다.

필드에 표시할 객체를 찾으려면 app/graphql/resolvers에 리졸버를 추가할 수 있습니다.

인수는 뮤테이션에서와 동일한 방식으로 리졸버에서 정의할 수 있습니다. 인수(Arguments) 섹션을 참조하세요.

수행되는 쿼리 횟수를 제한하려면 BatchLoader를 사용할 수 있습니다.

리졸버 작성하기#

코드는 finder와 서비스를 감싸는 얇은 선언형 래퍼를 목표로 해야 합니다. 인수 목록을 반복하거나 concern으로 추출할 수 있습니다. 대부분의 경우 상속보다 합성(Composition)이 선호됩니다. 리졸버를 컨트롤러처럼 다루세요: 리졸버는 다른 애플리케이션 추상화를 합성하는 DSL이어야 합니다.

예시:

class PostResolver < BaseResolver
  type Post.connection_type, null: true
  authorize :read_blog
  description 'Blog posts, optionally filtered by name'

  argument :name, [::GraphQL::Types::String], required: false, as: :slug

  alias_method :blog, :object

  def resolve(**args)
    PostFinder.new(blog, current_user, args).execute
  end
end

동일한 객체가 노출되는 두 개의 다른 필드와 같이 두 곳에서 동일한 리졸버 클래스를 사용할 수 있지만, 리졸버 객체를 직접 재사용해서는 안 됩니다. 리졸버는 복잡한 라이프사이클을 가지며,

인가(권한 부여), 준비 상태 확인, 해석 오케스트레이션은 프레임워크가 담당하며, 각 단계에서 배치 기회를 활용하기 위해 지연 값(lazy values)을 반환할 수 있습니다. 애플리케이션 코드에서 리졸버나 뮤테이션을 직접 인스턴스화하지 마십시오.

대신, 코드 재사용 단위는 나머지 애플리케이션에서와 동일합니다:

  • 데이터를 조회하는 쿼리의 Finder.

  • 작업을 적용하는 뮤테이션의 Service.

  • 쿼리 전용 Loader(배치 인식 Finder).

뮤테이션에서 배치를 사용해야 할 이유는 없습니다. 뮤테이션은 순차적으로 실행되므로 배치 기회가 없습니다. 모든 값은 요청되는 즉시 즉시 평가되므로 배치는 불필요한 오버헤드입니다. 다음을 작성하는 경우:

  • Mutation이라면 객체를 직접 조회해도 됩니다.

  • Resolver 또는 BaseObject의 메서드라면 배치를 허용해야 합니다.

오류 처리#

리졸버는 오류를 발생시킬 수 있으며, 이는 적절한 경우 최상위 오류로 변환됩니다. 예상되는 모든 오류는 포착하여 적절한 GraphQL 오류로 변환해야 합니다( Gitlab::Graphql::Errors 참조). 포착되지 않은 오류는 억제되며 클라이언트는 Internal service error 메시지를 수신합니다.

한 가지 특수 사례는 권한 오류입니다. REST API에서는 사용자에게 접근 권한이 없는 리소스에 대해 404 Not Found를 반환합니다. GraphQL에서 이에 해당하는 동작은 존재하지 않거나 인가되지 않은 모든 리소스에 대해 null을 반환하는 것입니다. 쿼리 리졸버는 인가되지 않은 리소스에 대해 오류를 발생시켜서는 안 됩니다.

이렇게 하는 이유는 클라이언트가 레코드의 부재와 접근 권한이 없는 레코드의 존재를 구별할 수 없어야 하기 때문입니다. 그렇지 않으면 숨기고자 하는 정보가 유출되는 보안 취약점이 됩니다.

대부분의 경우 이에 대해 걱정할 필요가 없습니다. 이는 authorize DSL 호출로 선언하는 리졸버 필드 인가에 의해 올바르게 처리됩니다. 그러나 더 커스텀한 작업이 필요한 경우, 필드를 해석할 때 current_user가 접근 권한이 없는 객체를 만나면 전체 필드가 null로 해석되어야 한다는 점을 기억하십시오.

리졸버 파생#

(BaseResolver.singleBaseResolver.last 포함)

일부 사용 사례에서는 다른 리졸버에서 리졸버를 파생할 수 있습니다. 주요 사용 사례는 모든 항목을 찾는 리졸버와 특정 항목 하나를 찾는 리졸버입니다. 이를 위해 편의 메서드를 제공합니다:

  • BaseResolver.single: 첫 번째 항목을 선택하는 새 리졸버를 생성합니다.

  • BaseResolver.last: 마지막 항목을 선택하는 리졸버를 생성합니다.

올바른 단수 타입은 컬렉션 타입에서 유추되므로 여기서 type을 별도로 정의할 필요가 없습니다.

이 메서드를 사용하기 전에, 다음 중 하나가 더 간단한지 고려하십시오:

  • 자체 인수를 정의하는 다른 리졸버 작성.

  • 쿼리를 추상화하는 concern 작성.

BaseResolver.single을 너무 자유롭게 사용하는 것은 안티패턴입니다. 이는 인수가 없을 때 단순히 첫 번째 MR을 반환하는 Project.mergeRequest 필드와 같이 의미 없는 필드로 이어질 수 있습니다. 컬렉션 리졸버에서 단일 리졸버를 파생할 때는 반드시 더 제한적인 인수를 가져야 합니다.

이를 가능하게 하려면 when_single 블록을 사용하여 단일 리졸버를 커스터마이즈하십시오. 모든 when_single 블록은 반드시:

  • 하나 이상의 인수를 정의(또는 재정의)해야 합니다.

  • 선택적 필터를 필수로 만들어야 합니다.

예를 들어, 기존 선택적 인수를 재정의하여 타입을 변경하고 필수로 만들 수 있습니다:

class JobsResolver < BaseResolver
  type JobType.connection_type, null: true
  authorize :read_pipeline

  argument :name, [::GraphQL::Types::String], required: false

  when_single do
    argument :name, ::GraphQL::Types::String, required: true
  end

  def resolve(**args)
    JobsFinder.new(pipeline, current_user, args.compact).execute
  end

여기서는 파이프라인 job을 가져오는 리졸버가 있습니다. name 인수는 목록을 가져올 때는 선택적이지만 단일 job을 가져올 때는 필수입니다.

여러 인수가 있고 어느 것도 필수로 만들 수 없는 경우, 블록을 사용하여 준비 조건을 추가할 수 있습니다:

class JobsResolver < BaseResolver
  alias_method :pipeline, :object

  type JobType.connection_type, null: true
  authorize :read_pipeline

  argument :name, [::GraphQL::Types::String], required: false
  argument :id, [::Types::GlobalIDType[::Job]],
           required: false,
           prepare: ->(ids, ctx) { ids.map(&:model_id) }

  when_single do
    argument :name, ::GraphQL::Types::String, required: false
    argument :id, ::Types::GlobalIDType[::Job],
             required: false
             prepare: ->(id, ctx) { id.model_id }

    def ready?(**args)
      raise ::Gitlab::Graphql::Errors::ArgumentError, 'Only one argument may be provided' unless args.size == 1
    end
  end

  def resolve(**args)
    JobsFinder.new(pipeline, current_user, args.compact).execute
  end

그런 다음 이 리졸버를 필드에서 사용할 수 있습니다.

# In PipelineType

field :jobs, resolver: JobsResolver, description: 'All jobs.'
field :job, resolver: JobsResolver.single, description: 'A single job.'

리졸버 최적화#

룩어헤드(Look-Ahead)#

실행 중에 전체 쿼리를 미리 알 수 있으므로, lookahead를 활용하여 쿼리를 최적화하고 필요한 연관 관계를 일괄 로드할 수 있습니다. N+1 성능 문제를 방지하기 위해 리졸버에 룩어헤드 지원을 추가하는 것을 고려하세요.

일반적인 룩어헤드 사용 사례(자식 필드가 요청될 때 연관 관계를 사전 로드)에 대한 지원을 활성화하려면, LooksAhead를 include할 수 있습니다. 예를 들어:

# Assuming a model `MyThing` with attributes `[child_attribute, other_attribute, nested]`,
# where nested has an attribute named `included_attribute`.
class MyThingResolver < BaseResolver
  include LooksAhead

  # Rather than defining `resolve(**args)`, we implement: `resolve_with_lookahead(**args)`
  def resolve_with_lookahead(**args)
    apply_lookahead(MyThingFinder.new(current_user).execute)
  end

  # We list things that should always be preloaded:
  # For example, if child_attribute is always needed (during authorization
  # perhaps), then we can include it here.
  def unconditional_includes
    [:child_attribute]
  end

  # We list things that should be included if a certain field is selected:
  def preloads
    {
        field_one: [:other_attribute],
        field_two: [{ nested: [:included_attribute] }]
    }
  end
end

기본적으로 #preloads에 정의된 필드는 쿼리에서 해당 필드가 선택될 때 사전 로드됩니다. 경우에 따라 너무 많거나 잘못된 콘텐츠를 사전 로드하지 않도록 더 세밀한 제어가 필요할 수 있습니다.

위의 예시를 확장하면, 특정 필드들이 함께 요청될 때 다른 연관 관계를 사전 로드하고 싶을 수 있습니다. 이는 #filtered_preloads를 오버라이드하여 수행할 수 있습니다.

class MyThingResolver < BaseResolver
  # ...

  def filtered_preloads
    return [:alternate_attribute] if lookahead.selects?(:field_one) && lookahead.selects?(:field_two)

    super
  end
end

LooksAhead concern은 중첩된 GraphQL 필드 정의를 기반으로 연관 관계를 사전 로드하는 기능도 제공합니다. 중첩 필드가 선택될 때 지정한 연관 관계를 사전 로드하려면 해시 키로 필드 이름 배열을 사용하세요. 예를 들어:

class MyThingResolver < BaseResolver
  # ...

  def preloads
    {
      [:root_field, :nested_field1] => :association_to_preload,
      [:root_field, :nested_field2] => [:association1, :association2],
      [:root_field, :nested_field2, :nested_field3] => :association3,
      other_root_field: :other_association,
    }
  end
end

실제 사용 예시는 WorkItems::LookAheadPreloads를 참고하세요.

before_connection_authorization#

before_connection_authorization 훅은 타입 인가(type authorization) 권한 검사에서 발생하는 N+1 문제를 리졸버가 해소하는 데 도움을 줄 수 있습니다.

before_connection_authorization 메서드는 해석된 노드와 현재 사용자를 인수로 받습니다. 블록 내에서 ActiveRecord::Associations::Preloader 또는 Preloaders:: 클래스를 사용하여 타입 인가 검사를 위한 데이터를 사전 로드하세요.

예시:

class LabelsResolver < BaseResolver
  before_connection_authorization do |labels, current_user|
    Preloaders::LabelsPreloader.new(labels, current_user).preload_all
  end
end

배치 로딩(BatchLoading)#

GraphQL BatchLoader를 참고하세요.

Resolver#ready?의 올바른 사용#

Resolver에는 프레임워크의 일부로서 두 가지 공개 API 메서드가 있습니다: #ready?(**args)#resolve(**args). #ready?를 사용하면 #resolve를 호출하지 않고도 초기 설정 또는 조기 반환을 수행할 수 있습니다.

#ready?를 사용하는 타당한 이유는 다음과 같습니다:

  • 사전에 결과가 없을 것을 알고 있는 경우 Relation.none을 반환합니다.

  • 인스턴스 변수 초기화와 같은 설정 작업을 수행합니다(단, 이 경우에는 지연 초기화 메서드를 고려하세요).

Resolver#ready?(**args) 구현 시 다음과 같이 (Boolean, early_return_data)를 반환해야 합니다:

def ready?(**args)
  [false, 'have this instead']
end

이러한 이유로, resolver를 직접 호출할 때(주로 테스트에서. 프레임워크 추상화 Resolver는 재사용 가능하다고 간주해서는 안 되며, finder를 사용하는 것이 권장됩니다)는 resolve를 호출하기 전에 반드시 ready? 메서드를 호출하고 불리언 플래그를 확인해야 합니다. 예시는 GraphqlHelpers에서 확인할 수 있습니다.

인수 유효성 검사에는 #ready? 대신 validator 사용을 권장합니다.

부정 인수#

부정 필터를 사용하면 일부 리소스를 필터링할 수 있습니다(예: bug 라벨이 있지만 bug2 라벨은 없는 이슈를 모두 찾기). 부정 인수를 전달하는 데 권장되는 문법은 not 인수입니다:

issues(labelName: "bug", not: {labelName: "bug2"}) {
  nodes {
    id
    title
  }
}

타입 또는 resolver에서 Gitlab::Graphql::NegatableArgumentsnegated 헬퍼를 사용할 수 있습니다. 예시:

extend ::Gitlab::Graphql::NegatableArguments

negated do
  argument :labels, [GraphQL::STRING_TYPE],
            required: false,
            as: :label_name,
            description: 'Array of label names. All resolved merge requests will not have these labels.'
end

메타데이터#

resolver를 사용할 때, resolver는 필드 메타데이터의 단일 진실 공급원(Single Source Of Truth, SSOT) 역할을 해야 합니다. 모든 필드 옵션(필드 이름 제외)을 resolver에 선언할 수 있습니다. 여기에는 다음이 포함됩니다:

  • type (필수 - 모든 resolver에는 타입 어노테이션이 포함되어야 합니다)

  • extras

  • description

  • Gitaly 어노테이션 (calls_gitaly! 사용)

예시:

module Resolvers
  MyResolver < BaseResolver
    type Types::MyType, null: true
    extras [:lookahead]
    description 'Retrieve a single MyType'
    calls_gitaly!
  end
end

상위 객체를 자식 Presenter로 전달#

때로는 필드를 계산하기 위해 자식 컨텍스트에서 쿼리의 상위 객체에 접근해야 합니다. 일반적으로 상위 객체는 Resolver 클래스에서만 parent로 접근 가능합니다.

Presenter 클래스에서 상위 객체를 찾으려면:

resolver의 resolve 메서드에서 GraphQL context에 상위 객체를 추가합니다:

  def resolve(**args)
    context[:parent_object] = parent
  end

resolver 또는 필드가 parent 필드 컨텍스트를 필요로 한다고 선언합니다. 예시:

  # in ChildType
  field :computed_field, SomeType, null: true,
        method: :my_computing_method,
        extras: [:parent], # Necessary
        description: 'My field description.'

  field :resolver_field, resolver: SomeTypeResolver

  # In SomeTypeResolver

  extras [:parent]
  type SomeType, null: true
  description 'My field description.'

Presenter 클래스에서 필드의 메서드를 선언하고 parent 키워드 인수를 받도록 합니다. 이 인수는 상위 GraphQL context를 포함하므로, Resolver에서 사용한 키를 이용하여 parent[:parent_object]와 같이 상위 객체에 접근해야 합니다:

  # in ChildPresenter
  def my_computing_method(parent:)
    # do something with `parent[:parent_object]` here
  end

  # In SomeTypeResolver

  def resolve(parent:)
    # ...
  end

실제 사용 예시는 IterationPresenterscopedPathscopedUrl을 추가한 MR을 참고하세요.

Mutations#

Mutation은 저장된 값을 변경하거나 액션을 트리거하는 데 사용됩니다. GET 요청이 데이터를 수정해서는 안 되는 것처럼, 일반 GraphQL 쿼리에서는 데이터를 수정할 수 없습니다. 그러나 mutation에서는 가능합니다.

Mutation 구성#

Mutation은 app/graphql/mutations에 저장되며, 서비스와 유사하게 변경 대상 리소스별로 그룹화하는 것이 이상적입니다. Mutation은 Mutations::BaseMutation을 상속해야 합니다. mutation에 정의된 필드는 mutation의 결과로 반환됩니다.

Update mutation 세분화#

GitLab의 서비스 지향 아키텍처에서는 대부분의 mutation이 UpdateMergeRequestService와 같이 Create, Delete, 또는 Update 서비스를 호출합니다. Update mutation의 경우, 객체의 한 측면만 업데이트하고 싶을 때가 있으므로 MergeRequest::SetDraft와 같은 세밀한(fine-grained) mutation만 필요할 수 있습니다.

세밀한 mutation과 거친(coarse-grained) mutation을 함께 사용하는 것은 허용되지만, 지나치게 많은 세밀한 mutation은 유지보수성, 코드 이해도, 테스트 측면에서 관리상의 어려움을 초래할 수 있음을 유의하세요. 각 mutation은 새로운 클래스를 필요로 하므로 기술 부채로 이어질 수 있습니다. 또한 스키마가 매우 커져 사용자가 스키마를 탐색하기 어려워질 수 있습니다. 새로운 mutation마다 테스트(느린 요청 통합 테스트 포함)가 필요하기 때문에, mutation을 추가하면 테스트 스위트가 느려집니다.

변경을 최소화하려면:

  • 가능한 경우 MergeRequest::Update와 같은 기존 mutation을 사용합니다.

  • 기존 서비스를 거친(coarse-grained) mutation으로 노출합니다.

세밀한 mutation이 더 적합할 수 있는 경우:

  • 특정 권한이나 기타 전문화된 로직이 필요한 속성을 수정하는 경우.

  • 상태 머신과 유사한 전환(이슈 잠금, MR 머지, 에픽 닫기 등)을 노출하는 경우.

  • 중첩된 속성을 허용하는 경우(하위 객체의 속성을 받는 경우).

  • mutation의 의미가 명확하고 간결하게 표현될 수 있는 경우.

자세한 배경은 이슈 #233063을 참고하세요.

명명 규칙#

각 mutation은 GraphQL 스키마에서 mutation의 이름인 graphql_name을 정의해야 합니다.

예시:

class UserUpdateMutation < BaseMutation
  graphql_name 'UserUpdate'
end

graphql-ruby gem의 1.13 버전 변경으로 인해, 타입 이름이 올바르게 생성되도록 graphql_name은 클래스의 첫 번째 줄에 위치해야 합니다. Graphql::GraphqlNamePosition cop이 이를 강제합니다. 자세한 배경은 이슈 #27536을 참고하세요.

GitLab의 GraphQL mutation 이름은 역사적으로 일관성이 부족했지만, 새로운 mutation 이름은 '{Resource}{Action}' 또는 '{Resource}{Action}{Attribute}' 규칙을 따라야 합니다.

새 리소스를 생성하는 mutation은 동사 Create를 사용해야 합니다.

예시:

  • CommitCreate

데이터를 업데이트하는 mutation은 다음을 사용합니다:

  • 동사 Update.

  • 더 적합한 경우 Set, Add, Toggle과 같은 도메인별 동사.

예시:

  • EpicTreeReorder

  • IssueSetWeight

  • IssueUpdate

  • TodoMarkDone

데이터를 삭제하는 mutation은 다음을 사용합니다:

  • Destroy 대신 동사 Delete.

  • 더 적합한 경우 Remove와 같은 도메인별 동사.

예시:

  • AwardEmojiRemove

  • NoteDelete

뮤테이션 네이밍에 대한 조언이 필요하다면 Slack #graphql 채널에서 피드백을 구하세요.

필드#

가장 일반적인 상황에서 뮤테이션은 2개의 필드를 반환합니다:

  • 수정 중인 리소스

  • 작업을 수행할 수 없었던 이유를 설명하는 오류 목록. 뮤테이션이 성공하면 이 목록은 비어 있습니다.

새 뮤테이션을 Mutations::BaseMutation에서 상속하면 errors 필드가 자동으로 추가됩니다. clientMutationId 필드도 추가되며, 클라이언트는 이를 사용하여 단일 요청에서 여러 뮤테이션이 수행될 때 단일 뮤테이션의 결과를 식별할 수 있습니다.

resolve 메서드#

리졸버 작성과 유사하게, 뮤테이션의 resolve 메서드는 서비스를 감싸는 얇고 선언적인 래퍼를 목표로 해야 합니다.

resolve 메서드는 뮤테이션의 인수를 키워드 인수로 받습니다. 여기서 리소스를 수정하는 서비스를 호출할 수 있습니다.

resolve 메서드는 errors 배열을 포함하여 뮤테이션에 정의된 필드 이름과 동일한 해시를 반환해야 합니다. 예를 들어, Mutations::MergeRequests::SetDraftmerge_request 필드를 정의합니다:

field :merge_request,
      Types::MergeRequestType,
      null: true,
      description: "The merge request after mutation."

즉, 이 뮤테이션의 resolve에서 반환되는 해시는 다음과 같아야 합니다:

{
  # The merge request modified, this will be wrapped in the type
  # defined on the field
  merge_request: merge_request,
  # An array of strings if the mutation failed after authorization.
  # The `errors_on_object` helper collects `errors.full_messages`
  errors: errors_on_object(merge_request)
}

뮤테이션 마운트#

뮤테이션을 사용 가능하게 하려면 graphql/types/mutation_type에 저장된 뮤테이션 타입에 정의되어야 합니다. mount_mutation 헬퍼 메서드는 뮤테이션의 GraphQL 이름을 기반으로 필드를 정의합니다:

module Types
  class MutationType < BaseObject
    graphql_name 'Mutation'

    include Gitlab::Graphql::MountMutation

    mount_mutation Mutations::MergeRequests::SetDraft
  end
end

이는 Mutations::MergeRequests::SetDraft를 resolve하는 mergeRequestSetDraft라는 필드를 생성합니다.

리소스 인가#

뮤테이션 내에서 리소스를 인가하려면 먼저 뮤테이션에 필요한 권한을 다음과 같이 제공합니다:

module Mutations
  module MergeRequests
    class SetDraft < Base
      graphql_name 'MergeRequestSetDraft'

      authorize :update_merge_request
    end
  end
end

그런 다음 resolve 메서드에서 authorize!를 호출하여 권한을 검증할 리소스를 전달할 수 있습니다.

또는 뮤테이션에서 오브젝트를 로드하는 find_object 메서드를 추가할 수 있습니다. 이렇게 하면 authorized_find! 헬퍼 메서드를 사용할 수 있습니다.

사용자가 해당 작업을 수행할 권한이 없거나, 인가로 인해 리소스를 찾을 수 없는 경우 (사용자가 리소스에 접근할 수 없음), resolve 메서드에서 raise_resource_not_available_error!를 호출하여 Gitlab::Graphql::Errors::ResourceNotAvailable을 발생시켜야 합니다. 사용자 입력 유효성 검사 오류(예: 잘못된 프로젝트 경로 또는 형식이 잘못된 식별자)의 경우, 클라이언트가 사용자에게 의미 있는 메시지를 표시할 수 있도록 뮤테이션 페이로드의 errors 배열에 오류를 반환하세요. 자세한 내용은 뮤테이션의 오류를 참조하세요.

뮤테이션의 오류#

뮤테이션에는 데이터로서의 오류 관행을 따르는 것을 권장합니다. 이는 오류를 처리할 수 있는 대상에 따라 오류를 구분합니다.

주요 사항:

  • 모든 뮤테이션 응답에는 errors 필드가 있습니다. 이 필드는 실패 시 반드시 채워져야 하며, 성공 시에도 채워질 수 있습니다.

  • 에러를 확인해야 하는 대상이 사용자인지 개발자인지 고려하세요.

  • 클라이언트는 뮤테이션을 수행할 때 항상 errors 필드를 요청해야 합니다.

  • 에러는 $root.errors(최상위 에러) 또는 $root.data.mutationName.errors(뮤테이션 에러)에서 사용자에게 보고될 수 있습니다. 위치는 에러의 종류와 에러가 담고 있는 정보에 따라 결정됩니다.

  • 뮤테이션 필드는 반드시 null: true여야 합니다.

doTheThing이라는 예시 뮤테이션이 두 개의 필드 errors: [String]thing: ThingType을 포함한 응답을 반환한다고 가정해 보세요. thing 자체의 구체적인 내용은 이 예시에서 중요하지 않으며, 에러를 중심으로 살펴봅니다.

뮤테이션 응답이 가질 수 있는 세 가지 상태는 다음과 같습니다:

Success#

정상적인 경우, 예상 페이로드와 함께 에러가 반환될 수 있지만, 모든 것이 성공적이라면 errors는 빈 배열이어야 합니다. 사용자에게 알려야 할 문제가 없기 때문입니다.

{
  data: {
    doTheThing: {
      errors: [] // if successful, this array will generally be empty.
      thing: { .. }
    }
  }
}

Failure (relevant to the user)#

사용자에게 영향을 미치는 에러가 발생한 경우입니다. 이를 뮤테이션 에러라고 합니다.

create 뮤테이션에서는 일반적으로 반환할 thing이 없습니다.

update 뮤테이션에서는 thing의 현재 실제 상태를 반환합니다. 개발자는 이를 보장하기 위해 thing 인스턴스에서 #reset을 호출해야 할 수 있습니다.

{
  data: {
    doTheThing: {
      errors: ["you cannot touch the thing"],
      thing: { .. }
    }
  }
}

이에 해당하는 예시는 다음과 같습니다:

  • 모델 유효성 검사 오류: 사용자가 입력값을 변경해야 할 수 있습니다.

  • 권한 에러: 사용자는 이 작업을 수행할 수 없다는 것을 알아야 하며, 권한을 요청하거나 로그인해야 할 수 있습니다.

  • 사용자의 작업을 방해하는 애플리케이션 상태 문제(예: 머지 충돌 또는 잠긴 리소스).

이상적으로는 사용자가 이 단계까지 오지 않도록 방지해야 하지만, 만약 그렇게 된다면 사용자에게 무엇이 잘못되었는지 알려주어야 합니다. 그래야 사용자가 실패 원인을 이해하고 의도한 작업을 완수하기 위해 무엇을 해야 하는지 알 수 있습니다. 예를 들어, 단순히 요청을 재시도하기만 하면 될 수도 있습니다.

복구 가능한 에러를 뮤테이션 데이터와 함께 반환하는 것도 가능합니다. 예를 들어, 사용자가 10개의 파일을 업로드했는데 그 중 3개가 실패하고 나머지가 성공했다면, 실패한 파일에 대한 에러를 성공한 파일에 대한 정보와 함께 사용자에게 제공할 수 있습니다.

Failure (irrelevant to the user)#

하나 이상의 복구 불가능한 에러가 최상위 레벨에서 반환될 수 있습니다. 이러한 에러는 사용자가 제어할 수 없거나 거의 통제할 수 없는 것으로, 주로 개발자가 알아야 하는 시스템 또는 프로그래밍 문제여야 합니다. 이 경우 data가 없습니다:

{
  errors: [
    {"message": "argument error: expected an integer, got null"},
  ]
}

이는 뮤테이션 실행 중 에러가 발생했을 때 나타납니다. 현재 구현에서는 인수 에러와 유효성 검사 에러의 메시지가 클라이언트에 반환되며, 그 외 모든 StandardError 인스턴스는 캐치되어 로깅되고 메시지가 "Internal server error"로 설정되어 클라이언트에 표시됩니다. 자세한 내용은 GraphqlController를 참조하세요.

이러한 에러는 다음과 같은 프로그래밍 에러를 나타냅니다:

  • GraphQL 구문 오류: String 대신 Int가 전달되거나 필수 인수가 없는 경우.

  • 스키마 에러: non-nullable 필드에 값을 제공할 수 없는 경우 등.

  • 시스템 에러: 예를 들어, Git 저장소 예외 또는 데이터베이스 사용 불가.

사용자는 정상적인 사용 중에 이러한 에러를 발생시킬 수 없어야 합니다. 이 범주의 에러는 내부 에러로 취급되며, 사용자에게 구체적인 내용을 표시해서는 안 됩니다.

뮤테이션이 실패했을 때 사용자에게 알려야 하지만, 그 이유를 알릴 필요는 없습니다. 사용자가 이를 유발할 수 없었고 사용자가 할 수 있는 조치도 없기 때문입니다. 다만 뮤테이션 재시도를 제안할 수 있습니다.

오류 분류#

뮤테이션을 작성할 때, 오류 상태가 이 두 가지 범주 중 어디에 해당하는지 인식하고 (프론트엔드 개발자와 소통하여 가정을 검증해야 합니다) 있어야 합니다. 이는 사용자의 요구와 클라이언트의 요구를 구별하는 것을 의미합니다.

사용자에게 알릴 필요가 없는 오류는 절대 캐치하지 마세요.

사용자에게 알릴 필요가 있다면, 프론트엔드 개발자와 소통하여 전달하는 오류 정보가 관련성 있고 목적에 부합하는지 확인하세요.

프론트엔드 GraphQL 가이드도 참고하세요.

뮤테이션 별칭 지정 및 Deprecated 처리#

#mount_aliased_mutation 헬퍼를 사용하면 MutationType에서 뮤테이션에 다른 이름의 별칭을 지정할 수 있습니다.

예를 들어, FooMutation이라는 뮤테이션에 BarMutation이라는 별칭을 지정하려면:

mount_aliased_mutation 'BarMutation', Mutations::FooMutation

이를 통해 뮤테이션의 이름을 변경하면서 기존 이름도 계속 지원할 수 있으며, deprecated 인수와 함께 사용하면 됩니다.

예시:

mount_aliased_mutation 'UpdateFoo',
                        Mutations::Foo::Update,
                        deprecated: { reason: 'Use fooUpdate', milestone: '13.2' }

Deprecated된 뮤테이션은 Types::DeprecatedMutations에 추가하고 Types::MutationType의 단위 테스트에서 테스트해야 합니다. 머지 리퀘스트 !34798을 이에 대한 예시로 참고할 수 있으며, deprecated된 별칭 뮤테이션의 테스트 방법도 포함되어 있습니다.

EE 뮤테이션 Deprecated 처리#

EE 뮤테이션도 동일한 절차를 따라야 합니다. 머지 리퀘스트 절차에 대한 예시는 머지 리퀘스트 !42588을 참고하세요.

구독(Subscriptions)#

구독을 사용하여 클라이언트에 업데이트를 푸시합니다. Action Cable 구현을 사용하여 웹소켓을 통해 메시지를 전달합니다.

클라이언트가 구독을 시작하면, Puma 워커의 인메모리에 쿼리가 저장됩니다. 그런 다음 구독이 트리거되면, Puma 워커가 저장된 GraphQL 쿼리를 실행하고 결과를 클라이언트에 푸시합니다.

GraphiQL은 Action Cable 클라이언트를 필요로 하며 현재 GraphiQL이 이를 지원하지 않기 때문에, GraphiQL을 사용하여 구독을 테스트할 수 없습니다.

구독 빌드#

Types::SubscriptionType 아래의 모든 필드는 클라이언트가 구독할 수 있는 구독입니다. 이 필드들은 Subscriptions::BaseSubscription의 하위 클래스이며 app/graphql/subscriptions 아래에 저장되는 구독 클래스를 필요로 합니다.

구독에 필요한 인수와 반환되는 필드는 구독 클래스에서 정의됩니다. 동일한 인수를 갖고 동일한 필드를 반환하는 경우, 여러 필드가 동일한 구독 클래스를 공유할 수 있습니다.

이 클래스는 초기 구독 요청과 이후 업데이트 시에 실행됩니다. 이에 대한 자세한 내용은 GraphQL Ruby 가이드에서 확인할 수 있습니다.

인가(Authorization)#

초기 구독과 이후 업데이트가 인가되도록 구독 클래스의 #authorized? 메서드를 구현해야 합니다.

사용자가 인가되지 않은 경우, 실행을 중단하고 사용자의 구독을 해제하도록 unauthorized! 헬퍼를 호출해야 합니다.

Global ID 또는 구독 대상 객체를 기반으로 권한을 확인하는 일반적인 경우에는 #authorize_object_or_gid! 헬퍼를 사용하세요. 초기 구독 시에는 객체가 존재하지 않으므로, 주어진 Global ID를 사용하여 객체를 가져옵니다. 하지만 이후 업데이트 시에는 동일한 객체의 다른 인스턴스를 가져오지 않도록 사용자에게 반환하는 객체를 사용합니다. object 인수를 사용하여 인가할 객체를 지정할 수도 있습니다.

구독 트리거#

구독을 트리거하는 메서드는 GraphqlTriggers 모듈 아래에 정의하세요. 단일 진실 공급원(Single Source Of Truth, SSOT)을 유지하고 서로 다른 인수와 객체로 구독이 트리거되는 것을 방지하기 위해, 애플리케이션 코드에서 GitlabSchema.subscriptions.trigger를 직접 호출하지 마세요.

페이지네이션 구현#

자세한 내용은 GraphQL 페이지네이션을 참고하세요.

인수(Arguments)#

리졸버(resolver) 또는 뮤테이션의 인수argument를 사용하여 정의합니다.

예시:

argument :my_arg, GraphQL::Types::String,
         required: true,
         description: "A description of the argument."

loads: 사용 금지#

인수 정의에서 loads: 옵션을 사용하지 마세요. 이 옵션은 "찾을 수 없음"과 "인가되지 않음"에 대해 서로 다른 오류를 반환하여 리소스 존재 여부에 대한 정보를 노출합니다. 대신 Global ID를 수락하고 authorized_find!를 사용하여 객체를 수동으로 로드하세요. 자세한 내용과 예시는 인수 정의에서 loads: 사용 금지를 참고하세요.

널 허용 여부(Nullability)#

인수는 required: true로 표시할 수 있으며, 이는 값이 반드시 존재하고 null이 아니어야 함을 의미합니다. 필수 인수의 값이 null일 수 있는 경우, required: :nullable 선언을 사용하세요.

예시:

argument :due_date,
         Types::TimeType,
         required: :nullable,
         description: 'The desired due date for the issue. Due date is removed if null.'

위 예시에서 due_date 인수는 반드시 제공되어야 하지만, GraphQL 스펙과 달리 값이 null일 수 있습니다. 이를 통해 기한 제거를 위한 별도 mutation을 만들지 않고도 단일 mutation에서 기한을 '해제'할 수 있습니다.

{ due_date: null } # => OK
{ due_date: "2025-01-10" } # => OK
{  } # => invalid (not given)

Nullability and required: false#

인수에 required: false가 표시된 경우, 클라이언트는 null을 값으로 전송할 수 있습니다. 이는 종종 바람직하지 않습니다.

인수가 선택적이지만 null이 허용되지 않는 경우, 유효성 검사를 사용하여 null 전달 시 오류가 반환되도록 합니다:

argument :name, GraphQL::Types::String,
         required: false,
         validates: { allow_null: false }

또는 null이 허용되지 않는 값일 때 이를 허용하고 싶다면, 기본값으로 대체할 수 있습니다:

argument :name, GraphQL::Types::String,
         required: false,
         default_value: "No Name Provided",
         replace_null_with_default: true

자세한 내용은 Validation, NullabilityDefault Values를 참조하세요.

상호 배타적 인수#

인수를 상호 배타적으로 표시하여 동시에 제공되지 않도록 할 수 있습니다. 나열된 인수 중 둘 이상이 제공되면 최상위 오류가 추가됩니다.

예시:

argument :user_id, GraphQL::Types::String, required: false
argument :username, GraphQL::Types::String, required: false

validates mutually_exclusive: [:user_id, :username]

정확히 하나의 인수가 필요한 경우 exactly_one_of 유효성 검사기를 사용할 수 있습니다.

예시:

argument :group_path, GraphQL::Types::String, required: false
argument :project_path, GraphQL::Types::String, required: false

validates exactly_one_of: [:group_path, :project_path]

Keywords#

정의된 각 GraphQL argument는 키워드 인수로 mutation의 #resolve 메서드에 전달됩니다.

예시:

def resolve(my_arg:)
  # Perform mutation ...
end

Input Types#

graphql-ruby는 인수를 input type으로 래핑합니다.

예를 들어, mergeRequestSetDraft mutation은 다음 인수를 정의합니다(일부는 상속을 통해):

argument :project_path, GraphQL::Types::ID,
         required: true,
         description: "Project the merge request belongs to."

argument :iid, GraphQL::Types::String,
         required: true,
         description: "IID of the merge request."

argument :draft,
         GraphQL::Types::Boolean,
         required: false,
         description: <<~DESC
           Whether or not to set the merge request as a draft.
         DESC

이 인수들은 지정한 3개의 인수와 clientMutationId를 포함하는 MergeRequestSetDraftInput이라는 input type을 자동으로 생성합니다.

Object identifier arguments#

객체를 식별하는 인수는 다음과 같아야 합니다:

Full path object identifier arguments#

역사적으로 전체 경로 인수의 명명이 일관되지 않았지만, 다음과 같이 인수를 명명하는 것을 권장합니다:

  • 프로젝트 전체 경로에는 project_path

  • 그룹 전체 경로에는 group_path

  • 네임스페이스 전체 경로에는 namespace_path

ciJobTokenScopeRemoveProject mutation의 예시:

argument :project_path, GraphQL::Types::ID,
         required: true,
         description: 'Project the CI job token scope belongs to.'

IID object identifier arguments#

객체의 iid를 상위 project_path 또는 group_path와 조합하여 사용합니다. 예시:

argument :project_path, GraphQL::Types::ID,
         required: true,
         description: 'Project the issue belongs to.'

argument :iid, GraphQL::Types::String,
         required: true,
         description: 'IID of the issue.'

Global ID object identifier arguments#

discussionToggleResolve mutation의 예시:

argument :id, Types::GlobalIDType[Discussion],
         required: true,
         description: 'Global ID of the discussion.'

Global ID 폐기(Deprecate Global IDs)도 참조하세요.

Workhorse-assisted uploads#

파일 콘텐츠를 허용하는 모든 GraphQL API mutation은 Workhorse-assisted uploads를 사용해야 합니다.

구현 세부 정보는 Workhorse uploads 문서를 참조하세요.

Sort arguments#

정렬 인수는 가능한 경우 항상 enum 타입을 사용하여 사용 가능한 정렬 값의 집합을 설명해야 합니다.

enum은 일부 공통 값을 상속받기 위해 Types::SortEnum에서 상속받을 수 있습니다.

enum 값은 {PROPERTY}_{DIRECTION} 형식을 따라야 합니다. 예시:

TITLE_ASC

정렬 enum의 설명 스타일 가이드도 참조하세요.

ContainerRepositoriesResolver의 예시:

# Types::ContainerRegistry::ContainerRepositorySortEnum:
module Types
  module ContainerRegistry
    class ContainerRepositorySortEnum < SortEnum
      graphql_name 'ContainerRepositorySort'
      description 'Values for sorting container repositories'

      value 'NAME_ASC', 'Name by ascending order.', value: :name_asc
      value 'NAME_DESC', 'Name by descending order.', value: :name_desc
    end
  end
end

# Resolvers::ContainerRepositoriesResolver:
argument :sort, Types::ContainerRegistry::ContainerRepositorySortEnum,
          description: 'Sort container repositories by this criteria.',
          required: false,
          default_value: :created_desc

GitLab custom scalars#

Types::TimeType#

Types::TimeType은 Ruby의 TimeDateTime 객체를 다루는 모든 필드와 인수의 타입으로 사용되어야 합니다.

이 타입은 커스텀 스칼라로서:

  • GraphQL 필드의 타입으로 사용될 때 Ruby의 TimeDateTime 객체를 ISO-8601 형식의 표준화된 문자열로 변환합니다.

  • GraphQL 인수의 타입으로 사용될 때 ISO-8601 형식의 시간 문자열을 Ruby Time 객체로 변환합니다.

이를 통해 GraphQL API가 시간을 표현하는 표준화된 방식을 갖출 수 있습니다

시간 입력을 처리합니다.

예시:

field :created_at, Types::TimeType, null: true, description: 'Timestamp of when the issue was created.'

Global ID 스칼라#

모든 Global ID는 커스텀 스칼라입니다. 이 스칼라들은 추상 스칼라 클래스 Types::GlobalIDType으로부터 동적으로 생성됩니다.

테스트#

쿼리 또는 뮤테이션이 올바르게 실행되고 해석되는지 완전히 검증할 수 있는 것은 통합 테스트뿐입니다.

단위 테스트는 타입에 특정 필드가 있거나 뮤테이션에 특정 필수 인수가 있는지와 같이 스키마의 특정 측면을 정적으로 검증하는 경우에만 사용하세요. 필드나 인수를 정적으로 검증하는 것 이상으로 리졸버를 단위 테스트하지 마세요.

그 외 모든 테스트에는 통합 테스트를 사용하세요.

통합 테스트 작성#

통합 테스트는 GraphQL 쿼리 또는 뮤테이션의 전체 스택을 검사하며 spec/requests/api/graphql에 저장됩니다.

모든 실행 단계를 완전히 테스트하기 위해 통합 테스트를 사용합니다. 완전한 요청 통합 테스트만이 다음 사항을 검증할 수 있습니다:

  • 뮤테이션이 실제로 스키마에서 쿼리 가능한지(MutationType에 마운트되었는지).

  • 리졸버 또는 뮤테이션이 반환하는 데이터가 필드의 반환 타입과 올바르게 일치하며 오류 없이 해석되는지.

  • 인수가 입력 시 올바르게 강제 변환되고, 필드가 출력 시 올바르게 직렬화되는지.

  • 인수 전처리가 올바르게 동작하는지.

  • 인수 또는 스칼라의 유효성 검사가 올바르게 적용되는지.

  • 인수의 default_value가 올바르게 적용되는지.

  • 리졸버 또는 뮤테이션의 #ready? 메서드의 로직이 올바르게 적용되는지.

  • 객체가 성공적으로 해석되고 N+1 문제가 없는지.

쿼리를 추가할 때 a working graphql query that returns dataa working graphql query that returns no data 공유 예제를 사용하여 쿼리가 유효한 결과를 렌더링하는지 테스트할 수 있습니다.

GraphQL 통합을 수행하려면 post_graphql 헬퍼를 사용하세요.

예시:

# Good:
gql_query = %q(some query text...)
post_graphql(gql_query, current_user: current_user)
# or:
GitlabSchema.execute(gql_query, context: { current_user: current_user })

# Deprecated: avoid
resolve(described_class, obj: project, ctx: { current_user: current_user })

GraphqlHelpers#all_graphql_fields_for 헬퍼를 사용하여 사용 가능한 모든 필드를 포함하는 쿼리를 구성할 수 있습니다. 이를 통해 쿼리의 가능한 모든 필드를 렌더링하는 테스트를 더 간편하게 추가할 수 있습니다.

페이지네이션 및 정렬을 지원하는 쿼리에 필드를 추가하는 경우, 자세한 내용은 테스트를 참고하세요.

GraphQL 뮤테이션 요청을 테스트하기 위해 GraphqlHelpers는 두 가지 헬퍼를 제공합니다: graphql_mutation은 뮤테이션 이름과 뮤테이션의 입력값이 담긴 해시를 받습니다. 이 헬퍼는 뮤테이션 쿼리와 준비된 변수가 포함된 구조체를 반환합니다.

이 구조체를 post_graphql_mutation 헬퍼에 전달하면 GraphQL 클라이언트처럼 올바른 파라미터와 함께 요청을 포스트합니다.

뮤테이션의 응답에 접근하려면 graphql_mutation_response 헬퍼를 사용할 수 있습니다.

이러한 헬퍼를 사용하여 다음과 같이 스펙을 작성할 수 있습니다:

let(:mutation) do
  graphql_mutation(
    :merge_request_set_wip,
    project_path: 'gitlab-org/gitlab-foss',
    iid: '1',
    wip: true
  )
end

it 'returns a successful response' do
   post_graphql_mutation(mutation, current_user: user)

   expect(response).to have_gitlab_http_status(:success)
   expect(graphql_mutation_response(:merge_request_set_wip)['errors']).to be_empty
end

테스트 팁과 트릭#

GraphqlHelpers 지원 모듈의 메서드에 익숙해지세요. 이 메서드 중 다수는 GraphQL 테스트 작성을 더 쉽게 만들어 줍니다.

GraphqlHelpers#graphql_data_atGraphqlHelpers#graphql_dig_at과 같은 순회 헬퍼를 사용하여 결과 필드에 접근하세요. 예시:

result = GitlabSchema.execute(query)

mr_iid = graphql_dig_at(result.to_h, :data, :project, :merge_request, :iid)

결과에 대해 매칭할 때는 GraphqlHelpers#a_graphql_entity_for를 사용하세요. 예를 들어:

post_graphql(some_query)

# checks that it is a hash containing { id => global_id_of(issue) }
expect(graphql_data_at(:project, :issues, :nodes))
  .to contain_exactly(a_graphql_entity_for(issue))

# Additional fields can be passed, either as names of methods, or with values
expect(graphql_data_at(:project, :issues, :nodes))
  .to contain_exactly(a_graphql_entity_for(issue, :iid, :title, created_at: some_time))

빈 스키마를 직접 만드는 대신 GraphqlHelpers#empty_schema를 사용하여 빈 스키마를 생성하세요. 예를 들어:

# good
let(:schema) { empty_schema }

# bad
let(:query_type) { GraphQL::ObjectType.new }
let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}

double('query', schema: nil) 대신 GraphqlHelpers#query_double(schema: nil)을 사용하세요. 예를 들어:

# good
let(:query) { query_double(schema: GitlabSchema) }

# bad
let(:query) { double('Query', schema: GitlabSchema) }

프론트엔드에서 사용하는 쿼리를 테스트할 때는 GraphqlHelpers#get_graphql_query_as_string을 사용하세요. 예를 들어:

let(:query) { get_graphql_query_as_string('work_items/graphql/project_work_items.query.graphql') }
let(:variables) { { 'fullPath' => project.full_path } }

...

post_graphql(query, variables: variables)

거짓 양성(false positive)을 피하세요:

post_graphqlcurrent_user: 인자로 사용자를 인증하면, 동일한 사용자에 대한 이후 요청보다 첫 번째 요청에서 더 많은 쿼리가 생성됩니다. QueryRecorder를 사용하여 N+1 쿼리를 테스트할 경우, 각 요청마다 다른 사용자를 사용하세요.

아래 예시는 N+1 쿼리를 방지하는 테스트가 어떤 형태여야 하는지 보여줍니다:

RSpec.describe 'Query.project(fullPath).pipelines' do
  include GraphqlHelpers

  let(:project) { create(:project) }

  let(:query) do
    %(
      {
        project(fullPath: "#{project.full_path}") {
          pipelines {
            nodes {
              id
            }
          }
        }
      }
    )
  end

  it 'avoids N+1 queries' do
    first_user = create(:user)
    second_user = create(:user)
    create(:ci_pipeline, project: project)

    control_count = ActiveRecord::QueryRecorder.new do
      post_graphql(query, current_user: first_user)
    end

    create(:ci_pipeline, project: project)

    expect do
      post_graphql(query, current_user: second_user)  # use a different user to avoid a false positive from authentication queries
    end.not_to exceed_query_limit(control_count)
  end
end

app/graphql/types의 폴더 구조를 그대로 따르세요:

예를 들어, app/graphql/types/ci/pipeline_type.rb에 있는 Types::Ci::PipelineType의 필드에 대한 테스트는, 파이프라인 데이터를 가져오는 데 사용되는 쿼리에 관계없이 spec/requests/api/graphql/ci/pipeline_spec.rb에 저장해야 합니다.

단위 테스트 작성#

단위 테스트는 스키마를 정적으로 검증하는 용도로만 사용하세요. 예를 들어 다음 사항을 확인하는 데 활용합니다:

  • 타입, 뮤테이션, 또는 리졸버에 특정 이름의 필드가 있는지

  • 타입, 뮤테이션, 또는 리졸버에 특정 이름의 authorize 권한이 있는지 (단, 인가 여부는 통합 테스트를 통해 검증하세요)

  • 뮤테이션 또는 리졸버에 특정 이름의 인자가 있는지, 그리고 해당 인자가 필수인지 여부

정적 스키마 테스트 외에는 리졸버가 어떻게 resolve하거나 인가를 적용하는지를 단위 테스트로 검증하지 마세요. 대신 통합 테스트를 사용하여 전체 실행 단계를 테스트하세요.

쿼리 흐름 및 GraphQL 인프라에 관한 참고 사항#

GitLab GraphQL 인프라는 lib/gitlab/graphql에서 찾을 수 있습니다.

Instrumentation은 쿼리 실행 전후를 감싸는 기능입니다. Instrumentation 클래스를 사용하는 모듈로 구현됩니다.

예시: Present

module Gitlab
  module Graphql
    module Present
      #... some code above...

      def self.use(schema_definition)
        schema_definition.instrument(:field, ::Gitlab::Graphql::Present::Instrumentation.new)
      end
    end
  end
end

Query Analyzer는 쿼리가 실행되기 전에 이를 검증하는 일련의 콜백을 포함합니다. 각 필드는 analyzer를 통과할 수 있으며, 최종 값도 사용할 수 있습니다.

Multiplex 쿼리는 단일 요청에 여러 쿼리를 담아 보낼 수 있게 해줍니다. 이를 통해 서버에 전송하는 요청 수를 줄일 수 있습니다. (GraphQL Ruby에서는 커스텀 Multiplex Query Analyzer와 Multiplex Instrumentation을 제공합니다.)

쿼리 제한#

과도하거나 악의적인 쿼리로부터 서버 리소스를 보호하기 위해, 쿼리와 뮤테이션은 깊이(depth), 복잡도(complexity), 재귀(recursion)에 의해 제한됩니다. 이 값은 기본값으로 설정할 수 있으며, 필요에 따라 특정 쿼리에서 재정의할 수 있습니다. 복잡도 값은 객체별로 설정할 수도 있으며, 최종 쿼리 복잡도는 반환되는 객체 수를 기반으로 평가됩니다. 이는 비용이 큰 객체(예: Gitaly 호출이 필요한 경우)에 활용할 수 있습니다.

예를 들어, 리졸버에서 조건부 복잡도 메서드를 사용하는 경우:

def self.resolver_complexity(args, child_complexity:)
  complexity = super
  complexity += 2 if args[:label_name]

  complexity
end

복잡도에 대한 자세한 내용은 GraphQL Ruby 문서를 참고하세요.

문서 및 스키마#

스키마는 app/graphql/gitlab_schema.rb에 위치합니다. 자세한 내용은 스키마 레퍼런스를 참고하세요.

스키마가 변경될 때마다 생성된 GraphQL 문서를 업데이트해야 합니다. GraphQL 문서와 스키마 파일 생성 방법에 대한 자세한 내용은 스키마 문서 업데이트를 참고하세요.

독자를 돕기 위해 GraphQL API 문서에 새 페이지를 추가하는 것이 좋습니다. 가이드는 GraphQL API 페이지를 참고하세요.

변경 로그 항목 포함#

모든 클라이언트 대면 변경 사항에는 반드시 변경 로그 항목을 포함해야 합니다.

지연 평가 (Laziness)#

GraphQL에서 성능을 관리하는 데 특화된 중요한 기법 중 하나는 지연(lazy) 값을 사용하는 것입니다. 지연 값은 결과에 대한 약속을 나타내며, 해당 작업을 나중에 실행할 수 있게 해줌으로써 쿼리 트리의 여러 부분에서 쿼리를 일괄 처리할 수 있습니다. 코드에서 지연 값의 주요 예시는 GraphQL BatchLoader입니다.

지연 값을 직접 관리하려면 Gitlab::Graphql::Lazy를 참고하세요. 특히 Gitlab::Graphql::Laziness를 확인하세요. 이 모듈은 #force#delay를 포함하며, 필요에 따라 지연 값의 생성과 제거라는 기본 연산을 구현하는 데 도움을 줍니다.

지연 값을 강제 실행하지 않고 처리하려면 Gitlab::Graphql::Lazy.with_value를 사용하세요.

백엔드 GraphQL API 가이드

GitLab v19.1
원문 보기
요약

GraphQL 및 REST API 섹션을 참조하세요. GraphQL API는 버전이 없습니다(versionless). GraphQL API는 버전이 없지만, 업데이트 간 하위 호환성을 고려해야 하며, 이것이 일부 사용자에게 사이드바가 로드되지 않은 문제와 같은 인시던트를 유발할 수 있습니다.


백엔드 GraphQL API 가이드#

  이 문서는 [GitLab GraphQL API](/19.1/api/graphql/)의 백엔드를 구현하는 엔지니어를 위한 스타일 및 기술 지침을 담고 있습니다.

REST API와의 관계#

GraphQL 및 REST API 섹션을 참조하세요.

버전 관리#

GraphQL API는 버전이 없습니다(versionless).

다중 버전 호환성#

GraphQL API는 버전이 없지만, 업데이트 간 하위 호환성을 고려해야 하며, 이것이 일부 사용자에게 사이드바가 로드되지 않은 문제와 같은 인시던트를 유발할 수 있습니다.

완화 방법#

인시던트의 위험을 줄이기 위해, GitLab Self-Managed 및 GitLab Dedicated에서는 @gl_introduced 디렉티브를 사용하여 노드가 도입된 GitLab 버전을 백엔드에 알릴 수 있습니다. 이렇게 하면 쿼리가 이전 버전의 백엔드에 도달했을 때, 해당 미래 노드가 쿼리에서 제거됩니다.

이 방법은 GitLab.com에서 발생하는 문제를 완화하지는 못합니다. 새로운 GraphQL 필드는 프론트엔드 이전에 백엔드가 먼저 GitLab.com에 배포되어야 합니다.

예를 들어 다음과 같이 임의의 필드에 @gl_introduced 디렉티브를 사용할 수 있습니다:

fragment otherFieldsWithFuture on Namespace {
  webUrl
  otherFutureField @gl_introduced(version: "99.9.9")
}

query namespaceWithFutureFields {
  futureField @gl_introduced(version: "99.9.9")
  namespace(fullPath: "gitlab-org") {
    name
    futureField @gl_introduced(version: "99.9.9")
    ...otherFieldsWithFuture
  }
}

응답:

{
  "data": {
    "futureField": null,
    "namespace": {
      "name": "Gitlab Org",
      "futureField": null,
      "webUrl": "http://gdk.test:3000/groups/gitlab-org",
      "otherFutureField": null
    }
  }
}

다음의 경우에는 이 디렉티브를 사용하지 않아야 합니다:

인수(Arguments): 실행 가능한 디렉티브는 인수를 지원하지 않습니다.

프래그먼트(Fragment): 대신 프래그먼트 노드에서 디렉티브를 사용하세요.

쿼리 또는 객체 내의 단독 미래 필드, 예를 들어:

query fetchData {
  futureField @gl_introduced(version: "99.9.9")
}

응답:

{
  "errors": [
    {
      "graphQLErrors": [
        {
          "message": "Field must have selections (query 'fetchData' returns Query but has no selections. Did you mean 'fetchData { ... }'?)",
          "locations": [
            {
              "line": 1,
              "column": 1
            }
          ],
          "path": [
            "query fetchData"
          ],
          "extensions": {
            "code": "selectionMismatch",
            "nodeName": "query 'fetchData'",
            "typeName": "Query"
          }
        }
      ],
      "clientErrors": [],
      "networkError": null,
      "message": "Field must have selections (query 'fetchData' returns Query but has no selections. Did you mean 'fetchData { ... }'?)",
      "stack": ""
    }
  ]
}

Query:

query fetchData {
  futureField @gl_introduced(version: "99.9.9") {
    id
  }
}

Response:

{
  "errors": [
    {
      "graphQLErrors": [
        {
          "message": "Field must have selections (query 'fetchData' returns Query but has no selections. Did you mean 'fetchData { ... }'?)",
          "locations": [
            {
              "line": 1,
              "column": 1
            }
          ],
          "path": [
            "query fetchData"
          ],
          "extensions": {
            "code": "selectionMismatch",
            "nodeName": "query 'fetchData'",
            "typeName": "Query"
          }
        }
      ],
      "clientErrors": [],
      "networkError": null,
      "message": "Field must have selections (query 'fetchData' returns Query but has no selections. Did you mean 'fetchData { ... }'?)",
      "stack": ""
    }
  ]
}

Query:

query fetchData {
  project(fullPath: "gitlab-org/gitlab") {
    futureField @gl_introduced(version: "99.9.9")
  }
}

Response:

{
  "errors": [
    {
      "graphQLErrors": [
        {
          "message": "Field must have selections (field 'project' returns Project but has no selections. Did you mean 'project { ... }'?)",
          "locations": [
            {
              "line": 2,
              "column": 3
            }
          ],
          "path": [
            "query fetchData",
            "project"
          ],
          "extensions": {
            "code": "selectionMismatch",
            "nodeName": "field 'project'",
            "typeName": "Project"
          }
        }
      ],
      "clientErrors": [],
      "networkError": null,
      "message": "Field must have selections (field 'project' returns Project but has no selections. Did you mean 'project { ... }'?)",
      "stack": ""
    }
  ]
}
Non-nullable fields#

@gl_introduced 디렉티브가 적용된 미래 필드는 백엔드에 존재하지 않을 때 null로 폴백됩니다. 따라서 non-nullable 필드라도 @gl_introduced 디렉티브가 있는 경우 프론트엔드에서 null 체크가 필요합니다.

GitLab에서 GraphQL 학습하기#

GitLab에서 GraphQL을 배우고자 하는 백엔드 엔지니어는 이 가이드를 GraphQL Ruby gem 가이드와 함께 읽어야 합니다. 해당 가이드에서는 gem의 기능을 설명하며, 그 내용은 일반적으로 이 문서에서 중복 기술하지 않습니다.

GraphQL 자체의 설계와 기능에 대해서는 GraphQL spec의 내용을 쉽게 정리한 graphql.org 가이드를 읽어보세요.

Deep Dive#

2019년 3월, Nick Thomas가 GitLab GraphQL API에 관한 Deep Dive(GitLab 팀원 전용: https://gitlab.com/gitlab-org/create-stage/issues/1)를 진행했습니다. 이 세션은 향후 코드베이스의 해당 영역을 담당할 수 있는 모든 구성원과 도메인 지식을 공유하기 위한 것이었습니다. 다음 링크에서 관련 자료를 확인할 수 있습니다:

YouTube 녹화본, 슬라이드는 Google SlidesPDF에서 제공됩니다. 그 이후로 세부 사항이 일부 변경되었지만, 좋은 입문 자료로 활용할 수 있습니다.

GitLab의 GraphQL 구현 방식#

Robert Mosolgo가 작성한 GraphQL Ruby gem을 사용합니다. 또한 GraphQL Pro 구독을 보유하고 있습니다. 자세한 내용은 GraphQL Pro 구독을 참고하세요.

모든 GraphQL 쿼리는 단일 엔드포인트 (app/controllers/graphql_controller.rb#execute)로 전달되며, /api/graphql에서 API 엔드포인트로 노출됩니다.

GraphiQL#

GraphiQL은 기존 쿼리를 실험해볼 수 있는 인터랙티브 GraphQL API 탐색기입니다. https://<your-gitlab-site.com>/-/graphql-explorer에서 모든 GitLab 환경에 접근할 수 있습니다. 예를 들어, GitLab.com의 탐색기를 사용할 수 있습니다.

GraphQL 변경 사항이 포함된 머지 리퀘스트 리뷰#

GraphQL 프레임워크에는 주의해야 할 몇 가지 특이 사항이 있으며, 이를 충족시키기 위해 도메인 전문 지식이 필요합니다.

GraphQL 파일을 수정하거나 엔드포인트를 추가하는 머지 리퀘스트를 리뷰해야 하는 경우, GraphQL 리뷰 가이드를 참고하세요.

GraphQL 로그 읽기#

GraphQL 요청 로그를 검사하고 GraphQL 쿼리의 성능을 모니터링하는 방법에 대한 팁은 GraphQL 로그 읽기 가이드를 참고하세요.

해당 페이지에는 다음과 같은 방법에 대한 팁이 있습니다:

  • 더 이상 사용되지 않는 필드의 사용 현황 확인.

  • 쿼리가 프론트엔드에서 온 것인지 여부 확인.

인증#

인증은 GraphqlController를 통해 이루어지며, 현재는 Rails 애플리케이션과 동일한 인증 방식을 사용합니다. 따라서 세션을 공유할 수 있습니다.

쿼리 문자열에 private_token을 추가하거나, HTTP_PRIVATE_TOKEN 헤더를 추가하는 것도 가능합니다.

제한 사항#

GraphQL API에는 여러 제한 사항이 적용되며, 그 중 일부는 개발자가 재정의할 수 있습니다.

최대 페이지 크기#

기본적으로 연결(connection)app/graphql/gitlab_schema.rb에서 정의된 최대 레코드 수만큼만 페이지당 반환할 수 있습니다.

개발자는 연결을 정의할 때 커스텀 최대 페이지 크기를 지정할 수 있습니다.

최대 복잡도#

복잡도에 대한 설명은 클라이언트용 API 페이지에서 확인할 수 있습니다.

필드는 기본적으로 쿼리의 복잡도 점수에 1을 추가하지만, 개발자는 필드를 정의할 때 커스텀 복잡도를 지정할 수 있습니다.

쿼리의 복잡도 점수는 직접 쿼리할 수도 있습니다.

요청 타임아웃#

요청은 30초 후 타임아웃됩니다.

최대 필드 호출 횟수 제한#

경우에 따라, 특정 필드가 여러 상위 노드에서 평가되는 것을 방지해야 할 수 있습니다. 이는 N+1 쿼리 문제를 야기하지만 최적의 해결책이 없는 경우입니다. 이 방법은 연관 관계 미리 로드를 위한 lookahead 또는 배치 처리 사용과 같은 방법을 먼저 고려한 후 사용하는 최후의 수단으로만 활용해야 합니다.

예를 들어:

# This usage is expected.
query {
  project {
    environments
  }
}

# This usage is NOT expected.
# It results in N+1 query problem. EnvironmentsResolver can't use GraphQL batch loader in favor of GraphQL pagination.
query {
  projects {
    nodes {
      environments
    }
  }
}

이를 방지하려면 필드에 Gitlab::Graphql::Limit::FieldCallCount 확장을 사용할 수 있습니다:

# This allows maximum 1 call to the `environments` field. If the field is evaluated on more than one node,
# it raises an error.
field :environments do
        extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
      end

또는 리졸버 클래스에 확장을 적용할 수 있습니다:

module Resolvers
  class EnvironmentsResolver < BaseResolver
    extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
    # ...
  end
end

이 제한을 추가할 때는 영향을 받는 필드의 description도 그에 맞게 업데이트해야 합니다. 예를 들면,

field :environments,
      description: 'Environments of the project. This field can only be resolved for one project in any single request.'

브레이킹 체인지(Breaking Changes)#

GitLab GraphQL API는 버전이 없는(versionless) 방식이므로 개발자는 더 이상 사용되지 않음(Deprecation) 및 제거 프로세스를 숙지해야 합니다.

브레이킹 체인지는 다음과 같습니다:

  • 필드, 인수, 열거형 값, 또는 뮤테이션의 제거 또는 이름 변경.

  • 인수의 타입 또는 타입 이름 변경. 인수의 타입은 클라이언트가 변수를 사용할 때 선언하며, 변경하면 기존 타입 이름을 사용하는 쿼리가 API에서 거부됩니다.

  • 필드 또는 열거형 값의 스칼라 타입(scalar type) 변경으로 인해 값이 JSON으로 직렬화되는 방식이 달라지는 경우. 예를 들어 JSON String에서 JSON Number로의 변경, 또는 String 형식이 변경되는 경우. 오브젝트 타입(object type)으로의 변경은 오브젝트의 모든 스칼라 타입 필드가 동일한 방식으로 계속 직렬화되는 경우에 한해 허용될 수 있습니다.

  • 필드의 복잡도(complexity) 또는 리졸버의 복잡도 배수 증가.

  • Nullable 필드에서 설명한 바와 같이, 필드를 nullable이 아닌(null: false) 상태에서 nullable(null: true)로 변경.

  • 인수를 선택 사항(required: false)에서 필수(required: true)로 변경.

  • 연결(connection)의 최대 페이지 크기 변경.

  • 쿼리 복잡도 및 깊이에 대한 전역 제한 낮추기.

  • 이전에 허용되던 제한에 쿼리가 걸리는 결과를 초래하는 기타 모든 변경 사항.

항목을 더 이상 사용하지 않음으로 표시하는 방법은 스키마 항목 더 이상 사용되지 않음 처리 섹션을 참조하세요.

브레이킹 체인지 예외 사항#

GraphQL API 브레이킹 체인지 예외 사항 문서를 참조하세요.

Global ID#

GitLab GraphQL API는 Global ID(예: "gid://gitlab/MyObject/123")를 사용하며 데이터베이스 기본 키 ID는 절대 사용하지 않습니다.

Global ID는 클라이언트 사이드 라이브러리에서 캐싱 및 페칭에 사용되는 컨벤션입니다.

참고:

값이 GlobalID인 경우 입력 및 출력 인수의 타입으로 커스텀 스칼라 타입(Types::GlobalIDType)을 사용해야 합니다. 이 타입을 ID 대신 사용하면 다음과 같은 이점이 있습니다:

  • 값이 GlobalID인지 유효성을 검사합니다.

  • 사용자 코드에 전달하기 전에 GlobalID로 파싱합니다.

  • 오브젝트의 타입으로 파라미터화할 수 있어(예: GlobalIDType[Project]) 더욱 강력한 유효성 검사와 보안을 제공합니다.

모든 새 인수 및 결과 타입에 이 타입을 사용하는 것을 고려하세요. 더 넓은 범위의 오브젝트를 허용하려면(예: GlobalIDType[Issue] 대신 GlobalIDType[Issuable]) concern 또는 상위 타입(supertype)으로 이 타입을 파라미터화하는 것도 완전히 가능합니다.

최적화#

기본적으로 GraphQL은 적극적으로 최소화하려는 노력을 기울이지 않으면 N+1 문제가 발생하기 쉽습니다.

안정성과 확장성을 위해 쿼리에 N+1 성능 문제가 발생하지 않도록 해야 합니다.

다음은 GraphQL 코드를 최적화하는 데 도움이 되는 도구 목록입니다:

개발 중 N+1 문제 확인 방법#

개발 중에 N+1 문제는 다음 방법으로 발견할 수 있습니다:

  • 컬렉션 데이터를 반환하는 GraphQL 쿼리를 실행하면서 development.log를 추적(tail)합니다. Bullet이 도움이 될 수 있습니다.

  • GitLab UI에서 쿼리를 실행하는 경우 성능 표시줄을 관찰합니다.

  • 기능의 N+1 문제가 없거나 제한적임을 검증하는 request spec을 추가합니다.

필드#

타입#

우리는 코드 우선(code-first) 스키마를 사용하며, Ruby에서 모든 타입을 선언합니다.

예를 들어, app/graphql/types/project_type.rb:

graphql_name 'Project'

field :full_path, GraphQL::Types::ID, null: true
field :name, GraphQL::Types::String, null: true

각 타입에는 이름을 부여합니다(이 경우 Project).

full_pathname스칼라(scalar) GraphQL 타입입니다. full_pathGraphQL::Types::ID 타입입니다 (GraphQL::Types::ID 사용 시기 참조). name은 일반적인 GraphQL::Types::String 타입입니다. 스칼라 데이터 타입을 위해 커스텀 GraphQL 데이터 타입을 선언할 수도 있습니다(예: TimeType).

GraphQL API를 통해 모델을 노출할 때는 app/graphql/types에 새 타입을 생성하는 방식으로 수행합니다.

타입에서 속성을 노출할 때는 정의 내부의 로직을 최대한 간결하게 유지해야 합니다. 그 대신, 로직을 프레젠터(presenter)로 이동하는 것을 고려하세요:

class Types::MergeRequestType < BaseObject
  present_using MergeRequestPresenter

  name 'MergeRequest'
end

기존 프레젠터를 사용할 수도 있지만, GraphQL 전용 새 프레젠터를 생성하는 것도 가능합니다.

프레젠터는 필드에 의해 리졸브된 객체와 컨텍스트를 사용하여 초기화됩니다.

Nullable 필드#

GraphQL에서는 필드를 “nullable” 또는 “non-nullable”로 지정할 수 있습니다. 전자는 지정된 타입의 값 대신 null이 반환될 수 있음을 의미합니다. 일반적으로 다음과 같은 이유로 non-nullable 필드보다 nullable 필드를 사용하는 것이 좋습니다:

  • 데이터가 필수에서 선택으로, 또는 그 반대로 전환되는 경우가 흔합니다

  • 필드가 선택적으로 변할 가능성이 없더라도, 쿼리 시점에 해당 필드가 사용 가능하지 않을 수 있습니다

예를 들어, blob의 content를 Gitaly에서 조회해야 할 수 있습니다

  • content가 nullable이라면, 전체 쿼리를 실패시키는 대신 부분적인 응답을 반환할 수 있습니다

  • 버전 없는 스키마에서 non-nullable 필드를 nullable 필드로 변경하는 것은 어렵습니다

Non-nullable 필드는 필드가 필수이고, 향후 선택적으로 변할 가능성이 매우 낮으며, 계산이 간단한 경우에만 사용해야 합니다. id 필드가 그 예시입니다.

Non-nullable GraphQL 스키마 필드는 오브젝트 타입 뒤에 느낌표(bang) !가 붙습니다. 다음은 gitlab_schema.graphql 파일의 예시입니다:

  id: ProjectID!

다음은 non-nullable GraphQL 배열의 예시입니다:


  errors: [String!]!

추가 참고 자료:

Global ID 노출#

GitLab의 Global ID 사용 방침에 따라, 노출 시 항상 데이터베이스 기본 키 ID를 Global ID로 변환해야 합니다.

id라는 이름의 모든 필드는 자동으로 변환됩니다.

id라는 이름이 아닌 필드는 수동으로 변환해야 합니다. 이를 위해 Gitlab::GlobalID.build를 사용하거나, GlobalID::Identification 모듈이 믹스인된 객체에서 #to_global_id를 호출하면 됩니다.

Types::Notes::DiscussionType의 예시:

field :reply_id, Types::GlobalIDType[Discussion]

def reply_id
  Gitlab::GlobalId.build(object, id: object.reply_id)
end

GraphQL::Types::ID 사용 시기#

GraphQL::Types::ID를 사용하면 해당 필드는 GraphQL ID 타입이 되며, JSON 문자열로 직렬화됩니다. 그러나 ID는 클라이언트에게 특별한 의미를 가집니다. GraphQL 스펙에서는 다음과 같이 말합니다:

ID 스칼라 타입은 고유 식별자를 나타내며, 주로 객체를 다시 가져오거나 캐시의 키로 사용됩니다.

GraphQL 명세는 ID의 고유성 범위를 명확히 정의하지 않습니다. GitLab에서는 ID가 최소한 타입 이름 기준으로 고유해야 한다고 결정했습니다. 타입 이름은 Types:: 클래스 중 하나의 graphql_name으로, 예를 들어 Project 또는 Issue입니다.

이를 바탕으로:

  • Project.fullPathID여야 합니다. API 전체에서 동일한 fullPath를 가진 다른 Project가 없으며, 해당 필드가 식별자이기도 하기 때문입니다.

  • Issue.iidID되어서는 안 됩니다. API 전체에서 동일한 iid를 가진 Issue 타입이 여러 개 존재할 수 있기 때문입니다. 서로 다른 프로젝트의 Issue를 캐싱하는 클라이언트가 있다면 이를 ID로 취급하는 것은 문제가 됩니다.

  • Project.id는 일반적으로 ID가 될 자격이 있습니다. 해당 ID 값을 가진 Project는 하나뿐이기 때문입니다. 단, 데이터베이스(DB) ID 값에는 ID 타입 대신 Global ID 타입을 사용하므로 Global ID로 타입을 지정하게 됩니다.

다음 표에 이를 정리했습니다:

필드 목적 GraphQL::Types::ID 사용 여부
Full path check-circle 예
데이터베이스 ID dotted-circle 아니오
IID dotted-circle 아니오

markdown_field#

markdown_fieldfield를 감싸는 헬퍼 메서드로, 렌더링된 Markdown을 반환하는 필드에는 항상 이 메서드를 사용해야 합니다.

이 헬퍼는 GraphQL 쿼리 컨텍스트를 헬퍼에서 사용할 수 있도록 하여 기존 MarkupHelper로 모델의 Markdown 필드를 렌더링합니다.

헬퍼에서 컨텍스트를 사용할 수 있어야 하는 이유는 현재 사용자가 볼 수 없는 리소스에 대한 링크를 삭제하기 위해서입니다.

HTML 렌더링 시 쿼리가 발생할 수 있으므로, 이러한 필드의 복잡도는 기본값보다 5 높게 설정됩니다.

Markdown 필드 헬퍼는 다음과 같이 사용할 수 있습니다:

markdown_field :note_html, null: false

이 코드는 모델의 Markdown 필드 note를 렌더링하는 필드를 생성합니다. method: 인수를 추가하여 이를 재정의할 수 있습니다.

markdown_field :body_html, null: false, method: :note

이 필드에는 기본적으로 다음 설명이 제공됩니다:

note의 GitLab Flavored Markdown 렌더링 결과

description: 인수를 전달하여 이를 재정의할 수 있습니다.

Connection 타입#

구현에 대한 자세한 내용은 [페이지네이션 구현](/19.1/development/api_graphql_styleguide/#pagination-implementation)을 참조하세요.

GraphQL은 커서 기반 페이지네이션을 사용하여 항목 컬렉션을 노출합니다. 이를 통해 클라이언트에 많은 유연성을 제공하면서 백엔드에서 다양한 페이지네이션 모델을 사용할 수 있습니다.

CountableConnectionType과 LimitedCountableConnectionType 중 선택하기#

GitLab은 카운팅을 지원하는 컬렉션을 위한 두 가지 connection 타입을 제공합니다:

  • CountableConnectionType - 기본적으로 정확한 카운트를 반환하며, 성능 최적화를 위한 선택적 limit 인수를 제공합니다.

  • LimitedCountableConnectionType - 항상 제한된 카운트를 반환합니다(기본 제한: 1000).

CountableConnectionType 사용 시기:

  • 정확한 카운트가 사용자 경험에 중요한 경우(예: 이슈 총 수 표시)

  • 컬렉션 크기가 일반적으로 소규모에서 중간 규모인 경우

  • 정확한 카운트가 필요하지 않을 때 클라이언트가 limit 인수를 제공하여 성능 최적화를 선택할 수 있는 경우

LimitedCountableConnectionType 사용 시기:

  • 정확한 카운트가 사용자 경험에 필수적이지 않은 경우

  • 컬렉션이 매우 크고 모든 항목을 카운팅하는 데 비용이 많이 드는 경우

  • 기본적으로 성능 제한을 적용하고자 하는 경우

두 connection 타입은 제한이 적용될 때 동일한 제한된 카운팅 로직을 사용하며, CountableConnectionHelper 모듈을 통해 구현을 공유합니다.

리소스 컬렉션을 노출하려면 connection 타입을 사용할 수 있습니다. 이는 배열을 기본 페이지네이션 필드로 감쌉니다. 예를 들어 프로젝트 파이프라인에 대한 쿼리는 다음과 같을 수 있습니다:

query($project_path: ID!) {
  project(fullPath: $project_path) {
    pipelines(first: 2) {
      pageInfo {
        hasNextPage
        hasPreviousPage
      }
      edges {
        cursor
        node {
          id
          status
        }
      }
    }
  }
}

이 쿼리는 프로젝트의 첫 번째 파이프라인 2개와 관련 페이지네이션 정보를 ID 내림차순으로 반환합니다. 반환되는 데이터는 다음과 같습니다:

{
  "data": {
    "project": {
      "pipelines": {
        "pageInfo": {
          "hasNextPage": true,
          "hasPreviousPage": false
        },
        "edges": [
          {
            "cursor": "Nzc=",
            "node": {
              "id": "gid://gitlab/Pipeline/77",
              "status": "FAILED"
            }
          },
          {
            "cursor": "Njc=",
            "node": {
              "id": "gid://gitlab/Pipeline/67",
              "status": "FAILED"
            }
          }
        ]
      }
    }
  }
}

다음 페이지를 가져오려면 마지막으로 알려진 요소의 커서를 전달할 수 있습니다:

query($project_path: ID!) {
  project(fullPath: $project_path) {
    pipelines(first: 2, after: "Njc=") {
      pageInfo {
        hasNextPage
        hasPreviousPage
      }
      edges {
        cursor
        node {
          id
          status
        }
      }
    }
  }
}

일관된 정렬을 보장하기 위해 기본 키에 대한 정렬을 내림차순으로 추가합니다. 기본 키는 일반적으로 id이므로, 관계 끝에 order(id: :desc)를 추가합니다. 기본 키는 기반 테이블에서 반드시 사용 가능해야 합니다.

단축 필드(Shortcut fields)#

때로는 파라미터가 전달되지 않을 경우 컬렉션의 첫 번째 항목을 반환하는 "단축 필드"를 구현하는 것이 간단해 보일 수 있습니다. 이러한 "단축 필드"는 유지 관리 부담을 증가시키기 때문에 권장하지 않습니다. 단축 필드는 정식 필드와 동기화 상태를 유지해야 하며, 정식 필드가 변경되면 deprecated 처리하거나 수정해야 합니다. 특별히 다르게 해야 할 이유가 없는 한 프레임워크에서 제공하는 기능을 사용하세요.

예를 들어, latest_pipeline 대신 pipelines(last: 1)을 사용하세요.

페이지 크기 제한#

기본적으로 API는 연결(connection)에서 페이지당 최대 레코드 수를 반환하며, 이 값은 app/graphql/gitlab_schema.rb에 정의되어 있습니다. 또한 클라이언트가 제한 인수(first: 또는 last:)를 제공하지 않을 경우 페이지당 반환되는 기본 레코드 수이기도 합니다.

max_page_size 인수를 사용하면 연결에 대해 다른 페이지 크기 제한을 지정할 수 있습니다.

기본값은 GraphQL API의 성능을 보장하기 위해 설정된 것이므로, `max_page_size`를 높이는 것보다 프런트엔드 클라이언트나 제품 요구 사항을 페이지당 많은 레코드가 필요하지 않도록 변경하는 것이 더 좋습니다.

예를 들어:

field :tags,
  Types::ContainerRegistry::ContainerRepositoryTagType.connection_type,
  null: true,
  description: 'Tags of the container repository',
  max_page_size: 20

필드 복잡도(Field complexity)#

GitLab GraphQL API는 복잡도(complexity) 점수를 사용하여 과도하게 복잡한 쿼리의 실행을 제한합니다. 복잡도에 대한 설명은 해당 주제에 관한 클라이언트 문서에서 확인할 수 있습니다.

복잡도 제한은 app/graphql/gitlab_schema.rb에 정의되어 있습니다.

기본적으로 필드는 쿼리의 복잡도 점수에 1을 추가합니다. 필드에 대한 커스텀 complexity 값 제공을 통해 이를 재정의할 수 있습니다.

개발자는 서버가 데이터를 반환하기 위해 더 많은 작업을 수행하는 필드에 더 높은 복잡도를 지정해야 합니다. 대부분의 경우 idtitle처럼 거의 또는 전혀 작업 없이 반환할 수 있는 데이터를 나타내는 필드에는 복잡도 0을 부여할 수 있습니다.

calls_gitaly#

리졸빙 시 Gitaly 호출을 수행할 가능성이 있는 필드는 해당 필드를 정의할 때 fieldcalls_gitaly: true를 전달하여 반드시 표시해야 합니다.

예를 들어:

field :blob, type: Types::Snippets::BlobType,
      description: 'Snippet blob',
      null: false,
      calls_gitaly: true

이렇게 하면 해당 필드의 complexity 점수1 증가합니다.

리졸버가 Gitaly를 호출하는 경우, BaseResolver.calls_gitaly!로 어노테이션할 수 있습니다. 이렇게 하면 해당 리졸버를 사용하는 모든 필드에 calls_gitaly: true가 전달됩니다.

예를 들면 다음과 같습니다:

class BranchResolver < BaseResolver
  type ::Types::BranchType, null: true
  calls_gitaly!

  argument name: ::GraphQL::Types::String, required: true

  def resolve(name:)
    object.branch(name)
  end
end

이후 이를 사용할 때, BranchResolver를 사용하는 모든 필드에 calls_gitaly:에 대한 올바른 값이 설정됩니다.

타입에 대한 권한 노출#

현재 사용자가 리소스에 대해 가진 권한을 노출하려면, 해당 리소스에 대한 권한을 나타내는 별도의 타입을 expose_permissions에 전달하여 호출할 수 있습니다.

예를 들면 다음과 같습니다:

module Types
  class MergeRequestType < BaseObject
    expose_permissions Types::MergeRequestPermissionsType
  end
end

권한 타입은 BasePermissionType을 상속하며, 이 클래스에는 권한을 null이 아닌 불리언으로 노출할 수 있는 몇 가지 헬퍼 메서드가 포함되어 있습니다:

class MergeRequestPermissionsType < BasePermissionType
  graphql_name 'MergeRequestPermissions'

  present_using MergeRequestPresenter

  abilities :admin_merge_request, :update_merge_request, :create_note

  ability_field :resolve_note,
                description: 'Indicates the user can resolve discussions on the merge request.'
  permission_field :push_to_source_branch, method: :can_push_to_source_branch?
end
  • permission_field: graphql-rubyfield 메서드와 동일하게 동작하지만, 기본 설명과 타입을 설정하고 null이 아닌 값으로 만듭니다. 이러한 옵션은 인수로 추가하여 재정의할 수 있습니다.

  • ability_field: 정책에 정의된 ability를 노출합니다. permission_field와 동일한 방식으로 동작하며, 동일한 인수를 재정의할 수 있습니다.

  • abilities: 정책에 정의된 여러 ability를 한 번에 노출할 수 있습니다. 이 필드들은 모두 기본 설명이 있는 null이 아닌 불리언이어야 합니다.

피처 플래그#

GraphQL에서 피처 플래그를 구현하여 다음을 토글할 수 있습니다:

  • 필드의 반환 값.

  • 인수 또는 뮤테이션의 동작.

이는 리졸버, 타입, 또는 모델 메서드에서도 구현할 수 있으며, 선호도와 상황에 따라 선택하면 됩니다.

피처 플래그 뒤에 있는 동안에는 [해당 항목을 실험으로 표시](/19.1/development/api_graphql_styleguide/#mark-schema-items-as-experiments)하는 것도 권장합니다.

이는 공개 GraphQL API의 소비자에게 해당 필드가 아직 사용하기 위한 것이 아님을 알립니다. 또한 더 이상 사용 중단(deprecate) 처리 없이도 언제든지 실험적 항목을 변경하거나 제거할 수 있습니다. 플래그가 제거되면, 스키마 항목의 experiment 속성을 제거하여 항목을 "릴리즈"하고 공개 상태로 만드세요.

피처 플래그가 적용된 항목의 설명#

피처 플래그를 사용하여 스키마 항목의 값 또는 동작을 토글하는 경우, 해당 항목의 description은 반드시:

  • 값 또는 동작이 피처 플래그로 토글될 수 있음을 명시해야 합니다.

  • 피처 플래그 이름을 명시해야 합니다.

  • 피처 플래그가 비활성화(또는 활성화가 더 적절한 경우 활성화)되었을 때 필드가 반환하는 값 또는 동작이 무엇인지 명시해야 합니다.

피처 플래그 사용 예시#

피처 플래그가 적용된 필드#

필드 값은 피처 플래그 상태에 따라 토글됩니다. 일반적인 사용 예로는 피처 플래그가 비활성화된 경우 null을 반환하는 것이 있습니다:

field :foo, GraphQL::Types::String, null: true,
      experiment: { milestone: '10.0' },
      description: 'Some test field. Returns `null`' \
                   'if `my_feature_flag` feature flag is disabled.'

def foo
  object.foo if Feature.enabled?(:my_feature_flag, object)
end

피처 플래그가 적용된 argument#

argument는 피처 플래그 상태에 따라 무시되거나 값이 변경될 수 있습니다. 일반적인 사용 사례는 피처 플래그가 비활성화된 경우 argument를 무시하는 것입니다:

argument :foo, type: GraphQL::Types::String, required: false,
         experiment: { milestone: '10.0' },
         description: 'Some test argument. Is ignored if ' \
                      '`my_feature_flag` feature flag is disabled.'

def resolve(args)
  args.delete(:foo) unless Feature.enabled?(:my_feature_flag, object)
  # ...
end

피처 플래그가 적용된 mutation#

피처 플래그 상태로 인해 수행할 수 없는 mutation은 복구 불가능한 mutation 오류로 처리됩니다. 오류는 최상위 레벨에서 반환됩니다:

description 'Mutates an object. Does not mutate the object if ' \
            '`my_feature_flag` feature flag is disabled.'

def resolve(id: )
  object = authorized_find!(id: id)

  raise_resource_not_available_error! '`my_feature_flag` feature flag is disabled.' \
    if Feature.disabled?(:my_feature_flag, object)
  # ...
end

스키마 항목 deprecated 처리#

GitLab GraphQL API는 버전이 없으므로, 모든 변경 사항에서 API의 이전 버전과의 하위 호환성을 유지합니다.

필드, argument, enum 값, 또는 mutation을 제거하는 대신, 반드시 deprecated 처리해야 합니다.

deprecated 처리된 스키마 항목들은 이후 GitLab deprecation 프로세스에 따라 향후 릴리즈에서 제거될 수 있습니다.

GraphQL에서 스키마 항목을 deprecated 처리하려면:

다음도 참조하세요:

deprecation 이슈 생성#

모든 GraphQL deprecation에는 deprecation 및 제거를 추적하기 위해 Deprecations 이슈 템플릿을 사용하여 생성된 deprecation 이슈가 있어야 합니다.

deprecation 이슈에 다음 두 라벨을 적용하세요:

  • ~GraphQL

  • ~deprecation

항목을 deprecated로 표시#

필드, argument, enum 값, mutation은 deprecated 속성을 사용하여 deprecated 처리합니다. 속성의 값은 다음으로 구성된 Hash입니다:

  • reason - deprecated 처리 이유.

  • milestone - 필드가 deprecated 처리된 마일스톤.

예시:

field :token, GraphQL::Types::String, null: true,
      deprecated: { reason: 'Login via token has been removed', milestone: '10.0' },
      description: 'Token for login.'

deprecated 처리되는 항목의 원래 description은 유지되어야 하며, deprecated 처리를 언급하도록 업데이트해서는 안 됩니다. 대신, reasondescription에 추가됩니다.

deprecation reason 스타일 가이드#

필드, argument, 또는 enum 값이 교체됨으로 인한 deprecated 처리인 경우, reason은 대체 항목을 명시해야 합니다. 예를 들어, 다음은 교체된 필드에 대한 reason입니다:

Use `otherFieldName`

예시:

field :designs, ::Types::DesignManagement::DesignCollectionType, null: true,
      deprecated: { reason: 'Use `designCollection`', milestone: '10.0' },
      description: 'The designs associated with this issue.',
module Types
  class TodoStateEnum < BaseEnum
    value 'pending', deprecated: { reason: 'Use PENDING', milestone: '10.0' }
    value 'done', deprecated: { reason: 'Use DONE', milestone: '10.0' }
    value 'PENDING', value: 'pending'
    value 'DONE', value: 'done'
  end
end

더 이상 사용되지 않는 필드, 인수, 또는 열거형 값이 대체 항목 없이 폐기되는 경우, 설명이 포함된 폐기 reason을 제공해야 합니다.

Global ID 폐기#

Global ID를 생성하고 파싱하기 위해 rails/globalid gem을 사용하며, 이로 인해 Global ID는 모델 이름과 결합됩니다. 모델 이름을 변경하면 Global ID도 변경됩니다.

Global ID가 스키마 어딘가에서 인수 타입으로 사용되는 경우, Global ID 변경은 일반적으로 하위 호환성을 깨는 변경에 해당합니다.

이전 Global ID 인수를 사용하는 클라이언트를 계속 지원하기 위해, Gitlab::GlobalId::Deprecations에 폐기 항목을 추가합니다.

Global ID가 *오직* [필드로만 노출](/19.1/development/api_graphql_styleguide/#exposing-global-ids)되는 경우에는

폐기 처리가 필요하지 않습니다. 필드에서 Global ID가 표현되는 방식의 변경은 하위 호환성이 있다고 간주합니다. 클라이언트는 이러한 값을 파싱하지 않을 것으로 기대하며, 이 값들은 불투명한 토큰으로 취급되어야 합니다. 값에 내재된 구조는 우연한 것이며 의존해서는 안 됩니다.

예시 시나리오:

이 예시 시나리오는 다음 머지 리퀘스트를 기반으로 합니다.

PrometheusService라는 모델이 Integrations::Prometheus로 이름이 변경됩니다. 기존 모델 이름은 뮤테이션의 인수로 사용되는 Global ID 타입을 만드는 데 사용됩니다:

# Mutations::UpdatePrometheus:

argument :id, Types::GlobalIDType[::PrometheusService],
              required: true,
              description: "The ID of the integration to mutate."

클라이언트는 "gid://gitlab/PrometheusService/1" 형식의 Global ID 문자열을 PrometheusServiceID라는 이름으로, input.id 인수로 전달하여 뮤테이션을 호출합니다:

mutation updatePrometheus($id: PrometheusServiceID!, $active: Boolean!) {
  prometheusIntegrationUpdate(input: { id: $id, active: $active }) {
    errors
    integration {
      active
    }
  }
}

모델을 Integrations::Prometheus로 이름 변경하고 코드베이스 전체를 새 이름으로 업데이트합니다. 뮤테이션을 업데이트할 때 Types::GlobalIDType[]에 이름이 변경된 모델을 전달합니다:

# Mutations::UpdatePrometheus:

argument :id, Types::GlobalIDType[::Integrations::Prometheus],
              required: true,
              description: "The ID of the integration to mutate."

이로 인해 뮤테이션에 하위 호환성을 깨는 변경이 발생합니다. API가 이제 id 인수를 "gid://gitlab/PrometheusService/1"로 전달하거나, 쿼리 시그니처에서 인수 타입을 PrometheusServiceID로 지정하는 클라이언트를 거부하기 때문입니다.

클라이언트가 변경 없이 뮤테이션을 계속 사용할 수 있도록 하려면, Gitlab::GlobalId::DeprecationsDEPRECATIONS 상수를 수정하여 배열에 새 Deprecation을 추가합니다:

DEPRECATIONS = [
  Gitlab::Graphql::DeprecationsBase::NameDeprecation.new(old_name: 'PrometheusService', new_name: 'Integrations::Prometheus', milestone: '14.0')
].freeze

그런 다음 일반적인 폐기 프로세스를 따릅니다. 이전 인수 스타일에 대한 지원을 나중에 제거하려면 Deprecation을 삭제합니다:

DEPRECATIONS = [].freeze

폐기 기간 동안 API는 인수 값에 대해 다음 형식 중 하나를 허용합니다:

  • "gid://gitlab/PrometheusService/1"

  • "gid://gitlab/Integrations::Prometheus/1"

API는 또한 인수의 쿼리 시그니처에서 다음 타입을 허용합니다:

  • PrometheusServiceID

  • IntegrationsPrometheusID

    이전 타입(이 예시에서는 PrometheusServiceID)을 사용하는 쿼리는 API에서 유효하고 실행 가능한 것으로 간주되지만, 유효성 검사 도구는 이를 유효하지 않은 것으로 간주합니다. 이는 GraphQL 표준 메커니즘 외부의 맞춤형 방법을 사용하여 폐기를 처리하고 있기 때문에 유효하지 않은 것으로 간주됩니다.

@deprecated 지시어이므로 유효성 검사기는 지원 여부를 인식하지 못합니다.

이 문서에서는 기존 Global ID 스타일이 이제 더 이상 사용되지 않는다고 언급합니다.

스키마 항목을 실험 기능으로 표시#

GraphQL 스키마 항목(필드, 인수, enum 값, mutation)을 실험 기능으로 표시할 수 있습니다.

실험 기능으로 표시된 항목은 사용 중단 프로세스에서 면제되며, 예고 없이 언제든지 제거될 수 있습니다. 변경 가능성이 있고 공개 사용 준비가 되지 않은 항목은 실험 기능으로 표시하세요.

새 항목만 실험 기능으로 표시하세요. 기존 항목은 이미 공개되었으므로

실험 기능으로 표시하지 마세요.

스키마 항목을 실험 기능으로 표시하려면 experiment: 키워드를 사용하세요. 실험적 항목이 도입된 milestone:을 반드시 제공해야 합니다.

예시:

field :token, GraphQL::Types::String, null: true,
      experiment: { milestone: '10.0' },
      description: 'Token for login.'

마찬가지로, app/graphql/types/mutation_type.rb에서 mutation이 마운트되는 위치를 업데이트하여 전체 mutation을 실험 기능으로 표시할 수도 있습니다:

mount_mutation Mutations::Ci::JobArtifact::BulkDestroy, experiment: { milestone: '15.10' }

실험적 GraphQL 항목은 GraphQL 사용 중단을 활용하는 GitLab 커스텀 기능입니다. 실험적 항목은 GraphQL 스키마에서 사용 중단된 것으로 표시됩니다. 사용 중단된 모든 스키마 항목과 마찬가지로, 대화형 GraphQL 탐색기(GraphiQL)에서 실험적 필드를 테스트할 수 있습니다. 다만, GraphiQL 자동 완성 편집기는 사용 중단된 필드를 제안하지 않으므로 주의하세요.

생성된 GraphQL 문서 및 GraphQL 스키마 설명에서 해당 항목은 experiment로 표시됩니다.

Enum#

GitLab GraphQL enum은 app/graphql/types에 정의됩니다. 새 enum을 정의할 때는 다음 규칙이 적용됩니다:

  • 값은 대문자여야 합니다.

  • 클래스 이름은 반드시 Enum 문자열로 끝나야 합니다.

  • graphql_name에는 Enum 문자열이 포함되어서는 안 됩니다.

예시:

module Types
  class TrafficLightStateEnum < BaseEnum
    graphql_name 'TrafficLightState'
    description 'State of a traffic light'

    value 'RED', description: 'Drivers must stop.'
    value 'YELLOW', description: 'Drivers must stop when it is safe to.'
    value 'GREEN', description: 'Drivers can start or keep driving.'
  end
end

enum이 대문자 문자열이 아닌 Ruby 클래스 프로퍼티에 사용되는 경우, 대문자 값을 조정하는 value: 옵션을 제공할 수 있습니다.

다음 예시에서:

  • GraphQL 입력값 OPENED'opened'로 변환됩니다.

  • Ruby 값 'opened'는 GraphQL 응답에서 "OPENED"로 변환됩니다.

module Types
  class EpicStateEnum < BaseEnum
    graphql_name 'EpicState'
    description 'State of a GitLab epic'

    value 'OPENED', value: 'opened', description: 'An open Epic.'
    value 'CLOSED', value: 'closed', description: 'A closed Epic.'
  end
end

Enum 값은 deprecated 키워드를 사용하여 사용 중단으로 표시할 수 있습니다.

Rails enum에서 동적으로 GraphQL enum 정의하기#

GraphQL enum이 Rails enum을 기반으로 하는 경우, Rails enum을 사용하여 GraphQL enum 값을 동적으로 정의하는 것을 고려하세요. 이렇게 하면 GraphQL enum 값이 Rails enum 정의에 바인딩되므로, Rails enum에 값이 추가될 경우 GraphQL enum에 자동으로 반영됩니다.

예시:

module Types
  class IssuableSeverityEnum < BaseEnum
    graphql_name 'IssuableSeverity'
    description 'Incident severity'

    ::IssuableSeverity.severities.each_key do |severity|
      value severity.upcase, value: severity, description: "#{severity.titleize} severity."
    end
  end
end

JSON#

GraphQL이 반환할 데이터가 JSON으로 저장되어 있는 경우, 가능한 한 GraphQL 타입을 계속 사용해야 합니다. 반환되는 JSON 데이터가 진정으로 비구조적인 경우가 아니라면 GraphQL::Types::JSON 타입 사용을 피하세요.

JSON 데이터의 구조가 다양하지만 알려진 가능한 구조 집합 중 하나인 경우, union을 사용하세요. 이 목적으로 union을 사용하는 예시는 !30129에서 확인할 수 있습니다.

필요한 경우 hash_key: 키워드를 사용하여 필드 이름을 해시 데이터 키에 매핑할 수 있습니다.

예를 들어, 다음과 같은 JSON 데이터가 있을 때:

{
  "title": "My chart",
  "data": [
    { "x": 0, "y": 1 },
    { "x": 1, "y": 1 },
    { "x": 2, "y": 2 }
  ]
}

다음과 같이 GraphQL 타입을 사용할 수 있습니다:

module Types
  class ChartType < BaseObject
    field :title, GraphQL::Types::String, null: true, description: 'Title of the chart.'
    field :data, [Types::ChartDatumType], null: true, description: 'Data of the chart.'
  end
end

module Types
  class ChartDatumType < BaseObject
    field :x, GraphQL::Types::Int, null: true, description: 'X-axis value of the chart datum.'
    field :y, GraphQL::Types::Int, null: true, description: 'Y-axis value of the chart datum.'
  end
end

Descriptions#

모든 필드와 인수에는 반드시 설명이 있어야 합니다.

필드 또는 인수에 대한 설명은 description: 키워드를 사용하여 제공합니다. 예를 들어:

field :id, GraphQL::Types::ID, description: 'ID of the issue.'
field :confidential, GraphQL::Types::Boolean, description: 'Indicates the issue is confidential.'
field :closed_at, Types::TimeType, description: 'Timestamp of when the issue was closed.'

필드 및 인수의 설명은 다음에서 확인할 수 있습니다:

Description 스타일 가이드#

언어 및 구두점#

필드와 인수를 설명할 때는 가능하면 {x} of the {y} 형식을 사용하세요. 여기서 {x}는 설명하려는 항목이고 {y}는 해당 항목이 적용되는 리소스입니다. 예를 들어:

ID of the issue.
Author of the epics.

정렬하거나 검색하는 인수에는 적절한 동사로 시작하세요. 지정된 값을 나타낼 때는 간결함을 위해 the given 또는 the specified 대신 this를 사용할 수 있습니다. 예를 들어:

Sort issues by this criteria.

일관성과 간결함을 위해 설명을 The 또는 A로 시작하지 마세요.

모든 설명은 마침표(.)로 끝내야 합니다.

Boolean#

boolean 필드(GraphQL::Types::Boolean)에는 해당 필드가 무엇을 하는지 설명하는 동사로 시작하세요. 예를 들어:

Indicates the issue is confidential.

필요한 경우 기본값을 제공하세요. 예를 들어:

Sets the issue to confidential. Default is false.

Sort enum#

정렬용 Enum의 설명은 'Values for sorting {x}.' 형식으로 작성해야 합니다. 예를 들어:

Values for sorting container repositories.

Types::TimeType 필드 설명#

Types::TimeType GraphQL 필드에는 timestamp라는 단어를 포함하세요. 이렇게 하면 독자가 해당 속성의 형식이 단순히 Date가 아닌 Time임을 알 수 있습니다.

예시:

field :closed_at, Types::TimeType, description: 'Timestamp of when the issue was closed.'

copy_field_description 헬퍼#

두 설명이 항상 동일하게 유지되도록 하려는 경우가 있습니다. 예를 들어, 타입 필드 설명과 뮤테이션 인수가 동일한 속성을 나타낼 때 두 설명을 동일하게 유지하려는 경우입니다.

설명을 직접 제공하는 대신 copy_field_description 헬퍼를 사용할 수 있습니다. 타입과 복사할 필드 이름을 인수로 전달합니다.

예시:

argument :title, GraphQL::Types::String,
          required: false,
          description: copy_field_description(Types::MergeRequestType, :title)

문서 참조#

설명에 외부 URL을 참조하고 싶을 때가 있습니다. 이를 쉽게 처리하고 생성된 레퍼런스 문서에 적절한 마크업을 제공하기 위해 필드에 see 속성을 제공합니다. 예시:

field :genus,
      type: GraphQL::Types::String,
      null: true,
      description: 'A taxonomic genus.'
      see: { 'Wikipedia page on genera' => 'https://wikipedia.org/wiki/Genus' }

이 내용은 문서에서 다음과 같이 렌더링됩니다:

A taxonomic genus. See: [Wikipedia page on genera](https://wikipedia.org/wiki/Genus)

여러 문서 참조를 제공할 수 있습니다. 이 속성의 문법은 키가 텍스트 설명이고 값이 URL인 HashMap입니다.

구독 티어 배지#

필드나 인수가 다른 필드보다 더 높은 구독 티어에서만 사용 가능한 경우, 인라인 가용성 세부 정보를 추가하세요.

예시:

description: 'Full path of a custom template. Premium and Ultimate only.'

인가(Authorization)#

참조: GraphQL 인가

리졸버(Resolvers)#

애플리케이션이 응답을 제공하는 방법은 app/graphql/resolvers 디렉터리에 저장된 리졸버를 사용하여 정의합니다. 리졸버는 해당 객체를 조회하는 실제 구현 로직을 제공합니다.

필드에 표시할 객체를 찾으려면 app/graphql/resolvers에 리졸버를 추가할 수 있습니다.

인수는 뮤테이션에서와 동일한 방식으로 리졸버에서 정의할 수 있습니다. 인수(Arguments) 섹션을 참조하세요.

수행되는 쿼리 횟수를 제한하려면 BatchLoader를 사용할 수 있습니다.

리졸버 작성하기#

코드는 finder와 서비스를 감싸는 얇은 선언형 래퍼를 목표로 해야 합니다. 인수 목록을 반복하거나 concern으로 추출할 수 있습니다. 대부분의 경우 상속보다 합성(Composition)이 선호됩니다. 리졸버를 컨트롤러처럼 다루세요: 리졸버는 다른 애플리케이션 추상화를 합성하는 DSL이어야 합니다.

예시:

class PostResolver < BaseResolver
  type Post.connection_type, null: true
  authorize :read_blog
  description 'Blog posts, optionally filtered by name'

  argument :name, [::GraphQL::Types::String], required: false, as: :slug

  alias_method :blog, :object

  def resolve(**args)
    PostFinder.new(blog, current_user, args).execute
  end
end

동일한 객체가 노출되는 두 개의 다른 필드와 같이 두 곳에서 동일한 리졸버 클래스를 사용할 수 있지만, 리졸버 객체를 직접 재사용해서는 안 됩니다. 리졸버는 복잡한 라이프사이클을 가지며,

인가(권한 부여), 준비 상태 확인, 해석 오케스트레이션은 프레임워크가 담당하며, 각 단계에서 배치 기회를 활용하기 위해 지연 값(lazy values)을 반환할 수 있습니다. 애플리케이션 코드에서 리졸버나 뮤테이션을 직접 인스턴스화하지 마십시오.

대신, 코드 재사용 단위는 나머지 애플리케이션에서와 동일합니다:

  • 데이터를 조회하는 쿼리의 Finder.

  • 작업을 적용하는 뮤테이션의 Service.

  • 쿼리 전용 Loader(배치 인식 Finder).

뮤테이션에서 배치를 사용해야 할 이유는 없습니다. 뮤테이션은 순차적으로 실행되므로 배치 기회가 없습니다. 모든 값은 요청되는 즉시 즉시 평가되므로 배치는 불필요한 오버헤드입니다. 다음을 작성하는 경우:

  • Mutation이라면 객체를 직접 조회해도 됩니다.

  • Resolver 또는 BaseObject의 메서드라면 배치를 허용해야 합니다.

오류 처리#

리졸버는 오류를 발생시킬 수 있으며, 이는 적절한 경우 최상위 오류로 변환됩니다. 예상되는 모든 오류는 포착하여 적절한 GraphQL 오류로 변환해야 합니다( Gitlab::Graphql::Errors 참조). 포착되지 않은 오류는 억제되며 클라이언트는 Internal service error 메시지를 수신합니다.

한 가지 특수 사례는 권한 오류입니다. REST API에서는 사용자에게 접근 권한이 없는 리소스에 대해 404 Not Found를 반환합니다. GraphQL에서 이에 해당하는 동작은 존재하지 않거나 인가되지 않은 모든 리소스에 대해 null을 반환하는 것입니다. 쿼리 리졸버는 인가되지 않은 리소스에 대해 오류를 발생시켜서는 안 됩니다.

이렇게 하는 이유는 클라이언트가 레코드의 부재와 접근 권한이 없는 레코드의 존재를 구별할 수 없어야 하기 때문입니다. 그렇지 않으면 숨기고자 하는 정보가 유출되는 보안 취약점이 됩니다.

대부분의 경우 이에 대해 걱정할 필요가 없습니다. 이는 authorize DSL 호출로 선언하는 리졸버 필드 인가에 의해 올바르게 처리됩니다. 그러나 더 커스텀한 작업이 필요한 경우, 필드를 해석할 때 current_user가 접근 권한이 없는 객체를 만나면 전체 필드가 null로 해석되어야 한다는 점을 기억하십시오.

리졸버 파생#

(BaseResolver.singleBaseResolver.last 포함)

일부 사용 사례에서는 다른 리졸버에서 리졸버를 파생할 수 있습니다. 주요 사용 사례는 모든 항목을 찾는 리졸버와 특정 항목 하나를 찾는 리졸버입니다. 이를 위해 편의 메서드를 제공합니다:

  • BaseResolver.single: 첫 번째 항목을 선택하는 새 리졸버를 생성합니다.

  • BaseResolver.last: 마지막 항목을 선택하는 리졸버를 생성합니다.

올바른 단수 타입은 컬렉션 타입에서 유추되므로 여기서 type을 별도로 정의할 필요가 없습니다.

이 메서드를 사용하기 전에, 다음 중 하나가 더 간단한지 고려하십시오:

  • 자체 인수를 정의하는 다른 리졸버 작성.

  • 쿼리를 추상화하는 concern 작성.

BaseResolver.single을 너무 자유롭게 사용하는 것은 안티패턴입니다. 이는 인수가 없을 때 단순히 첫 번째 MR을 반환하는 Project.mergeRequest 필드와 같이 의미 없는 필드로 이어질 수 있습니다. 컬렉션 리졸버에서 단일 리졸버를 파생할 때는 반드시 더 제한적인 인수를 가져야 합니다.

이를 가능하게 하려면 when_single 블록을 사용하여 단일 리졸버를 커스터마이즈하십시오. 모든 when_single 블록은 반드시:

  • 하나 이상의 인수를 정의(또는 재정의)해야 합니다.

  • 선택적 필터를 필수로 만들어야 합니다.

예를 들어, 기존 선택적 인수를 재정의하여 타입을 변경하고 필수로 만들 수 있습니다:

class JobsResolver < BaseResolver
  type JobType.connection_type, null: true
  authorize :read_pipeline

  argument :name, [::GraphQL::Types::String], required: false

  when_single do
    argument :name, ::GraphQL::Types::String, required: true
  end

  def resolve(**args)
    JobsFinder.new(pipeline, current_user, args.compact).execute
  end

여기서는 파이프라인 job을 가져오는 리졸버가 있습니다. name 인수는 목록을 가져올 때는 선택적이지만 단일 job을 가져올 때는 필수입니다.

여러 인수가 있고 어느 것도 필수로 만들 수 없는 경우, 블록을 사용하여 준비 조건을 추가할 수 있습니다:

class JobsResolver < BaseResolver
  alias_method :pipeline, :object

  type JobType.connection_type, null: true
  authorize :read_pipeline

  argument :name, [::GraphQL::Types::String], required: false
  argument :id, [::Types::GlobalIDType[::Job]],
           required: false,
           prepare: ->(ids, ctx) { ids.map(&:model_id) }

  when_single do
    argument :name, ::GraphQL::Types::String, required: false
    argument :id, ::Types::GlobalIDType[::Job],
             required: false
             prepare: ->(id, ctx) { id.model_id }

    def ready?(**args)
      raise ::Gitlab::Graphql::Errors::ArgumentError, 'Only one argument may be provided' unless args.size == 1
    end
  end

  def resolve(**args)
    JobsFinder.new(pipeline, current_user, args.compact).execute
  end

그런 다음 이 리졸버를 필드에서 사용할 수 있습니다.

# In PipelineType

field :jobs, resolver: JobsResolver, description: 'All jobs.'
field :job, resolver: JobsResolver.single, description: 'A single job.'

리졸버 최적화#

룩어헤드(Look-Ahead)#

실행 중에 전체 쿼리를 미리 알 수 있으므로, lookahead를 활용하여 쿼리를 최적화하고 필요한 연관 관계를 일괄 로드할 수 있습니다. N+1 성능 문제를 방지하기 위해 리졸버에 룩어헤드 지원을 추가하는 것을 고려하세요.

일반적인 룩어헤드 사용 사례(자식 필드가 요청될 때 연관 관계를 사전 로드)에 대한 지원을 활성화하려면, LooksAhead를 include할 수 있습니다. 예를 들어:

# Assuming a model `MyThing` with attributes `[child_attribute, other_attribute, nested]`,
# where nested has an attribute named `included_attribute`.
class MyThingResolver < BaseResolver
  include LooksAhead

  # Rather than defining `resolve(**args)`, we implement: `resolve_with_lookahead(**args)`
  def resolve_with_lookahead(**args)
    apply_lookahead(MyThingFinder.new(current_user).execute)
  end

  # We list things that should always be preloaded:
  # For example, if child_attribute is always needed (during authorization
  # perhaps), then we can include it here.
  def unconditional_includes
    [:child_attribute]
  end

  # We list things that should be included if a certain field is selected:
  def preloads
    {
        field_one: [:other_attribute],
        field_two: [{ nested: [:included_attribute] }]
    }
  end
end

기본적으로 #preloads에 정의된 필드는 쿼리에서 해당 필드가 선택될 때 사전 로드됩니다. 경우에 따라 너무 많거나 잘못된 콘텐츠를 사전 로드하지 않도록 더 세밀한 제어가 필요할 수 있습니다.

위의 예시를 확장하면, 특정 필드들이 함께 요청될 때 다른 연관 관계를 사전 로드하고 싶을 수 있습니다. 이는 #filtered_preloads를 오버라이드하여 수행할 수 있습니다.

class MyThingResolver < BaseResolver
  # ...

  def filtered_preloads
    return [:alternate_attribute] if lookahead.selects?(:field_one) && lookahead.selects?(:field_two)

    super
  end
end

LooksAhead concern은 중첩된 GraphQL 필드 정의를 기반으로 연관 관계를 사전 로드하는 기능도 제공합니다. 중첩 필드가 선택될 때 지정한 연관 관계를 사전 로드하려면 해시 키로 필드 이름 배열을 사용하세요. 예를 들어:

class MyThingResolver < BaseResolver
  # ...

  def preloads
    {
      [:root_field, :nested_field1] => :association_to_preload,
      [:root_field, :nested_field2] => [:association1, :association2],
      [:root_field, :nested_field2, :nested_field3] => :association3,
      other_root_field: :other_association,
    }
  end
end

실제 사용 예시는 WorkItems::LookAheadPreloads를 참고하세요.

before_connection_authorization#

before_connection_authorization 훅은 타입 인가(type authorization) 권한 검사에서 발생하는 N+1 문제를 리졸버가 해소하는 데 도움을 줄 수 있습니다.

before_connection_authorization 메서드는 해석된 노드와 현재 사용자를 인수로 받습니다. 블록 내에서 ActiveRecord::Associations::Preloader 또는 Preloaders:: 클래스를 사용하여 타입 인가 검사를 위한 데이터를 사전 로드하세요.

예시:

class LabelsResolver < BaseResolver
  before_connection_authorization do |labels, current_user|
    Preloaders::LabelsPreloader.new(labels, current_user).preload_all
  end
end

배치 로딩(BatchLoading)#

GraphQL BatchLoader를 참고하세요.

Resolver#ready?의 올바른 사용#

Resolver에는 프레임워크의 일부로서 두 가지 공개 API 메서드가 있습니다: #ready?(**args)#resolve(**args). #ready?를 사용하면 #resolve를 호출하지 않고도 초기 설정 또는 조기 반환을 수행할 수 있습니다.

#ready?를 사용하는 타당한 이유는 다음과 같습니다:

  • 사전에 결과가 없을 것을 알고 있는 경우 Relation.none을 반환합니다.

  • 인스턴스 변수 초기화와 같은 설정 작업을 수행합니다(단, 이 경우에는 지연 초기화 메서드를 고려하세요).

Resolver#ready?(**args) 구현 시 다음과 같이 (Boolean, early_return_data)를 반환해야 합니다:

def ready?(**args)
  [false, 'have this instead']
end

이러한 이유로, resolver를 직접 호출할 때(주로 테스트에서. 프레임워크 추상화 Resolver는 재사용 가능하다고 간주해서는 안 되며, finder를 사용하는 것이 권장됩니다)는 resolve를 호출하기 전에 반드시 ready? 메서드를 호출하고 불리언 플래그를 확인해야 합니다. 예시는 GraphqlHelpers에서 확인할 수 있습니다.

인수 유효성 검사에는 #ready? 대신 validator 사용을 권장합니다.

부정 인수#

부정 필터를 사용하면 일부 리소스를 필터링할 수 있습니다(예: bug 라벨이 있지만 bug2 라벨은 없는 이슈를 모두 찾기). 부정 인수를 전달하는 데 권장되는 문법은 not 인수입니다:

issues(labelName: "bug", not: {labelName: "bug2"}) {
  nodes {
    id
    title
  }
}

타입 또는 resolver에서 Gitlab::Graphql::NegatableArgumentsnegated 헬퍼를 사용할 수 있습니다. 예시:

extend ::Gitlab::Graphql::NegatableArguments

negated do
  argument :labels, [GraphQL::STRING_TYPE],
            required: false,
            as: :label_name,
            description: 'Array of label names. All resolved merge requests will not have these labels.'
end

메타데이터#

resolver를 사용할 때, resolver는 필드 메타데이터의 단일 진실 공급원(Single Source Of Truth, SSOT) 역할을 해야 합니다. 모든 필드 옵션(필드 이름 제외)을 resolver에 선언할 수 있습니다. 여기에는 다음이 포함됩니다:

  • type (필수 - 모든 resolver에는 타입 어노테이션이 포함되어야 합니다)

  • extras

  • description

  • Gitaly 어노테이션 (calls_gitaly! 사용)

예시:

module Resolvers
  MyResolver < BaseResolver
    type Types::MyType, null: true
    extras [:lookahead]
    description 'Retrieve a single MyType'
    calls_gitaly!
  end
end

상위 객체를 자식 Presenter로 전달#

때로는 필드를 계산하기 위해 자식 컨텍스트에서 쿼리의 상위 객체에 접근해야 합니다. 일반적으로 상위 객체는 Resolver 클래스에서만 parent로 접근 가능합니다.

Presenter 클래스에서 상위 객체를 찾으려면:

resolver의 resolve 메서드에서 GraphQL context에 상위 객체를 추가합니다:

  def resolve(**args)
    context[:parent_object] = parent
  end

resolver 또는 필드가 parent 필드 컨텍스트를 필요로 한다고 선언합니다. 예시:

  # in ChildType
  field :computed_field, SomeType, null: true,
        method: :my_computing_method,
        extras: [:parent], # Necessary
        description: 'My field description.'

  field :resolver_field, resolver: SomeTypeResolver

  # In SomeTypeResolver

  extras [:parent]
  type SomeType, null: true
  description 'My field description.'

Presenter 클래스에서 필드의 메서드를 선언하고 parent 키워드 인수를 받도록 합니다. 이 인수는 상위 GraphQL context를 포함하므로, Resolver에서 사용한 키를 이용하여 parent[:parent_object]와 같이 상위 객체에 접근해야 합니다:

  # in ChildPresenter
  def my_computing_method(parent:)
    # do something with `parent[:parent_object]` here
  end

  # In SomeTypeResolver

  def resolve(parent:)
    # ...
  end

실제 사용 예시는 IterationPresenterscopedPathscopedUrl을 추가한 MR을 참고하세요.

Mutations#

Mutation은 저장된 값을 변경하거나 액션을 트리거하는 데 사용됩니다. GET 요청이 데이터를 수정해서는 안 되는 것처럼, 일반 GraphQL 쿼리에서는 데이터를 수정할 수 없습니다. 그러나 mutation에서는 가능합니다.

Mutation 구성#

Mutation은 app/graphql/mutations에 저장되며, 서비스와 유사하게 변경 대상 리소스별로 그룹화하는 것이 이상적입니다. Mutation은 Mutations::BaseMutation을 상속해야 합니다. mutation에 정의된 필드는 mutation의 결과로 반환됩니다.

Update mutation 세분화#

GitLab의 서비스 지향 아키텍처에서는 대부분의 mutation이 UpdateMergeRequestService와 같이 Create, Delete, 또는 Update 서비스를 호출합니다. Update mutation의 경우, 객체의 한 측면만 업데이트하고 싶을 때가 있으므로 MergeRequest::SetDraft와 같은 세밀한(fine-grained) mutation만 필요할 수 있습니다.

세밀한 mutation과 거친(coarse-grained) mutation을 함께 사용하는 것은 허용되지만, 지나치게 많은 세밀한 mutation은 유지보수성, 코드 이해도, 테스트 측면에서 관리상의 어려움을 초래할 수 있음을 유의하세요. 각 mutation은 새로운 클래스를 필요로 하므로 기술 부채로 이어질 수 있습니다. 또한 스키마가 매우 커져 사용자가 스키마를 탐색하기 어려워질 수 있습니다. 새로운 mutation마다 테스트(느린 요청 통합 테스트 포함)가 필요하기 때문에, mutation을 추가하면 테스트 스위트가 느려집니다.

변경을 최소화하려면:

  • 가능한 경우 MergeRequest::Update와 같은 기존 mutation을 사용합니다.

  • 기존 서비스를 거친(coarse-grained) mutation으로 노출합니다.

세밀한 mutation이 더 적합할 수 있는 경우:

  • 특정 권한이나 기타 전문화된 로직이 필요한 속성을 수정하는 경우.

  • 상태 머신과 유사한 전환(이슈 잠금, MR 머지, 에픽 닫기 등)을 노출하는 경우.

  • 중첩된 속성을 허용하는 경우(하위 객체의 속성을 받는 경우).

  • mutation의 의미가 명확하고 간결하게 표현될 수 있는 경우.

자세한 배경은 이슈 #233063을 참고하세요.

명명 규칙#

각 mutation은 GraphQL 스키마에서 mutation의 이름인 graphql_name을 정의해야 합니다.

예시:

class UserUpdateMutation < BaseMutation
  graphql_name 'UserUpdate'
end

graphql-ruby gem의 1.13 버전 변경으로 인해, 타입 이름이 올바르게 생성되도록 graphql_name은 클래스의 첫 번째 줄에 위치해야 합니다. Graphql::GraphqlNamePosition cop이 이를 강제합니다. 자세한 배경은 이슈 #27536을 참고하세요.

GitLab의 GraphQL mutation 이름은 역사적으로 일관성이 부족했지만, 새로운 mutation 이름은 '{Resource}{Action}' 또는 '{Resource}{Action}{Attribute}' 규칙을 따라야 합니다.

새 리소스를 생성하는 mutation은 동사 Create를 사용해야 합니다.

예시:

  • CommitCreate

데이터를 업데이트하는 mutation은 다음을 사용합니다:

  • 동사 Update.

  • 더 적합한 경우 Set, Add, Toggle과 같은 도메인별 동사.

예시:

  • EpicTreeReorder

  • IssueSetWeight

  • IssueUpdate

  • TodoMarkDone

데이터를 삭제하는 mutation은 다음을 사용합니다:

  • Destroy 대신 동사 Delete.

  • 더 적합한 경우 Remove와 같은 도메인별 동사.

예시:

  • AwardEmojiRemove

  • NoteDelete

뮤테이션 네이밍에 대한 조언이 필요하다면 Slack #graphql 채널에서 피드백을 구하세요.

필드#

가장 일반적인 상황에서 뮤테이션은 2개의 필드를 반환합니다:

  • 수정 중인 리소스

  • 작업을 수행할 수 없었던 이유를 설명하는 오류 목록. 뮤테이션이 성공하면 이 목록은 비어 있습니다.

새 뮤테이션을 Mutations::BaseMutation에서 상속하면 errors 필드가 자동으로 추가됩니다. clientMutationId 필드도 추가되며, 클라이언트는 이를 사용하여 단일 요청에서 여러 뮤테이션이 수행될 때 단일 뮤테이션의 결과를 식별할 수 있습니다.

resolve 메서드#

리졸버 작성과 유사하게, 뮤테이션의 resolve 메서드는 서비스를 감싸는 얇고 선언적인 래퍼를 목표로 해야 합니다.

resolve 메서드는 뮤테이션의 인수를 키워드 인수로 받습니다. 여기서 리소스를 수정하는 서비스를 호출할 수 있습니다.

resolve 메서드는 errors 배열을 포함하여 뮤테이션에 정의된 필드 이름과 동일한 해시를 반환해야 합니다. 예를 들어, Mutations::MergeRequests::SetDraftmerge_request 필드를 정의합니다:

field :merge_request,
      Types::MergeRequestType,
      null: true,
      description: "The merge request after mutation."

즉, 이 뮤테이션의 resolve에서 반환되는 해시는 다음과 같아야 합니다:

{
  # The merge request modified, this will be wrapped in the type
  # defined on the field
  merge_request: merge_request,
  # An array of strings if the mutation failed after authorization.
  # The `errors_on_object` helper collects `errors.full_messages`
  errors: errors_on_object(merge_request)
}

뮤테이션 마운트#

뮤테이션을 사용 가능하게 하려면 graphql/types/mutation_type에 저장된 뮤테이션 타입에 정의되어야 합니다. mount_mutation 헬퍼 메서드는 뮤테이션의 GraphQL 이름을 기반으로 필드를 정의합니다:

module Types
  class MutationType < BaseObject
    graphql_name 'Mutation'

    include Gitlab::Graphql::MountMutation

    mount_mutation Mutations::MergeRequests::SetDraft
  end
end

이는 Mutations::MergeRequests::SetDraft를 resolve하는 mergeRequestSetDraft라는 필드를 생성합니다.

리소스 인가#

뮤테이션 내에서 리소스를 인가하려면 먼저 뮤테이션에 필요한 권한을 다음과 같이 제공합니다:

module Mutations
  module MergeRequests
    class SetDraft < Base
      graphql_name 'MergeRequestSetDraft'

      authorize :update_merge_request
    end
  end
end

그런 다음 resolve 메서드에서 authorize!를 호출하여 권한을 검증할 리소스를 전달할 수 있습니다.

또는 뮤테이션에서 오브젝트를 로드하는 find_object 메서드를 추가할 수 있습니다. 이렇게 하면 authorized_find! 헬퍼 메서드를 사용할 수 있습니다.

사용자가 해당 작업을 수행할 권한이 없거나, 인가로 인해 리소스를 찾을 수 없는 경우 (사용자가 리소스에 접근할 수 없음), resolve 메서드에서 raise_resource_not_available_error!를 호출하여 Gitlab::Graphql::Errors::ResourceNotAvailable을 발생시켜야 합니다. 사용자 입력 유효성 검사 오류(예: 잘못된 프로젝트 경로 또는 형식이 잘못된 식별자)의 경우, 클라이언트가 사용자에게 의미 있는 메시지를 표시할 수 있도록 뮤테이션 페이로드의 errors 배열에 오류를 반환하세요. 자세한 내용은 뮤테이션의 오류를 참조하세요.

뮤테이션의 오류#

뮤테이션에는 데이터로서의 오류 관행을 따르는 것을 권장합니다. 이는 오류를 처리할 수 있는 대상에 따라 오류를 구분합니다.

주요 사항:

  • 모든 뮤테이션 응답에는 errors 필드가 있습니다. 이 필드는 실패 시 반드시 채워져야 하며, 성공 시에도 채워질 수 있습니다.

  • 에러를 확인해야 하는 대상이 사용자인지 개발자인지 고려하세요.

  • 클라이언트는 뮤테이션을 수행할 때 항상 errors 필드를 요청해야 합니다.

  • 에러는 $root.errors(최상위 에러) 또는 $root.data.mutationName.errors(뮤테이션 에러)에서 사용자에게 보고될 수 있습니다. 위치는 에러의 종류와 에러가 담고 있는 정보에 따라 결정됩니다.

  • 뮤테이션 필드는 반드시 null: true여야 합니다.

doTheThing이라는 예시 뮤테이션이 두 개의 필드 errors: [String]thing: ThingType을 포함한 응답을 반환한다고 가정해 보세요. thing 자체의 구체적인 내용은 이 예시에서 중요하지 않으며, 에러를 중심으로 살펴봅니다.

뮤테이션 응답이 가질 수 있는 세 가지 상태는 다음과 같습니다:

Success#

정상적인 경우, 예상 페이로드와 함께 에러가 반환될 수 있지만, 모든 것이 성공적이라면 errors는 빈 배열이어야 합니다. 사용자에게 알려야 할 문제가 없기 때문입니다.

{
  data: {
    doTheThing: {
      errors: [] // if successful, this array will generally be empty.
      thing: { .. }
    }
  }
}

Failure (relevant to the user)#

사용자에게 영향을 미치는 에러가 발생한 경우입니다. 이를 뮤테이션 에러라고 합니다.

create 뮤테이션에서는 일반적으로 반환할 thing이 없습니다.

update 뮤테이션에서는 thing의 현재 실제 상태를 반환합니다. 개발자는 이를 보장하기 위해 thing 인스턴스에서 #reset을 호출해야 할 수 있습니다.

{
  data: {
    doTheThing: {
      errors: ["you cannot touch the thing"],
      thing: { .. }
    }
  }
}

이에 해당하는 예시는 다음과 같습니다:

  • 모델 유효성 검사 오류: 사용자가 입력값을 변경해야 할 수 있습니다.

  • 권한 에러: 사용자는 이 작업을 수행할 수 없다는 것을 알아야 하며, 권한을 요청하거나 로그인해야 할 수 있습니다.

  • 사용자의 작업을 방해하는 애플리케이션 상태 문제(예: 머지 충돌 또는 잠긴 리소스).

이상적으로는 사용자가 이 단계까지 오지 않도록 방지해야 하지만, 만약 그렇게 된다면 사용자에게 무엇이 잘못되었는지 알려주어야 합니다. 그래야 사용자가 실패 원인을 이해하고 의도한 작업을 완수하기 위해 무엇을 해야 하는지 알 수 있습니다. 예를 들어, 단순히 요청을 재시도하기만 하면 될 수도 있습니다.

복구 가능한 에러를 뮤테이션 데이터와 함께 반환하는 것도 가능합니다. 예를 들어, 사용자가 10개의 파일을 업로드했는데 그 중 3개가 실패하고 나머지가 성공했다면, 실패한 파일에 대한 에러를 성공한 파일에 대한 정보와 함께 사용자에게 제공할 수 있습니다.

Failure (irrelevant to the user)#

하나 이상의 복구 불가능한 에러가 최상위 레벨에서 반환될 수 있습니다. 이러한 에러는 사용자가 제어할 수 없거나 거의 통제할 수 없는 것으로, 주로 개발자가 알아야 하는 시스템 또는 프로그래밍 문제여야 합니다. 이 경우 data가 없습니다:

{
  errors: [
    {"message": "argument error: expected an integer, got null"},
  ]
}

이는 뮤테이션 실행 중 에러가 발생했을 때 나타납니다. 현재 구현에서는 인수 에러와 유효성 검사 에러의 메시지가 클라이언트에 반환되며, 그 외 모든 StandardError 인스턴스는 캐치되어 로깅되고 메시지가 "Internal server error"로 설정되어 클라이언트에 표시됩니다. 자세한 내용은 GraphqlController를 참조하세요.

이러한 에러는 다음과 같은 프로그래밍 에러를 나타냅니다:

  • GraphQL 구문 오류: String 대신 Int가 전달되거나 필수 인수가 없는 경우.

  • 스키마 에러: non-nullable 필드에 값을 제공할 수 없는 경우 등.

  • 시스템 에러: 예를 들어, Git 저장소 예외 또는 데이터베이스 사용 불가.

사용자는 정상적인 사용 중에 이러한 에러를 발생시킬 수 없어야 합니다. 이 범주의 에러는 내부 에러로 취급되며, 사용자에게 구체적인 내용을 표시해서는 안 됩니다.

뮤테이션이 실패했을 때 사용자에게 알려야 하지만, 그 이유를 알릴 필요는 없습니다. 사용자가 이를 유발할 수 없었고 사용자가 할 수 있는 조치도 없기 때문입니다. 다만 뮤테이션 재시도를 제안할 수 있습니다.

오류 분류#

뮤테이션을 작성할 때, 오류 상태가 이 두 가지 범주 중 어디에 해당하는지 인식하고 (프론트엔드 개발자와 소통하여 가정을 검증해야 합니다) 있어야 합니다. 이는 사용자의 요구와 클라이언트의 요구를 구별하는 것을 의미합니다.

사용자에게 알릴 필요가 없는 오류는 절대 캐치하지 마세요.

사용자에게 알릴 필요가 있다면, 프론트엔드 개발자와 소통하여 전달하는 오류 정보가 관련성 있고 목적에 부합하는지 확인하세요.

프론트엔드 GraphQL 가이드도 참고하세요.

뮤테이션 별칭 지정 및 Deprecated 처리#

#mount_aliased_mutation 헬퍼를 사용하면 MutationType에서 뮤테이션에 다른 이름의 별칭을 지정할 수 있습니다.

예를 들어, FooMutation이라는 뮤테이션에 BarMutation이라는 별칭을 지정하려면:

mount_aliased_mutation 'BarMutation', Mutations::FooMutation

이를 통해 뮤테이션의 이름을 변경하면서 기존 이름도 계속 지원할 수 있으며, deprecated 인수와 함께 사용하면 됩니다.

예시:

mount_aliased_mutation 'UpdateFoo',
                        Mutations::Foo::Update,
                        deprecated: { reason: 'Use fooUpdate', milestone: '13.2' }

Deprecated된 뮤테이션은 Types::DeprecatedMutations에 추가하고 Types::MutationType의 단위 테스트에서 테스트해야 합니다. 머지 리퀘스트 !34798을 이에 대한 예시로 참고할 수 있으며, deprecated된 별칭 뮤테이션의 테스트 방법도 포함되어 있습니다.

EE 뮤테이션 Deprecated 처리#

EE 뮤테이션도 동일한 절차를 따라야 합니다. 머지 리퀘스트 절차에 대한 예시는 머지 리퀘스트 !42588을 참고하세요.

구독(Subscriptions)#

구독을 사용하여 클라이언트에 업데이트를 푸시합니다. Action Cable 구현을 사용하여 웹소켓을 통해 메시지를 전달합니다.

클라이언트가 구독을 시작하면, Puma 워커의 인메모리에 쿼리가 저장됩니다. 그런 다음 구독이 트리거되면, Puma 워커가 저장된 GraphQL 쿼리를 실행하고 결과를 클라이언트에 푸시합니다.

GraphiQL은 Action Cable 클라이언트를 필요로 하며 현재 GraphiQL이 이를 지원하지 않기 때문에, GraphiQL을 사용하여 구독을 테스트할 수 없습니다.

구독 빌드#

Types::SubscriptionType 아래의 모든 필드는 클라이언트가 구독할 수 있는 구독입니다. 이 필드들은 Subscriptions::BaseSubscription의 하위 클래스이며 app/graphql/subscriptions 아래에 저장되는 구독 클래스를 필요로 합니다.

구독에 필요한 인수와 반환되는 필드는 구독 클래스에서 정의됩니다. 동일한 인수를 갖고 동일한 필드를 반환하는 경우, 여러 필드가 동일한 구독 클래스를 공유할 수 있습니다.

이 클래스는 초기 구독 요청과 이후 업데이트 시에 실행됩니다. 이에 대한 자세한 내용은 GraphQL Ruby 가이드에서 확인할 수 있습니다.

인가(Authorization)#

초기 구독과 이후 업데이트가 인가되도록 구독 클래스의 #authorized? 메서드를 구현해야 합니다.

사용자가 인가되지 않은 경우, 실행을 중단하고 사용자의 구독을 해제하도록 unauthorized! 헬퍼를 호출해야 합니다.

Global ID 또는 구독 대상 객체를 기반으로 권한을 확인하는 일반적인 경우에는 #authorize_object_or_gid! 헬퍼를 사용하세요. 초기 구독 시에는 객체가 존재하지 않으므로, 주어진 Global ID를 사용하여 객체를 가져옵니다. 하지만 이후 업데이트 시에는 동일한 객체의 다른 인스턴스를 가져오지 않도록 사용자에게 반환하는 객체를 사용합니다. object 인수를 사용하여 인가할 객체를 지정할 수도 있습니다.

구독 트리거#

구독을 트리거하는 메서드는 GraphqlTriggers 모듈 아래에 정의하세요. 단일 진실 공급원(Single Source Of Truth, SSOT)을 유지하고 서로 다른 인수와 객체로 구독이 트리거되는 것을 방지하기 위해, 애플리케이션 코드에서 GitlabSchema.subscriptions.trigger를 직접 호출하지 마세요.

페이지네이션 구현#

자세한 내용은 GraphQL 페이지네이션을 참고하세요.

인수(Arguments)#

리졸버(resolver) 또는 뮤테이션의 인수argument를 사용하여 정의합니다.

예시:

argument :my_arg, GraphQL::Types::String,
         required: true,
         description: "A description of the argument."

loads: 사용 금지#

인수 정의에서 loads: 옵션을 사용하지 마세요. 이 옵션은 "찾을 수 없음"과 "인가되지 않음"에 대해 서로 다른 오류를 반환하여 리소스 존재 여부에 대한 정보를 노출합니다. 대신 Global ID를 수락하고 authorized_find!를 사용하여 객체를 수동으로 로드하세요. 자세한 내용과 예시는 인수 정의에서 loads: 사용 금지를 참고하세요.

널 허용 여부(Nullability)#

인수는 required: true로 표시할 수 있으며, 이는 값이 반드시 존재하고 null이 아니어야 함을 의미합니다. 필수 인수의 값이 null일 수 있는 경우, required: :nullable 선언을 사용하세요.

예시:

argument :due_date,
         Types::TimeType,
         required: :nullable,
         description: 'The desired due date for the issue. Due date is removed if null.'

위 예시에서 due_date 인수는 반드시 제공되어야 하지만, GraphQL 스펙과 달리 값이 null일 수 있습니다. 이를 통해 기한 제거를 위한 별도 mutation을 만들지 않고도 단일 mutation에서 기한을 '해제'할 수 있습니다.

{ due_date: null } # => OK
{ due_date: "2025-01-10" } # => OK
{  } # => invalid (not given)

Nullability and required: false#

인수에 required: false가 표시된 경우, 클라이언트는 null을 값으로 전송할 수 있습니다. 이는 종종 바람직하지 않습니다.

인수가 선택적이지만 null이 허용되지 않는 경우, 유효성 검사를 사용하여 null 전달 시 오류가 반환되도록 합니다:

argument :name, GraphQL::Types::String,
         required: false,
         validates: { allow_null: false }

또는 null이 허용되지 않는 값일 때 이를 허용하고 싶다면, 기본값으로 대체할 수 있습니다:

argument :name, GraphQL::Types::String,
         required: false,
         default_value: "No Name Provided",
         replace_null_with_default: true

자세한 내용은 Validation, NullabilityDefault Values를 참조하세요.

상호 배타적 인수#

인수를 상호 배타적으로 표시하여 동시에 제공되지 않도록 할 수 있습니다. 나열된 인수 중 둘 이상이 제공되면 최상위 오류가 추가됩니다.

예시:

argument :user_id, GraphQL::Types::String, required: false
argument :username, GraphQL::Types::String, required: false

validates mutually_exclusive: [:user_id, :username]

정확히 하나의 인수가 필요한 경우 exactly_one_of 유효성 검사기를 사용할 수 있습니다.

예시:

argument :group_path, GraphQL::Types::String, required: false
argument :project_path, GraphQL::Types::String, required: false

validates exactly_one_of: [:group_path, :project_path]

Keywords#

정의된 각 GraphQL argument는 키워드 인수로 mutation의 #resolve 메서드에 전달됩니다.

예시:

def resolve(my_arg:)
  # Perform mutation ...
end

Input Types#

graphql-ruby는 인수를 input type으로 래핑합니다.

예를 들어, mergeRequestSetDraft mutation은 다음 인수를 정의합니다(일부는 상속을 통해):

argument :project_path, GraphQL::Types::ID,
         required: true,
         description: "Project the merge request belongs to."

argument :iid, GraphQL::Types::String,
         required: true,
         description: "IID of the merge request."

argument :draft,
         GraphQL::Types::Boolean,
         required: false,
         description: <<~DESC
           Whether or not to set the merge request as a draft.
         DESC

이 인수들은 지정한 3개의 인수와 clientMutationId를 포함하는 MergeRequestSetDraftInput이라는 input type을 자동으로 생성합니다.

Object identifier arguments#

객체를 식별하는 인수는 다음과 같아야 합니다:

Full path object identifier arguments#

역사적으로 전체 경로 인수의 명명이 일관되지 않았지만, 다음과 같이 인수를 명명하는 것을 권장합니다:

  • 프로젝트 전체 경로에는 project_path

  • 그룹 전체 경로에는 group_path

  • 네임스페이스 전체 경로에는 namespace_path

ciJobTokenScopeRemoveProject mutation의 예시:

argument :project_path, GraphQL::Types::ID,
         required: true,
         description: 'Project the CI job token scope belongs to.'

IID object identifier arguments#

객체의 iid를 상위 project_path 또는 group_path와 조합하여 사용합니다. 예시:

argument :project_path, GraphQL::Types::ID,
         required: true,
         description: 'Project the issue belongs to.'

argument :iid, GraphQL::Types::String,
         required: true,
         description: 'IID of the issue.'

Global ID object identifier arguments#

discussionToggleResolve mutation의 예시:

argument :id, Types::GlobalIDType[Discussion],
         required: true,
         description: 'Global ID of the discussion.'

Global ID 폐기(Deprecate Global IDs)도 참조하세요.

Workhorse-assisted uploads#

파일 콘텐츠를 허용하는 모든 GraphQL API mutation은 Workhorse-assisted uploads를 사용해야 합니다.

구현 세부 정보는 Workhorse uploads 문서를 참조하세요.

Sort arguments#

정렬 인수는 가능한 경우 항상 enum 타입을 사용하여 사용 가능한 정렬 값의 집합을 설명해야 합니다.

enum은 일부 공통 값을 상속받기 위해 Types::SortEnum에서 상속받을 수 있습니다.

enum 값은 {PROPERTY}_{DIRECTION} 형식을 따라야 합니다. 예시:

TITLE_ASC

정렬 enum의 설명 스타일 가이드도 참조하세요.

ContainerRepositoriesResolver의 예시:

# Types::ContainerRegistry::ContainerRepositorySortEnum:
module Types
  module ContainerRegistry
    class ContainerRepositorySortEnum < SortEnum
      graphql_name 'ContainerRepositorySort'
      description 'Values for sorting container repositories'

      value 'NAME_ASC', 'Name by ascending order.', value: :name_asc
      value 'NAME_DESC', 'Name by descending order.', value: :name_desc
    end
  end
end

# Resolvers::ContainerRepositoriesResolver:
argument :sort, Types::ContainerRegistry::ContainerRepositorySortEnum,
          description: 'Sort container repositories by this criteria.',
          required: false,
          default_value: :created_desc

GitLab custom scalars#

Types::TimeType#

Types::TimeType은 Ruby의 TimeDateTime 객체를 다루는 모든 필드와 인수의 타입으로 사용되어야 합니다.

이 타입은 커스텀 스칼라로서:

  • GraphQL 필드의 타입으로 사용될 때 Ruby의 TimeDateTime 객체를 ISO-8601 형식의 표준화된 문자열로 변환합니다.

  • GraphQL 인수의 타입으로 사용될 때 ISO-8601 형식의 시간 문자열을 Ruby Time 객체로 변환합니다.

이를 통해 GraphQL API가 시간을 표현하는 표준화된 방식을 갖출 수 있습니다

시간 입력을 처리합니다.

예시:

field :created_at, Types::TimeType, null: true, description: 'Timestamp of when the issue was created.'

Global ID 스칼라#

모든 Global ID는 커스텀 스칼라입니다. 이 스칼라들은 추상 스칼라 클래스 Types::GlobalIDType으로부터 동적으로 생성됩니다.

테스트#

쿼리 또는 뮤테이션이 올바르게 실행되고 해석되는지 완전히 검증할 수 있는 것은 통합 테스트뿐입니다.

단위 테스트는 타입에 특정 필드가 있거나 뮤테이션에 특정 필수 인수가 있는지와 같이 스키마의 특정 측면을 정적으로 검증하는 경우에만 사용하세요. 필드나 인수를 정적으로 검증하는 것 이상으로 리졸버를 단위 테스트하지 마세요.

그 외 모든 테스트에는 통합 테스트를 사용하세요.

통합 테스트 작성#

통합 테스트는 GraphQL 쿼리 또는 뮤테이션의 전체 스택을 검사하며 spec/requests/api/graphql에 저장됩니다.

모든 실행 단계를 완전히 테스트하기 위해 통합 테스트를 사용합니다. 완전한 요청 통합 테스트만이 다음 사항을 검증할 수 있습니다:

  • 뮤테이션이 실제로 스키마에서 쿼리 가능한지(MutationType에 마운트되었는지).

  • 리졸버 또는 뮤테이션이 반환하는 데이터가 필드의 반환 타입과 올바르게 일치하며 오류 없이 해석되는지.

  • 인수가 입력 시 올바르게 강제 변환되고, 필드가 출력 시 올바르게 직렬화되는지.

  • 인수 전처리가 올바르게 동작하는지.

  • 인수 또는 스칼라의 유효성 검사가 올바르게 적용되는지.

  • 인수의 default_value가 올바르게 적용되는지.

  • 리졸버 또는 뮤테이션의 #ready? 메서드의 로직이 올바르게 적용되는지.

  • 객체가 성공적으로 해석되고 N+1 문제가 없는지.

쿼리를 추가할 때 a working graphql query that returns dataa working graphql query that returns no data 공유 예제를 사용하여 쿼리가 유효한 결과를 렌더링하는지 테스트할 수 있습니다.

GraphQL 통합을 수행하려면 post_graphql 헬퍼를 사용하세요.

예시:

# Good:
gql_query = %q(some query text...)
post_graphql(gql_query, current_user: current_user)
# or:
GitlabSchema.execute(gql_query, context: { current_user: current_user })

# Deprecated: avoid
resolve(described_class, obj: project, ctx: { current_user: current_user })

GraphqlHelpers#all_graphql_fields_for 헬퍼를 사용하여 사용 가능한 모든 필드를 포함하는 쿼리를 구성할 수 있습니다. 이를 통해 쿼리의 가능한 모든 필드를 렌더링하는 테스트를 더 간편하게 추가할 수 있습니다.

페이지네이션 및 정렬을 지원하는 쿼리에 필드를 추가하는 경우, 자세한 내용은 테스트를 참고하세요.

GraphQL 뮤테이션 요청을 테스트하기 위해 GraphqlHelpers는 두 가지 헬퍼를 제공합니다: graphql_mutation은 뮤테이션 이름과 뮤테이션의 입력값이 담긴 해시를 받습니다. 이 헬퍼는 뮤테이션 쿼리와 준비된 변수가 포함된 구조체를 반환합니다.

이 구조체를 post_graphql_mutation 헬퍼에 전달하면 GraphQL 클라이언트처럼 올바른 파라미터와 함께 요청을 포스트합니다.

뮤테이션의 응답에 접근하려면 graphql_mutation_response 헬퍼를 사용할 수 있습니다.

이러한 헬퍼를 사용하여 다음과 같이 스펙을 작성할 수 있습니다:

let(:mutation) do
  graphql_mutation(
    :merge_request_set_wip,
    project_path: 'gitlab-org/gitlab-foss',
    iid: '1',
    wip: true
  )
end

it 'returns a successful response' do
   post_graphql_mutation(mutation, current_user: user)

   expect(response).to have_gitlab_http_status(:success)
   expect(graphql_mutation_response(:merge_request_set_wip)['errors']).to be_empty
end

테스트 팁과 트릭#

GraphqlHelpers 지원 모듈의 메서드에 익숙해지세요. 이 메서드 중 다수는 GraphQL 테스트 작성을 더 쉽게 만들어 줍니다.

GraphqlHelpers#graphql_data_atGraphqlHelpers#graphql_dig_at과 같은 순회 헬퍼를 사용하여 결과 필드에 접근하세요. 예시:

result = GitlabSchema.execute(query)

mr_iid = graphql_dig_at(result.to_h, :data, :project, :merge_request, :iid)

결과에 대해 매칭할 때는 GraphqlHelpers#a_graphql_entity_for를 사용하세요. 예를 들어:

post_graphql(some_query)

# checks that it is a hash containing { id => global_id_of(issue) }
expect(graphql_data_at(:project, :issues, :nodes))
  .to contain_exactly(a_graphql_entity_for(issue))

# Additional fields can be passed, either as names of methods, or with values
expect(graphql_data_at(:project, :issues, :nodes))
  .to contain_exactly(a_graphql_entity_for(issue, :iid, :title, created_at: some_time))

빈 스키마를 직접 만드는 대신 GraphqlHelpers#empty_schema를 사용하여 빈 스키마를 생성하세요. 예를 들어:

# good
let(:schema) { empty_schema }

# bad
let(:query_type) { GraphQL::ObjectType.new }
let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}

double('query', schema: nil) 대신 GraphqlHelpers#query_double(schema: nil)을 사용하세요. 예를 들어:

# good
let(:query) { query_double(schema: GitlabSchema) }

# bad
let(:query) { double('Query', schema: GitlabSchema) }

프론트엔드에서 사용하는 쿼리를 테스트할 때는 GraphqlHelpers#get_graphql_query_as_string을 사용하세요. 예를 들어:

let(:query) { get_graphql_query_as_string('work_items/graphql/project_work_items.query.graphql') }
let(:variables) { { 'fullPath' => project.full_path } }

...

post_graphql(query, variables: variables)

거짓 양성(false positive)을 피하세요:

post_graphqlcurrent_user: 인자로 사용자를 인증하면, 동일한 사용자에 대한 이후 요청보다 첫 번째 요청에서 더 많은 쿼리가 생성됩니다. QueryRecorder를 사용하여 N+1 쿼리를 테스트할 경우, 각 요청마다 다른 사용자를 사용하세요.

아래 예시는 N+1 쿼리를 방지하는 테스트가 어떤 형태여야 하는지 보여줍니다:

RSpec.describe 'Query.project(fullPath).pipelines' do
  include GraphqlHelpers

  let(:project) { create(:project) }

  let(:query) do
    %(
      {
        project(fullPath: "#{project.full_path}") {
          pipelines {
            nodes {
              id
            }
          }
        }
      }
    )
  end

  it 'avoids N+1 queries' do
    first_user = create(:user)
    second_user = create(:user)
    create(:ci_pipeline, project: project)

    control_count = ActiveRecord::QueryRecorder.new do
      post_graphql(query, current_user: first_user)
    end

    create(:ci_pipeline, project: project)

    expect do
      post_graphql(query, current_user: second_user)  # use a different user to avoid a false positive from authentication queries
    end.not_to exceed_query_limit(control_count)
  end
end

app/graphql/types의 폴더 구조를 그대로 따르세요:

예를 들어, app/graphql/types/ci/pipeline_type.rb에 있는 Types::Ci::PipelineType의 필드에 대한 테스트는, 파이프라인 데이터를 가져오는 데 사용되는 쿼리에 관계없이 spec/requests/api/graphql/ci/pipeline_spec.rb에 저장해야 합니다.

단위 테스트 작성#

단위 테스트는 스키마를 정적으로 검증하는 용도로만 사용하세요. 예를 들어 다음 사항을 확인하는 데 활용합니다:

  • 타입, 뮤테이션, 또는 리졸버에 특정 이름의 필드가 있는지

  • 타입, 뮤테이션, 또는 리졸버에 특정 이름의 authorize 권한이 있는지 (단, 인가 여부는 통합 테스트를 통해 검증하세요)

  • 뮤테이션 또는 리졸버에 특정 이름의 인자가 있는지, 그리고 해당 인자가 필수인지 여부

정적 스키마 테스트 외에는 리졸버가 어떻게 resolve하거나 인가를 적용하는지를 단위 테스트로 검증하지 마세요. 대신 통합 테스트를 사용하여 전체 실행 단계를 테스트하세요.

쿼리 흐름 및 GraphQL 인프라에 관한 참고 사항#

GitLab GraphQL 인프라는 lib/gitlab/graphql에서 찾을 수 있습니다.

Instrumentation은 쿼리 실행 전후를 감싸는 기능입니다. Instrumentation 클래스를 사용하는 모듈로 구현됩니다.

예시: Present

module Gitlab
  module Graphql
    module Present
      #... some code above...

      def self.use(schema_definition)
        schema_definition.instrument(:field, ::Gitlab::Graphql::Present::Instrumentation.new)
      end
    end
  end
end

Query Analyzer는 쿼리가 실행되기 전에 이를 검증하는 일련의 콜백을 포함합니다. 각 필드는 analyzer를 통과할 수 있으며, 최종 값도 사용할 수 있습니다.

Multiplex 쿼리는 단일 요청에 여러 쿼리를 담아 보낼 수 있게 해줍니다. 이를 통해 서버에 전송하는 요청 수를 줄일 수 있습니다. (GraphQL Ruby에서는 커스텀 Multiplex Query Analyzer와 Multiplex Instrumentation을 제공합니다.)

쿼리 제한#

과도하거나 악의적인 쿼리로부터 서버 리소스를 보호하기 위해, 쿼리와 뮤테이션은 깊이(depth), 복잡도(complexity), 재귀(recursion)에 의해 제한됩니다. 이 값은 기본값으로 설정할 수 있으며, 필요에 따라 특정 쿼리에서 재정의할 수 있습니다. 복잡도 값은 객체별로 설정할 수도 있으며, 최종 쿼리 복잡도는 반환되는 객체 수를 기반으로 평가됩니다. 이는 비용이 큰 객체(예: Gitaly 호출이 필요한 경우)에 활용할 수 있습니다.

예를 들어, 리졸버에서 조건부 복잡도 메서드를 사용하는 경우:

def self.resolver_complexity(args, child_complexity:)
  complexity = super
  complexity += 2 if args[:label_name]

  complexity
end

복잡도에 대한 자세한 내용은 GraphQL Ruby 문서를 참고하세요.

문서 및 스키마#

스키마는 app/graphql/gitlab_schema.rb에 위치합니다. 자세한 내용은 스키마 레퍼런스를 참고하세요.

스키마가 변경될 때마다 생성된 GraphQL 문서를 업데이트해야 합니다. GraphQL 문서와 스키마 파일 생성 방법에 대한 자세한 내용은 스키마 문서 업데이트를 참고하세요.

독자를 돕기 위해 GraphQL API 문서에 새 페이지를 추가하는 것이 좋습니다. 가이드는 GraphQL API 페이지를 참고하세요.

변경 로그 항목 포함#

모든 클라이언트 대면 변경 사항에는 반드시 변경 로그 항목을 포함해야 합니다.

지연 평가 (Laziness)#

GraphQL에서 성능을 관리하는 데 특화된 중요한 기법 중 하나는 지연(lazy) 값을 사용하는 것입니다. 지연 값은 결과에 대한 약속을 나타내며, 해당 작업을 나중에 실행할 수 있게 해줌으로써 쿼리 트리의 여러 부분에서 쿼리를 일괄 처리할 수 있습니다. 코드에서 지연 값의 주요 예시는 GraphQL BatchLoader입니다.

지연 값을 직접 관리하려면 Gitlab::Graphql::Lazy를 참고하세요. 특히 Gitlab::Graphql::Laziness를 확인하세요. 이 모듈은 #force#delay를 포함하며, 필요에 따라 지연 값의 생성과 제거라는 기본 연산을 구현하는 데 도움을 줍니다.

지연 값을 강제 실행하지 않고 처리하려면 Gitlab::Graphql::Lazy.with_value를 사용하세요.