본문 바로가기
← 패키지 목록

Laravel Webhook Server

spatie/laravel-webhook-server

제출자 Laravel Korea Discovery Bot
최신 v3.11.0라이센스 MIT
큐·작업

Laravel 앱에서 웹훅을 전송하세요

Laravel 앱에서 웹훅 보내기

Latest Version on Packagist run-testsTotal Downloads

웹훅(webhook)은 어떤 앱이 특정 이벤트가 발생했음을 다른 앱에게 알려주는 방법입니다. 두 앱은 단순한 HTTP 요청을 주고받는 방식으로 통신합니다.

이 패키지를 사용하면 Laravel 앱에서 웹훅을 손쉽게 설정하고 전송할 수 있습니다. 요청 서명(signing), 재시도 및 백오프 전략을 지원합니다.

반대로 웹훅을 수신하고 처리해야 한다면 laravel-webhook-client 패키지를 참고하세요.

우리를 응원해주세요

저희는 최고 수준의 오픈소스 패키지를 만드는 데 많은 리소스를 투자하고 있습니다. 유료 제품을 구매하시면 저희를 응원하실 수 있습니다.

사용 중인 패키지를 언급하며 여러분의 고향에서 엽서를 보내주시면 정말 감사하겠습니다. 주소는 저희 연락처 페이지에서 확인하실 수 있습니다. 받은 엽서는 모두 엽서 갤러리에 게시하고 있습니다.

설치

Composer를 통해 패키지를 설치할 수 있습니다.

composer require spatie/laravel-webhook-server

다음 명령어로 설정 파일을 게시할 수 있습니다.

php artisan vendor:publish --provider="Spatie\WebhookServer\WebhookServerServiceProvider"

이 명령어를 실행하면 config/webhook-server.php 파일이 다음과 같은 내용으로 생성됩니다.

return [ /* * 웹훅 요청을 전송할 때 기본으로 사용할 큐입니다. */ 'queue' => 'default', /* * 기본으로 사용할 HTTP 메서드입니다. */ 'http_verb' => 'post', /* * 웹훅 요청 헤더에 추가될 서명을 계산하는 역할을 하는 클래스입니다. * 웹훅을 받는 쪽에서는 이 서명을 통해 요청이 변조되지 않았는지 검증할 수 있습니다. */ 'signer' => \Spatie\WebhookServer\Signer\DefaultSigner::class, /* * 서명 값이 담길 헤더의 이름입니다. */ 'signature_header_name' => 'Signature', /* * 타임스탬프 값이 담길 헤더의 이름입니다. */ 'timestamp_header_name' => 'Timestamp', /* * 모든 웹훅 요청에 추가로 포함될 헤더입니다. */ 'headers' => [], /* * 웹훅 호출이 이 시간(초)보다 오래 걸리면 실패한 시도로 간주합니다. */ 'timeout_in_seconds' => 3, /* * 포기하기 전까지 웹훅을 호출할 횟수입니다. */ 'tries' => 3, /* * 재시도 사이의 대기 시간(초)을 결정하는 클래스입니다. */ 'backoff_strategy' => \Spatie\WebhookServer\BackoffStrategy\ExponentialBackoffStrategy::class, /* * 웹훅을 큐에 디스패치할 때 사용되는 Job 클래스입니다. */ 'webhook_job' => \Spatie\WebhookServer\CallWebhookJob::class, /* * 기본적으로 웹훅 대상 URL의 SSL 인증서가 유효한지 검증합니다. */ 'verify_ssl' => true, /* * true로 설정하면 마지막 시도까지 실패했을 때 예외가 발생합니다. */ 'throw_exception_on_failure' => false, /* * Laravel Horizon을 사용하는 경우, 웹훅 요청을 수행하는 Job에 * 적용할 태그를 지정할 수 있습니다. */ 'tags' => [], ];

이 패키지는 기본적으로 큐를 사용해 실패한 웹훅 요청을 재시도합니다. 로컬 환경이 아니라면 sync가 아닌 실제 큐 드라이버를 반드시 설정해두세요.

