콘텐츠로 이동

dealcode가 존재하는 이유

English | 한국어

문제

모든 제품은 결국 사람에게 보이는 짧은 식별자가 필요해집니다. 주문번호, 쿠폰 코드, 초대 코드, 티켓 번호. 이때 두 요구가 즉시 충돌합니다.

  1. 유일해야 한다. 두 고객이 같은 주문번호를 가질 수는 없습니다.
  2. 내부를 드러내면 안 된다. ORDER-000042는 주문이 42건뿐이라는 걸 전 세계에 알리고 누구든 주문 테이블을 순회할 수 있게 해줍니다. 일련번호로 생산량을 추정하는 건 문자 그대로 2차대전 정보전 기법입니다 (독일 전차 문제).

흔한 두 가지 비(非)해법:

  • 랜덤 코드 + 충돌 시 재시도. 랜덤은 직관보다 훨씬 일찍 겹칩니다. 6자리 숫자(100만 개)면 약 1,200개 발급 시점에 첫 중복이 기대됩니다 — 생일 역설이라 희귀 사건이 아닙니다. 그래서 발급마다 유일성 조회, 재시도 루프, 동시성 처리 스토리가 필요해지고, 공간이 차오를수록 재시도는 눈덩이처럼 늘어납니다(50% 채우면 시도의 절반이 실패). 결정적인 일을 확률 알고리즘으로 하는 셈입니다.
  • 시퀀스 그대로 노출. 유일성은 공짜지만 프라이버시는 0. base62로 인코딩하거나 "큰 오프셋 더하기"는 분장일 뿐 누수는 그대로입니다 — 연속 주문은 여전히 눈에 띄게 연관된 코드가 됩니다.

dealcode의 답

시퀀스는 유지하고(반복 없는 정수는 DB가 이미 동시성 안전하게, 힘 안 들이고 만들어 줍니다), 시퀀스와 바깥세상 사이에 키 기반 순열을 끼웁니다.

시퀀스  ──►  FF1 암호화 (당신의 키)  ──►  코드
  n           d자리 위의 전단사 함수        랜덤처럼 보이고, 증명 가능하게 유일

순열은 잘 섞인 카드 덱입니다. 모든 입력이 정확히 하나의 출력에 대응하고 그 역도 성립합니다. 유일성은 시퀀스에서 구조적으로 상속됩니다 — 검사가 아니라 구성에 의해서요. 섞는 방식은 AES 기반 FF1(NIST SP 800-38G)이라 키가 없으면 출력 순서는 계산적으로 랜덤과 구별 불가능합니다.

체화할 가치가 있는 결과들:

  • 코드베이스에 충돌 처리 코드가 존재하지 않습니다. "충돌이 드물다"가 아니라 그 코드 경로 자체가 없습니다.
  • 발급이 순수 함수입니다. 추가 테이블도, 앱 서버 간 조율도, 캐시나 락도 없습니다. nextval() + 암호화 한 번.
  • 코드가 최소 길이를 유지합니다. 랜덤 방식은 충돌률을 견디려고 길이를 과잉 확보해야 합니다. 순열은 공간을 남김없이 씁니다. 6자리 hex 1,670만 개를 다 쓰고 나서야 — 그때서야 — 7자리가 됩니다.
  • decode가 공짜입니다. 코드가 곧 (암호화된) 기본 키입니다. 조회에 보조 인덱스가 필요 없고 형식이 잘못된 입력(길이·문자셋 위반)은 DB에 닿기 전에 걸러집니다. 뒷면도 알아두세요: 형식이 올바른 코드는 항상 어떤 카운터로는 복호화됩니다 — decode는 파싱이고, 존재 여부는 DB 조회가 확정합니다.

이런 곳에 쓰세요

케이스 왜 맞는가
주문 / 인보이스 / 배송 번호 유일하고 짧으며 물량·속도를 감춤
쿠폰·상품권 코드 사람이 타이핑 가능(Crockford 프리셋), 추측이 크게 위험하지 않은 값
초대 / 추천 코드 짧고, 열거 불가능하고, 초대자 행으로 바로 복호화
티켓 / 상담 / 예약 번호 전화로 불러줄 수 있음
숏링크 slug = 행 id의 인코딩, slug 충돌 로직 불필요
공개용 난독화 id UUID 마이그레이션 없이 URL의 user_id=48291 누수 차단

