<?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>Pessimistic Lock on jyukki's Blog</title><link>https://jyukki.com/tags/pessimistic-lock/</link><description>Recent content in Pessimistic Lock on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Sun, 03 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/pessimistic-lock/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Optimistic Lock, Pessimistic Lock, Atomic Update를 경쟁 비용 기준으로 고르는 법</title><link>https://jyukki.com/learning/deep-dive/deep-dive-optimistic-pessimistic-atomic-update-playbook/</link><pubDate>Sun, 03 May 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-optimistic-pessimistic-atomic-update-playbook/</guid><description>동시성 제어는 락을 세게 거는 문제가 아니라 충돌 비용과 대기 비용을 어디에 둘지 정하는 일입니다. Optimistic Lock, Pessimistic Lock, Atomic Update를 실무 숫자 기준으로 비교합니다.</description><content:encoded><![CDATA[<p>동시성 제어를 공부할 때 많은 팀이 먼저 묻는 질문은 &ldquo;낙관적 락이 좋나요, 비관적 락이 좋나요&quot;입니다. 그런데 실무에서 더 중요한 질문은 따로 있습니다. <strong>이 작업의 실패 비용이 큰가, 대기 비용이 큰가, 아니면 한 SQL 문으로 충돌 자체를 닫을 수 있는가</strong>입니다. 같은 재고 차감 문제라도 경쟁 빈도, 초과 판매 비용, API 왕복 구조에 따라 정답이 완전히 달라집니다.</p>
<p>특히 <code>Optimistic Lock</code>과 <code>Pessimistic Lock</code>만 비교하면 의외로 놓치는 축이 있습니다. 바로 <code>Atomic Update</code>입니다. 읽고 판단하고 다시 쓰는 2단계 흐름이 아니라, <code>UPDATE ... WHERE 조건</code> 한 번으로 규칙을 밀어 넣을 수 있다면 락 전략 자체가 더 단순해집니다. 그래서 이 글은 <a href="/learning/deep-dive/deep-dive-snapshot-isolation-serializable-write-skew-playbook/">Snapshot Isolation과 Write Skew</a>, <a href="/learning/deep-dive/deep-dive-database-locking-contention-playbook/">DB Lock Contention 플레이북</a>, <a href="/learning/deep-dive/deep-dive-idempotency/">멱등성 설계</a>, <a href="/learning/deep-dive/deep-dive-mysql-isolation-locks/">MySQL 트랜잭션 격리 수준과 락</a>을 잇는 관점으로, 세 가지 선택지를 <strong>경쟁 비용 기준</strong>으로 정리합니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>Optimistic Lock, Pessimistic Lock, Atomic Update가 각각 어떤 종류의 충돌에 강한지 구분할 수 있습니다.</li>
<li>충돌률, lock wait, retry 성공률, 초과 판매 비용 같은 운영 숫자로 선택 기준을 세울 수 있습니다.</li>
<li>단순히 &ldquo;락을 더 세게 건다&quot;가 아니라, 한 SQL 문으로 닫을 수 있는 문제와 그렇지 않은 문제를 분리할 수 있습니다.</li>
<li>재고, 쿠폰, 예약, 상태 전이처럼 자주 나오는 경로에서 어떤 전략을 먼저 검토해야 하는지 우선순위를 가져갈 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-세-전략의-차이는-동시성-철학보다-충돌-처리-위치에-있다">1) 세 전략의 차이는 동시성 철학보다 충돌 처리 위치에 있다</h3>
<p>세 가지는 모두 &ldquo;같은 데이터를 동시에 바꾸려는 상황&quot;을 다루지만, 충돌을 처리하는 위치가 다릅니다.</p>
<ul>
<li><strong>Optimistic Lock</strong>: 일단 읽고 작업한 뒤, 커밋 시점에 버전 충돌을 감지합니다.</li>
<li><strong>Pessimistic Lock</strong>: 읽는 순간 또는 수정 직전에 잠금을 잡아 다른 작업을 대기시킵니다.</li>
<li><strong>Atomic Update</strong>: 애플리케이션 메모리에서 판단하지 않고, SQL 조건문 안에 비즈니스 조건을 넣어 한 번에 갱신합니다.</li>
</ul>
<p>핵심은 &ldquo;어느 것이 더 고급인가&quot;가 아니라, <strong>충돌을 재시도로 흡수할지, 대기로 흡수할지, 아예 DB 한 문장으로 축약할지</strong>입니다. 이 차이를 먼저 보면 선택이 훨씬 덜 감정적이 됩니다.</p>
<h3 id="2-optimistic-lock은-충돌이-드문데-사용자-사고-시간이-긴-경로에-잘-맞는다">2) Optimistic Lock은 충돌이 드문데 사용자 사고 시간이 긴 경로에 잘 맞는다</h3>
<p>Optimistic Lock의 가장 큰 장점은 잠금 대기가 거의 없다는 점입니다. 예를 들어 관리자가 주문을 열어 보고 몇 초 뒤 승인 버튼을 누르는 경로처럼, 읽기와 쓰기 사이에 사람 판단이 들어가면 비관적 락은 거의 항상 과합니다. 잠금을 오래 잡고 있으면 다른 요청이 줄줄이 막히기 때문입니다.</p>
<p>이때는 <code>version</code> 컬럼이나 <code>updated_at</code> 기반 검사를 두고, 충돌 시 &ldquo;다른 사용자가 먼저 수정했습니다&rdquo; 또는 짧은 자동 재시도로 처리하는 편이 낫습니다. 보통 아래 조건이면 Optimistic Lock이 출발점으로 꽤 좋습니다.</p>
<ul>
<li>동일 키 충돌률이 <strong>1~2% 이하</strong></li>
<li>충돌 시 재시도 비용이 <strong>100ms~수백 ms 수준</strong></li>
<li>읽기와 쓰기 사이에 사람 확인, 외부 API, 긴 비즈니스 로직이 있다</li>
<li>잘못된 중복 반영보다 &ldquo;수정 실패 후 다시 시도&quot;가 더 싸다</li>
</ul>
<p>반대로 충돌률이 계속 <strong>5%를 넘고</strong>, 같은 엔티티를 여러 사용자가 동시에 자주 만지며, 실패 시 UX가 크게 흔들리면 Optimistic Lock만으로는 피로가 커집니다. 이때는 전략을 바꾸거나, 최소한 <a href="/learning/deep-dive/deep-dive-timeout-retry-backoff/">Timeout/Retry/Backoff</a>와 멱등성 정책을 같이 봐야 합니다.</p>
<h3 id="3-pessimistic-lock은-실패를-줄이지만-대기와-데드락-비용을-앞으로-당긴다">3) Pessimistic Lock은 실패를 줄이지만, 대기와 데드락 비용을 앞으로 당긴다</h3>
<p>Pessimistic Lock은 &ldquo;충돌이 나면 나중에 실패하자&quot;가 아니라 &ldquo;애초에 한 명씩만 들어오자&quot;에 가깝습니다. <code>SELECT ... FOR UPDATE</code>나 유사 잠금으로 먼저 선점하면, 후속 요청은 대기하거나 timeout으로 빠집니다. 초과 판매, 중복 출금, 중복 쿠폰 발급처럼 <strong>한 번 잘못 커밋되면 복구 비용이 큰 경로</strong>에서는 이 접근이 훨씬 단순할 때가 많습니다.</p>
<p>다만 비용도 분명합니다.</p>
<ul>
<li>lock wait가 p95, p99 지연시간으로 바로 보입니다.</li>
<li>잠금 순서가 어긋나면 데드락 위험이 생깁니다.</li>
<li>트랜잭션 안에 외부 API 호출이나 긴 계산이 있으면 잠금 보유 시간이 길어집니다.</li>
<li>높은 QPS에서 대기가 재시도와 만나면 큐 적체가 커질 수 있습니다.</li>
</ul>
<p>실무 출발선으로는 아래 정도가 보수적입니다.</p>
<ul>
<li>동일 키 경합이 초당 <strong>5회 이상</strong> 반복된다</li>
<li>잘못 한 번 반영됐을 때 금전 또는 신뢰 비용이 크다</li>
<li>잠금 보유 구간을 <strong>50ms 이하</strong>로 유지할 수 있다</li>
<li><code>lock_wait_p95</code>를 <strong>100ms 이하</strong>로 묶을 자신이 있다</li>
</ul>
<p>이 조건을 만족하지 못하면 비관적 락은 문제 해결보다 병목 생성 장치가 되기 쉽습니다. 그래서 <a href="/learning/deep-dive/deep-dive-database-locking-contention-playbook/">DB Lock Contention 플레이북</a>처럼 잠금 범위, 보유 시간, 접근 순서를 같이 설계해야 합니다.</p>
<h3 id="4-atomic-update는-생각보다-많은-문제의-기본값이다">4) Atomic Update는 생각보다 많은 문제의 기본값이다</h3>
<p>재고 차감, 잔액 감소, 상태 전이, quota 소모처럼 많은 경로는 사실 &ldquo;먼저 읽고 나중에 쓰는&rdquo; 애플리케이션 판단이 꼭 필요하지 않습니다. 예를 들어 아래처럼 한 문장으로 닫을 수 있습니다.</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">UPDATE</span> inventory
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SET</span> stock <span style="color:#ff79c6">=</span> stock <span style="color:#ff79c6">-</span> <span style="color:#bd93f9">1</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> product_id <span style="color:#ff79c6">=</span> :id
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> stock <span style="color:#ff79c6">&gt;=</span> <span style="color:#bd93f9">1</span>;
</span></span></code></pre></div><p>영향 받은 행 수가 1이면 성공, 0이면 재고 부족입니다. 이 방식의 장점은 명확합니다.</p>
<ul>
<li>round trip이 줄어듭니다.</li>
<li>읽은 뒤 다른 요청이 끼어드는 창이 줄어듭니다.</li>
<li>Optimistic Lock처럼 별도 재조회가 없어도 됩니다.</li>
<li>Pessimistic Lock처럼 긴 잠금 설계를 직접 들고 가지 않아도 됩니다.</li>
</ul>
<p>그래서 저는 단일 행 또는 단일 조건으로 표현 가능한 규칙이라면 <strong>Atomic Update를 제일 먼저 검토하는 편</strong>이 맞다고 봅니다. 특히 재고, 포인트 차감, 상태가 <code>PENDING</code>일 때만 <code>PAID</code>로 바뀌는 전이 같은 경로는 이 방식이 가장 운영 친화적입니다.</p>
<p>물론 한계도 있습니다. 여러 행의 합계나 부재 조건처럼 <strong>집합 불변식</strong>은 SQL 한 문장으로 표현이 까다롭고, 이 경우는 <a href="/learning/deep-dive/deep-dive-snapshot-isolation-serializable-write-skew-playbook/">Snapshot Isolation과 Write Skew</a>에서 본 것처럼 추가 잠금이나 Serializable, 보조 row 모델링이 필요합니다.</p>
<h3 id="5-전략-선택은-기술-취향이-아니라-불변식-모양에-달려-있다">5) 전략 선택은 기술 취향이 아니라 불변식 모양에 달려 있다</h3>
<p>세 전략을 가장 빨리 가르는 질문은 아래 네 개입니다.</p>
<ol>
<li>이 규칙은 <strong>한 행</strong>으로 닫히는가, 아니면 <strong>여러 행</strong>을 봐야 하는가</li>
<li>충돌 시 <strong>실패 후 재시도</strong>가 더 싼가, <strong>대기 후 성공 보장</strong>이 더 싼가</li>
<li>잘못 한 번 반영됐을 때 비용이 큰가</li>
<li>트랜잭션이 짧고 예측 가능한가</li>
</ol>
<p>이 네 질문에 답하면 방향이 꽤 빨리 나옵니다.</p>
<ul>
<li>단일 행 규칙, 짧은 쓰기, 높은 QPS → Atomic Update 우선</li>
<li>충돌 드묾, 사람 개입 많음, 재시도 가능 → Optimistic Lock 우선</li>
<li>충돌 잦음, 오류 비용 큼, 짧은 임계 구간 → Pessimistic Lock 우선</li>
<li>여러 행 불변식 → 별도 aggregate row, 제약조건, Serializable까지 검토</li>
</ul>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-빠른-선택-매트릭스">1) 빠른 선택 매트릭스</h3>
<table>
  <thead>
      <tr>
          <th>상황</th>
          <th>우선 전략</th>
          <th>이유</th>
          <th>주의점</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>재고 1개 차감, 포인트 차감, quota 소모</td>
          <td>Atomic Update</td>
          <td>한 SQL 문으로 규칙 표현 가능</td>
          <td>영향 행 수 검사 필수</td>
      </tr>
      <tr>
          <td>관리자 수정, 사용자가 긴 시간 편집 후 저장</td>
          <td>Optimistic Lock</td>
          <td>잠금 오래 잡지 않음</td>
          <td>충돌 메시지/재시도 UX 필요</td>
      </tr>
      <tr>
          <td>쿠폰 선착순, 중복 결제 방지, 강한 선점 필요</td>
          <td>Pessimistic Lock</td>
          <td>사전 차단이 복구보다 쌈</td>
          <td>lock hold time 최소화</td>
      </tr>
      <tr>
          <td>여러 행 합계, 교대표, 좌석 묶음 규칙</td>
          <td>혼합 전략</td>
          <td>집합 불변식</td>
          <td>lock row 또는 Serializable 검토</td>
      </tr>
  </tbody>
