<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Mvcc on kastori</title><link>http://blog.kastori.dev/tags/mvcc/</link><description>Recent content in Mvcc on kastori</description><generator>Hugo -- gohugo.io</generator><language>ko-kr</language><lastBuildDate>Sun, 31 May 2026 00:00:00 +0900</lastBuildDate><atom:link href="http://blog.kastori.dev/tags/mvcc/index.xml" rel="self" type="application/rss+xml"/><item><title>[DB 완전 정복 #6] MySQL과 PostgreSQL, 뭐가 다른가 — MVCC 구현부터 JSONB까지</title><link>http://blog.kastori.dev/tech/2026-05-31-db-06-mysql-vs-postgresql/</link><pubDate>Sun, 31 May 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-05-31-db-06-mysql-vs-postgresql/</guid><description>&lt;h2 id="그냥-mysql-쓰면-안-되나요"&gt;&lt;a href="#%ea%b7%b8%eb%83%a5-mysql-%ec%93%b0%eb%a9%b4-%ec%95%88-%eb%90%98%eb%82%98%ec%9a%94" class="header-anchor"&gt;&lt;/a&gt;&amp;ldquo;그냥 MySQL 쓰면 안 되나요?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;사실 대부분의 경우 MySQL로 충분하다. 하지만 &amp;ldquo;왜 MySQL인가요?&amp;ldquo;라는 질문에 &amp;ldquo;익숙해서요&amp;quot;는 좋은 답이 아니다. 두 DB의 내부 구조 차이를 알면, 상황에 따른 선택 근거를 만들 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="mvcc-구현-방식--가장-근본적인-차이"&gt;&lt;a href="#mvcc-%ea%b5%ac%ed%98%84-%eb%b0%a9%ec%8b%9d--%ea%b0%80%ec%9e%a5-%ea%b7%bc%eb%b3%b8%ec%a0%81%ec%9d%b8-%ec%b0%a8%ec%9d%b4" class="header-anchor"&gt;&lt;/a&gt;MVCC 구현 방식 — 가장 근본적인 차이
&lt;/h2&gt;&lt;p&gt;앞서 MVCC가 트랜잭션의 일관된 읽기를 보장한다고 배웠다. 그런데 MySQL과 PostgreSQL은 이 MVCC를 다른 방식으로 구현한다.&lt;/p&gt;
&lt;h3 id="mysql-innodb-undo-log-방식"&gt;&lt;a href="#mysql-innodb-undo-log-%eb%b0%a9%ec%8b%9d" class="header-anchor"&gt;&lt;/a&gt;MySQL InnoDB: Undo Log 방식
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[테이블 페이지]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 행 데이터: TX_ID=200, name=&amp;#34;수정됨&amp;#34; ← 항상 최신 버전
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └──(포인터)──▶ [Undo Log]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; TX_ID=100, name=&amp;#34;원본&amp;#34; ← 이전 버전
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └──▶ [Undo Log]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; TX_ID=50, name=&amp;#34;초기값&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;현재 데이터 페이지에는 &lt;strong&gt;최신 버전만&lt;/strong&gt; 저장한다. 이전 버전은 별도 Undo Log 공간에 체인으로 보관한다. 읽기 시 자신의 스냅샷보다 최신 데이터면 Undo Log에서 이전 버전을 찾아 읽는다.&lt;/p&gt;
&lt;h3 id="postgresql-heap-방식"&gt;&lt;a href="#postgresql-heap-%eb%b0%a9%ec%8b%9d" class="header-anchor"&gt;&lt;/a&gt;PostgreSQL: Heap 방식
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[테이블 파일 (Heap)]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; t_xmin=100, t_xmax=200, name=&amp;#34;원본&amp;#34; ← 이전 버전 (dead tuple)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; t_xmin=200, t_xmax=0, name=&amp;#34;수정됨&amp;#34; ← 현재 버전 (live tuple)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;같은 테이블 파일 안에 &lt;strong&gt;여러 버전이 공존&lt;/strong&gt;한다. 각 행에 생성 트랜잭션(&lt;code&gt;t_xmin&lt;/code&gt;)과 삭제 트랜잭션(&lt;code&gt;t_xmax&lt;/code&gt;)을 기록한다. 읽기 시 내 트랜잭션 기준으로 어떤 버전을 볼지 visibility check로 판단한다.&lt;/p&gt;
&lt;h3 id="운영상-차이"&gt;&lt;a href="#%ec%9a%b4%ec%98%81%ec%83%81-%ec%b0%a8%ec%9d%b4" class="header-anchor"&gt;&lt;/a&gt;운영상 차이
&lt;/h3&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;항목&lt;/th&gt;
 &lt;th&gt;MySQL&lt;/th&gt;
 &lt;th&gt;PostgreSQL&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;이전 버전 위치&lt;/td&gt;
 &lt;td&gt;별도 Undo Log&lt;/td&gt;
 &lt;td&gt;같은 테이블 파일&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;정리 방법&lt;/td&gt;
 &lt;td&gt;Purge 스레드 자동&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;VACUUM&lt;/strong&gt; 필수&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;운영 주의점&lt;/td&gt;
 &lt;td&gt;긴 트랜잭션 (Undo 과다)&lt;/td&gt;
 &lt;td&gt;table bloat, autovacuum 튜닝&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;PostgreSQL은 오래된 dead tuple이 쌓이면 &lt;strong&gt;table bloat&lt;/strong&gt; 현상이 발생한다. autovacuum이 자동으로 정리하지만, 대용량 테이블에서는 VACUUM 튜닝이 운영의 핵심이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="인덱스-구조--clustered-vs-heap"&gt;&lt;a href="#%ec%9d%b8%eb%8d%b1%ec%8a%a4-%ea%b5%ac%ec%a1%b0--clustered-vs-heap" class="header-anchor"&gt;&lt;/a&gt;인덱스 구조 — Clustered vs Heap
