<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Database Sequence on jyukki's Blog</title><link>https://jyukki.com/tags/database-sequence/</link><description>Recent content in Database Sequence on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Sat, 23 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/database-sequence/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: 분산 ID 생성 전략, UUIDv7·Snowflake·DB Sequence를 실무 기준으로 고르는 법</title><link>https://jyukki.com/learning/deep-dive/deep-dive-distributed-id-generation-uuidv7-snowflake-playbook/</link><pubDate>Sat, 23 May 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-distributed-id-generation-uuidv7-snowflake-playbook/</guid><description>대규모 백엔드에서 ID 생성 전략을 감으로 고르지 않도록 UUIDv7, Snowflake, DB sequence, 자연키의 장단점과 운영 기준을 정리합니다.</description><content:encoded><![CDATA[<p>서비스가 작을 때 ID는 별 문제가 아닙니다. <code>AUTO_INCREMENT</code>를 쓰거나 <code>UUID.randomUUID()</code>를 호출하면 끝나는 것처럼 보입니다. 하지만 트래픽이 늘고, 테이블이 커지고, 여러 리전과 여러 서비스가 같은 비즈니스 객체를 다루기 시작하면 ID 전략은 성능, 보안, 데이터 모델, 운영 복구까지 영향을 줍니다. 특히 주문, 결제, 이벤트, 로그, 파일 객체처럼 쓰기량이 많고 장기 보존되는 데이터는 ID 선택을 나중에 바꾸기 어렵습니다.</p>
<p>이 글은 ID를 &ldquo;유일한 값&quot;으로만 보지 않고, <strong>정렬성, 분산 생성 가능성, 인덱스 비용, 노출 안전성, 장애 복구성</strong>을 함께 보는 플레이북입니다. 같이 보면 좋은 글은 <a href="/learning/deep-dive/deep-dive-database-schema-design-basics/">DB 스키마 설계 기본기</a>, <a href="/learning/deep-dive/deep-dive-sharding-consistent-hashing/">샤딩과 Consistent Hashing</a>, <a href="/learning/deep-dive/deep-dive-clock-skew-time-semantics-playbook/">Clock Skew 시간 의미론</a>, <a href="/learning/deep-dive/deep-dive-idempotency/">멱등성 설계</a>입니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>UUIDv4, UUIDv7, Snowflake, DB sequence를 언제 고르면 되는지 기준을 잡을 수 있습니다.</li>
<li>ID의 정렬성과 랜덤성이 B-Tree 인덱스, 파티셔닝, 쓰기 처리량에 주는 영향을 이해할 수 있습니다.</li>
<li>외부 노출 ID와 내부 PK를 분리해야 하는 조건을 판단할 수 있습니다.</li>
<li>ID 생성 장애, clock rollback, worker id 충돌 같은 운영 리스크를 숫자 기준으로 점검할 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-id는-유일성보다-접근-패턴이-먼저다">1) ID는 유일성보다 접근 패턴이 먼저다</h3>
<p>ID 전략을 고를 때 첫 질문은 &ldquo;충돌하지 않는가&quot;가 아니라 &ldquo;이 ID로 어떻게 읽고 쓰는가&quot;입니다. 예를 들어 단일 DB에 주문을 저장하고 최신순 목록을 자주 본다면 단조 증가 bigint나 UUIDv7처럼 시간 정렬성이 있는 ID가 유리합니다. 반대로 외부에 공개되는 초대 코드나 다운로드 토큰처럼 추측이 어려워야 하는 값은 랜덤성이 더 중요합니다.</p>
<p>실무에서는 아래처럼 나눠 보는 편이 안전합니다.</p>
<table>
  <thead>
      <tr>
          <th>요구</th>
          <th>우선 후보</th>
          <th>피해야 할 후보</th>
          <th>판단 기준</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>단일 DB 쓰기, FK 많음</td>
          <td>bigint sequence</td>
          <td>랜덤 UUIDv4 PK</td>
          <td>조인 비용과 인덱스 크기 최소화</td>
      </tr>
      <tr>
          <td>다중 인스턴스에서 중앙 의존 없이 생성</td>
          <td>UUIDv7, Snowflake</td>
          <td>단일 DB sequence</td>
          <td>생성 병목 제거</td>
      </tr>
      <tr>
          <td>외부 노출, 추측 방지</td>
          <td>UUIDv4, 난수 slug</td>
          <td>auto increment 직접 노출</td>
          <td>enumeration 방지</td>
      </tr>
      <tr>
          <td>시간순 조회와 범위 스캔</td>
          <td>UUIDv7, Snowflake, sequence</td>
          <td>순수 랜덤 키</td>
          <td>최신순 페이지, 파티션 pruning</td>
      </tr>
      <tr>
          <td>멱등 요청 키</td>
          <td>클라이언트 생성 UUID</td>
          <td>서버 sequence</td>
          <td>재시도 시 같은 키 재사용</td>
      </tr>
  </tbody>