사용법

가장 간단하게 웹훅을 호출하는 방법은 다음과 같습니다.

WebhookCall::create() ->url('https://other-app.com/webhooks') ->payload(['key' => 'value']) ->useSecret('sign-using-this-secret') ->dispatch();

위 코드는 https://other-app.com/webhooks로 POST 요청을 전송합니다. 요청 본문은 payload에 전달한 배열을 JSON으로 인코딩한 값이 됩니다. 요청에는 Signature라는 헤더가 포함되며, 이 값을 이용해 수신 측 앱은 페이로드가 변조되지 않았는지 검증할 수 있습니다. 웹훅 호출을 디스패치하면 DispatchingWebhookCallEvent 이벤트도 함께 발생합니다.

수신 앱이 2로 시작하지 않는 응답 코드를 반환하면, 이 패키지는 10초 후 웹훅 호출을 재시도합니다. 두 번째 시도도 실패하면 100초 후 마지막으로 한 번 더 시도합니다. 이마저 실패하면 FinalWebhookCallFailedEvent 이벤트가 발생합니다.

웹훅을 동기적으로 전송하기

웹훅을 큐에 넣지 않고 즉시(동기적으로) 호출하고 싶다면 dispatchSync 메서드를 사용하세요. 이 메서드를 사용하면 웹훅이 큐잉되지 않고 바로 실행됩니다. 이미 큐에 등록되어 있는 더 큰 Job의 일부로 웹훅을 보내야 하는 상황에서 유용합니다.

WebhookCall::create() ... ->dispatchSync();

조건부로 웹훅 보내기

특정 조건에 따라 웹훅을 디스패치하고 싶다면 dispatchIf, dispatchUnless, dispatchSyncIf, dispatchSyncUnless 메서드를 사용할 수 있습니다.

WebhookCall::create() ... ->dispatchIf($condition); WebhookCall::create() ... ->dispatchUnless($condition); WebhookCall::create() ... ->dispatchSyncIf($condition); WebhookCall::create() ... ->dispatchSyncUnless($condition);

요청 서명(signing)이 동작하는 방식

일반적으로 설정 단계에서 시크릿 값을 생성하여 저장하고, 이를 웹훅을 수신할 앱과 공유합니다. 시크릿 값은 Illuminate\Support\Str::random()으로 생성해도 되고, 원하는 다른 방식을 사용해도 무방합니다. 이 패키지는 이 시크릿 값을 이용해 웹훅 호출에 서명합니다.

기본적으로 이 패키지는 Signature라는 헤더를 추가하며, 수신 측은 이 값을 이용해 페이로드가 변조되지 않았는지 확인할 수 있습니다. 서명은 다음과 같이 계산됩니다.

// payload는 웹훅의 `payload` 메서드에 전달한 배열입니다. // secret은 웹훅의 `signUsingSecret` 메서드에 전달한 문자열입니다. $payloadJson = json_encode($payload); $signature = hash_hmac('sha256', $payloadJson, $secret);

요청 서명 건너뛰기

권장하지는 않지만, 웹훅 요청에 서명을 하지 않으려면 doNotSign 메서드를 호출하세요.

WebhookCall::create() ->doNotSign() ...

이 메서드를 호출하면 Signature 헤더가 설정되지 않습니다.

요청 서명 방식 커스터마이징하기

서명 방식을 커스터마이징하고 싶다면 직접 서명 클래스를 만들 수 있습니다. 서명 클래스는 Spatie\WebhookServer\Signer 인터페이스를 구현한 클래스라면 무엇이든 가능합니다.

인터페이스는 다음과 같이 생겼습니다.

namespace Spatie\WebhookServer\Signer; interface Signer { public function signatureHeaderName(): string; public function calculateSignature(array $payload, string $secret): string; }

커스텀 서명 클래스를 만든 후, webhook-server 설정 파일의 signer 키에 해당 클래스명을 지정하면 이후 모든 웹훅 호출에서 기본적으로 사용됩니다.

