InfoGrab DocsInfoGrab Docs

웹훅 개발자 가이드

요약

이 페이지는 GitLab 웹훅에 대한 개발자 가이드입니다. 웹훅은 GitLab에서 발생한 이벤트나 변경 사항에 대한 JSON 데이터를 웹훅 수신기로 POST 합니다. 다음은 웹훅이 트리거되어 실행될 때 일어나는 일을 개괄적으로 설명한 것입니다.

이 페이지는 GitLab 웹훅에 대한 개발자 가이드입니다.

웹훅은 GitLab에서 발생한 이벤트나 변경 사항에 대한 JSON 데이터를 웹훅 수신기로 POST 합니다. 웹훅을 사용하면 고객이 API를 폴링하지 않아도 특정 변경이 발생했을 때 알림을 받습니다.

웹훅 흐름#

다음은 웹훅이 트리거되어 실행될 때 일어나는 일을 개괄적으로 설명한 것입니다.

Mermaid 다이어그램 (8줄)
소스 코드 보기
sequenceDiagram
    Web or API node->>+Database: Fetch data for payload
    Database-->>-Web or API node: Build payload
    Note over Web or API node,Database: Webhook triggered
    Web or API node->>Sidekiq: Queue webhook execution
    Sidekiq->>+Remote webhook receiver: POST webhook payload
    Remote webhook receiver-)-Database: Save response in WebHookLog
    Note over Database,Remote webhook receiver: Webhook executed

새 웹훅 추가#

웹훅은 리소스 중심입니다. 예를 들어 "emoji" 웹훅은 이모지가 부여되거나 회수될 때마다 트리거됩니다.

리소스에 웹훅 지원을 추가하는 방법은 다음과 같습니다.

  1. web_hooks 테이블에 새 칼럼을 추가합니다. 새 칼럼의 조건은 다음과 같습니다.

    • 불리언 타입
    • Not null
    • <resource>_events 형식의 이름
    • 기본값 false.

    마이그레이션의 #change 메서드 예시입니다.

    def change
      add_column :web_hooks, :emoji_events, :boolean, null: false, default: false
    end
    
  2. TriggerableHooks.available_triggers에 새 웹훅 지원을 추가합니다.

  3. 해당 웹훅을 프로젝트, 그룹, GitLab 인스턴스 중 어디에서 설정할 수 있게 할지에 따라 ProjectHook, GroupHook, SystemHook의 triggerable_hooks 목록에 추가합니다. 판단 기준은 프로젝트, 그룹 및 시스템 웹훅을 참고합니다.

  4. app/views/shared/web_hooks/_form.html.haml의 웹훅 설정 폼에 새 체크박스에 대한 프론트엔드 지원을 추가합니다.

  5. TestHooks::ProjectService와 TestHooks::SystemService에 새 웹훅 테스트 지원을 추가합니다. TestHooks::GroupService는 ProjectService를 실행 하기만 하므로 갱신할 필요가 없습니다.

  6. 웹훅 페이로드를 정의합니다.

  7. 웹훅을 트리거하도록 GitLab을 갱신합니다.

  8. 웹훅 문서를 추가합니다.

  9. REST API 지원을 추가합니다.

    1. 해당 인수를 지원하도록 API::ProjectHooks, API::GroupHooks, API::SystemHooks를 갱신합니다.
    2. 새 필드를 지원하도록 API::Entities::ProjectHook과 API::Entities::GroupHook을 갱신합니다. (시스템 훅은 일반 API::Entities::Hook을 사용합니다.)
    3. 프로젝트 웹훅, 그룹 웹훅, 시스템 훅의 API 문서를 갱신합니다.

결정: 프로젝트, 그룹 및 시스템 웹훅#

웹훅을 프로젝트, 그룹, GitLab 인스턴스 중 어디에서 설정할 수 있게 할지 판단할 때는 다음을 참고합니다.

  • 특정 수준에 속하는 리소스와 관련된 웹훅은 그 수준에서 설정할 수 있어야 합니다. 예를 들어 이슈 웹훅은 프로젝트에서, 그룹 멤버십 웹훅은 그룹에서, 사용자 로그인 실패 웹훅은 GitLab 인스턴스에서 설정할 수 있습니다.
  • 프로젝트에서 설정할 수 있는 웹훅은 일반적으로 그룹에서도 설정할 수 있게 해야 합니다. 그룹 소유자가 해당 그룹의 프로젝트에서 일어나는 모든 이벤트를 받기 위해 그룹 웹훅을 설정하는 경우가 많고, 프로젝트 웹훅이 트리거될 때 그룹 웹훅도 자동으로 실행 되기 때문입니다.
  • 일반적으로 프로젝트나 그룹(또는 둘 다)에서 설정할 수 있는 웹훅은, 인스턴스 관리자가 이를 수신해야 한다는 명확한 기능 요청이 있을 때만 인스턴스에서 설정할 수 있게 합니다. 현재의 프로젝트 및 그룹 웹훅 상당수는 인스턴스 수준에서 설정할 수 없습니다.

