<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nosql on kastori</title><link>http://blog.kastori.dev/tags/nosql/</link><description>Recent content in Nosql 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/tags/nosql/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></channel></rss>