공통점: 카운터가 이미 있고(또는 쉽게 만들 수 있고), 값이 유일하고 비노출적이어야 하지만 기밀 수준일 필요는 없다는 것.

이런 곳엔 쓰지 마세요

  • 그 자체로 인증이 되는 값 — 세션 토큰, API 키, 비밀번호 재설정/매직 로그인 링크. dealcode의 코드 공간은 의도적으로 작습니다(그래서 코드가 짧습니다). 온라인으로 코드를 시도할 수 있는 공격자는 발급량 / 전체 공간 비율로 성공합니다. 128비트 이상 CSPRNG 토큰(secrets.token_urlsafe(32) 류)을 쓰세요. dealcode 코드 조회에도 기본 위생으로 rate limit을 거세요.
  • 중앙 카운터가 없는 ID. 공유 시퀀스 없이 독립된 머신들이 id를 만들어야 하면 UUIDv4/v7, ULID, Snowflake를 쓰세요. dealcode의 트릭 전체가 "이미 있는 카운터에 기대기"입니다.
  • 정렬 가능한 식별자. 순서를 감추는 게 기능입니다. 시간 정렬이 필요하면 UUIDv7/ULID.
  • 일회용 비밀번호. OTP는 사용자별 비밀키로 검증하고 전역 중복을 허용합니다. 그건 HOTP/TOTP(RFC 4226/6238)라는 다른 문제입니다.
  • 실제 데이터 암호화. 작은 도메인 위의 FF1은 카운터 난독화에는 충분하지만 기밀 필드를 보호하는 방법이 아닙니다.

대안 요약표

도구 보장 이럴 때
dealcode 충돌 0, 순서 은닉, 복호화 가능, 최소 길이 카운터가 있고, 코드가 사람을 향할 때
UUIDv4 충돌 무시 가능, 조율 불필요 카운터 없는 머신 간 id
UUIDv7 / ULID 위 + 시간 정렬 분산 + 인덱스 친화
Snowflake 정렬 가능, 컴팩트, 조율 최소 고처리량 분산 id
nanoid / secrets 안전한 랜덤, 자유 길이 추측 불가능해야 하는 토큰
sqids / hashids 짧고 가역, 비암호학적 겉치레 난독화만 필요할 때
HOTP / TOTP 사용자별 일회 증명 인증 코드

(sqids/hashids는 같은 "정수 감추기" 문제를 풀지만 셔플이 비암호학적이라 코드 몇 개로 salt 복원이 가능합니다. dealcode는 같은 개발 경험에 밑바닥만 AES로 바꾼 것입니다.)

운영 수칙

  1. 설정은 write-once. 키, 알파벳, min_length, max_length, domain — 첫 코드가 나가는 순간 동결. 다른 설정은 다른 순열이고, 한 카운터 공간 위의 두 순열은 충돌합니다. 새 체계 → 새 도메인/네임스페이스.
  2. 네임스페이스 하나에 카운터 하나. 두 시퀀스를 같은 codec+domain에 넣지 말고 시퀀스를 뒤로 되돌리지(setval) 마세요.
  3. 저장된 코드에 UNIQUE 인덱스를 경보 장치로 두세요. 수칙 1이나 2가 깨졌을 때만 울릴 수 있습니다. 울리면 알림 + 조사. 절대 재시도 금지.
  4. 키는 운영 시크릿으로 취급하세요. 키는 유일성을 지키는 게 아니라(그건 구조가 지킵니다) 연결 불가능성을 지킵니다 — 새면 모든 코드의 시퀀스 상 위치가 공개됩니다. KMS/Vault, 환경별 키. 그리고 로테이션 = 새 네임스페이스 (수칙 1).
  5. 공개 조회에 rate limit. 코드 공간이 작으니 온라인 추측을 지루하게 만드세요.

이름에 대해

잘 섞인 덱에서 카드를 나눠주는 것(dealing): 각 카드는 정확히 한 번, 순서는 랜덤처럼, 딜러가 기억할 것은 몇 장 돌렸는지 하나. 그게 이 라이브러리의 전부입니다.