</table>
<p>핵심은 하나의 ID가 모든 요구를 만족하지 않는다는 점입니다. 내부 PK는 <code>bigint</code>로 두고 외부 노출용 <code>public_id</code>는 UUIDv7이나 난수 slug로 별도 관리하는 설계가 흔히 더 낫습니다. 특히 외부 API에서 <code>order_id=12345</code>처럼 연속값을 노출하면 고객 수, 주문량, 데이터 증가 속도가 추측될 수 있습니다.</p>
<h3 id="2-uuidv4는-편하지만-쓰기-인덱스에는-비용이-있다">2) UUIDv4는 편하지만 쓰기 인덱스에는 비용이 있다</h3>
<p>UUIDv4는 중앙 조율 없이 만들 수 있고 충돌 가능성이 매우 낮습니다. 그래서 마이크로서비스나 클라이언트 생성 ID에 편합니다. 문제는 랜덤성입니다. B-Tree 인덱스는 정렬된 키에 최적화되어 있는데, UUIDv4는 새 값이 인덱스 여기저기에 꽂힙니다. 테이블이 커질수록 page split, cache miss, 인덱스 bloat가 늘 수 있습니다.</p>
<p>쓰기량이 낮은 관리 테이블이나 외부 공개 식별자에는 UUIDv4가 충분합니다. 하지만 초당 수천 건 이상 쓰는 주문, 이벤트, 로그 테이블의 클러스터링 키로 UUIDv4를 쓰면 비용이 커질 수 있습니다. 이때는 UUIDv7이나 Snowflake처럼 시간 정렬성이 있는 ID를 검토합니다. UUIDv7은 표준 UUID 형식을 유지하면서 앞부분에 시간 정보를 담아 정렬성을 개선합니다. Snowflake 계열은 보통 timestamp, worker id, sequence를 조합해 64bit 정수로 만듭니다.</p>
<p>단, 시간 정렬 ID는 &ldquo;시간을 믿는 설계&quot;가 됩니다. clock skew와 rollback을 다루지 않으면 같은 worker에서 역전 ID가 생기거나, 밀리초당 sequence 한도를 넘을 수 있습니다. 이 부분은 <a href="/learning/deep-dive/deep-dive-clock-skew-time-semantics-playbook/">Clock Skew 시간 의미론</a>과 연결해서 봐야 합니다.</p>
<h3 id="3-snowflake는-빠르지만-운영-계약이-필요하다">3) Snowflake는 빠르지만 운영 계약이 필요하다</h3>
<p>Snowflake 계열 ID는 대개 아래 구조를 가집니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>timestamp bits | worker bits | sequence bits
</span></span></code></pre></div><p>예를 들어 41bit timestamp, 10bit worker id, 12bit sequence를 쓰면 worker당 밀리초 4096개 수준의 ID를 만들 수 있습니다. 장점은 빠르고, 정렬 가능하며, bigint라 DB 인덱스와 조인에 유리하다는 점입니다. 단점은 worker id 중복과 clock rollback에 민감하다는 점입니다.</p>
<p>운영 기준은 숫자로 정해야 합니다.</p>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>권장 출발 기준</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>worker id 할당</td>
          <td>배포 환경에서 중복 불가, lease 또는 고정 registry 사용</td>
      </tr>
      <tr>
          <td>clock rollback 허용</td>
          <td>5ms 이내는 대기, 그 이상은 생성 중단</td>
      </tr>
      <tr>
          <td>sequence overflow</td>
          <td>다음 millisecond까지 대기, drop 금지</td>
      </tr>
      <tr>
          <td>ID 생성 오류율</td>
          <td>5분 동안 0건이어야 정상, 1건 이상이면 경고</td>
      </tr>
      <tr>
          <td>node clock offset</td>
          <td>100ms 초과 경고, 250ms 초과 즉시 조치</td>
      </tr>
  </tbody>