</table>
<p>중요한 건 한 서비스가 하나의 전략만 쓰지 않아도 된다는 점입니다. 주문 서비스 안에서도 재고는 Atomic Update, 주문 수정은 Optimistic Lock, 정산 마감은 Pessimistic Lock처럼 섞이는 편이 더 현실적입니다.</p>
<h3 id="2-숫자로-보는-의사결정-기준">2) 숫자로 보는 의사결정 기준</h3>
<p>보통 아래 기준으로 시작하면 실무에서 크게 벗어나지 않습니다.</p>
<ul>
<li><code>conflict_rate &lt; 2%</code> 이고 충돌 재시도 성공률이 <strong>80% 이상</strong> → Optimistic Lock 유지 가능</li>
<li><code>conflict_rate &gt;= 5%</code> 이고 실패 1건의 비용이 큼 → Pessimistic Lock 또는 Atomic Update 검토</li>
<li>규칙을 <code>UPDATE ... WHERE 조건</code>으로 표현 가능 → 다른 전략보다 Atomic Update 먼저 검토</li>
<li><code>lock_wait_p95 &gt; 100ms</code> 또는 데드락이 주간 단위로 반복 → 잠금 범위 축소 또는 전략 재설계</li>
<li>트랜잭션 안에 외부 API 호출, 파일 I/O, 사용자 입력이 있다 → Pessimistic Lock 기본값 금지에 가깝게 보기</li>
<li>초과 판매/중복 승인 사고 비용이 크다 → Optimistic Lock 단독보다 사전 차단 또는 보조 제약조건 우선</li>
</ul>
<p>우선순위는 대개 이 순서가 안전합니다.</p>
<ol>
<li>잘못 커밋된 효과를 막는다</li>
<li>한 문장으로 닫을 수 있는 규칙은 최대한 DB로 내린다</li>
<li>그다음 재시도 또는 대기 비용을 비교한다</li>
<li>마지막에 평균 latency를 최적화한다</li>
</ol>
<h3 id="3-대표-시나리오별-추천-출발점">3) 대표 시나리오별 추천 출발점</h3>
<ul>
<li><strong>재고 차감</strong>: <code>UPDATE ... WHERE stock &gt;= n</code> 형태 Atomic Update, 필요 시 주문 idempotency 추가</li>
<li><strong>쿠폰 선착순 발급</strong>: 남은 수량 row를 Atomic Update 또는 짧은 Pessimistic Lock으로 보호</li>
<li><strong>관리자 상세 편집 화면</strong>: Optimistic Lock + 충돌 시 diff 안내</li>
<li><strong>결제 중복 승인 방지</strong>: 상태 전이 Atomic Update + 외부 결제 호출 전후 멱등 키 고정</li>
<li><strong>좌석/병상/당직표 같은 집합 규칙</strong>: aggregate row 잠금 또는 Serializable 제한 적용</li>
</ul>
<p>여기서 많이 하는 실수가 &ldquo;모든 경쟁은 락으로 해결&rdquo; 또는 반대로 &ldquo;버전 컬럼만 있으면 다 된다&quot;입니다. 실제로는 도메인별로 불변식 모양이 달라서 전략도 달라져야 합니다.</p>
<h3 id="4-운영-지표-최소-세트">4) 운영 지표 최소 세트</h3>
<p>세 전략을 썼다면 아래 지표는 최소로 보는 편이 좋습니다.</p>
<ul>
<li><code>conflict_rate</code></li>
<li><code>lock_wait_p95</code>, <code>lock_wait_timeout_rate</code></li>
<li><code>retry_success_rate</code></li>
<li><code>rows_affected_zero_rate</code> (Atomic Update 실패율)</li>
<li><code>deadlock_count</code></li>
<li><code>duplicate_effect_detected</code></li>
</ul>
<p>Atomic Update에서는 <code>rows_affected_zero_rate</code> 해석이 특히 중요합니다. 이 수치가 갑자기 오르면 진짜 재고 부족인지, 경쟁이 심해진 것인지, 조건식이 과도한지 구분해야 합니다. Optimistic Lock은 retry 성공률을, Pessimistic Lock은 lock wait와 deadlock을 먼저 봐야 판단이 빨라집니다.</p>
<h3 id="5-2주-도입-플레이북">5) 2주 도입 플레이북</h3>
<p><strong>1주차</strong></p>
<ul>
<li>충돌이 잦은 쓰기 경로 5개를 고릅니다.</li>
<li>각 경로를 <code>단일 행 규칙</code>, <code>상태 전이</code>, <code>집합 불변식</code>, <code>사람 개입 긴 편집</code>으로 분류합니다.</li>
<li>현재 충돌률, retry 성공률, lock wait를 측정합니다.</li>
</ul>
<p><strong>2주차</strong></p>
<ul>
<li>단일 행 규칙 1개는 Atomic Update로 바꿔 봅니다.</li>
<li>긴 편집 경로 1개는 Optimistic Lock 충돌 UX를 다듬습니다.</li>
<li>오류 비용이 큰 경로 1개는 짧은 Pessimistic Lock 또는 제약조건으로 보호합니다.</li>
<li>이후 지표를 비교해 어떤 비용이 실제로 줄었는지 확인합니다.</li>
</ul>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, <strong>Optimistic Lock은 충돌이 적을 때만 낙관적입니다.</strong> 충돌률이 높아지면 사용자는 저장 실패를 반복해서 보게 되고, 운영자는 재시도 폭증을 보게 됩니다.</p>
<p>둘째, <strong>Pessimistic Lock은 안전해 보여도 긴 트랜잭션과 만나면 바로 병목이 됩니다.</strong> 트랜잭션 안에서 외부 API를 부르거나 여러 테이블을 넓게 잡으면 lock wait가 본체가 됩니다.</p>
<p>셋째, <strong>Atomic Update는 단순하지만, 애플리케이션이 성공/실패 분기를 엄격하게 처리해야 합니다.</strong> 영향 행 수 0을 그냥 일반 오류로 넘기면 재고 부족과 시스템 오류가 섞여 버립니다.</p>
<p>넷째, <strong>집합 불변식은 셋 중 하나만으로 닫히지 않는 경우가 많습니다.</strong> 이때는 제약조건, aggregate row, Serializable, <a href="/learning/deep-dive/deep-dive-advisory-locks-coordination-playbook/">Advisory Lock</a>, <a href="/learning/deep-dive/deep-dive-idempotency/">멱등성</a>를 조합해야 합니다.</p>
<p>다섯째, <strong>동시성 제어는 DB 안에서 끝나지 않습니다.</strong> API 레벨 중복 요청, 큐 재처리, 외부 결제 재시도까지 연결되므로 <a href="/learning/deep-dive/deep-dive-idempotency/">멱등성 설계</a>와 <a href="/learning/deep-dive/deep-dive-timeout-retry-backoff/">Timeout/Retry/Backoff</a>를 같이 봐야 운영이 닫힙니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 이 규칙이 단일 행 조건으로 닫히는지 먼저 확인했다.</li>
<li><input disabled="" type="checkbox"> 충돌률, lock wait, retry 성공률을 실제 숫자로 측정했다.</li>
<li><input disabled="" type="checkbox"> 사람 개입이 긴 경로에 비관적 락을 무심코 적용하지 않았다.</li>
<li><input disabled="" type="checkbox"> Atomic Update 실패와 시스템 오류를 다른 코드 경로로 처리한다.</li>
<li><input disabled="" type="checkbox"> 초과 판매, 중복 승인 같은 고비용 경로는 사전 차단 전략을 우선 검토했다.</li>
<li><input disabled="" type="checkbox"> 집합 불변식 경로는 제약조건 또는 aggregate row 모델링을 같이 검토했다.</li>
</ul>
<h3 id="연습-과제">연습 과제</h3>
<ol>
<li>현재 서비스의 쓰기 경로 5개를 골라 <code>단일 행 규칙 / 상태 전이 / 집합 불변식 / 긴 편집</code>으로 분류해 보세요.</li>
<li>그중 1개는 Atomic Update로 바꿀 수 있는지 SQL 수준에서 다시 써 보세요.</li>
<li>충돌률이 가장 높은 경로 1개를 골라 Optimistic Lock 재시도와 Pessimistic Lock 대기 중 어느 비용이 더 큰지 추정해 보세요.</li>
<li>최근 중복 처리 사고나 초과 판매 사고가 있었다면, 그 경로가 실제로 어떤 전략을 택했어야 했는지 포스트모템 형태로 적어 보세요.</li>
</ol>
<h2 id="관련-글">관련 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-snapshot-isolation-serializable-write-skew-playbook/">Snapshot Isolation, Serializable, Write Skew 실무 판단 플레이북</a></li>
<li><a href="/learning/deep-dive/deep-dive-database-locking-contention-playbook/">DB Lock Contention 대응 플레이북</a></li>
<li><a href="/learning/deep-dive/deep-dive-mysql-isolation-locks/">MySQL 트랜잭션 격리 수준과 락</a></li>
<li><a href="/learning/deep-dive/deep-dive-idempotency/">멱등성 설계와 중복 요청 제어</a></li>
<li><a href="/learning/deep-dive/deep-dive-advisory-locks-coordination-playbook/">Advisory Lock 실무 플레이북</a></li>
<li><a href="/learning/modules/backend-data-system-phase/">백엔드 데이터 시스템 단계</a></li>
</ul>
]]></content:encoded></item></channel></rss>