콘텐츠로 이동

@gj-kit/toss-payments-postgresql

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

  • npm 0.5.2
  • 런타임 의존성 0 — pg조차 peer가 아닙니다
  • @ts-expect-error 컴파일 가드 55개
  • unit 테스트 250개 이상 · 실 PostgreSQL 대상 31건 별도
  • 테이블 7종, 명시적 migrate() 한 번

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

  • 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’)를 던집니다.

완료 상태: 명시적인 migration 단계 하나를 갖는 PostgreSQL 기반 Toss store를 만듭니다.

Terminal window
pnpm add @gj-kit/toss-payments-postgresql

2. 앱이 소유할 경계를 정합니다

섹션 제목: “2. 앱이 소유할 경계를 정합니다”

fromPgPool로 pool을 adapter에 연결하고 실제 암호화 sensitiveValueProtector를 사용하며, migration은 배포 중에 실행합니다.

먼저 아래 코드를 복사한 뒤, 위에서 언급한 앱 소유 값만 교체하세요.

import { createTossPaymentsPostgres, fromPgPool, type PgPoolLike, type SensitiveValueProtector } from '@gj-kit/toss-payments-postgresql';
declare const pool: PgPoolLike;
declare const sensitiveValueProtector: SensitiveValueProtector; // App KMS/encryption boundary.
export const stores = createTossPaymentsPostgres({
sql: fromPgPool(pool),
sensitiveValueProtector,
});
// Run await stores.migrate() once in deployment, never per request.

protector는 빠뜨릴 수 있는 옵션이 아니고, raw 식별자는 lock key 자리에 들어가지 못합니다. 둘 다 DB에 닿기 전에 tsc가 TS2345로 거부합니다. 두 lock을 함께 잡는 API도 withOpaqueMutationLock 하나뿐이라, opaque → customer 순서는 호출자가 정할 여지가 없습니다.

import type { Pool } from 'pg';
import type { BillingKeyRecord } from '@gj-kit/toss-payments/server';
import { createAes256GcmSensitiveValueProtector, createOpaqueAdvisoryLockKey, createTossPaymentsPostgres, fromPgPool } from '@gj-kit/toss-payments-postgresql';
declare const pool: Pool; // app owns the pg Pool
declare const keyHex: string; // app owns key custody and rotation
declare const blindIndex: string; // app owns the blind index of the customer id
declare const record: BillingKeyRecord; // app owns the freshly issued billing key
export const pg = createTossPaymentsPostgres({
sql: fromPgPool(pool),
sensitiveValueProtector: createAes256GcmSensitiveValueProtector({ key: keyHex, keyId: '2026-08' }),
});
// Without sensitiveValueProtector: TS2345 '{ sql: SqlClient; }' is not assignable to 'TossPaymentsPostgresOptions'.
export const previous = await pg.billingKeys.withOpaqueMutationLock(
createOpaqueAdvisoryLockKey(blindIndex), // raw blindIndex: TS2345 'string' is not assignable to 'OpaqueAdvisoryLockKey'
record.customerKey,
(mutation) => mutation.replaceAndGetPrevious(record),
);

앱이 connection lifecycle과 key custody를 유지하면서 Toss payment store에 검증된 PostgreSQL 구현이 필요할 때 사용합니다.

migration을 request 또는 앱 시작 시 실행하거나 production에서 plaintext protector를 사용하지 마세요.

엔진: node >=20

Peer 지원 범위 필요한 위치
@gj-kit/toss-payments `^0.5.0
@nestjs/common `^10
reflect-metadata `^0.1.13
rxjs ^7 문서화된 optional subpath에서만
Entry Exported symbols
Root entry 55
./nestjs 14
./testing 30

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

전체 API reference에는 선언 파일에서 생성한 모든 공개 함수, 클래스, 타입, 상수, 오류 계약이 있습니다. 각 항목은 import path, release signature, package version을 표시합니다.

관계 패키지 함께 쓰는 이유
필수 @gj-kit/toss-payments 이 패키지의 공개 계약을 구현하거나 사용합니다.
선택 조합 @gj-kit/toss-payments-nestjs PostgreSQL 기반 store를 typed Nest payment provider로 전달할 때 함께 사용합니다.

npm · GitHub