EE 전용 고려 사항#

그룹 웹훅은 Premium 라이선스 기능입니다. 그룹 웹훅 트리거와 관련된 모든 코드, 그리고 그룹에서만 설정할 수 있는 웹훅의 페이로드를 구성하는 코드는 ee/ 디렉터리에 있어야 합니다.

웹훅 트리거#

프로젝트 및 그룹 웹훅 트리거#

프로젝트 및 그룹 웹훅은 프로젝트나 그룹에서 #execute_hooks를 호출해 트리거합니다.

#execute_hooks 메서드에는 다음이 전달됩니다.

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

project.execute_hooks(payload, :emoji_hooks)

단일 프로젝트나 그룹에서 #execute_hooks를 호출하면 트리거가 상위 그룹으로 자동으로 전파되어 상위 그룹도 함께 실행합니다. 이를 통해 그룹은 자신의 하위 그룹이나 프로젝트에서 발생한 이벤트에 대한 웹훅을 받도록 설정할 수 있습니다.

이 메서드를 호출하는 대상에 따라 동작은 다음과 같습니다.

  • 프로젝트에서 호출하면, 해당 프로젝트에 설정된 해당 유형의 웹훅이 실행되는 것에 더해 해당 프로젝트의 그룹과 상위 그룹에 설정된 같은 유형의 웹훅도 함께 실행됩니다. 해당 유형으로 설정된 인스턴스(시스템) 웹훅도 함께 실행됩니다.
  • 그룹에서 호출하면, 해당 그룹에 설정된 해당 유형의 웹훅이 실행되는 것에 더해 그 그룹의 상위 그룹에 설정된 웹훅도 함께 실행됩니다.

페이로드 구성은 대개 데이터베이스에서 더 많은 레코드를 읽어야 하므로 비용이 큽니다. 따라서 웹훅을 트리거하기 전에 프로젝트에서 #has_active_hooks?를 확인합니다 (그룹에 대한 유사 메서드 지원은 이슈 #517890에서 추적합니다).

이 메서드는 다음 중 하나에 해당하면 true를 반환합니다.

  • 프로젝트나 그룹, 또는 상위 그룹에 해당 유형의 웹훅이 설정되어 있어 최소 한 개의 웹훅이 실행되어야 하는 경우.
  • 프로젝트에서 호출했을 때 해당 유형에 대한 시스템 웹훅이 설정된 경우.

예시입니다.

def execute_emoji_hooks
  return unless project.has_active_hooks?(:emoji_hooks)

  payload = Gitlab::DataBuilder::Emoji.build(emoji)
  project.execute_hooks(payload, :emoji_hooks)
end

인스턴스(시스템) 웹훅 트리거#

프로젝트 웹훅이 트리거되면 해당 웹훅 유형으로 설정된 시스템 웹훅이 자동으로 실행됩니다.

해당 웹훅을 프로젝트에서는 설정할 수 없는 경우 SystemHooksService를 통해 시스템 훅을 트리거할 수도 있습니다.

예시입니다.

SystemHooksService.new.execute_hooks_for(user, :create)

해당 리소스의 데이터를 구성하도록 SystemHooksService를 갱신해야 합니다.

정확한 페이로드로 트리거#

웹훅 페이로드는 이벤트 발생 시점의 데이터 상태를 정확히 나타내야 합니다. 경합 조건이나 데이터 상태를 바꾸는 동시 프로세스로 인해 부정확한 페이로드가 웹훅 수신기로 전송되는 문제가 생기지 않도록 주의해야 합니다.

이를 위한 방법은 다음과 같습니다.

  • 다른 프로세스가 객체의 상태를 바꿨을 수 있으므로 페이로드를 구성하기 전에 객체를 다시 로드하지 않도록 합니다.
  • 페이로드를 위해 추가로 로드하는 데이터도 이벤트 시점의 상태를 반영하도록, 이벤트가 발생한 직후에 페이로드를 구성합니다.

두 항목 모두, 페이로드를 Sidekiq로 비동기 처리하지 않고 요청 안에서 구성해야 한다는 뜻입니다.

페이로드가 언제나 불변 데이터만 담는 경우는 예외이지만, 대개는 그렇지 않습니다.

웹훅 페이로드#

웹훅 페이로드는 웹훅 수신기로 POST 되는 JSON 데이터입니다.

기존 웹훅 페이로드는 웹훅 이벤트 문서에 정리되어 있습니다.

웹훅 페이로드에 포함하지 않아야 할 항목#

