케이스 스터디
CASE STUDY — 운영

결제 → 세금계산서 → 알림톡, 사람 개입 0 — 토스페이먼츠 웹훅을 큐로 받는 법

도입 B2B 서비스 / 쇼핑몰구축 5일
토스페이먼츠 Webhook전자세금계산서 API알림톡n8nPostgres

결제는 이미 자동이었습니다. 그런데 그 다음이 전부 수기였습니다. 세금계산서를 발행하고, 거래처에 알리고, 입금을 확인하는 일을 매번 사람이 했습니다.

세 가지 모두 API가 있습니다. 아무도 이어붙이지 않았을 뿐입니다. 그런데 이걸 웹훅 하나에 일렬로 연결하면 거의 확실히 사고가 납니다. 이 케이스의 절반은 그 이유와, 그걸 피하는 구조에 대한 이야기입니다.

payment-taxinvoice.workflowLIVE
WH결제 웹훅200즉시 응답 · 큐 적재TI세금계산서 발행AT알림톡 통보

왜 한 줄로 이으면 안 되는가

토스페이먼츠 웹훅 문서에는 두 가지 제약이 명시돼 있습니다. 이 두 문장이 이 케이스의 전체 설계를 결정합니다.

  • 응답 시간 — 문서 원문: *"10 초 이내로 200 응답을 보내주세요."* 그런데 세금계산서 발행 API와 알림톡 발송 API를 순서대로 호출하면 10초를 넘길 수 있습니다. 상대 서버가 느린 날에는 확실히 넘깁니다.
  • 재시도 — 응답이 없거나 실패하면 최대 7회 재시도합니다. 간격은 1분 → 4분 → 16분 → 64분 → 256분 → 1024분 → 4096분으로 늘어나고, 문서 표현으로 마지막 재시도는 *"최초 전송으로부터 3일 19시간 후"* 입니다.
  • 둘을 합치면 최악의 시나리오가 보입니다. 발행은 성공했는데 응답이 10초를 넘겨 실패로 간주되면, 토스는 같은 결제를 다시 보냅니다. 그때마다 세금계산서가 또 발행됩니다. 세금계산서 중복 발행은 취소·재발행 절차가 따르는 실제 사고입니다.
그래서 이 워크플로우는 두 개로 나뉩니다. 위쪽 웹훅 플로우는 검증하고 큐에 넣고 즉시 200을 응답합니다. 아래쪽 스케줄 플로우가 느린 벤더 호출을 따로 처리합니다. 웹훅은 절대 벤더 API를 기다리지 않습니다.

어떻게 동작하나

  1. Webhook 노드가 토스의 PAYMENT_STATUS_CHANGED 이벤트를 받습니다. 토스는 상태가 바뀔 때마다 이 이벤트를 보내므로 결제 완료(`DONE`)만 골라냅니다.
  2. Respond to Webhook 노드가 곧바로 200을 응답합니다. 이후 처리가 얼마나 걸리든 토스 입장에서는 이미 성공입니다. 10초 제한을 원천적으로 벗어납니다.
  3. Code 노드가 페이로드를 정리하고 paymentKey를 멱등키로 삼습니다. DONE이 아니거나 paymentKey가 없으면 빈 배열을 반환해 조용히 끝냅니다.
  4. Postgres 노드가 invoice_queue 테이블에 ON CONFLICT DO NOTHING으로 적재합니다. 같은 결제가 재전송돼도 행이 늘지 않습니다. 중복 발행을 막는 지점이 정확히 여기입니다.
  5. 별도의 Schedule Trigger가 5분마다 큐를 훑어 pending 행을 한 번에 20건까지 집어가면서 동시에 `processing`으로 바꿉니다. 한 문장으로 처리하므로 실행이 겹쳐도 같은 행을 두 번 집지 않습니다.
  6. 세금계산서 발행 API를 호출하고, 이어서 알림톡(또는 문자)을 발송한 뒤 해당 행을 done으로 표시합니다. 실패하면 attempts가 올라가고 5회를 넘으면 더 이상 집지 않습니다.

