← 목록으로

경쟁사가 당신의 성장률을 꿰뚫어 본다? 시퀀셜 ID가 숨긴 소름 돋는 진실과 UUID 선택 가이드

2026. 6. 29.

경쟁사가 당신의 성장률을 꿰뚫어 본다? 시퀀셜 ID가 숨긴 소름 돋는 진실과 UUID 선택 가이드

제2차 세계대전 당시, 연합군에게는 막대한 비용이 드는 질문이 하나 있었습니다. 바로 "독일이 전차를 얼마나 많이 생산하고 있는가?"였죠. 그 질문에 답할 마땅한 방법이 없었습니다.

공식적인 방법은 스파이를 심고, 무선 교신을 가로채고, 그리고 이런저런 추측을 동원하는 것이었습니다. 하지만 이 공식적인 방법은 결국 어처구니없이, 부끄러울 정도로 크게 빗나갔습니다. 당시 연합군 정보국은 독일의 전차 생산량이 월 1천 대를 훨씬 넘을 것이라고 추정했으니까요.

그러다 몇몇 통계학자들이 나타나 단순한 산수로 모두의 신비감을 박살 내버렸죠.

사실 독일군은 뛰어난 기술자들이었죠. 다시 말해, 병적으로 체계적인 민족이었습니다. 전차 한 대 한 대가 공장에서 나올 때마다 기어박스, 차대, 바퀴 등 모든 부품에 깔끔하게 순차적인 일련번호가 찍혀 있었습니다. 연합군이 전차를 나포하거나 파괴할 때마다, 이 번호들은 빠짐없이 기록되었습니다.

그래서 통계학자들은 더 이상 공장을 염탐하려 애쓰지 않았습니다. 대신 부서진 잔해에서 일련번호를 읽기 시작했죠. 만약 당신이 몇 대의 전차를 노획했고, 지금까지 본 가장 높은 일련번호가 m이며, 총 k대의 전차를 봤다면, 총생산량은 대략 다음과 같이 계산됩니다.

N ≈ m + (m / k) − 1

이 직관은 아름답고 살짝 사악합니다. 당신이 본 가장 큰 숫자는 전체 생산량에 얼마나 가까운지를 대략 알려주고, 몇 대를 보았는지는 그 추정치에 대한 확신을 줍니다. 당신이 가지고 있는 일련번호 사이의 간격은 당신이 가지고 있지 않은 번호들에 대한 정보를 흘려주는 셈이죠.

1942년 어느 한 달, 스파이들은 독일이 약 1,500대의 전차를 만들었다고 보고했습니다. 반면, 노획된 일련번호와 공식 외엔 아무것도 없었던 통계학자들은 327대라고 단언했습니다. 전쟁 후, 실제 독일의 생산 기록이 발견되었죠.

실제 숫자는 342대였습니다.

스파이들은 무려 1천 대나 틀렸습니다. 이론만 파던 괴짜들은 고작 15대 오차를 냈죠. 어딘가에서 매우 흡족한 표정의 통계학자가 훈장을 받았을 것이고, 이 교훈은 정보 업무의 기초로 영원히 새겨졌습니다.

순차적인 번호는 정보를 유출합니다. 만약 당신의 일련번호가 1, 2, 3, 4로 이어진다면, 그중 몇 개만 본 사람도 당신의 존재 수를 쉽게 추정할 수 있습니다. 제가 실무에서 이 부분을 테스트해 봤을 때, 작은 패턴이라도 데이터가 쌓이면 얼마나 많은 정보가 새어 나갈 수 있는지 매번 놀라곤 합니다. 저는 /users/1042 같은 URL을 볼 때마다 늘 이 독일 전차 문제를 떠올립니다.

당신의 데이터베이스는 독일군입니다

자, 이제 불편한 진실을 마주할 시간입니다. 당신이 지금까지 작성한 거의 모든 백엔드는 사실상 독일 국방군(Wehrmacht)과 같습니다. 모든 것에 순차적인 일련번호를 천진난만하게 찍어대고는 낯선 사람들에게 기꺼이 넘겨주고 있죠.