민감한 데이터는 웹훅 페이로드에 절대 포함하지 않습니다. 여기에는 시크릿과 비공개 사용자 이메일이 포함됩니다(비공개 사용자 이메일은 User#hook_attrs를 통해 자동으로 마스킹 됩니다).

웹훅 페이로드 구성은 성능이 매우 중요하므로, 웹훅 페이로드에 추가하는 모든 속성은 데이터베이스에서 이를 가져오는 부담을 감수할 만한지 따져 보아야 합니다.

모든 고객이 웹훅 페이로드로 데이터를 받게 하는 것과, 일부 고객이 더 작은 웹훅을 받은 뒤 API로 객체 데이터를 추가 조회하게 하는 것 중 무엇이 나은지 종합적으로 검토합니다. 후자의 경우 웹훅이 구성된 시점과 고객이 추가 데이터를 조회하는 시점 사이에 시간 차가 생깁니다. 이 지연으로 API 데이터와 웹훅 데이터가 서로 다른 시점의 상태를 나타낼 수 있습니다.

페이로드 정의#

객체에는 웹훅 페이로드용 객체 속성을 반환하는 #hook_attrs 메서드를 정의해야 합니다.

#hook_attrs의 속성은 정적 키로 정의해야 합니다. 이 메서드는 #attributes 나 #as_json 이 반환하는 속성을 그대로 반환하지 않고, 특정한 속성 집합을 반환해야 합니다. 그렇지 않으면 이후에 모델에 추가되는 모든 속성이 웹훅 페이로드에 포함됩니다 (이슈 440384 참고).

전체 페이로드는 Gitlab::DataBuilder::의 모듈이나 클래스가 구성해야 합니다. 전체 페이로드에는 보통 연관 객체가 포함됩니다.

전체 페이로드의 구조는 페이로드 스키마를 참고합니다.

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

# An object defines #hook_attrs:
class Car < ApplicationRecord
  def hook_attrs
    {
      make: make,
      color: color
    }
  end
end

# A Gitlab::DataBuilder module or class composes the full webhook payload:
module Gitlab
  module DataBuilder
    module Car
      extend self

      def build(car, action)
        {
          object_kind: 'car',
          action: action,
          object_attributes: car.hook_attrs,
          driver: car.driver.hook_attrs # Calling #hook_attrs on associated data
        }
      end
    end
  end
end

# Building the payload:
Gitlab::DataBuilder::Car.build(car, 'start')

페이로드 스키마#

지금까지 웹훅 유형별 페이로드 스키마 사이에는 일관성이 부족한 부분이 많았습니다.

앞으로는 일관성을 위해 기존 웹훅과 비슷한 페이로드를 써야 하는 경우(예를 들어 새로운 이슈어블에 대한 웹훅)를 제외하고, 새 웹훅의 스키마는 다음 규칙을 따라야 합니다.

  • 새 웹훅의 스키마에는 다음 필수 속성이 있어야 합니다.
    • "object_kind". 스네이크 케이스로 표기한 객체의 종류입니다. 예: "merge_request".
    • "action". 방금 일어난 일을 현재 시제로 나타내는 도메인 고유 동사입니다. 예: "create", "assign", "update", "revoke". 이를 통해 수신기는 객체 수명 주기의 여러 시점에 웹훅이 트리거될 때 객체에 일어난 서로 다른 종류의 변경을 식별하고 처리할 수 있습니다.
    • "object_attributes". 이벤트 이후 객체의 속성을 담습니다. 이 속성은 #hook_attrs에서 생성됩니다.
  • 연관 데이터는 "object_attributes" 안에 중첩하지 않고 페이로드의 최상위에 두어야 합니다.
  • 페이로드에 변경된 속성 값 기록이 포함된다면 최상위 "changes" 객체에 두어야 합니다.

위 내용을 JSON 스키마로 기술하면 다음과 같습니다.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "description": "Recommended GitLab webhook payload schema",
  "type": "object",
  "properties": {
    "object_kind": {
      "type": "string",
      "description": "Kind of object in snake case. Example: merge_request",
      "pattern": "^([a-zA-Z]+(_[a-zA-Z]+)*)$"
    },
    "action": {
      "type": "string",
      "description": "A domain-specific verb of what just happened to the object, using present tense. Examples: create, revoke",
    },
    "object_attributes": {
      "type": "object",
      "description": "Attributes of the object after the event"
    },
    "changes": {
      "type": "object",
      "description": "Optional object attributes that were changed during the event",
      "patternProperties": {
        ".+" : {
          "type" : "object",
          "properties": {
            "previous": {
              "description": "Value of attribute before the event"
            },
            "current": {
              "description": "Value of attribute after the event"
            }
          },
          "required": ["previous", "current"]
        }
      }
    }
  },
  "required": ["object_kind", "action", "object_attributes"]
}