</table>
<p>Snowflake를 단순 유틸 클래스로 넣으면 위험합니다. ID generator는 사실상 작은 인프라 컴포넌트입니다. 배포, 재시작, 오토스케일링, 리전 장애, 시간 동기화 정책과 함께 운영해야 합니다. 특히 컨테이너가 빠르게 뜨고 지는 환경에서는 worker id를 환경변수로 손으로 넣는 방식이 오래가지 않습니다.</p>
<h3 id="4-db-sequence는-구식이-아니라-강한-선택지다">4) DB sequence는 구식이 아니라 강한 선택지다</h3>
<p>분산 시스템을 공부하다 보면 중앙 sequence를 무조건 피해야 할 것처럼 느끼기 쉽습니다. 하지만 단일 primary DB 안에서 쓰는 핵심 도메인 테이블이라면 DB sequence는 여전히 좋은 선택입니다. 충돌이 없고, 작고, 정렬되고, FK와 조인 비용이 낮습니다. 트랜잭션과 백업, 복제도 DB가 책임집니다.</p>
<p>문제는 범위입니다. 여러 서비스가 각자 DB를 갖고 같은 ID 공간을 공유해야 하거나, 오프라인 생성이 필요하거나, 리전별 쓰기가 필요하면 단일 sequence가 병목 또는 결합점이 됩니다. 반대로 한 서비스의 내부 row id인데 괜히 UUIDv4를 PK로 쓰면 인덱스와 저장 비용만 늘어날 수 있습니다.</p>
<p>의사결정 기준은 간단합니다.</p>
<ol>
<li>단일 쓰기 DB가 있고 ID를 DB 밖에서 먼저 알아야 할 필요가 없다면 sequence를 우선 검토합니다.</li>
<li>API 요청 전에 클라이언트가 ID를 만들어 재시도해야 한다면 UUID 계열을 검토합니다.</li>
<li>다중 writer에서 정렬 가능한 bigint가 필요하면 Snowflake 계열을 검토합니다.</li>
<li>외부 노출과 내부 조인 요구가 충돌하면 내부 PK와 public id를 분리합니다.</li>
</ol>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-내부-pk와-외부-id를-분리한다">1) 내부 PK와 외부 ID를 분리한다</h3>
<p>가장 실용적인 기본안은 내부 PK와 외부 노출 ID를 분리하는 것입니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">TABLE</span> orders (
</span></span><span style="display:flex;"><span>  id <span style="color:#8be9fd;font-style:italic">BIGINT</span> <span style="color:#ff79c6">GENERATED</span> <span style="color:#ff79c6">BY</span> <span style="color:#ff79c6">DEFAULT</span> <span style="color:#ff79c6">AS</span> <span style="color:#ff79c6">IDENTITY</span> <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span>,
</span></span><span style="display:flex;"><span>  public_id UUID <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span> <span style="color:#ff79c6">UNIQUE</span>,
</span></span><span style="display:flex;"><span>  user_id <span style="color:#8be9fd;font-style:italic">BIGINT</span> <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  status <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">30</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  created_at <span style="color:#ff79c6">TIMESTAMP</span> <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_orders_user_created <span style="color:#ff79c6">ON</span> orders (user_id, created_at <span style="color:#ff79c6">DESC</span>, id <span style="color:#ff79c6">DESC</span>);
</span></span></code></pre></div><p>내부 로직과 FK는 <code>id</code>를 쓰고, 외부 API는 <code>public_id</code>를 씁니다. 이 구조는 저장 비용이 조금 늘지만 운영상 이점이 큽니다. 내부 조인은 빠르게 유지하면서도 외부에서는 연속값을 숨길 수 있습니다. 고객 지원, 로그 추적, API 응답에서는 public id를 쓰고, 내부 배치와 DB 조인은 numeric id를 쓰면 됩니다.</p>
<p>단, public id를 만들 때도 정책을 정해야 합니다. 단순 조회 URL에 쓰는 값이면 UUIDv7로 정렬성을 얻을 수 있고, 보안 토큰처럼 예측 불가능성이 핵심이면 충분한 난수 기반 token을 별도로 써야 합니다. public id와 secret token을 같은 것으로 쓰지 않는 것이 좋습니다.</p>
<h3 id="2-id-전략을-테이블-유형별로-나눈다">2) ID 전략을 테이블 유형별로 나눈다</h3>
<p>모든 테이블에 같은 전략을 강제하면 비용이 생깁니다. 출발점은 아래 정도가 현실적입니다.</p>
<table>
  <thead>
      <tr>
          <th>테이블 유형</th>
          <th>기본 후보</th>
          <th>이유</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>핵심 도메인 테이블</td>
          <td>bigint sequence + public UUID</td>
          <td>조인 성능과 외부 노출 분리</td>
      </tr>
      <tr>
          <td>이벤트/로그 테이블</td>
          <td>UUIDv7 또는 Snowflake</td>
          <td>분산 생성, 시간순 적재</td>
      </tr>
      <tr>
          <td>멱등성 record</td>
          <td>클라이언트 idempotency key</td>
          <td>재시도 시 동일 키 필요</td>
      </tr>
      <tr>
          <td>파일 객체 metadata</td>
          <td>UUIDv7 또는 random object key</td>
          <td>외부 경로 추측 방지</td>
      </tr>
      <tr>
          <td>샤딩된 대용량 테이블</td>
          <td>shard key + Snowflake/UUIDv7</td>
          <td>분포와 정렬성 균형</td>
      </tr>
  </tbody>
