콘텐츠로 이동

@gj-kit/toss-payments-postgresql 핵심 개념

토스페이먼츠 저장소를 직접 운영하는 PostgreSQL 위에 올리되, 암호화 protector를 빠뜨리면 컴파일이 실패합니다.

토스페이먼츠를 자체 DB에 붙이려면 테이블 7종, 원자적이어야 하는 webhook dedupe 전이, 그리고 billing key·가상계좌 secret·취소 재시도 티켓의 at-rest 암호화를 직접 설계해야 합니다. 직접 짜면 조회 후 삽입 방식의 dedupe에서 같은 이벤트의 동시 재전송 2건이 모두 처리권을 얻고, protector를 빠뜨린 store는 billing key를 평문으로 남깁니다. 둘 다 프로덕션에 올라가기 전까지는 아무 신호도 주지 않습니다.

민감값에는 앱 protector가 필요합니다

섹션 제목: “민감값에는 앱 protector가 필요합니다”

pool lifecycle과 key custody는 호스트 앱에 남습니다. 필수 protection은 빠뜨린 암호화가 billing 데이터를 plaintext로 저장하지 못하게 합니다.

migration은 명시적인 배포 작업입니다

섹션 제목: “migration은 명시적인 배포 작업입니다”

store aggregate는 배포 자동화를 위한 migration 단계 하나를 노출합니다. request 처리와 application startup은 일반 트래픽에서도 반복될 수 있습니다.

  • protector 없는 조립은 컴파일 실패 — createTossPaymentsPostgres({ sql })은 타입 에러입니다. aggregate와 민감 store factory 3종이 모두 sensitiveValueProtector를 필수 필드로 요구하므로, 평문 저장은 숨은 기본값이 될 수 없고 unsafePlaintextSensitiveValueProtector를 직접 적어 넣은 코드에만 남습니다.
  • raw string은 lock key가 아닙니다 — opaqueLocks.withLock은 branded OpaqueAdvisoryLockKey만 받습니다. customer ID를 raw string으로 넘기는 코드는 tsc가 TS2345로 거부합니다.
  • 타입이 transaction 요건을 강제합니다 — createPgBillingKeyStore는 SqlExecutor를 거부합니다. SELECT … FOR UPDATE → 복호화 → constant-time 비교 → DELETE가 한 connection, 한 transaction 안에서 끝나야 하므로 withConnection을 가진 SqlClient만 통과합니다.
  • webhook claim은 단일 문입니다 — src/stores/webhook-dedupe.ts의 claim은 INSERT … ON CONFLICT DO UPDATE 단일 CTE로 전이합니다. 그래서 동시 재전송 N건 중 정확히 1건만 ’claimed’를 받습니다.
  • deadlock이 테스트에서 즉시 드러납니다 — ./testing 인메모리 대역은 PostgreSQL이라면 그대로 멈출 중첩에서 MemoryLockContractError(‘nested-lock-api’ · ‘reentrant-lock’)를 던집니다.

민감값에는 앱 소유 KMS 또는 key-management 경계를 사용하고, 명시적 migration은 한 번 실행하며, 정리 작업은 멱등적으로 유지하세요.

다음은 검증된 레시피 두 개입니다.