위 페이로드 스키마를 따르는 가상의 Car 객체에 대한 웹훅 페이로드 예시입니다.

{
  "object_kind": "car",
  "action": "start",
  "object_attributes": {
    "make": "Toyota",
    "color": "grey"
  },
  "driver": {
    "name": "Kaya",
    "age": 18
  }
}

변경 객체 포함#

페이로드에 객체의 속성 변경 목록을 포함해야 한다면, 모델에 ReportableChanges 모듈을 추가합니다. 이 모듈은 객체가 로드된 시점부터 이후의 모든 저장에 이르기까지 속성 값의 모든 변경을 수집합니다. 하나의 요청 컨텍스트에서 객체에 대한 저장이 여러 번 일어나고, 마지막 훅이 가장 최근 저장의 변경분만이 아니라 누적된 변경분에 접근해야 하는 경우에 유용합니다.

페이로드에 속성 변경을 포함하는 방법은 페이로드 스키마를 참고합니다.

데이터베이스 요청 최소화#

일부 유형의 웹훅은 GitLab.com에서 하루에 수백만 번 트리거됩니다.

Sidekiq가 아니라 요청 안에서 페이로드를 구성해야 하므로, 웹훅 페이로드용 추가 데이터를 로드하는 작업은 성능이 좋아야 합니다. GitLab.com에서는 웹훅이 데이터베이스 쓰기 직후에 트리거되므로, 페이로드용 추가 데이터가 PostgreSQL 기본 노드에서 로드된다는 뜻이기도 합니다.

웹훅 페이로드를 구성할 때 데이터 요청을 최소화하는 방법은 다음과 같습니다.

  • 웹훅 페이로드에 데이터를 추가하는 것이 얼마나 중요한지 따져 봅니다.
  • N+1 문제를 피하도록 추가 데이터를 미리 로드합니다.
  • 회귀를 방지하도록 웹훅 페이로드 구성에 사용된 데이터베이스 호출 수를 테스트에서 단언합니다.

이미 로드된 레코드에 데이터를 미리 로드해야 할 수 있습니다. 이 경우 ActiveRecord::Associations::Preloader를 사용할 수 있습니다.

연관 데이터가 웹훅 페이로드 구성에만 필요하다면, #has_active_hooks? 확인을 통과한 뒤에만 해당 연관 데이터를 미리 로드합니다.

코드베이스의 좋은 실제 예시는 Gitlab::DataBuilder::Pipeline 입니다.

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

# Working with an issue that has been loaded
issue = Issue.first

# Imagine we have performed the #has_active_hooks? check and now are building the webhook payload.
# Doing this will perform N+1 database queries:
# issue.notes.map(&:author).map(&:name)
#
# Instead, first preload the associations to avoid the N+1
ActiveRecord::Associations::Preloader.new(records: [issue], associations: { notes: :author }).call;
issue.notes.map(&:author).map(&:name)

호환성을 깨는 변경#

웹훅 페이로드에는 호환성을 깨는 변경을 적용할 수 없습니다.

웹훅 페이로드 변경으로 웹훅 수신기에서 오류가 발생할 수 있다면 그 변경은 호환성을 깨는 변경입니다.

새 속성을 추가하는 등 추가적인 변경만 적용할 수 있습니다.

호환성을 깨는 변경에는 다음이 포함됩니다.

  • 속성 제거.
  • 속성 이름 변경.
  • "object_kind" 속성 값의 변경.
  • "action" 속성 값의 변경.

기능 제거 등으로 "object_kind" 나 "action" 이외의 속성 값을 바꿔야 한다면, 속성을 제거하지 말고 값을 null, {}, []로 설정합니다.

테스트#

DataBuilder 클래스의 단위 테스트를 작성할 때는 다음을 단언합니다.

  • QueryRecorder를 사용해 정해진 수의 데이터베이스 요청이 이루어지는지 확인합니다. QueryRecorder로 쿼리 수를 측정한 다음 스펙에서 그 수와 비교하면, 의식적인 선택 없이 쿼리 수가 바뀌는 일을 막을 수 있습니다. 연관 데이터의 미리 로드도 함께 참고합니다.
  • 페이로드에 기대한 속성이 있는지 확인합니다.

웹훅이 트리거되어야 하는(또는 트리거되지 않아야 하는) 시나리오도 테스트해 실제로 올바르게 트리거되는지 단언합니다.

QA로 변경 사항 확인#

웹훅 URL을 https://webhook.site가 제공하는 주소로 설정하면 웹훅이 트리거될 때 생성되는 전체 웹훅 헤더와 페이로드를 확인할 수 있습니다.

변경 사항 검토 요청#

웹훅 변경 사항은 일반적인 코드 리뷰 담당자 외에 Import & Integrate 의 백엔드 팀원에게도 검토를 받아야 합니다.