</table>
<p>샤딩이 들어가면 ID는 더 중요해집니다. ID 안에 tenant나 shard hint를 넣을지, 별도 shard key를 둘지 결정해야 합니다. 이 부분은 <a href="/learning/deep-dive/deep-dive-sharding-consistent-hashing/">샤딩과 Consistent Hashing</a>과 <a href="/learning/deep-dive/deep-dive-database-schema-design-basics/">DB 스키마 설계 기본기</a>를 같이 봐야 합니다.</p>
<h3 id="3-id-생성기를-관측-가능하게-만든다">3) ID 생성기를 관측 가능하게 만든다</h3>
<p>ID 생성은 너무 기본 기능이라 모니터링에서 빠지기 쉽습니다. 하지만 생성기가 멈추면 쓰기 경로 전체가 멈춥니다. 최소한 아래 지표는 남깁니다.</p>
<ul>
<li><code>id_generation_latency_p95</code></li>
<li><code>id_generation_error_total</code></li>
<li><code>clock_rollback_detected_total</code></li>
<li><code>worker_id_conflict_total</code></li>
<li><code>sequence_overflow_wait_total</code></li>
<li><code>generated_id_monotonic_violation_total</code></li>
</ul>
<p>Snowflake 계열은 특히 clock rollback과 worker id 충돌을 알람으로 둬야 합니다. UUID 계열은 충돌보다 라이브러리 버전, 형식 검증, 저장 타입이 중요합니다. DB에는 가능하면 문자열 UUID보다 native UUID 타입이나 binary 타입을 검토하고, 정렬과 인덱스 조건을 실제 데이터량으로 확인합니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, 정렬 가능한 ID는 추측 가능성도 일부 생깁니다. UUIDv7이나 Snowflake는 시간 정보가 들어가기 때문에 &ldquo;언제 생성됐는가&quot;를 어느 정도 드러낼 수 있습니다. 공개 URL에서 생성 시각 자체가 민감하면 별도 random token을 쓰는 편이 낫습니다.</p>
<p>둘째, Snowflake는 라이브러리 하나로 끝나지 않습니다. worker id 배정, 시간 동기화, overflow 대기, 장애 시 생성 중단 정책이 없으면 운영 중 더 어려워집니다. 초당 생성량이 크지 않다면 UUIDv7이나 DB sequence가 더 단순할 수 있습니다.</p>
<p>셋째, DB sequence는 중앙화가 단점이지만 동시에 장점입니다. 단일 DB 트랜잭션 안에서 생성되고 커밋되므로 추적이 쉽습니다. 실제 병목이 sequence인지 확인하기 전에 &ldquo;분산형이 더 멋있다&quot;는 이유로 바꾸면 구조만 복잡해질 수 있습니다.</p>
<p>넷째, ID는 멱등성과 다릅니다. 주문 ID가 같다고 같은 요청이라는 뜻은 아닙니다. 재시도 중복을 막으려면 <a href="/learning/deep-dive/deep-dive-idempotency/">멱등성 설계</a>와 <a href="/learning/deep-dive/deep-dive-upsert-unique-idempotency-write-path-playbook/">UPSERT, UNIQUE 제약, 멱등 키</a>처럼 별도 처리 이력을 설계해야 합니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="운영-체크리스트">운영 체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 내부 PK와 외부 노출 ID를 분리할지 결정했다.</li>
<li><input disabled="" type="checkbox"> ID 전략을 테이블 유형별로 문서화했다.</li>
<li><input disabled="" type="checkbox"> 외부에 auto increment 값을 직접 노출하지 않는다.</li>
<li><input disabled="" type="checkbox"> UUIDv4를 고쓰기 테이블 PK로 쓸 때 인덱스 bloat와 쓰기 p95를 측정했다.</li>
<li><input disabled="" type="checkbox"> Snowflake 계열이면 worker id 중복, clock rollback, sequence overflow 정책이 있다.</li>
<li><input disabled="" type="checkbox"> ID 생성 실패 지표와 알람이 있다.</li>
<li><input disabled="" type="checkbox"> 멱등 키를 비즈니스 ID와 혼동하지 않는다.</li>
</ul>
<h3 id="연습">연습</h3>
<ol>
<li>현재 서비스의 상위 쓰기 테이블 5개를 고르고 PK 타입, 외부 노출 여부, 초당 쓰기량, 주요 조회 패턴을 표로 정리해 보세요.</li>
<li>UUIDv4 PK 테이블이 있다면 최신순 조회와 대량 insert에서 인덱스 크기, p95 지연, page split 징후를 확인해 보세요.</li>
<li>주문 생성 API를 가정하고 내부 <code>id</code>, 외부 <code>public_id</code>, 재시도용 <code>idempotency_key</code>를 각각 어떤 타입으로 둘지 설계해 보세요.</li>
<li>Snowflake ID generator를 쓴다고 가정하고 clock rollback 20ms, worker id 중복, millisecond sequence overflow가 발생했을 때의 동작을 문서화해 보세요.</li>
</ol>
<p>좋은 ID 전략은 가장 최신 기술을 고르는 것이 아니라, <strong>읽기·쓰기·노출·운영 복구 요구를 분리해서 가장 단순한 조합을 고르는 것</strong>입니다. ID는 한 번 퍼지면 바꾸기 어렵습니다. 그래서 초기에 30분 더 써서 기준을 세우는 것이, 나중에 수십억 row를 마이그레이션하는 것보다 훨씬 쌉니다.</p>
]]></content:encoded></item></channel></rss>