실시간 보기 컴포넌트 빌드 및 배포

GitLab은 사용자 입력을 받아들이고 상태 변경을 사용자에게 다시 반영하는 개별 보기 컴포넌트를 통해 대화형 사용자 경험을 제공합니다. 예를 들어, 병합 요청 페이지에서 사용자는 승인, 의견 남기기, CI/CD 파이프라인과 상호 작용 등을 할 수 있습니다.

그러나 GitLab은 종종 상태 업데이트를 제때에 반영하지 않습니다. 이는 페이지 일부가 사용자가 페이지를 다시로드한 후에만 업데이트되는 오래된 데이터를 표시한다는 것을 의미합니다.

이를 해결하기 위해 GitLab은 WebSocket을 통해 실시간으로 상태 업데이트를 받을 수 있도록 보기 컴포넌트에 기술 및 프로그래밍 API를 도입했습니다.

다음 설명서에서는 GitLab Ruby on Rails 서버로부터 실시간으로 업데이트를 받는 보기 컴포넌트를 빌드하고 배포하는 방법에 대해 안내합니다.

참고: Action Cable과 GraphQL 구독은 아직 진행 중이며 활발히 개발 중입니다. 개발자들은 자신의 사용 사례를 평가하여 이것이 올바른 도구인지 확인해야 합니다. 확신이 없다면, #f_real-time 내부 Slack 채널에서 도움을 요청하세요.

실시간 보기 컴포넌트 빌드

필수 사항:

다음을 읽으세요:

GitLab에서 실시간 보기 컴포넌트를 빌드하려면 다음을 수행해야 합니다:

  • GitLab 프론트엔드에서 Apollo 구독과 Vue 컴포넌트를 통합합니다.
  • GitLab Ruby on Rails 백엔드에서 GraphQL 구독을 추가하고 트리거합니다.

Vue 컴포넌트를 Apollo 구독과 통합

참고: 현재 실시간 스택은 클라이언트 코드가 Vue를 렌더링 레이어로 사용하고 Apollo를 상태 및 네트워킹 레이어로 사용한다고 가정합니다. 아직 Vue + Apollo로 마이그레이션되지 않은 GitLab 프론트엔드와 작업 중이라면, 그 작업을 먼저 완료하십시오.

가정 IssueView Vue 컴포넌트는 GitLab Issue 데이터를 관찰하고 렌더링하는 가상의 예측상 상황입니다. 여기서는 단순함을 위해 이 컴포넌트가 문제의 제목과 설명만 렌더링한다고 가정합니다:

import issueQuery from '~/issues/queries/issue_view.query.graphql';

export default {
  props: {
    issueId: {
      type: Number,
      required: false,
      default: null,
    },
  },
  apollo: {
    // Apollo 쿼리 오브젝트의 이름입니다. `data`로 연결된 필드 이름과 일치해야 합니다.
    issue: {
      // 초기 가져오기에 사용되는 쿼리입니다.
      query: issueQuery,
      // 초기 가져오기 쿼리에 사용되는 인수를 바인딩합니다.
      variables() {
        return {
          iid: this.issueId,
        };
      },
      // 응답 데이터를 보기 속성으로 매핑합니다.
      update(data) {
        return data.project?.issue || {};
      },
    },
  },
  // 반응적인 Vue 컴포넌트 데이터입니다. Apollo는 쿼리가 반환되거나 구독이 발생할 때 이를 업데이트합니다.
  data() {
    return {
      issue: {}, // 뷰 로딩 중에 초기 상태를 반환하는 것이 좋은 관행입니다.
    };
  },
};

// <template> 코드는 설명의 목적에서 중요하지 않으므로 생략합니다.

쿼리:

  • app/assets/javascripts/issues/queries/issue_view.query.graqhql에 정의되어야 합니다.
  • 다음과 같은 GraphQL 작업을 포함해야 합니다:

    query gitlabIssue($iid: String!) {
      # 이 예제에서만 설명을 위해 경로를 고정하였습니다. 실제로는 이렇게 하면 안됩니다.
      project(fullPath: "gitlab-org/gitlab") {
        issue(iid: $iid) {
          title
          description
        }
      }
    }
    