웹훅 개발자 가이드

GitLab v19.4
원문 보기

요약

이 페이지는 GitLab 웹훅에 대한 개발자 가이드입니다. 웹훅은 GitLab에서 발생한 이벤트나 변경 사항에 대한 JSON 데이터를 웹훅 수신기로 POST 합니다. 다음은 웹훅이 트리거되어 실행될 때 일어나는 일을 개괄적으로 설명한 것입니다.

이 페이지는 GitLab 웹훅에 대한 개발자 가이드입니다.

웹훅은 GitLab에서 발생한 이벤트나 변경 사항에 대한 JSON 데이터를 웹훅 수신기로 POST 합니다. 웹훅을 사용하면 고객이 API를 폴링하지 않아도 특정 변경이 발생했을 때 알림을 받습니다.

웹훅 흐름#

다음은 웹훅이 트리거되어 실행될 때 일어나는 일을 개괄적으로 설명한 것입니다.

Mermaid 다이어그램 (8줄)
소스 코드 보기
sequenceDiagram
    Web or API node->>+Database: Fetch data for payload
    Database-->>-Web or API node: Build payload
    Note over Web or API node,Database: Webhook triggered
    Web or API node->>Sidekiq: Queue webhook execution
    Sidekiq->>+Remote webhook receiver: POST webhook payload
    Remote webhook receiver-)-Database: Save response in WebHookLog
    Note over Database,Remote webhook receiver: Webhook executed

새 웹훅 추가#

웹훅은 리소스 중심입니다. 예를 들어 "emoji" 웹훅은 이모지가 부여되거나 회수될 때마다 트리거됩니다.

리소스에 웹훅 지원을 추가하는 방법은 다음과 같습니다.

  1. web_hooks 테이블에 새 칼럼을 추가합니다. 새 칼럼의 조건은 다음과 같습니다.

    • 불리언 타입
    • Not null
    • <resource>_events 형식의 이름
    • 기본값 false.

    마이그레이션의 #change 메서드 예시입니다.

    def change
      add_column :web_hooks, :emoji_events, :boolean, null: false, default: false
    end
    
  2. TriggerableHooks.available_triggers에 새 웹훅 지원을 추가합니다.

  3. 해당 웹훅을 프로젝트, 그룹, GitLab 인스턴스 중 어디에서 설정할 수 있게 할지에 따라 ProjectHook, GroupHook, SystemHook의 triggerable_hooks 목록에 추가합니다. 판단 기준은 프로젝트, 그룹 및 시스템 웹훅을 참고합니다.

  4. app/views/shared/web_hooks/_form.html.haml의 웹훅 설정 폼에 새 체크박스에 대한 프론트엔드 지원을 추가합니다.

  5. TestHooks::ProjectService와 TestHooks::SystemService에 새 웹훅 테스트 지원을 추가합니다. TestHooks::GroupService는 ProjectService를 실행 하기만 하므로 갱신할 필요가 없습니다.

  6. 웹훅 페이로드를 정의합니다.

  7. 웹훅을 트리거하도록 GitLab을 갱신합니다.

  8. 웹훅 문서를 추가합니다.

  9. REST API 지원을 추가합니다.

    1. 해당 인수를 지원하도록 API::ProjectHooks, API::GroupHooks, API::SystemHooks를 갱신합니다.
    2. 새 필드를 지원하도록 API::Entities::ProjectHook과 API::Entities::GroupHook을 갱신합니다. (시스템 훅은 일반 API::Entities::Hook을 사용합니다.)
    3. 프로젝트 웹훅, 그룹 웹훅, 시스템 훅의 API 문서를 갱신합니다.

결정: 프로젝트, 그룹 및 시스템 웹훅#

웹훅을 프로젝트, 그룹, GitLab 인스턴스 중 어디에서 설정할 수 있게 할지 판단할 때는 다음을 참고합니다.

  • 특정 수준에 속하는 리소스와 관련된 웹훅은 그 수준에서 설정할 수 있어야 합니다. 예를 들어 이슈 웹훅은 프로젝트에서, 그룹 멤버십 웹훅은 그룹에서, 사용자 로그인 실패 웹훅은 GitLab 인스턴스에서 설정할 수 있습니다.
  • 프로젝트에서 설정할 수 있는 웹훅은 일반적으로 그룹에서도 설정할 수 있게 해야 합니다. 그룹 소유자가 해당 그룹의 프로젝트에서 일어나는 모든 이벤트를 받기 위해 그룹 웹훅을 설정하는 경우가 많고, 프로젝트 웹훅이 트리거될 때 그룹 웹훅도 자동으로 실행 되기 때문입니다.
  • 일반적으로 프로젝트나 그룹(또는 둘 다)에서 설정할 수 있는 웹훅은, 인스턴스 관리자가 이를 수신해야 한다는 명확한 기능 요청이 있을 때만 인스턴스에서 설정할 수 있게 합니다. 현재의 프로젝트 및 그룹 웹훅 상당수는 인스턴스 수준에서 설정할 수 없습니다.

