GitHub에서 이슈를 열었는데 연결한 알림이나 자동화가 실행되지 않는다면, 먼저 Recent deliveries의 전송 기록을 확인합니다. GitHub가 요청을 보냈는지, 서버가 받았는지, 받은 뒤 업무 처리를 끝냈는지 순서대로 좁혀 가야 합니다. 재전송부터 누르면 이미 끝난 작업이 다시 실행될 수도 있습니다.
이 글은 GitHub.com의 저장소 웹훅을 기준으로 전송 기록 읽기, 실패 복구, 중복 처리 점검 순서를 정리했습니다. 본문의 운영 사례와 확인표는 설명을 위한 가상 상황입니다. 2026년 10월 3일에는 서명·전송 ID 관련 공식 문서를 다시 확인하고, 3절에 합성 데이터로 직접 실행한 로컬 검증 예제를 추가했습니다. 이 예제도 실제 GitHub 전송이나 서버 장애, 알림 발송을 시험한 결과는 아닙니다.
1. 전송 기록에서 시작점을 찾습니다
저장소의 Settings → Webhooks → 해당 웹훅 URL → Recent deliveries로 이동합니다. 전송 항목의 GUID를 누르면 요청 헤더와 본문, 전송 시각, 서버가 돌려준 응답을 확인할 수 있습니다. 저장소 웹훅의 기록 조회에는 해당 저장소의 관리자 권한이 필요합니다.
이 화면에서 조회할 수 있는 범위는 최근 3일입니다. 오래된 장애를 조사하면서 목록이 비었다는 이유만으로 전송 자체가 없었다고 결론 내리면 안 됩니다. 최근 이벤트도 화면에 나타나기까지 몇 분 걸릴 수 있습니다. 발생 시각과 시간대를 적어 두고 잠시 뒤 다시 확인하세요. 조회 범위와 메뉴 경로는 GitHub 전송 기록 안내를 기준으로 했습니다.
- 기록이 없음: 이벤트를 만든 저장소, 구독한 이벤트 종류, 웹훅의 활성 상태부터 확인합니다.
- 실패 기록이 있음: 응답 코드와 오류 내용을 읽고, 같은 시각의 프록시·서버 로그를 찾습니다.
- 2xx인데 결과가 없음: 요청을 받은 뒤의 필터 조건, 작업 대기열, 처리 프로그램의 실패를 확인합니다.
기록이 없다면 조직의 OAuth 앱 접근 제한이나 이벤트별 제한도 확인할 대상입니다. 예를 들어 GitHub는 웹훅 본문 크기를 25MB로 제한하며, 이를 넘는 본문은 전달하지 않는다고 안내합니다. 반복해서 같은 대용량 작업을 실행하기 전에 이벤트와 본문 제한 및 누락 점검 절차를 확인합니다.
2. 실패 응답과 처리 지연을 구분합니다
응답 코드만으로 원인을 확정하지는 않습니다. 같은 403도 프록시 차단, 서버의 서명 검증 실패, 애플리케이션의 접근 규칙 등 서로 다른 지점에서 나올 수 있습니다. 전송 상세의 응답 본문과 서버 로그를 함께 봐야 어느 구간을 고칠지 정할 수 있습니다.
| 전송 기록에서 본 내용 | 먼저 확인할 것 |
| 연결 실패 | 도메인 해석, 연결 경로, 서버의 수신 여부 |
| TLS 인증서 오류 | 인증서 유효성 및 서버가 보내는 인증서 체인 |
| 4xx 또는 5xx | 어느 구성 요소가 응답했는지와 그 오류 로그 |
| Timed out | 응답 전에 오래 걸리는 작업을 수행했는지 |
| 2xx인데 알림이 없음 | 접수 이후의 작업 상태와 알림 대상 선정 조건 |
GitHub는 수신 서버가 10초 안에 2xx로 응답하도록 안내합니다. 그보다 오래 걸리면 연결을 종료하고 전송 실패로 판단합니다. 보고서 생성이나 외부 API 호출을 응답 전에 모두 마치려는 구조라면, 요청을 안전하게 접수한 뒤 별도 작업으로 처리하는 방식을 검토할 수 있습니다. GitHub 웹훅 권장 사항에도 비동기 처리를 위한 대기열 사용이 설명되어 있습니다.
다만 200을 먼저 보내고 작업을 메모리에만 남기면, 직후 서버가 꺼졌을 때 복구할 근거가 사라질 수 있습니다. 이 글에서 권하는 접수 기준은 재시작 뒤에도 꺼낼 수 있는 저장소에 작업이 남았는가입니다. 접수 성공과 실제 업무 완료를 각각 기록하면, GitHub 화면은 성공인데 알림이 없는 상황도 추적하기 쉬워집니다.
3. 서명 오류는 원본 본문부터 확인합니다
웹훅에 secret이 설정되어 있다면 X-Hub-Signature-256 헤더로 본문의 HMAC-SHA256 서명을 검증합니다. secret이 없으면 이 헤더도 전달되지 않습니다. GitHub는 오래된 SHA-1 헤더보다 이 헤더를 사용할 것을 권장합니다.
검증에는 수신한 원본 본문 바이트를 사용합니다. JSON을 객체로 바꾼 뒤 다시 문자열로 만들면 공백이나 표현이 달라질 수 있습니다. 프록시나 미들웨어가 본문을 바꿨는지, UTF-8 처리가 맞는지, 검증에 사용한 secret이 해당 웹훅의 것인지 확인합니다. 비교에는 일반 문자열 비교 대신 언어가 제공하는 상수 시간 비교 함수를 사용합니다. 자세한 기준은 GitHub 서명 검증 문서에서 확인할 수 있습니다.
오류를 없애려고 검증을 건너뛰지는 마세요. 전송 ID나 User-Agent만 보고 GitHub 요청이라고 믿는 것도 피합니다. 진단 로그에는 secret, 인증 헤더, 전체 본문을 무심코 출력하지 않고, 필요한 식별자와 검증 성공 여부를 남기는 편이 좋습니다.
직접 실행해 보기: 원본 바이트와 같은 전송 ID
아래 코드는 2026년 10월 3일 Linux·CPython 3.12.14에서 실행했습니다. 공개된 가짜 키, 합성 JSON, 가상 전송 ID만 사용하는 오프라인 예제입니다. 서버나 네트워크 연결, 실제 알림 발송 없이 Python 표준 라이브러리만 사용합니다. UTF-8로 article_example.py에 저장한 뒤 python3 article_example.py로 실행하세요. 이 키를 실제 웹훅에 사용하지 마세요.
import hashlib
import hmac
import json
import re
# 공개된 가짜 키와 합성 본문입니다. 실제 웹훅에 사용하지 마세요.
key = b"FAKE_LOCAL_FIXTURE_ONLY_20261003_NOT_A_REAL_SECRET"
body = '{"action":"opened","issue":{"number":7,"title":"로컬 검증"}}'.encode("utf-8")
def valid(raw, header, secret=key):
if not isinstance(header, str) or not re.fullmatch(r"sha256=[0-9a-f]{64}", header):
return False
expected = "sha256=" + hmac.new(secret, raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
header = "sha256=" + hmac.new(key, body, hashlib.sha256).hexdigest()
changed = json.dumps(json.loads(body), ensure_ascii=False).encode("utf-8")
print("bytes:", len(body), len(changed))
print("same_json:", json.loads(body) == json.loads(changed))
for name, raw, sig, secret in [
("original", body, header, key),
("tampered", body.replace(b'"number":7', b'"number":8'), header, key),
("newline", body + b"\n", header, key),
("reserialized", changed, header, key),
("missing_header", body, None, key),
("wrong_secret", body, header, b"FAKE_WRONG_KEY"),
]:
print(name + ":", valid(raw, sig, secret))
print("signature_twice:", [valid(body, header) for _ in range(2)])
# 순차 실행·메모리 모형입니다. 실제 알림, 영구 저장, 동시성 처리는 없습니다.
completed, effects, outcomes = set(), 0, []
delivery = "00000000-0000-4000-8000-000000000001" # 가상 ID
for delivery_id in (delivery, delivery):
if not valid(body, header):
outcomes.append("rejected")
elif delivery_id in completed:
outcomes.append("skipped_completed")
else:
effects += 1 # 실제 발송 대신 정수만 증가
completed.add(delivery_id)
outcomes.append("processed")
print("delivery_twice:", outcomes, "effects:", effects)
일반 실행과 격리 모드 실행에서 다음 출력이 같았습니다. original부터 wrong_secret까지는 True가 서명 검증 성공, False가 실패를 뜻합니다.
bytes: 64 70
same_json: True
original: True
tampered: False
newline: False
reserialized: False
missing_header: False
wrong_secret: False
signature_twice: [True, True]
delivery_twice: ['processed', 'skipped_completed'] effects: 1
이 예제에서는 JSON 값이 같아도 다시 직렬화하면서 공백이 늘어 원본 64바이트가 70바이트가 됐고, 원래 서명은 더 이상 맞지 않았습니다. 이슈 번호 변경, 끝 줄바꿈 추가, 헤더 누락, 다른 키도 검증에 실패했습니다. 사용하는 프레임워크에서 원본 본문을 읽는 위치는 별도로 확인해야 합니다.
같은 본문과 서명은 두 번 모두 검증에 성공했습니다. 완료 ID 집합을 추가한 순차 모형에서는 두 번째 호출을 건너뛰어 정수만 한 번 증가했습니다. GitHub가 재전송할 때 원래 전송 ID가 유지된다는 안내를 바탕으로 만든 모형이며, 실제 재전송을 관측한 결과는 아닙니다.
운영용 중복 처리 코드로 배포하지 마세요. 본문 HMAC은 전송 ID 헤더를 포함하지 않으며, 이 집합은 ID가 달라지거나 프로세스가 재시작되면 같은 업무를 다시 처리할 수 있습니다. 동시 요청, 영구 저장, 중단 복구, 외부 발송 결과 불명은 검증하지 않았습니다. 5절의 처리 상태·원자적 접수·업무 단위 멱등성 설계가 여전히 필요합니다. 이 공개 키로 GitHub 발신 여부나 서비스 보안성을 입증할 수도 없습니다. 비교 함수의 기준은 Python hmac 공식 문서를 참고하세요.
4. 원인을 고친 뒤 한 건부터 재전송합니다
GitHub는 실패한 웹훅을 자동으로 재전송하지 않습니다. 서버를 복구한 뒤 기다리기만 해서는 누락된 업무가 돌아오지 않을 수 있습니다. 최근 3일의 전송분은 전송 상세에서 Redeliver를 눌러 다시 보낼 수 있습니다. 저장소 웹훅은 REST API를 통한 재전송도 지원하지만, 처음 복구할 때는 범위를 식별하기 쉬운 한 건부터 확인하는 편이 좋습니다. 공식 재전송 안내
- 실패한 전송의 ID, 발생 시각, 이벤트와 처리 대상부터 적습니다.
- 서버의 실패 원인을 고치고, 같은 작업이 이미 완료됐는지 업무 이력을 확인합니다.
- 재전송 한 건을 실행한 뒤 새 전송 결과와 서버의 접수 기록을 비교합니다.
- 최종 업무 결과까지 확인한 다음, 나머지 실패 건의 복구 범위를 정합니다.
특히 타임아웃은 주의해야 합니다. GitHub가 응답을 받지 못한 동안 서버에서는 이미 작업을 마쳤을 수 있습니다. 알림 발송이나 배포처럼 결과가 바뀌는 작업은, 실패 표시만 보고 전체를 다시 실행하지 않습니다. 3일을 지난 건은 이 화면의 재전송으로 해결하려 하지 말고, 보유한 처리 이력과 GitHub의 현재 상태를 대조해 별도 복구가 필요한 항목을 정합니다.
5. 전송 ID와 업무 완료 상태를 함께 저장합니다
X-GitHub-Delivery는 이벤트를 식별하는 GUID이며, 같은 전송을 재전송하면 원래 값이 유지됩니다. X-GitHub-Event는 이벤트 종류, X-GitHub-Hook-ID는 웹훅의 식별자입니다. action이 있는 이벤트는 본문의 해당 값까지 확인해 처리 대상을 정합니다. 헤더의 의미는 전송 헤더 문서, 재전송 시 ID 유지 여부는 전송 ID 권장 사항에 나와 있습니다.
중복 방지를 만들 때는 “ID가 있으면 무조건 건너뛰기”에서 한 단계 더 생각해야 합니다. 접수 직후 ID만 저장하고 작업 전에 중단됐다면, 이후의 정상 재전송까지 버릴 수 있기 때문입니다. 아래는 운영 설계를 위한 예시이며 GitHub가 정한 상태 이름은 아닙니다.
- 접수됨: 영구 저장은 끝났지만 아직 업무를 시작하지 않았습니다.
- 처리 중: 한 작업자가 맡아 실행하고 있습니다.
- 완료: 업무 결과까지 확인되어 같은 작업을 다시 실행하지 않습니다.
- 실패: 실패 지점을 확인하고 안전한 재시도 방법을 결정합니다.
- 결과 확인 필요: 외부 서비스가 처리했는지 알 수 없어 조회나 수동 확인이 필요합니다.
여러 수신 프로세스가 있다면 데이터베이스의 유일성 제약이나 원자적 등록으로 같은 전송을 동시에 새 작업으로 만들지 않도록 설계합니다. 접수 기록과 실행할 작업도 함께 보존되어야 합니다. 작업자 중단 시 인계를 위한 기준을 두고, “처리 중” 상태가 끝없이 남는지도 살핍니다. 요청 식별자의 저장과 실제 변경을 따로 수행할 때 생기는 문제는 AWS의 멱등성 설명에서도 다룹니다.
외부 발송이 성공한 직후 완료 기록 저장에 실패하는 구간은 별도로 다뤄야 합니다. 공급자가 지원하는 멱등성 키나 결과 조회 기능을 확인하세요. 이름이나 본문이 같다는 이유만으로 중복이라고 판단하지 말고, 업무 대상과 변경 버전 등 실제로 같은 작업을 나타내는 기준을 정합니다. 수신 ID 저장만으로 모든 외부 작업의 정확히 한 번 실행이 보장되지는 않습니다.
6. 가상 사례로 복구 판단을 연습합니다
이슈 알림 한 건이 타임아웃으로 표시됐다고 가정해 보겠습니다. 아래 D-001은 설명용으로 줄인 가상 식별자이며, 실제 GUID나 관측 로그가 아닙니다. 표의 내용은 구현 후 확인해야 할 기대 동작입니다.
| 확인한 상황 | 기대하는 처리 |
| D-001의 알림이 이미 완료됨 | 재전송을 받아도 알림을 새로 보내지 않음 |
| D-001은 저장됐지만 작업이 시작되지 않음 | 기존 작업을 이어 처리하고 새 작업을 중복 생성하지 않음 |
| D-001의 외부 발송 결과를 모름 | 발송 이력을 확인한 뒤 복구 여부 결정 |
| 같은 전송이 동시에 두 번 들어옴 | 업무를 수행할 작업자가 중복으로 선정되지 않음 |
순서 문제도 함께 봅니다. GitHub는 이벤트 발생 순서대로 웹훅이 도착한다고 보장하지 않습니다. 닫힌 이슈에 대해 늦게 도착한 이전 이벤트를 처리한다면, 필요한 시점의 현재 상태나 본문의 시각 정보를 확인하는 규칙이 있어야 합니다. 위 표는 동시성이나 장애 복구를 실제로 검증했다는 뜻이 아니므로, 사용하는 저장소와 작업 대기열에서 별도 시험해야 합니다.
7. 복구 완료는 업무 결과로 판단합니다
마지막에는 전송 화면의 성공 표시와 실제 결과를 대조합니다. 장애 기록에 아래 항목을 남겨 두면, 다음 담당자도 무엇이 복구됐고 무엇이 남았는지 확인할 수 있습니다.
- 발생 시각과 시간대, 대상 저장소, 웹훅 ID, 전송 ID
- 이벤트 종류와 action, GitHub가 받은 응답 코드
- 서버의 접수 상태, 작업 ID, 최종 처리 결과
- 재전송한 범위와 횟수, 중복으로 건너뛴 작업
- 아직 결과를 확인하지 못한 건과 다음 확인 방법
정리하면 전송 기록 확인 → 실패 구간 수정 → 기존 업무 결과 확인 → 필요한 건만 재전송 → 중복과 최종 결과 점검 순서입니다. HTTP 응답을 읽는 기본 방법은 curl 응답 확인: 상태 코드·헤더·본문·종료 코드 구분하기, 알림의 중복·취소·결과 불명 상황은 예약 알림 자동화, 도입 전에 확인할 8가지 상황과 검증표를 함께 참고할 수 있습니다.
'개발 문제 해결' 카테고리의 다른 글
| Claude Opus 5.5 API 전환 가이드: 400 오류 나는 설정 4가지와 수정 코드 (0) | 2026.09.25 |
|---|---|
| 앱 날짜가 하루 달라 보일 때: 저장 시각과 표시 시간대 구분하기 (0) | 2026.09.23 |
| HTTP 429가 보일 때: Retry-After를 읽고 요청 간격 정하기 (0) | 2026.09.23 |
| git stash 사용법: 작업 중인 수정을 잠깐 치우고 다시 꺼내는 순서 (0) | 2026.09.18 |
| Git worktree 사용법: 작업 중인 코드를 그대로 두고 다른 브랜치 열기 (0) | 2026.09.18 |
댓글