필요한 것

  • n8n — 자체 호스팅 또는 Cloud. 웹훅을 외부에서 받아야 하므로 인터넷에서 접근 가능한 주소가 필요합니다. 로컬 개발 중이라면 터널링 도구를 쓰거나, n8n Cloud를 쓰세요.
  • Postgres 데이터베이스 하나 — 큐 테이블 하나를 만듭니다.
  • 토스페이먼츠 계정 — 웹훅 URL은 개발자센터의 웹훅 메뉴(developers.tosspayments.com/my/webhooks)에서 등록합니다. 테스트 키는 test로 시작하며, 문서에 따르면 전자결제 신청 전이라도 "개발 연동 체험 상점의 일부 테스트 키"를 확인할 수 있습니다 — 즉 개발·테스트 단계에서는 사업자등록 없이 진행할 수 있습니다. 인증은 시크릿 키를 이용한 Basic 인증이며, 키 뒤의 :까지 포함해 인코딩해야 합니다.
  • 전자세금계산서 발행 대행 서비스 계정 — 팝빌·바로빌 등. 홈택스에 직접 연동하는 방식은 권하지 않습니다(아래 예외 상황 참고). 팝빌의 경우 테스트 환경(test.popbill.com)이 별도로 있고, 문서상 *"파트너는 연동신청 시 입력한 계정정보(ID/PW)로 팝빌 테스트 환경과 운영 환경에 자동으로 가입"* 됩니다. 테스트용 공동인증서 신청도 안내되어 있습니다. 다만 연동신청에 사업자 정보가 필요한지는 공식 문서에서 확인되지 않았으므로, 착수 전 해당 벤더에 직접 문의하세요.
  • 알림톡을 쓸 경우 — 공식 딜러사 계약이 먼저입니다 — 카카오 공식 문서에 명시돼 있습니다: *"알림톡은 카카오와 파트너 계약이 되어있는 공식 딜러사를 통해 진행 가능합니다."* 즉 카카오에 직접 연동할 수 없고, 공식 딜러사 중 한 곳과 계약해야 합니다. 이 케이스에서 메시징 노드를 특정 벤더로 못 박지 않은 이유이기도 합니다 — 어느 딜러사를 쓰느냐에 따라 API가 달라집니다. 그리고 채널 조건도 있습니다: 카카오톡 채널을 개설해 비즈니스 채널로 전환하고, 홈공개를 ON으로 두고, 고객센터 정보를 입력해야 합니다. 메시지 자체도 *"정보통신망법에서 정한 영리목적 광고성 정보의 예외에 해당하는 정보성 메시지"*만 가능하며 광고성 메시지는 보낼 수 없습니다 — 결제·발행 통보는 여기에 해당합니다. 준비가 안 됐다면 이 단계를 문자(SMS)로 대체해도 워크플로우 구조는 그대로입니다.

1단계 — 큐 테이블 만들기