EE 전용 고려 사항#

그룹 웹훅은 Premium 라이선스 기능입니다. 그룹 웹훅 트리거와 관련된 모든 코드, 그리고 그룹에서만 설정할 수 있는 웹훅의 페이로드를 구성하는 코드는 ee/ 디렉터리에 있어야 합니다.

웹훅 트리거#

프로젝트 및 그룹 웹훅 트리거#

프로젝트 및 그룹 웹훅은 프로젝트나 그룹에서 #execute_hooks를 호출해 트리거합니다.

#execute_hooks 메서드에는 다음이 전달됩니다.

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

project.execute_hooks(payload, :emoji_hooks)

단일 프로젝트나 그룹에서 #execute_hooks를 호출하면 트리거가 상위 그룹으로 자동으로 전파되어 상위 그룹도 함께 실행합니다. 이를 통해 그룹은 자신의 하위 그룹이나 프로젝트에서 발생한 이벤트에 대한 웹훅을 받도록 설정할 수 있습니다.

이 메서드를 호출하는 대상에 따라 동작은 다음과 같습니다.

  • 프로젝트에서 호출하면, 해당 프로젝트에 설정된 해당 유형의 웹훅이 실행되는 것에 더해 해당 프로젝트의 그룹과 상위 그룹에 설정된 같은 유형의 웹훅도 함께 실행됩니다. 해당 유형으로 설정된 인스턴스(시스템) 웹훅도 함께 실행됩니다.
  • 그룹에서 호출하면, 해당 그룹에 설정된 해당 유형의 웹훅이 실행되는 것에 더해 그 그룹의 상위 그룹에 설정된 웹훅도 함께 실행됩니다.

페이로드 구성은 대개 데이터베이스에서 더 많은 레코드를 읽어야 하므로 비용이 큽니다. 따라서 웹훅을 트리거하기 전에 프로젝트에서 #has_active_hooks?를 확인합니다 (그룹에 대한 유사 메서드 지원은 이슈 #517890에서 추적합니다).

이 메서드는 다음 중 하나에 해당하면 true를 반환합니다.

  • 프로젝트나 그룹, 또는 상위 그룹에 해당 유형의 웹훅이 설정되어 있어 최소 한 개의 웹훅이 실행되어야 하는 경우.
  • 프로젝트에서 호출했을 때 해당 유형에 대한 시스템 웹훅이 설정된 경우.

예시입니다.

def execute_emoji_hooks
  return unless project.has_active_hooks?(:emoji_hooks)

  payload = Gitlab::DataBuilder::Emoji.build(emoji)
  project.execute_hooks(payload, :emoji_hooks)
end

인스턴스(시스템) 웹훅 트리거#

프로젝트 웹훅이 트리거되면 해당 웹훅 유형으로 설정된 시스템 웹훅이 자동으로 실행됩니다.

해당 웹훅을 프로젝트에서는 설정할 수 없는 경우 SystemHooksService를 통해 시스템 훅을 트리거할 수도 있습니다.

예시입니다.

SystemHooksService.new.execute_hooks_for(user, :create)

해당 리소스의 데이터를 구성하도록 SystemHooksService를 갱신해야 합니다.

정확한 페이로드로 트리거#

웹훅 페이로드는 이벤트 발생 시점의 데이터 상태를 정확히 나타내야 합니다. 경합 조건이나 데이터 상태를 바꾸는 동시 프로세스로 인해 부정확한 페이로드가 웹훅 수신기로 전송되는 문제가 생기지 않도록 주의해야 합니다.

이를 위한 방법은 다음과 같습니다.

  • 다른 프로세스가 객체의 상태를 바꿨을 수 있으므로 페이로드를 구성하기 전에 객체를 다시 로드하지 않도록 합니다.
  • 페이로드를 위해 추가로 로드하는 데이터도 이벤트 시점의 상태를 반영하도록, 이벤트가 발생한 직후에 페이로드를 구성합니다.

두 항목 모두, 페이로드를 Sidekiq로 비동기 처리하지 않고 요청 안에서 구성해야 한다는 뜻입니다.

페이로드가 언제나 불변 데이터만 담는 경우는 예외이지만, 대개는 그렇지 않습니다.

웹훅 페이로드#

웹훅 페이로드는 웹훅 수신기로 POST 되는 JSON 데이터입니다.

기존 웹훅 페이로드는 웹훅 이벤트 문서에 정리되어 있습니다.

웹훅 페이로드에 포함하지 않아야 할 항목#