당신이 Postgres 테이블을 만듭니다. 기본 키는 id SERIAL — 자동 증가 정수형이죠. 당연하게도, 그게 기본이고 깔끔하며 정렬도 잘 되니까요. 사용자 1은 당신, 사용자 2는 공동 창업자, 사용자 3은 당신의 어머니… 모든 것이 순조롭습니다.

그리고 프로필 페이지를 만듭니다. 라우트는 /users/3이죠. 배포합니다. 이제 당신은 독일 전차가 된 셈입니다.

왜냐하면 경쟁사, 혹은 심심한 십대, 아니면 기자를 포함한 브라우저만 가진 누구라도 지금부터 이런 일을 할 수 있습니다. 오늘 당신의 앱에 가입해서 id = 4,317을 받습니다. 일주일 기다립니다. 다른 이메일로 다시 가입해서 id = 4,981을 받습니다.

그리고 빼기만 하면 됩니다.

이번 주에 664명의 가입자가 생겼군요. 그들은 아무것도 침해하지 않았습니다. 당신을 해킹한 것도 아닙니다. 그들은 단지 당신의 시리얼 넘버를 읽었을 뿐입니다. 연합군이 전차의 기어박스에 찍힌 번호를 읽은 것과 똑같이 말이죠. 그리고 당신의 성장률은 이 간단한 산수에서 고스란히 드러났습니다. 투자자를 현혹할 "우리는 대박입니다!" 피치 덱이 고작 아홉 살짜리 뺄셈 능력과 두 개의 일회용 이메일을 가진 낯선 이에 의해 팩트 체크당한 셈입니다.

상황은 더 나빠집니다. ID는 단순히 개수 이상을 유출하거든요. 순서시간 정보까지 새어 나갑니다.

  • 오픈 당일 인보이스/58이라는 URL은 세상에 당신 회사가 창립 이래 정확히 58번의 청구를 했다는 사실을 알려줍니다.
  • #7번 지원 티켓은 당신의 기업 고객에게 자신들이 당신의 첫 일곱 기업 고객 중 하나라는 사실을 말해주죠. 참으로 감동적이지 않습니까?
  • 9,0009,003이라는 ID를 가진 두 개의 주문이 1분 간격으로 이루어졌다면, 경쟁사는 당신이 피크 시간에 대략 1분당 3건의 주문을 처리한다는 것을 알게 됩니다.

그리고 보안 담당자들이 신경 쓰는 부분이 있죠. 순차적인 ID는 단순히 정보를 제공하는 것을 넘어 추측이 가능하다는 점입니다. 제가 /api/orders/9000을 볼 수 있다면, 단순히 /api/orders/89998998을 시도해 볼 수도 있습니다. 만약 당신의 권한 부여(authorization) 로직이 조금이라도 허술하다면, 그리고 친구여, 솔직히 말해 당신의 시스템은 분명 그럴 겁니다. 저는 지금 다른 사람들의 주문 정보를 읽고 있는 것입니다. 여기에는 이름이 있습니다. 바로 IDOR(Insecure Direct Object Reference)이라고 불리며, OWASP Top 10에 거의 영원히 자리 잡고 있는 단골 손님입니다. 그리고 이는 거의 항상 순차적인 기본 키를 외부 세계에 노출하는 순간 탄생하죠.

가짜 콧수염을 붙이고 등장하는 UUID

해결책은 시리얼 넘버를 순서대로 찍는 것을 멈추는 것입니다.

UUID(Universally Unique Identifier)는 128비트 값으로, 가장 일반적인 형태(v4)는 본질적으로 무작위입니다.

f47ac10b-58cc-4372-a567-0e02b2c3d479

이 아름다운 무의미함을 보세요. 이전 사용자의 ID는 무엇일까요? 알 수 없습니다. 다음 ID는요? 역시 알 수 없습니다. 사용자가 총 몇 명일까요? 알아낼 수 없습니다. 읽을 수 있는 순서도, 기준이 될 최대값도, 측정할 간격도 없기 때문이죠. 독일 전차 문제는 줄지어 늘어선 시리얼 넘버가 필요합니다. UUID는 목재 파쇄기에 들어간 시리얼 넘버와 다름없습니다. 계산 공식이 물어뜯을 것이 없죠.