CREATE TABLE IF NOT EXISTS invoice_queue (
  payment_key    text PRIMARY KEY,          -- 멱등키. 중복 웹훅 방어의 핵심
  order_id       text,
  amount         numeric     NOT NULL DEFAULT 0,
  approved_at    timestamptz,
  customer_email text,
  status         text        NOT NULL DEFAULT 'pending',  -- pending|processing|done
  attempts       integer     NOT NULL DEFAULT 0,          -- 무한 재시도 방지
  created_at     timestamptz NOT NULL DEFAULT now(),
  updated_at     timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS invoice_queue_status_idx
  ON invoice_queue (status, created_at);
payment_key를 PRIMARY KEY로 둔 것이 이 설계에서 가장 중요한 한 줄입니다. 애플리케이션 코드가 아니라 데이터베이스 제약조건이 중복을 막습니다. 워크플로우에 버그가 있어도, 실행이 겹쳐도, 토스가 같은 웹훅을 일곱 번 보내도 행은 하나뿐입니다.

2단계 — 큐에 넣는 쿼리

INSERT INTO invoice_queue
  (payment_key, order_id, amount, approved_at, customer_email, status)
VALUES
  ('{{ $json.payment_key }}', '{{ $json.order_id }}', {{ $json.amount }},
   '{{ $json.approved_at }}', '{{ $json.customer_email }}', 'pending')
ON CONFLICT (payment_key) DO NOTHING;

재전송된 웹훅이 도착하면 INSERT 0 0이 반환되고 아무 일도 일어나지 않습니다. 에러가 아니라 정상적인 무시입니다. 워크플로우는 조용히 성공으로 끝납니다.

3단계 — 큐를 집어가는 쿼리

5분마다 도는 처리기가 쓰는 쿼리입니다. UPDATE ... RETURNING 한 문장으로 집어가기와 상태 변경을 동시에 합니다. 두 동작을 나누면 그 사이에 다른 실행이 같은 행을 집어갈 수 있습니다.

UPDATE invoice_queue
SET status = 'processing', attempts = attempts + 1, updated_at = now()
WHERE payment_key IN (
  SELECT payment_key FROM invoice_queue
  WHERE status = 'pending' AND attempts < 5
  ORDER BY created_at
  LIMIT 20
  FOR UPDATE SKIP LOCKED       -- 다른 실행이 잠근 행은 건너뜀
)
RETURNING payment_key, order_id, amount, approved_at, customer_email, attempts;
FOR UPDATE SKIP LOCKED가 동시 실행 안전장치입니다. 처리가 5분보다 오래 걸려 다음 실행이 겹쳐도, 두 번째 실행은 잠긴 행을 기다리지 않고 건너뜁니다. attempts < 5는 계속 실패하는 행이 큐를 영원히 막는 것을 방지합니다.

4단계 — 벤더 API 두 개 채우기

다운로드한 워크플로우에서 Issue Tax InvoiceSend AlimTalk / SMS 두 노드는 일부러 비워 두었습니다. 요청 본문 형식이 벤더마다 다르고, 이 문서에서 특정 벤더의 스키마를 옮겨 적으면 벤더가 API를 바꾸는 순간 틀린 문서가 되기 때문입니다. 각자 쓰는 서비스의 API 레퍼런스를 보고 URL·인증 헤더·본문을 채우세요. 워크플로우의 나머지 구조는 그대로 쓰시면 됩니다.

  • 세금계산서 발행 노드 — 타임아웃을 20초로 잡아 두었습니다. 큐 방식이라 오래 걸려도 웹훅에는 영향이 없습니다.
  • 알림톡 노드 — 승인된 템플릿 ID가 필요합니다. 발신프로필이 아직 없다면 이 노드를 문자 발송으로 바꾸세요. 앞뒤 구조는 동일합니다.
  • 두 노드 사이의 순서가 중요합니다 — 반드시 발행이 성공한 뒤에 통보해야 합니다. 발행 실패인데 "발행되었습니다" 알림이 나가면 그게 더 큰 사고입니다.

알림톡 템플릿을 설계할 때 반드시 알아야 할 것

이 부분에서 가장 많이 시간을 잃습니다. 공식 가이드에 명시된 제약들입니다.

  • 등록이 완료된 템플릿은 수정이 불가능합니다. 문구 하나를 고치려면 새 템플릿을 만들어 다시 심사를 받아야 합니다. 그러므로 처음 설계할 때 변수를 넉넉히 잡아두세요. 금액만 넣었다가 나중에 주문번호를 추가하고 싶어지는 일이 반드시 생깁니다.
  • 심사는 영업일 기준 2일 이내에 순차 처리됩니다. 원문: *"심사 요청하신 템플릿은 영업일 기준 2일 이내 순차 처리되며"*. 반려되면 수정 후 재심사를 받아야 하므로, 일정에는 왕복을 감안해 여유를 두세요.
  • 반려 사유로 명시된 것들 — 변수 오류, 과도한 변수 사용(40개 초과), 변수로만 이루어진 템플릿 내용, 변수가 포함된 버튼명, 변수가 포함된 미리보기 메시지 설정. 특히 '변수로만 이루어진 내용'이 흔한 실수입니다. 고정 문구 없이 변수만 나열하면 반려됩니다.
  • 대체 발송 설정은 템플릿과 별도로 관리되므로 언제든 변경할 수 있습니다. 템플릿은 못 고쳐도 문자 대체 설정은 나중에 조정할 수 있다는 뜻입니다.
⬇︎ 워크플로우 다운로드 (payment-taxinvoice.json)
Postgres 자격증명 하나와 큐 테이블만 있으면 웹훅 수신·큐 적재·큐 처리 구조가 그대로 돕니다. 벤더 API 두 노드는 각자 서비스의 레퍼런스로 채우세요.
검증 범위를 정확히 밝힙니다. 이 케이스에 인용된 토스페이먼츠의 응답 제한(10초), 재시도 정책(최대 7회, 1·4·16·64·256·1024·4096분 간격, 최초 전송으로부터 3일 19시간), 이벤트 10종, 테스트 키가 test로 시작한다는 점, 그리고 알림톡의 '등록 완료 템플릿 수정 불가'·'승인된 템플릿만 발송'·'영업일 기준 2일 이내 심사'·반려 사유 목록은 2026-08-15에 각 공식 문서를 직접 열어 확인한 내용입니다. 큐 동작(중복 payment_key 재적재 차단, attempts 소진 행 제외, 겹친 실행에서 같은 행 재처리 안 됨)은 PostgreSQL 17에서 픽스처로 실행해 확인했습니다. 확인하지 못한 것: 팝빌 연동신청에 사업자 정보가 필요한지, 알림톡 발신프로필 개설에 사업자등록이 필요한지 — 두 가지 모두 공식 문서에서 근거를 찾지 못해 본문에 단정하지 않았습니다. 실제 결제·발행·발송을 관통하는 종단 테스트도 수행하지 않았습니다. 마지막 검증: 2026-08-15.

설정 순서 (40분)

  1. 큐 테이블 생성 — 1단계 SQL을 실행합니다.
  2. Postgres 자격증명 등록 — n8n → Credentials → PostgresTest로 초록불 확인. n8n이 Docker 안이고 DB가 호스트면 호스트에 host.docker.internal을 넣으세요.
  3. 워크플로우 import — 다운로드한 JSON을 n8n → Workflows → ...Import from File.
  4. 웹훅 URL 확인Toss Webhook 노드를 열면 Test URL과 Production URL이 보입니다. Production URL을 복사하세요. 워크플로우를 Activate 해야 이 주소가 살아납니다.
  5. 토스에 웹훅 등록 — 개발자센터 → 웹훅 메뉴 → 웹훅 등록하기 → 방금 복사한 Production URL 입력 → 이벤트로 PAYMENT_STATUS_CHANGED 선택.
  6. 세금계산서 벤더 준비 — 사용할 서비스(팝빌·바로빌 등)의 테스트 환경에 가입하고, 테스트용 인증 정보와 필요 시 테스트 공동인증서를 발급받습니다. 이 단계가 이 케이스에서 리드타임이 가장 깁니다. 먼저 시작하세요.
  7. 발행 노드 채우기Issue Tax Invoice 노드에 벤더 레퍼런스대로 URL·인증 헤더·요청 본문을 넣습니다.
  8. 알림톡 준비 또는 문자 대체 — 발신프로필과 템플릿 심사에는 며칠이 걸립니다. 그 사이에는 Send AlimTalk / SMS 노드를 문자 발송으로 두고 개발을 진행하세요.
  9. 두 트리거 모두 Activate — 웹훅 플로우와 스케줄 플로우 둘 다 켜져 있어야 합니다. 하나만 켜면 큐에 쌓이기만 하거나, 아무것도 안 들어옵니다.
  10. 테스트 결제로 확인 — 테스트 키로 결제를 한 건 발생시킨 뒤 SELECT * FROM invoice_queue ORDER BY created_at DESC LIMIT 5;로 행이 들어왔는지 봅니다. 그다음 5분을 기다리거나 스케줄 워크플로우에서 Execute Workflow를 눌러 처리되는지 확인합니다.
  11. 중복 방어 확인 — 같은 결제의 웹훅을 한 번 더 보내보고 invoice_queue의 행 수가 그대로인지 확인합니다. 이 확인을 꼭 하세요. 여기가 뚫리면 세금계산서가 중복 발행됩니다.

예외 상황이 있으면 어떻게 되나

  • 같은 웹훅이 여러 번 도착ON CONFLICT DO NOTHING으로 두 번째부터는 무시됩니다. 토스가 최대 7회까지 재시도하므로 이 방어가 반드시 있어야 합니다.
  • 발행은 성공했는데 알림톡이 실패 — 행이 done으로 넘어가지 않고 attempts만 올라, 다음 회차에 다시 시도됩니다. 그러면 세금계산서가 또 발행될 수 있습니다. 실전에서는 invoice_queueinvoice_issued_at 같은 컬럼을 추가해 발행 완료를 따로 기록하고, 재시도 시 발행을 건너뛰게 하세요. 이 케이스의 기본 워크플로우에는 그 분리가 들어 있지 않습니다.
  • 계속 실패하는 행attempts가 5에 도달하면 더 이상 집어가지 않습니다. 큐가 막히지 않지만, 그 행은 사람이 봐야 합니다. SELECT * FROM invoice_queue WHERE attempts >= 5;를 주기적으로 확인하거나, 별도 알림을 거세요.
  • 결제 취소·부분 환불 — 이 워크플로우는 DONE만 처리합니다. 취소·환불은 세금계산서 수정발행이라는 완전히 다른 절차이므로 자동화하지 마시고, 별도 알림만 보내 사람이 처리하게 하세요.
  • 웹훅이 10초를 넘겨도 — 즉시 응답 구조라 이 상황 자체가 발생하지 않습니다. 다만 n8n 인스턴스가 죽어 있으면 응답이 아예 없고, 토스는 최대 7회·약 3일 19시간에 걸쳐 재시도합니다. 그 사이에 인스턴스를 복구하면 결제가 유실되지 않습니다.
  • 홈택스에 직접 연동하고 싶다면 — 권하지 않습니다. XML 스키마가 엄격하고, 서버용 공동인증서를 직접 관리해야 하며, 법령이 바뀔 때마다 유지보수가 발생합니다. 대행 서비스를 쓰는 편이 총비용이 낮습니다.
10초
웹훅 응답 제한
최대 7회
토스 재시도
0건
중복 발행
40분
셋업 시간
10 초 이내로 200 응답을 보내주세요.토스페이먼츠 웹훅 공식 문서

당신의 업무도
여기 들어갈 수 있어요.

가장 반복적인 업무 하나만 알려주세요. 자동화 시나리오를 그려드립니다.