민감한 데이터는 웹훅 페이로드에 절대 포함하지 않습니다. 여기에는 시크릿과 비공개 사용자 이메일이 포함됩니다(비공개 사용자 이메일은 User#hook_attrs를 통해 자동으로 마스킹 됩니다).

웹훅 페이로드 구성은 성능이 매우 중요하므로, 웹훅 페이로드에 추가하는 모든 속성은 데이터베이스에서 이를 가져오는 부담을 감수할 만한지 따져 보아야 합니다.

모든 고객이 웹훅 페이로드로 데이터를 받게 하는 것과, 일부 고객이 더 작은 웹훅을 받은 뒤 API로 객체 데이터를 추가 조회하게 하는 것 중 무엇이 나은지 종합적으로 검토합니다. 후자의 경우 웹훅이 구성된 시점과 고객이 추가 데이터를 조회하는 시점 사이에 시간 차가 생깁니다. 이 지연으로 API 데이터와 웹훅 데이터가 서로 다른 시점의 상태를 나타낼 수 있습니다.

페이로드 정의#

객체에는 웹훅 페이로드용 객체 속성을 반환하는 #hook_attrs 메서드를 정의해야 합니다.

#hook_attrs의 속성은 정적 키로 정의해야 합니다. 이 메서드는 #attributes 나 #as_json 이 반환하는 속성을 그대로 반환하지 않고, 특정한 속성 집합을 반환해야 합니다. 그렇지 않으면 이후에 모델에 추가되는 모든 속성이 웹훅 페이로드에 포함됩니다 (이슈 440384 참고).

전체 페이로드는 Gitlab::DataBuilder::의 모듈이나 클래스가 구성해야 합니다. 전체 페이로드에는 보통 연관 객체가 포함됩니다.

전체 페이로드의 구조는 페이로드 스키마를 참고합니다.

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

# An object defines #hook_attrs:
class Car < ApplicationRecord
  def hook_attrs
    {
      make: make,
      color: color
    }
  end
end

# A Gitlab::DataBuilder module or class composes the full webhook payload:
module Gitlab
  module DataBuilder
    module Car
      extend self

      def build(car, action)
        {
          object_kind: 'car',
          action: action,
          object_attributes: car.hook_attrs,
          driver: car.driver.hook_attrs # Calling #hook_attrs on associated data
        }
      end
    end
  end
end

# Building the payload:
Gitlab::DataBuilder::Car.build(car, 'start')

페이로드 스키마#

지금까지 웹훅 유형별 페이로드 스키마 사이에는 일관성이 부족한 부분이 많았습니다.

앞으로는 일관성을 위해 기존 웹훅과 비슷한 페이로드를 써야 하는 경우(예를 들어 새로운 이슈어블에 대한 웹훅)를 제외하고, 새 웹훅의 스키마는 다음 규칙을 따라야 합니다.

  • 새 웹훅의 스키마에는 다음 필수 속성이 있어야 합니다.
    • "object_kind". 스네이크 케이스로 표기한 객체의 종류입니다. 예: "merge_request".
    • "action". 방금 일어난 일을 현재 시제로 나타내는 도메인 고유 동사입니다. 예: "create", "assign", "update", "revoke". 이를 통해 수신기는 객체 수명 주기의 여러 시점에 웹훅이 트리거될 때 객체에 일어난 서로 다른 종류의 변경을 식별하고 처리할 수 있습니다.
    • "object_attributes". 이벤트 이후 객체의 속성을 담습니다. 이 속성은 #hook_attrs에서 생성됩니다.
  • 연관 데이터는 "object_attributes" 안에 중첩하지 않고 페이로드의 최상위에 두어야 합니다.
  • 페이로드에 변경된 속성 값 기록이 포함된다면 최상위 "changes" 객체에 두어야 합니다.

위 내용을 JSON 스키마로 기술하면 다음과 같습니다.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "description": "Recommended GitLab webhook payload schema",
  "type": "object",
  "properties": {
    "object_kind": {
      "type": "string",
      "description": "Kind of object in snake case. Example: merge_request",
      "pattern": "^([a-zA-Z]+(_[a-zA-Z]+)*)$"
    },
    "action": {
      "type": "string",
      "description": "A domain-specific verb of what just happened to the object, using present tense. Examples: create, revoke",
    },
    "object_attributes": {
      "type": "object",
      "description": "Attributes of the object after the event"
    },
    "changes": {
      "type": "object",
      "description": "Optional object attributes that were changed during the event",
      "patternProperties": {
        ".+" : {
          "type" : "object",
          "properties": {
            "previous": {
              "description": "Value of attribute before the event"
            },
            "current": {
              "description": "Value of attribute after the event"
            }
          },
          "required": ["previous", "current"]
        }
      }
    }
  },
  "required": ["object_kind", "action", "object_attributes"]
}