특정 웹훅 호출에만 서명 클래스를 지정할 수도 있습니다.

WebhookCall::create() ->signUsing(YourCustomSigner::class) ... ->dispatch();

헤더 이름만 바꾸고 싶다면 커스텀 서명 클래스를 만들 필요 없이, webhook-server 설정 파일의 signature_header_name 값만 변경하면 됩니다.

타임스탬프 사용하기

재전송 공격(replay attack)을 방지하는 데 도움이 되므로 사용을 강력히 권장합니다.

타임스탬프는 기본적으로 비활성화되어 있으며, useTimestamp() 메서드를 호출해 활성화할 수 있습니다.

WebhookCall::create() ->useTimestamp() ... ->dispatch();

이 메서드를 호출하면 Timestamp 헤더가 설정됩니다.

실패한 웹훅 재시도하기

웹훅을 전송받는 앱이 2xx 상태 코드로 응답하지 않으면 해당 호출은 실패로 간주됩니다. 원격 앱이 3초 이내에 응답하지 않는 경우에도 마찬가지로 실패로 처리됩니다.

이 기본 타임아웃 값은 webhook-server 설정 파일의 timeout_in_seconds 키에서 변경할 수 있습니다. 또는 특정 웹훅에 대해서만 다음과 같이 타임아웃을 재정의할 수도 있습니다.

WebhookCall::create() ->timeoutInSeconds(5) ... ->dispatch();

웹훅 호출이 실패하면 두 번 더 재시도합니다. 기본 재시도 횟수는 설정 파일의 tries 키에서 지정할 수 있으며, 특정 웹훅에 한해서는 다음과 같이 지정할 수 있습니다.

WebhookCall::create() ->maximumTries(5) ... ->dispatch();

원격 앱에 부담을 주지 않기 위해 각 시도 사이에는 일정 시간의 대기 시간을 둡니다. 기본적으로 첫 번째와 두 번째 시도 사이에는 10초, 세 번째와 네 번째 사이에는 100초, 네 번째와 다섯 번째 사이에는 1000초를 대기하는 식으로 늘어납니다. 최대 대기 시간은 100,000초(약 27시간)입니다. 이 동작은 기본 제공되는 ExponentialBackoffStrategy에 구현되어 있습니다.

Spatie\WebhookServer\BackoffStrategy\BackoffStrategy 인터페이스를 구현한 클래스를 만들면 나만의 백오프 전략을 정의할 수 있습니다. 인터페이스는 다음과 같습니다.

namespace Spatie\WebhookServer\BackoffStrategy; interface BackoffStrategy { public function waitInSecondsAfterAttempt(int $attempt): int; }

커스텀 전략의 완전한 클래스명을 webhook-server 설정 파일의 backoff_strategy 값으로 지정하면 기본 전략으로 사용됩니다. 특정 웹훅에 한해서는 다음과 같이 전략을 지정할 수도 있습니다.

WebhookCall::create() ->useBackoffStrategy(YourBackoffStrategy::class) ... ->dispatch();

내부적으로 웹훅 재시도는 지연 디스패치(delayed dispatching)를 이용해 구현되어 있습니다. Amazon SQS는 최대 지연 시간에 제약이 있으므로, SQS를 큐로 사용한다면 시도 사이의 간격이 15분을 넘지 않도록 설정에 주의하세요.

HTTP 메서드 커스터마이징하기

기본적으로 모든 웹훅은 post 메서드를 사용합니다. webhook-server 설정 파일의 http_verb 키를 통해 이 값을 변경할 수 있습니다.

특정 호출에 한해 useHttpVerb 메서드로 기본값을 재정의할 수도 있습니다.

WebhookCall::create() ->useHttpVerb('get') ... ->dispatch();

헤더 추가하기

webhook-server 설정 파일의 headers 키에 값을 추가하면 모든 웹훅 요청에 해당 헤더가 포함됩니다. 특정 웹훅에만 추가 헤더를 넣고 싶다면 withHeaders 메서드를 사용하세요.