덤으로, 이 공격 방어에는 관심 없는 사람들의 마음마저 사로잡는 보너스가 있습니다. UUID는 전 세계적으로 조율 없이도 고유합니다. 두 개의 다른 서버, 두 개의 다른 서비스, 비행기 안의 오프라인 모바일 클라이언트까지 모두 동시에 ID를 생성해도 충돌이 일어나지 않습니다. "다음으로 사용할 수 있는 번호가 무엇입니까?"라고 데이터베이스에 왕복 질의할 필요가 없습니다. 행이 생성되기도 전에 ID를 생성할 수 있습니다. 분산 시스템을 구축하는 사람이라면, 이 속성 하나만으로도 그 자체로 충분한 가치를 합니다.

자, 무작위적이고, 추측 불가능하며, 개수 정보를 숨기고, 조율이 필요 없습니다. 해결했습니다. 배포하세요. 탭을 닫으세요.

자, 여기서는 제가 잔소리꾼 친구가 될 수밖에 없는 이유입니다

이 아이디어에는 양쪽 끝에 실패 모드가 존재하며, 저는 UUID가 공짜라고 주장하는 글을 쓸 수가 없기 때문입니다.

Image description

절대 공짜가 아닙니다. bigint는 8바이트입니다. UUID는 16바이트이고, 미친 사람처럼 텍스트로 저장하면 36바이트가 됩니다. 수억 개의 행을 가진 테이블에 수십 개의 외래 키가 이를 가리키고 있다면, 이 오버헤드는 단순한 이론이 아닙니다. 당신의 스토리지 비용과 RAM 사용량에 직접적인 영향을 미칩니다.

하지만 진정한 아킬레스건은 바로 인덱스입니다. 데이터베이스는 기본 키를 B-트리 형태로 저장하는데, 새로운 값이 대략 증가하는 순서로 들어올 때 가장 빠르고 깔끔합니다. 새로운 삽입은 깔끔하게 트리의 끝에 추가되죠. 하지만 무작위 UUIDv4는 테이블마다 사람 사이에 비집고 앉는 술 취한 손님처럼 굴죠. 데이터베이스는 끊임없이 페이지를 분할하고, 데이터를 재배치하고, 디스크에서 오래된 인덱스 부분을 다시 읽어야 합니다. 이것을 쓰기 증폭(write amplification)과 페이지 단편화(page fragmentation)라고 부릅니다. 이 때문에 어떤 사람은 고 트래픽 테이블을 무작위 UUID로 마이그레이션했다가 삽입 성능이 급락하는 것을 보고는 격렬한 불평이 담긴 블로그 게시물을 작성했을 겁니다. (아마 당신도 같은 실수를 저지르기 직전에 그 글을 읽겠지만요. 일종의 전통이죠.)

그래서 우리는 엔지니어들이 항상 그랬듯이, 해결책을 다시 해결했습니다.

  • UUIDv7 (2024년 표준화)은 높은 비트에 타임스탬프를, 낮은 비트에 무작위 값을 넣습니다. 따라서 ID가 시간이 지남에 따라 상향 추세로 정렬됩니다. B-트리는 다시 행복해지고, 여전히 추측 불가능하고 셀 수 없습니다. 두 ID를 빼서 가입자 수를 알 수도 없죠. 이는 2026년 대부분의 앱에서 올바른 기본값이 될 겁니다.
  • ULID는 본질적으로 비슷한 트릭을 사용하지만, 더 친근하고 정렬 가능한 텍스트 인코딩을 가집니다.
  • Snowflake ID (트위터의 고전적인 방법)는 타임스탬프, 머신 ID, 카운터를 컴팩트한 64비트에 담습니다. 더 작고 정렬 가능하지만, 약간의 시간 정보를 유출하는 대가가 따릅니다.