지금까지 이 뷰 컴포넌트는 자신을 데이터로 채우는 초기 가져오기 쿼리만 정의했습니다. 이는 뷰가 서버에서의 이후 업데이트를 만들어내지 않는 보통의 GraphQL query 작업으로, 뷰에 의해 시작된 HTTP POST 요청으로 전송됩니다. 서버 상의 모든 후속 업데이트는 이 뷰를 오래된 상태로 만듭니다. 서버로부터 업데이트를 받으려면 다음을 수행해야 합니다:

  1. GraphQL 구독 정의를 추가합니다.
  2. Apollo 구독 훅을 정의합니다.

GraphQL 구독 정의 추가

구독은 GraphQL 쿼리를 정의하지만, 이는 GraphQL subscription 작업 내부에 래핑됩니다. 이 쿼리는 백엔드에서 시작되며 해당 결과는 WebSocket을 통해 뷰 컴포넌트로 푸시됩니다.

초기 가져오기 쿼리와 마찬가지로 다음이 필요합니다:

  • app/assets/javascripts/issues/queries/issue_updated.subscription.graqhql에 정의합니다.
  • 파일에 다음과 같은 GraphQL 작업을 포함합니다:

    subscription issueUpdatedSubscription($iid: String!) {
      issueUpdated($issueId: IssueID!) {
        issue(issueId: $issueId) {
          title
          description
        }
      }
    

새로운 구독을 추가할 때 다음 명명 지침을 사용합니다:

  • 구독 작업 이름을 항상 Subscription 또는 GitLab EE 전용이면 SubscriptionEE로 끝냅니다. 예: issueUpdatedSubscription 또는 issueUpdatedSubscriptionEE.
  • 이벤트 이름에 “발생한” 동사를 사용합니다. 예: issueUpdated.

구독 정의는 보통의 쿼리와 비슷해 보이지만, 이해해야 할 몇 가지 중요한 차이점이 있습니다:

  • 쿼리:
    • 프론트엔드에서 발생합니다.
    • URL에서 엔티티가 일반적으로 참조되는 방식인 내부 ID(iid, 숫자)를 사용합니다. 이 내부 ID는 포함하는 네임스페이스(이 예제에서는 project)에 상대적이므로 fullPath 아래에 쿼리를 중첩해야 합니다.
  • 구독:
    • 서버로부터 이후 업데이트를 받기 위해 백엔드로부터의 요청입니다.
    • 다음을 포함합니다:
      • 구독 자체를 설명하는 작업 이름 (issueUpdatedSubscription이 이 예제에서의 예시).
      • 중첩된 이벤트 쿼리(issueUpdated 이 예제에서의 예시). 중첩된 이벤트 쿼리:
        • 같은 이름의 GraphQL 트리거 실행시에 실행되며, 구독에서 사용된 이벤트 이름은 백엔드에서 사용된 트리거 필드와 일치해야 합니다.
        • GraphQL에서 리소스를 식별하는 기본 방법인 숫자형 내부 ID 대신에 전역 ID 문자열을 사용합니다. 자세한 정보는 GraphQL 전역 ID를 참조하세요.

Apollo 구독 후킹 정의

구독을 정의한 후 아폴로의 subscribeToMore 속성을 사용하여 보기 구성 요소에 추가하십시오.

import issueQuery from '~/issues/queries/issue_view.query.graqhql';
import issueUpdatedSubscription from '~/issues/queries/issue_updated.subscription.graqhql';

export default {
  // 이전과 동일합니다.
  // ...
  apollo: {
    issue: {
      // 이전과 동일합니다.
      // ...
      // 이 Apollo 후크는 실시간 푸시를 가능하게 합니다.
      subscribeToMore: {
        // 미래의 업데이트를 반환하는 구독 동작.
        document: issueUpdatedSubscription,
        // 구독 동작에 사용되는 인수를 바인딩합니다.
        variables() {
          return {
            iid: this.issueId,
          };
        },
        // 구독을 비활성화해야 하는 경우 true|false를 반환합니다.
        // 기능 플래그를 사용하는 경우 유용합니다.
        skip() {
          return this.shouldSkipRealTimeUpdates;
        },
      },
    },
  },
  // 이전과 동일합니다.
  // ...
  computed: {
    shouldSkipRealTimeUpdates() {
      return false; // 여기서 기능 플래그를 확인할 수 있습니다.
    },
  },
};

이제 아폴로를 통해 WebSocket 연결을 통해 보기 구성 요소를 업데이트할 수 있습니다. 다음으로, 백엔드에서 이벤트가 어떻게 트리거되어 프론트엔드로 푸시되는지에 대해 알아봅니다.

GraphQL 구독 트리거

WebSocket에서 업데이트를 받을 수 있는 보기 구성 요소를 작성하는 것은 이야기의 반쪽에 불과합니다. GitLab Rails 애플리케이션에서 다음 단계를 수행해야 합니다.

  1. GraphQL::Schema::Subscription 클래스를 구현합니다. 이 클래스는 다음을 정의합니다:
    • graphql-ruby가 프론트엔드에서 보낸 subscription 동작을 해결하는 데 사용됩니다.
    • 구독이 취하는 인수 및 호출자에게 반환하는 페이로드를 정의합니다(있는 경우).
    • 호출자가이 구독을 생성할 수 있는지 확인하려면 필요한 비즈니스 로직을 실행합니다.
  2. Types::SubscriptionType 클래스에 새로운 field를 추가합니다. 이 필드는 Vue 구성 요소를 통합할 때 사용되는 이벤트 이름을 GraphQL::Schema::Subscription 클래스에 매핑합니다.
  3. 이벤트 이름과 일치하는 GraphqlTriggers에 해당하는 GraphQL 트리거를 실행하는 메서드를 추가합니다.
  4. 도메인 로직으로의 일환으로 새 트리거를 실행하기 위해 서비스나 Active Record 모델 클래스를 사용합니다.

구독 구현

이미 GraphQL::Schema::Subscription로 구현된 이벤트에 구독하는 경우 이 단계는 선택 사항입니다. 그렇지 않은 경우, app/graphql/subscriptions/ 하위에 새로운 구독을 구현하는 클래스를 만듭니다. Issue가 업데이트되면 발생하는 issueUpdated 이벤트를 위한 예제에서 구독 구현은 다음과 같습니다:

module Subscriptions
  class IssueUpdated < BaseSubscription
    include Gitlab::Graphql::Laziness

    payload_type Types::IssueType

    argument :issue_id, Types::GlobalIDType[Issue],
              required: true,
              description: '이슈의 ID.'

    def authorized?(issue_id:)
      issue = force(GitlabSchema.find_by_gid(issue_id))

      unauthorized! unless issue && Ability.allowed?(current_user, :read_issue, issue)

      true
    end
  end
end

이 새 클래스를 만들 때:

  • 모든 구독 유형이 Subscriptions::BaseSubscription에서 상속되는지 확인합니다.
  • 구독된 쿼리가 액세스할 수 있는 데이터를 나타내는 적절한 payload_type을 사용하거나 노출하려는 개별 fields을 정의합니다.
  • 각 클라이언트가 구독 또는 이벤트가 발생할 때마다 호출되는 사용자 정의 subscribeupdate 훅을 정의할 수 있습니다. 이러한 메서드를 사용하는 방법에 대해서는 공식 문서를 참조하십시오.
  • 필요한 권한 확인을 수행하는 authorized?을 구현합니다. 이러한 확인은 subscribe 또는 update를 호출할 때마다 실행됩니다.

GraphQL 구독 클래스에 대해 자세히 알아보려면 공식 문서를 참조하십시오.

구독 연결

새 구독 클래스를 구현하면 해당 클래스를 실행하기 전에 SubscriptionTypefield에 해당 클래스를 매핑해야 합니다. Types::SubscriptionType 클래스를 열고 새로운 필드를 추가합니다:

module Types
  class SubscriptionType < ::Types::BaseObject
    graphql_name 'Subscription'

    # 기존 필드
    # ...

    field :issue_updated,
      subscription: Subscriptions::IssueUpdated, null: true,
      description: '이슈가 업데이트될 때 트리거됩니다.'
  end
end

참고: EE 구독을 연결 중이면 EE::Types::SubscriptionType을 업데이트합니다.

issue_updated 인수가 프론트엔드에서 camel-case(issueUpdated)로 보낸 subscription 요청과 일치하는지 확인하십시오. 그렇지 않으면 graphql-ruby가 어떤 구독자에게 통지해야 하는지 알 수 없습니다. 이제 이벤트를 트리거할 수 있습니다.

새로운 트리거 추가하기

기존 트리거를 재사용할 수 있다면 이 단계를 건너뛰세요.

GitlabSchema.subscriptions.trigger 주변에 façade를 사용하여 이벤트를 트리거하는 것을 더 간단하게 만들기 위해 사용합니다. GraphqlTriggers에 새로운 트리거를 추가하세요:

module GraphqlTriggers
  # 기존 트리거
  # ...

  def self.issue_updated(issue)
    GitlabSchema.subscriptions.trigger(:issue_updated, { issue_id: issue.to_gid }, issue)
  end
end

참고: 만약 트리거가 EE 구독을 위한 것이라면, 대신 EE::GraphqlTriggers를 업데이트하세요.

  • 첫 번째 argument인 :issue_updated는 이전 단계에서 사용된 field 이름과 일치해야 합니다.
  • argument 해시는 이벤트를 발행해야 하는 이슈를 지정합니다. GraphQL은 이 해시를 사용하여 발행할 주제를 식별합니다.

마지막 단계는 이 트리거 함수를 호출하는 것입니다.

트리거 실행하기

이 단계의 구현은 구체적으로 무엇을 구축하고 있는지에 따라 달라집니다. 예를 들어, 이슈의 필드가 변경된 경우, Issues::UpdateService를 확장하여 GraphqlTriggers.issue_updated를 호출할 수 있습니다.

실시간 뷰 구성요소는 이제 기능을 하고 있습니다. 이제 이슈의 업데이트는 GitLab UI로 즉시 전파되어야 합니다.

실시간 뷰 구성요소 배포

웹소켓은 GitLab에서 상대적으로 새로운 기술이며, 그것을 확장하는 것은 일부 도전을 야기합니다. 그 이유로, 새로운 기능은 아래의 지침을 사용하여 롤아웃되어야 합니다.

실시간 구성요소 배포

웹소켓을 통한 업데이트는 적절한 백엔드 코드 없이 시뮬레이션하기 어렵기 때문에 프론트엔드와 백엔드를 동시에 작업할 수 있습니다.

그러나 변경 사항을 별도의 MR(병합 요청)로 보내고 먼저 백엔드 변경 사항을 배포하는 것이 더 안전합니다. 이렇게 하면 프론트엔드가 이벤트를 구독하기 시작할 때 백엔드가 이미 이를 처리할 준비가 되어 있습니다.

기존 WebSocket 연결 재사용

기존 연결을 재사용하는 기능은 최소한의 위험을 야기합니다. 자체 호스팅 고객에게 더 많은 제어를 제공하기 위해 기능 플래그 롤아웃이 권장됩니다. 그러나 GitLab.com에 대해서는 백분율로 롤아웃할 필요가 없으며 새로운 연결을 추정할 필요가 없습니다.

새로운 WebSocket 연결 도입

GitLab 애플리케이션 일부에 WebSocket 연결을 도입하는 모든 변경은 오픈 연결을 유지하는 노드 및 Redis와 주 데이터베이스 같은 다운스트림 서비스에 대한 확장 가능성 위험을 야기합니다.

최대 연결 추정

GitLab.com에서 완전히 활성화된 첫 번째 실시간 기능은 실시간 담당자 지정이었습니다. 이슈 페이지에 대한 최대 처리량을 최대 동시 WebSocket 연결과 비교하여 약 1초당 1개 요청 당 약 4200개의 WebSocket 연결이 추가된다고 대략적으로 추정할 수 있습니다.

새로운 기능이 어떤 영향을 미칠지 이해하기 위해, 해당 페이지의 최대 처리량(RPS)(n)을 합산하고 다음 수식을 적용하세요:

(n * 4200) / peak_active_connections

현재 활성 연결은 이 Grafana 차트에서 확인할 수 있습니다.

이 계산은 대략적이며, 새로운 기능이 배포됨에 따라 재검토되어야 합니다. 기존 용량의 일정 비율로 지원해야 할 용량을 대략적으로 제공합니다.

점진적 롤아웃

현재 포화 상태와 필요한 새로운 연결의 비율에 따라 변경에 필요한 새로운 용량을 제공해야 할 수 있습니다. 대부분의 경우 Kubernetes가 이를 상대적으로 쉽게 만들지만, 여전히 하위 서비스에 대한 위험이 남아 있습니다.

이를 완화하기 위해 새로운 WebSocket 연결을 설정하는 코드가 기능 플래그로 설정되고 기본값이 off로 설정되어 있는지 확인하세요. 기능 플래그를 주의하여 백분율로 기반한 기능 플래그 롤아웃을 신중하게 진행하면 변경 사항을 WebSocket 대시보드에서 관찰할 수 있습니다.

  1. 기능 플래그 롤아웃이슈를 생성하세요.
  2. 예상되는 새로운 연결을 우리가 무엇을 예상하고 있는지 섹션에 추가하세요.
  3. Plan 및 Scalability 팀의 구성원을 복사하여 백분율 기반 롤아웃 계획을 추정해 달라고 요청하세요.

역호환성

기능 플래그 롤아웃 기간 동안 및 그 이후에 실시간 기능은 역호환될 수 있거나 적어도 gracefully하게 감소되어야 합니다. 모든 고객이 Action Cable을 활성화하지 않은 것으로, Action Cable이 기본적으로 활성화되기 전에 추가 작업이 필요합니다.

실시간을 요구사항으로 만드는 것은 파괴적인 변경을 나타내므로, 이를 수행할 수 있는 다음 기회는 15.0 버전입니다.

GitLab.com의 실시간 인프라

GitLab.com에서 WebSocket 연결은 별도의 인프라에서 제공되며 일반적인 웹 플리트와는 완전히 분리되어 있으며 Kubernetes로 배포됩니다. 이는 요청을 처리하는 노드에 대한 위험을 제한하지만 공유 서비스에는 위험을 제한하지 않습니다. WebSocket Kubernetes 배포에 대한 자세한 내용은 이 epic을 참조하세요.

GitLab 실시간 스택 심층 분석

서버에서 시작된 푸시는 네트워크를 통해 전파되어 사용자 상호 작용없이 클라이언트에서 보기 업데이트를 트리거해야 하므로, 실시간 기능은 프론트엔드 및 백엔드를 포함한 전체 스택을 살펴봄으로써만 이해할 수 있습니다.

참고: 역사적 이유로 클라이언트가 변경 사항에 대한 폴링 및 서비스 업데이트를 요청하는 컨트롤러 경로를 realtime_changes라고 부릅니다. 이들은 조건부 GET 요청을 사용하며, 본 안내서에서 다루는 실시간 동작과는 무관합니다.

클라이언트로 푸시된 실시간 업데이트는 GitLab 레일 응용 프로그램에서 발생합니다. 우리는 다음 기술을 사용하여 이러한 업데이트를 초기화하고 처리합니다.

GitLab Rails 백엔드:

  • Redis PubSub을 사용하여 구독 상태 처리
  • WebSocket 연결 및 데이터 전송을 처리하기 위해 Action Cable을 사용
  • GraphQL 구독 및 트리거를 구현하기 위해 graphql-ruby 사용

GitLab 프론트엔드:

  • GraphQL 요청, 라우팅 및 캐싱을 처리하기 위해 Apollo Client를 사용
  • 실시간으로 업데이트되는 보기 구성 요소를 정의하고 렌더링하기 위해 Vue.js 사용

다음 그림은 이러한 레이어 간 데이터가 전파되는 방식을 설명합니다.

sequenceDiagram participant V as Vue Component participant AP as Apollo Client participant P as Rails/GraphQL participant AC as Action Cable/GraphQL participant R as Redis PubSub AP-->>V: 주입됨 AP->>P: HTTP GET /-/cable AC-->>P: TCP 연결 탈취 AC->>+R: SUBSCRIBE(클라이언트) R-->>-AC: 채널 구독 AC-->>AP: HTTP 101: 프로토콜 전환 par V->>AP: 쿼리(gql) Note over AP,P: 이 보기의 초기 데이터 검색 AP->>+P: HTTP POST /api/graphql (초기 쿼리) P-->>-AP: 초기 쿼리 응답 AP->>AP: 캐시 및/또는 응답 변환 AP->>V: 업데이트 트리거 V->>V: 다시 렌더링 and Note over AP,AC: 이 보기에 대한 후속 업데이트 구독 V->>AP: subscribeToMore(event, gql) AP->>+AC: WS: subscribe(event, query) AC->>+R: SUBSCRIBE(event) R-->>-AC: 이벤트 구독 AC-->>-AP: confirm_subscription end Note over V,R: 시간 흐름 P->>+AC: 이벤트 트리거 AC->>+R: PUBLISH(event) R-->>-AC: 구독 loop 각 구독자에 대해 AC->>AC: GQL 쿼리 실행 AC->>+R: PUBLISH(클라이언트, 쿼리 결과) R-->>-AC: 콜백 AC->>-AP: WS: 쿼리 결과 전송 end AP->>AP: 캐시 및/또는 응답 변환 AP->>V: 업데이트 트리거 V->>V: 다시 렌더링

다음 섹션에서는이 스택의 각 요소를 자세히 설명합니다.

Action Cable 및 웹 소켓

Action Cable은 Ruby on Rails에 WebSocket 지원을 추가하는 라이브러리입니다. 웹 소켓은 기존의 HTTP 기반 서버 및 애플리케이션을 양방향 통신이 가능한 단일 TCP 연결을 통해 향상시키기 위한 HTTP 친화적인 솔루션으로 개발되었습니다. 클라이언트는 먼저 일반적인 HTTP 요청을 서버에 보내어 연결을 WebSocket으로 업그레이드하도록 요청합니다. 성공 시 동일한 TCP 연결은 클라이언트 및 서버가 양쪽으로 데이터를 보내고 수신하는 데 사용될 수 있습니다.

WebSocket 프로토콜은 전송된 데이터가 어떻게 인코딩되거나 구조화되는지를 규정하지 않기 때문에, Action Cable과 같은 라이브러리가 이런 문제들을 처리하도록 필요합니다. Action Cable은 다음을 수행합니다.

  • HTTP에서 WebSocket 프로토콜로의 초기 연결 업그레이드를 처리합니다. 그런 다음 ws:// scheme을 사용한 후속 요청은 Action Cable 서버가 처리하며 Action Pack이 아닙니다.
  • WebSocket을 통해 전송된 데이터가 어떻게 인코딩되는지 정의합니다. Action Cable은 이를 JSON으로 지정합니다. 이로써 응용 프로그램은 데이터를 Ruby Hash로 제공하고 Action Cable은 JSON으로부터 (데)시리얼라이즈합니다.
  • 클라이언트 연결 또는 분리 및 클라이언트 인증을 처리하기 위한 콜백 후크를 제공합니다.
  • 발행/구독 및 원격 프로시저 호출을 구현하기 위한 개발자 추상화로 ActionCable::Channel을 제공합니다.

Action Cable은 ActionCable::Channel에 어떤 클라이언트가 어떤 ActionCable::Channel에 구독되었는지 추적하기 위한 다른 구현을 지원합니다. GitLab에서는 Redis 어댑터를 사용하는데, 이는 분산 메시지 버스로 Redis PubSub 채널을 사용합니다. 다른 Puma 인스턴스에서 서로 다른 클라이언트가 동일한 Action Cable 채널에 연결할 수 있기 때문에 공유 스토리지가 필요합니다.

참고: Action Cable 채널을 Redis PubSub 채널과 혼동해서는 안 됩니다. Action Cable의 Channel 객체는 WebSocket 연결을 통해 전송되는 다양한 종류의 데이터를 분류하고 처리하기 위한 프로그래밍 추상화입니다. Action Cable에서 기밀 PubSub 채널은 브로드캐스팅으로 참조되며 클라이언트와 브로드캐스팅 간의 관계를 구독이라고 합니다. 특히, 각 Action Cable Channel에 여러 브로드캐스팅 (PubSub 채널) 및 구독이 있을 수 있습니다.

Action Cable은 Channel API를 통해 다양한 종류의 동작을 표현하고 어떤 Channel에 대한 업데이트도 동일한 WebSocket 연결을 사용할 수 있기 때문에 각 GitLab 페이지에 대해 실시간 동작이 필요한 보기 구성 요소를 향상시키기 위해 하나의 WebSocket 연결만 필요합니다.

GitLab 페이지에서 실시간 업데이트를 구현하기 위해 우리는 개별 Channel 구현을 작성하지 않습니다. 대신, 모든 GitLab 페이지를 통해 푸시 기반 업데이트가 필요한 모든 페이지에 대해 GraphqlChannel을 제공합니다.

GraphQL 구독: Backend

GitLab은 클라이언트가 GraphQL 쿼리를 사용하여 구조화된 데이터를 서버에서 요청할 수 있도록 GraphQL을 지원합니다. GraphQL에 대한 자세한 내용은 GitLab GraphQL 개요를 참조하여 GraphQL을 채택한 이유에 대해 알아보세요. GitLab 백엔드에서의 GraphQL 지원은 graphql-ruby 젬에 의해 제공됩니다.

보통 GraphQL 쿼리는 표준 요청-응답 주기를 따르는 클라이언트 시작 HTTP POST 요청입니다. 실시간 기능을 위해 대신에 GraphQL 구독을 사용합니다. 이는 발행/구독 패턴의 구현입니다. 이 방식에서 클라이언트는 먼저 GraphqlChannel에 구독 요청을 보내는데, 이때 다음과 같은 정보를 포함합니다:

  • 구독 필드의 이름(이벤트 이름).
  • 이 이벤트가 트리거될 때 실행할 GraphQL 쿼리.

이 정보는 서버에서 사용되어 이 이벤트 스트림을 나타내는 주제를 생성합니다. 주제는 구독 인수 및 이벤트 이름에서 파생된 고유한 이름이며, 이 이벤트가 트리거되면 알림을 받아야 하는 모든 구독자를 식별하는 데 사용됩니다. 동일한 주제에 대해 둘 이상의 클라이언트가 구독할 수 있습니다. 예를 들어 issuableAssigneesUpdated:issuableId:<hashed_id>는 주어진 ID를 가진 이슈의 담당자가 변경될 때 업데이트를 받기를 원하는 클라이언트가 구독하는 주제로 사용될 수 있습니다.

백엔드는 “에픽에 이슈가 추가됨” 또는 “이슈에 사용자가 할당됨”과 같은 도메인 이벤트에 대한 응답으로 구독을 트리거합니다. GitLab에서는 서비스 객체 또는 ActiveRecord 모델 객체일 수 있습니다. 트리거는 해당 이벤트 이름과 인수를 사용하여 GitlabSchema.subscriptions.trigger를 호출함으로써 실행되며, 여기서 graphql-ruby는 주제를 파생시킵니다. 그런 다음 해당 주제의 모든 구독자를 찾아 각 구독자의 쿼리를 실행하고 결과를 모든 주제 구독자에게 다시 푸시합니다.

우리가 GraphQL 구독의 기본 전송으로 Action Cable을 사용하기 때문에 주제는 Action Cable 브로드캐스팅으로 구현됩니다. 앞에서 언급했듯이, 이는 Redis PubSub 채널을 나타냅니다. 이는 각 구독자에 대해 두 개의 PubSub 채널이 사용됨을 의미합니다:

  • 각 주제당 하나의 graphql-event:<namespace>:<topic> 채널. 이 채널은 어떤 클라이언트가 어떤 이벤트에 구독되었는지를 추적하는 데 사용되며 모든 가능한 클라이언트 사이에서 공유됩니다. namespace의 사용은 선택적이며 비어 있을 수 있습니다.
  • 각 클라이언트당 하나의 graphql-subscription:<subscription-id> 채널. 이 채널은 쿼리 결과를 해당 클라이언트에 다시 전송하는 데 사용되며 다른 클라이언트 간에 공유될 수 없습니다.

다음 섹션에서는 GitLab 프론트엔드가 실시간 업데이트를 구현하는 데 GraphQL 구독을 사용하는 방법에 대해 설명합니다.

GraphQL 구독: Frontend

GitLab 프론트엔드는 루비가 아니라 JavaScript을 실행하기 때문에 클라이언트에서 서버로 GraphQL 쿼리, 뮤테이션 및 구독을 보내기 위해 다른 GraphQL 구현이 필요합니다. 이를 위해 Apollo를 사용합니다.

Apollo는 JavaScript에서의 GraphQL의 포괄적인 구현이며, apollo-serverapollo-client 및 추가 유틸리티 모듈로 분할됩니다. 우리는 루비 백엔드를 실행하기 때문에 apollo-server 대신 apollo-client를 사용합니다.

이것은 다음을 단순화합니다:

  • 네트워킹, 연결 관리 및 요청 라우팅.
  • 클라이언트 측 상태 관리 및 응답 캐싱.
  • 뷰 구성 요소를 사용하여 GraphQL을 통합하는 데 사용되는 브릿지 모듈.

참고: Apollo Client 문서를 읽을 때, 뷰 렌더링에 React.js가 사용된다고 가정합니다. 그러나 GitLab에서는 React.js를 사용하지 않습니다. 대신에 Apollo를 Vue.js 어댑터를 사용하여 통합합니다.

Apollo는 다음과 같은 함수 및 후크를 제공하여 다음과 같은 방식으로 정의합니다:

  • 뷰에서 쿼리, 뮤테이션 또는 구독을 보내는 방법.
  • 응답을 다루어야 하는지 여부.
  • 응답 데이터를 캐싱해야 하는지 여부.

진입점은 페이지 내의 모든 뷰 구성 요소에서 공유되는 GraphQL 클라이언트 객체인 ApolloClient입니다. 모든 뷰 구성 요소는 이 객체를 내부적으로 사용하여 서버와 통신합니다.

다양한 유형의 요청이 어떻게 라우팅되어야 하는지를 결정하기 위해 Apollo는 ApolloLink 추상화를 사용합니다. 구체적으로, 실시간 서버 구독을 기타 GraphQL 요청에서 분리하는 데 ActionCableLink를 사용합니다. 이는 다음을 수행합니다:

  • Action Cable에 대한 WebSocket 연결을 설정합니다.
  • 서버 푸시를 클라이언트의 Observable 이벤트 스트림에 매핑하여 뷰가 자신을 업데이트하기 위해 구독할 수 있도록 합니다.

Apollo 및 Vue.js에 대한 자세한 내용은 GitLab GraphQL 개발 가이드를 참조하세요.