dealcode가 존재하는 이유¶
English | 한국어
문제¶
모든 제품은 결국 사람에게 보이는 짧은 식별자가 필요해집니다. 주문번호, 쿠폰 코드, 초대 코드, 티켓 번호. 이때 두 요구가 즉시 충돌합니다.
- 유일해야 한다. 두 고객이 같은 주문번호를 가질 수는 없습니다.
- 내부를 드러내면 안 된다.
ORDER-000042는 주문이 42건뿐이라는 걸 전 세계에 알리고 누구든 주문 테이블을 순회할 수 있게 해줍니다. 일련번호로 생산량을 추정하는 건 문자 그대로 2차대전 정보전 기법입니다 (독일 전차 문제).
흔한 두 가지 비(非)해법:
- 랜덤 코드 + 충돌 시 재시도. 랜덤은 직관보다 훨씬 일찍 겹칩니다. 6자리 숫자(100만 개)면 약 1,200개 발급 시점에 첫 중복이 기대됩니다 — 생일 역설이라 희귀 사건이 아닙니다. 그래서 발급마다 유일성 조회, 재시도 루프, 동시성 처리 스토리가 필요해지고, 공간이 차오를수록 재시도는 눈덩이처럼 늘어납니다(50% 채우면 시도의 절반이 실패). 결정적인 일을 확률 알고리즘으로 하는 셈입니다.
- 시퀀스 그대로 노출. 유일성은 공짜지만 프라이버시는 0. base62로 인코딩하거나 "큰 오프셋 더하기"는 분장일 뿐 누수는 그대로입니다 — 연속 주문은 여전히 눈에 띄게 연관된 코드가 됩니다.
dealcode의 답¶
시퀀스는 유지하고(반복 없는 정수는 DB가 이미 동시성 안전하게, 힘 안 들이고 만들어 줍니다), 시퀀스와 바깥세상 사이에 키 기반 순열을 끼웁니다.
순열은 잘 섞인 카드 덱입니다. 모든 입력이 정확히 하나의 출력에 대응하고 그 역도 성립합니다. 유일성은 시퀀스에서 구조적으로 상속됩니다 — 검사가 아니라 구성에 의해서요. 섞는 방식은 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로 바꾼 것입니다.)
운영 수칙¶
- 설정은 write-once. 키, 알파벳,
min_length,max_length,domain— 첫 코드가 나가는 순간 동결. 다른 설정은 다른 순열이고, 한 카운터 공간 위의 두 순열은 충돌합니다. 새 체계 → 새 도메인/네임스페이스. - 네임스페이스 하나에 카운터 하나. 두 시퀀스를 같은 codec+domain에 넣지
말고 시퀀스를 뒤로 되돌리지(
setval) 마세요. - 저장된 코드에
UNIQUE인덱스를 경보 장치로 두세요. 수칙 1이나 2가 깨졌을 때만 울릴 수 있습니다. 울리면 알림 + 조사. 절대 재시도 금지. - 키는 운영 시크릿으로 취급하세요. 키는 유일성을 지키는 게 아니라(그건 구조가 지킵니다) 연결 불가능성을 지킵니다 — 새면 모든 코드의 시퀀스 상 위치가 공개됩니다. KMS/Vault, 환경별 키. 그리고 로테이션 = 새 네임스페이스 (수칙 1).
- 공개 조회에 rate limit. 코드 공간이 작으니 온라인 추측을 지루하게 만드세요.
이름에 대해¶
잘 섞인 덱에서 카드를 나눠주는 것(dealing): 각 카드는 정확히 한 번, 순서는 랜덤처럼, 딜러가 기억할 것은 몇 장 돌렸는지 하나. 그게 이 라이브러리의 전부입니다.