<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Database on kastori</title><link>http://blog.kastori.dev/categories/database/</link><description>Recent content in Database on kastori</description><generator>Hugo -- gohugo.io</generator><language>ko-kr</language><lastBuildDate>Wed, 03 Jun 2026 00:00:00 +0900</lastBuildDate><atom:link href="http://blog.kastori.dev/categories/database/index.xml" rel="self" type="application/rss+xml"/><item><title>[DB 완전 정복 #9] 어떤 DB를 왜 선택하는가 — MySQL, PostgreSQL, Redis, MongoDB 선택 기준</title><link>http://blog.kastori.dev/tech/2026-06-03-db-09-db-selection/</link><pubDate>Wed, 03 Jun 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-06-03-db-09-db-selection/</guid><description>&lt;h2 id="왜-이-db를-쓰나요"&gt;&lt;a href="#%ec%99%9c-%ec%9d%b4-db%eb%a5%bc-%ec%93%b0%eb%82%98%ec%9a%94" class="header-anchor"&gt;&lt;/a&gt;&amp;ldquo;왜 이 DB를 쓰나요?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;기술 면접에서 DB 관련 질문은 보통 여기서 끝난다. &amp;ldquo;MySQL 쓰셨다고 하셨는데, 왜 MySQL을 선택하셨나요?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&amp;ldquo;익숙해서요&amp;quot;는 답이 아니다. &amp;ldquo;팀에서 써왔어서요&amp;quot;도 부족하다. 트레이드오프를 설명할 수 있어야 한다. 이 시리즈 마지막 편에서는 각 DB를 선택하는 근거를 정리한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="sql-vs-nosql--먼저-이-구분부터"&gt;&lt;a href="#sql-vs-nosql--%eb%a8%bc%ec%a0%80-%ec%9d%b4-%ea%b5%ac%eb%b6%84%eb%b6%80%ed%84%b0" class="header-anchor"&gt;&lt;/a&gt;SQL vs NoSQL — 먼저 이 구분부터
&lt;/h2&gt;&lt;p&gt;NoSQL이 SQL보다 무조건 빠른 게 아니다. &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;SQL을 선택할 때:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 데이터 간 관계가 명확하고 JOIN이 자주 필요한 경우
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 트랜잭션(ACID) 보장이 필수인 경우 (결제, 계좌)
&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;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;NoSQL을 선택할 때:
&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; - 수평 확장(Sharding)이 필수인 대규모 트래픽
&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; - 최종 일관성(Eventual Consistency)으로 충분한 경우
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="mysql--웹-서비스의-검증된-선택"&gt;&lt;a href="#mysql--%ec%9b%b9-%ec%84%9c%eb%b9%84%ec%8a%a4%ec%9d%98-%ea%b2%80%ec%a6%9d%eb%90%9c-%ec%84%a0%ed%83%9d" class="header-anchor"&gt;&lt;/a&gt;MySQL — 웹 서비스의 검증된 선택
&lt;/h2&gt;&lt;p&gt;MySQL을 선택하는 진짜 이유:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;기술적 근거&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;InnoDB의 MVCC와 행 레벨 락으로 높은 동시성&lt;/li&gt;
&lt;li&gt;Clustered Index로 기본키 조회 최고 성능&lt;/li&gt;
&lt;li&gt;갭 락으로 Phantom Read 방지 (REPEATABLE READ 기본)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;실무적 근거&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Boot + JPA 조합의 레퍼런스 압도적&lt;/li&gt;
&lt;li&gt;AWS RDS, Aurora MySQL 등 클라우드 지원 완벽&lt;/li&gt;
&lt;li&gt;DBA, 운영 인력 시장에서 경험자가 가장 많음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;언제 MySQL이 부족한가: JSON 데이터를 쿼리하고 인덱스도 필요할 때, 지리 데이터를 다룰 때, AI 벡터 검색이 필요할 때.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="postgresql--기능과-확장성이-필요할-때"&gt;&lt;a href="#postgresql--%ea%b8%b0%eb%8a%a5%ea%b3%bc-%ed%99%95%ec%9e%a5%ec%84%b1%ec%9d%b4-%ed%95%84%ec%9a%94%ed%95%a0-%eb%95%8c" class="header-anchor"&gt;&lt;/a&gt;PostgreSQL — 기능과 확장성이 필요할 때
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;JSONB가 필요하다면 PostgreSQL&lt;/strong&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;-- 상품의 동적 속성을 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;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="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;#34;size&amp;#34;: &amp;#34;L&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MySQL에서 같은 쿼리를 하려면 Generated Column + 인덱스 트릭이 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI/ML 연동이 필요하다면 PostgreSQL&lt;/strong&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;-- pgvector: 텍스트 임베딩을 저장하고 유사도 검색
&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[0.1, 0.2, ...]&amp;#39;&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="n"&gt;distance&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;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;documents&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;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;LLM 기반 서비스를 만든다면 pgvector를 지원하는 PostgreSQL이 사실상 표준이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;지리 데이터라면 PostGIS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;위경도 거리 계산, 반경 내 검색, 지도 위에서의 공간 쿼리가 필요하면 PostGIS 확장이 있는 PostgreSQL이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="redis--인메모리의-다양한-역할"&gt;&lt;a href="#redis--%ec%9d%b8%eb%a9%94%eb%aa%a8%eb%a6%ac%ec%9d%98-%eb%8b%a4%ec%96%91%ed%95%9c-%ec%97%ad%ed%95%a0" class="header-anchor"&gt;&lt;/a&gt;Redis — 인메모리의 다양한 역할
&lt;/h2&gt;&lt;p&gt;Redis를 캐시로만 쓰면 절반만 쓰는 것이다.&lt;/p&gt;
&lt;h3 id="캐시"&gt;&lt;a href="#%ec%ba%90%ec%8b%9c" class="header-anchor"&gt;&lt;/a&gt;캐시
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Cacheable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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="s"&gt;&amp;#34;product&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;key&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="s"&gt;&amp;#34;#id&amp;#34;&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="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Long&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;productRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&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="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;p&gt;DB 조회가 많고 데이터 변경이 적은 경우 Redis 캐시로 DB 부하를 줄인다. TTL을 설정해 무효화를 자동화한다.&lt;/p&gt;
&lt;h3 id="세션-저장소"&gt;&lt;a href="#%ec%84%b8%ec%85%98-%ec%a0%80%ec%9e%a5%ec%86%8c" class="header-anchor"&gt;&lt;/a&gt;세션 저장소
&lt;/h3&gt;&lt;p&gt;다중 서버 환경에서 세션을 공유할 때. 서버가 재시작되거나 로드밸런서가 다른 서버로 요청을 보내도 세션이 유지된다.&lt;/p&gt;
&lt;h3 id="분산-락"&gt;&lt;a href="#%eb%b6%84%ec%82%b0-%eb%9d%bd" class="header-anchor"&gt;&lt;/a&gt;분산 락
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&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="n"&gt;RLock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lock&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;redissonClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;lock:product:&amp;#34;&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;productId&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;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TimeUnit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&lt;/span&gt;&lt;span class="p"&gt;))&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="p"&gt;...&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;단일 서버에서는 &lt;code&gt;synchronized&lt;/code&gt;로 충분하지만, 서버가 2대 이상이면 Redis 분산 락이 필요하다.&lt;/p&gt;
&lt;h3 id="실시간-랭킹"&gt;&lt;a href="#%ec%8b%a4%ec%8b%9c%ea%b0%84-%eb%9e%ad%ed%82%b9" class="header-anchor"&gt;&lt;/a&gt;실시간 랭킹
&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;ZADD ranking 2300 &amp;#34;user:2&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZREVRANGE ranking 0 9 // 상위 10명 — O(log N)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Sorted Set의 O(log N) 연산으로 수백만 명 랭킹도 실시간 처리 가능하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="mongodb--스키마가-유연해야-할-때"&gt;&lt;a href="#mongodb--%ec%8a%a4%ed%82%a4%eb%a7%88%ea%b0%80-%ec%9c%a0%ec%97%b0%ed%95%b4%ec%95%bc-%ed%95%a0-%eb%95%8c" class="header-anchor"&gt;&lt;/a&gt;MongoDB — 스키마가 유연해야 할 때
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;임베딩이 JOIN을 대체한다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;관계형 DB에서 3개 테이블을 JOIN해야 하는 상품 정보를, MongoDB에서는 단일 문서 조회 1번으로 처리한다. 계층 구조 데이터에서 읽기 성능이 좋다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;초기 개발의 스키마 유연성&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;서비스 초기에 데이터 구조가 자주 바뀐다. 관계형 DB에서 컬럼 추가/삭제는 마이그레이션이 필요하다. MongoDB는 문서 구조가 자유로워 마이그레이션 없이 바꿀 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;단, 복잡한 관계와 트랜잭션이 많은 도메인은 MySQL이 맞다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="실무-조합-패턴"&gt;&lt;a href="#%ec%8b%a4%eb%ac%b4-%ec%a1%b0%ed%95%a9-%ed%8c%a8%ed%84%b4" 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;일반 웹 서비스:
&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; Redis ← 캐시, 세션, 분산 락
&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;JSON 데이터가 많은 서비스:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; PostgreSQL (JSONB) ← 메인 데이터
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Redis ← 캐시, 세션
&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;span class="line"&gt;&lt;span class="cl"&gt; MySQL / PostgreSQL ← 핵심 비즈니스 데이터
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; MongoDB ← 로그, 이벤트, 비정형 데이터
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Redis ← 실시간 집계, 캐시
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="cap-이론--왜-nosql은-최종-일관성인가"&gt;&lt;a href="#cap-%ec%9d%b4%eb%a1%a0--%ec%99%9c-nosql%ec%9d%80-%ec%b5%9c%ec%a2%85-%ec%9d%bc%ea%b4%80%ec%84%b1%ec%9d%b8%ea%b0%80" class="header-anchor"&gt;&lt;/a&gt;CAP 이론 — 왜 NoSQL은 최종 일관성인가
&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-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;C (Consistency) — 모든 노드가 같은 데이터를 봄
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;A (Availability) — 모든 요청이 응답을 받음
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;P (Partition Tolerance) — 네트워크 분리 상황에서도 동작
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;네트워크 분리(P)는 분산 시스템에서 항상 발생 가능한 사건이다. P를 포기할 수 없으니, 실제로는 C와 A 중 하나를 선택한다.&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 → CP (일관성 우선, 가용성 희생 가능)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Redis Cluster → AP (가용성 우선, 최종 일관성)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Cassandra → AP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MongoDB → 기본 CP, 설정에 따라 AP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Redis가 캐시에 어울리는 이유도 여기 있다. 캐시는 약간 오래된 데이터를 써도 괜찮다(AP). 반면 결제는 항상 최신 데이터가 필요하다(CP, MySQL).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="면접-답변-정리"&gt;&lt;a href="#%eb%a9%b4%ec%a0%91-%eb%8b%b5%eb%b3%80-%ec%a0%95%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;면접 답변 정리
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;&amp;ldquo;어떤 DB를 선택하시겠어요?&amp;rdquo;&lt;/strong&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&amp;ldquo;서비스 특성에 따라 다릅니다. 관계형 데이터와 ACID 트랜잭션이 중요하면 MySQL을, JSON 데이터 쿼리나 지리 기능이 필요하면 PostgreSQL을 선택합니다. 캐시와 세션은 Redis를, 스키마가 유동적이고 계층 구조 데이터가 많으면 MongoDB를 고려합니다.&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;MySQL을 쓰는 이유가 뭔가요?&amp;rdquo;&lt;/strong&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&amp;ldquo;Spring + JPA 조합에서 레퍼런스가 가장 많고, InnoDB의 MVCC와 행 레벨 락으로 높은 동시성을 지원하며, AWS RDS와 Aurora MySQL을 통한 운영 편의성도 고려했습니다.&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="시리즈를-마치며"&gt;&lt;a href="#%ec%8b%9c%eb%a6%ac%ec%a6%88%eb%a5%bc-%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;시리즈를 마치며
&lt;/h2&gt;&lt;p&gt;DB는 단순히 데이터를 저장하는 도구가 아니다. 각 DB는 특정 문제를 풀기 위해 설계됐고, 그 설계 철학이 성능과 트레이드오프를 결정한다.&lt;/p&gt;
&lt;p&gt;인덱스부터 시작해서 트랜잭션, 쿼리 최적화, 정규화, 커넥션 풀, MySQL vs PostgreSQL, Redis, MongoDB, 그리고 DB 선택 기준까지. 이 시리즈에서 다룬 내용이 면접과 실무에서 근거 있는 선택을 하는 데 도움이 되길 바란다.&lt;/p&gt;</description></item><item><title>[DB 완전 정복 #8] MongoDB는 왜 임베딩을 권장하나 — 문서 모델과 WiredTiger</title><link>http://blog.kastori.dev/tech/2026-06-02-db-08-mongodb-internals/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-06-02-db-08-mongodb-internals/</guid><description>&lt;h2 id="mongodb는-그냥-json-저장하는-db-아닌가요"&gt;&lt;a href="#mongodb%eb%8a%94-%ea%b7%b8%eb%83%a5-json-%ec%a0%80%ec%9e%a5%ed%95%98%eb%8a%94-db-%ec%95%84%eb%8b%8c%ea%b0%80%ec%9a%94" class="header-anchor"&gt;&lt;/a&gt;&amp;ldquo;MongoDB는 그냥 JSON 저장하는 DB 아닌가요?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;MongoDB를 처음 접하면 이런 인식이 생기기 쉽다. 실제로 저장 형태가 JSON처럼 보이기 때문이다. 하지만 MongoDB가 진짜로 해결하려는 문제는 &amp;ldquo;JSON 저장&amp;quot;이 아니라 &lt;strong&gt;관계형 모델이 맞지 않는 데이터 구조&lt;/strong&gt;다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="문서-모델--계층-구조를-그대로-저장한다"&gt;&lt;a href="#%eb%ac%b8%ec%84%9c-%eb%aa%a8%eb%8d%b8--%ea%b3%84%ec%b8%b5-%ea%b5%ac%ec%a1%b0%eb%a5%bc-%ea%b7%b8%eb%8c%80%eb%a1%9c-%ec%a0%80%ec%9e%a5%ed%95%9c%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;문서 모델 — 계층 구조를 그대로 저장한다
&lt;/h2&gt;&lt;p&gt;관계형 DB는 데이터를 정규화해서 여러 테이블에 나눠 저장한다. 조회 시 JOIN으로 합친다. MongoDB는 반대로, &lt;strong&gt;관련 데이터를 하나의 문서 안에 중첩해서&lt;/strong&gt; 저장한다.&lt;/p&gt;
&lt;p&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;관계형 DB:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; products (id, name, price)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; product_options (id, product_id, size, color, stock)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; product_images (id, product_id, url, order)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 조회 시 3개 테이블 JOIN
&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;MongoDB:
&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; _id: ObjectId(&amp;#34;...&amp;#34;),
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; name: &amp;#34;운동화&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; price: 89000,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; options: [
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; { size: &amp;#34;260&amp;#34;, color: &amp;#34;black&amp;#34;, stock: 10 },
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; { size: &amp;#34;270&amp;#34;, color: &amp;#34;white&amp;#34;, stock: 5 }
&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; images: [
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; { url: &amp;#34;https://...&amp;#34;, order: 1 },
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; { url: &amp;#34;https://...&amp;#34;, order: 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; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 단일 문서 조회 1번
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;JOIN 없이 한 번의 조회로 모든 데이터를 가져온다. 그리고 컬렉션 내 문서마다 구조가 달라도 된다 — 어떤 상품은 &lt;code&gt;options&lt;/code&gt; 필드가 없어도 괜찮다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="임베딩-vs-참조--mongodb-설계의-핵심-결정"&gt;&lt;a href="#%ec%9e%84%eb%b2%a0%eb%94%a9-vs-%ec%b0%b8%ec%a1%b0--mongodb-%ec%84%a4%ea%b3%84%ec%9d%98-%ed%95%b5%ec%8b%ac-%ea%b2%b0%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;임베딩 vs 참조 — MongoDB 설계의 핵심 결정
&lt;/h2&gt;&lt;p&gt;MongoDB 설계에서 가장 중요한 질문은 &amp;ldquo;관련 데이터를 한 문서 안에 넣을까(임베딩), 별도 컬렉션으로 분리할까(참조)?&amp;ldquo;다.&lt;/p&gt;
&lt;h3 id="임베딩--함께-조회되는-데이터"&gt;&lt;a href="#%ec%9e%84%eb%b2%a0%eb%94%a9--%ed%95%a8%ea%bb%98-%ec%a1%b0%ed%9a%8c%eb%90%98%eb%8a%94-%eb%8d%b0%ec%9d%b4%ed%84%b0" class="header-anchor"&gt;&lt;/a&gt;임베딩 — 함께 조회되는 데이터
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&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="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;order1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;orderNo&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ORD-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;홍길동&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;노트북&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1500000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;마우스&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;totalAmount&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1560000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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;ul&gt;
&lt;li&gt;항상 함께 조회하는 데이터&lt;/li&gt;
&lt;li&gt;1:1 또는 1:소수 관계&lt;/li&gt;
&lt;li&gt;임베딩된 데이터가 독립적으로 존재할 필요가 없는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;주문 생성 당시의 상품 정보를 함께 저장하는 것은 의미가 있다. 나중에 상품 가격이 바뀌어도 주문 당시 가격이 보존된다.&lt;/p&gt;
&lt;h3 id="참조--무제한-증가하거나-독립-조회가-필요한-경우"&gt;&lt;a href="#%ec%b0%b8%ec%a1%b0--%eb%ac%b4%ec%a0%9c%ed%95%9c-%ec%a6%9d%ea%b0%80%ed%95%98%ea%b1%b0%eb%82%98-%eb%8f%85%eb%a6%bd-%ec%a1%b0%ed%9a%8c%ea%b0%80-%ed%95%84%ec%9a%94%ed%95%9c-%ea%b2%bd%ec%9a%b0" class="header-anchor"&gt;&lt;/a&gt;참조 — 무제한 증가하거나 독립 조회가 필요한 경우
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&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="c1"&gt;// posts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;post1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;글 제목&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;...&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&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 class="c1"&gt;// comments
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;c1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;post1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;댓글1&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;c2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;post1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;댓글2&amp;#34;&lt;/span&gt; &lt;span class="p"&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;// ... 댓글이 수천 개가 되어도 posts 문서 크기는 그대로
&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;ul&gt;
&lt;li&gt;배열이 무제한 증가할 수 있는 경우 (댓글, 로그)&lt;/li&gt;
&lt;li&gt;양쪽 데이터를 독립적으로 수정하는 경우&lt;/li&gt;
&lt;li&gt;N:M 관계&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;문서 크기 한도는 16MB다. 배열에 데이터를 계속 추가하면 결국 이 한도에 부딪힌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="wiredtiger--mongodb의-스토리지-엔진"&gt;&lt;a href="#wiredtiger--mongodb%ec%9d%98-%ec%8a%a4%ed%86%a0%eb%a6%ac%ec%a7%80-%ec%97%94%ec%a7%84" class="header-anchor"&gt;&lt;/a&gt;WiredTiger — MongoDB의 스토리지 엔진
&lt;/h2&gt;&lt;p&gt;MongoDB 3.2부터 기본 스토리지 엔진이다.&lt;/p&gt;
&lt;h3 id="문서-레벨-락"&gt;&lt;a href="#%eb%ac%b8%ec%84%9c-%eb%a0%88%eb%b2%a8-%eb%9d%bd" class="header-anchor"&gt;&lt;/a&gt;문서 레벨 락
&lt;/h3&gt;&lt;p&gt;구 엔진(MMAPv1)은 컬렉션 전체에 락을 걸었다. 같은 컬렉션 안의 다른 문서도 동시 쓰기가 불가능했다.&lt;/p&gt;
&lt;p&gt;WiredTiger는 &lt;strong&gt;문서 레벨 락&lt;/strong&gt;을 지원한다. 같은 컬렉션의 다른 문서에 동시에 쓸 수 있다. MVCC도 지원해서 읽기와 쓰기가 서로를 방해하지 않는다.&lt;/p&gt;
&lt;h3 id="압축"&gt;&lt;a href="#%ec%95%95%ec%b6%95" class="header-anchor"&gt;&lt;/a&gt;압축
&lt;/h3&gt;&lt;p&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;기본 압축: snappy (빠름, 적당한 압축률)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;높은 압축: zlib 또는 zstd (더 강한 압축, CPU 비용 있음)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 실제로 디스크 사용량 50~80% 감소
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="wal-journal"&gt;&lt;a href="#wal-journal" class="header-anchor"&gt;&lt;/a&gt;WAL (Journal)
&lt;/h3&gt;&lt;p&gt;쓰기 전에 Journal(WAL)에 먼저 기록한다. 서버가 갑자기 꺼져도 Journal로 복구할 수 있다. 기본적으로 100ms마다 또는 128KB마다 flush된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="집계-파이프라인--sql의-group-by를-대체하는-방법"&gt;&lt;a href="#%ec%a7%91%ea%b3%84-%ed%8c%8c%ec%9d%b4%ed%94%84%eb%9d%bc%ec%9d%b8--sql%ec%9d%98-group-by%eb%a5%bc-%eb%8c%80%ec%b2%b4%ed%95%98%eb%8a%94-%eb%b0%a9%eb%b2%95" class="header-anchor"&gt;&lt;/a&gt;집계 파이프라인 — SQL의 GROUP BY를 대체하는 방법
&lt;/h2&gt;&lt;p&gt;MongoDB에서 집계 쿼리는 &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-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// SQL: SELECT status, COUNT(*), SUM(amount)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// FROM orders WHERE created_at &amp;gt;= &amp;#39;2026-01-01&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// GROUP BY status HAVING COUNT(*) &amp;gt; 10
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ORDER BY SUM(amount) DESC
&lt;/span&gt;&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 class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$match&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$gte&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;2026-01-01&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$group&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$sum&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;totalAmount&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$sum&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;$amount&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$match&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$gt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$sort&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;totalAmount&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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;code&gt;$match&lt;/code&gt;를 파이프라인 앞에 배치해서 처리 문서 수를 먼저 줄여야 한다. 뒤에 두면 전체 문서를 집계한 다음 필터링하는 낭비가 생긴다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="트랜잭션"&gt;&lt;a href="#%ed%8a%b8%eb%9e%9c%ec%9e%ad%ec%85%98" class="header-anchor"&gt;&lt;/a&gt;트랜잭션
&lt;/h2&gt;&lt;p&gt;MongoDB는 &lt;strong&gt;단일 문서 내 업데이트는 항상 원자적&lt;/strong&gt;이다. 여러 필드를 한 번에 수정해도 일부만 반영되는 일이 없다.&lt;/p&gt;
&lt;p&gt;멀티 문서 트랜잭션은 4.0부터 지원되지만, 관계형 DB 트랜잭션보다 비용이 높다. MongoDB 설계 철학은 &lt;strong&gt;트랜잭션이 필요 없도록 임베딩으로 설계&lt;/strong&gt;하는 것이다. 트랜잭션은 임베딩으로 해결이 안 되는 경우에만 쓴다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="언제-mongodb를-선택하나"&gt;&lt;a href="#%ec%96%b8%ec%a0%9c-mongodb%eb%a5%bc-%ec%84%a0%ed%83%9d%ed%95%98%eb%82%98" class="header-anchor"&gt;&lt;/a&gt;언제 MongoDB를 선택하나
&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;✅ 이런 경우 MongoDB
&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;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; - 지리 데이터 (2dsphere 인덱스 내장)
&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;❌ 이런 경우 MongoDB 비적합
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 복잡한 JOIN이 많은 경우 ($lookup은 있지만 SQL JOIN보다 느림)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 결제, 재고처럼 강한 ACID 트랜잭션이 필요한 경우
&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;MongoDB의 강점은 &amp;ldquo;스키마 없이 저장한다&amp;quot;가 아니라 &lt;strong&gt;계층 구조 데이터를 JOIN 없이 한 번에 읽는다&lt;/strong&gt;는 것이다. 임베딩 vs 참조를 올바르게 선택하는 것이 MongoDB 성능의 핵심이다.&lt;/p&gt;
&lt;p&gt;마지막 편에서는 이번 시리즈를 정리하며 각 DB를 어떤 기준으로 선택하는지 종합한다.&lt;/p&gt;</description></item><item><title>[DB 완전 정복 #7] Redis가 빠른 진짜 이유 — 단일 스레드, 이벤트 루프, 자료구조</title><link>http://blog.kastori.dev/tech/2026-06-01-db-07-redis-internals/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-06-01-db-07-redis-internals/</guid><description>&lt;h2 id="redis가-빠른-이유가-인메모리라서-아닌가요"&gt;&lt;a href="#redis%ea%b0%80-%eb%b9%a0%eb%a5%b8-%ec%9d%b4%ec%9c%a0%ea%b0%80-%ec%9d%b8%eb%a9%94%eb%aa%a8%eb%a6%ac%eb%9d%bc%ec%84%9c-%ec%95%84%eb%8b%8c%ea%b0%80%ec%9a%94" class="header-anchor"&gt;&lt;/a&gt;&amp;ldquo;Redis가 빠른 이유가 인메모리라서 아닌가요?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;인메모리인 것은 맞지만, 그것만으로는 설명이 부족하다. Memcached도 인메모리인데 Redis만큼 다양하게 쓰이지 않는다. Redis가 특별한 이유는 &lt;strong&gt;단일 스레드 + 이벤트 루프 + 정교한 자료구조&lt;/strong&gt;의 조합이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="단일-스레드인데-왜-느리지-않나"&gt;&lt;a href="#%eb%8b%a8%ec%9d%bc-%ec%8a%a4%eb%a0%88%eb%93%9c%ec%9d%b8%eb%8d%b0-%ec%99%9c-%eb%8a%90%eb%a6%ac%ec%a7%80-%ec%95%8a%eb%82%98" class="header-anchor"&gt;&lt;/a&gt;단일 스레드인데 왜 느리지 않나
&lt;/h2&gt;&lt;p&gt;Redis는 모든 명령을 &lt;strong&gt;단일 스레드&lt;/strong&gt;가 처리한다. 멀티코어 서버에서 코어 하나만 쓴다는 뜻이다. 직관적으로 비효율적으로 보이지만, 이것이 Redis의 핵심 설계 철학이다.&lt;/p&gt;
&lt;p&gt;멀티 스레드의 문제는 &lt;strong&gt;동기화 비용&lt;/strong&gt;이다. 여러 스레드가 같은 데이터에 접근할 때 Race Condition을 막으려면 뮤텍스와 락이 필요하다. 락을 거는 것 자체가 비용이고, Context Switch도 비용이다.&lt;/p&gt;
&lt;p&gt;Redis는 이 문제를 &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;단일 스레드의 장점:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Lock 없음 → Race Condition 없음
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 모든 명령이 원자적(Atomic) 보장
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Context Switch 없음
&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;p&gt;그렇다면 I/O는 어떻게 처리할까? 단일 스레드가 소켓을 하나씩 읽으면 느리지 않나?&lt;/p&gt;
&lt;h3 id="이벤트-루프--io-multiplexing"&gt;&lt;a href="#%ec%9d%b4%eb%b2%a4%ed%8a%b8-%eb%a3%a8%ed%94%84--io-multiplexing" class="header-anchor"&gt;&lt;/a&gt;이벤트 루프 + I/O Multiplexing
&lt;/h3&gt;&lt;p&gt;Redis는 &lt;code&gt;epoll&lt;/code&gt;(Linux) 또는 &lt;code&gt;kqueue&lt;/code&gt;(macOS) 같은 OS 레벨 I/O Multiplexing을 사용한다.&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;[클라이언트 수백 개]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Client A ──┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Client B ──┤──▶ epoll (OS 커널) ──▶ 이벤트 큐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Client C ──┘
&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;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;p&gt;OS 커널이 수백 개의 소켓을 동시에 감시하다가, 읽을 데이터가 생기면 이벤트 큐에 넣는다. 단일 스레드는 큐에서 이벤트를 꺼내 처리한다. &lt;strong&gt;I/O 대기 시간을 OS가 흡수&lt;/strong&gt;하므로 단일 스레드도 충분히 많은 클라이언트를 처리할 수 있다.&lt;/p&gt;
&lt;p&gt;결국 Redis가 빠른 이유는: &lt;strong&gt;인메모리 + Lock 없는 단일 스레드 + OS가 처리하는 I/O 대기&lt;/strong&gt; 세 가지의 조합이다.&lt;/p&gt;
&lt;h3 id="60에서-멀티스레드-io"&gt;&lt;a href="#60%ec%97%90%ec%84%9c-%eb%a9%80%ed%8b%b0%ec%8a%a4%eb%a0%88%eb%93%9c-io" class="header-anchor"&gt;&lt;/a&gt;6.0+에서 멀티스레드 I/O
&lt;/h3&gt;&lt;p&gt;Redis 6.0부터 네트워크 I/O(패킷 읽기/쓰기)는 멀티스레드로 처리하도록 개선됐다. 하지만 실제 명령 실행은 여전히 단일 스레드다. 원자성과 단순함을 유지하면서 대용량 데이터 전송 성능만 개선한 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="자료구조별-내부-구현"&gt;&lt;a href="#%ec%9e%90%eb%a3%8c%ea%b5%ac%ec%a1%b0%eb%b3%84-%eb%82%b4%eb%b6%80-%ea%b5%ac%ed%98%84" class="header-anchor"&gt;&lt;/a&gt;자료구조별 내부 구현
&lt;/h2&gt;&lt;p&gt;Redis의 또 다른 강점은 상황에 따라 &lt;strong&gt;내부 인코딩을 자동으로 최적화&lt;/strong&gt;한다는 것이다.&lt;/p&gt;
&lt;h3 id="sorted-set--실시간-랭킹의-핵심"&gt;&lt;a href="#sorted-set--%ec%8b%a4%ec%8b%9c%ea%b0%84-%eb%9e%ad%ed%82%b9%ec%9d%98-%ed%95%b5%ec%8b%ac" class="header-anchor"&gt;&lt;/a&gt;Sorted Set — 실시간 랭킹의 핵심
&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;데이터가 128개 이하: listpack (연속 메모리, 캐시 효율)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;데이터가 많아지면: skiplist + hashtable 조합
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - skiplist: 정렬된 순서로 빠른 범위 조회 (O(log N))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - hashtable: 멤버 → 점수 O(1) 조회
&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-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZADD ranking 1500 &amp;#34;user:1&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZADD ranking 2300 &amp;#34;user:2&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZADD ranking 1800 &amp;#34;user:3&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;ZREVRANGE ranking 0 9 WITHSCORES → 상위 10명 O(log N + 10)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZRANK ranking &amp;#34;user:1&amp;#34; → 내 순위 O(log N)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZSCORE ranking &amp;#34;user:2&amp;#34; → 점수 조회 O(1)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;점수 변경이 잦은 실시간 랭킹에 최적이다.&lt;/p&gt;
&lt;h3 id="hash--세션-저장의-패턴"&gt;&lt;a href="#hash--%ec%84%b8%ec%85%98-%ec%a0%80%ec%9e%a5%ec%9d%98-%ed%8c%a8%ed%84%b4" class="header-anchor"&gt;&lt;/a&gt;Hash — 세션 저장의 패턴
&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;필드가 128개 이하: listpack (연속 메모리)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;필드가 많아지면: hashtable
&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;HSET session:abc userId 1 username &amp;#34;kastori&amp;#34; // O(1)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HGET session:abc userId // O(1)
&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;p&gt;사용자 세션 데이터를 Hash로 저장하면 필드별로 읽고 쓸 수 있어 효율적이다.&lt;/p&gt;
&lt;h3 id="자료구조-시간복잡도-요약"&gt;&lt;a href="#%ec%9e%90%eb%a3%8c%ea%b5%ac%ec%a1%b0-%ec%8b%9c%ea%b0%84%eb%b3%b5%ec%9e%a1%eb%8f%84-%ec%9a%94%ec%95%bd" 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;주요 연산&lt;/th&gt;
 &lt;th&gt;시간복잡도&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;String&lt;/td&gt;
 &lt;td&gt;SET/GET&lt;/td&gt;
 &lt;td&gt;O(1)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;List&lt;/td&gt;
 &lt;td&gt;LPUSH/RPUSH, LPOP/RPOP&lt;/td&gt;
 &lt;td&gt;O(1)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Hash&lt;/td&gt;
 &lt;td&gt;HSET/HGET&lt;/td&gt;
 &lt;td&gt;O(1)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Set&lt;/td&gt;
 &lt;td&gt;SADD/SISMEMBER&lt;/td&gt;
 &lt;td&gt;O(1)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Sorted Set&lt;/td&gt;
 &lt;td&gt;ZADD/ZSCORE&lt;/td&gt;
 &lt;td&gt;O(log N)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Sorted Set&lt;/td&gt;
 &lt;td&gt;ZRANGE&lt;/td&gt;
 &lt;td&gt;O(log N + M)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="영속성--인메모리인데-재시작하면-데이터는"&gt;&lt;a href="#%ec%98%81%ec%86%8d%ec%84%b1--%ec%9d%b8%eb%a9%94%eb%aa%a8%eb%a6%ac%ec%9d%b8%eb%8d%b0-%ec%9e%ac%ec%8b%9c%ec%9e%91%ed%95%98%eb%a9%b4-%eb%8d%b0%ec%9d%b4%ed%84%b0%eb%8a%94" class="header-anchor"&gt;&lt;/a&gt;영속성 — 인메모리인데 재시작하면 데이터는?
&lt;/h2&gt;&lt;p&gt;Redis는 기본적으로 인메모리라 서버가 꺼지면 데이터가 사라진다. 영속성이 필요하다면 두 가지 방법이 있다.&lt;/p&gt;
&lt;h3 id="rdb--스냅샷"&gt;&lt;a href="#rdb--%ec%8a%a4%eb%83%85%ec%83%b7" class="header-anchor"&gt;&lt;/a&gt;RDB — 스냅샷
&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;주기적으로 현재 메모리를 .rdb 파일로 저장
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → fork() 로 자식 프로세스가 저장, 부모는 계속 요청 처리
&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;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;h3 id="aof--명령-로그"&gt;&lt;a href="#aof--%eb%aa%85%eb%a0%b9-%eb%a1%9c%ea%b7%b8" class="header-anchor"&gt;&lt;/a&gt;AOF — 명령 로그
&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;재시작 시 파일을 재실행해서 복구
&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;appendfsync everysec → 1초마다 flush (권장, 최대 1초 유실)
&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;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;h3 id="권장-혼합-모드"&gt;&lt;a href="#%ea%b6%8c%ec%9e%a5-%ed%98%bc%ed%95%a9-%eb%aa%a8%eb%93%9c" class="header-anchor"&gt;&lt;/a&gt;권장: 혼합 모드
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# redis.conf&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="l"&gt;aof-use-rdb-preamble yes&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="nt"&gt;→ AOF 파일 앞&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;RDB 스냅샷 (빠른 로딩)&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="nt"&gt;→ AOF 파일 뒤&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&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;hr&gt;
&lt;h2 id="실무에서-꼭-지켜야-할-것"&gt;&lt;a href="#%ec%8b%a4%eb%ac%b4%ec%97%90%ec%84%9c-%ea%bc%ad-%ec%a7%80%ec%bc%9c%ec%95%bc-%ed%95%a0-%ea%b2%83" class="header-anchor"&gt;&lt;/a&gt;실무에서 꼭 지켜야 할 것
&lt;/h2&gt;&lt;h3 id="keys-명령-절대-금지"&gt;&lt;a href="#keys-%eb%aa%85%eb%a0%b9-%ec%a0%88%eb%8c%80-%ea%b8%88%ec%a7%80" class="header-anchor"&gt;&lt;/a&gt;KEYS 명령 절대 금지
&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;KEYS * → 전체 키 순회, O(N), 단일 스레드 완전 블로킹
&lt;/span&gt;&lt;/span&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;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;대신 SCAN:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; SCAN 0 MATCH user:* COUNT 100 → 커서 기반, 블로킹 없음
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="ttl-설정-필수"&gt;&lt;a href="#ttl-%ec%84%a4%ec%a0%95-%ed%95%84%ec%88%98" class="header-anchor"&gt;&lt;/a&gt;TTL 설정 필수
&lt;/h3&gt;&lt;p&gt;캐시 목적의 키는 반드시 만료 시간을 설정해야 한다. 안 하면 메모리가 계속 쌓이다가 &lt;code&gt;maxmemory&lt;/code&gt;에 도달하고, eviction 정책에 따라 데이터가 예고 없이 삭제된다.&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;SET key value EX 3600 // 1시간 후 자동 만료
&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;Redis를 &amp;ldquo;그냥 캐시&amp;quot;로만 알면 반쪽이다. 단일 스레드 이벤트 루프가 어떻게 동시 처리를 하는지, 자료구조마다 다른 내부 인코딩이 성능에 어떤 영향을 주는지 이해하면 Redis를 더 잘 쓸 수 있다. 다음 편에서는 문서 지향 DB인 MongoDB의 내부 구조를 다룬다.&lt;/p&gt;</description></item><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 완전 정복 #5] HikariCP와 커넥션 풀 — 설정 하나가 서비스를 멈춘다</title><link>http://blog.kastori.dev/tech/2026-05-30-db-05-connection-pool/</link><pubDate>Sat, 30 May 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-05-30-db-05-connection-pool/</guid><description>&lt;h2 id="요청-1000개에-커넥션-1000개를-만들면"&gt;&lt;a href="#%ec%9a%94%ec%b2%ad-1000%ea%b0%9c%ec%97%90-%ec%bb%a4%eb%84%a5%ec%85%98-1000%ea%b0%9c%eb%a5%bc-%eb%a7%8c%eb%93%a4%eb%a9%b4" class="header-anchor"&gt;&lt;/a&gt;요청 1000개에 커넥션 1000개를 만들면?
&lt;/h2&gt;&lt;p&gt;트래픽이 급증하는 상황을 상상해보자. 매 요청마다 DB 커넥션을 새로 만들면 어떻게 될까.&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 커넥션 생성 과정:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 1. TCP 소켓 연결 (3-way handshake)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 2. DB 서버 인증 (사용자/비밀번호 검증)
&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;→ 수십 ~ 수백 ms 소요
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 초당 1000 요청이면 커넥션 생성만으로 서버 과부하
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;커넥션 풀은 이 문제를 미리 만들어 두고 재사용하는 방식으로 해결한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="hikaricp--spring-boot의-기본-커넥션-풀"&gt;&lt;a href="#hikaricp--spring-boot%ec%9d%98-%ea%b8%b0%eb%b3%b8-%ec%bb%a4%eb%84%a5%ec%85%98-%ed%92%80" class="header-anchor"&gt;&lt;/a&gt;HikariCP — Spring Boot의 기본 커넥션 풀
&lt;/h2&gt;&lt;p&gt;Spring Boot 2.0부터 HikariCP가 기본이다. &amp;ldquo;세상에서 가장 빠른 커넥션 풀&amp;quot;이라는 별명이 있다. 별도 설정 없이도 동작하지만, 기본값이 모든 상황에 맞지 않다는 게 함정이다.&lt;/p&gt;
&lt;h3 id="동작-원리"&gt;&lt;a href="#%eb%8f%99%ec%9e%91-%ec%9b%90%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;동작 원리
&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; → 미리 커넥션 N개 생성해서 풀에 보관
&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;HTTP 요청 처리
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 풀에서 유휴 커넥션 꺼내기 (borrow)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → DB 작업 수행
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 커넥션 반납 (close가 아님, return)
&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;span class="line"&gt;&lt;span class="cl"&gt; → connectionTimeout 동안 대기
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 초과 → SQLTimeoutException
&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;connection.close()&lt;/code&gt;를 호출해도 실제로 닫히지 않고 풀로 돌아간다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="풀-사이즈-공식--너무-크면-오히려-느리다"&gt;&lt;a href="#%ed%92%80-%ec%82%ac%ec%9d%b4%ec%a6%88-%ea%b3%b5%ec%8b%9d--%eb%84%88%eb%ac%b4-%ed%81%ac%eb%a9%b4-%ec%98%a4%ed%9e%88%eb%a0%a4-%eb%8a%90%eb%a6%ac%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;풀 사이즈 공식 — 너무 크면 오히려 느리다
&lt;/h2&gt;&lt;p&gt;직관적으로는 커넥션이 많을수록 좋을 것 같다. 하지만 커넥션이 너무 많으면 DB 서버 자체가 부하를 받는다.&lt;/p&gt;
&lt;p&gt;HikariCP 공식 문서가 제안하는 공식:&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;maximumPoolSize = (CPU 코어 수 × 2) + 유효 디스크 수
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;예: CPU 4코어 + SSD 1개 = &lt;strong&gt;9개&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;놀랍도록 작은 숫자처럼 보인다. 하지만 실제로 DB 연산은 CPU와 I/O를 번갈아 쓰므로, 코어당 2개가 적절하다. 그 이상은 Context Switch 오버헤드만 늘어난다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# application.yml&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="nt"&gt;spring&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="nt"&gt;datasource&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="nt"&gt;hikari&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="nt"&gt;maximum-pool-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&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="nt"&gt;minimum-idle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&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="nt"&gt;connection-timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;30000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 커넥션 대기 최대 시간 (ms)&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="nt"&gt;idle-timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;600000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 유휴 커넥션 유지 시간 (ms)&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="nt"&gt;max-lifetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1800000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 커넥션 최대 수명 (ms)&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;h3 id="max-lifetime-주의사항"&gt;&lt;a href="#max-lifetime-%ec%a3%bc%ec%9d%98%ec%82%ac%ed%95%ad" class="header-anchor"&gt;&lt;/a&gt;max-lifetime 주의사항
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;max-lifetime&lt;/code&gt;은 DB 서버의 &lt;code&gt;wait_timeout&lt;/code&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;DB 서버 wait_timeout: 8시간 (기본값)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;max-lifetime: 30분 (권장)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ DB가 연결을 끊기 전에 풀에서 먼저 커넥션을 교체
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;max-lifetime&lt;/code&gt;이 &lt;code&gt;wait_timeout&lt;/code&gt;보다 길면, DB가 이미 닫은 커넥션을 풀이 계속 들고 있다가 사용 시점에 &lt;code&gt;Connection is closed&lt;/code&gt; 에러가 난다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="커넥션-고갈--가장-흔한-장애-원인"&gt;&lt;a href="#%ec%bb%a4%eb%84%a5%ec%85%98-%ea%b3%a0%ea%b0%88--%ea%b0%80%ec%9e%a5-%ed%9d%94%ed%95%9c-%ec%9e%a5%ec%95%a0-%ec%9b%90%ec%9d%b8" class="header-anchor"&gt;&lt;/a&gt;커넥션 고갈 — 가장 흔한 장애 원인
&lt;/h2&gt;&lt;p&gt;풀의 모든 커넥션이 사용 중이면, 새 요청은 &lt;code&gt;connectionTimeout&lt;/code&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;HikariPool-1 - Connection is not available, request timed out after 30000ms
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="커넥션-고갈의-주요-원인"&gt;&lt;a href="#%ec%bb%a4%eb%84%a5%ec%85%98-%ea%b3%a0%ea%b0%88%ec%9d%98-%ec%a3%bc%ec%9a%94-%ec%9b%90%ec%9d%b8" class="header-anchor"&gt;&lt;/a&gt;커넥션 고갈의 주요 원인
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;1. 트랜잭션 안에서 외부 I/O&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Transactional&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="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;processOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&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&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="n"&gt;Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order&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;orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&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 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="n"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 이메일 전송 (3~5초)&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="c1"&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="n"&gt;orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&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;p&gt;이메일 전송이 5초 걸리는 동안 커넥션 1개가 묶여 있다. 동시 요청이 많으면 풀이 금방 고갈된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt;: 트랜잭션 밖으로 느린 I/O를 분리한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;processOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&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&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="n"&gt;Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order&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;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&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 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="n"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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 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="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&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 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="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;p&gt;&lt;strong&gt;2. 풀 사이즈가 너무 작음&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;요청 동시성 대비 풀이 작으면 대기 큐가 쌓인다. 모니터링으로 실제 사용량을 확인하고 조정해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 긴 쿼리&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;인덱스 없는 쿼리가 10초씩 걸리면 그동안 커넥션이 묶인다. 쿼리 최적화로 해결한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="모니터링--actuator로-풀-상태-보기"&gt;&lt;a href="#%eb%aa%a8%eb%8b%88%ed%84%b0%eb%a7%81--actuator%eb%a1%9c-%ed%92%80-%ec%83%81%ed%83%9c-%eb%b3%b4%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;모니터링 — Actuator로 풀 상태 보기
&lt;/h2&gt;&lt;p&gt;Spring Boot Actuator를 쓰면 커넥션 풀 상태를 실시간으로 볼 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# application.yml&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="nt"&gt;management&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="nt"&gt;endpoints&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="nt"&gt;web&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="nt"&gt;exposure&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="nt"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;health, metrics&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-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GET /actuator/metrics/hikaricp.connections.active
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GET /actuator/metrics/hikaricp.connections.pending
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GET /actuator/metrics/hikaricp.connections.timeout
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;pending&lt;/code&gt;(대기 중)이 0이 아니거나 &lt;code&gt;timeout&lt;/code&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;커넥션 풀은 조용히 잘 동작하다가 특정 임계점을 넘으면 갑자기 서비스 전체가 멈춘다. 평소에 &lt;code&gt;maximum-pool-size&lt;/code&gt;, &lt;code&gt;max-lifetime&lt;/code&gt; 설정을 확인하고, 트랜잭션 안에 느린 I/O가 없는지 리뷰하는 것만으로도 많은 장애를 예방할 수 있다.&lt;/p&gt;
&lt;p&gt;다음 편부터는 심화 시리즈로, MySQL과 PostgreSQL의 내부 구조 차이를 다룬다.&lt;/p&gt;</description></item><item><title>[DB 완전 정복 #4] 정규화는 언제 깨야 하나 — 1NF부터 3NF, 실무 비정규화까지</title><link>http://blog.kastori.dev/tech/2026-05-29-db-04-normalization/</link><pubDate>Fri, 29 May 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-05-29-db-04-normalization/</guid><description>&lt;h2 id="테이블-하나에-다-넣으면-안-되나요"&gt;&lt;a href="#%ed%85%8c%ec%9d%b4%eb%b8%94-%ed%95%98%eb%82%98%ec%97%90-%eb%8b%a4-%eb%84%a3%ec%9c%bc%eb%a9%b4-%ec%95%88-%eb%90%98%eb%82%98%ec%9a%94" class="header-anchor"&gt;&lt;/a&gt;테이블 하나에 다 넣으면 안 되나요?
&lt;/h2&gt;&lt;p&gt;DB를 처음 설계할 때 이런 생각을 한 적이 있을 것이다. 그냥 필요한 정보를 테이블 하나에 다 넣으면 조인도 없고 편하지 않을까?&lt;/p&gt;
&lt;p&gt;실제로 해보면 금방 문제가 생긴다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="이상-현상--설계가-나쁠-때-생기는-일"&gt;&lt;a href="#%ec%9d%b4%ec%83%81-%ed%98%84%ec%83%81--%ec%84%a4%ea%b3%84%ea%b0%80-%eb%82%98%ec%81%a0-%eb%95%8c-%ec%83%9d%ea%b8%b0%eb%8a%94-%ec%9d%bc" 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-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;| order_id | member_id | member_name | member_email | product_id | product_name | price |
&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;| 1 | 100 | 홍길동 | hong@test.com | 200 | 노트북 | 1500 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 2 | 100 | 홍길동 | hong@test.com | 201 | 마우스 | 30 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 3 | 101 | 김철수 | kim@test.com | 200 | 노트북 | 1500 |
&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;order_id&lt;/code&gt;가 없으니 행을 추가할 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;삭제 이상&lt;/strong&gt;: 주문 1번을 삭제한다. 그런데 홍길동의 이메일 정보가 주문 2번에만 남는 게 아니라, 만약 주문이 1번 하나뿐이었다면 회원 정보 자체가 사라진다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;수정 이상&lt;/strong&gt;: 홍길동이 이메일을 변경했다. &lt;code&gt;order_id = 1&lt;/code&gt;과 &lt;code&gt;order_id = 2&lt;/code&gt; 두 군데를 모두 수정해야 한다. 하나만 고치면 같은 사람의 이메일이 두 개가 된다.&lt;/p&gt;
&lt;p&gt;이것이 **이상 현상(Anomaly)**이다. 정규화는 이 문제를 해결하기 위한 설계 원칙이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="1nf--원자값-원칙"&gt;&lt;a href="#1nf--%ec%9b%90%ec%9e%90%ea%b0%92-%ec%9b%90%ec%b9%99" class="header-anchor"&gt;&lt;/a&gt;1NF — 원자값 원칙
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;모든 컬럼의 값이 더 이상 분리할 수 없는 원자적인 값이어야 한다.&lt;/p&gt;

 &lt;/blockquote&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;❌ 1NF 위반
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| order_id | products |
&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;| 1 | 노트북, 마우스 | ← 여러 값이 한 컬럼에
&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;✅ 1NF 준수
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| order_id | product_id | product_name |
&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;| 1 | 200 | 노트북 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 1 | 201 | 마우스 |
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;컬럼에 여러 값을 콤마로 넣거나, 배열처럼 넣는 것이 대표적인 위반이다. 이렇게 하면 &lt;code&gt;WHERE products LIKE '%마우스%'&lt;/code&gt;처럼 검색해야 하고, 인덱스도 제대로 못 탄다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="2nf--부분-함수-종속-제거"&gt;&lt;a href="#2nf--%eb%b6%80%eb%b6%84-%ed%95%a8%ec%88%98-%ec%a2%85%ec%86%8d-%ec%a0%9c%ea%b1%b0" class="header-anchor"&gt;&lt;/a&gt;2NF — 부분 함수 종속 제거
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;기본키가 복합키일 때, 모든 일반 컬럼이 복합키 전체에 종속되어야 한다. (기본키의 일부에만 종속되면 안 된다)&lt;/p&gt;

 &lt;/blockquote&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;order_items 테이블 (1NF)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;기본키: (order_id, product_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;| order_id | product_id | product_name | qty |
&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;| 1 | 200 | 노트북 | 1 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 1 | 201 | 마우스 | 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;문제: product_name은 product_id에만 종속 (order_id와는 무관)
&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;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;✅ 2NF 준수 (분리)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;order_items: (order_id, product_id, qty)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;products: (product_id, product_name, price)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="3nf--이행적-함수-종속-제거"&gt;&lt;a href="#3nf--%ec%9d%b4%ed%96%89%ec%a0%81-%ed%95%a8%ec%88%98-%ec%a2%85%ec%86%8d-%ec%a0%9c%ea%b1%b0" class="header-anchor"&gt;&lt;/a&gt;3NF — 이행적 함수 종속 제거
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;기본키가 아닌 일반 컬럼 간의 종속이 없어야 한다.&lt;/p&gt;

 &lt;/blockquote&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;members 테이블 (2NF)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;기본키: member_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;| member_id | zip_code | city | district |
&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;| 1 | 06130 | 서울 | 강남구 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 2 | 06130 | 서울 | 강남구 |
&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;문제: city와 district는 zip_code에 종속 (member_id → zip_code → city/district)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 06130 우편번호의 시/구가 바뀌면 모든 행을 수정해야 함
&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-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✅ 3NF 준수 (분리)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;members: (member_id, zip_code)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zip_codes: (zip_code, city, district)
&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="#%ec%a0%95%ea%b7%9c%ed%99%94-vs-%eb%b9%84%ec%a0%95%ea%b7%9c%ed%99%94--%ed%8a%b8%eb%a0%88%ec%9d%b4%eb%93%9c%ec%98%a4%ed%94%84" class="header-anchor"&gt;&lt;/a&gt;정규화 vs 비정규화 — 트레이드오프
&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-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; ✅ 데이터 중복 없음 → 수정 이상 없음
&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;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;span class="line"&gt;&lt;span class="cl"&gt; ❌ 조회 시 JOIN 필요 → 쿼리 복잡
&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;p&gt;실무에서는 &lt;strong&gt;의도적으로 정규화를 깨는&lt;/strong&gt; 경우도 있다.&lt;/p&gt;
&lt;h3 id="비정규화가-정당한-경우"&gt;&lt;a href="#%eb%b9%84%ec%a0%95%ea%b7%9c%ed%99%94%ea%b0%80-%ec%a0%95%eb%8b%b9%ed%95%9c-%ea%b2%bd%ec%9a%b0" class="header-anchor"&gt;&lt;/a&gt;비정규화가 정당한 경우
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;1. 집계 결과를 미리 저장&lt;/strong&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;-- 매번 COUNT 집계 쿼리 실행
&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="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;posts&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;member_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="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;-- 비정규화: members 테이블에 post_count 컬럼 추가
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 게시글 INSERT/DELETE 시 post_count를 함께 업데이트
&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;post_count&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;members&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="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;2. 자주 함께 조회되는 컬럼 중복 저장&lt;/strong&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;-- orders 테이블에 member_name을 중복 저장
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 주문 목록 조회 시 members 테이블 JOIN 불필요
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order_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="n"&gt;member_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="n"&gt;member_name&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;total_amount&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;주문 당시의 회원명을 보존한다는 의미도 있다(회원이 탈퇴해도 주문 이력 유지).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 읽기 전용 분석 테이블&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OLAP(분석 쿼리) 용도의 테이블은 처음부터 비정규화로 설계한다. 수십 개 테이블을 조인하는 분석 쿼리보다 평탄화된 넓은 테이블이 훨씬 빠르다.&lt;/p&gt;
&lt;h3 id="jpa와-정규화의-관계"&gt;&lt;a href="#jpa%ec%99%80-%ec%a0%95%ea%b7%9c%ed%99%94%ec%9d%98-%ea%b4%80%ea%b3%84" class="header-anchor"&gt;&lt;/a&gt;JPA와 정규화의 관계
&lt;/h3&gt;&lt;p&gt;JPA 연관관계(&lt;code&gt;@OneToMany&lt;/code&gt;, &lt;code&gt;@ManyToOne&lt;/code&gt;)는 정규화된 테이블을 객체로 매핑하는 방식이다. 정규화가 잘 되어 있으면 JPA 설계도 자연스럽게 따라온다.&lt;/p&gt;
&lt;p&gt;비정규화를 하면 JPA 매핑이 어색해질 수 있다. 그래서 비정규화는 신중하게, 이유가 명확할 때만 해야 한다.&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;정규화는 목표가 아니라 도구다. &lt;strong&gt;이상 현상을 없애기 위해 필요한 만큼&lt;/strong&gt; 하고, 성능이 중요하고 이유가 명확한 경우에만 의도적으로 비정규화를 선택한다. &amp;ldquo;정규화를 깨는 이유가 뭔가요?&amp;ldquo;라는 질문에 답할 수 있다면 충분하다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 DB 연결 자체의 비용을 다루는 커넥션 풀(HikariCP)을 다룬다.&lt;/p&gt;</description></item><item><title>[DB 완전 정복 #3] 느린 쿼리를 잡아라 — EXPLAIN 읽는 법과 페이징 최적화</title><link>http://blog.kastori.dev/tech/2026-05-28-db-03-query-optimization/</link><pubDate>Thu, 28 May 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-05-28-db-03-query-optimization/</guid><description>&lt;h2 id="쿼리가-느리다-어디서부터-봐야-할까"&gt;&lt;a href="#%ec%bf%bc%eb%a6%ac%ea%b0%80-%eb%8a%90%eb%a6%ac%eb%8b%a4-%ec%96%b4%eb%94%94%ec%84%9c%eb%b6%80%ed%84%b0-%eb%b4%90%ec%95%bc-%ed%95%a0%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;쿼리가 느리다. 어디서부터 봐야 할까
&lt;/h2&gt;&lt;p&gt;개발 환경에서는 데이터가 몇 백 건이라 빠르다. 그런데 운영 환경에 올라가면 특정 페이지가 버벅인다. 로그를 보면 DB 쿼리가 범인이다.&lt;/p&gt;
&lt;p&gt;이때 시작점은 하나다. &lt;strong&gt;EXPLAIN&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="explain--옵티마이저의-속마음을-보는-방법"&gt;&lt;a href="#explain--%ec%98%b5%ed%8b%b0%eb%a7%88%ec%9d%b4%ec%a0%80%ec%9d%98-%ec%86%8d%eb%a7%88%ec%9d%8c%ec%9d%84-%eb%b3%b4%eb%8a%94-%eb%b0%a9%eb%b2%95" class="header-anchor"&gt;&lt;/a&gt;EXPLAIN — 옵티마이저의 속마음을 보는 방법
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;EXPLAIN&lt;/code&gt;은 MySQL 옵티마이저가 쿼리를 어떻게 실행할지 보여주는 명령이다.&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;EXPLAIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;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="n"&gt;o&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;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;member_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="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&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;PENDING&amp;#39;&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;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&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;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&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;p&gt;출력에서 봐야 할 컬럼 네 가지:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;컬럼&lt;/th&gt;
 &lt;th&gt;의미&lt;/th&gt;
 &lt;th&gt;좋은 값&lt;/th&gt;
 &lt;th&gt;나쁜 값&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;type&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;테이블 접근 방식&lt;/td&gt;
 &lt;td&gt;const, ref, range&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;ALL&lt;/strong&gt; (풀스캔)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;실제 사용한 인덱스&lt;/td&gt;
 &lt;td&gt;인덱스명&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;NULL&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;rows&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;예상 스캔 행 수&lt;/td&gt;
 &lt;td&gt;작을수록&lt;/td&gt;
 &lt;td&gt;클수록&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Extra&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;부가 정보&lt;/td&gt;
 &lt;td&gt;Using index&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Using filesort&lt;/strong&gt;, &lt;strong&gt;Using temporary&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="type-값--빠른-순서"&gt;&lt;a href="#type-%ea%b0%92--%eb%b9%a0%eb%a5%b8-%ec%88%9c%ec%84%9c" class="header-anchor"&gt;&lt;/a&gt;type 값 — 빠른 순서
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;PK로&lt;/span&gt; &lt;span class="err"&gt;단&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;개&lt;/span&gt; &lt;span class="err"&gt;행&lt;/span&gt; &lt;span class="err"&gt;조회&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;가장&lt;/span&gt; &lt;span class="err"&gt;빠름&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;eq_ref&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;조인에서&lt;/span&gt; &lt;span class="n"&gt;PK로&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;개&lt;/span&gt; &lt;span class="err"&gt;행&lt;/span&gt; &lt;span class="err"&gt;조회&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;일반&lt;/span&gt; &lt;span class="err"&gt;인덱스로&lt;/span&gt; &lt;span class="err"&gt;여러&lt;/span&gt; &lt;span class="err"&gt;행&lt;/span&gt; &lt;span class="err"&gt;조회&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;range&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;인덱스&lt;/span&gt; &lt;span class="err"&gt;범위&lt;/span&gt; &lt;span class="err"&gt;스캔&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BETWEEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;인덱스&lt;/span&gt; &lt;span class="err"&gt;풀&lt;/span&gt; &lt;span class="err"&gt;스캔&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;테이블보다는&lt;/span&gt; &lt;span class="err"&gt;낫지만&lt;/span&gt; &lt;span class="err"&gt;느림&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ALL&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;테이블&lt;/span&gt; &lt;span class="err"&gt;풀&lt;/span&gt; &lt;span class="err"&gt;스캔&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;가장&lt;/span&gt; &lt;span class="err"&gt;느림&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;즉시&lt;/span&gt; &lt;span class="err"&gt;개선&lt;/span&gt; &lt;span class="err"&gt;필요&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;type: ALL&lt;/code&gt;이 보이면 인덱스가 없거나 무력화된 것이다.&lt;/p&gt;
&lt;h3 id="extra-해석"&gt;&lt;a href="#extra-%ed%95%b4%ec%84%9d" class="header-anchor"&gt;&lt;/a&gt;Extra 해석
&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;Using index → 커버링 인덱스. 테이블 접근 없음 (좋음)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using where → 인덱스 후 WHERE 필터링 (보통)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using filesort → ORDER BY를 정렬 버퍼에서 처리 (느림, 인덱스로 해결 가능)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using temporary → 임시 테이블 사용 (느림, GROUP BY/DISTINCT 최적화 필요)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using index condition → ICP (인덱스 레벨에서 조건 필터링, 개선)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Using filesort&lt;/code&gt;와 &lt;code&gt;Using temporary&lt;/code&gt;가 함께 나오면 위험 신호다. ORDER BY나 GROUP BY 컬럼에 인덱스를 추가하는 것을 검토해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="슬로우-쿼리-로그--운영에서-느린-쿼리-찾기"&gt;&lt;a href="#%ec%8a%ac%eb%a1%9c%ec%9a%b0-%ec%bf%bc%eb%a6%ac-%eb%a1%9c%ea%b7%b8--%ec%9a%b4%ec%98%81%ec%97%90%ec%84%9c-%eb%8a%90%eb%a6%b0-%ec%bf%bc%eb%a6%ac-%ec%b0%be%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;슬로우 쿼리 로그 — 운영에서 느린 쿼리 찾기
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;EXPLAIN&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="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;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GLOBAL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;slow_query_log&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;ON&amp;#39;&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;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GLOBAL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;long_query_time&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="c1"&gt;-- 1초 이상 걸리는 쿼리 기록
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GLOBAL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;slow_query_log_file&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;/var/log/mysql/slow.log&amp;#39;&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;p&gt;로그에 쌓인 쿼리를 &lt;code&gt;mysqldumpslow&lt;/code&gt;로 집계하면 가장 자주 느린 쿼리 순으로 정렬할 수 있다.&lt;/p&gt;
&lt;p&gt;Spring Boot에서는 &lt;code&gt;p6spy&lt;/code&gt; 라이브러리를 추가하면 JPA가 실행하는 쿼리의 실행 시간을 콘솔에서 볼 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="조인-최적화--드라이빙-테이블이-결과를-결정한다"&gt;&lt;a href="#%ec%a1%b0%ec%9d%b8-%ec%b5%9c%ec%a0%81%ed%99%94--%eb%93%9c%eb%9d%bc%ec%9d%b4%eb%b9%99-%ed%85%8c%ec%9d%b4%eb%b8%94%ec%9d%b4-%ea%b2%b0%ea%b3%bc%eb%a5%bc-%ea%b2%b0%ec%a0%95%ed%95%9c%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;조인 최적화 — 드라이빙 테이블이 결과를 결정한다
&lt;/h2&gt;&lt;p&gt;MySQL은 조인 시 **드라이빙 테이블(Driving Table)**을 먼저 읽고, 그 결과로 드리븐 테이블(Driven Table)을 검색한다.&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="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;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="n"&gt;o&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;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;member_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="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&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;PENDING&amp;#39;&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;p&gt;옵티마이저는 어떤 테이블을 드라이빙으로 할지 결정한다. 일반적으로 &lt;strong&gt;결과 행 수가 적은 테이블&lt;/strong&gt;이 드라이빙이 된다.&lt;/p&gt;
&lt;p&gt;핵심 원칙: &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-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- members.id에 인덱스가 없으면
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- orders 행 수만큼 members 풀스캔 반복 → O(N*M)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- members.id에 인덱스 있으면
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 각 orders 행에서 members를 O(log M)으로 조회 → O(N*log M)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="offset-페이징의-함정"&gt;&lt;a href="#offset-%ed%8e%98%ec%9d%b4%ec%a7%95%ec%9d%98-%ed%95%a8%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;OFFSET 페이징의 함정
&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="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;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OFFSET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&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;p&gt;문제는 &lt;code&gt;OFFSET 100000&lt;/code&gt;이다. MySQL은 첫 번째 행부터 100,020번째 행까지 읽어서, 앞 100,000개를 버리고 20개만 반환한다. 페이지 번호가 높아질수록 버리는 데이터가 늘어난다.&lt;/p&gt;
&lt;p&gt;페이지 1: 20개 읽기&lt;br&gt;
페이지 5000: 100,020개 읽고 100,000개 버리기&lt;/p&gt;
&lt;h3 id="해결책-1-커버링-인덱스--서브쿼리"&gt;&lt;a href="#%ed%95%b4%ea%b2%b0%ec%b1%85-1-%ec%bb%a4%eb%b2%84%eb%a7%81-%ec%9d%b8%eb%8d%b1%ec%8a%a4--%ec%84%9c%eb%b8%8c%ec%bf%bc%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;해결책 1: 커버링 인덱스 + 서브쿼리
&lt;/h3&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;-- 1단계: 인덱스만으로 PK 목록 조회 (테이블 접근 없음)
&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="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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&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;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OFFSET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&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;-- 2단계: PK로 실제 데이터 조회
&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;o&lt;/span&gt;&lt;span class="p"&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="n"&gt;o&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;JOIN&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&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;SELECT&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;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&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;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&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;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OFFSET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&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="p"&gt;)&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="n"&gt;sub&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;o&lt;/span&gt;&lt;span class="p"&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="n"&gt;sub&lt;/span&gt;&lt;span class="p"&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;서브쿼리는 인덱스만 읽으니 빠르고, 최종 데이터는 PK 20개로만 조회한다.&lt;/p&gt;
&lt;h3 id="해결책-2-커서-기반-페이징-no-offset"&gt;&lt;a href="#%ed%95%b4%ea%b2%b0%ec%b1%85-2-%ec%bb%a4%ec%84%9c-%ea%b8%b0%eb%b0%98-%ed%8e%98%ec%9d%b4%ec%a7%95-no-offset" class="header-anchor"&gt;&lt;/a&gt;해결책 2: 커서 기반 페이징 (No Offset)
&lt;/h3&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="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;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&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;-- 마지막으로 받은 created_at이 &amp;#39;2026-05-01 12:00:00&amp;#39;이라면
&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;-- 다음 페이지 (OFFSET 없이 조건으로)
&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&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;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2026-05-01 12:00:00&amp;#39;&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;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&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;p&gt;OFFSET 없이 항상 인덱스 범위 스캔으로 처리된다. 몇 백만 페이지가 되어도 속도가 일정하다. 단, 임의 페이지 이동(페이지 번호 클릭)이 필요하면 쓸 수 없다. 무한 스크롤에 최적이다.&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;쿼리 최적화의 순서:&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;1. EXPLAIN으로 실행 계획 확인
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. type: ALL → 인덱스 추가
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. Using filesort → ORDER BY 컬럼에 인덱스
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. 조인: 드리븐 테이블 조인 조건에 인덱스
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. 페이징: OFFSET 높으면 커버링 인덱스 또는 커서 기반으로 전환
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;다음 편에서는 테이블 설계의 기초인 정규화와, 실무에서 의도적으로 정규화를 깨는 경우를 다룬다.&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><item><title>[DB 완전 정복 #1] 쿼리가 느린 이유는 대부분 인덱스에 있다</title><link>http://blog.kastori.dev/tech/2026-05-24-db-01-index/</link><pubDate>Sun, 24 May 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-05-24-db-01-index/</guid><description>&lt;h2 id="인덱스-걸었는데-왜-느리죠"&gt;&lt;a href="#%ec%9d%b8%eb%8d%b1%ec%8a%a4-%ea%b1%b8%ec%97%88%eb%8a%94%eb%8d%b0-%ec%99%9c-%eb%8a%90%eb%a6%ac%ec%a3%a0" class="header-anchor"&gt;&lt;/a&gt;&amp;ldquo;인덱스 걸었는데 왜 느리죠?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;개발하다 보면 이런 상황을 만난다. 분명히 인덱스를 걸었는데 쿼리가 여전히 느리다. &lt;code&gt;EXPLAIN&lt;/code&gt;을 돌려보면 &lt;code&gt;type: ALL&lt;/code&gt;이 찍혀 있다. 풀스캔이다.&lt;/p&gt;
&lt;p&gt;인덱스를 걸었는데 왜 안 탈까? 이 글에서는 그 이유를 포함해서, 인덱스가 실제로 어떻게 동작하는지 처음부터 짚어본다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="인덱스는-책의-목차다"&gt;&lt;a href="#%ec%9d%b8%eb%8d%b1%ec%8a%a4%eb%8a%94-%ec%b1%85%ec%9d%98-%eb%aa%a9%ec%b0%a8%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;인덱스는 책의 목차다
&lt;/h2&gt;&lt;p&gt;테이블에서 특정 행을 찾는 방법은 두 가지다. 첫 번째는 처음부터 끝까지 전부 읽는 것(Full Table Scan), 두 번째는 목차를 먼저 보고 해당 페이지로 바로 가는 것(Index Scan)이다.&lt;/p&gt;
&lt;p&gt;100만 건짜리 테이블에서 &lt;code&gt;member_id = 123&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="c1"&gt;-- 인덱스 없음: 100만 건 전부 스캔
&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;member_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;123&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;-- 인덱스 있음: B-Tree로 O(log N) 탐색
&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_member_id&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;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;member_id&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;member_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;123&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;p&gt;인덱스는 &lt;strong&gt;조회를 빠르게&lt;/strong&gt; 하지만 &lt;strong&gt;쓰기를 느리게&lt;/strong&gt; 한다. 행을 삽입·수정·삭제할 때마다 인덱스도 같이 업데이트되기 때문이다. 트레이드오프를 이해하고 써야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="b-tree--mysql-인덱스의-기본-자료구조"&gt;&lt;a href="#b-tree--mysql-%ec%9d%b8%eb%8d%b1%ec%8a%a4%ec%9d%98-%ea%b8%b0%eb%b3%b8-%ec%9e%90%eb%a3%8c%ea%b5%ac%ec%a1%b0" class="header-anchor"&gt;&lt;/a&gt;B-Tree — MySQL 인덱스의 기본 자료구조
&lt;/h2&gt;&lt;p&gt;MySQL InnoDB의 기본 인덱스는 **B-Tree(Balanced Tree)**다.&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; [ 루트 노드: 50 ]
&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; [ 내부 노드: 20, 35 ] [ 내부 노드: 65, 80 ]
&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;span class="line"&gt;&lt;span class="cl"&gt; 10,15 20,25 35,40 50,55 65,70 80,90
&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;BETWEEN&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;)이 효율적이다. 50 이상을 찾으면 50이 있는 리프 노드에서 오른쪽으로 쭉 따라가면 된다.&lt;/p&gt;
&lt;h3 id="clustered-index-vs-secondary-index"&gt;&lt;a href="#clustered-index-vs-secondary-index" class="header-anchor"&gt;&lt;/a&gt;Clustered Index vs Secondary Index
&lt;/h3&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;Clustered Index (기본키 인덱스)
&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;→ 테이블당 1개, 기본키 기준으로 물리적 정렬
&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;Secondary Index (일반 인덱스)
&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;→ 조회 시: 인덱스에서 PK 찾기 → PK로 다시 Clustered Index 조회 (2단계)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Secondary Index 조회는 2단계를 거친다. 이 2단계 과정이 &amp;ldquo;인덱스 랜덤 I/O&amp;quot;다. 후술할 커버링 인덱스가 이 비용을 없애는 방법이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="복합-인덱스--순서가-전부다"&gt;&lt;a href="#%eb%b3%b5%ed%95%a9-%ec%9d%b8%eb%8d%b1%ec%8a%a4--%ec%88%9c%ec%84%9c%ea%b0%80-%ec%a0%84%eb%b6%80%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;복합 인덱스 — 순서가 전부다
&lt;/h2&gt;&lt;p&gt;여러 컬럼을 하나의 인덱스로 묶을 때, &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-sql" data-lang="sql"&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_member_status&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;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;member_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;status&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;p&gt;인덱스는 &lt;strong&gt;왼쪽부터 순서대로&lt;/strong&gt; 사용된다. 이걸 &amp;ldquo;선두 컬럼 규칙&amp;quot;이라 한다.&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;-- ✅ member_id + status 둘 다 인덱스 사용
&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;member_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="n"&gt;status&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;DONE&amp;#39;&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;-- ✅ member_id만 인덱스 사용 (선두 컬럼)
&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;member_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="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;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;status&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;DONE&amp;#39;&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;p&gt;&lt;strong&gt;컬럼 순서 결정 기준&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;카디널리티 높은 것 먼저&lt;/strong&gt; — 값이 다양한 컬럼. &lt;code&gt;member_id&lt;/code&gt;(수백만 종류)가 &lt;code&gt;status&lt;/code&gt;(5종류)보다 앞에&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;등호(&lt;code&gt;=&lt;/code&gt;) 먼저, 범위(&lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;) 나중&lt;/strong&gt; — 범위 조건이 앞에 오면 뒤 컬럼은 인덱스를 못 씀&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="커버링-인덱스--테이블을-아예-안-보는-방법"&gt;&lt;a href="#%ec%bb%a4%eb%b2%84%eb%a7%81-%ec%9d%b8%eb%8d%b1%ec%8a%a4--%ed%85%8c%ec%9d%b4%eb%b8%94%ec%9d%84-%ec%95%84%ec%98%88-%ec%95%88-%eb%b3%b4%eb%8a%94-%eb%b0%a9%eb%b2%95" class="header-anchor"&gt;&lt;/a&gt;커버링 인덱스 — 테이블을 아예 안 보는 방법
&lt;/h2&gt;&lt;p&gt;쿼리에 필요한 모든 컬럼이 인덱스 안에 있으면, 테이블 본체를 읽지 않아도 된다. 이것이 &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-sql" data-lang="sql"&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_cover&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;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;member_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;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&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="c1"&gt;-- SELECT 절의 member_id, status, amount가 모두 인덱스에 있음
&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;member_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;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&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;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&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;member_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="n"&gt;status&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;DONE&amp;#39;&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;p&gt;&lt;code&gt;EXPLAIN&lt;/code&gt;에서 &lt;code&gt;Extra: Using index&lt;/code&gt;가 보이면 커버링 인덱스가 작동 중이다. 테이블 랜덤 I/O가 없으니 성능이 극적으로 좋아진다.&lt;/p&gt;
&lt;p&gt;JPA + QueryDSL을 쓴다면 페이징 COUNT 쿼리에서 커버링 인덱스를 활용할 수 있다. &lt;code&gt;COUNT(*)&lt;/code&gt;는 실제 행 데이터가 필요 없으니 인덱스만으로 처리 가능하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="인덱스가-무력화되는-6가지-패턴"&gt;&lt;a href="#%ec%9d%b8%eb%8d%b1%ec%8a%a4%ea%b0%80-%eb%ac%b4%eb%a0%a5%ed%99%94%eb%90%98%eb%8a%94-6%ea%b0%80%ec%a7%80-%ed%8c%a8%ed%84%b4" class="header-anchor"&gt;&lt;/a&gt;인덱스가 무력화되는 6가지 패턴
&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;-- 1. 컬럼에 함수 적용
&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="k"&gt;YEAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&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;2026&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;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&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;2026-01-01&amp;#39;&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="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;-- 2. 앞쪽 와일드카드 LIKE
&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;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIKE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;%홍길동&amp;#39;&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;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIKE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;홍길동%&amp;#39;&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="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;-- 3. 컬럼이 다른 OR 조건
&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;member_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;OR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&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;DONE&amp;#39;&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="c1"&gt;-- → UNION으로 분리하거나 각각 인덱스 조회 후 합치는 방식 검토
&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;-- 4. 데이터 타입 불일치
&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;member_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="s1"&gt;&amp;#39;123&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ❌ INT 컬럼에 문자열 → 묵시적 형변환
&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;member_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;123&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="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;-- 5. 부정 조건
&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;status&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;DONE&amp;#39;&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;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PENDING&amp;#39;&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;PROCESSING&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="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;-- 6. 카디널리티 낮은 컬럼
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- status가 &amp;#39;Y&amp;#39;/&amp;#39;N&amp;#39; 두 가지뿐이면 옵티마이저가 인덱스 대신 풀스캔 선택
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- (50% 이상 행을 어차피 읽어야 하니 풀스캔이 더 효율적이라 판단)
&lt;/span&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="#%ec%9d%b8%eb%8d%b1%ec%8a%a4%eb%a5%bc-%ea%b1%b8%ec%96%b4%ec%95%bc-%ed%95%a0-%eb%95%8c-%ed%94%bc%ed%95%b4%ec%95%bc-%ed%95%a0-%eb%95%8c" 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;✅ 인덱스를 걸어야 하는 경우
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - WHERE 절에 자주 등장하는 컬럼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - JOIN 조건으로 쓰이는 컬럼 (FK)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - ORDER BY, GROUP BY에 사용되는 컬럼
&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;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; - 카디널리티가 낮은 컬럼 (성별, Y/N 등)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 자주 변경되는 컬럼 (INSERT/UPDATE/DELETE 비용 증가)
&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;&amp;ldquo;쿼리가 느리다&amp;quot;는 증상의 원인 대부분은 인덱스에 있다. 인덱스가 없거나, 있어도 무력화되거나, 복합 인덱스 순서가 잘못됐거나.&lt;/p&gt;
&lt;p&gt;느린 쿼리를 만나면 가장 먼저 &lt;code&gt;EXPLAIN&lt;/code&gt;을 돌려보자. &lt;code&gt;type: ALL&lt;/code&gt;이 보이면 인덱스 문제다. 다음 편에서는 EXPLAIN을 읽는 방법과 트랜잭션·락 동작 원리를 다룬다.&lt;/p&gt;</description></item></channel></rss>