위 페이로드 스키마를 따르는 가상의 Car 객체에 대한 웹훅 페이로드 예시입니다.

{
  "object_kind": "car",
  "action": "start",
  "object_attributes": {
    "make": "Toyota",
    "color": "grey"
  },
  "driver": {
    "name": "Kaya",
    "age": 18
  }
}

변경 객체 포함#

페이로드에 객체의 속성 변경 목록을 포함해야 한다면, 모델에 ReportableChanges 모듈을 추가합니다. 이 모듈은 객체가 로드된 시점부터 이후의 모든 저장에 이르기까지 속성 값의 모든 변경을 수집합니다. 하나의 요청 컨텍스트에서 객체에 대한 저장이 여러 번 일어나고, 마지막 훅이 가장 최근 저장의 변경분만이 아니라 누적된 변경분에 접근해야 하는 경우에 유용합니다.

페이로드에 속성 변경을 포함하는 방법은 페이로드 스키마를 참고합니다.

데이터베이스 요청 최소화#

일부 유형의 웹훅은 GitLab.com에서 하루에 수백만 번 트리거됩니다.

Sidekiq가 아니라 요청 안에서 페이로드를 구성해야 하므로, 웹훅 페이로드용 추가 데이터를 로드하는 작업은 성능이 좋아야 합니다. GitLab.com에서는 웹훅이 데이터베이스 쓰기 직후에 트리거되므로, 페이로드용 추가 데이터가 PostgreSQL 기본 노드에서 로드된다는 뜻이기도 합니다.

웹훅 페이로드를 구성할 때 데이터 요청을 최소화하는 방법은 다음과 같습니다.

  • 웹훅 페이로드에 데이터를 추가하는 것이 얼마나 중요한지 따져 봅니다.
  • N+1 문제를 피하도록 추가 데이터를 미리 로드합니다.
  • 회귀를 방지하도록 웹훅 페이로드 구성에 사용된 데이터베이스 호출 수를 테스트에서 단언합니다.

이미 로드된 레코드에 데이터를 미리 로드해야 할 수 있습니다. 이 경우 ActiveRecord::Associations::Preloader를 사용할 수 있습니다.

연관 데이터가 웹훅 페이로드 구성에만 필요하다면, #has_active_hooks? 확인을 통과한 뒤에만 해당 연관 데이터를 미리 로드합니다.

코드베이스의 좋은 실제 예시는 Gitlab::DataBuilder::Pipeline 입니다.

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

# Working with an issue that has been loaded
issue = Issue.first

# Imagine we have performed the #has_active_hooks? check and now are building the webhook payload.
# Doing this will perform N+1 database queries:
# issue.notes.map(&:author).map(&:name)
#
# Instead, first preload the associations to avoid the N+1
ActiveRecord::Associations::Preloader.new(records: [issue], associations: { notes: :author }).call;
issue.notes.map(&:author).map(&:name)

호환성을 깨는 변경#

웹훅 페이로드에는 호환성을 깨는 변경을 적용할 수 없습니다.

웹훅 페이로드 변경으로 웹훅 수신기에서 오류가 발생할 수 있다면 그 변경은 호환성을 깨는 변경입니다.

새 속성을 추가하는 등 추가적인 변경만 적용할 수 있습니다.

호환성을 깨는 변경에는 다음이 포함됩니다.

  • 속성 제거.
  • 속성 이름 변경.
  • "object_kind" 속성 값의 변경.
  • "action" 속성 값의 변경.

기능 제거 등으로 "object_kind" 나 "action" 이외의 속성 값을 바꿔야 한다면, 속성을 제거하지 말고 값을 null, {}, []로 설정합니다.

테스트#

DataBuilder 클래스의 단위 테스트를 작성할 때는 다음을 단언합니다.

  • QueryRecorder를 사용해 정해진 수의 데이터베이스 요청이 이루어지는지 확인합니다. QueryRecorder로 쿼리 수를 측정한 다음 스펙에서 그 수와 비교하면, 의식적인 선택 없이 쿼리 수가 바뀌는 일을 막을 수 있습니다. 연관 데이터의 미리 로드도 함께 참고합니다.
  • 페이로드에 기대한 속성이 있는지 확인합니다.

웹훅이 트리거되어야 하는(또는 트리거되지 않아야 하는) 시나리오도 테스트해 실제로 올바르게 트리거되는지 단언합니다.

QA로 변경 사항 확인#

웹훅 URL을 https://webhook.site가 제공하는 주소로 설정하면 웹훅이 트리거될 때 생성되는 전체 웹훅 헤더와 페이로드를 확인할 수 있습니다.

변경 사항 검토 요청#

웹훅 변경 사항은 일반적인 코드 리뷰 담당자 외에 Import & Integrate 의 백엔드 팀원에게도 검토를 받아야 합니다.