WebhookCall::create() ->withHeaders([ 'Another Header' => 'Value of Another Header' ]) ... ->dispatch();

프록시 사용하기

webhook-server 설정 파일의 proxy 키를 지정하면 웹훅 요청이 프록시를 경유하도록 설정할 수 있습니다. 특정 요청에만 프록시를 지정하려면 useProxy 메서드를 사용하세요.

WebhookCall::create() ->useProxy('http://proxy.server:3128') ...

상호 TLS(mTLS) 인증 사용하기

웹훅 데이터 전송의 무결성을 보장하려면, 웹훅 페이로드를 받는 대상이 실제로 의도한 수신자인지 인증하는 과정이 중요합니다. 상호 TLS 인증은 이를 위한 견고한 방법입니다. 클라이언트만 서버를 검증하는 일반적인 TLS와 달리, 상호 TLS는 웹훅 엔드포인트(클라이언트 역할)와 웹훅 제공자(서버 역할) 양쪽 모두가 서로를 인증하도록 요구합니다. 이는 TLS 핸드셰이크 과정에서 인증서를 교환함으로써 이루어지며, 양측 모두 상대방의 신원을 확인할 수 있게 됩니다.

참고: 자체 인증 기관(CA) 인증서를 포함해야 한다면, verifySsl() 메서드에 인증서 경로를 전달하세요.

WebhookCall::create() ->mutualTls( certPath: storage_path('path/to/cert.pem'), certPassphrase: 'optional_cert_passphrase', sslKeyPath: storage_path('path/to/key.pem'), sslKeyPassphrase: 'optional_key_passphrase' )

프록시 지정 형식은 guzzlehttp 프록시 형식을 따릅니다.

수신 앱의 SSL 인증서 검증하기

URL이 https://로 시작하는 경우, 이 패키지는 수신 측의 SSL 인증서가 유효한지 검증합니다. 인증서가 유효하지 않으면 웹훅 호출은 실패한 것으로 처리됩니다. 권장하지는 않지만, webhook-server 설정 파일의 verify_ssl 키를 false로 설정하면 이 검증을 비활성화할 수 있습니다.

특정 웹훅 호출에 한해서는 doNotVerifySsl 메서드로 검증을 비활성화할 수도 있습니다.

WebhookCall::create() ->doNotVerifySsl() ... ->dispatch();

메타 정보 추가하기

웹훅에 추가적인 메타 정보를 담을 수 있습니다. 이 메타 정보는 실제로 전송되지 않으며, 이 패키지가 발생시키는 이벤트에 전달되는 용도로만 사용됩니다.

메타 정보는 다음과 같이 추가할 수 있습니다.

WebhookCall::create() ->meta($arrayWithMetaInformation) ... ->dispatch();

태그 추가하기

큐 시스템으로 Laravel Horizon을 사용하고 있다면, 태그 기능도 지원한다는 점을 알아두면 좋습니다.

웹훅 호출을 수행하는 Job에 태그를 추가하려면, webhook-server 설정 파일의 tags 키에 지정하거나 withTags 메서드를 사용하세요.

WebhookCall::create() ->withTags($tags) ... ->dispatch();

예외 처리

기본적으로 이 패키지는 웹훅 전송 중 발생한 예외를 별도로 로그에 남기지 않습니다.

예외를 처리하려면 Spatie\WebhookServer\Events\WebhookCallFailedEvent 및/또는 Spatie\WebhookServer\Events\FinalWebhookCallFailedEvent 이벤트에 대한 리스너를 만들어야 합니다.

실패한 실행 재시도

기본적으로 실패한 Job은 그대로 무시됩니다. Job의 마지막 시도가 실패했을 때 예외를 발생시키고 싶다면 throwExceptionOnFailure를 호출하세요.

WebhookCall::create() ->throwExceptionOnFailure() ... ->dispatch();