하지만 함정을 눈치채세요. 독일 전차 문제가 옆문으로 다시 슬그머니 들어오는 격이니까요. 시간 순서대로 정렬된 ID는 여전히 그 정렬 기준인 시간 정보를 유출합니다. UUIDv7은 당신의 총 사용자 수를 알려주지 않겠지만, 각 레코드가 대략 언제 생성되었는지 속삭여 줄 것입니다. 이것은 "빼기로 성장률 계산"보다는 훨씬 작은 유출이지만, 여전히 0은 아닙니다. 만약 생성 타임스탬프가 당신의 도메인에서 민감한 정보라면, v7조차도 부분적인 노출과 같습니다. 의도적으로 독을 고르세요.

그리고 가장 교활한 함정은 바로 이겁니다. 추측 불가능한 ID가 권한 부여 시스템은 아닙니다. UUID가 추측하기 어렵다는 것이 UUID가 보호된다는 의미와 같지 않습니다. 만약 다른 사람의 인보이스를 읽는 것을 막는 당신의 유일한 방어가 "음, 그들이 122비트 무작위 숫자를 맞춰야 할 텐데요"라면, 당신은 비밀번호라고 착각하고 ID로 사용한 셈입니다. UUID는 *열거(enumeration)*를 막는 문을 쾅 닫아버립니다. 하지만 ID를 가진 사람이 실제로 그것을 사용할 권한이 있는지 확인하는 것을 잊었다면, UUID는 아무런 역할도 하지 못합니다. OWASP Top 10에 IDOR이 괜히 있는 게 아닙니다. 당신의 권한 부여 로직을 다시 확인하세요. 무작위 ID는 자물쇠이고, 권한 확인은 경비원입니다. 둘 다 필요합니다.

그럼 당장 내일 무엇을 해야 할까요?

이미 배포된 시스템의 시리얼 넘버를 지울 수는 없습니다. 하지만 당신은 두 개의 다이얼을 완벽하게 제어할 수 있으니, 의도적으로 설정하세요.

  1. 기본 키 노출을 중단하세요. 대부분의 팀에서 가장 깔끔한 접근 방식은 이겁니다. 지루한 자동 증가 bigint내부 기본 키로 유지합니다(인덱스는 빠르고, 조인은 저렴합니다). 그리고 외부 세계에 노출되는 모든 것 — URL, API 응답, 인보이스 번호 등에는 — 별도의 무작위 외부 ID, 즉 UUIDv7을 추가하세요. 빠른 키는 지하에 숨겨두고, 정보가 새지 않는 '목재 파쇄기 키'는 외부에 노출하는 거죠. 제가 구축하는 대부분의 시스템에서는 이런 이중 ID 전략을 기본으로 사용합니다. 내부적인 효율성과 외부적인 보안/프라이버시를 동시에 잡을 수 있는 가장 현실적인 타협점이라고 생각하거든요.
  2. 새로운 공용 ID의 기본값을 v4 대신 UUIDv7로 설정하세요. 쓰기 성능을 떨어뜨리지 않으면서도 개수 정보를 숨길 수 있습니다. 시간 정보가 전혀 없는 것을 특별히 원하고 인덱스 지역성에는 신경 쓰지 않는 경우에만 v4를 선택하세요.
  3. 그럼에도 불구하고 권한 부여를 확인하세요. ID는 애초에 보안 경계선이 아니었습니다. 그저 사람들이 당신을 노획된 기어박스처럼 읽는 것을 막을 뿐입니다.

연합군은 공식과 일련번호 더미로 전쟁의 특정 라운드에서 승리했습니다. 상대방이 모든 것을 순서대로 정리하고 그 번호가 노출되도록 방치하는 부주의함 덕분이었죠.

당신에게 불리하게 작용하는 곳에서는 깔끔함을 고집하지 마세요. 당신의 전차에 무작위로 번호를 매기세요.

통계학자들은 여전히 그곳에 있습니다. 그들은 여전히 매우 흡족해하고 있죠. 그리고 다음번에 누군가가 당신의 앱에 일주일 만에 두 번 가입해서 ID를 빼려는 시도를 할 때, 이미 UUIDv7을 적용해서 배포했고, 그래서 심술 맞게도, 아무 문제 없는 당신이 될 수 있을 겁니다.


원문: https://dev.to/towernter/the-german-tank-problem-why-you-need-uuids-85p 수집일: 2026-06-29 00:24:10