&lt;/h2&gt;&lt;p&gt;MySQL의 기본키 인덱스는 &lt;strong&gt;Clustered Index&lt;/strong&gt;다. 리프 노드에 실제 행 데이터가 저장되고, 기본키 순서로 물리적으로 정렬된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MySQL Secondary Index 조회:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 인덱스에서 PK 찾기 → PK로 Clustered Index 조회 (2단계)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PostgreSQL 모든 인덱스 조회:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 인덱스에서 heap tuple pointer 찾기 → heap에서 행 조회 (1단계)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MySQL의 Secondary Index는 2단계를 거치지만, 기본키 조회는 Clustered Index 덕분에 최고 성능이다. PostgreSQL은 모든 인덱스가 동등한 구조를 가진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="jsonb--postgresql이-mysql을-압도하는-부분"&gt;&lt;a href="#jsonb--postgresql%ec%9d%b4-mysql%ec%9d%84-%ec%95%95%eb%8f%84%ed%95%98%eb%8a%94-%eb%b6%80%eb%b6%84" class="header-anchor"&gt;&lt;/a&gt;JSONB — PostgreSQL이 MySQL을 압도하는 부분
&lt;/h2&gt;&lt;p&gt;반정형 데이터(JSON)를 DB에 저장해야 한다면, PostgreSQL의 JSONB가 압도적으로 유리하다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- MySQL JSON: 텍스트로 저장, 조회마다 파싱
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COLUMN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 인덱스를 걸려면 Generated Column 트릭 필요
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COLUMN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meta_color&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GENERATED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ALWAYS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_UNQUOTE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$.color&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;STORED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idx_color&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta_color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- PostgreSQL JSONB: 바이너리 파싱 후 저장, GIN 인덱스 직접 가능
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COLUMN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idx_meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USING&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 풍부한 연산자
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;@&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&amp;#34;color&amp;#34;: &amp;#34;red&amp;#34;}&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 포함 여부
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;color&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 키 존재 여부
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;PostgreSQL JSONB는 바이너리로 저장해 파싱 없이 빠르고, GIN 인덱스로 JSON 내부 키에 직접 인덱싱할 수 있다. MySQL은 이 수준의 JSON 쿼리 지원이 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="postgresql의-확장-생태계"&gt;&lt;a href="#postgresql%ec%9d%98-%ed%99%95%ec%9e%a5-%ec%83%9d%ed%83%9c%ea%b3%84" class="header-anchor"&gt;&lt;/a&gt;PostgreSQL의 확장 생태계
&lt;/h2&gt;&lt;p&gt;PostgreSQL은 확장(Extension)으로 기능을 추가할 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PostGIS → 지리 데이터, 위경도 거리 계산, 공간 인덱스
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pgvector → AI 벡터 임베딩 저장 및 유사도 검색 (LLM 연동)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pg_trgm → 문자열 유사도 검색 (LIKE &amp;#39;%검색어%&amp;#39; 인덱스)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TimescaleDB → 시계열 데이터 최적화
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;LLM, AI 서비스와 연동하거나 지리 기능이 필요하다면 PostgreSQL이 사실상 유일한 선택이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="라이선스-차이"&gt;&lt;a href="#%eb%9d%bc%ec%9d%b4%ec%84%a0%ec%8a%a4-%ec%b0%a8%ec%9d%b4" class="header-anchor"&gt;&lt;/a&gt;라이선스 차이
&lt;/h2&gt;&lt;p&gt;MySQL은 GPL 라이선스다. 오픈소스 프로젝트에서 MySQL을 수정해 배포하면 소스를 공개해야 한다. MariaDB 포크가 생긴 배경이기도 하다.&lt;/p&gt;
&lt;p&gt;PostgreSQL은 BSD 계열(PostgreSQL License)로 &lt;strong&gt;상업용 포함 완전 자유&lt;/strong&gt;다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="선택-기준-정리"&gt;&lt;a href="#%ec%84%a0%ed%83%9d-%ea%b8%b0%ec%a4%80-%ec%a0%95%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;선택 기준 정리
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MySQL을 선택하는 경우:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Spring Boot + JPA 조합 (레퍼런스와 생태계 압도적)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 단순 CRUD 위주의 일반 웹 서비스
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 팀에 MySQL 운영 경험이 있는 경우
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - AWS Aurora MySQL 사용 계획
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PostgreSQL을 선택하는 경우:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - JSON 데이터를 쿼리하고 인덱스도 필요한 경우 (JSONB)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 지리 데이터 (PostGIS)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - AI/ML 벡터 검색 (pgvector)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 복잡한 분석 쿼리 위주 서비스
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 오픈소스 라이선스 자유도가 중요한 경우
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;MySQL은 웹 서비스의 검증된 선택이고, PostgreSQL은 확장성과 기능 다양성에서 강하다. 어느 쪽이 낫다는 게 아니라, &lt;strong&gt;사용하는 이유를 설명할 수 있느냐&lt;/strong&gt;가 중요하다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 관계형 DB와 전혀 다른 구조로 설계된 Redis의 내부 동작 원리를 다룬다.&lt;/p&gt;</description></item><item><title>[DB 완전 정복 #2] MySQL이 동시 요청을 처리하는 방법 — MVCC, 갭 락, 데드락</title><link>http://blog.kastori.dev/tech/2026-05-26-db-02-transaction-lock/</link><pubDate>Tue, 26 May 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-05-26-db-02-transaction-lock/</guid><description>&lt;h2 id="동시에-100명이-같은-행을-읽고-있다면"&gt;&lt;a href="#%eb%8f%99%ec%8b%9c%ec%97%90-100%eb%aa%85%ec%9d%b4-%ea%b0%99%ec%9d%80-%ed%96%89%ec%9d%84-%ec%9d%bd%ea%b3%a0-%ec%9e%88%eb%8b%a4%eb%a9%b4" class="header-anchor"&gt;&lt;/a&gt;동시에 100명이 같은 행을 읽고 있다면
&lt;/h2&gt;&lt;p&gt;서버에 동시 요청이 들어오면 여러 트랜잭션이 같은 데이터를 동시에 읽고 쓴다. 이때 두 가지 문제가 생긴다.&lt;/p&gt;
&lt;p&gt;첫 번째는 &lt;strong&gt;일관성&lt;/strong&gt;. A가 읽는 도중 B가 데이터를 수정하면, A는 수정 전을 봐야 할까 수정 후를 봐야 할까?&lt;/p&gt;
&lt;p&gt;두 번째는 &lt;strong&gt;성능&lt;/strong&gt;. 읽기마다 락을 걸면 안전하지만, 100개 스레드가 모두 대기 상태가 된다.&lt;/p&gt;
&lt;p&gt;MySQL InnoDB는 &lt;strong&gt;MVCC&lt;/strong&gt;로 이 두 문제를 동시에 해결한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="mvcc--락-없이-일관된-읽기"&gt;&lt;a href="#mvcc--%eb%9d%bd-%ec%97%86%ec%9d%b4-%ec%9d%bc%ea%b4%80%eb%90%9c-%ec%9d%bd%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;MVCC — 락 없이 일관된 읽기
&lt;/h2&gt;&lt;p&gt;MVCC(Multi-Version Concurrency Control)는 &lt;strong&gt;각 트랜잭션이 자신이 시작한 시점의 스냅샷을 본다&lt;/strong&gt;는 개념이다.&lt;/p&gt;
&lt;p&gt;InnoDB는 모든 행에 숨겨진 컬럼을 둔다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DB_TRX_ID : 이 행을 마지막으로 수정한 트랜잭션 ID
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DB_ROLL_PTR : Undo Log 포인터 (이전 버전으로 가는 링크)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;트랜잭션이 시작되면 &lt;strong&gt;스냅샷 ID&lt;/strong&gt;가 부여된다. 이후 데이터를 읽을 때, 해당 행의 &lt;code&gt;DB_TRX_ID&lt;/code&gt;가 자신의 스냅샷 ID보다 크면(= 내가 시작한 이후에 수정됨) Undo Log에서 이전 버전을 찾아 읽는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[현재 데이터] TX_ID=200, name=&amp;#34;수정됨&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └──▶ [Undo Log] TX_ID=100, name=&amp;#34;원본&amp;#34; ← TX 150이 읽는 버전
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └──▶ [Undo Log] TX_ID=50, name=&amp;#34;초기값&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;트랜잭션 150은 TX 200이 수정한 내용을 보지 않는다. 락 없이도 일관된 데이터를 읽는다. 이것이 MVCC의 핵심이다.&lt;/p&gt;
&lt;p&gt;덕분에 &lt;strong&gt;읽기는 쓰기를 방해하지 않고, 쓰기는 읽기를 방해하지 않는다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="행-레벨-락--필요한-행만-잠근다"&gt;&lt;a href="#%ed%96%89-%eb%a0%88%eb%b2%a8-%eb%9d%bd--%ed%95%84%ec%9a%94%ed%95%9c-%ed%96%89%eb%a7%8c-%ec%9e%a0%ea%b7%bc%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;행 레벨 락 — 필요한 행만 잠근다
&lt;/h2&gt;&lt;p&gt;읽기는 MVCC로, 쓰기는 &lt;strong&gt;행 레벨 락&lt;/strong&gt;으로 처리한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;S락 (Shared Lock) : 읽기 락. 여러 트랜잭션이 동시에 획득 가능
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X락 (Exclusive Lock): 쓰기 락. 하나의 트랜잭션만 획득 가능
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;S + S → 허용
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;S + X → 대기
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X + X → 대기
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;InnoDB는 테이블 전체가 아닌 &lt;strong&gt;특정 행&lt;/strong&gt;에만 락을 건다. &lt;code&gt;id = 1&lt;/code&gt;에 락이 걸려 있어도 &lt;code&gt;id = 2&lt;/code&gt;는 다른 트랜잭션이 자유롭게 수정할 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- id=1에만 X락
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- id=2, 3, 4 ... 다른 트랜잭션이 자유롭게 접근 가능
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="갭-락--phantom-read를-막는-방법"&gt;&lt;a href="#%ea%b0%ad-%eb%9d%bd--phantom-read%eb%a5%bc-%eb%a7%89%eb%8a%94-%eb%b0%a9%eb%b2%95" class="header-anchor"&gt;&lt;/a&gt;갭 락 — Phantom Read를 막는 방법
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;REPEATABLE READ&lt;/code&gt; 격리 수준에서는 같은 쿼리를 두 번 실행해도 결과가 같아야 한다. 하지만 락 없이 범위 조회를 하면, 그 사이 다른 트랜잭션이 새 행을 삽입(INSERT)할 수 있다. 이것이 &lt;strong&gt;Phantom Read&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;InnoDB는 이를 **갭 락(Gap Lock)**으로 막는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- orders에 id: 10, 20, 30이 있다고 가정
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BETWEEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 10~30 범위의 &amp;#34;빈 공간(갭)&amp;#34;에도 락 → 이 범위에 INSERT 불가
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;갭 락은 인덱스 레코드 사이의 공간에 걸린다. 행이 없어도 &amp;ldquo;그 자리&amp;quot;를 잠가서 새 INSERT를 막는다.&lt;/p&gt;
&lt;p&gt;실무에서 갭 락 주의사항: 인덱스가 없는 컬럼으로 UPDATE하면 범위를 특정할 수 없어 테이블 전체에 갭 락이 걸린다. 다른 트랜잭션의 INSERT가 전부 막힌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="데드락--서로를-기다리는-교착-상태"&gt;&lt;a href="#%eb%8d%b0%eb%93%9c%eb%9d%bd--%ec%84%9c%eb%a1%9c%eb%a5%bc-%ea%b8%b0%eb%8b%a4%eb%a6%ac%eb%8a%94-%ea%b5%90%ec%b0%a9-%ec%83%81%ed%83%9c" class="header-anchor"&gt;&lt;/a&gt;데드락 — 서로를 기다리는 교착 상태
&lt;/h2&gt;&lt;p&gt;두 트랜잭션이 서로 상대방의 락이 풀리기를 기다리면 데드락이 발생한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- TX A -- TX B
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- A가 id=1 X락 획득 -- B가 id=2 X락 획득
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- id=2 락 대기 (B가 가지고 있음) -- id=1 락 대기 (A가 가지고 있음)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- → 영원히 대기 = 데드락
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;InnoDB는 데드락을 자동으로 감지해서 &lt;strong&gt;한쪽 트랜잭션을 강제 롤백&lt;/strong&gt;한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERROR 1213 (40001): Deadlock found when trying to get lock;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;try restarting transaction
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="데드락-예방-4가지"&gt;&lt;a href="#%eb%8d%b0%eb%93%9c%eb%9d%bd-%ec%98%88%eb%b0%a9-4%ea%b0%80%ec%a7%80" class="header-anchor"&gt;&lt;/a&gt;데드락 예방 4가지
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. 모든 트랜잭션에서 테이블/행 접근 순서를 일관되게 유지
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; (항상 id 오름차순으로 락을 잡으면 교차가 발생하지 않는다)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 트랜잭션을 짧게 유지 — 락 보유 시간 최소화
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 인덱스를 통한 정확한 행 접근 — 갭 락 범위 최소화
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. @Transactional(timeout = 5) — 일정 시간 후 자동 롤백
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="낙관적-락-vs-비관적-락--언제-뭘-써야-하나"&gt;&lt;a href="#%eb%82%99%ea%b4%80%ec%a0%81-%eb%9d%bd-vs-%eb%b9%84%ea%b4%80%ec%a0%81-%eb%9d%bd--%ec%96%b8%ec%a0%9c-%eb%ad%98-%ec%8d%a8%ec%95%bc-%ed%95%98%eb%82%98" class="header-anchor"&gt;&lt;/a&gt;낙관적 락 vs 비관적 락 — 언제 뭘 써야 하나
&lt;/h2&gt;&lt;p&gt;충돌이 드문지 잦은지에 따라 전략이 달라진다.&lt;/p&gt;
&lt;h3 id="낙관적-락--충돌이-드물다"&gt;&lt;a href="#%eb%82%99%ea%b4%80%ec%a0%81-%eb%9d%bd--%ec%b6%a9%eb%8f%8c%ec%9d%b4-%eb%93%9c%eb%ac%bc%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;낙관적 락 — &amp;ldquo;충돌이 드물다&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;읽을 때는 락을 걸지 않고, 수정할 때 version 조건을 추가한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 읽기 (락 없음)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 결과: stock=10, version=5
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 수정 (version 조건 추가)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 영향받은 행이 0이면 → 다른 트랜잭션이 먼저 수정 → 애플리케이션에서 재시도
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;JPA에서는 &lt;code&gt;@Version&lt;/code&gt; 어노테이션으로 자동 처리된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;적합한 케이스&lt;/strong&gt;: 게시글 수정, 사용자 프로필 변경 — 동시 수정이 드문 경우&lt;/p&gt;
&lt;h3 id="비관적-락--충돌이-잦다"&gt;&lt;a href="#%eb%b9%84%ea%b4%80%ec%a0%81-%eb%9d%bd--%ec%b6%a9%eb%8f%8c%ec%9d%b4-%ec%9e%a6%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;비관적 락 — &amp;ldquo;충돌이 잦다&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;읽을 때부터 X락을 걸어 다른 트랜잭션의 접근 자체를 막는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- X락
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 커밋 시 락 해제
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;적합한 케이스&lt;/strong&gt;: 재고 차감, 선착순 쿠폰, 포인트 사용 — 동시 수정이 잦고 정확성이 중요한 경우&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;MVCC는 읽기와 쓰기가 서로를 방해하지 않게 해준다. 행 레벨 락은 필요한 부분만 잠근다. 갭 락은 Phantom Read를 막는다. 그리고 데드락은 락 순서를 통일하면 예방할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Transactional&lt;/code&gt;을 쓸 때 격리 수준을 설정하는 이유, JPA의 &lt;code&gt;@Version&lt;/code&gt;이 하는 일 — 모두 여기서 나온다. 다음 편에서는 느린 쿼리를 진단하는 EXPLAIN을 다룬다.&lt;/p&gt;</description></item></channel></rss>