또는 webhook-server 설정 파일의 전역 옵션 throw_exception_on_failure를 활성화해도 됩니다.

JSON 대신 원시 문자열 본문 전송하기

기본적으로 모든 웹훅은 페이로드를 JSON으로 변환하여 전송합니다. JSON 대신 임의의 문자열을 전송하고 싶다면 sendRawBody(string $body) 옵션을 사용하세요.

Signer API의 타입 불일치 문제로 인해, 현재는 원시 데이터 요청에 대한 서명 기능은 지원되지 않습니다. sendRawBody 옵션을 사용하면 WebhookEvents에서 페이로드를 문자열(string) 형태로 받게 됩니다.

WebhookCall::create() ->sendRawBody("<root>someXMLContent</root>") ->doNotSign() ... ->dispatch();

이벤트

이 패키지는 다음 이벤트를 발생시킵니다.

  • DispatchingWebhookCallEvent: 웹훅 호출이 큐에 디스패치되기 직전에 발생합니다.
  • WebhookCallSucceededEvent: 원격 앱이 2xx 응답 코드를 반환했을 때 발생합니다.
  • WebhookCallFailedEvent: 원격 앱이 2xx가 아닌 응답 코드를 반환했거나 아예 응답하지 않았을 때 발생합니다.
  • FinalWebhookCallFailedEvent: 웹훅 호출의 마지막 시도까지 실패했을 때 발생합니다.

이 이벤트들은 모두 다음 속성을 가지고 있습니다.

  • httpVerb: 요청에 사용된 HTTP 메서드
  • webhookUrl: 요청이 전송된 URL
  • payload: 사용된 페이로드
  • headers: 전송된 헤더 목록. 서명 헤더도 포함됩니다.
  • meta: meta 호출을 통해 웹훅에 전달한 값의 배열
  • tags: 사용된 태그 배열
  • uuid: 해당 호출을 식별하는 고유 문자열입니다. 하나의 웹훅 호출에 대한 모든 시도는 동일한 uuid를 가집니다.

DispatchingWebhookCallEvent를 제외한 나머지 이벤트는 다음 속성을 추가로 가집니다.

  • attempt: 시도 횟수
  • response: 원격 앱이 반환한 응답. \GuzzleHttp\Psr7\Response의 인스턴스이거나 null일 수 있습니다.

테스트

composer test

웹훅 테스트하기

자동화된 테스트에서 이 패키지를 사용할 때는, 실제 웹사이트로 웹훅이 전송되지 않도록 다음 중 한 가지 방법을 사용하는 것이 좋습니다.

Bus

use Illuminate\Support\Facades\Bus; use Spatie\WebhookServer\CallWebhookJob; use Tests\TestCase; class TestFile extends TestCase { public function testJobIsDispatched() { Bus::fake(); ... 웹훅 호출 수행 ... Bus::assertDispatched(CallWebhookJob::class); } }

Queue

use Illuminate\Support\Facades\Queue; use Spatie\WebhookServer\CallWebhookJob; use Tests\TestCase; class TestFile extends TestCase { public function testJobIsQueued() { Queue::fake(); ... 웹훅 호출 수행 ... Queue::assertPushed(CallWebhookJob::class); } }

변경 이력

최근 변경 사항은 CHANGELOG에서 확인할 수 있습니다.

기여하기

자세한 내용은 CONTRIBUTING 문서를 참고하세요.

보안

보안 관련 문제를 발견하셨다면, 이슈 트래커 대신 freek@spatie.be로 이메일을 보내주세요.

Postcardware

이 패키지는 자유롭게 사용하실 수 있습니다. 다만 프로덕션 환경에 적용하셨다면, 사용 중인 패키지를 언급하며 고향에서 엽서를 보내주시면 정말 감사하겠습니다.

주소: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium

받은 엽서는 모두 저희 회사 웹사이트에 게시하고 있습니다.

크레딧

라이선스

MIT 라이선스(MIT)를 따릅니다. 자세한 내용은 라이선스 파일을 참고하세요.