<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>DooDoo IT Blog</title>
    <description>Backend development blog by DoYoon Kim. Notes on algorithms, data structures, Kotlin, Java, Spring Boot, and system design. | 백엔드 개발 블로그 — 알고리즘, 자료구조, Kotlin, Java, Spring Boot 학습 기록.</description>
    <link>https://doodoo3804.github.io/</link>
    <atom:link href="https://doodoo3804.github.io/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Fri, 03 Apr 2026 14:44:30 +0900</pubDate>
    <lastBuildDate>Fri, 03 Apr 2026 14:44:30 +0900</lastBuildDate>
    <language>ko</language>
    <managingEditor>doodoo3804@gmail.com (DooDoo IT Blog)</managingEditor>
    <generator>Jekyll v4.3.2</generator>
    
      <item>
        <title>데이터베이스 트랜잭션과 격리 수준</title>
        <description>들어가며</description>
        <content:encoded><![CDATA[<h2 id="들어가며">들어가며</h2>

<p>트랜잭션은 데이터베이스의 가장 근본적인 개념이다. “데이터 정합성”이라는 단어를 들어봤다면, 그 정합성을 보장하는 메커니즘이 바로 트랜잭션이다.</p>

<p>하지만 트랜잭션을 단순히 “커밋 아니면 롤백”으로만 이해하면 실무에서 동시성 문제에 부딪힌다. 이 글에서는 ACID 특성부터 격리 수준, 데드락, 락 전략까지 <strong>트랜잭션의 전체 그림</strong>을 정리한다.</p>

<hr />

<h2 id="트랜잭션-acid-특성">트랜잭션 ACID 특성</h2>

<p>트랜잭션은 네 가지 특성을 보장해야 한다.</p>

<h3 id="atomicity-원자성">Atomicity (원자성)</h3>

<p>트랜잭션의 모든 연산은 <strong>전부 성공하거나 전부 실패</strong>한다. 중간 상태는 없다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">BEGIN</span><span class="p">;</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span> <span class="o">-</span> <span class="mi">10000</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">-- 출금</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span> <span class="o">+</span> <span class="mi">10000</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>  <span class="c1">-- 입금</span>
<span class="k">COMMIT</span><span class="p">;</span>
<span class="c1">-- 둘 중 하나라도 실패하면 ROLLBACK → 둘 다 취소</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="consistency-일관성">Consistency (일관성)</h3>

<p>트랜잭션 전후로 데이터베이스는 항상 <strong>일관된 상태</strong>를 유지한다. 제약 조건(NOT NULL, UNIQUE, FK 등)이 깨지지 않는다.</p>

<h3 id="isolation-격리성">Isolation (격리성)</h3>

<p>동시에 실행되는 트랜잭션들이 서로 <strong>간섭하지 않아야</strong> 한다. 격리 수준에 따라 간섭의 정도가 달라진다.</p>

<h3 id="durability-지속성">Durability (지속성)</h3>

<p>커밋된 트랜잭션의 결과는 <strong>영구적으로 보존</strong>된다. 시스템 장애가 발생해도 커밋된 데이터는 유지된다 (WAL, redo log 등을 통해).</p>

<hr />

<h2 id="동시성-문제">동시성 문제</h2>

<p>격리성이 완벽하지 않을 때 발생하는 세 가지 대표적인 문제:</p>

<h3 id="dirty-read-더티-리드">Dirty Read (더티 리드)</h3>

<p>다른 트랜잭션이 <strong>커밋하지 않은</strong> 데이터를 읽는 것.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>TX1: UPDATE accounts SET balance = 0 WHERE id = 1;  -- 아직 커밋 안 함
TX2: SELECT balance FROM accounts WHERE id = 1;      -- 0을 읽음 (Dirty Read)
TX1: ROLLBACK;                                        -- 원래 값으로 되돌아감
-- TX2는 존재하지 않는 데이터를 읽은 셈
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="non-repeatable-read-반복-불가능-읽기">Non-Repeatable Read (반복 불가능 읽기)</h3>

<p>같은 쿼리를 두 번 실행했을 때 <strong>결과가 다른</strong> 것. 다른 트랜잭션이 <strong>커밋한 UPDATE</strong> 때문에 발생.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>TX1: SELECT balance FROM accounts WHERE id = 1;  -- 10000
TX2: UPDATE accounts SET balance = 5000 WHERE id = 1; COMMIT;
TX1: SELECT balance FROM accounts WHERE id = 1;  -- 5000 (값이 바뀜!)
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="phantom-read-팬텀-리드">Phantom Read (팬텀 리드)</h3>

<p>같은 조건으로 조회했을 때 <strong>행의 수가 달라지는</strong> 것. 다른 트랜잭션이 <strong>커밋한 INSERT/DELETE</strong> 때문에 발생.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>TX1: SELECT COUNT(*) FROM orders WHERE status = 'PENDING';  -- 5건
TX2: INSERT INTO orders (status) VALUES ('PENDING'); COMMIT;
TX1: SELECT COUNT(*) FROM orders WHERE status = 'PENDING';  -- 6건 (유령 행!)
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="격리-수준-4단계">격리 수준 4단계</h2>

<p>SQL 표준은 네 가지 격리 수준을 정의한다. 아래로 갈수록 격리성이 높고, 동시성(성능)은 떨어진다.</p>

<h3 id="read-uncommitted">READ UNCOMMITTED</h3>

<p>가장 낮은 격리 수준. 커밋되지 않은 데이터를 읽을 수 있다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">SET</span> <span class="n">TRANSACTION</span> <span class="k">ISOLATION</span> <span class="k">LEVEL</span> <span class="k">READ</span> <span class="k">UNCOMMITTED</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li>Dirty Read: <strong>발생</strong></li>
  <li>Non-Repeatable Read: <strong>발생</strong></li>
  <li>Phantom Read: <strong>발생</strong></li>
</ul>

<p>실무에서 거의 사용하지 않는다.</p>

<h3 id="read-committed">READ COMMITTED</h3>

<p><strong>대부분의 RDBMS 기본 격리 수준</strong> (PostgreSQL, Oracle, SQL Server 포함). 커밋된 데이터만 읽는다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">SET</span> <span class="n">TRANSACTION</span> <span class="k">ISOLATION</span> <span class="k">LEVEL</span> <span class="k">READ</span> <span class="k">COMMITTED</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li>Dirty Read: <strong>방지</strong></li>
  <li>Non-Repeatable Read: <strong>발생</strong></li>
  <li>Phantom Read: <strong>발생</strong></li>
</ul>

<h3 id="repeatable-read">REPEATABLE READ</h3>

<p>트랜잭션 시작 시점의 스냅샷을 기준으로 읽는다. <strong>MySQL InnoDB의 기본 격리 수준</strong>이다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">SET</span> <span class="n">TRANSACTION</span> <span class="k">ISOLATION</span> <span class="k">LEVEL</span> <span class="k">REPEATABLE</span> <span class="k">READ</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li>Dirty Read: <strong>방지</strong></li>
  <li>Non-Repeatable Read: <strong>방지</strong></li>
  <li>Phantom Read: <strong>발생</strong> (MySQL InnoDB는 Gap Lock으로 대부분 방지)</li>
</ul>

<h3 id="serializable">SERIALIZABLE</h3>

<p>가장 높은 격리 수준. 트랜잭션을 <strong>직렬로 실행한 것과 동일한 결과</strong>를 보장한다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">SET</span> <span class="n">TRANSACTION</span> <span class="k">ISOLATION</span> <span class="k">LEVEL</span> <span class="k">SERIALIZABLE</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li>Dirty Read: <strong>방지</strong></li>
  <li>Non-Repeatable Read: <strong>방지</strong></li>
  <li>Phantom Read: <strong>방지</strong></li>
</ul>

<p>성능 오버헤드가 크므로 꼭 필요한 경우에만 사용한다.</p>

<h3 id="격리-수준-비교표">격리 수준 비교표</h3>

<table>
  <thead>
    <tr>
      <th>격리 수준</th>
      <th>Dirty Read</th>
      <th>Non-Repeatable Read</th>
      <th>Phantom Read</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>READ UNCOMMITTED</td>
      <td>O</td>
      <td>O</td>
      <td>O</td>
    </tr>
    <tr>
      <td>READ COMMITTED</td>
      <td>X</td>
      <td>O</td>
      <td>O</td>
    </tr>
    <tr>
      <td>REPEATABLE READ</td>
      <td>X</td>
      <td>X</td>
      <td>O</td>
    </tr>
    <tr>
      <td>SERIALIZABLE</td>
      <td>X</td>
      <td>X</td>
      <td>X</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="postgresql-격리-수준-실제-동작">PostgreSQL 격리 수준 실제 동작</h2>

<p>PostgreSQL은 내부적으로 <strong>MVCC(Multi-Version Concurrency Control)</strong> 를 사용한다. PostgreSQL의 쿼리 성능 최적화가 필요하다면 <a href="/database/2026/03/25/postgresql-index/">PostgreSQL 인덱스 제대로 이해하기</a>도 함께 참고하자. 각 트랜잭션에 스냅샷을 할당하여 락 없이도 격리성을 제공한다.</p>

<h3 id="postgresql의-특이점">PostgreSQL의 특이점</h3>

<p>PostgreSQL은 실제로 <strong>3단계</strong> 격리 수준만 구현한다:</p>

<table>
  <thead>
    <tr>
      <th>설정값</th>
      <th>실제 동작</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>READ UNCOMMITTED</td>
      <td><strong>READ COMMITTED</strong>로 동작 (Dirty Read 허용 안 함)</td>
    </tr>
    <tr>
      <td>READ COMMITTED</td>
      <td>READ COMMITTED</td>
    </tr>
    <tr>
      <td>REPEATABLE READ</td>
      <td>Snapshot Isolation (REPEATABLE READ)</td>
    </tr>
    <tr>
      <td>SERIALIZABLE</td>
      <td>Serializable Snapshot Isolation (SSI)</td>
    </tr>
  </tbody>
</table>

<p>PostgreSQL은 어떤 격리 수준에서도 <strong>Dirty Read를 허용하지 않는다.</strong></p>

<h3 id="read-committed에서의-동작">READ COMMITTED에서의 동작</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="c1">-- TX1</span>
<span class="k">BEGIN</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="n">balance</span> <span class="k">FROM</span> <span class="n">accounts</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">-- 10000</span>

<span class="c1">-- TX2</span>
<span class="k">BEGIN</span><span class="p">;</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="mi">5000</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">COMMIT</span><span class="p">;</span>

<span class="c1">-- TX1 (계속)</span>
<span class="k">SELECT</span> <span class="n">balance</span> <span class="k">FROM</span> <span class="n">accounts</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">-- 5000 (TX2의 커밋 반영)</span>
<span class="k">COMMIT</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>각 <strong>SQL문 실행 시점</strong>에 새로운 스냅샷을 가져온다. 같은 트랜잭션 내에서도 다른 트랜잭션의 커밋이 보인다.</p>

<h3 id="repeatable-read에서의-동작">REPEATABLE READ에서의 동작</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="c1">-- TX1</span>
<span class="k">BEGIN</span> <span class="n">TRANSACTION</span> <span class="k">ISOLATION</span> <span class="k">LEVEL</span> <span class="k">REPEATABLE</span> <span class="k">READ</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="n">balance</span> <span class="k">FROM</span> <span class="n">accounts</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">-- 10000</span>

<span class="c1">-- TX2</span>
<span class="k">BEGIN</span><span class="p">;</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="mi">5000</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">COMMIT</span><span class="p">;</span>

<span class="c1">-- TX1 (계속)</span>
<span class="k">SELECT</span> <span class="n">balance</span> <span class="k">FROM</span> <span class="n">accounts</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">-- 10000 (스냅샷 유지!)</span>
<span class="k">COMMIT</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>트랜잭션 <strong>시작 시점</strong>의 스냅샷을 계속 사용한다. 다른 트랜잭션의 커밋을 볼 수 없다.</p>

<h3 id="repeatable-read에서의-충돌-감지">REPEATABLE READ에서의 충돌 감지</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="c1">-- TX1</span>
<span class="k">BEGIN</span> <span class="n">TRANSACTION</span> <span class="k">ISOLATION</span> <span class="k">LEVEL</span> <span class="k">REPEATABLE</span> <span class="k">READ</span><span class="p">;</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span> <span class="o">-</span> <span class="mi">1000</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

<span class="c1">-- TX2</span>
<span class="k">BEGIN</span> <span class="n">TRANSACTION</span> <span class="k">ISOLATION</span> <span class="k">LEVEL</span> <span class="k">REPEATABLE</span> <span class="k">READ</span><span class="p">;</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span> <span class="o">-</span> <span class="mi">2000</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="c1">-- TX2는 TX1이 커밋할 때까지 대기</span>

<span class="c1">-- TX1</span>
<span class="k">COMMIT</span><span class="p">;</span>

<span class="c1">-- TX2</span>
<span class="c1">-- ERROR: could not serialize access due to concurrent update</span>
<span class="k">ROLLBACK</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>같은 행을 동시에 수정하면 <strong>먼저 커밋한 쪽이 승리</strong>하고, 나중 트랜잭션은 에러가 발생한다. 애플리케이션에서 재시도 로직이 필요하다.</p>

<h3 id="serializable-ssi">SERIALIZABLE (SSI)</h3>

<p>PostgreSQL의 SERIALIZABLE은 <strong>Serializable Snapshot Isolation(SSI)</strong> 알고리즘을 사용한다. 전통적인 락 기반이 아닌, 스냅샷 격리에 직렬화 충돌 감지를 추가한 방식이다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c1">-- TX1</span>
<span class="k">BEGIN</span> <span class="n">TRANSACTION</span> <span class="k">ISOLATION</span> <span class="k">LEVEL</span> <span class="k">SERIALIZABLE</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="k">SUM</span><span class="p">(</span><span class="n">balance</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">accounts</span> <span class="k">WHERE</span> <span class="n">branch</span> <span class="o">=</span> <span class="s1">'A'</span><span class="p">;</span>  <span class="c1">-- 결과를 기반으로 작업</span>
<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">audit_log</span> <span class="p">(</span><span class="n">total</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span><span class="mi">50000</span><span class="p">);</span>

<span class="c1">-- TX2</span>
<span class="k">BEGIN</span> <span class="n">TRANSACTION</span> <span class="k">ISOLATION</span> <span class="k">LEVEL</span> <span class="k">SERIALIZABLE</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="k">SUM</span><span class="p">(</span><span class="n">balance</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">accounts</span> <span class="k">WHERE</span> <span class="n">branch</span> <span class="o">=</span> <span class="s1">'A'</span><span class="p">;</span>  <span class="c1">-- 같은 데이터를 읽음</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span> <span class="o">+</span> <span class="mi">1000</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

<span class="c1">-- 둘 다 커밋 시도 → 직렬화 불가능하면 한쪽이 실패</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>읽기-쓰기 의존성(rw-dependency)을 추적하여 직렬화 불가능한 패턴을 감지하면 한쪽 트랜잭션을 롤백한다.</p>

<hr />

<h2 id="spring-transactional-옵션">Spring @Transactional 옵션</h2>

<h3 id="propagation-전파-속성">propagation (전파 속성)</h3>

<p>트랜잭션이 이미 존재할 때 새 트랜잭션을 어떻게 처리할지 결정한다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="nd">@Transactional</span><span class="o">(</span><span class="n">propagation</span> <span class="o">=</span> <span class="nc">Propagation</span><span class="o">.</span><span class="na">REQUIRED</span><span class="o">)</span>  <span class="c1">// 기본값</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">methodA</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// 기존 트랜잭션이 있으면 참여, 없으면 새로 생성</span>
<span class="o">}</span>

<span class="nd">@Transactional</span><span class="o">(</span><span class="n">propagation</span> <span class="o">=</span> <span class="nc">Propagation</span><span class="o">.</span><span class="na">REQUIRES_NEW</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">methodB</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// 항상 새 트랜잭션 생성 (기존 트랜잭션은 일시 중단)</span>
<span class="o">}</span>

<span class="nd">@Transactional</span><span class="o">(</span><span class="n">propagation</span> <span class="o">=</span> <span class="nc">Propagation</span><span class="o">.</span><span class="na">MANDATORY</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">methodC</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// 기존 트랜잭션이 반드시 있어야 함 (없으면 예외)</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>전파 속성</th>
      <th>기존 TX 있음</th>
      <th>기존 TX 없음</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>REQUIRED (기본)</td>
      <td>참여</td>
      <td>새로 생성</td>
    </tr>
    <tr>
      <td>REQUIRES_NEW</td>
      <td>새로 생성 (기존 중단)</td>
      <td>새로 생성</td>
    </tr>
    <tr>
      <td>MANDATORY</td>
      <td>참여</td>
      <td>예외 발생</td>
    </tr>
    <tr>
      <td>SUPPORTS</td>
      <td>참여</td>
      <td>TX 없이 실행</td>
    </tr>
    <tr>
      <td>NOT_SUPPORTED</td>
      <td>TX 없이 실행 (기존 중단)</td>
      <td>TX 없이 실행</td>
    </tr>
    <tr>
      <td>NEVER</td>
      <td>예외 발생</td>
      <td>TX 없이 실행</td>
    </tr>
    <tr>
      <td>NESTED</td>
      <td>중첩 TX (세이브포인트)</td>
      <td>새로 생성</td>
    </tr>
  </tbody>
</table>

<h3 id="isolation-격리-수준">isolation (격리 수준)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="nd">@Transactional</span><span class="o">(</span><span class="n">isolation</span> <span class="o">=</span> <span class="nc">Isolation</span><span class="o">.</span><span class="na">READ_COMMITTED</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">readData</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// READ COMMITTED 격리 수준으로 실행</span>
<span class="o">}</span>

<span class="nd">@Transactional</span><span class="o">(</span><span class="n">isolation</span> <span class="o">=</span> <span class="nc">Isolation</span><span class="o">.</span><span class="na">REPEATABLE_READ</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">consistentRead</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// REPEATABLE READ 격리 수준으로 실행</span>
<span class="o">}</span>

<span class="nd">@Transactional</span><span class="o">(</span><span class="n">isolation</span> <span class="o">=</span> <span class="nc">Isolation</span><span class="o">.</span><span class="na">SERIALIZABLE</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">criticalOperation</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// SERIALIZABLE 격리 수준으로 실행 (가장 엄격)</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="readonly">readOnly</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nd">@Transactional</span><span class="o">(</span><span class="n">readOnly</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Order</span><span class="o">&gt;</span> <span class="nf">findOrders</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// Hibernate: 더티 체킹 비활성화 → 성능 향상</span>
    <span class="c1">// DB: 읽기 전용 힌트 전달 → 일부 DB에서 최적화</span>
    <span class="k">return</span> <span class="n">orderRepository</span><span class="o">.</span><span class="na">findAll</span><span class="o">();</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>읽기 전용 메서드에는 <code class="language-plaintext highlighter-rouge">readOnly = true</code>를 반드시 설정하자.</strong> Hibernate의 더티 체킹 비용을 절약하고, DB 레플리카로 라우팅할 수도 있다.</p>

<h3 id="timeout과-rollbackfor">timeout과 rollbackFor</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="nd">@Transactional</span><span class="o">(</span>
    <span class="n">timeout</span> <span class="o">=</span> <span class="mi">5</span><span class="o">,</span>                          <span class="c1">// 5초 초과 시 롤백</span>
    <span class="n">rollbackFor</span> <span class="o">=</span> <span class="nc">BusinessException</span><span class="o">.</span><span class="na">class</span>  <span class="c1">// Checked 예외에도 롤백</span>
<span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">processOrder</span><span class="o">(</span><span class="nc">Long</span> <span class="n">orderId</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// ...</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>기본적으로 <code class="language-plaintext highlighter-rouge">@Transactional</code>은 <strong>Unchecked 예외(RuntimeException)에만 롤백</strong>한다. Checked 예외에도 롤백하려면 <code class="language-plaintext highlighter-rouge">rollbackFor</code>를 지정해야 한다.</p>

<h3 id="주의-프록시-기반-동작">주의: 프록시 기반 동작</h3>

<p>Spring의 <code class="language-plaintext highlighter-rouge">@Transactional</code>은 <strong>AOP 프록시</strong>를 통해 동작한다. 같은 클래스 내부에서 호출하면 프록시를 거치지 않아 트랜잭션이 적용되지 않는다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderService</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">outer</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// 같은 클래스의 inner() 호출 → 프록시를 안 거침</span>
        <span class="c1">// inner()의 @Transactional이 동작하지 않음!</span>
        <span class="k">this</span><span class="o">.</span><span class="na">inner</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Transactional</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">inner</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// ...</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>해결 방법:</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">inner()</code>를 별도 클래스로 분리</li>
  <li><code class="language-plaintext highlighter-rouge">self-injection</code> 사용 (<code class="language-plaintext highlighter-rouge">@Lazy</code> 등)</li>
  <li><code class="language-plaintext highlighter-rouge">TransactionTemplate</code> 프로그래밍 방식 사용</li>
</ol>

<hr />

<h2 id="데드락-발생-원인과-해결">데드락 발생 원인과 해결</h2>

<h3 id="데드락이란">데드락이란</h3>

<p>두 개 이상의 트랜잭션이 서로가 보유한 락을 기다리며 <strong>무한 대기</strong> 상태에 빠지는 것.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>TX1: Lock(A) → Lock(B) 시도 → 대기 (TX2가 B를 보유)
TX2: Lock(B) → Lock(A) 시도 → 대기 (TX1이 A를 보유)
→ 교착 상태!
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="sql-예시">SQL 예시</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="c1">-- TX1</span>
<span class="k">BEGIN</span><span class="p">;</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span> <span class="o">-</span> <span class="mi">100</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">-- id=1 락</span>
<span class="c1">-- (잠시 후)</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span> <span class="o">+</span> <span class="mi">100</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>  <span class="c1">-- id=2 락 대기</span>

<span class="c1">-- TX2</span>
<span class="k">BEGIN</span><span class="p">;</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span> <span class="o">-</span> <span class="mi">200</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>  <span class="c1">-- id=2 락</span>
<span class="c1">-- (잠시 후)</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span> <span class="o">+</span> <span class="mi">200</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">-- id=1 락 대기</span>

<span class="c1">-- 데드락! DB가 한쪽을 강제 롤백</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="데드락-감지">데드락 감지</h3>

<p>PostgreSQL은 <code class="language-plaintext highlighter-rouge">deadlock_timeout</code>(기본 1초) 후 데드락을 감지하고, 비용이 적은 트랜잭션을 롤백한다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 67890;
        blocked by process 67891.
        Process 67891 waits for ShareLock on transaction 12345;
        blocked by process 12345.
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="데드락-예방-전략">데드락 예방 전략</h3>

<p><strong>1. 락 순서 통일</strong></p>

<p>가장 효과적인 방법. 모든 트랜잭션이 동일한 순서로 리소스에 접근하면 데드락이 발생하지 않는다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kt">void</span> <span class="nf">transfer</span><span class="o">(</span><span class="nc">Long</span> <span class="n">fromId</span><span class="o">,</span> <span class="nc">Long</span> <span class="n">toId</span><span class="o">,</span> <span class="kt">int</span> <span class="n">amount</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// 항상 작은 ID 먼저 락</span>
    <span class="nc">Long</span> <span class="n">firstId</span> <span class="o">=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">min</span><span class="o">(</span><span class="n">fromId</span><span class="o">,</span> <span class="n">toId</span><span class="o">);</span>
    <span class="nc">Long</span> <span class="n">secondId</span> <span class="o">=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">max</span><span class="o">(</span><span class="n">fromId</span><span class="o">,</span> <span class="n">toId</span><span class="o">);</span>

    <span class="nc">Account</span> <span class="n">first</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">.</span><span class="na">findByIdWithLock</span><span class="o">(</span><span class="n">firstId</span><span class="o">);</span>
    <span class="nc">Account</span> <span class="n">second</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">.</span><span class="na">findByIdWithLock</span><span class="o">(</span><span class="n">secondId</span><span class="o">);</span>

    <span class="c1">// 이체 로직</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>2. 트랜잭션 범위 최소화</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="c1">// Bad: 외부 API 호출까지 트랜잭션에 포함</span>
<span class="nd">@Transactional</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">processOrder</span><span class="o">(</span><span class="nc">Long</span> <span class="n">orderId</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Order</span> <span class="n">order</span> <span class="o">=</span> <span class="n">orderRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">orderId</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
    <span class="n">paymentGateway</span><span class="o">.</span><span class="na">charge</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">getAmount</span><span class="o">());</span>  <span class="c1">// 외부 API (수 초 소요)</span>
    <span class="n">order</span><span class="o">.</span><span class="na">complete</span><span class="o">();</span>
<span class="o">}</span>

<span class="c1">// Good: 트랜잭션 범위를 DB 작업만으로 제한</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">processOrder</span><span class="o">(</span><span class="nc">Long</span> <span class="n">orderId</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Order</span> <span class="n">order</span> <span class="o">=</span> <span class="n">orderRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">orderId</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
    <span class="n">paymentGateway</span><span class="o">.</span><span class="na">charge</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">getAmount</span><span class="o">());</span>  <span class="c1">// 트랜잭션 밖</span>

    <span class="n">completeOrder</span><span class="o">(</span><span class="n">orderId</span><span class="o">);</span>  <span class="c1">// 별도 트랜잭션</span>
<span class="o">}</span>

<span class="nd">@Transactional</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">completeOrder</span><span class="o">(</span><span class="nc">Long</span> <span class="n">orderId</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Order</span> <span class="n">order</span> <span class="o">=</span> <span class="n">orderRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">orderId</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
    <span class="n">order</span><span class="o">.</span><span class="na">complete</span><span class="o">();</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>3. 타임아웃 설정</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="nd">@Transactional</span><span class="o">(</span><span class="n">timeout</span> <span class="o">=</span> <span class="mi">5</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">criticalUpdate</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// 5초 내에 완료되지 않으면 롤백</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="낙관적-락-vs-비관적-락">낙관적 락 vs 비관적 락</h2>

<p>동시성 제어를 위한 두 가지 전략이다.</p>

<h3 id="비관적-락-pessimistic-lock">비관적 락 (Pessimistic Lock)</h3>

<p><strong>“충돌이 자주 발생할 것”</strong>이라고 가정하고, 데이터를 읽는 시점에 <strong>락을 건다.</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">AccountRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Account</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="c1">// SELECT ... FOR UPDATE</span>
    <span class="nd">@Lock</span><span class="o">(</span><span class="nc">LockModeType</span><span class="o">.</span><span class="na">PESSIMISTIC_WRITE</span><span class="o">)</span>
    <span class="nd">@Query</span><span class="o">(</span><span class="s">"SELECT a FROM Account a WHERE a.id = :id"</span><span class="o">)</span>
    <span class="nc">Account</span> <span class="nf">findByIdWithLock</span><span class="o">(</span><span class="nd">@Param</span><span class="o">(</span><span class="s">"id"</span><span class="o">)</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">);</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>실행되는 SQL:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">accounts</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">FOR</span> <span class="k">UPDATE</span><span class="p">;</span>
<span class="c1">-- 이 행에 대해 다른 트랜잭션의 UPDATE/DELETE를 블로킹</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>사용 시점:</strong></p>
<ul>
  <li>충돌이 빈번한 경우 (재고 차감, 선착순 이벤트)</li>
  <li>데이터 정합성이 반드시 보장되어야 하는 경우</li>
  <li>트랜잭션이 짧은 경우</li>
</ul>

<p><strong>주의사항:</strong></p>
<ul>
  <li>락 대기로 인한 성능 저하</li>
  <li>데드락 가능성</li>
  <li>타임아웃 설정 필수</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="nd">@Lock</span><span class="o">(</span><span class="nc">LockModeType</span><span class="o">.</span><span class="na">PESSIMISTIC_WRITE</span><span class="o">)</span>
<span class="nd">@QueryHints</span><span class="o">({</span><span class="nd">@QueryHint</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"jakarta.persistence.lock.timeout"</span><span class="o">,</span> <span class="n">value</span> <span class="o">=</span> <span class="s">"3000"</span><span class="o">)})</span>
<span class="nd">@Query</span><span class="o">(</span><span class="s">"SELECT a FROM Account a WHERE a.id = :id"</span><span class="o">)</span>
<span class="nc">Account</span> <span class="nf">findByIdWithLock</span><span class="o">(</span><span class="nd">@Param</span><span class="o">(</span><span class="s">"id"</span><span class="o">)</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="낙관적-락-optimistic-lock">낙관적 락 (Optimistic Lock)</h3>

<p><strong>“충돌이 드물 것”</strong>이라고 가정하고, 실제 업데이트 시점에 <strong>충돌을 감지</strong>한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="nd">@Entity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Product</span> <span class="o">{</span>
    <span class="nd">@Id</span> <span class="nd">@GeneratedValue</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">stock</span><span class="o">;</span>

    <span class="nd">@Version</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">version</span><span class="o">;</span>  <span class="c1">// 버전 필드</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>동작 방식:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="c1">-- 조회 시: version = 1</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">products</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

<span class="c1">-- 업데이트 시: WHERE에 version 조건 추가</span>
<span class="k">UPDATE</span> <span class="n">products</span>
<span class="k">SET</span> <span class="n">stock</span> <span class="o">=</span> <span class="mi">99</span><span class="p">,</span> <span class="k">version</span> <span class="o">=</span> <span class="mi">2</span>
<span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">AND</span> <span class="k">version</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

<span class="c1">-- 영향받은 행이 0이면 → 다른 트랜잭션이 먼저 수정한 것</span>
<span class="c1">-- → ObjectOptimisticLockingFailureException 발생</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>서비스 계층에서 재시도 처리:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ProductService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ProductRepository</span> <span class="n">productRepository</span><span class="o">;</span>

    <span class="nd">@Retryable</span><span class="o">(</span>
        <span class="n">retryFor</span> <span class="o">=</span> <span class="nc">ObjectOptimisticLockingFailureException</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
        <span class="n">maxAttempts</span> <span class="o">=</span> <span class="mi">3</span><span class="o">,</span>
        <span class="n">backoff</span> <span class="o">=</span> <span class="nd">@Backoff</span><span class="o">(</span><span class="n">delay</span> <span class="o">=</span> <span class="mi">100</span><span class="o">)</span>
    <span class="o">)</span>
    <span class="nd">@Transactional</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">decreaseStock</span><span class="o">(</span><span class="nc">Long</span> <span class="n">productId</span><span class="o">,</span> <span class="kt">int</span> <span class="n">quantity</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Product</span> <span class="n">product</span> <span class="o">=</span> <span class="n">productRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">productId</span><span class="o">)</span>
            <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">EntityNotFoundException</span><span class="o">(</span><span class="s">"상품이 없습니다"</span><span class="o">));</span>

        <span class="n">product</span><span class="o">.</span><span class="na">decreaseStock</span><span class="o">(</span><span class="n">quantity</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">@Retryable</code>을 사용하려면 <code class="language-plaintext highlighter-rouge">spring-retry</code> 의존성과 <code class="language-plaintext highlighter-rouge">@EnableRetry</code> 설정이 필요하다.</p>

<p><strong>사용 시점:</strong></p>
<ul>
  <li>충돌이 드문 경우 (일반적인 게시글 수정 등)</li>
  <li>락으로 인한 성능 저하를 피하고 싶은 경우</li>
  <li>읽기가 많고 쓰기가 적은 경우</li>
</ul>

<h3 id="비교-정리">비교 정리</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>비관적 락</th>
      <th>낙관적 락</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>전략</td>
      <td>충돌 예방 (락 선점)</td>
      <td>충돌 감지 (버전 체크)</td>
    </tr>
    <tr>
      <td>구현</td>
      <td><code class="language-plaintext highlighter-rouge">SELECT FOR UPDATE</code></td>
      <td><code class="language-plaintext highlighter-rouge">@Version</code> 필드</td>
    </tr>
    <tr>
      <td>성능</td>
      <td>락 대기 오버헤드</td>
      <td>충돌 시 재시도 오버헤드</td>
    </tr>
    <tr>
      <td>데드락</td>
      <td>가능</td>
      <td>불가능</td>
    </tr>
    <tr>
      <td>적합한 상황</td>
      <td>충돌 빈번, 짧은 TX</td>
      <td>충돌 드묾, 읽기 위주</td>
    </tr>
    <tr>
      <td>실패 시</td>
      <td>대기 후 실행</td>
      <td>예외 발생 → 재시도</td>
    </tr>
  </tbody>
</table>

<h3 id="실무-의사결정-가이드">실무 의사결정 가이드</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>동시 수정이 발생하는가?
├── 거의 없음 → 낙관적 락 (@Version)
└── 빈번함
    ├── 정합성이 절대적 → 비관적 락 (SELECT FOR UPDATE)
    └── 약간의 지연 허용 → 낙관적 락 + 재시도
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="마무리">마무리</h2>

<p>트랜잭션은 단순히 <code class="language-plaintext highlighter-rouge">@Transactional</code>을 붙이는 것으로 끝나지 않는다.</p>

<ol>
  <li><strong>ACID</strong>를 이해하고, 특히 <strong>Isolation이 성능과 트레이드오프</strong> 관계임을 인지하자.</li>
  <li><strong>격리 수준</strong>은 기본값(READ COMMITTED)을 유지하되, 특정 비즈니스 로직에서 REPEATABLE READ가 필요한지 검토하자.</li>
  <li><strong>데드락</strong>은 락 순서 통일과 트랜잭션 범위 최소화로 예방하자.</li>
  <li><strong>낙관적 락 vs 비관적 락</strong>은 충돌 빈도에 따라 선택하자. 대부분의 경우 낙관적 락으로 시작하는 것이 좋다.</li>
  <li>Spring의 <code class="language-plaintext highlighter-rouge">@Transactional</code>은 <strong>프록시 기반</strong>임을 잊지 말고, 내부 호출 시 트랜잭션이 적용되지 않는 함정을 주의하자.</li>
</ol>

<p>항상 <code class="language-plaintext highlighter-rouge">spring.jpa.show-sql=true</code>와 로그 레벨 설정을 통해 실제 실행되는 SQL과 트랜잭션 경계를 확인하는 습관을 들이자.</p>
]]></content:encoded>
        <pubDate>Mon, 06 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/backend/2026/04/06/database-transaction-isolation/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/backend/2026/04/06/database-transaction-isolation/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Database</category>
        
        <category>Transaction</category>
        
        <category>Isolation</category>
        
        <category>PostgreSQL</category>
        
        <category>Backend</category>
        
        
        <category>backend</category>
        
      </item>
    
      <item>
        <title>Spring Security 아키텍처 완전 이해</title>
        <description>들어가며</description>
        <content:encoded><![CDATA[<h2 id="들어가며">들어가며</h2>

<p>Spring Security는 Spring 기반 애플리케이션의 인증(Authentication)과 인가(Authorization)를 담당하는 프레임워크다. 강력하지만 그만큼 내부 구조가 복잡하다.</p>

<p>설정을 복사해서 붙여넣기만 하면 “왜 이렇게 동작하는지” 이해하지 못한 채로 개발하게 된다. 이 글에서는 Spring Security 6.x(Spring Boot 3.x) 기준으로 <strong>아키텍처를 밑바닥부터</strong> 정리한다.</p>

<hr />

<h2 id="spring-security-필터-체인-구조">Spring Security 필터 체인 구조</h2>

<h3 id="서블릿-필터-기반">서블릿 필터 기반</h3>

<p>Spring Security는 <strong>서블릿 필터(Servlet Filter)</strong> 기반으로 동작한다. 클라이언트의 HTTP 요청이 <code class="language-plaintext highlighter-rouge">DispatcherServlet</code>에 도달하기 전에 보안 필터 체인을 거친다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>Client → [Filter₁] → [Filter₂] → ... → [FilterN] → DispatcherServlet → Controller
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="delegatingfilterproxy와-filterchainproxy">DelegatingFilterProxy와 FilterChainProxy</h3>

<p>Spring Security는 서블릿 컨테이너와 Spring 컨테이너를 연결하기 위해 두 가지 핵심 컴포넌트를 사용한다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre>서블릿 컨테이너
└── DelegatingFilterProxy (서블릿 필터)
    └── FilterChainProxy (Spring Bean)
        └── SecurityFilterChain
            ├── DisableEncodeUrlFilter
            ├── SecurityContextHolderFilter
            ├── CsrfFilter
            ├── LogoutFilter
            ├── UsernamePasswordAuthenticationFilter
            ├── BearerTokenAuthenticationFilter
            ├── AuthorizationFilter
            └── ...
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li><strong>DelegatingFilterProxy</strong>: 서블릿 필터로 등록되지만, 실제 처리를 Spring Bean인 <code class="language-plaintext highlighter-rouge">FilterChainProxy</code>에 위임한다.</li>
  <li><strong>FilterChainProxy</strong>: 여러 <code class="language-plaintext highlighter-rouge">SecurityFilterChain</code>을 관리하며, 요청 URL에 맞는 체인을 선택하여 실행한다.</li>
</ul>

<h3 id="securityfilterchain-설정">SecurityFilterChain 설정</h3>

<p>Spring Security 6.x에서는 <code class="language-plaintext highlighter-rouge">SecurityFilterChain</code>을 Bean으로 등록하여 설정한다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SecurityConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">filterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">http</span>
            <span class="o">.</span><span class="na">csrf</span><span class="o">(</span><span class="n">csrf</span> <span class="o">-&gt;</span> <span class="n">csrf</span><span class="o">.</span><span class="na">disable</span><span class="o">())</span>
            <span class="o">.</span><span class="na">sessionManagement</span><span class="o">(</span><span class="n">session</span> <span class="o">-&gt;</span>
                <span class="n">session</span><span class="o">.</span><span class="na">sessionCreationPolicy</span><span class="o">(</span><span class="nc">SessionCreationPolicy</span><span class="o">.</span><span class="na">STATELESS</span><span class="o">))</span>
            <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/auth/**"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/admin/**"</span><span class="o">).</span><span class="na">hasRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
            <span class="o">);</span>

        <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="다중-securityfilterchain">다중 SecurityFilterChain</h3>

<p>URL 패턴에 따라 서로 다른 보안 설정을 적용할 수 있다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="nd">@Bean</span>
<span class="nd">@Order</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">apiFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="n">http</span>
        <span class="o">.</span><span class="na">securityMatcher</span><span class="o">(</span><span class="s">"/api/**"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span><span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">())</span>
        <span class="o">.</span><span class="na">addFilterBefore</span><span class="o">(</span><span class="n">jwtAuthFilter</span><span class="o">,</span> <span class="nc">UsernamePasswordAuthenticationFilter</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>

<span class="nd">@Bean</span>
<span class="nd">@Order</span><span class="o">(</span><span class="mi">2</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">webFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="n">http</span>
        <span class="o">.</span><span class="na">securityMatcher</span><span class="o">(</span><span class="s">"/**"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span><span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">permitAll</span><span class="o">());</span>
    <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">@Order</code>가 작을수록 먼저 매칭을 시도한다. <code class="language-plaintext highlighter-rouge">/api/users</code> 요청은 <code class="language-plaintext highlighter-rouge">apiFilterChain</code>에, <code class="language-plaintext highlighter-rouge">/home</code> 요청은 <code class="language-plaintext highlighter-rouge">webFilterChain</code>에 매칭된다.</p>

<hr />

<h2 id="authentication-vs-authorization">Authentication vs Authorization</h2>

<p>Spring Security의 두 핵심 개념을 명확히 구분해야 한다.</p>

<h3 id="authentication-인증">Authentication (인증)</h3>

<p><strong>“너는 누구냐?”</strong> — 사용자의 신원을 확인하는 과정.</p>

<ul>
  <li>로그인 (ID/PW 확인)</li>
  <li>JWT 토큰 검증</li>
  <li>OAuth2 소셜 로그인</li>
</ul>

<h3 id="authorization-인가">Authorization (인가)</h3>

<p><strong>“너는 이걸 할 수 있느냐?”</strong> — 인증된 사용자가 특정 리소스에 접근할 권한이 있는지 확인하는 과정.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ROLE_USER</code>는 <code class="language-plaintext highlighter-rouge">/api/users</code>에 접근 가능</li>
  <li><code class="language-plaintext highlighter-rouge">ROLE_ADMIN</code>만 <code class="language-plaintext highlighter-rouge">/api/admin</code>에 접근 가능</li>
</ul>

<h3 id="처리-순서">처리 순서</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>요청 → [인증 필터] → SecurityContext에 Authentication 저장 → [인가 필터] → Controller
</pre></td></tr></tbody></table></code></pre></div></div>

<p>인증이 먼저 수행되고, 그 결과(Authentication 객체)를 바탕으로 인가가 수행된다.</p>

<hr />

<h2 id="securitycontextholder와-securitycontext">SecurityContextHolder와 SecurityContext</h2>

<h3 id="구조">구조</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre>SecurityContextHolder
└── SecurityContext
    └── Authentication
        ├── Principal (사용자 정보)
        ├── Credentials (비밀번호 등)
        └── Authorities (권한 목록)
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="securitycontextholder">SecurityContextHolder</h3>

<p><code class="language-plaintext highlighter-rouge">SecurityContext</code>를 보관하는 저장소다. 기본적으로 <strong>ThreadLocal</strong>을 사용하여 스레드별로 <code class="language-plaintext highlighter-rouge">SecurityContext</code>를 관리한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c1">// 현재 인증된 사용자 정보 가져오기</span>
<span class="nc">SecurityContext</span> <span class="n">context</span> <span class="o">=</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">();</span>
<span class="nc">Authentication</span> <span class="n">authentication</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getAuthentication</span><span class="o">();</span>

<span class="nc">String</span> <span class="n">username</span> <span class="o">=</span> <span class="n">authentication</span><span class="o">.</span><span class="na">getName</span><span class="o">();</span>
<span class="nc">Collection</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">GrantedAuthority</span><span class="o">&gt;</span> <span class="n">authorities</span> <span class="o">=</span> <span class="n">authentication</span><span class="o">.</span><span class="na">getAuthorities</span><span class="o">();</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="authentication-인터페이스">Authentication 인터페이스</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">Authentication</span> <span class="kd">extends</span> <span class="nc">Principal</span><span class="o">,</span> <span class="nc">Serializable</span> <span class="o">{</span>
    <span class="c1">// 권한 목록 (ROLE_USER, ROLE_ADMIN 등)</span>
    <span class="nc">Collection</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">GrantedAuthority</span><span class="o">&gt;</span> <span class="nf">getAuthorities</span><span class="o">();</span>

    <span class="c1">// 비밀번호 (인증 후 보안을 위해 null로 지워짐)</span>
    <span class="nc">Object</span> <span class="nf">getCredentials</span><span class="o">();</span>

    <span class="c1">// 사용자 상세 정보</span>
    <span class="nc">Object</span> <span class="nf">getDetails</span><span class="o">();</span>

    <span class="c1">// UserDetails 또는 사용자 식별자</span>
    <span class="nc">Object</span> <span class="nf">getPrincipal</span><span class="o">();</span>

    <span class="c1">// 인증 완료 여부</span>
    <span class="kt">boolean</span> <span class="nf">isAuthenticated</span><span class="o">();</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="컨트롤러에서-인증-정보-접근">컨트롤러에서 인증 정보 접근</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="nd">@RestController</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/api"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserController</span> <span class="o">{</span>

    <span class="c1">// 방법 1: @AuthenticationPrincipal</span>
    <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/me"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">UserResponse</span> <span class="nf">getMyInfo</span><span class="o">(</span><span class="nd">@AuthenticationPrincipal</span> <span class="nc">UserDetails</span> <span class="n">userDetails</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">userService</span><span class="o">.</span><span class="na">findByUsername</span><span class="o">(</span><span class="n">userDetails</span><span class="o">.</span><span class="na">getUsername</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="c1">// 방법 2: SecurityContextHolder 직접 접근</span>
    <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/me2"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">UserResponse</span> <span class="nf">getMyInfo2</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">Authentication</span> <span class="n">auth</span> <span class="o">=</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">getAuthentication</span><span class="o">();</span>
        <span class="nc">String</span> <span class="n">username</span> <span class="o">=</span> <span class="n">auth</span><span class="o">.</span><span class="na">getName</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">userService</span><span class="o">.</span><span class="na">findByUsername</span><span class="o">(</span><span class="n">username</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="userdetailsservice와-userdetails-구현">UserDetailsService와 UserDetails 구현</h2>

<h3 id="userdetails-인터페이스">UserDetails 인터페이스</h3>

<p>Spring Security가 이해하는 사용자 정보 형태다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserDetails</span> <span class="kd">extends</span> <span class="nc">Serializable</span> <span class="o">{</span>
    <span class="nc">Collection</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">GrantedAuthority</span><span class="o">&gt;</span> <span class="nf">getAuthorities</span><span class="o">();</span>
    <span class="nc">String</span> <span class="nf">getPassword</span><span class="o">();</span>
    <span class="nc">String</span> <span class="nf">getUsername</span><span class="o">();</span>
    <span class="kt">boolean</span> <span class="nf">isAccountNonExpired</span><span class="o">();</span>
    <span class="kt">boolean</span> <span class="nf">isAccountNonLocked</span><span class="o">();</span>
    <span class="kt">boolean</span> <span class="nf">isCredentialsNonExpired</span><span class="o">();</span>
    <span class="kt">boolean</span> <span class="nf">isEnabled</span><span class="o">();</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="커스텀-userdetails-구현">커스텀 UserDetails 구현</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
</pre></td><td class="rouge-code"><pre><span class="nd">@Getter</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CustomUserDetails</span> <span class="kd">implements</span> <span class="nc">UserDetails</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">User</span> <span class="n">user</span><span class="o">;</span>  <span class="c1">// 우리 도메인의 User 엔티티</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Collection</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">GrantedAuthority</span><span class="o">&gt;</span> <span class="nf">getAuthorities</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="k">new</span> <span class="nc">SimpleGrantedAuthority</span><span class="o">(</span><span class="s">"ROLE_"</span> <span class="o">+</span> <span class="n">user</span><span class="o">.</span><span class="na">getRole</span><span class="o">().</span><span class="na">name</span><span class="o">()));</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getPassword</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">user</span><span class="o">.</span><span class="na">getPassword</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getUsername</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">user</span><span class="o">.</span><span class="na">getEmail</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isAccountNonExpired</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isAccountNonLocked</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isCredentialsNonExpired</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isEnabled</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="n">user</span><span class="o">.</span><span class="na">isActive</span><span class="o">();</span> <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="userdetailsservice-구현">UserDetailsService 구현</h3>

<p>DB에서 사용자 정보를 조회하여 <code class="language-plaintext highlighter-rouge">UserDetails</code>로 변환하는 역할:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CustomUserDetailsService</span> <span class="kd">implements</span> <span class="nc">UserDetailsService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">UserRepository</span> <span class="n">userRepository</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">UserDetails</span> <span class="nf">loadUserByUsername</span><span class="o">(</span><span class="nc">String</span> <span class="n">email</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">UsernameNotFoundException</span> <span class="o">{</span>
        <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findByEmail</span><span class="o">(</span><span class="n">email</span><span class="o">)</span>
            <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span>
                <span class="k">new</span> <span class="nf">UsernameNotFoundException</span><span class="o">(</span><span class="s">"사용자를 찾을 수 없습니다: "</span> <span class="o">+</span> <span class="n">email</span><span class="o">));</span>

        <span class="k">return</span> <span class="k">new</span> <span class="nf">CustomUserDetails</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="authenticationprovider와의-관계">AuthenticationProvider와의 관계</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>AuthenticationFilter
  → AuthenticationManager
    → AuthenticationProvider
      → UserDetailsService.loadUserByUsername()
      → PasswordEncoder.matches()
    → Authentication 객체 반환
  → SecurityContextHolder에 저장
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">AuthenticationProvider</code>는 <code class="language-plaintext highlighter-rouge">UserDetailsService</code>로 사용자를 조회하고, <code class="language-plaintext highlighter-rouge">PasswordEncoder</code>로 비밀번호를 검증한 뒤 <code class="language-plaintext highlighter-rouge">Authentication</code> 객체를 만들어 반환한다.</p>

<hr />

<h2 id="jwt-기반-인증-구현">JWT 기반 인증 구현</h2>

<p>실제 JWT 인증의 전체 구현 코드와 Refresh Token 전략은 <a href="/spring/2026/04/01/spring-security-jwt/">Spring Security 6 + JWT 인증 구현</a>에서 다룬다.</p>

<h3 id="jwt-유틸리티-클래스">JWT 유틸리티 클래스</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
</pre></td><td class="rouge-code"><pre><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">JwtTokenProvider</span> <span class="o">{</span>

    <span class="nd">@Value</span><span class="o">(</span><span class="s">"${jwt.secret}"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">secretKey</span><span class="o">;</span>

    <span class="nd">@Value</span><span class="o">(</span><span class="s">"${jwt.access-token-validity}"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="kt">long</span> <span class="n">accessTokenValidity</span><span class="o">;</span>  <span class="c1">// 예: 30분</span>

    <span class="nd">@Value</span><span class="o">(</span><span class="s">"${jwt.refresh-token-validity}"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="kt">long</span> <span class="n">refreshTokenValidity</span><span class="o">;</span>  <span class="c1">// 예: 7일</span>

    <span class="kd">private</span> <span class="nc">SecretKey</span> <span class="nf">getSigningKey</span><span class="o">()</span> <span class="o">{</span>
        <span class="kt">byte</span><span class="o">[]</span> <span class="n">keyBytes</span> <span class="o">=</span> <span class="nc">Decoders</span><span class="o">.</span><span class="na">BASE64</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="n">secretKey</span><span class="o">);</span>
        <span class="k">return</span> <span class="nc">Keys</span><span class="o">.</span><span class="na">hmacShaKeyFor</span><span class="o">(</span><span class="n">keyBytes</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">generateAccessToken</span><span class="o">(</span><span class="nc">Authentication</span> <span class="n">authentication</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">CustomUserDetails</span> <span class="n">userDetails</span> <span class="o">=</span> <span class="o">(</span><span class="nc">CustomUserDetails</span><span class="o">)</span> <span class="n">authentication</span><span class="o">.</span><span class="na">getPrincipal</span><span class="o">();</span>
        <span class="nc">Date</span> <span class="n">now</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Date</span><span class="o">();</span>
        <span class="nc">Date</span> <span class="n">expiry</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Date</span><span class="o">(</span><span class="n">now</span><span class="o">.</span><span class="na">getTime</span><span class="o">()</span> <span class="o">+</span> <span class="n">accessTokenValidity</span><span class="o">);</span>

        <span class="k">return</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
            <span class="o">.</span><span class="na">subject</span><span class="o">(</span><span class="n">userDetails</span><span class="o">.</span><span class="na">getUsername</span><span class="o">())</span>
            <span class="o">.</span><span class="na">claim</span><span class="o">(</span><span class="s">"role"</span><span class="o">,</span> <span class="n">userDetails</span><span class="o">.</span><span class="na">getUser</span><span class="o">().</span><span class="na">getRole</span><span class="o">().</span><span class="na">name</span><span class="o">())</span>
            <span class="o">.</span><span class="na">issuedAt</span><span class="o">(</span><span class="n">now</span><span class="o">)</span>
            <span class="o">.</span><span class="na">expiration</span><span class="o">(</span><span class="n">expiry</span><span class="o">)</span>
            <span class="o">.</span><span class="na">signWith</span><span class="o">(</span><span class="n">getSigningKey</span><span class="o">())</span>
            <span class="o">.</span><span class="na">compact</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getUsernameFromToken</span><span class="o">(</span><span class="nc">String</span> <span class="n">token</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">parser</span><span class="o">()</span>
            <span class="o">.</span><span class="na">verifyWith</span><span class="o">(</span><span class="n">getSigningKey</span><span class="o">())</span>
            <span class="o">.</span><span class="na">build</span><span class="o">()</span>
            <span class="o">.</span><span class="na">parseSignedClaims</span><span class="o">(</span><span class="n">token</span><span class="o">)</span>
            <span class="o">.</span><span class="na">getPayload</span><span class="o">()</span>
            <span class="o">.</span><span class="na">getSubject</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">validateToken</span><span class="o">(</span><span class="nc">String</span> <span class="n">token</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">Jwts</span><span class="o">.</span><span class="na">parser</span><span class="o">()</span>
                <span class="o">.</span><span class="na">verifyWith</span><span class="o">(</span><span class="n">getSigningKey</span><span class="o">())</span>
                <span class="o">.</span><span class="na">build</span><span class="o">()</span>
                <span class="o">.</span><span class="na">parseSignedClaims</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
            <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">JwtException</span> <span class="o">|</span> <span class="nc">IllegalArgumentException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="jwtauthenticationfilter">JwtAuthenticationFilter</h3>

<p>모든 요청에서 JWT를 검증하고 <code class="language-plaintext highlighter-rouge">SecurityContext</code>에 인증 정보를 저장하는 필터:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
</pre></td><td class="rouge-code"><pre><span class="nd">@Component</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">JwtAuthenticationFilter</span> <span class="kd">extends</span> <span class="nc">OncePerRequestFilter</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">JwtTokenProvider</span> <span class="n">jwtTokenProvider</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">CustomUserDetailsService</span> <span class="n">userDetailsService</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">doFilterInternal</span><span class="o">(</span>
            <span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span>
            <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span>
            <span class="nc">FilterChain</span> <span class="n">filterChain</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span>

        <span class="c1">// 1. 헤더에서 JWT 추출</span>
        <span class="nc">String</span> <span class="n">token</span> <span class="o">=</span> <span class="n">resolveToken</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>

        <span class="c1">// 2. 토큰 검증</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">token</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">jwtTokenProvider</span><span class="o">.</span><span class="na">validateToken</span><span class="o">(</span><span class="n">token</span><span class="o">))</span> <span class="o">{</span>
            <span class="c1">// 3. 토큰에서 사용자 정보 추출</span>
            <span class="nc">String</span> <span class="n">username</span> <span class="o">=</span> <span class="n">jwtTokenProvider</span><span class="o">.</span><span class="na">getUsernameFromToken</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>

            <span class="c1">// 4. UserDetailsService로 사용자 조회</span>
            <span class="nc">UserDetails</span> <span class="n">userDetails</span> <span class="o">=</span> <span class="n">userDetailsService</span><span class="o">.</span><span class="na">loadUserByUsername</span><span class="o">(</span><span class="n">username</span><span class="o">);</span>

            <span class="c1">// 5. Authentication 객체 생성 및 SecurityContext에 저장</span>
            <span class="nc">UsernamePasswordAuthenticationToken</span> <span class="n">authentication</span> <span class="o">=</span>
                <span class="k">new</span> <span class="nf">UsernamePasswordAuthenticationToken</span><span class="o">(</span>
                    <span class="n">userDetails</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">userDetails</span><span class="o">.</span><span class="na">getAuthorities</span><span class="o">());</span>

            <span class="n">authentication</span><span class="o">.</span><span class="na">setDetails</span><span class="o">(</span>
                <span class="k">new</span> <span class="nf">WebAuthenticationDetailsSource</span><span class="o">().</span><span class="na">buildDetails</span><span class="o">(</span><span class="n">request</span><span class="o">));</span>

            <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">setAuthentication</span><span class="o">(</span><span class="n">authentication</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="n">filterChain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="nf">resolveToken</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">bearerToken</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">bearerToken</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">bearerToken</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"Bearer "</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">bearerToken</span><span class="o">.</span><span class="na">substring</span><span class="o">(</span><span class="mi">7</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="securityconfig에-필터-등록">SecurityConfig에 필터 등록</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
</pre></td><td class="rouge-code"><pre><span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SecurityConfig</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">JwtAuthenticationFilter</span> <span class="n">jwtAuthFilter</span><span class="o">;</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">filterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">http</span>
            <span class="o">.</span><span class="na">csrf</span><span class="o">(</span><span class="n">csrf</span> <span class="o">-&gt;</span> <span class="n">csrf</span><span class="o">.</span><span class="na">disable</span><span class="o">())</span>
            <span class="o">.</span><span class="na">sessionManagement</span><span class="o">(</span><span class="n">session</span> <span class="o">-&gt;</span>
                <span class="n">session</span><span class="o">.</span><span class="na">sessionCreationPolicy</span><span class="o">(</span><span class="nc">SessionCreationPolicy</span><span class="o">.</span><span class="na">STATELESS</span><span class="o">))</span>
            <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/auth/**"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/admin/**"</span><span class="o">).</span><span class="na">hasRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
            <span class="o">)</span>
            <span class="o">.</span><span class="na">addFilterBefore</span><span class="o">(</span><span class="n">jwtAuthFilter</span><span class="o">,</span>
                <span class="nc">UsernamePasswordAuthenticationFilter</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

        <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">AuthenticationManager</span> <span class="nf">authenticationManager</span><span class="o">(</span>
            <span class="nc">AuthenticationConfiguration</span> <span class="n">config</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">config</span><span class="o">.</span><span class="na">getAuthenticationManager</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">PasswordEncoder</span> <span class="nf">passwordEncoder</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">BCryptPasswordEncoder</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="로그인--토큰-발급-api">로그인 / 토큰 발급 API</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="nd">@RestController</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/api/auth"</span><span class="o">)</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AuthController</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AuthenticationManager</span> <span class="n">authenticationManager</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">JwtTokenProvider</span> <span class="n">jwtTokenProvider</span><span class="o">;</span>

    <span class="nd">@PostMapping</span><span class="o">(</span><span class="s">"/login"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">TokenResponse</span> <span class="nf">login</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nd">@Valid</span> <span class="nc">LoginRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// AuthenticationManager가 UserDetailsService + PasswordEncoder로 검증</span>
        <span class="nc">Authentication</span> <span class="n">authentication</span> <span class="o">=</span> <span class="n">authenticationManager</span><span class="o">.</span><span class="na">authenticate</span><span class="o">(</span>
            <span class="k">new</span> <span class="nf">UsernamePasswordAuthenticationToken</span><span class="o">(</span>
                <span class="n">request</span><span class="o">.</span><span class="na">getEmail</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getPassword</span><span class="o">()));</span>

        <span class="c1">// 인증 성공 시 토큰 발급</span>
        <span class="nc">String</span> <span class="n">accessToken</span> <span class="o">=</span> <span class="n">jwtTokenProvider</span><span class="o">.</span><span class="na">generateAccessToken</span><span class="o">(</span><span class="n">authentication</span><span class="o">);</span>

        <span class="k">return</span> <span class="k">new</span> <span class="nf">TokenResponse</span><span class="o">(</span><span class="n">accessToken</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="전체-인증-흐름-정리">전체 인증 흐름 정리</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre>1. POST /api/auth/login (email, password)
2. AuthenticationManager → UserDetailsService → DB 조회
3. PasswordEncoder.matches()로 비밀번호 검증
4. 검증 성공 → JWT 토큰 발급 → 클라이언트에 반환

5. GET /api/users (Authorization: Bearer xxx)
6. JwtAuthenticationFilter → 토큰 파싱 → 유효성 검증
7. UserDetailsService → DB에서 사용자 조회
8. SecurityContext에 Authentication 저장
9. AuthorizationFilter → 권한 확인
10. Controller 도달
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="method-security">Method Security</h2>

<p>URL 패턴 기반 인가 외에, <strong>메서드 레벨</strong>에서도 권한을 제어할 수 있다.</p>

<h3 id="활성화">활성화</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="nd">@Configuration</span>
<span class="nd">@EnableMethodSecurity</span>  <span class="c1">// Spring Security 6.x</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MethodSecurityConfig</span> <span class="o">{</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="preauthorize">@PreAuthorize</h3>

<p>메서드 실행 <strong>전</strong>에 권한을 검사한다. SpEL(Spring Expression Language)을 사용하여 유연한 조건을 표현할 수 있다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">PostService</span> <span class="o">{</span>

    <span class="c1">// ADMIN 역할만 삭제 가능</span>
    <span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"hasRole('ADMIN')"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">deletePost</span><span class="o">(</span><span class="nc">Long</span> <span class="n">postId</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">postRepository</span><span class="o">.</span><span class="na">deleteById</span><span class="o">(</span><span class="n">postId</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="c1">// 본인 게시글만 수정 가능</span>
    <span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"#userId == authentication.principal.user.id"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">PostResponse</span> <span class="nf">updatePost</span><span class="o">(</span><span class="nc">Long</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">Long</span> <span class="n">postId</span><span class="o">,</span> <span class="nc">PostUpdateRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// ...</span>
    <span class="o">}</span>

    <span class="c1">// ADMIN이거나 본인인 경우</span>
    <span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"hasRole('ADMIN') or #userId == authentication.principal.user.id"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">UserResponse</span> <span class="nf">getUserProfile</span><span class="o">(</span><span class="nc">Long</span> <span class="n">userId</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// ...</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="postauthorize">@PostAuthorize</h3>

<p>메서드 실행 <strong>후</strong>에 반환값을 기반으로 권한을 검사한다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="c1">// 반환된 게시글의 작성자만 볼 수 있음</span>
<span class="nd">@PostAuthorize</span><span class="o">(</span><span class="s">"returnObject.authorId == authentication.principal.user.id"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">PostDetailResponse</span> <span class="nf">getSecretPost</span><span class="o">(</span><span class="nc">Long</span> <span class="n">postId</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">postRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">postId</span><span class="o">)</span>
        <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">PostDetailResponse:</span><span class="o">:</span><span class="n">from</span><span class="o">)</span>
        <span class="o">.</span><span class="na">orElseThrow</span><span class="o">();</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="secured">@Secured</h3>

<p>간단한 역할 기반 인가에 사용한다. SpEL은 지원하지 않는다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="nd">@Secured</span><span class="o">(</span><span class="s">"ROLE_ADMIN"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">adminOnly</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// ...</span>
<span class="o">}</span>

<span class="nd">@Secured</span><span class="o">({</span><span class="s">"ROLE_ADMIN"</span><span class="o">,</span> <span class="s">"ROLE_MANAGER"</span><span class="o">})</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">adminOrManager</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// ...</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="preauthorize-vs-secured-비교">@PreAuthorize vs @Secured 비교</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>@PreAuthorize</th>
      <th>@Secured</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SpEL 지원</td>
      <td>O</td>
      <td>X</td>
    </tr>
    <tr>
      <td>복잡한 조건</td>
      <td><code class="language-plaintext highlighter-rouge">hasRole() and #id == ...</code></td>
      <td>역할 이름만 가능</td>
    </tr>
    <tr>
      <td>파라미터 접근</td>
      <td><code class="language-plaintext highlighter-rouge">#paramName</code>으로 접근 가능</td>
      <td>불가</td>
    </tr>
    <tr>
      <td>권장 여부</td>
      <td><strong>권장</strong></td>
      <td>단순한 경우만</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="csrf-설정">CSRF 설정</h2>

<h3 id="csrf란">CSRF란</h3>

<p><strong>Cross-Site Request Forgery</strong> — 사용자가 의도하지 않은 요청을 보내도록 유도하는 공격이다. 세션 쿠키 기반 인증에서 위험하다.</p>

<h3 id="rest-api에서의-csrf">REST API에서의 CSRF</h3>

<p>JWT 기반의 stateless API는 쿠키를 사용하지 않으므로 CSRF 공격에 취약하지 않다. 따라서 <strong>비활성화</strong>하는 것이 일반적이다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">http</span><span class="o">.</span><span class="na">csrf</span><span class="o">(</span><span class="n">csrf</span> <span class="o">-&gt;</span> <span class="n">csrf</span><span class="o">.</span><span class="na">disable</span><span class="o">());</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="csrf를-활성화해야-하는-경우">CSRF를 활성화해야 하는 경우</h3>

<p>서버 렌더링(Thymeleaf 등) + 세션 기반 인증을 사용하는 경우 CSRF 보호를 유지해야 한다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">http</span><span class="o">.</span><span class="na">csrf</span><span class="o">(</span><span class="n">csrf</span> <span class="o">-&gt;</span> <span class="n">csrf</span>
    <span class="o">.</span><span class="na">csrfTokenRepository</span><span class="o">(</span><span class="nc">CookieCsrfTokenRepository</span><span class="o">.</span><span class="na">withHttpOnlyFalse</span><span class="o">())</span>
    <span class="o">.</span><span class="na">csrfTokenRequestHandler</span><span class="o">(</span><span class="k">new</span> <span class="nc">CsrfTokenRequestAttributeHandler</span><span class="o">())</span>
<span class="o">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c">&lt;!-- Thymeleaf 폼에서 자동으로 CSRF 토큰 포함 --&gt;</span>
<span class="nt">&lt;form</span> <span class="na">th:action=</span><span class="s">"@{/api/posts}"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">name=</span><span class="s">"title"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;button</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">&gt;</span>작성<span class="nt">&lt;/button&gt;</span>
<span class="nt">&lt;/form&gt;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="cors-설정">CORS 설정</h2>

<h3 id="cors란">CORS란</h3>

<p><strong>Cross-Origin Resource Sharing</strong> — 브라우저가 다른 출처(Origin)의 리소스에 접근할 수 있도록 허용하는 메커니즘이다.</p>

<p>프론트엔드(<code class="language-plaintext highlighter-rouge">localhost:3000</code>)와 백엔드(<code class="language-plaintext highlighter-rouge">localhost:8080</code>)가 다른 포트에서 실행되면 CORS 에러가 발생한다.</p>

<h3 id="spring-security에서-cors-설정">Spring Security에서 CORS 설정</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">filterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="n">http</span>
        <span class="o">.</span><span class="na">cors</span><span class="o">(</span><span class="n">cors</span> <span class="o">-&gt;</span> <span class="n">cors</span><span class="o">.</span><span class="na">configurationSource</span><span class="o">(</span><span class="n">corsConfigurationSource</span><span class="o">()))</span>
        <span class="c1">// ... 나머지 설정</span>
    <span class="o">;</span>
    <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>

<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">CorsConfigurationSource</span> <span class="nf">corsConfigurationSource</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">CorsConfiguration</span> <span class="n">config</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CorsConfiguration</span><span class="o">();</span>

    <span class="n">config</span><span class="o">.</span><span class="na">setAllowedOrigins</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
        <span class="s">"http://localhost:3000"</span><span class="o">,</span>
        <span class="s">"https://myapp.com"</span>
    <span class="o">));</span>
    <span class="n">config</span><span class="o">.</span><span class="na">setAllowedMethods</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"GET"</span><span class="o">,</span> <span class="s">"POST"</span><span class="o">,</span> <span class="s">"PUT"</span><span class="o">,</span> <span class="s">"DELETE"</span><span class="o">,</span> <span class="s">"PATCH"</span><span class="o">));</span>
    <span class="n">config</span><span class="o">.</span><span class="na">setAllowedHeaders</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"*"</span><span class="o">));</span>
    <span class="n">config</span><span class="o">.</span><span class="na">setExposedHeaders</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">));</span>
    <span class="n">config</span><span class="o">.</span><span class="na">setAllowCredentials</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
    <span class="n">config</span><span class="o">.</span><span class="na">setMaxAge</span><span class="o">(</span><span class="mi">3600L</span><span class="o">);</span>

    <span class="nc">UrlBasedCorsConfigurationSource</span> <span class="n">source</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">UrlBasedCorsConfigurationSource</span><span class="o">();</span>
    <span class="n">source</span><span class="o">.</span><span class="na">registerCorsConfiguration</span><span class="o">(</span><span class="s">"/api/**"</span><span class="o">,</span> <span class="n">config</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">source</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="주요-설정-항목">주요 설정 항목</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">allowedOrigins</code></td>
      <td>허용할 출처 목록 (<code class="language-plaintext highlighter-rouge">*</code> 사용 시 credentials 불가)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">allowedMethods</code></td>
      <td>허용할 HTTP 메서드</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">allowedHeaders</code></td>
      <td>허용할 요청 헤더</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">exposedHeaders</code></td>
      <td>클라이언트에서 읽을 수 있는 응답 헤더</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">allowCredentials</code></td>
      <td>쿠키/인증 헤더 포함 여부</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">maxAge</code></td>
      <td>Preflight 요청 캐싱 시간(초)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="마무리">마무리</h2>

<p>Spring Security의 핵심 아키텍처를 정리하면:</p>

<ol>
  <li><strong>필터 체인</strong>: 요청이 Controller에 도달하기 전에 보안 필터를 순서대로 통과한다.</li>
  <li><strong>인증(Authentication)</strong>: 사용자가 누구인지 확인하고 <code class="language-plaintext highlighter-rouge">SecurityContext</code>에 저장한다.</li>
  <li><strong>인가(Authorization)</strong>: <code class="language-plaintext highlighter-rouge">SecurityContext</code>의 권한 정보를 바탕으로 접근을 허용/거부한다.</li>
  <li><strong>JWT 인증</strong>: 커스텀 필터에서 토큰을 검증하고 <code class="language-plaintext highlighter-rouge">SecurityContext</code>에 수동으로 인증 정보를 설정한다.</li>
  <li><strong>Method Security</strong>: URL 패턴뿐 아니라 메서드 레벨에서도 세밀한 권한 제어가 가능하다.</li>
</ol>

<p>이 구조를 이해하면 Spring Security의 어떤 기능을 쓰더라도 “왜 이렇게 동작하는지” 설명할 수 있다. 복사-붙여넣기 설정에서 벗어나 <strong>자신만의 보안 설계</strong>를 할 수 있게 된다.</p>
]]></content:encoded>
        <pubDate>Sun, 05 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/spring/2026/04/05/spring-security-architecture/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/spring/2026/04/05/spring-security-architecture/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Spring</category>
        
        <category>Security</category>
        
        <category>Authentication</category>
        
        <category>Authorization</category>
        
        <category>JWT</category>
        
        <category>Backend</category>
        
        
        <category>spring</category>
        
      </item>
    
      <item>
        <title>JPA N+1 문제 완전 정복</title>
        <description>들어가며</description>
        <content:encoded><![CDATA[<h2 id="들어가며">들어가며</h2>

<p><a href="/spring/2026/03/15/spring-boot-jpa-basics/">Spring Boot + JPA 기초</a>에서 기본적인 CRUD API를 구현했다면, 실무에서 반드시 만나게 되는 문제가 있다. 바로 <strong>N+1 문제</strong>다. 엔티티 하나를 조회했을 뿐인데 연관된 엔티티를 가져오기 위해 추가 쿼리가 N번 더 나가는 현상이다.</p>

<p>개발 단계에서는 데이터가 적어 문제를 인지하지 못하다가, 운영 환경에서 데이터가 쌓이면 성능이 급격히 떨어진다. 이 글에서는 N+1 문제의 원인을 정확히 이해하고, 상황에 맞는 해결 전략을 코드와 함께 정리한다.</p>

<hr />

<h2 id="n1-문제란-무엇인가">N+1 문제란 무엇인가</h2>

<h3 id="개념">개념</h3>

<p>N+1 문제란 <strong>1번의 쿼리로 N개의 엔티티를 조회한 뒤, 각 엔티티의 연관 관계를 조회하기 위해 N번의 추가 쿼리가 발생</strong>하는 현상이다.</p>

<p>예를 들어 <code class="language-plaintext highlighter-rouge">Team</code>과 <code class="language-plaintext highlighter-rouge">Member</code>가 1:N 관계일 때:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre><span class="nd">@Entity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Team</span> <span class="o">{</span>
    <span class="nd">@Id</span> <span class="nd">@GeneratedValue</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>

    <span class="nd">@OneToMany</span><span class="o">(</span><span class="n">mappedBy</span> <span class="o">=</span> <span class="s">"team"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Member</span><span class="o">&gt;</span> <span class="n">members</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>
<span class="o">}</span>

<span class="nd">@Entity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Member</span> <span class="o">{</span>
    <span class="nd">@Id</span> <span class="nd">@GeneratedValue</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">username</span><span class="o">;</span>

    <span class="nd">@ManyToOne</span>
    <span class="nd">@JoinColumn</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"team_id"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">Team</span> <span class="n">team</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>팀 목록을 조회하고 각 팀의 멤버를 출력하면:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="nc">List</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="n">teams</span> <span class="o">=</span> <span class="n">teamRepository</span><span class="o">.</span><span class="na">findAll</span><span class="o">();</span> <span class="c1">// 쿼리 1번</span>

<span class="k">for</span> <span class="o">(</span><span class="nc">Team</span> <span class="n">team</span> <span class="o">:</span> <span class="n">teams</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">team</span><span class="o">.</span><span class="na">getMembers</span><span class="o">().</span><span class="na">size</span><span class="o">());</span> <span class="c1">// 팀마다 쿼리 1번씩</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>팀이 10개면 <strong>1(팀 전체 조회) + 10(각 팀의 멤버 조회) = 11번</strong>의 쿼리가 실행된다. 팀이 1000개면 1001번이다.</p>

<h3 id="발생-원인">발생 원인</h3>

<p>N+1 문제는 <strong>JPA가 연관 엔티티를 프록시 객체로 감싸고, 실제 접근 시점에 쿼리를 실행하는 지연 로딩(Lazy Loading) 메커니즘</strong> 때문에 발생한다.</p>

<p>JPQL이나 Spring Data JPA의 <code class="language-plaintext highlighter-rouge">findAll()</code>은 엔티티 자체만 조회하는 SQL을 생성한다. 연관 관계는 별도로 조회하지 않으며, 이후 해당 필드에 접근할 때 개별 쿼리가 나간다.</p>

<hr />

<h2 id="지연-로딩-vs-즉시-로딩">지연 로딩 vs 즉시 로딩</h2>

<h3 id="즉시-로딩-eager">즉시 로딩 (EAGER)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nd">@OneToMany</span><span class="o">(</span><span class="n">mappedBy</span> <span class="o">=</span> <span class="s">"team"</span><span class="o">,</span> <span class="n">fetch</span> <span class="o">=</span> <span class="nc">FetchType</span><span class="o">.</span><span class="na">EAGER</span><span class="o">)</span>
<span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Member</span><span class="o">&gt;</span> <span class="n">members</span><span class="o">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>엔티티 조회 시 연관 엔티티도 <strong>즉시</strong> 함께 가져온다. 하지만 이것이 N+1을 해결하지는 않는다.</p>

<p><strong>JPQL을 사용하면 즉시 로딩이어도 N+1이 발생한다.</strong> JPQL은 SQL로 번역될 때 연관 관계를 고려하지 않고, 조회 후 즉시 로딩 설정을 보고 추가 쿼리를 실행하기 때문이다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c1">// EAGER로 설정해도 JPQL 사용 시 N+1 발생</span>
<span class="nd">@Query</span><span class="o">(</span><span class="s">"SELECT t FROM Team t"</span><span class="o">)</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">findAllTeams</span><span class="o">();</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="지연-로딩-lazy">지연 로딩 (LAZY)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nd">@OneToMany</span><span class="o">(</span><span class="n">mappedBy</span> <span class="o">=</span> <span class="s">"team"</span><span class="o">,</span> <span class="n">fetch</span> <span class="o">=</span> <span class="nc">FetchType</span><span class="o">.</span><span class="na">LAZY</span><span class="o">)</span>
<span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Member</span><span class="o">&gt;</span> <span class="n">members</span><span class="o">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>연관 엔티티에 <strong>실제로 접근할 때</strong> 쿼리가 나간다. 접근하지 않으면 쿼리가 나가지 않는다는 장점이 있다.</p>

<h3 id="결론-기본-전략은-lazy">결론: 기본 전략은 LAZY</h3>

<p><strong>모든 연관 관계는 <code class="language-plaintext highlighter-rouge">FetchType.LAZY</code>로 설정하는 것이 원칙이다.</strong> <code class="language-plaintext highlighter-rouge">@ManyToOne</code>, <code class="language-plaintext highlighter-rouge">@OneToOne</code>은 기본값이 <code class="language-plaintext highlighter-rouge">EAGER</code>이므로 명시적으로 <code class="language-plaintext highlighter-rouge">LAZY</code>를 지정해야 한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nd">@ManyToOne</span><span class="o">(</span><span class="n">fetch</span> <span class="o">=</span> <span class="nc">FetchType</span><span class="o">.</span><span class="na">LAZY</span><span class="o">)</span>
<span class="nd">@JoinColumn</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"team_id"</span><span class="o">)</span>
<span class="kd">private</span> <span class="nc">Team</span> <span class="n">team</span><span class="o">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>지연 로딩으로 설정한 뒤, 필요한 시점에 적절한 방법으로 연관 엔티티를 함께 가져오는 것이 N+1 문제 해결의 핵심이다.</p>

<hr />

<h2 id="해결-전략-1-fetch-join">해결 전략 1: Fetch Join</h2>

<p>가장 많이 사용하는 해결 방법이다. JPQL에서 <code class="language-plaintext highlighter-rouge">JOIN FETCH</code>를 사용하면 연관 엔티티를 <strong>한 번의 쿼리로</strong> 함께 조회한다.</p>

<h3 id="기본-사용법">기본 사용법</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">TeamRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="nd">@Query</span><span class="o">(</span><span class="s">"SELECT t FROM Team t JOIN FETCH t.members"</span><span class="o">)</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">findAllWithMembers</span><span class="o">();</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>실행되는 SQL:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="n">t</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="n">m</span><span class="p">.</span><span class="o">*</span>
<span class="k">FROM</span> <span class="n">team</span> <span class="n">t</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">member</span> <span class="n">m</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="n">team_id</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>쿼리 <strong>1번</strong>으로 팀과 멤버를 모두 가져온다.</p>

<h3 id="중복-제거-distinct">중복 제거: DISTINCT</h3>

<p>일대다 Fetch Join은 카테시안 곱 때문에 <strong>결과가 중복</strong>될 수 있다. 팀 1개에 멤버 3명이면 팀이 3번 반복된다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nd">@Query</span><span class="o">(</span><span class="s">"SELECT DISTINCT t FROM Team t JOIN FETCH t.members"</span><span class="o">)</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">findAllWithMembers</span><span class="o">();</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Hibernate 6(Spring Boot 3.x)부터는 일대다 Fetch Join 시 자동으로 중복을 제거해주지만, 명시적으로 <code class="language-plaintext highlighter-rouge">DISTINCT</code>를 쓰는 것이 안전하다.</p>

<h3 id="주의사항-페이징-불가">주의사항: 페이징 불가</h3>

<p><strong>일대다(1:N) Fetch Join에서는 페이징을 사용할 수 없다.</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c1">// 위험! 메모리에서 페이징 처리됨</span>
<span class="nd">@Query</span><span class="o">(</span><span class="s">"SELECT t FROM Team t JOIN FETCH t.members"</span><span class="o">)</span>
<span class="nc">Page</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">findAllWithMembers</span><span class="o">(</span><span class="nc">Pageable</span> <span class="n">pageable</span><span class="o">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Hibernate는 경고 로그를 남기면서 전체 데이터를 메모리에 올린 뒤 애플리케이션 레벨에서 페이징한다. 데이터가 많으면 <strong>OutOfMemoryError</strong>가 발생한다.</p>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory</code></p>
</blockquote>

<p><strong>다대일(N:1) Fetch Join은 페이징에 문제가 없다.</strong> 결과 row 수가 변하지 않기 때문이다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c1">// 안전! 다대일 Fetch Join + 페이징</span>
<span class="nd">@Query</span><span class="o">(</span><span class="s">"SELECT m FROM Member m JOIN FETCH m.team"</span><span class="o">)</span>
<span class="nc">Page</span><span class="o">&lt;</span><span class="nc">Member</span><span class="o">&gt;</span> <span class="nf">findAllWithTeam</span><span class="o">(</span><span class="nc">Pageable</span> <span class="n">pageable</span><span class="o">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="주의사항-둘-이상의-컬렉션-fetch-join-불가">주의사항: 둘 이상의 컬렉션 Fetch Join 불가</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c1">// MultipleBagFetchException 발생!</span>
<span class="nd">@Query</span><span class="o">(</span><span class="s">"SELECT t FROM Team t JOIN FETCH t.members JOIN FETCH t.projects"</span><span class="o">)</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">findAllWithMembersAndProjects</span><span class="o">();</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>둘 이상의 <code class="language-plaintext highlighter-rouge">@OneToMany</code> 컬렉션을 동시에 Fetch Join하면 카테시안 곱이 기하급수적으로 증가한다. Hibernate는 이를 <code class="language-plaintext highlighter-rouge">MultipleBagFetchException</code>으로 차단한다.</p>

<hr />

<h2 id="해결-전략-2-entitygraph">해결 전략 2: @EntityGraph</h2>

<p><code class="language-plaintext highlighter-rouge">@EntityGraph</code>는 JPQL 없이도 Fetch Join과 동일한 효과를 낼 수 있다.</p>

<h3 id="기본-사용법-1">기본 사용법</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">TeamRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="nd">@EntityGraph</span><span class="o">(</span><span class="n">attributePaths</span> <span class="o">=</span> <span class="o">{</span><span class="s">"members"</span><span class="o">})</span>
    <span class="nd">@Query</span><span class="o">(</span><span class="s">"SELECT t FROM Team t"</span><span class="o">)</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">findAllWithMembers</span><span class="o">();</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="spring-data-jpa-메서드-이름-쿼리와-함께-사용">Spring Data JPA 메서드 이름 쿼리와 함께 사용</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nd">@EntityGraph</span><span class="o">(</span><span class="n">attributePaths</span> <span class="o">=</span> <span class="o">{</span><span class="s">"members"</span><span class="o">})</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">findByName</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>JPQL을 직접 작성하지 않아도 되므로 간단한 경우에 유용하다.</p>

<h3 id="중첩-연관-관계-로딩">중첩 연관 관계 로딩</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nd">@EntityGraph</span><span class="o">(</span><span class="n">attributePaths</span> <span class="o">=</span> <span class="o">{</span><span class="s">"members"</span><span class="o">,</span> <span class="s">"members.address"</span><span class="o">})</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">findAllWithMembersAndAddress</span><span class="o">();</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>점(<code class="language-plaintext highlighter-rouge">.</code>)으로 중첩 경로를 지정할 수 있다.</p>

<h3 id="named-entitygraph">Named EntityGraph</h3>

<p>엔티티 클래스에 미리 정의하고 재사용할 수 있다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="nd">@Entity</span>
<span class="nd">@NamedEntityGraph</span><span class="o">(</span>
    <span class="n">name</span> <span class="o">=</span> <span class="s">"Team.withMembers"</span><span class="o">,</span>
    <span class="n">attributeNodes</span> <span class="o">=</span> <span class="nd">@NamedAttributeNode</span><span class="o">(</span><span class="s">"members"</span><span class="o">)</span>
<span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Team</span> <span class="o">{</span>
    <span class="c1">// ...</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nd">@EntityGraph</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"Team.withMembers"</span><span class="o">)</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">findAll</span><span class="o">();</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="fetch-join-vs-entitygraph">Fetch Join vs @EntityGraph</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Fetch Join</th>
      <th>@EntityGraph</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>사용법</td>
      <td>JPQL에 <code class="language-plaintext highlighter-rouge">JOIN FETCH</code> 작성</td>
      <td>어노테이션으로 지정</td>
    </tr>
    <tr>
      <td>JOIN 방식</td>
      <td>INNER JOIN</td>
      <td>LEFT OUTER JOIN</td>
    </tr>
    <tr>
      <td>유연성</td>
      <td>WHERE 조건 등 자유롭게 조합</td>
      <td>단순 연관 로딩에 적합</td>
    </tr>
    <tr>
      <td>가독성</td>
      <td>JPQL이 길어질 수 있음</td>
      <td>깔끔하고 선언적</td>
    </tr>
  </tbody>
</table>

<p><strong><code class="language-plaintext highlighter-rouge">@EntityGraph</code>는 LEFT OUTER JOIN</strong>을 사용하므로, 연관 엔티티가 없는 경우에도 부모 엔티티가 조회된다. Fetch Join의 INNER JOIN과 동작이 다르니 주의하자.</p>

<hr />

<h2 id="해결-전략-3-batch-size">해결 전략 3: Batch Size</h2>

<p><strong>페이징이 필요한 일대다 관계</strong>에서 가장 실용적인 해결책이다.</p>

<h3 id="동작-원리">동작 원리</h3>

<p><code class="language-plaintext highlighter-rouge">@BatchSize</code>를 설정하면 지연 로딩 시 개별 쿼리 대신 <strong>IN 절로 묶어서</strong> 한 번에 조회한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nd">@Entity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Team</span> <span class="o">{</span>
    <span class="nd">@OneToMany</span><span class="o">(</span><span class="n">mappedBy</span> <span class="o">=</span> <span class="s">"team"</span><span class="o">)</span>
    <span class="nd">@BatchSize</span><span class="o">(</span><span class="n">size</span> <span class="o">=</span> <span class="mi">100</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Member</span><span class="o">&gt;</span> <span class="n">members</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>팀 10개를 조회하고 각 팀의 멤버에 접근하면:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c1">-- Batch Size 없이: 쿼리 10번</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">member</span> <span class="k">WHERE</span> <span class="n">team_id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">member</span> <span class="k">WHERE</span> <span class="n">team_id</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="p">...</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">member</span> <span class="k">WHERE</span> <span class="n">team_id</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>

<span class="c1">-- Batch Size = 100: 쿼리 1번</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">member</span> <span class="k">WHERE</span> <span class="n">team_id</span> <span class="k">IN</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="p">...,</span> <span class="mi">10</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="글로벌-설정">글로벌 설정</h3>

<p>개별 엔티티마다 <code class="language-plaintext highlighter-rouge">@BatchSize</code>를 붙이는 대신, <code class="language-plaintext highlighter-rouge">application.yml</code>에서 글로벌로 설정할 수 있다:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="na">spring</span><span class="pi">:</span>
  <span class="na">jpa</span><span class="pi">:</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">hibernate</span><span class="pi">:</span>
        <span class="na">default_batch_fetch_size</span><span class="pi">:</span> <span class="m">100</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>실무에서는 글로벌 설정을 100~1000 사이로 잡는 것을 권장한다.</strong> 김영한님의 JPA 강의에서도 이 방식을 강조한다.</p>

<h3 id="batch-size--페이징-조합">Batch Size + 페이징 조합</h3>

<p>Fetch Join으로 페이징이 불가능한 일대다 관계에서의 해결 패턴:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c1">// 1. 부모 엔티티만 페이징 조회</span>
<span class="nd">@Query</span><span class="o">(</span><span class="s">"SELECT t FROM Team t"</span><span class="o">)</span>
<span class="nc">Page</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">findAllPaged</span><span class="o">(</span><span class="nc">Pageable</span> <span class="n">pageable</span><span class="o">);</span>

<span class="c1">// 2. Batch Size 설정으로 자식 엔티티를 IN절로 조회</span>
<span class="c1">// application.yml에 default_batch_fetch_size: 100 설정</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nc">Page</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="n">teams</span> <span class="o">=</span> <span class="n">teamRepository</span><span class="o">.</span><span class="na">findAllPaged</span><span class="o">(</span><span class="nc">PageRequest</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">10</span><span class="o">));</span>

<span class="k">for</span> <span class="o">(</span><span class="nc">Team</span> <span class="n">team</span> <span class="o">:</span> <span class="n">teams</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// Batch Size 덕분에 IN절 쿼리 1번으로 10개 팀의 멤버를 모두 조회</span>
    <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">team</span><span class="o">.</span><span class="na">getMembers</span><span class="o">().</span><span class="na">size</span><span class="o">());</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>총 쿼리: <strong>2번</strong> (팀 페이징 1번 + 멤버 IN절 1번). N+1이 1+1로 최적화된다.</p>

<hr />

<h2 id="해결-전략-4-querydsl">해결 전략 4: QueryDSL</h2>

<p>복잡한 조건이 필요하거나 동적 쿼리가 필요한 경우 QueryDSL이 강력하다.</p>

<h3 id="기본-fetch-join">기본 Fetch Join</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="nd">@Repository</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TeamQueryRepository</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">JPAQueryFactory</span> <span class="n">queryFactory</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">findAllWithMembers</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">queryFactory</span>
            <span class="o">.</span><span class="na">selectFrom</span><span class="o">(</span><span class="n">team</span><span class="o">)</span>
            <span class="o">.</span><span class="na">leftJoin</span><span class="o">(</span><span class="n">team</span><span class="o">.</span><span class="na">members</span><span class="o">,</span> <span class="n">member</span><span class="o">).</span><span class="na">fetchJoin</span><span class="o">()</span>
            <span class="o">.</span><span class="na">distinct</span><span class="o">()</span>
            <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="동적-조건--fetch-join">동적 조건 + Fetch Join</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Team</span><span class="o">&gt;</span> <span class="nf">searchTeams</span><span class="o">(</span><span class="nc">TeamSearchCondition</span> <span class="n">condition</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">queryFactory</span>
        <span class="o">.</span><span class="na">selectFrom</span><span class="o">(</span><span class="n">team</span><span class="o">)</span>
        <span class="o">.</span><span class="na">leftJoin</span><span class="o">(</span><span class="n">team</span><span class="o">.</span><span class="na">members</span><span class="o">,</span> <span class="n">member</span><span class="o">).</span><span class="na">fetchJoin</span><span class="o">()</span>
        <span class="o">.</span><span class="na">where</span><span class="o">(</span>
            <span class="n">teamNameContains</span><span class="o">(</span><span class="n">condition</span><span class="o">.</span><span class="na">getTeamName</span><span class="o">()),</span>
            <span class="n">memberCountGoe</span><span class="o">(</span><span class="n">condition</span><span class="o">.</span><span class="na">getMinMemberCount</span><span class="o">())</span>
        <span class="o">)</span>
        <span class="o">.</span><span class="na">distinct</span><span class="o">()</span>
        <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="nc">BooleanExpression</span> <span class="nf">teamNameContains</span><span class="o">(</span><span class="nc">String</span> <span class="n">teamName</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nf">hasText</span><span class="o">(</span><span class="n">teamName</span><span class="o">)</span> <span class="o">?</span> <span class="n">team</span><span class="o">.</span><span class="na">name</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">teamName</span><span class="o">)</span> <span class="o">:</span> <span class="kc">null</span><span class="o">;</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="nc">BooleanExpression</span> <span class="nf">memberCountGoe</span><span class="o">(</span><span class="nc">Integer</span> <span class="n">minCount</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">minCount</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span> <span class="n">team</span><span class="o">.</span><span class="na">members</span><span class="o">.</span><span class="na">size</span><span class="o">().</span><span class="na">goe</span><span class="o">(</span><span class="n">minCount</span><span class="o">)</span> <span class="o">:</span> <span class="kc">null</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="dto-직접-조회로-n1-원천-차단">DTO 직접 조회로 N+1 원천 차단</h3>

<p>연관 엔티티의 특정 필드만 필요하다면, DTO로 직접 조회하여 N+1 자체를 없앨 수 있다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">TeamMemberDto</span><span class="o">&gt;</span> <span class="nf">findTeamMemberDtos</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">queryFactory</span>
        <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="nc">Projections</span><span class="o">.</span><span class="na">constructor</span><span class="o">(</span><span class="nc">TeamMemberDto</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
            <span class="n">team</span><span class="o">.</span><span class="na">name</span><span class="o">,</span>
            <span class="n">member</span><span class="o">.</span><span class="na">username</span><span class="o">,</span>
            <span class="n">member</span><span class="o">.</span><span class="na">age</span>
        <span class="o">))</span>
        <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">team</span><span class="o">)</span>
        <span class="o">.</span><span class="na">leftJoin</span><span class="o">(</span><span class="n">team</span><span class="o">.</span><span class="na">members</span><span class="o">,</span> <span class="n">member</span><span class="o">)</span>
        <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>이 경우 엔티티가 아닌 순수 DTO를 반환하므로 지연 로딩 자체가 발생하지 않는다.</p>

<hr />

<h2 id="실무-가이드-언제-무엇을-쓸까">실무 가이드: 언제 무엇을 쓸까</h2>

<h3 id="의사결정-흐름">의사결정 흐름</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre>연관 엔티티를 함께 조회해야 하는가?
├── NO → Lazy Loading 유지 (쿼리 추가 없음)
└── YES
    ├── 페이징이 필요한가?
    │   ├── 다대일(N:1) → Fetch Join + 페이징 ✅
    │   └── 일대다(1:N) → Batch Size + 페이징 ✅
    ├── 단순 연관 로딩인가?
    │   ├── YES → @EntityGraph (간결)
    │   └── NO → Fetch Join (유연)
    ├── 동적 조건이 필요한가?
    │   └── YES → QueryDSL + Fetch Join
    └── 특정 컬럼만 필요한가?
        └── YES → DTO 직접 조회 (QueryDSL Projection)
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="실무-권장-설정">실무 권장 설정</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="c1"># application.yml</span>
<span class="na">spring</span><span class="pi">:</span>
  <span class="na">jpa</span><span class="pi">:</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">hibernate</span><span class="pi">:</span>
        <span class="na">default_batch_fetch_size</span><span class="pi">:</span> <span class="m">100</span>  <span class="c1"># 글로벌 Batch Size</span>
    <span class="na">open-in-view</span><span class="pi">:</span> <span class="kc">false</span>                <span class="c1"># OSIV 끄기 (권장)</span>

<span class="na">logging</span><span class="pi">:</span>
  <span class="na">level</span><span class="pi">:</span>
    <span class="na">org.hibernate.SQL</span><span class="pi">:</span> <span class="s">debug</span>                              <span class="c1"># SQL 로그</span>
    <span class="na">org.hibernate.orm.jdbc.bind</span><span class="pi">:</span> <span class="s">trace</span>                    <span class="c1"># 바인딩 파라미터</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="정리">정리</h3>

<table>
  <thead>
    <tr>
      <th>방법</th>
      <th>장점</th>
      <th>단점</th>
      <th>사용 시점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Fetch Join</td>
      <td>쿼리 1번, 가장 직관적</td>
      <td>페이징 제한, 컬렉션 2개 이상 불가</td>
      <td>기본 해결책</td>
    </tr>
    <tr>
      <td>@EntityGraph</td>
      <td>선언적, JPQL 불필요</td>
      <td>LEFT JOIN 고정, 복잡한 조건 어려움</td>
      <td>단순 연관 로딩</td>
    </tr>
    <tr>
      <td>Batch Size</td>
      <td>페이징 가능, 설정 간단</td>
      <td>쿼리가 1+1번 (완전한 1번은 아님)</td>
      <td>페이징 + 일대다</td>
    </tr>
    <tr>
      <td>QueryDSL</td>
      <td>동적 쿼리, 타입 안전</td>
      <td>설정 복잡, 러닝 커브</td>
      <td>복잡한 조건, 동적 쿼리</td>
    </tr>
    <tr>
      <td>DTO 조회</td>
      <td>N+1 원천 차단, 성능 최적</td>
      <td>엔티티가 아닌 DTO 반환</td>
      <td>조회 전용 API</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="마무리">마무리</h2>

<p>N+1 문제는 JPA를 쓰는 한 피할 수 없다. 중요한 것은 <strong>문제를 인지하고, 상황에 맞는 해결책을 선택하는 것</strong>이다.</p>

<ol>
  <li><strong>기본 전략</strong>: 모든 연관 관계를 <code class="language-plaintext highlighter-rouge">LAZY</code>로 설정하고, <code class="language-plaintext highlighter-rouge">default_batch_fetch_size</code>를 글로벌로 잡아둔다.</li>
  <li><strong>필요한 곳에서</strong>: Fetch Join 또는 @EntityGraph로 한 방 쿼리를 만든다.</li>
  <li><strong>페이징이 필요하면</strong>: Batch Size를 활용한다.</li>
  <li><strong>복잡한 조건이면</strong>: QueryDSL로 해결한다.</li>
</ol>

<p>항상 <code class="language-plaintext highlighter-rouge">hibernate.SQL</code> 로그를 켜두고, 개발 단계에서 쿼리 수를 확인하는 습관을 들이자. 운영에서 터지기 전에 잡을 수 있다.</p>
]]></content:encoded>
        <pubDate>Sat, 04 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/spring/2026/04/04/jpa-n-plus-one-problem/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/spring/2026/04/04/jpa-n-plus-one-problem/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>JPA</category>
        
        <category>Spring</category>
        
        <category>Hibernate</category>
        
        <category>Performance</category>
        
        <category>Backend</category>
        
        
        <category>spring</category>
        
      </item>
    
      <item>
        <title>Apache Kafka 입문부터 실전까지</title>
        <description>Apache Kafka는 분산 이벤트 스트리밍 플랫폼이다. 단순한 메시지 큐를 넘어서 실시간 데이터 파이프라인과 이벤트 기반 아키텍처의 중심에 있다. 이 글에서는 핵심 개념부터 실전 패턴까지 정리한다.</description>
        <content:encoded><![CDATA[<p>Apache Kafka는 <strong>분산 이벤트 스트리밍 플랫폼</strong>이다. 단순한 메시지 큐를 넘어서 실시간 데이터 파이프라인과 이벤트 기반 아키텍처의 중심에 있다. 이 글에서는 핵심 개념부터 실전 패턴까지 정리한다.</p>

<p><br /></p>

<h2 id="1-kafka-핵심-개념">1. Kafka 핵심 개념</h2>

<h3 id="11-아키텍처-개요">1.1 아키텍처 개요</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre>Producer ──→ [Broker Cluster] ──→ Consumer Group
                  │
            ┌─────┴─────┐
            │  Topic A   │
            │ ┌─────────┐│
            │ │Partition0││
            │ │Partition1││
            │ │Partition2││
            │ └─────────┘│
            └────────────┘
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="12-핵심-용어-정리">1.2 핵심 용어 정리</h3>

<table>
  <thead>
    <tr>
      <th>개념</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Topic</strong></td>
      <td>메시지를 분류하는 논리적 채널. 하나의 주제(예: <code class="language-plaintext highlighter-rouge">order-events</code>)</td>
    </tr>
    <tr>
      <td><strong>Partition</strong></td>
      <td>Topic을 물리적으로 나눈 단위. 각 파티션 내부는 순서 보장</td>
    </tr>
    <tr>
      <td><strong>Broker</strong></td>
      <td>Kafka 서버 인스턴스. 클러스터는 여러 브로커로 구성</td>
    </tr>
    <tr>
      <td><strong>Consumer Group</strong></td>
      <td>동일 그룹의 컨슈머가 파티션을 분담하여 병렬 소비</td>
    </tr>
    <tr>
      <td><strong>Offset</strong></td>
      <td>파티션 내 메시지의 고유 순번(0, 1, 2, …)</td>
    </tr>
    <tr>
      <td><strong>Producer</strong></td>
      <td>메시지를 Topic에 발행하는 클라이언트</td>
    </tr>
    <tr>
      <td><strong>Consumer</strong></td>
      <td>메시지를 Topic에서 읽는 클라이언트</td>
    </tr>
  </tbody>
</table>

<h3 id="13-partition과-순서-보장">1.3 Partition과 순서 보장</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>Topic: order-events (3 partitions)

Partition 0: [주문A 생성] → [주문A 결제] → [주문A 배송]
Partition 1: [주문B 생성] → [주문B 결제]
Partition 2: [주문C 생성] → [주문C 취소]
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li><strong>파티션 내부</strong>: 순서 보장 (같은 주문은 같은 파티션으로)</li>
  <li><strong>파티션 간</strong>: 순서 보장 안 됨</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c1">// 같은 키(orderId)를 가진 메시지는 동일 파티션으로</span>
<span class="n">producer</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="k">new</span> <span class="nc">ProducerRecord</span><span class="o">&lt;&gt;(</span><span class="s">"order-events"</span><span class="o">,</span> <span class="n">orderId</span><span class="o">,</span> <span class="n">event</span><span class="o">));</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="14-consumer-group-동작-방식">1.4 Consumer Group 동작 방식</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre>Topic: order-events (3 partitions)

Consumer Group "order-service":
  Consumer A ← Partition 0, 1
  Consumer B ← Partition 2

Consumer Group "analytics":
  Consumer C ← Partition 0, 1, 2  (독립적으로 전체 소비)
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li><strong>같은 그룹</strong>: 파티션을 분담 → 메시지를 한 번만 처리</li>
  <li><strong>다른 그룹</strong>: 독립적으로 전체 메시지를 소비</li>
</ul>

<p><br /></p>

<h2 id="2-producerconsumer-동작-원리">2. Producer/Consumer 동작 원리</h2>

<h3 id="21-producer-동작">2.1 Producer 동작</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="nc">Properties</span> <span class="n">props</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Properties</span><span class="o">();</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ProducerConfig</span><span class="o">.</span><span class="na">BOOTSTRAP_SERVERS_CONFIG</span><span class="o">,</span> <span class="s">"localhost:9092"</span><span class="o">);</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ProducerConfig</span><span class="o">.</span><span class="na">KEY_SERIALIZER_CLASS_CONFIG</span><span class="o">,</span>
        <span class="nc">StringSerializer</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ProducerConfig</span><span class="o">.</span><span class="na">VALUE_SERIALIZER_CLASS_CONFIG</span><span class="o">,</span>
        <span class="nc">StringSerializer</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>

<span class="c1">// acks 설정</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ProducerConfig</span><span class="o">.</span><span class="na">ACKS_CONFIG</span><span class="o">,</span> <span class="s">"all"</span><span class="o">);</span> <span class="c1">// 모든 레플리카 확인</span>

<span class="nc">KafkaProducer</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">producer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">KafkaProducer</span><span class="o">&lt;&gt;(</span><span class="n">props</span><span class="o">);</span>

<span class="c1">// 비동기 전송</span>
<span class="n">producer</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="k">new</span> <span class="nc">ProducerRecord</span><span class="o">&lt;&gt;(</span><span class="s">"order-events"</span><span class="o">,</span> <span class="n">orderId</span><span class="o">,</span> <span class="n">orderJson</span><span class="o">),</span>
    <span class="o">(</span><span class="n">metadata</span><span class="o">,</span> <span class="n">exception</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">exception</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"전송 실패: {}"</span><span class="o">,</span> <span class="n">exception</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
            <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"전송 성공: topic={}, partition={}, offset={}"</span><span class="o">,</span>
                    <span class="n">metadata</span><span class="o">.</span><span class="na">topic</span><span class="o">(),</span> <span class="n">metadata</span><span class="o">.</span><span class="na">partition</span><span class="o">(),</span> <span class="n">metadata</span><span class="o">.</span><span class="na">offset</span><span class="o">());</span>
        <span class="o">}</span>
    <span class="o">});</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>acks 설정별 동작:</strong></p>

<table>
  <thead>
    <tr>
      <th>acks</th>
      <th>동작</th>
      <th>성능</th>
      <th>안정성</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0</code></td>
      <td>전송 후 확인 안 함</td>
      <td>최고</td>
      <td>최저</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">1</code></td>
      <td>리더 브로커 기록 확인</td>
      <td>중간</td>
      <td>중간</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">all</code> (<code class="language-plaintext highlighter-rouge">-1</code>)</td>
      <td>모든 ISR 레플리카 기록 확인</td>
      <td>최저</td>
      <td>최고</td>
    </tr>
  </tbody>
</table>

<h3 id="22-consumer-동작">2.2 Consumer 동작</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="nc">Properties</span> <span class="n">props</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Properties</span><span class="o">();</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ConsumerConfig</span><span class="o">.</span><span class="na">BOOTSTRAP_SERVERS_CONFIG</span><span class="o">,</span> <span class="s">"localhost:9092"</span><span class="o">);</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ConsumerConfig</span><span class="o">.</span><span class="na">GROUP_ID_CONFIG</span><span class="o">,</span> <span class="s">"order-service"</span><span class="o">);</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ConsumerConfig</span><span class="o">.</span><span class="na">KEY_DESERIALIZER_CLASS_CONFIG</span><span class="o">,</span>
        <span class="nc">StringDeserializer</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ConsumerConfig</span><span class="o">.</span><span class="na">VALUE_DESERIALIZER_CLASS_CONFIG</span><span class="o">,</span>
        <span class="nc">StringDeserializer</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ConsumerConfig</span><span class="o">.</span><span class="na">ENABLE_AUTO_COMMIT_CONFIG</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span> <span class="c1">// 수동 커밋</span>

<span class="nc">KafkaConsumer</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">consumer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">KafkaConsumer</span><span class="o">&lt;&gt;(</span><span class="n">props</span><span class="o">);</span>
<span class="n">consumer</span><span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"order-events"</span><span class="o">));</span>

<span class="k">while</span> <span class="o">(</span><span class="kc">true</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">ConsumerRecords</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">records</span> <span class="o">=</span>
            <span class="n">consumer</span><span class="o">.</span><span class="na">poll</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMillis</span><span class="o">(</span><span class="mi">1000</span><span class="o">));</span>

    <span class="k">for</span> <span class="o">(</span><span class="nc">ConsumerRecord</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">record</span> <span class="o">:</span> <span class="n">records</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">processOrder</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">value</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="c1">// 처리 완료 후 오프셋 커밋</span>
    <span class="n">consumer</span><span class="o">.</span><span class="na">commitSync</span><span class="o">();</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="23-commit-방식">2.3 Commit 방식</h3>

<table>
  <thead>
    <tr>
      <th>방식</th>
      <th>설정</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Auto Commit</strong></td>
      <td><code class="language-plaintext highlighter-rouge">enable.auto.commit=true</code></td>
      <td>주기적 자동 커밋, 중복/유실 가능</td>
    </tr>
    <tr>
      <td><strong>Sync Commit</strong></td>
      <td><code class="language-plaintext highlighter-rouge">commitSync()</code></td>
      <td>커밋 완료까지 블로킹, 안전</td>
    </tr>
    <tr>
      <td><strong>Async Commit</strong></td>
      <td><code class="language-plaintext highlighter-rouge">commitAsync()</code></td>
      <td>논블로킹, 실패 시 재시도 어려움</td>
    </tr>
  </tbody>
</table>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c1">// 레코드 단위 커밋 (가장 정밀)</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">ConsumerRecord</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">record</span> <span class="o">:</span> <span class="n">records</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">processOrder</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">value</span><span class="o">());</span>

    <span class="n">consumer</span><span class="o">.</span><span class="na">commitSync</span><span class="o">(</span><span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
            <span class="k">new</span> <span class="nf">TopicPartition</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">topic</span><span class="o">(),</span> <span class="n">record</span><span class="o">.</span><span class="na">partition</span><span class="o">()),</span>
            <span class="k">new</span> <span class="nf">OffsetAndMetadata</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">offset</span><span class="o">()</span> <span class="o">+</span> <span class="mi">1</span><span class="o">)</span>
    <span class="o">));</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="3-메시지-보장-수준-비교">3. 메시지 보장 수준 비교</h2>

<h3 id="31-at-least-once-at-most-once-exactly-once">3.1 At-least-once, At-most-once, Exactly-once</h3>

<table>
  <thead>
    <tr>
      <th>보장 수준</th>
      <th>설명</th>
      <th>중복</th>
      <th>유실</th>
      <th>설정</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>At-most-once</strong></td>
      <td>최대 한 번 전달</td>
      <td>✗</td>
      <td>✓</td>
      <td>처리 전 커밋</td>
    </tr>
    <tr>
      <td><strong>At-least-once</strong></td>
      <td>최소 한 번 전달</td>
      <td>✓</td>
      <td>✗</td>
      <td>처리 후 커밋</td>
    </tr>
    <tr>
      <td><strong>Exactly-once</strong></td>
      <td>정확히 한 번 전달</td>
      <td>✗</td>
      <td>✗</td>
      <td>트랜잭션 + 멱등성</td>
    </tr>
  </tbody>
</table>

<h3 id="32-at-most-once-구현">3.2 At-most-once 구현</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c1">// 먼저 커밋 → 처리 중 실패하면 메시지 유실</span>
<span class="nc">ConsumerRecords</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">records</span> <span class="o">=</span> <span class="n">consumer</span><span class="o">.</span><span class="na">poll</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMillis</span><span class="o">(</span><span class="mi">1000</span><span class="o">));</span>
<span class="n">consumer</span><span class="o">.</span><span class="na">commitSync</span><span class="o">();</span> <span class="c1">// 먼저 커밋!</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">ConsumerRecord</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">record</span> <span class="o">:</span> <span class="n">records</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">processOrder</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">value</span><span class="o">());</span> <span class="c1">// 여기서 실패하면 유실</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="33-at-least-once-구현">3.3 At-least-once 구현</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="c1">// 처리 후 커밋 → 커밋 전 실패하면 재처리(중복)</span>
<span class="nc">ConsumerRecords</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">records</span> <span class="o">=</span> <span class="n">consumer</span><span class="o">.</span><span class="na">poll</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMillis</span><span class="o">(</span><span class="mi">1000</span><span class="o">));</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">ConsumerRecord</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">record</span> <span class="o">:</span> <span class="n">records</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">processOrder</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">value</span><span class="o">());</span> <span class="c1">// 먼저 처리!</span>
<span class="o">}</span>
<span class="n">consumer</span><span class="o">.</span><span class="na">commitSync</span><span class="o">();</span> <span class="c1">// 처리 완료 후 커밋</span>
<span class="c1">// 여기서 실패하면 → 다음 poll에서 같은 메시지 다시 수신</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>멱등성(Idempotency)</strong> 으로 중복 처리 방어:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kt">void</span> <span class="nf">processOrder</span><span class="o">(</span><span class="nc">OrderEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// 이미 처리한 이벤트인지 확인</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">processedEventRepository</span><span class="o">.</span><span class="na">existsById</span><span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getEventId</span><span class="o">()))</span> <span class="o">{</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"이미 처리된 이벤트: {}"</span><span class="o">,</span> <span class="n">event</span><span class="o">.</span><span class="na">getEventId</span><span class="o">());</span>
        <span class="k">return</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="n">orderService</span><span class="o">.</span><span class="na">createOrder</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
    <span class="n">processedEventRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="k">new</span> <span class="nc">ProcessedEvent</span><span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getEventId</span><span class="o">()));</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="34-exactly-once-kafka-transactions">3.4 Exactly-once (Kafka Transactions)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="c1">// Producer: 트랜잭션 설정</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ProducerConfig</span><span class="o">.</span><span class="na">ENABLE_IDEMPOTENCE_CONFIG</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ProducerConfig</span><span class="o">.</span><span class="na">TRANSACTIONAL_ID_CONFIG</span><span class="o">,</span> <span class="s">"order-tx-1"</span><span class="o">);</span>

<span class="nc">KafkaProducer</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">producer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">KafkaProducer</span><span class="o">&lt;&gt;(</span><span class="n">props</span><span class="o">);</span>
<span class="n">producer</span><span class="o">.</span><span class="na">initTransactions</span><span class="o">();</span>

<span class="k">try</span> <span class="o">{</span>
    <span class="n">producer</span><span class="o">.</span><span class="na">beginTransaction</span><span class="o">();</span>
    <span class="n">producer</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="k">new</span> <span class="nc">ProducerRecord</span><span class="o">&lt;&gt;(</span><span class="s">"order-events"</span><span class="o">,</span> <span class="n">key</span><span class="o">,</span> <span class="n">value</span><span class="o">));</span>
    <span class="n">producer</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="k">new</span> <span class="nc">ProducerRecord</span><span class="o">&lt;&gt;(</span><span class="s">"payment-events"</span><span class="o">,</span> <span class="n">key</span><span class="o">,</span> <span class="n">paymentValue</span><span class="o">));</span>
    <span class="n">producer</span><span class="o">.</span><span class="na">commitTransaction</span><span class="o">();</span> <span class="c1">// 두 메시지 모두 성공 또는 모두 실패</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">producer</span><span class="o">.</span><span class="na">abortTransaction</span><span class="o">();</span>
    <span class="k">throw</span> <span class="n">e</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c1">// Consumer: read_committed로 커밋된 메시지만 소비</span>
<span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ConsumerConfig</span><span class="o">.</span><span class="na">ISOLATION_LEVEL_CONFIG</span><span class="o">,</span> <span class="s">"read_committed"</span><span class="o">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="4-spring-boot-kafka-연동">4. Spring Boot Kafka 연동</h2>

<h3 id="41-의존성-및-설정">4.1 의존성 및 설정</h3>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c1">// build.gradle</span>
<span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.kafka:spring-kafka'</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="c1"># application.yml</span>
<span class="na">spring</span><span class="pi">:</span>
  <span class="na">kafka</span><span class="pi">:</span>
    <span class="na">bootstrap-servers</span><span class="pi">:</span> <span class="s">localhost:9092</span>
    <span class="na">producer</span><span class="pi">:</span>
      <span class="na">key-serializer</span><span class="pi">:</span> <span class="s">org.apache.kafka.common.serialization.StringSerializer</span>
      <span class="na">value-serializer</span><span class="pi">:</span> <span class="s">org.springframework.kafka.support.serializer.JsonSerializer</span>
      <span class="na">acks</span><span class="pi">:</span> <span class="s">all</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">3</span>
    <span class="na">consumer</span><span class="pi">:</span>
      <span class="na">group-id</span><span class="pi">:</span> <span class="s">order-service</span>
      <span class="na">key-deserializer</span><span class="pi">:</span> <span class="s">org.apache.kafka.common.serialization.StringDeserializer</span>
      <span class="na">value-deserializer</span><span class="pi">:</span> <span class="s">org.springframework.kafka.support.serializer.JsonDeserializer</span>
      <span class="na">auto-offset-reset</span><span class="pi">:</span> <span class="s">earliest</span>
      <span class="na">enable-auto-commit</span><span class="pi">:</span> <span class="kc">false</span>
      <span class="na">properties</span><span class="pi">:</span>
        <span class="na">spring.json.trusted.packages</span><span class="pi">:</span> <span class="s2">"</span><span class="s">com.example.event"</span>
    <span class="na">listener</span><span class="pi">:</span>
      <span class="na">ack-mode</span><span class="pi">:</span> <span class="s">manual</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="42-kafkatemplate--메시지-발행">4.2 KafkaTemplate — 메시지 발행</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
</pre></td><td class="rouge-code"><pre><span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderEventProducer</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">KafkaTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">OrderEvent</span><span class="o">&gt;</span> <span class="n">kafkaTemplate</span><span class="o">;</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">publishOrderCreated</span><span class="o">(</span><span class="nc">Order</span> <span class="n">order</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">OrderEvent</span> <span class="n">event</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">OrderEvent</span><span class="o">(</span>
                <span class="no">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">(),</span>
                <span class="n">order</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span>
                <span class="s">"ORDER_CREATED"</span><span class="o">,</span>
                <span class="n">order</span>
        <span class="o">);</span>

        <span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">SendResult</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">OrderEvent</span><span class="o">&gt;&gt;</span> <span class="n">future</span> <span class="o">=</span>
                <span class="n">kafkaTemplate</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="s">"order-events"</span><span class="o">,</span> <span class="n">order</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">event</span><span class="o">);</span>

        <span class="n">future</span><span class="o">.</span><span class="na">whenComplete</span><span class="o">((</span><span class="n">result</span><span class="o">,</span> <span class="n">ex</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">ex</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"메시지 전송 실패: {}"</span><span class="o">,</span> <span class="n">ex</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
            <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"메시지 전송 성공: topic={}, partition={}, offset={}"</span><span class="o">,</span>
                        <span class="n">result</span><span class="o">.</span><span class="na">getRecordMetadata</span><span class="o">().</span><span class="na">topic</span><span class="o">(),</span>
                        <span class="n">result</span><span class="o">.</span><span class="na">getRecordMetadata</span><span class="o">().</span><span class="na">partition</span><span class="o">(),</span>
                        <span class="n">result</span><span class="o">.</span><span class="na">getRecordMetadata</span><span class="o">().</span><span class="na">offset</span><span class="o">());</span>
            <span class="o">}</span>
        <span class="o">});</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="43-kafkalistener--메시지-소비">4.3 @KafkaListener — 메시지 소비</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
</pre></td><td class="rouge-code"><pre><span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderEventConsumer</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">OrderService</span> <span class="n">orderService</span><span class="o">;</span>

    <span class="nd">@KafkaListener</span><span class="o">(</span>
            <span class="n">topics</span> <span class="o">=</span> <span class="s">"order-events"</span><span class="o">,</span>
            <span class="n">groupId</span> <span class="o">=</span> <span class="s">"order-service"</span><span class="o">,</span>
            <span class="n">containerFactory</span> <span class="o">=</span> <span class="s">"kafkaListenerContainerFactory"</span>
    <span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleOrderEvent</span><span class="o">(</span>
            <span class="nd">@Payload</span> <span class="nc">OrderEvent</span> <span class="n">event</span><span class="o">,</span>
            <span class="nd">@Header</span><span class="o">(</span><span class="nc">KafkaHeaders</span><span class="o">.</span><span class="na">RECEIVED_PARTITION</span><span class="o">)</span> <span class="kt">int</span> <span class="n">partition</span><span class="o">,</span>
            <span class="nd">@Header</span><span class="o">(</span><span class="nc">KafkaHeaders</span><span class="o">.</span><span class="na">OFFSET</span><span class="o">)</span> <span class="kt">long</span> <span class="n">offset</span><span class="o">,</span>
            <span class="nc">Acknowledgment</span> <span class="n">ack</span><span class="o">)</span> <span class="o">{</span>

        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"수신: event={}, partition={}, offset={}"</span><span class="o">,</span>
                <span class="n">event</span><span class="o">.</span><span class="na">getType</span><span class="o">(),</span> <span class="n">partition</span><span class="o">,</span> <span class="n">offset</span><span class="o">);</span>

        <span class="k">try</span> <span class="o">{</span>
            <span class="k">switch</span> <span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getType</span><span class="o">())</span> <span class="o">{</span>
                <span class="k">case</span> <span class="s">"ORDER_CREATED"</span> <span class="o">-&gt;</span> <span class="n">orderService</span><span class="o">.</span><span class="na">handleCreated</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
                <span class="k">case</span> <span class="s">"ORDER_PAID"</span> <span class="o">-&gt;</span> <span class="n">orderService</span><span class="o">.</span><span class="na">handlePaid</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
                <span class="k">case</span> <span class="s">"ORDER_CANCELLED"</span> <span class="o">-&gt;</span> <span class="n">orderService</span><span class="o">.</span><span class="na">handleCancelled</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
                <span class="k">default</span> <span class="o">-&gt;</span> <span class="n">log</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"알 수 없는 이벤트: {}"</span><span class="o">,</span> <span class="n">event</span><span class="o">.</span><span class="na">getType</span><span class="o">());</span>
            <span class="o">}</span>
            <span class="n">ack</span><span class="o">.</span><span class="na">acknowledge</span><span class="o">();</span> <span class="c1">// 수동 커밋</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"이벤트 처리 실패: {}"</span><span class="o">,</span> <span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
            <span class="c1">// ack 하지 않음 → 재시도 또는 DLQ로 이동</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="44-listener-container-factory-설정">4.4 Listener Container Factory 설정</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
</pre></td><td class="rouge-code"><pre><span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">KafkaConfig</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">KafkaTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">OrderEvent</span><span class="o">&gt;</span> <span class="n">kafkaTemplate</span><span class="o">;</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">ConcurrentKafkaListenerContainerFactory</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">OrderEvent</span><span class="o">&gt;</span>
            <span class="nf">kafkaListenerContainerFactory</span><span class="o">(</span>
                    <span class="nc">ConsumerFactory</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">OrderEvent</span><span class="o">&gt;</span> <span class="n">consumerFactory</span><span class="o">)</span> <span class="o">{</span>

        <span class="nc">ConcurrentKafkaListenerContainerFactory</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">OrderEvent</span><span class="o">&gt;</span> <span class="n">factory</span> <span class="o">=</span>
                <span class="k">new</span> <span class="nc">ConcurrentKafkaListenerContainerFactory</span><span class="o">&lt;&gt;();</span>
        <span class="n">factory</span><span class="o">.</span><span class="na">setConsumerFactory</span><span class="o">(</span><span class="n">consumerFactory</span><span class="o">);</span>
        <span class="n">factory</span><span class="o">.</span><span class="na">setConcurrency</span><span class="o">(</span><span class="mi">3</span><span class="o">);</span> <span class="c1">// 3개 스레드로 병렬 소비</span>
        <span class="n">factory</span><span class="o">.</span><span class="na">getContainerProperties</span><span class="o">()</span>
                <span class="o">.</span><span class="na">setAckMode</span><span class="o">(</span><span class="nc">ContainerProperties</span><span class="o">.</span><span class="na">AckMode</span><span class="o">.</span><span class="na">MANUAL</span><span class="o">);</span>

        <span class="c1">// 에러 핸들러 + DLQ</span>
        <span class="n">factory</span><span class="o">.</span><span class="na">setCommonErrorHandler</span><span class="o">(</span>
                <span class="k">new</span> <span class="nf">DefaultErrorHandler</span><span class="o">(</span>
                        <span class="k">new</span> <span class="nf">DeadLetterPublishingRecoverer</span><span class="o">(</span><span class="n">kafkaTemplate</span><span class="o">),</span>
                        <span class="k">new</span> <span class="nf">FixedBackOff</span><span class="o">(</span><span class="mi">1000L</span><span class="o">,</span> <span class="mi">3</span><span class="o">)</span> <span class="c1">// 1초 간격, 3번 재시도</span>
                <span class="o">));</span>

        <span class="k">return</span> <span class="n">factory</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="5-consumer-group-rebalancing">5. Consumer Group Rebalancing</h2>

<h3 id="51-rebalancing이-발생하는-시점">5.1 Rebalancing이 발생하는 시점</h3>

<ul>
  <li>컨슈머가 그룹에 <strong>새로 참가</strong>하거나 <strong>이탈</strong>할 때</li>
  <li>컨슈머가 <strong>heartbeat 타임아웃</strong> (세션 만료)</li>
  <li>토픽의 <strong>파티션 수가 변경</strong>될 때</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre>Before Rebalancing:
  Consumer A ← P0, P1
  Consumer B ← P2

Consumer C 참가 → Rebalancing 발생

After Rebalancing:
  Consumer A ← P0
  Consumer B ← P1
  Consumer C ← P2
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="52-rebalancing-전략">5.2 Rebalancing 전략</h3>

<table>
  <thead>
    <tr>
      <th>전략</th>
      <th>동작</th>
      <th>장점</th>
      <th>단점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Eager (Range/RoundRobin)</strong></td>
      <td>모든 파티션 해제 → 재할당</td>
      <td>구현 단순</td>
      <td>전체 중단(Stop-the-World)</td>
    </tr>
    <tr>
      <td><strong>Cooperative (Incremental)</strong></td>
      <td>이동이 필요한 파티션만 재할당</td>
      <td>중단 최소화</td>
      <td>여러 라운드 필요</td>
    </tr>
  </tbody>
</table>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c1"># Cooperative Rebalancing 활성화</span>
<span class="na">spring</span><span class="pi">:</span>
  <span class="na">kafka</span><span class="pi">:</span>
    <span class="na">consumer</span><span class="pi">:</span>
      <span class="na">properties</span><span class="pi">:</span>
        <span class="na">partition.assignment.strategy</span><span class="pi">:</span> <span class="s">org.apache.kafka.clients.consumer.CooperativeStickyAssignor</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="53-rebalancing-최적화">5.3 Rebalancing 최적화</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="na">spring</span><span class="pi">:</span>
  <span class="na">kafka</span><span class="pi">:</span>
    <span class="na">consumer</span><span class="pi">:</span>
      <span class="na">properties</span><span class="pi">:</span>
        <span class="c1"># Heartbeat 간격 (기본 3초)</span>
        <span class="na">heartbeat.interval.ms</span><span class="pi">:</span> <span class="m">3000</span>
        <span class="c1"># 세션 타임아웃 (기본 45초)</span>
        <span class="na">session.timeout.ms</span><span class="pi">:</span> <span class="m">45000</span>
        <span class="c1"># poll 최대 간격 — 이 시간 내 poll() 호출 필수</span>
        <span class="na">max.poll.interval.ms</span><span class="pi">:</span> <span class="m">300000</span>
        <span class="c1"># 한 번에 가져오는 레코드 수</span>
        <span class="na">max.poll.records</span><span class="pi">:</span> <span class="m">500</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>Tip</strong>: <code class="language-plaintext highlighter-rouge">max.poll.records</code>를 줄이면 처리 시간이 <code class="language-plaintext highlighter-rouge">max.poll.interval.ms</code>를 넘기는 것을 방지할 수 있다.</p>
</blockquote>

<p><br /></p>

<h2 id="6-실전-패턴-dead-letter-queue--retry-전략">6. 실전 패턴: Dead Letter Queue &amp; Retry 전략</h2>

<h3 id="61-dead-letter-queue-dlq">6.1 Dead Letter Queue (DLQ)</h3>

<p>처리에 반복적으로 실패한 메시지를 <strong>별도 토픽</strong>으로 이동시켜 메인 처리 흐름을 보호한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>order-events → Consumer (처리 시도)
    ├── 성공 → ack
    └── 3회 실패 → order-events.DLT (Dead Letter Topic)
                      └── DLQ 모니터링 &amp; 수동 처리
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">KafkaConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">DefaultErrorHandler</span> <span class="nf">errorHandler</span><span class="o">(</span><span class="nc">KafkaTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">template</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// DLQ로 보내는 Recoverer</span>
        <span class="nc">DeadLetterPublishingRecoverer</span> <span class="n">recoverer</span> <span class="o">=</span>
                <span class="k">new</span> <span class="nf">DeadLetterPublishingRecoverer</span><span class="o">(</span><span class="n">template</span><span class="o">,</span>
                        <span class="o">(</span><span class="n">record</span><span class="o">,</span> <span class="n">ex</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">TopicPartition</span><span class="o">(</span>
                                <span class="n">record</span><span class="o">.</span><span class="na">topic</span><span class="o">()</span> <span class="o">+</span> <span class="s">".DLT"</span><span class="o">,</span> <span class="n">record</span><span class="o">.</span><span class="na">partition</span><span class="o">()));</span>

        <span class="c1">// 1초, 2초, 4초 간격으로 3번 재시도 후 DLQ</span>
        <span class="nc">ExponentialBackOff</span> <span class="n">backOff</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ExponentialBackOff</span><span class="o">(</span><span class="mi">1000L</span><span class="o">,</span> <span class="mf">2.0</span><span class="o">);</span>
        <span class="n">backOff</span><span class="o">.</span><span class="na">setMaxElapsedTime</span><span class="o">(</span><span class="mi">10000L</span><span class="o">);</span>

        <span class="nc">DefaultErrorHandler</span> <span class="n">handler</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultErrorHandler</span><span class="o">(</span><span class="n">recoverer</span><span class="o">,</span> <span class="n">backOff</span><span class="o">);</span>

        <span class="c1">// 특정 예외는 재시도 없이 바로 DLQ로</span>
        <span class="n">handler</span><span class="o">.</span><span class="na">addNotRetryableExceptions</span><span class="o">(</span>
                <span class="nc">InvalidMessageException</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="nc">DeserializationException</span><span class="o">.</span><span class="na">class</span>
        <span class="o">);</span>

        <span class="k">return</span> <span class="n">handler</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="62-dlq-메시지-모니터링--재처리">6.2 DLQ 메시지 모니터링 &amp; 재처리</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="nd">@KafkaListener</span><span class="o">(</span><span class="n">topics</span> <span class="o">=</span> <span class="s">"order-events.DLT"</span><span class="o">,</span> <span class="n">groupId</span> <span class="o">=</span> <span class="s">"dlq-handler"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleDlq</span><span class="o">(</span>
        <span class="nc">ConsumerRecord</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">OrderEvent</span><span class="o">&gt;</span> <span class="n">record</span><span class="o">,</span>
        <span class="nd">@Header</span><span class="o">(</span><span class="nc">KafkaHeaders</span><span class="o">.</span><span class="na">DLT_EXCEPTION_MESSAGE</span><span class="o">)</span> <span class="nc">String</span> <span class="n">errorMessage</span><span class="o">,</span>
        <span class="nd">@Header</span><span class="o">(</span><span class="nc">KafkaHeaders</span><span class="o">.</span><span class="na">DLT_ORIGINAL_TOPIC</span><span class="o">)</span> <span class="nc">String</span> <span class="n">originalTopic</span><span class="o">)</span> <span class="o">{</span>

    <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"DLQ 수신: key={}, error={}, originalTopic={}"</span><span class="o">,</span>
            <span class="n">record</span><span class="o">.</span><span class="na">key</span><span class="o">(),</span> <span class="n">errorMessage</span><span class="o">,</span> <span class="n">originalTopic</span><span class="o">);</span>

    <span class="c1">// 알림 발송 (Slack, PagerDuty 등)</span>
    <span class="n">alertService</span><span class="o">.</span><span class="na">sendDlqAlert</span><span class="o">(</span><span class="n">record</span><span class="o">,</span> <span class="n">errorMessage</span><span class="o">);</span>

    <span class="c1">// 필요 시 수동 재처리 큐에 저장</span>
    <span class="n">dlqRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="k">new</span> <span class="nc">DlqRecord</span><span class="o">(</span><span class="n">record</span><span class="o">,</span> <span class="n">errorMessage</span><span class="o">,</span> <span class="n">originalTopic</span><span class="o">));</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="63-retry-topic-패턴">6.3 Retry Topic 패턴</h3>

<p>DLQ 대신 <strong>단계별 재시도 토픽</strong>을 사용하는 고급 패턴:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>order-events → 실패 → order-events-retry-1 (1분 후)
                         → 실패 → order-events-retry-2 (10분 후)
                                    → 실패 → order-events-DLT
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="nd">@RetryableTopic</span><span class="o">(</span>
        <span class="n">attempts</span> <span class="o">=</span> <span class="s">"4"</span><span class="o">,</span>
        <span class="n">backoff</span> <span class="o">=</span> <span class="nd">@Backoff</span><span class="o">(</span><span class="n">delay</span> <span class="o">=</span> <span class="mi">60000</span><span class="o">,</span> <span class="n">multiplier</span> <span class="o">=</span> <span class="mi">10</span><span class="o">,</span> <span class="n">maxDelay</span> <span class="o">=</span> <span class="mi">600000</span><span class="o">),</span>
        <span class="n">dltStrategy</span> <span class="o">=</span> <span class="nc">DltStrategy</span><span class="o">.</span><span class="na">FAIL_ON_ERROR</span><span class="o">,</span>
        <span class="n">topicSuffixingStrategy</span> <span class="o">=</span> <span class="nc">TopicSuffixingStrategy</span><span class="o">.</span><span class="na">SUFFIX_WITH_INDEX_VALUE</span>
<span class="o">)</span>
<span class="nd">@KafkaListener</span><span class="o">(</span><span class="n">topics</span> <span class="o">=</span> <span class="s">"order-events"</span><span class="o">,</span> <span class="n">groupId</span> <span class="o">=</span> <span class="s">"order-service"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleOrderEvent</span><span class="o">(</span><span class="nc">OrderEvent</span> <span class="n">event</span><span class="o">,</span> <span class="nc">Acknowledgment</span> <span class="n">ack</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">orderService</span><span class="o">.</span><span class="na">process</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
    <span class="n">ack</span><span class="o">.</span><span class="na">acknowledge</span><span class="o">();</span>
<span class="o">}</span>

<span class="nd">@DltHandler</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleDlt</span><span class="o">(</span><span class="nc">OrderEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"최종 실패 — DLT 도착: {}"</span><span class="o">,</span> <span class="n">event</span><span class="o">);</span>
    <span class="n">alertService</span><span class="o">.</span><span class="na">sendCriticalAlert</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="7-이벤트-소싱-cqrs와의-조합">7. 이벤트 소싱, CQRS와의 조합</h2>

<h3 id="71-이벤트-소싱-event-sourcing">7.1 이벤트 소싱 (Event Sourcing)</h3>

<p>상태를 직접 저장하는 대신 <strong>상태 변경 이벤트를 순차적으로 저장</strong>하고, 이벤트를 재생하여 현재 상태를 도출한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>주문 #123의 이벤트 로그:
  1. OrderCreated  {items: [...], total: 50000}
  2. PaymentCompleted {paymentId: "pay-1"}
  3. ItemAdded {item: "keyboard", amount: 30000}
  4. OrderShipped {trackingNo: "T-456"}

현재 상태 = 이벤트 1~4를 순차 적용한 결과
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c1">// Kafka를 이벤트 저장소로 활용</span>
<span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderEventStore</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">KafkaTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">OrderEvent</span><span class="o">&gt;</span> <span class="n">kafkaTemplate</span><span class="o">;</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">append</span><span class="o">(</span><span class="nc">String</span> <span class="n">orderId</span><span class="o">,</span> <span class="nc">OrderEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 주문 ID를 키로 → 같은 파티션에 순서대로 저장</span>
        <span class="n">kafkaTemplate</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="s">"order-event-store"</span><span class="o">,</span> <span class="n">orderId</span><span class="o">,</span> <span class="n">event</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="72-cqrs-command-query-responsibility-segregation">7.2 CQRS (Command Query Responsibility Segregation)</h3>

<p><strong>쓰기 모델</strong>과 <strong>읽기 모델</strong>을 분리하여 각각 최적화한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>[Command Side]                    [Query Side]
  Order Command ──→ Event Store   Event Store ──→ Kafka ──→ Read Model
  (정규화 DB)      (Kafka)                                  (비정규화, Elasticsearch 등)
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
</pre></td><td class="rouge-code"><pre><span class="c1">// Command: 주문 생성</span>
<span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderCommandService</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">createOrder</span><span class="o">(</span><span class="nc">CreateOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Order</span> <span class="n">order</span> <span class="o">=</span> <span class="nc">Order</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>
        <span class="n">orderRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>

        <span class="c1">// 이벤트 발행</span>
        <span class="n">kafkaTemplate</span><span class="o">.</span><span class="na">send</span><span class="o">(</span><span class="s">"order-events"</span><span class="o">,</span> <span class="n">order</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span>
                <span class="k">new</span> <span class="nf">OrderCreatedEvent</span><span class="o">(</span><span class="n">order</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// Query: 읽기 모델 업데이트</span>
<span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderQueryProjector</span> <span class="o">{</span>

    <span class="nd">@KafkaListener</span><span class="o">(</span><span class="n">topics</span> <span class="o">=</span> <span class="s">"order-events"</span><span class="o">,</span> <span class="n">groupId</span> <span class="o">=</span> <span class="s">"order-query"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">project</span><span class="o">(</span><span class="nc">OrderEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">switch</span> <span class="o">(</span><span class="n">event</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">case</span> <span class="nc">OrderCreatedEvent</span> <span class="n">e</span> <span class="o">-&gt;</span> <span class="o">{</span>
                <span class="nc">OrderView</span> <span class="n">view</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">OrderView</span><span class="o">(</span>
                        <span class="n">e</span><span class="o">.</span><span class="na">getOrderId</span><span class="o">(),</span> <span class="n">e</span><span class="o">.</span><span class="na">getCustomerName</span><span class="o">(),</span>
                        <span class="n">e</span><span class="o">.</span><span class="na">getItems</span><span class="o">(),</span> <span class="n">e</span><span class="o">.</span><span class="na">getTotal</span><span class="o">(),</span> <span class="s">"CREATED"</span>
                <span class="o">);</span>
                <span class="n">orderViewRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">view</span><span class="o">);</span> <span class="c1">// Elasticsearch, MongoDB 등</span>
            <span class="o">}</span>
            <span class="k">case</span> <span class="nc">OrderShippedEvent</span> <span class="n">e</span> <span class="o">-&gt;</span> <span class="o">{</span>
                <span class="n">orderViewRepository</span><span class="o">.</span><span class="na">updateStatus</span><span class="o">(</span>
                        <span class="n">e</span><span class="o">.</span><span class="na">getOrderId</span><span class="o">(),</span> <span class="s">"SHIPPED"</span><span class="o">,</span> <span class="n">e</span><span class="o">.</span><span class="na">getTrackingNo</span><span class="o">());</span>
            <span class="o">}</span>
            <span class="c1">// ...</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// Query: 읽기 전용 API</span>
<span class="nd">@RestController</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderQueryController</span> <span class="o">{</span>

    <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/orders/{orderId}"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">OrderView</span> <span class="nf">getOrder</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">String</span> <span class="n">orderId</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">orderViewRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">orderId</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/orders/search"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">OrderView</span><span class="o">&gt;</span> <span class="nf">searchOrders</span><span class="o">(</span><span class="nd">@RequestParam</span> <span class="nc">String</span> <span class="n">keyword</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">orderViewRepository</span><span class="o">.</span><span class="na">searchByKeyword</span><span class="o">(</span><span class="n">keyword</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="73-조합의-장단점">7.3 조합의 장단점</h3>

<table>
  <thead>
    <tr>
      <th>장점</th>
      <th>단점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>이벤트 이력 완전 보존</td>
      <td>시스템 복잡도 증가</td>
    </tr>
    <tr>
      <td>읽기/쓰기 독립 확장</td>
      <td>최종 일관성(Eventual Consistency)</td>
    </tr>
    <tr>
      <td>다양한 읽기 모델 구축 가능</td>
      <td>이벤트 스키마 버전 관리 필요</td>
    </tr>
    <tr>
      <td>감사 로그 자동 확보</td>
      <td>이벤트 재생 시간</td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<h2 id="정리">정리</h2>

<table>
  <thead>
    <tr>
      <th>개념</th>
      <th>핵심</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Topic / Partition</td>
      <td>논리적 채널과 물리적 분산 단위</td>
    </tr>
    <tr>
      <td>Consumer Group</td>
      <td>파티션 분담으로 병렬 처리</td>
    </tr>
    <tr>
      <td>Offset Commit</td>
      <td>메시지 처리 위치 관리, 보장 수준 결정</td>
    </tr>
    <tr>
      <td>acks</td>
      <td>Producer의 안정성-성능 트레이드오프</td>
    </tr>
    <tr>
      <td>At-least-once + 멱등성</td>
      <td>실전에서 가장 많이 사용되는 보장 전략</td>
    </tr>
    <tr>
      <td>@KafkaListener</td>
      <td>Spring Boot의 선언적 메시지 소비</td>
    </tr>
    <tr>
      <td>Dead Letter Queue</td>
      <td>실패 메시지 격리로 안정성 확보</td>
    </tr>
    <tr>
      <td>Event Sourcing + CQRS</td>
      <td>Kafka를 중심으로 한 이벤트 기반 아키텍처</td>
    </tr>
  </tbody>
</table>

<p>Kafka는 단순한 메시지 전달이 아니라, <strong>이벤트 기반 시스템의 근간</strong>이다. 메시지 보장 수준과 장애 처리 전략을 올바르게 설계하는 것이 안정적인 시스템의 핵심이다. Kafka와 함께 자주 논의되는 시스템 안정성 주제로 <a href="/system-design/2026/04/01/api-rate-limiting/">API Rate Limiting</a>도 참고하자.</p>

<p><br /></p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://kafka.apache.org/documentation/">Apache Kafka Documentation</a></li>
  <li><a href="https://www.confluent.io/resources/kafka-the-definitive-guide-v2/">Kafka: The Definitive Guide — Confluent</a></li>
  <li><a href="https://docs.spring.io/spring-kafka/reference/">Spring for Apache Kafka — Reference Documentation</a></li>
  <li><a href="https://docs.confluent.io/platform/current/clients/consumer.html">Kafka Consumers — Confluent Documentation</a></li>
  <li><a href="https://docs.spring.io/spring-kafka/reference/kafka/annotation-error-handling.html">Error Handling &amp; Dead Letter Queue — Spring Kafka</a></li>
  <li><a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing">Event Sourcing pattern — Microsoft Azure Architecture</a></li>
  <li><a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs">CQRS pattern — Microsoft Azure Architecture</a></li>
</ul>
]]></content:encoded>
        <pubDate>Fri, 03 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/backend/2026/04/03/kafka-introduction/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/backend/2026/04/03/kafka-introduction/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Kafka</category>
        
        <category>MessageQueue</category>
        
        <category>EventStreaming</category>
        
        <category>Backend</category>
        
        
        <category>backend</category>
        
      </item>
    
      <item>
        <title>Redis 캐싱 전략 완전 정복</title>
        <description>캐싱은 백엔드 성능 최적화의 핵심이다. 캐싱 전략 기초에서 Cache-Aside, Write-Through, Write-Behind 패턴의 개념을 다뤘다면, 이 글에서는 Redis를 활용한 구체적인 구현과 실전에서 마주치는 문제, 해결책을 정리한다.</description>
        <content:encoded><![CDATA[<p>캐싱은 백엔드 성능 최적화의 핵심이다. <a href="/system-design/2026/03/28/caching-strategy/">캐싱 전략 기초</a>에서 Cache-Aside, Write-Through, Write-Behind 패턴의 개념을 다뤘다면, 이 글에서는 Redis를 활용한 구체적인 구현과 실전에서 마주치는 문제, 해결책을 정리한다.</p>

<p><br /></p>

<h2 id="1-캐시-전략-비교">1. 캐시 전략 비교</h2>

<h3 id="11-전략-비교-표">1.1 전략 비교 표</h3>

<table>
  <thead>
    <tr>
      <th>전략</th>
      <th>읽기 흐름</th>
      <th>쓰기 흐름</th>
      <th>장점</th>
      <th>단점</th>
      <th>적합한 상황</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Cache-Aside</strong></td>
      <td>캐시 조회 → miss 시 DB 조회 → 캐시 저장</td>
      <td>DB 직접 쓰기 → 캐시 무효화</td>
      <td>구현 단순, 필요한 데이터만 캐싱</td>
      <td>첫 요청 항상 miss, 일관성 위험</td>
      <td>읽기 비중 높은 일반 서비스</td>
    </tr>
    <tr>
      <td><strong>Read-Through</strong></td>
      <td>캐시에 위임 → miss 시 캐시가 DB 조회</td>
      <td>DB 직접 쓰기</td>
      <td>애플리케이션 코드 단순</td>
      <td>캐시 라이브러리 의존</td>
      <td>캐시 미들웨어 사용 시</td>
    </tr>
    <tr>
      <td><strong>Write-Through</strong></td>
      <td>캐시 조회</td>
      <td>캐시 쓰기 → 캐시가 DB에 동기 쓰기</td>
      <td>캐시-DB 항상 일관</td>
      <td>쓰기 지연 증가</td>
      <td>데이터 일관성이 중요할 때</td>
    </tr>
    <tr>
      <td><strong>Write-Behind</strong></td>
      <td>캐시 조회</td>
      <td>캐시 쓰기 → 비동기로 DB에 쓰기</td>
      <td>쓰기 성능 극대화</td>
      <td>데이터 유실 위험</td>
      <td>쓰기 빈도 높고 유실 허용 시</td>
    </tr>
  </tbody>
</table>

<h3 id="12-cache-aside-lazy-loading">1.2 Cache-Aside (Lazy Loading)</h3>

<p>가장 널리 사용되는 전략이다. 애플리케이션이 캐시를 직접 관리한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="nc">User</span> <span class="nf">getUser</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// 1. 캐시 조회</span>
    <span class="nc">String</span> <span class="n">cacheKey</span> <span class="o">=</span> <span class="s">"user:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">;</span>
    <span class="nc">User</span> <span class="n">cached</span> <span class="o">=</span> <span class="o">(</span><span class="nc">User</span><span class="o">)</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">cached</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">cached</span><span class="o">;</span> <span class="c1">// Cache Hit</span>
    <span class="o">}</span>

    <span class="c1">// 2. Cache Miss → DB 조회</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">userId</span><span class="o">)</span>
            <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">UserNotFoundException</span><span class="o">(</span><span class="n">userId</span><span class="o">));</span>

    <span class="c1">// 3. 캐시에 저장</span>
    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">,</span> <span class="n">user</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>
    <span class="k">return</span> <span class="n">user</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>쓰기 시 캐시 무효화:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="nc">User</span> <span class="nf">updateUser</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">UserUpdateRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">toEntity</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">request</span><span class="o">));</span>

    <span class="c1">// 캐시 삭제 (다음 읽기에서 최신 데이터로 갱신)</span>
    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="s">"user:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">user</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="13-write-through">1.3 Write-Through</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="nc">User</span> <span class="nf">updateUser</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">UserUpdateRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">toEntity</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">request</span><span class="o">));</span>

    <span class="c1">// DB 저장 직후 캐시도 동기적으로 업데이트</span>
    <span class="nc">String</span> <span class="n">cacheKey</span> <span class="o">=</span> <span class="s">"user:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">;</span>
    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">,</span> <span class="n">user</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>
    <span class="k">return</span> <span class="n">user</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="14-write-behind-write-back">1.4 Write-Behind (Write-Back)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kt">void</span> <span class="nf">updateUserAsync</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">UserUpdateRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">cacheKey</span> <span class="o">=</span> <span class="s">"user:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">;</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">toEntity</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">request</span><span class="o">);</span>

    <span class="c1">// 1. 캐시에 즉시 반영</span>
    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">,</span> <span class="n">user</span><span class="o">);</span>

    <span class="c1">// 2. 변경 사항을 큐에 등록 → 비동기로 DB에 쓰기</span>
    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForList</span><span class="o">().</span><span class="na">rightPush</span><span class="o">(</span><span class="s">"write-behind:queue"</span><span class="o">,</span>
            <span class="k">new</span> <span class="nf">WriteTask</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">,</span> <span class="n">user</span><span class="o">));</span>
<span class="o">}</span>

<span class="c1">// 별도 스케줄러가 큐를 소비하여 DB에 배치 저장</span>
<span class="nd">@Scheduled</span><span class="o">(</span><span class="n">fixedDelay</span> <span class="o">=</span> <span class="mi">5000</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">flushWriteBehindQueue</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">WriteTask</span><span class="o">&gt;</span> <span class="n">tasks</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>
    <span class="nc">WriteTask</span> <span class="n">task</span><span class="o">;</span>
    <span class="k">while</span> <span class="o">((</span><span class="n">task</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForList</span><span class="o">()</span>
            <span class="o">.</span><span class="na">leftPop</span><span class="o">(</span><span class="s">"write-behind:queue"</span><span class="o">))</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">tasks</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">task</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">if</span> <span class="o">(!</span><span class="n">tasks</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
        <span class="n">userRepository</span><span class="o">.</span><span class="na">batchUpdate</span><span class="o">(</span><span class="n">tasks</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="2-ttl-설계-전략-및-만료-패턴">2. TTL 설계 전략 및 만료 패턴</h2>

<h3 id="21-ttl-설계-가이드라인">2.1 TTL 설계 가이드라인</h3>

<table>
  <thead>
    <tr>
      <th>데이터 유형</th>
      <th>권장 TTL</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>세션 정보</td>
      <td>30분 ~ 2시간</td>
      <td>사용자 세션 라이프사이클</td>
    </tr>
    <tr>
      <td>사용자 프로필</td>
      <td>10분 ~ 1시간</td>
      <td>변경 빈도 낮음</td>
    </tr>
    <tr>
      <td>상품 목록</td>
      <td>5분 ~ 15분</td>
      <td>가격/재고 변동 반영</td>
    </tr>
    <tr>
      <td>인기 검색어 / 랭킹</td>
      <td>1분 ~ 5분</td>
      <td>실시간성 중요</td>
    </tr>
    <tr>
      <td>설정 / 코드 테이블</td>
      <td>1시간 ~ 24시간</td>
      <td>거의 변경되지 않음</td>
    </tr>
    <tr>
      <td>API Rate Limit 카운터</td>
      <td>1분 (sliding window)</td>
      <td>정확한 윈도우 필요</td>
    </tr>
  </tbody>
</table>

<h3 id="22-ttl-안티패턴">2.2 TTL 안티패턴</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="c1">// ❌ Bad: 모든 캐시에 동일한 TTL → 동시 만료 (Stampede 유발)</span>
<span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">value</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>

<span class="c1">// ✅ Good: TTL에 지터(jitter) 추가</span>
<span class="kt">long</span> <span class="n">baseTtl</span> <span class="o">=</span> <span class="mi">30</span> <span class="o">*</span> <span class="mi">60</span><span class="o">;</span> <span class="c1">// 30분</span>
<span class="kt">long</span> <span class="n">jitter</span> <span class="o">=</span> <span class="nc">ThreadLocalRandom</span><span class="o">.</span><span class="na">current</span><span class="o">().</span><span class="na">nextLong</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">5</span> <span class="o">*</span> <span class="mi">60</span><span class="o">);</span> <span class="c1">// 0~5분</span>
<span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">value</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="n">baseTtl</span> <span class="o">+</span> <span class="n">jitter</span><span class="o">));</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="23-만료-패턴">2.3 만료 패턴</h3>

<p><strong>Passive Expiration</strong>: 키에 접근할 때 만료 확인 → 삭제
<strong>Active Expiration</strong>: Redis가 주기적으로 만료된 키 샘플링 → 삭제</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c1">// Sliding TTL — 접근할 때마다 TTL 갱신</span>
<span class="kd">public</span> <span class="nc">User</span> <span class="nf">getUser</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">cacheKey</span> <span class="o">=</span> <span class="s">"user:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">;</span>
    <span class="nc">User</span> <span class="n">cached</span> <span class="o">=</span> <span class="o">(</span><span class="nc">User</span><span class="o">)</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">cached</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 접근 시 TTL 리셋</span>
        <span class="n">redisTemplate</span><span class="o">.</span><span class="na">expire</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>
        <span class="k">return</span> <span class="n">cached</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="c1">// ... DB 조회 및 캐시 저장</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="3-cache-stampede--thundering-herd-문제와-해결책">3. Cache Stampede / Thundering Herd 문제와 해결책</h2>

<h3 id="31-문제-정의">3.1 문제 정의</h3>

<p><strong>Cache Stampede</strong>: 인기 키가 만료되는 순간, 수백 개의 요청이 동시에 DB를 조회하는 현상</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre>시간 T: 인기 상품 캐시 만료
→ 요청 1: cache miss → DB 조회
→ 요청 2: cache miss → DB 조회
→ 요청 3: cache miss → DB 조회
→ ... 수백 요청이 동시에 DB hit
→ DB 과부하 / 장애
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="32-해결책-1-mutex-lock-분산-락">3.2 해결책 1: Mutex Lock (분산 락)</h3>

<p>오직 하나의 요청만 DB를 조회하고, 나머지는 대기한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="nc">User</span> <span class="nf">getUserWithLock</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">cacheKey</span> <span class="o">=</span> <span class="s">"user:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">;</span>
    <span class="nc">String</span> <span class="n">lockKey</span> <span class="o">=</span> <span class="s">"lock:"</span> <span class="o">+</span> <span class="n">cacheKey</span><span class="o">;</span>

    <span class="nc">User</span> <span class="n">cached</span> <span class="o">=</span> <span class="o">(</span><span class="nc">User</span><span class="o">)</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">cached</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">cached</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="c1">// 분산 락 획득 시도 (SETNX + TTL)</span>
    <span class="nc">Boolean</span> <span class="n">acquired</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">()</span>
            <span class="o">.</span><span class="na">setIfAbsent</span><span class="o">(</span><span class="n">lockKey</span><span class="o">,</span> <span class="s">"1"</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">10</span><span class="o">));</span>

    <span class="k">if</span> <span class="o">(</span><span class="nc">Boolean</span><span class="o">.</span><span class="na">TRUE</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">acquired</span><span class="o">))</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="c1">// 락 획득 성공 → DB 조회 후 캐시 갱신</span>
            <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">userId</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
            <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">,</span> <span class="n">user</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>
            <span class="k">return</span> <span class="n">user</span><span class="o">;</span>
        <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
            <span class="n">redisTemplate</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="n">lockKey</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
        <span class="c1">// 락 획득 실패 → 짧은 대기 후 재시도</span>
        <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">50</span><span class="o">);</span>
        <span class="k">return</span> <span class="nf">getUserWithLock</span><span class="o">(</span><span class="n">userId</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="33-해결책-2-probabilistic-early-expiration">3.3 해결책 2: Probabilistic Early Expiration</h3>

<p>캐시 만료 전에 확률적으로 미리 갱신한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="nc">User</span> <span class="nf">getUserWithEarlyExpire</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">cacheKey</span> <span class="o">=</span> <span class="s">"user:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">;</span>
    <span class="nc">CacheEntry</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="n">entry</span> <span class="o">=</span> <span class="o">(</span><span class="nc">CacheEntry</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;)</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">);</span>

    <span class="k">if</span> <span class="o">(</span><span class="n">entry</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">long</span> <span class="n">ttlRemaining</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">getExpire</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">,</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">SECONDS</span><span class="o">);</span>
        <span class="kt">long</span> <span class="n">delta</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="na">getComputeTime</span><span class="o">();</span> <span class="c1">// 원래 계산 소요 시간(초)</span>
        <span class="kt">double</span> <span class="n">beta</span> <span class="o">=</span> <span class="mf">1.0</span><span class="o">;</span>

        <span class="c1">// 남은 TTL이 짧을수록 갱신 확률 증가</span>
        <span class="kt">boolean</span> <span class="n">shouldRefresh</span> <span class="o">=</span> <span class="o">(</span><span class="n">delta</span> <span class="o">*</span> <span class="n">beta</span> <span class="o">*</span> <span class="nc">Math</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="nc">Math</span><span class="o">.</span><span class="na">random</span><span class="o">()))</span> <span class="o">*</span> <span class="o">-</span><span class="mi">1</span>
                <span class="o">&gt;=</span> <span class="n">ttlRemaining</span><span class="o">;</span>

        <span class="k">if</span> <span class="o">(!</span><span class="n">shouldRefresh</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">entry</span><span class="o">.</span><span class="na">getData</span><span class="o">();</span>
        <span class="o">}</span>
        <span class="c1">// 확률적으로 선택된 요청만 갱신 수행 (아래로 계속)</span>
    <span class="o">}</span>

    <span class="c1">// Cache miss 또는 갱신 대상 → DB 조회</span>
    <span class="kt">long</span> <span class="n">start</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">();</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">userId</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
    <span class="kt">long</span> <span class="n">computeTime</span> <span class="o">=</span> <span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">-</span> <span class="n">start</span><span class="o">)</span> <span class="o">/</span> <span class="mi">1000</span><span class="o">;</span>

    <span class="nc">CacheEntry</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="n">newEntry</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CacheEntry</span><span class="o">&lt;&gt;(</span><span class="n">user</span><span class="o">,</span> <span class="n">computeTime</span><span class="o">);</span>
    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">,</span> <span class="n">newEntry</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>
    <span class="k">return</span> <span class="n">user</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="34-해결책-3-백그라운드-갱신">3.4 해결책 3: 백그라운드 갱신</h3>

<p>인기 키를 TTL 만료 전에 백그라운드에서 주기적으로 갱신한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="nd">@Scheduled</span><span class="o">(</span><span class="n">fixedRate</span> <span class="o">=</span> <span class="mi">60_000</span><span class="o">)</span> <span class="c1">// 1분마다</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">refreshHotKeys</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">hotKeys</span> <span class="o">=</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"product:best-seller"</span><span class="o">,</span> <span class="s">"ranking:daily"</span><span class="o">);</span>
    <span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">key</span> <span class="o">:</span> <span class="n">hotKeys</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Object</span> <span class="n">freshData</span> <span class="o">=</span> <span class="n">loadFromDB</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
        <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">freshData</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">5</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="4-redis-자료구조-활용">4. Redis 자료구조 활용</h2>

<h3 id="41-언제-뭘-쓸지">4.1 언제 뭘 쓸지</h3>

<table>
  <thead>
    <tr>
      <th>자료구조</th>
      <th>용도</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>String</strong></td>
      <td>단순 K-V, 카운터, 세션</td>
      <td><code class="language-plaintext highlighter-rouge">user:123 → JSON</code>, <code class="language-plaintext highlighter-rouge">counter:page → 42</code></td>
    </tr>
    <tr>
      <td><strong>Hash</strong></td>
      <td>객체 필드별 접근</td>
      <td><code class="language-plaintext highlighter-rouge">user:123 → {name, email, age}</code></td>
    </tr>
    <tr>
      <td><strong>List</strong></td>
      <td>큐, 최근 기록</td>
      <td>최근 본 상품, 메시지 큐</td>
    </tr>
    <tr>
      <td><strong>Set</strong></td>
      <td>고유 집합, 태그</td>
      <td>좋아요 사용자 목록, 태그 필터</td>
    </tr>
    <tr>
      <td><strong>Sorted Set</strong></td>
      <td>랭킹, 스코어 기반 정렬</td>
      <td>리더보드, 인기 검색어</td>
    </tr>
  </tbody>
</table>

<h3 id="42-string--세션--카운터">4.2 String — 세션 &amp; 카운터</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c"># 세션 저장</span>
SET session:abc123 <span class="s1">'{"userId":"u1","role":"admin"}'</span> EX 1800

<span class="c"># 페이지 조회수 카운터</span>
INCR page:views:home
INCRBY page:views:home 5
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c1">// Spring Boot</span>
<span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="s">"session:"</span> <span class="o">+</span> <span class="n">sessionId</span><span class="o">,</span> <span class="n">sessionData</span><span class="o">,</span>
        <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>

<span class="nc">Long</span> <span class="n">views</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">increment</span><span class="o">(</span><span class="s">"page:views:"</span> <span class="o">+</span> <span class="n">pageId</span><span class="o">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="43-hash--객체-필드별-접근">4.3 Hash — 객체 필드별 접근</h3>

<p>전체 객체 대신 필요한 필드만 읽고 수정할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>HSET user:123 name <span class="s2">"홍길동"</span> email <span class="s2">"hong@example.com"</span> age 28
HGET user:123 name          <span class="c"># "홍길동"</span>
HINCRBY user:123 age 1      <span class="c"># 나이 1 증가</span>
HGETALL user:123             <span class="c"># 전체 필드</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nc">HashOperations</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">hashOps</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForHash</span><span class="o">();</span>
<span class="n">hashOps</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"user:123"</span><span class="o">,</span> <span class="s">"name"</span><span class="o">,</span> <span class="s">"홍길동"</span><span class="o">);</span>
<span class="n">hashOps</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"user:123"</span><span class="o">,</span> <span class="s">"email"</span><span class="o">,</span> <span class="s">"hong@example.com"</span><span class="o">);</span>

<span class="nc">String</span> <span class="n">name</span> <span class="o">=</span> <span class="n">hashOps</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"user:123"</span><span class="o">,</span> <span class="s">"name"</span><span class="o">);</span>
<span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">user</span> <span class="o">=</span> <span class="n">hashOps</span><span class="o">.</span><span class="na">entries</span><span class="o">(</span><span class="s">"user:123"</span><span class="o">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="44-set--좋아요-태그">4.4 Set — 좋아요, 태그</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>SADD post:456:likes user:1 user:2 user:3
SISMEMBER post:456:likes user:1    <span class="c"># 좋아요 여부: 1 (true)</span>
SCARD post:456:likes               <span class="c"># 좋아요 수: 3</span>
SINTER post:456:likes post:789:likes  <span class="c"># 두 게시물 모두 좋아요한 사용자</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="nc">SetOperations</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">setOps</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForSet</span><span class="o">();</span>
<span class="n">setOps</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="s">"post:456:likes"</span><span class="o">,</span> <span class="s">"user:1"</span><span class="o">,</span> <span class="s">"user:2"</span><span class="o">);</span>

<span class="nc">Boolean</span> <span class="n">isLiked</span> <span class="o">=</span> <span class="n">setOps</span><span class="o">.</span><span class="na">isMember</span><span class="o">(</span><span class="s">"post:456:likes"</span><span class="o">,</span> <span class="s">"user:1"</span><span class="o">);</span>
<span class="nc">Long</span> <span class="n">likeCount</span> <span class="o">=</span> <span class="n">setOps</span><span class="o">.</span><span class="na">size</span><span class="o">(</span><span class="s">"post:456:likes"</span><span class="o">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="45-sorted-set--리더보드">4.5 Sorted Set — 리더보드</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>ZADD leaderboard 1500 <span class="s2">"player:A"</span> 2300 <span class="s2">"player:B"</span> 1800 <span class="s2">"player:C"</span>
ZREVRANGE leaderboard 0 2 WITHSCORES   <span class="c"># 상위 3명 (높은 점수순)</span>
ZRANK leaderboard <span class="s2">"player:A"</span>            <span class="c"># 순위 조회</span>
ZINCRBY leaderboard 200 <span class="s2">"player:A"</span>      <span class="c"># 점수 증가</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="nc">ZSetOperations</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">zSetOps</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">();</span>
<span class="n">zSetOps</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="s">"leaderboard"</span><span class="o">,</span> <span class="s">"player:A"</span><span class="o">,</span> <span class="mi">1500</span><span class="o">);</span>
<span class="n">zSetOps</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="s">"leaderboard"</span><span class="o">,</span> <span class="s">"player:B"</span><span class="o">,</span> <span class="mi">2300</span><span class="o">);</span>

<span class="c1">// 상위 10명</span>
<span class="nc">Set</span><span class="o">&lt;</span><span class="nc">ZSetOperations</span><span class="o">.</span><span class="na">TypedTuple</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;&gt;</span> <span class="n">top10</span> <span class="o">=</span>
        <span class="n">zSetOps</span><span class="o">.</span><span class="na">reverseRangeWithScores</span><span class="o">(</span><span class="s">"leaderboard"</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">9</span><span class="o">);</span>

<span class="c1">// 점수 증가</span>
<span class="n">zSetOps</span><span class="o">.</span><span class="na">incrementScore</span><span class="o">(</span><span class="s">"leaderboard"</span><span class="o">,</span> <span class="s">"player:A"</span><span class="o">,</span> <span class="mi">200</span><span class="o">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="5-spring-boot--redis-연동">5. Spring Boot + Redis 연동</h2>

<h3 id="51-의존성-및-설정">5.1 의존성 및 설정</h3>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c1">// build.gradle</span>
<span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-data-redis'</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="c1"># application.yml</span>
<span class="na">spring</span><span class="pi">:</span>
  <span class="na">data</span><span class="pi">:</span>
    <span class="na">redis</span><span class="pi">:</span>
      <span class="na">host</span><span class="pi">:</span> <span class="s">localhost</span>
      <span class="na">port</span><span class="pi">:</span> <span class="m">6379</span>
      <span class="na">password</span><span class="pi">:</span> <span class="s">mypassword</span>
      <span class="na">lettuce</span><span class="pi">:</span>
        <span class="na">pool</span><span class="pi">:</span>
          <span class="na">max-active</span><span class="pi">:</span> <span class="m">8</span>
          <span class="na">max-idle</span><span class="pi">:</span> <span class="m">8</span>
          <span class="na">min-idle</span><span class="pi">:</span> <span class="m">2</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="52-redistemplate-설정">5.2 RedisTemplate 설정</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RedisConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RedisTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="nf">redisTemplate</span><span class="o">(</span>
            <span class="nc">RedisConnectionFactory</span> <span class="n">connectionFactory</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">RedisTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">template</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RedisTemplate</span><span class="o">&lt;&gt;();</span>
        <span class="n">template</span><span class="o">.</span><span class="na">setConnectionFactory</span><span class="o">(</span><span class="n">connectionFactory</span><span class="o">);</span>

        <span class="c1">// Key: String, Value: JSON 직렬화</span>
        <span class="n">template</span><span class="o">.</span><span class="na">setKeySerializer</span><span class="o">(</span><span class="k">new</span> <span class="nc">StringRedisSerializer</span><span class="o">());</span>
        <span class="n">template</span><span class="o">.</span><span class="na">setValueSerializer</span><span class="o">(</span>
                <span class="k">new</span> <span class="nf">GenericJackson2JsonRedisSerializer</span><span class="o">());</span>

        <span class="n">template</span><span class="o">.</span><span class="na">setHashKeySerializer</span><span class="o">(</span><span class="k">new</span> <span class="nc">StringRedisSerializer</span><span class="o">());</span>
        <span class="n">template</span><span class="o">.</span><span class="na">setHashValueSerializer</span><span class="o">(</span>
                <span class="k">new</span> <span class="nf">GenericJackson2JsonRedisSerializer</span><span class="o">());</span>

        <span class="k">return</span> <span class="n">template</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="53-cacheable-기반-선언적-캐싱">5.3 @Cacheable 기반 선언적 캐싱</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
</pre></td><td class="rouge-code"><pre><span class="nd">@Configuration</span>
<span class="nd">@EnableCaching</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CacheConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RedisCacheManager</span> <span class="nf">cacheManager</span><span class="o">(</span>
            <span class="nc">RedisConnectionFactory</span> <span class="n">connectionFactory</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">RedisCacheConfiguration</span> <span class="n">config</span> <span class="o">=</span> <span class="nc">RedisCacheConfiguration</span>
                <span class="o">.</span><span class="na">defaultCacheConfig</span><span class="o">()</span>
                <span class="o">.</span><span class="na">entryTtl</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">))</span>
                <span class="o">.</span><span class="na">serializeKeysWith</span><span class="o">(</span>
                        <span class="nc">SerializationPair</span><span class="o">.</span><span class="na">fromSerializer</span><span class="o">(</span>
                                <span class="k">new</span> <span class="nf">StringRedisSerializer</span><span class="o">()))</span>
                <span class="o">.</span><span class="na">serializeValuesWith</span><span class="o">(</span>
                        <span class="nc">SerializationPair</span><span class="o">.</span><span class="na">fromSerializer</span><span class="o">(</span>
                                <span class="k">new</span> <span class="nf">GenericJackson2JsonRedisSerializer</span><span class="o">()))</span>
                <span class="o">.</span><span class="na">disableCachingNullValues</span><span class="o">();</span>

        <span class="c1">// 캐시별 TTL 커스터마이징</span>
        <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">RedisCacheConfiguration</span><span class="o">&gt;</span> <span class="n">perCacheConfig</span> <span class="o">=</span> <span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
                <span class="s">"users"</span><span class="o">,</span> <span class="n">config</span><span class="o">.</span><span class="na">entryTtl</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">60</span><span class="o">)),</span>
                <span class="s">"products"</span><span class="o">,</span> <span class="n">config</span><span class="o">.</span><span class="na">entryTtl</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">10</span><span class="o">)),</span>
                <span class="s">"rankings"</span><span class="o">,</span> <span class="n">config</span><span class="o">.</span><span class="na">entryTtl</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">1</span><span class="o">))</span>
        <span class="o">);</span>

        <span class="k">return</span> <span class="nc">RedisCacheManager</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="n">connectionFactory</span><span class="o">)</span>
                <span class="o">.</span><span class="na">cacheDefaults</span><span class="o">(</span><span class="n">config</span><span class="o">)</span>
                <span class="o">.</span><span class="na">withInitialCacheConfigurations</span><span class="o">(</span><span class="n">perCacheConfig</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserService</span> <span class="o">{</span>

    <span class="nd">@Cacheable</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"users"</span><span class="o">,</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"#userId"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">User</span> <span class="nf">getUser</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Cache miss 시에만 실행</span>
        <span class="k">return</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">userId</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@CachePut</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"users"</span><span class="o">,</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"#userId"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">User</span> <span class="nf">updateUser</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">UserUpdateRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 항상 실행, 결과를 캐시에 저장</span>
        <span class="k">return</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">toEntity</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">request</span><span class="o">));</span>
    <span class="o">}</span>

    <span class="nd">@CacheEvict</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"users"</span><span class="o">,</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"#userId"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">deleteUser</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 실행 후 캐시에서 삭제</span>
        <span class="n">userRepository</span><span class="o">.</span><span class="na">deleteById</span><span class="o">(</span><span class="n">userId</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@CacheEvict</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"users"</span><span class="o">,</span> <span class="n">allEntries</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">clearAllUserCache</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// 모든 users 캐시 삭제</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="6-캐시-일관성-문제-cache-invalidation-전략">6. 캐시 일관성 문제 (Cache Invalidation 전략)</h2>

<blockquote>
  <p><em>“There are only two hard things in Computer Science: cache invalidation and naming things.”</em>
— Phil Karlton</p>
</blockquote>

<h3 id="61-일관성-문제-시나리오">6.1 일관성 문제 시나리오</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre>Thread A: DB 업데이트 (v2) → 캐시 삭제 예정
Thread B:                      캐시 miss → DB 조회 (v2) → 캐시 저장 (v2)
Thread A:                                                    캐시 삭제!
→ 캐시 비어 있음 → 다음 요청에서 다시 DB 조회 (괜찮음)

그러나 타이밍이 꼬이면:
Thread A: DB 업데이트 (v2)
Thread B:                   캐시 miss → DB 조회 (v1, 아직 커밋 안 됨)
Thread A: 캐시 삭제
Thread B:                   캐시 저장 (v1) ← 오래된 데이터!
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="62-전략-1-delete-after-write-기본">6.2 전략 1: Delete After Write (기본)</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kt">void</span> <span class="nf">updateUser</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">UserUpdateRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">userRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">toEntity</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">request</span><span class="o">));</span>
    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="s">"user:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">);</span> <span class="c1">// 쓰기 후 삭제</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>단순하지만 위 경합 상황에서 stale 데이터 가능성이 있다.</p>

<h3 id="63-전략-2-짧은-ttl--삭제">6.3 전략 2: 짧은 TTL + 삭제</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kt">void</span> <span class="nf">updateUser</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">UserUpdateRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">userRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">toEntity</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">request</span><span class="o">));</span>
    <span class="c1">// 즉시 삭제 대신 매우 짧은 TTL로 교체 → 경합 윈도우 최소화</span>
    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">expire</span><span class="o">(</span><span class="s">"user:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">1</span><span class="o">));</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="64-전략-3-버전-기반-무효화">6.4 전략 3: 버전 기반 무효화</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kt">void</span> <span class="nf">updateUser</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">UserUpdateRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">toEntity</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">request</span><span class="o">));</span>

    <span class="c1">// 버전 번호로 캐시 키 분리</span>
    <span class="nc">String</span> <span class="n">versionKey</span> <span class="o">=</span> <span class="s">"user:version:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">;</span>
    <span class="nc">Long</span> <span class="n">newVersion</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">increment</span><span class="o">(</span><span class="n">versionKey</span><span class="o">);</span>

    <span class="nc">String</span> <span class="n">cacheKey</span> <span class="o">=</span> <span class="s">"user:"</span> <span class="o">+</span> <span class="n">userId</span> <span class="o">+</span> <span class="s">":v"</span> <span class="o">+</span> <span class="n">newVersion</span><span class="o">;</span>
    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">,</span> <span class="n">user</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="65-전략-4-cdc-change-data-capture-기반">6.5 전략 4: CDC (Change Data Capture) 기반</h3>

<p>데이터 변경을 이벤트로 발행하여 캐시를 비동기 갱신한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>DB 변경 → Debezium (CDC) → Kafka → Cache Updater → Redis 갱신
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="nd">@KafkaListener</span><span class="o">(</span><span class="n">topics</span> <span class="o">=</span> <span class="s">"dbserver.public.users"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">onUserChange</span><span class="o">(</span><span class="nc">ConsumerRecord</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">record</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">UserChangeEvent</span> <span class="n">event</span> <span class="o">=</span> <span class="n">objectMapper</span><span class="o">.</span><span class="na">readValue</span><span class="o">(</span>
            <span class="n">record</span><span class="o">.</span><span class="na">value</span><span class="o">(),</span> <span class="nc">UserChangeEvent</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="nc">String</span> <span class="n">cacheKey</span> <span class="o">=</span> <span class="s">"user:"</span> <span class="o">+</span> <span class="n">event</span><span class="o">.</span><span class="na">getUserId</span><span class="o">();</span>

    <span class="k">if</span> <span class="o">(</span><span class="s">"DELETE"</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getOperation</span><span class="o">()))</span> <span class="o">{</span>
        <span class="n">redisTemplate</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
        <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">cacheKey</span><span class="o">,</span>
                <span class="n">event</span><span class="o">.</span><span class="na">getAfter</span><span class="o">(),</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="66-전략-선택-가이드">6.6 전략 선택 가이드</h3>

<table>
  <thead>
    <tr>
      <th>상황</th>
      <th>권장 전략</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>단순 CRUD, 약간의 stale 허용</td>
      <td>Delete After Write + 짧은 TTL</td>
    </tr>
    <tr>
      <td>높은 일관성 필요</td>
      <td>Write-Through 또는 버전 기반</td>
    </tr>
    <tr>
      <td>대규모 분산 시스템</td>
      <td>CDC 기반 비동기 무효화</td>
    </tr>
    <tr>
      <td>읽기 극단적 부하</td>
      <td>Cache-Aside + Mutex Lock + Jitter TTL</td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<h2 id="정리">정리</h2>

<table>
  <thead>
    <tr>
      <th>개념</th>
      <th>핵심</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Cache-Aside</td>
      <td>가장 범용적, 애플리케이션이 캐시 직접 관리</td>
    </tr>
    <tr>
      <td>Write-Through / Behind</td>
      <td>쓰기 일관성 vs 쓰기 성능 트레이드오프</td>
    </tr>
    <tr>
      <td>TTL Jitter</td>
      <td>동시 만료 방지의 핵심</td>
    </tr>
    <tr>
      <td>Cache Stampede 방지</td>
      <td>Mutex Lock 또는 확률적 조기 갱신</td>
    </tr>
    <tr>
      <td>Redis 자료구조</td>
      <td>데이터 특성에 맞는 자료구조 선택이 성능 좌우</td>
    </tr>
    <tr>
      <td>@Cacheable</td>
      <td>Spring의 선언적 캐싱으로 보일러플레이트 제거</td>
    </tr>
    <tr>
      <td>Cache Invalidation</td>
      <td>삭제 + 짧은 TTL이 실용적, 대규모는 CDC</td>
    </tr>
  </tbody>
</table>

<p>캐싱은 단순히 “Redis에 저장”이 아니라, <strong>데이터 특성에 맞는 전략을 선택하고 일관성과 성능 사이의 트레이드오프를 관리</strong>하는 것이다.</p>

<p><br /></p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://redis.io/docs/">Redis Documentation</a></li>
  <li><a href="https://redis.io/docs/latest/develop/use/client-side-caching/">Redis Caching — Redis Best Practices</a></li>
  <li><a href="https://redis.io/docs/latest/develop/data-types/">Redis Data Types</a></li>
  <li><a href="https://docs.spring.io/spring-data/redis/reference/">Spring Data Redis — Reference Documentation</a></li>
  <li><a href="https://docs.spring.io/spring-framework/reference/integration/cache.html">Cache Abstraction — Spring Framework Documentation</a></li>
  <li><a href="https://cseweb.ucsd.edu/~avattani/papers/cache_stampede.pdf">Optimal Probabilistic Cache Stampede Prevention — XFetch Paper</a></li>
</ul>
]]></content:encoded>
        <pubDate>Thu, 02 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/backend/2026/04/02/redis-caching-strategy/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/backend/2026/04/02/redis-caching-strategy/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Redis</category>
        
        <category>Caching</category>
        
        <category>Backend</category>
        
        <category>Performance</category>
        
        
        <category>backend</category>
        
      </item>
    
      <item>
        <title>API Rate Limiting — 설계와 구현 전략</title>
        <description>Rate Limiting이 필요한 이유</description>
        <content:encoded><![CDATA[<h2 id="rate-limiting이-필요한-이유">Rate Limiting이 필요한 이유</h2>

<p>공개 API를 운영하면 예상치 못한 트래픽 폭주를 경험하게 된다. 의도적인 DDoS 공격이 아니더라도, 클라이언트의 버그나 잘못된 재시도 로직만으로 서버가 과부하에 빠질 수 있다.</p>

<p>Rate Limiting은 세 가지 문제를 해결한다.</p>

<ol>
  <li><strong>서비스 보호</strong>: 과도한 요청으로부터 서버 리소스를 보호한다.</li>
  <li><strong>공정한 사용</strong>: 특정 사용자가 리소스를 독점하지 못하게 한다.</li>
  <li><strong>비용 제어</strong>: 클라우드 환경에서 불필요한 트래픽으로 인한 비용 폭증을 방지한다.</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>정상 트래픽:   ████████░░░░░░░  (60% 용량)  ✓ 안정
트래픽 폭주:   ████████████████████████████  (200% 용량)  ✗ 장애
Rate Limit:   ████████████████░░░░░░░░░░░  (100% 이하 유지)  ✓ 안정
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="알고리즘-비교">알고리즘 비교</h2>

<h3 id="token-bucket">Token Bucket</h3>

<p>버킷에 일정 속도로 토큰이 채워진다. 요청이 들어오면 토큰을 소비한다. 토큰이 없으면 요청을 거부한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre>[버킷 용량: 10, 충전 속도: 1개/초]

t=0   토큰: 10  → 요청 5개 처리 → 토큰: 5
t=1   토큰: 6   → 요청 2개 처리 → 토큰: 4
t=2   토큰: 5   → 요청 0개      → 토큰: 5 (최대 10까지만 충전)
t=3   토큰: 6   → 요청 8개 시도 → 6개 처리, 2개 거부
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>특징</strong>: 버스트 트래픽을 허용한다. 버킷에 토큰이 쌓여 있으면 일시적으로 높은 트래픽을 처리할 수 있다. Amazon API Gateway, Stripe 등 많은 서비스에서 사용한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kd">class</span> <span class="nc">TokenBucket</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">maxTokens</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="kt">double</span> <span class="n">refillRate</span><span class="o">;</span>    <span class="c1">// tokens per second</span>
    <span class="kd">private</span> <span class="kt">double</span> <span class="n">currentTokens</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">long</span> <span class="n">lastRefillTime</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">TokenBucket</span><span class="o">(</span><span class="kt">int</span> <span class="n">maxTokens</span><span class="o">,</span> <span class="kt">double</span> <span class="n">refillRate</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">maxTokens</span> <span class="o">=</span> <span class="n">maxTokens</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">refillRate</span> <span class="o">=</span> <span class="n">refillRate</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">currentTokens</span> <span class="o">=</span> <span class="n">maxTokens</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">lastRefillTime</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">nanoTime</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">synchronized</span> <span class="kt">boolean</span> <span class="nf">tryConsume</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">refill</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">currentTokens</span> <span class="o">&gt;=</span> <span class="mi">1</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">currentTokens</span> <span class="o">-=</span> <span class="mi">1</span><span class="o">;</span>
            <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">refill</span><span class="o">()</span> <span class="o">{</span>
        <span class="kt">long</span> <span class="n">now</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">nanoTime</span><span class="o">();</span>
        <span class="kt">double</span> <span class="n">elapsed</span> <span class="o">=</span> <span class="o">(</span><span class="n">now</span> <span class="o">-</span> <span class="n">lastRefillTime</span><span class="o">)</span> <span class="o">/</span> <span class="mf">1_000_000_000.0</span><span class="o">;</span>
        <span class="n">currentTokens</span> <span class="o">=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">min</span><span class="o">(</span><span class="n">maxTokens</span><span class="o">,</span> <span class="n">currentTokens</span> <span class="o">+</span> <span class="n">elapsed</span> <span class="o">*</span> <span class="n">refillRate</span><span class="o">);</span>
        <span class="n">lastRefillTime</span> <span class="o">=</span> <span class="n">now</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="leaky-bucket">Leaky Bucket</h3>

<p>요청이 버킷에 들어가고, 일정 속도로 빠져나간다. 버킷이 가득 차면 새 요청은 버려진다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>[버킷 용량: 10, 유출 속도: 2개/초]

요청 유입:  ████████████████  (버스트)
버킷 내부:  ██████████        (최대 10개 대기)
유출(처리): ██  ██  ██  ██    (일정 속도로 처리)
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>특징</strong>: 유출 속도가 일정하므로 트래픽을 평탄화(smoothing)한다. 버스트를 허용하지 않고 균일한 처리율을 보장해야 할 때 적합하다.</p>

<h3 id="fixed-window-counter">Fixed Window Counter</h3>

<p>시간 윈도우(예: 1분)를 고정하고, 윈도우 내 요청 수를 카운트한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>[1분당 100개 제한]

12:00:00 ~ 12:00:59  → 카운트: 0 ... 100 → 100 이후 거부
12:01:00 ~ 12:01:59  → 카운트: 0 (리셋)
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>문제점</strong>: 윈도우 경계에서 버스트가 발생할 수 있다. 12:00:55에 100개, 12:01:00에 100개가 들어오면 10초 사이에 200개가 처리된다.</p>

<h3 id="sliding-window-log">Sliding Window Log</h3>

<p>각 요청의 타임스탬프를 기록하고, 현재 시점에서 윈도우 크기만큼 뒤로 가서 요청 수를 센다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kd">class</span> <span class="nc">SlidingWindowLog</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">maxRequests</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="kt">long</span> <span class="n">windowSizeMs</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">LinkedList</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">requestLog</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedList</span><span class="o">&lt;&gt;();</span>

    <span class="kd">public</span> <span class="nf">SlidingWindowLog</span><span class="o">(</span><span class="kt">int</span> <span class="n">maxRequests</span><span class="o">,</span> <span class="kt">long</span> <span class="n">windowSizeMs</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">maxRequests</span> <span class="o">=</span> <span class="n">maxRequests</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">windowSizeMs</span> <span class="o">=</span> <span class="n">windowSizeMs</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">synchronized</span> <span class="kt">boolean</span> <span class="nf">tryAcquire</span><span class="o">()</span> <span class="o">{</span>
        <span class="kt">long</span> <span class="n">now</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">();</span>
        <span class="kt">long</span> <span class="n">windowStart</span> <span class="o">=</span> <span class="n">now</span> <span class="o">-</span> <span class="n">windowSizeMs</span><span class="o">;</span>

        <span class="c1">// 윈도우 밖의 오래된 기록 제거</span>
        <span class="k">while</span> <span class="o">(!</span><span class="n">requestLog</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">()</span> <span class="o">&amp;&amp;</span> <span class="n">requestLog</span><span class="o">.</span><span class="na">peekFirst</span><span class="o">()</span> <span class="o">&lt;=</span> <span class="n">windowStart</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">requestLog</span><span class="o">.</span><span class="na">pollFirst</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">requestLog</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">&lt;</span> <span class="n">maxRequests</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">requestLog</span><span class="o">.</span><span class="na">addLast</span><span class="o">(</span><span class="n">now</span><span class="o">);</span>
            <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>특징</strong>: 경계 문제가 없어 가장 정확하지만, 요청마다 타임스탬프를 저장하므로 메모리 사용량이 높다.</p>

<h3 id="sliding-window-counter">Sliding Window Counter</h3>

<p>Fixed Window와 Sliding Window Log의 절충안이다. 이전 윈도우와 현재 윈도우의 카운트를 가중 평균으로 계산한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>[1분당 100개 제한, 현재 12:00:45]

이전 윈도우(11:59:00~11:59:59) 카운트: 80
현재 윈도우(12:00:00~12:00:59) 카운트: 30

가중치 = (60 - 45) / 60 = 0.25
예상 카운트 = 80 * 0.25 + 30 = 50  → 허용
</pre></td></tr></tbody></table></code></pre></div></div>

<p>메모리 효율적이면서 경계 문제를 완화한다. 실무에서 가장 많이 채택되는 방식이다.</p>

<hr />

<h2 id="분산-환경에서의-rate-limiting">분산 환경에서의 Rate Limiting</h2>

<p>서버가 여러 대인 분산 환경에서는 각 서버가 독립적으로 카운트하면 전체 제한이 깨진다. <strong>Redis</strong>를 중앙 저장소로 활용해 분산 Rate Limiting을 구현한다.</p>

<h3 id="redis-기반-fixed-window-구현">Redis 기반 Fixed Window 구현</h3>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="c1">-- rate_limit.lua</span>
<span class="kd">local</span> <span class="n">key</span> <span class="o">=</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="kd">local</span> <span class="n">limit</span> <span class="o">=</span> <span class="nb">tonumber</span><span class="p">(</span><span class="n">ARGV</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
<span class="kd">local</span> <span class="n">window</span> <span class="o">=</span> <span class="nb">tonumber</span><span class="p">(</span><span class="n">ARGV</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span>

<span class="kd">local</span> <span class="n">current</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s1">'INCR'</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span>

<span class="k">if</span> <span class="n">current</span> <span class="o">==</span> <span class="mi">1</span> <span class="k">then</span>
    <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s1">'EXPIRE'</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">window</span><span class="p">)</span>
<span class="k">end</span>

<span class="k">if</span> <span class="n">current</span> <span class="o">&gt;</span> <span class="n">limit</span> <span class="k">then</span>
    <span class="k">return</span> <span class="mi">0</span>    <span class="c1">-- 거부</span>
<span class="k">else</span>
    <span class="k">return</span> <span class="mi">1</span>    <span class="c1">-- 허용</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre><span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RedisRateLimiter</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RedisTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">redisTemplate</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RedisScript</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">rateLimitScript</span><span class="o">;</span>

    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isAllowed</span><span class="o">(</span><span class="nc">String</span> <span class="n">clientId</span><span class="o">,</span> <span class="kt">int</span> <span class="n">limit</span><span class="o">,</span> <span class="kt">int</span> <span class="n">windowSeconds</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"rate_limit:"</span> <span class="o">+</span> <span class="n">clientId</span> <span class="o">+</span> <span class="s">":"</span> <span class="o">+</span> <span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">/</span> <span class="o">(</span><span class="n">windowSeconds</span> <span class="o">*</span> <span class="mi">1000</span><span class="o">));</span>

        <span class="nc">Long</span> <span class="n">result</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span>
            <span class="n">rateLimitScript</span><span class="o">,</span>
            <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">key</span><span class="o">),</span>
            <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">limit</span><span class="o">),</span>
            <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">windowSeconds</span><span class="o">)</span>
        <span class="o">);</span>

        <span class="k">return</span> <span class="n">result</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">result</span> <span class="o">==</span> <span class="mi">1</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="redis-기반-sliding-window-구현">Redis 기반 Sliding Window 구현</h3>

<p>Sorted Set을 활용하면 Sliding Window를 구현할 수 있다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isAllowedSlidingWindow</span><span class="o">(</span><span class="nc">String</span> <span class="n">clientId</span><span class="o">,</span> <span class="kt">int</span> <span class="n">limit</span><span class="o">,</span> <span class="kt">int</span> <span class="n">windowSeconds</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"rate_limit:sliding:"</span> <span class="o">+</span> <span class="n">clientId</span><span class="o">;</span>
    <span class="kt">long</span> <span class="n">now</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">();</span>
    <span class="kt">long</span> <span class="n">windowStart</span> <span class="o">=</span> <span class="n">now</span> <span class="o">-</span> <span class="o">(</span><span class="n">windowSeconds</span> <span class="o">*</span> <span class="mi">1000L</span><span class="o">);</span>

    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="k">new</span> <span class="nc">SessionCallback</span><span class="o">&lt;&gt;()</span> <span class="o">{</span>
        <span class="nd">@Override</span>
        <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">execute</span><span class="o">(</span><span class="nc">RedisOperations</span> <span class="n">operations</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">operations</span><span class="o">.</span><span class="na">multi</span><span class="o">();</span>
            <span class="c1">// 윈도우 밖의 오래된 항목 제거</span>
            <span class="n">operations</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">().</span><span class="na">removeRangeByScore</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">windowStart</span><span class="o">);</span>
            <span class="c1">// 현재 요청 추가</span>
            <span class="n">operations</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">().</span><span class="na">add</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="no">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">(),</span> <span class="n">now</span><span class="o">);</span>
            <span class="c1">// 현재 윈도우 내 요청 수 조회</span>
            <span class="n">operations</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">().</span><span class="na">zCard</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
            <span class="c1">// TTL 설정</span>
            <span class="n">operations</span><span class="o">.</span><span class="na">expire</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="n">windowSeconds</span><span class="o">));</span>
            <span class="k">return</span> <span class="n">operations</span><span class="o">.</span><span class="na">exec</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">});</span>

    <span class="nc">Long</span> <span class="n">count</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForZSet</span><span class="o">().</span><span class="na">zCard</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">count</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">count</span> <span class="o">&lt;=</span> <span class="n">limit</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="spring-boot--redis-rate-limiter-구현">Spring Boot + Redis Rate Limiter 구현</h2>

<h3 id="bucket4j-라이브러리-활용">Bucket4j 라이브러리 활용</h3>

<p>Bucket4j는 Token Bucket 알고리즘을 구현한 Java 라이브러리다. Redis 백엔드를 지원한다.</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c1">// build.gradle</span>
<span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s1">'com.bucket4j:bucket4j-core:8.7.0'</span>
    <span class="n">implementation</span> <span class="s1">'com.bucket4j:bucket4j-redis:8.7.0'</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RateLimitConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">ProxyManager</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">proxyManager</span><span class="o">(</span><span class="nc">RedisConnectionFactory</span> <span class="n">factory</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">LettuceBasedProxyManager</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">proxyManager</span> <span class="o">=</span> <span class="nc">LettuceBasedProxyManager</span>
            <span class="o">.</span><span class="na">builderFor</span><span class="o">(</span><span class="nc">RedisClient</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="s">"redis://localhost:6379"</span><span class="o">))</span>
            <span class="o">.</span><span class="na">withExpirationStrategy</span><span class="o">(</span>
                <span class="nc">ExpirationAfterWriteStrategy</span><span class="o">.</span><span class="na">basedOnTimeForRefillingBucketUpToMax</span><span class="o">(</span>
                    <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">1</span><span class="o">)))</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">proxyManager</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
</pre></td><td class="rouge-code"><pre><span class="nd">@Component</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RateLimitInterceptor</span> <span class="kd">implements</span> <span class="nc">HandlerInterceptor</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ProxyManager</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">proxyManager</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">BucketConfiguration</span> <span class="no">BUCKET_CONFIG</span> <span class="o">=</span> <span class="nc">BucketConfiguration</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
        <span class="o">.</span><span class="na">addLimit</span><span class="o">(</span><span class="nc">Bandwidth</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
            <span class="o">.</span><span class="na">capacity</span><span class="o">(</span><span class="mi">100</span><span class="o">)</span>
            <span class="o">.</span><span class="na">refillGreedy</span><span class="o">(</span><span class="mi">100</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">1</span><span class="o">))</span>
            <span class="o">.</span><span class="na">build</span><span class="o">())</span>
        <span class="o">.</span><span class="na">build</span><span class="o">();</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">preHandle</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span>
                             <span class="nc">Object</span> <span class="n">handler</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">clientId</span> <span class="o">=</span> <span class="n">resolveClientId</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
        <span class="nc">BucketProxy</span> <span class="n">bucket</span> <span class="o">=</span> <span class="n">proxyManager</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
            <span class="o">.</span><span class="na">build</span><span class="o">(</span><span class="n">clientId</span><span class="o">,</span> <span class="o">()</span> <span class="o">-&gt;</span> <span class="no">BUCKET_CONFIG</span><span class="o">);</span>

        <span class="nc">ConsumptionProbe</span> <span class="n">probe</span> <span class="o">=</span> <span class="n">bucket</span><span class="o">.</span><span class="na">tryConsumeAndReturnRemaining</span><span class="o">(</span><span class="mi">1</span><span class="o">);</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">probe</span><span class="o">.</span><span class="na">isConsumed</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">response</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"X-Rate-Limit-Remaining"</span><span class="o">,</span>
                <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">probe</span><span class="o">.</span><span class="na">getRemainingTokens</span><span class="o">()));</span>
            <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
            <span class="n">response</span><span class="o">.</span><span class="na">setStatus</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">TOO_MANY_REQUESTS</span><span class="o">.</span><span class="na">value</span><span class="o">());</span>
            <span class="n">response</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"Retry-After"</span><span class="o">,</span>
                <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">probe</span><span class="o">.</span><span class="na">getNanosToWaitForRefill</span><span class="o">()</span> <span class="o">/</span> <span class="mi">1_000_000_000</span><span class="o">));</span>
            <span class="n">response</span><span class="o">.</span><span class="na">getWriter</span><span class="o">().</span><span class="na">write</span><span class="o">(</span><span class="sh">"""
                {"error": "Too Many Requests", "message": "Rate limit exceeded. Please try again later."}
                """</span><span class="o">);</span>
            <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="nf">resolveClientId</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// API Key 우선, 없으면 IP 기반</span>
        <span class="nc">String</span> <span class="n">apiKey</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="s">"X-API-Key"</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">apiKey</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">apiKey</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">return</span> <span class="s">"api_key:"</span> <span class="o">+</span> <span class="n">apiKey</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="s">"ip:"</span> <span class="o">+</span> <span class="n">request</span><span class="o">.</span><span class="na">getRemoteAddr</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="커스텀-어노테이션-기반-구현">커스텀 어노테이션 기반 구현</h3>

<p>더 세밀한 제어를 위해 어노테이션 기반으로 구현할 수도 있다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="nd">@Target</span><span class="o">(</span><span class="nc">ElementType</span><span class="o">.</span><span class="na">METHOD</span><span class="o">)</span>
<span class="nd">@Retention</span><span class="o">(</span><span class="nc">RetentionPolicy</span><span class="o">.</span><span class="na">RUNTIME</span><span class="o">)</span>
<span class="kd">public</span> <span class="nd">@interface</span> <span class="nc">RateLimit</span> <span class="o">{</span>
    <span class="kt">int</span> <span class="nf">requests</span><span class="o">()</span> <span class="k">default</span> <span class="mi">100</span><span class="o">;</span>
    <span class="kt">int</span> <span class="nf">windowSeconds</span><span class="o">()</span> <span class="k">default</span> <span class="mi">60</span><span class="o">;</span>
    <span class="nc">String</span> <span class="nf">key</span><span class="o">()</span> <span class="k">default</span> <span class="s">""</span><span class="o">;</span>    <span class="c1">// SpEL 지원</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
</pre></td><td class="rouge-code"><pre><span class="nd">@Aspect</span>
<span class="nd">@Component</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RateLimitAspect</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RedisTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">redisTemplate</span><span class="o">;</span>

    <span class="nd">@Around</span><span class="o">(</span><span class="s">"@annotation(rateLimit)"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">enforce</span><span class="o">(</span><span class="nc">ProceedingJoinPoint</span> <span class="n">joinPoint</span><span class="o">,</span> <span class="nc">RateLimit</span> <span class="n">rateLimit</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Throwable</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="n">resolveKey</span><span class="o">(</span><span class="n">joinPoint</span><span class="o">,</span> <span class="n">rateLimit</span><span class="o">);</span>
        <span class="kt">int</span> <span class="n">limit</span> <span class="o">=</span> <span class="n">rateLimit</span><span class="o">.</span><span class="na">requests</span><span class="o">();</span>
        <span class="kt">int</span> <span class="n">window</span> <span class="o">=</span> <span class="n">rateLimit</span><span class="o">.</span><span class="na">windowSeconds</span><span class="o">();</span>

        <span class="nc">String</span> <span class="n">redisKey</span> <span class="o">=</span> <span class="s">"rate:"</span> <span class="o">+</span> <span class="n">key</span> <span class="o">+</span> <span class="s">":"</span> <span class="o">+</span> <span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">/</span> <span class="o">(</span><span class="n">window</span> <span class="o">*</span> <span class="mi">1000</span><span class="o">));</span>
        <span class="nc">Long</span> <span class="n">count</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">increment</span><span class="o">(</span><span class="n">redisKey</span><span class="o">);</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">count</span> <span class="o">==</span> <span class="mi">1</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">redisTemplate</span><span class="o">.</span><span class="na">expire</span><span class="o">(</span><span class="n">redisKey</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="n">window</span><span class="o">));</span>
        <span class="o">}</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">count</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">count</span> <span class="o">&gt;</span> <span class="n">limit</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">RateLimitExceededException</span><span class="o">(</span><span class="s">"요청 한도를 초과했습니다."</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">joinPoint</span><span class="o">.</span><span class="na">proceed</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="nf">resolveKey</span><span class="o">(</span><span class="nc">ProceedingJoinPoint</span> <span class="n">joinPoint</span><span class="o">,</span> <span class="nc">RateLimit</span> <span class="n">rateLimit</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">rateLimit</span><span class="o">.</span><span class="na">key</span><span class="o">().</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">rateLimit</span><span class="o">.</span><span class="na">key</span><span class="o">();</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">joinPoint</span><span class="o">.</span><span class="na">getSignature</span><span class="o">().</span><span class="na">toShortString</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="nd">@RestController</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/api/orders"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderController</span> <span class="o">{</span>

    <span class="nd">@RateLimit</span><span class="o">(</span><span class="n">requests</span> <span class="o">=</span> <span class="mi">50</span><span class="o">,</span> <span class="n">windowSeconds</span> <span class="o">=</span> <span class="mi">60</span><span class="o">)</span>
    <span class="nd">@PostMapping</span>
    <span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">OrderResponse</span><span class="o">&gt;</span> <span class="nf">createOrder</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">OrderRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// ...</span>
    <span class="o">}</span>

    <span class="nd">@RateLimit</span><span class="o">(</span><span class="n">requests</span> <span class="o">=</span> <span class="mi">200</span><span class="o">,</span> <span class="n">windowSeconds</span> <span class="o">=</span> <span class="mi">60</span><span class="o">)</span>
    <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/{id}"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">OrderResponse</span><span class="o">&gt;</span> <span class="nf">getOrder</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// ...</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="rate-limit-응답-설계">Rate Limit 응답 설계</h2>

<h3 id="표준-http-응답">표준 HTTP 응답</h3>

<p>Rate Limit 초과 시 <code class="language-plaintext highlighter-rouge">429 Too Many Requests</code>를 반환한다.</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">429</span> <span class="ne">Too Many Requests</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/json</span>
<span class="na">Retry-After</span><span class="p">:</span> <span class="s">30</span>
<span class="na">X-Rate-Limit-Limit</span><span class="p">:</span> <span class="s">100</span>
<span class="na">X-Rate-Limit-Remaining</span><span class="p">:</span> <span class="s">0</span>
<span class="na">X-Rate-Limit-Reset</span><span class="p">:</span> <span class="s">1714531200</span>

<span class="p">{</span><span class="w">
    </span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Too Many Requests"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Rate limit exceeded. Please retry after 30 seconds."</span><span class="p">,</span><span class="w">
    </span><span class="nl">"limit"</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w">
    </span><span class="nl">"remaining"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"retryAfter"</span><span class="p">:</span><span class="w"> </span><span class="mi">30</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>주요 응답 헤더:</p>

<table>
  <thead>
    <tr>
      <th>헤더</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Retry-After</code></td>
      <td>다음 요청까지 대기 시간(초)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">X-Rate-Limit-Limit</code></td>
      <td>윈도우당 최대 요청 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">X-Rate-Limit-Remaining</code></td>
      <td>남은 요청 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">X-Rate-Limit-Reset</code></td>
      <td>윈도우 리셋 시각(Unix timestamp)</td>
    </tr>
  </tbody>
</table>

<p>정상 요청에도 <code class="language-plaintext highlighter-rouge">X-Rate-Limit-Remaining</code> 헤더를 포함시켜 클라이언트가 자체적으로 요청 속도를 조절할 수 있게 한다.</p>

<hr />

<h2 id="실무-고려사항">실무 고려사항</h2>

<h3 id="per-user-vs-per-ip-vs-per-api-key">per-user vs per-IP vs per-API-key</h3>

<table>
  <thead>
    <tr>
      <th>기준</th>
      <th>장점</th>
      <th>단점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>per-IP</td>
      <td>구현 간단, 인증 불필요</td>
      <td>NAT/프록시 뒤 사용자 구분 불가</td>
    </tr>
    <tr>
      <td>per-user</td>
      <td>정확한 사용자별 제한</td>
      <td>인증 필수</td>
    </tr>
    <tr>
      <td>per-API-key</td>
      <td>서비스/클라이언트별 제한</td>
      <td>키 관리 필요</td>
    </tr>
  </tbody>
</table>

<p>실무에서는 <strong>계층적 Rate Limiting</strong>을 적용한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>1차: per-IP (DDoS 방어, 인증 전 단계)  — 1000 req/min
2차: per-user (공정 사용)              — 100 req/min
3차: per-API-key (플랜별 차등)          — Free: 60, Pro: 600, Enterprise: 무제한
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="그-외-고려사항">그 외 고려사항</h3>

<ul>
  <li><strong>Graceful Degradation</strong>: Rate Limit 저장소(Redis)에 장애가 발생하면 요청을 통과시킬지 차단할지 결정해야 한다. 보통은 통과시키는 것이 서비스 가용성 측면에서 낫다.</li>
  <li><strong>분산 환경 시간 동기화</strong>: 서버 간 시각이 다르면 윈도우 계산이 틀어진다. NTP로 시각을 동기화하거나, Redis 서버의 시각을 기준으로 삼는다.</li>
  <li><strong>API별 차등 제한</strong>: 리소스 소모가 큰 API(검색, 리포트 생성)는 더 낮은 제한을 두고, 가벼운 API(상태 조회)는 높은 제한을 둔다.</li>
</ul>

<hr />

<h2 id="정리">정리</h2>

<p>Rate Limiting은 API 서비스의 안정성과 공정성을 보장하는 핵심 메커니즘이다. 대규모 트래픽을 비동기로 처리하려면 <a href="/backend/2026/04/03/kafka-introduction/">Apache Kafka</a>와 조합하는 것도 효과적인 전략이다. Token Bucket이 가장 범용적으로 쓰이지만, 요구사항에 따라 Sliding Window Counter도 좋은 선택이다. 분산 환경에서는 Redis가 사실상 표준이며, Spring Boot에서는 Bucket4j 라이브러리나 커스텀 어노테이션 기반 AOP로 깔끔하게 구현할 수 있다. 무엇보다 중요한 것은 클라이언트에게 명확한 Rate Limit 정보를 제공해 협력적인 트래픽 관리를 유도하는 것이다.</p>
]]></content:encoded>
        <pubDate>Wed, 01 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/system-design/2026/04/01/api-rate-limiting/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/system-design/2026/04/01/api-rate-limiting/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>RateLimiting</category>
        
        <category>SystemDesign</category>
        
        <category>Redis</category>
        
        <category>Backend</category>
        
        <category>Spring</category>
        
        
        <category>system-design</category>
        
      </item>
    
      <item>
        <title>Spring Security 6 + JWT 인증 구현</title>
        <description>들어가며</description>
        <content:encoded><![CDATA[<h2 id="들어가며">들어가며</h2>

<p><a href="/spring/2026/03/15/spring-boot-jpa-basics/">이전 글(Spring Boot + JPA로 REST API 만들기)</a>에서 기본적인 CRUD API를 구현했다. 이번에는 이 API에 <strong>인증(Authentication)</strong> 을 적용한다. 세션 기반 인증 대신 <strong>JWT(JSON Web Token)</strong> 를 사용해서 stateless한 인증 시스템을 만들 것이다.</p>

<p>Spring Security 6.x + Spring Boot 3.x 기준으로 작성했다. Spring Security 5.x 이하와 설정 방식이 상당히 다르니 주의하자.</p>

<hr />

<h2 id="spring-security-아키텍처-개요">Spring Security 아키텍처 개요</h2>

<p>Spring Security는 <strong>서블릿 필터 체인</strong> 기반으로 동작한다. 요청이 Controller에 도달하기 전에 여러 보안 필터를 거치게 된다. 아키텍처를 더 깊이 이해하려면 <a href="/spring/2026/04/05/spring-security-architecture/">Spring Security 아키텍처 완전 이해</a>를 참고하자.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre>HTTP Request
    ↓
[DelegatingFilterProxy]
    ↓
[FilterChainProxy]
    ↓
[SecurityFilterChain]
    ├── DisableEncodeUrlFilter
    ├── SecurityContextHolderFilter
    ├── CsrfFilter
    ├── LogoutFilter
    ├── UsernamePasswordAuthenticationFilter  ← 우리가 대체할 부분
    ├── ExceptionTranslationFilter
    └── AuthorizationFilter
    ↓
DispatcherServlet → Controller
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="핵심-컴포넌트">핵심 컴포넌트</h3>

<ul>
  <li><strong>SecurityFilterChain</strong> — 어떤 URL 패턴에 어떤 필터/인가 규칙을 적용할지 정의</li>
  <li><strong>AuthenticationManager</strong> — 인증 처리를 위임받는 핵심 인터페이스</li>
  <li><strong>AuthenticationProvider</strong> — 실제 인증 로직 수행 (DB 조회, 비밀번호 검증 등)</li>
  <li><strong>UserDetailsService</strong> — 사용자 정보를 로드하는 인터페이스</li>
  <li><strong>SecurityContextHolder</strong> — 인증된 사용자 정보를 ThreadLocal에 저장</li>
</ul>

<p>JWT 인증에서는 <code class="language-plaintext highlighter-rouge">UsernamePasswordAuthenticationFilter</code> 대신 <strong>커스텀 JWT 필터</strong>를 끼워 넣어서, 매 요청마다 토큰을 검증하고 SecurityContext에 인증 정보를 세팅한다.</p>

<hr />

<h2 id="jwt-구조">JWT 구조</h2>

<p>JWT는 <code class="language-plaintext highlighter-rouge">.</code>으로 구분된 세 파트로 구성된다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>xxxxx.yyyyy.zzzzz
 ↑       ↑      ↑
Header  Payload  Signature
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="header">Header</h3>

<p>서명 알고리즘과 토큰 타입을 명시한다.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="p">{</span><span class="w">
  </span><span class="nl">"alg"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HS256"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"typ"</span><span class="p">:</span><span class="w"> </span><span class="s2">"JWT"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="payload-claims">Payload (Claims)</h3>

<p>토큰에 담길 데이터. 표준 클레임과 커스텀 클레임이 있다.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="p">{</span><span class="w">
  </span><span class="nl">"sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user@example.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"iat"</span><span class="p">:</span><span class="w"> </span><span class="mi">1711900800</span><span class="p">,</span><span class="w">
  </span><span class="nl">"exp"</span><span class="p">:</span><span class="w"> </span><span class="mi">1711987200</span><span class="p">,</span><span class="w">
  </span><span class="nl">"roles"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"ROLE_USER"</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">sub</code> (subject) — 사용자 식별자</li>
  <li><code class="language-plaintext highlighter-rouge">iat</code> (issued at) — 발급 시각</li>
  <li><code class="language-plaintext highlighter-rouge">exp</code> (expiration) — 만료 시각</li>
</ul>

<h3 id="signature">Signature</h3>

<p>Header와 Payload를 인코딩한 값에 비밀키로 서명한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="서명-알고리즘">서명 알고리즘</h3>

<table>
  <thead>
    <tr>
      <th>알고리즘</th>
      <th>방식</th>
      <th>키</th>
      <th>사용 사례</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>HS256</strong></td>
      <td>대칭키 (HMAC)</td>
      <td>하나의 secret</td>
      <td>단일 서버, 간단한 구조</td>
    </tr>
    <tr>
      <td><strong>RS256</strong></td>
      <td>비대칭키 (RSA)</td>
      <td>private/public key pair</td>
      <td>MSA, 외부 검증 필요 시</td>
    </tr>
  </tbody>
</table>

<p>HS256은 구현이 간단하지만, 검증하는 쪽도 비밀키를 알아야 한다. MSA 환경에서는 RS256이 더 적합하다 — 발급 서버만 private key를 갖고, 다른 서비스는 public key로 검증만 하면 된다.</p>

<hr />

<h2 id="의존성-추가">의존성 추가</h2>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="c1">// build.gradle</span>
<span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-security'</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-web'</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-data-jpa'</span>

    <span class="c1">// JWT (jjwt 0.12.x)</span>
    <span class="n">implementation</span> <span class="s1">'io.jsonwebtoken:jjwt-api:0.12.6'</span>
    <span class="n">runtimeOnly</span>    <span class="s1">'io.jsonwebtoken:jjwt-impl:0.12.6'</span>
    <span class="n">runtimeOnly</span>    <span class="s1">'io.jsonwebtoken:jjwt-jackson:0.12.6'</span>

    <span class="n">compileOnly</span> <span class="s1">'org.projectlombok:lombok'</span>
    <span class="n">annotationProcessor</span> <span class="s1">'org.projectlombok:lombok'</span>
    <span class="n">runtimeOnly</span> <span class="s1">'com.h2database:h2'</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="구현">구현</h2>

<h3 id="1-user-entity--repository">1. User Entity &amp; Repository</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
</pre></td><td class="rouge-code"><pre><span class="nd">@Entity</span>
<span class="nd">@Table</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"users"</span><span class="o">)</span>
<span class="nd">@Getter</span>
<span class="nd">@NoArgsConstructor</span><span class="o">(</span><span class="n">access</span> <span class="o">=</span> <span class="nc">AccessLevel</span><span class="o">.</span><span class="na">PROTECTED</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">User</span> <span class="o">{</span>

    <span class="nd">@Id</span>
    <span class="nd">@GeneratedValue</span><span class="o">(</span><span class="n">strategy</span> <span class="o">=</span> <span class="nc">GenerationType</span><span class="o">.</span><span class="na">IDENTITY</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>

    <span class="nd">@Column</span><span class="o">(</span><span class="n">nullable</span> <span class="o">=</span> <span class="kc">false</span><span class="o">,</span> <span class="n">unique</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">email</span><span class="o">;</span>

    <span class="nd">@Column</span><span class="o">(</span><span class="n">nullable</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">password</span><span class="o">;</span>

    <span class="nd">@Enumerated</span><span class="o">(</span><span class="nc">EnumType</span><span class="o">.</span><span class="na">STRING</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">Role</span> <span class="n">role</span><span class="o">;</span>

    <span class="nd">@Builder</span>
    <span class="kd">public</span> <span class="nf">User</span><span class="o">(</span><span class="nc">String</span> <span class="n">email</span><span class="o">,</span> <span class="nc">String</span> <span class="n">password</span><span class="o">,</span> <span class="nc">Role</span> <span class="n">role</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">email</span> <span class="o">=</span> <span class="n">email</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">password</span> <span class="o">=</span> <span class="n">password</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">role</span> <span class="o">=</span> <span class="n">role</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">enum</span> <span class="nc">Role</span> <span class="o">{</span>
    <span class="no">ROLE_USER</span><span class="o">,</span>
    <span class="no">ROLE_ADMIN</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="o">{</span>
    <span class="nc">Optional</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="nf">findByEmail</span><span class="o">(</span><span class="nc">String</span> <span class="n">email</span><span class="o">);</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="2-userdetailsservice-구현">2. UserDetailsService 구현</h3>

<p>Spring Security가 사용자 정보를 로드할 때 사용하는 인터페이스다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CustomUserDetailsService</span> <span class="kd">implements</span> <span class="nc">UserDetailsService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">UserRepository</span> <span class="n">userRepository</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">UserDetails</span> <span class="nf">loadUserByUsername</span><span class="o">(</span><span class="nc">String</span> <span class="n">email</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">UsernameNotFoundException</span> <span class="o">{</span>
        <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findByEmail</span><span class="o">(</span><span class="n">email</span><span class="o">)</span>
                <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">UsernameNotFoundException</span><span class="o">(</span><span class="s">"사용자를 찾을 수 없습니다: "</span> <span class="o">+</span> <span class="n">email</span><span class="o">));</span>

        <span class="k">return</span> <span class="n">org</span><span class="o">.</span><span class="na">springframework</span><span class="o">.</span><span class="na">security</span><span class="o">.</span><span class="na">core</span><span class="o">.</span><span class="na">userdetails</span><span class="o">.</span><span class="na">User</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
                <span class="o">.</span><span class="na">username</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getEmail</span><span class="o">())</span>
                <span class="o">.</span><span class="na">password</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getPassword</span><span class="o">())</span>
                <span class="o">.</span><span class="na">roles</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getRole</span><span class="o">().</span><span class="na">name</span><span class="o">().</span><span class="na">replace</span><span class="o">(</span><span class="s">"ROLE_"</span><span class="o">,</span> <span class="s">""</span><span class="o">))</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="3-jwttokenprovider">3. JwtTokenProvider</h3>

<p>토큰 생성과 검증을 담당하는 핵심 클래스다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
</pre></td><td class="rouge-code"><pre><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">JwtTokenProvider</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">SecretKey</span> <span class="n">secretKey</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="kt">long</span> <span class="n">accessTokenValidity</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="kt">long</span> <span class="n">refreshTokenValidity</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">JwtTokenProvider</span><span class="o">(</span>
            <span class="nd">@Value</span><span class="o">(</span><span class="s">"${jwt.secret}"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">secret</span><span class="o">,</span>
            <span class="nd">@Value</span><span class="o">(</span><span class="s">"${jwt.access-token-validity}"</span><span class="o">)</span> <span class="kt">long</span> <span class="n">accessTokenValidity</span><span class="o">,</span>
            <span class="nd">@Value</span><span class="o">(</span><span class="s">"${jwt.refresh-token-validity}"</span><span class="o">)</span> <span class="kt">long</span> <span class="n">refreshTokenValidity</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">secretKey</span> <span class="o">=</span> <span class="nc">Keys</span><span class="o">.</span><span class="na">hmacShaKeyFor</span><span class="o">(</span><span class="nc">Decoders</span><span class="o">.</span><span class="na">BASE64</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="n">secret</span><span class="o">));</span>
        <span class="k">this</span><span class="o">.</span><span class="na">accessTokenValidity</span> <span class="o">=</span> <span class="n">accessTokenValidity</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">refreshTokenValidity</span> <span class="o">=</span> <span class="n">refreshTokenValidity</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="c1">// Access Token 생성</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">createAccessToken</span><span class="o">(</span><span class="nc">Authentication</span> <span class="n">authentication</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nf">createToken</span><span class="o">(</span><span class="n">authentication</span><span class="o">,</span> <span class="n">accessTokenValidity</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="c1">// Refresh Token 생성</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">createRefreshToken</span><span class="o">(</span><span class="nc">Authentication</span> <span class="n">authentication</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nf">createToken</span><span class="o">(</span><span class="n">authentication</span><span class="o">,</span> <span class="n">refreshTokenValidity</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="nf">createToken</span><span class="o">(</span><span class="nc">Authentication</span> <span class="n">authentication</span><span class="o">,</span> <span class="kt">long</span> <span class="n">validity</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">authorities</span> <span class="o">=</span> <span class="n">authentication</span><span class="o">.</span><span class="na">getAuthorities</span><span class="o">().</span><span class="na">stream</span><span class="o">()</span>
                <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">GrantedAuthority:</span><span class="o">:</span><span class="n">getAuthority</span><span class="o">)</span>
                <span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nc">Collectors</span><span class="o">.</span><span class="na">joining</span><span class="o">(</span><span class="s">","</span><span class="o">));</span>

        <span class="nc">Date</span> <span class="n">now</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Date</span><span class="o">();</span>
        <span class="nc">Date</span> <span class="n">expiry</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Date</span><span class="o">(</span><span class="n">now</span><span class="o">.</span><span class="na">getTime</span><span class="o">()</span> <span class="o">+</span> <span class="n">validity</span><span class="o">);</span>

        <span class="k">return</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
                <span class="o">.</span><span class="na">subject</span><span class="o">(</span><span class="n">authentication</span><span class="o">.</span><span class="na">getName</span><span class="o">())</span>
                <span class="o">.</span><span class="na">claim</span><span class="o">(</span><span class="s">"roles"</span><span class="o">,</span> <span class="n">authorities</span><span class="o">)</span>
                <span class="o">.</span><span class="na">issuedAt</span><span class="o">(</span><span class="n">now</span><span class="o">)</span>
                <span class="o">.</span><span class="na">expiration</span><span class="o">(</span><span class="n">expiry</span><span class="o">)</span>
                <span class="o">.</span><span class="na">signWith</span><span class="o">(</span><span class="n">secretKey</span><span class="o">)</span>
                <span class="o">.</span><span class="na">compact</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="c1">// 토큰에서 Authentication 객체 추출</span>
    <span class="kd">public</span> <span class="nc">Authentication</span> <span class="nf">getAuthentication</span><span class="o">(</span><span class="nc">String</span> <span class="n">token</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Claims</span> <span class="n">claims</span> <span class="o">=</span> <span class="n">parseClaims</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>

        <span class="nc">String</span> <span class="n">roles</span> <span class="o">=</span> <span class="n">claims</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"roles"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="nc">Collection</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">GrantedAuthority</span><span class="o">&gt;</span> <span class="n">authorities</span> <span class="o">=</span>
                <span class="nc">Arrays</span><span class="o">.</span><span class="na">stream</span><span class="o">(</span><span class="n">roles</span><span class="o">.</span><span class="na">split</span><span class="o">(</span><span class="s">","</span><span class="o">))</span>
                        <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">SimpleGrantedAuthority:</span><span class="o">:</span><span class="k">new</span><span class="o">)</span>
                        <span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nc">Collectors</span><span class="o">.</span><span class="na">toList</span><span class="o">());</span>

        <span class="nc">UserDetails</span> <span class="n">principal</span> <span class="o">=</span> <span class="k">new</span> <span class="n">org</span><span class="o">.</span><span class="na">springframework</span><span class="o">.</span><span class="na">security</span><span class="o">.</span><span class="na">core</span><span class="o">.</span><span class="na">userdetails</span><span class="o">.</span><span class="na">User</span><span class="o">(</span>
                <span class="n">claims</span><span class="o">.</span><span class="na">getSubject</span><span class="o">(),</span> <span class="s">""</span><span class="o">,</span> <span class="n">authorities</span><span class="o">);</span>

        <span class="k">return</span> <span class="k">new</span> <span class="nf">UsernamePasswordAuthenticationToken</span><span class="o">(</span><span class="n">principal</span><span class="o">,</span> <span class="n">token</span><span class="o">,</span> <span class="n">authorities</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="c1">// 토큰 유효성 검증</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">validateToken</span><span class="o">(</span><span class="nc">String</span> <span class="n">token</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="n">parseClaims</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
            <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">JwtException</span> <span class="o">|</span> <span class="nc">IllegalArgumentException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">Claims</span> <span class="nf">parseClaims</span><span class="o">(</span><span class="nc">String</span> <span class="n">token</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">parser</span><span class="o">()</span>
                <span class="o">.</span><span class="na">verifyWith</span><span class="o">(</span><span class="n">secretKey</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">()</span>
                <span class="o">.</span><span class="na">parseSignedClaims</span><span class="o">(</span><span class="n">token</span><span class="o">)</span>
                <span class="o">.</span><span class="na">getPayload</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">application.yml</code> 설정:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="na">jwt</span><span class="pi">:</span>
  <span class="c1"># 최소 256-bit(32바이트) 이상의 Base64 인코딩 키</span>
  <span class="na">secret</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Y2xhdWRlLWNvZGUtc3ByaW5nLXNlY3VyaXR5LWp3dC1zZWNyZXQta2V5LTMyYg=="</span>
  <span class="na">access-token-validity</span><span class="pi">:</span> <span class="m">1800000</span>   <span class="c1"># 30분 (ms)</span>
  <span class="na">refresh-token-validity</span><span class="pi">:</span> <span class="m">604800000</span> <span class="c1"># 7일 (ms)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="4-jwtauthenticationfilter">4. JwtAuthenticationFilter</h3>

<p>매 요청마다 <code class="language-plaintext highlighter-rouge">Authorization</code> 헤더에서 JWT를 추출하고, 유효하면 SecurityContext에 인증 정보를 세팅한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
</pre></td><td class="rouge-code"><pre><span class="nd">@Component</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">JwtAuthenticationFilter</span> <span class="kd">extends</span> <span class="nc">OncePerRequestFilter</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">JwtTokenProvider</span> <span class="n">jwtTokenProvider</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">doFilterInternal</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span>
                                    <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span>
                                    <span class="nc">FilterChain</span> <span class="n">filterChain</span><span class="o">)</span>
            <span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span>

        <span class="nc">String</span> <span class="n">token</span> <span class="o">=</span> <span class="n">resolveToken</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">token</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">jwtTokenProvider</span><span class="o">.</span><span class="na">validateToken</span><span class="o">(</span><span class="n">token</span><span class="o">))</span> <span class="o">{</span>
            <span class="nc">Authentication</span> <span class="n">auth</span> <span class="o">=</span> <span class="n">jwtTokenProvider</span><span class="o">.</span><span class="na">getAuthentication</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
            <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">setAuthentication</span><span class="o">(</span><span class="n">auth</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="n">filterChain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="nf">resolveToken</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">bearer</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">bearer</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">bearer</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"Bearer "</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">bearer</span><span class="o">.</span><span class="na">substring</span><span class="o">(</span><span class="mi">7</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="5-securityconfig">5. SecurityConfig</h3>

<p>Spring Security 6.x에서는 <code class="language-plaintext highlighter-rouge">WebSecurityConfigurerAdapter</code>가 제거되었다. 대신 <code class="language-plaintext highlighter-rouge">SecurityFilterChain</code>을 <code class="language-plaintext highlighter-rouge">@Bean</code>으로 등록한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
</pre></td><td class="rouge-code"><pre><span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SecurityConfig</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">JwtAuthenticationFilter</span> <span class="n">jwtAuthenticationFilter</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">CustomUserDetailsService</span> <span class="n">userDetailsService</span><span class="o">;</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">securityFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">http</span>
            <span class="c1">// REST API이므로 CSRF 비활성화</span>
            <span class="o">.</span><span class="na">csrf</span><span class="o">(</span><span class="n">csrf</span> <span class="o">-&gt;</span> <span class="n">csrf</span><span class="o">.</span><span class="na">disable</span><span class="o">())</span>

            <span class="c1">// 세션 사용 안 함 (JWT는 stateless)</span>
            <span class="o">.</span><span class="na">sessionManagement</span><span class="o">(</span><span class="n">session</span> <span class="o">-&gt;</span>
                <span class="n">session</span><span class="o">.</span><span class="na">sessionCreationPolicy</span><span class="o">(</span><span class="nc">SessionCreationPolicy</span><span class="o">.</span><span class="na">STATELESS</span><span class="o">))</span>

            <span class="c1">// URL별 인가 규칙</span>
            <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/auth/**"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/admin/**"</span><span class="o">).</span><span class="na">hasRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
            <span class="o">)</span>

            <span class="c1">// JWT 필터를 UsernamePasswordAuthenticationFilter 앞에 추가</span>
            <span class="o">.</span><span class="na">addFilterBefore</span><span class="o">(</span><span class="n">jwtAuthenticationFilter</span><span class="o">,</span>
                <span class="nc">UsernamePasswordAuthenticationFilter</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

        <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">AuthenticationManager</span> <span class="nf">authenticationManager</span><span class="o">(</span>
            <span class="nc">AuthenticationConfiguration</span> <span class="n">config</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">config</span><span class="o">.</span><span class="na">getAuthenticationManager</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">PasswordEncoder</span> <span class="nf">passwordEncoder</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">BCryptPasswordEncoder</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="6-authcontroller--로그인--회원가입">6. AuthController — 로그인 &amp; 회원가입</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
</pre></td><td class="rouge-code"><pre><span class="nd">@RestController</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/api/auth"</span><span class="o">)</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AuthController</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AuthenticationManager</span> <span class="n">authenticationManager</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">JwtTokenProvider</span> <span class="n">jwtTokenProvider</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">UserRepository</span> <span class="n">userRepository</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">PasswordEncoder</span> <span class="n">passwordEncoder</span><span class="o">;</span>

    <span class="nd">@PostMapping</span><span class="o">(</span><span class="s">"/signup"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">signup</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">SignupRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">userRepository</span><span class="o">.</span><span class="na">findByEmail</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getEmail</span><span class="o">()).</span><span class="na">isPresent</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">return</span> <span class="nc">ResponseEntity</span><span class="o">.</span><span class="na">badRequest</span><span class="o">().</span><span class="na">body</span><span class="o">(</span><span class="s">"이미 존재하는 이메일입니다."</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="nc">User</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
                <span class="o">.</span><span class="na">email</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getEmail</span><span class="o">())</span>
                <span class="o">.</span><span class="na">password</span><span class="o">(</span><span class="n">passwordEncoder</span><span class="o">.</span><span class="na">encode</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getPassword</span><span class="o">()))</span>
                <span class="o">.</span><span class="na">role</span><span class="o">(</span><span class="nc">Role</span><span class="o">.</span><span class="na">ROLE_USER</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>

        <span class="n">userRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
        <span class="k">return</span> <span class="nc">ResponseEntity</span><span class="o">.</span><span class="na">status</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">CREATED</span><span class="o">).</span><span class="na">body</span><span class="o">(</span><span class="s">"회원가입 완료"</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@PostMapping</span><span class="o">(</span><span class="s">"/login"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">TokenResponse</span><span class="o">&gt;</span> <span class="nf">login</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">LoginRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Authentication</span> <span class="n">authentication</span> <span class="o">=</span> <span class="n">authenticationManager</span><span class="o">.</span><span class="na">authenticate</span><span class="o">(</span>
                <span class="k">new</span> <span class="nf">UsernamePasswordAuthenticationToken</span><span class="o">(</span>
                        <span class="n">request</span><span class="o">.</span><span class="na">getEmail</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getPassword</span><span class="o">()));</span>

        <span class="nc">String</span> <span class="n">accessToken</span> <span class="o">=</span> <span class="n">jwtTokenProvider</span><span class="o">.</span><span class="na">createAccessToken</span><span class="o">(</span><span class="n">authentication</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">refreshToken</span> <span class="o">=</span> <span class="n">jwtTokenProvider</span><span class="o">.</span><span class="na">createRefreshToken</span><span class="o">(</span><span class="n">authentication</span><span class="o">);</span>

        <span class="k">return</span> <span class="nc">ResponseEntity</span><span class="o">.</span><span class="na">ok</span><span class="o">(</span><span class="k">new</span> <span class="nc">TokenResponse</span><span class="o">(</span><span class="n">accessToken</span><span class="o">,</span> <span class="n">refreshToken</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>DTO 클래스:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre><span class="nd">@Getter</span>
<span class="nd">@NoArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SignupRequest</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">email</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">password</span><span class="o">;</span>
<span class="o">}</span>

<span class="nd">@Getter</span>
<span class="nd">@NoArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">LoginRequest</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">email</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">password</span><span class="o">;</span>
<span class="o">}</span>

<span class="nd">@Getter</span>
<span class="nd">@AllArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TokenResponse</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">accessToken</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">refreshToken</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="로그인-플로우">로그인 플로우</h2>

<p>전체 인증 흐름을 정리하면:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre>[회원가입]
POST /api/auth/signup { email, password }
    → PasswordEncoder.encode(password) → DB 저장

[로그인]
POST /api/auth/login { email, password }
    → AuthenticationManager.authenticate()
        → CustomUserDetailsService.loadUserByUsername()
        → BCrypt 비밀번호 검증
    → JwtTokenProvider.createAccessToken()
    → JwtTokenProvider.createRefreshToken()
    → { accessToken, refreshToken } 응답

[인증된 API 호출]
GET /api/posts (Authorization: Bearer &lt;accessToken&gt;)
    → JwtAuthenticationFilter.doFilterInternal()
        → resolveToken() → Bearer에서 토큰 추출
        → validateToken() → 서명 검증 + 만료 확인
        → getAuthentication() → SecurityContext에 세팅
    → Controller 정상 처리
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="토큰-갱신refresh-전략">토큰 갱신(Refresh) 전략</h2>

<p>Access Token의 유효기간은 짧게 설정한다 (15~30분). 만료되면 Refresh Token으로 새 Access Token을 발급받는다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="nd">@PostMapping</span><span class="o">(</span><span class="s">"/refresh"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">TokenResponse</span><span class="o">&gt;</span> <span class="nf">refresh</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">RefreshRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">refreshToken</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getRefreshToken</span><span class="o">();</span>

    <span class="k">if</span> <span class="o">(!</span><span class="n">jwtTokenProvider</span><span class="o">.</span><span class="na">validateToken</span><span class="o">(</span><span class="n">refreshToken</span><span class="o">))</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">ResponseEntity</span><span class="o">.</span><span class="na">status</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">UNAUTHORIZED</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nc">Authentication</span> <span class="n">auth</span> <span class="o">=</span> <span class="n">jwtTokenProvider</span><span class="o">.</span><span class="na">getAuthentication</span><span class="o">(</span><span class="n">refreshToken</span><span class="o">);</span>
    <span class="nc">String</span> <span class="n">newAccessToken</span> <span class="o">=</span> <span class="n">jwtTokenProvider</span><span class="o">.</span><span class="na">createAccessToken</span><span class="o">(</span><span class="n">auth</span><span class="o">);</span>

    <span class="c1">// Refresh Token은 재사용 (RTR 전략을 쓰려면 여기서 새로 발급)</span>
    <span class="k">return</span> <span class="nc">ResponseEntity</span><span class="o">.</span><span class="na">ok</span><span class="o">(</span><span class="k">new</span> <span class="nc">TokenResponse</span><span class="o">(</span><span class="n">newAccessToken</span><span class="o">,</span> <span class="n">refreshToken</span><span class="o">));</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="rtr-refresh-token-rotation-전략">RTR (Refresh Token Rotation) 전략</h3>

<p>보안을 강화하려면 Refresh Token도 갱신할 때마다 새로 발급하고, 이전 토큰을 무효화한다. 이를 <strong>Refresh Token Rotation</strong> 이라 한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>[기본 전략]
Access Token 만료 → Refresh Token으로 갱신 → 같은 Refresh Token 재사용

[RTR 전략]
Access Token 만료 → Refresh Token으로 갱신 → 새 Refresh Token 발급 + 이전 무효화
</pre></td></tr></tbody></table></code></pre></div></div>

<p>RTR을 구현하려면 Refresh Token을 DB나 Redis에 저장하고 관리해야 한다. 서버 부하는 증가하지만, 탈취된 Refresh Token의 피해를 최소화할 수 있다.</p>

<hr />

<h2 id="흔한-함정과-주의사항">흔한 함정과 주의사항</h2>

<h3 id="1-토큰-저장-위치-localstorage-vs-httponly-cookie">1. 토큰 저장 위치: localStorage vs HttpOnly Cookie</h3>

<table>
  <thead>
    <tr>
      <th>방식</th>
      <th>XSS 취약</th>
      <th>CSRF 취약</th>
      <th>구현 난이도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>localStorage</strong></td>
      <td>O (JS로 접근 가능)</td>
      <td>X</td>
      <td>쉬움</td>
    </tr>
    <tr>
      <td><strong>HttpOnly Cookie</strong></td>
      <td>X (JS 접근 불가)</td>
      <td>O</td>
      <td>보통</td>
    </tr>
  </tbody>
</table>

<p><strong>권장</strong>: HttpOnly + Secure + SameSite=Strict 쿠키에 저장한다. XSS는 토큰 탈취로 이어지지만, CSRF는 SameSite 속성으로 효과적으로 방어된다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="c1">// 쿠키로 토큰을 내려보내는 예시</span>
<span class="nc">ResponseCookie</span> <span class="n">cookie</span> <span class="o">=</span> <span class="nc">ResponseCookie</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="s">"access_token"</span><span class="o">,</span> <span class="n">accessToken</span><span class="o">)</span>
        <span class="o">.</span><span class="na">httpOnly</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
        <span class="o">.</span><span class="na">secure</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
        <span class="o">.</span><span class="na">sameSite</span><span class="o">(</span><span class="s">"Strict"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">path</span><span class="o">(</span><span class="s">"/"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">maxAge</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">))</span>
        <span class="o">.</span><span class="na">build</span><span class="o">();</span>

<span class="n">response</span><span class="o">.</span><span class="na">addHeader</span><span class="o">(</span><span class="nc">HttpHeaders</span><span class="o">.</span><span class="na">SET_COOKIE</span><span class="o">,</span> <span class="n">cookie</span><span class="o">.</span><span class="na">toString</span><span class="o">());</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="2-jwt--csrf">2. JWT + CSRF</h3>

<p>JWT를 Authorization 헤더로 보내면 CSRF 보호가 불필요하다 — 브라우저가 자동으로 첨부하는 값이 아니기 때문이다. 하지만 JWT를 <strong>쿠키</strong>에 저장하면 CSRF 보호를 다시 활성화해야 한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c1">// 쿠키 기반 JWT라면 CSRF를 활성화해야 한다</span>
<span class="o">.</span><span class="na">csrf</span><span class="o">(</span><span class="n">csrf</span> <span class="o">-&gt;</span> <span class="n">csrf</span>
    <span class="o">.</span><span class="na">csrfTokenRepository</span><span class="o">(</span><span class="nc">CookieCsrfTokenRepository</span><span class="o">.</span><span class="na">withHttpOnlyFalse</span><span class="o">())</span>
    <span class="o">.</span><span class="na">csrfTokenRequestHandler</span><span class="o">(</span><span class="k">new</span> <span class="nc">CsrfTokenRequestAttributeHandler</span><span class="o">()))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="3-토큰-만료-처리">3. 토큰 만료 처리</h3>

<p>클라이언트는 <code class="language-plaintext highlighter-rouge">401 Unauthorized</code> 응답을 받으면 Refresh Token으로 갱신을 시도해야 한다. Axios interceptor 패턴이 일반적이다:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="c1">// Axios interceptor 예시</span>
<span class="nx">api</span><span class="p">.</span><span class="nx">interceptors</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nf">use</span><span class="p">(</span>
  <span class="nx">response</span> <span class="o">=&gt;</span> <span class="nx">response</span><span class="p">,</span>
  <span class="k">async</span> <span class="nx">error</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">originalRequest</span> <span class="o">=</span> <span class="nx">error</span><span class="p">.</span><span class="nx">config</span><span class="p">;</span>

    <span class="k">if </span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">response</span><span class="p">?.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">401</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">originalRequest</span><span class="p">.</span><span class="nx">_retry</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">originalRequest</span><span class="p">.</span><span class="nx">_retry</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>

      <span class="kd">const</span> <span class="p">{</span> <span class="nx">data</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">api</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/auth/refresh</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">refreshToken</span><span class="p">:</span> <span class="nf">getRefreshToken</span><span class="p">()</span>
      <span class="p">});</span>

      <span class="nf">setAccessToken</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">accessToken</span><span class="p">);</span>
      <span class="nx">originalRequest</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">Authorization</span> <span class="o">=</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">accessToken</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
      <span class="k">return</span> <span class="nf">api</span><span class="p">(</span><span class="nx">originalRequest</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nf">reject</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="4-토큰-무효화-로그아웃">4. 토큰 무효화 (로그아웃)</h3>

<p>JWT는 stateless이므로 서버에서 강제 만료시킬 수 없다. 로그아웃 구현 방법:</p>

<ul>
  <li><strong>블랙리스트</strong> — Redis에 로그아웃된 토큰을 저장, 매 요청마다 확인</li>
  <li><strong>짧은 만료</strong> — Access Token을 5~15분으로 짧게 설정해서 피해 최소화</li>
  <li><strong>토큰 버전</strong> — DB에 사용자별 토큰 버전을 두고, 로그아웃 시 버전을 올림</li>
</ul>

<h3 id="5-secret-key-관리">5. Secret Key 관리</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="c1"># 절대 이렇게 하지 마세요</span>
<span class="na">jwt</span><span class="pi">:</span>
  <span class="na">secret</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mysecret"</span>  <span class="c1"># 너무 짧고, 코드에 하드코딩</span>

<span class="c1"># 환경변수나 외부 설정 관리 도구 사용</span>
<span class="na">jwt</span><span class="pi">:</span>
  <span class="na">secret</span><span class="pi">:</span> <span class="s">${JWT_SECRET}</span>  <span class="c1"># 환경변수에서 주입</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>프로덕션에서는 <strong>AWS Secrets Manager</strong>, <strong>HashiCorp Vault</strong>, 또는 <strong>Spring Cloud Config</strong> 같은 외부 설정 관리 도구를 사용해야 한다.</p>

<hr />

<h2 id="전체-프로젝트-구조">전체 프로젝트 구조</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre>src/main/java/com/example/demo/
├── config/
│   └── SecurityConfig.java
├── controller/
│   └── AuthController.java
├── domain/
│   ├── User.java
│   └── Role.java
├── dto/
│   ├── LoginRequest.java
│   ├── SignupRequest.java
│   ├── RefreshRequest.java
│   └── TokenResponse.java
├── repository/
│   └── UserRepository.java
├── security/
│   ├── JwtTokenProvider.java
│   ├── JwtAuthenticationFilter.java
│   └── CustomUserDetailsService.java
└── DemoApplication.java
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="마무리">마무리</h2>

<p>Spring Security + JWT 인증의 핵심은 <strong>SecurityFilterChain에 커스텀 JWT 필터를 끼워 넣는 것</strong>이다. 나머지는 토큰을 만들고, 검증하고, SecurityContext에 세팅하는 흐름을 따른다.</p>

<p>실무 체크리스트:</p>

<ol>
  <li><strong>Access Token은 짧게</strong> (15~30분), <strong>Refresh Token은 길게</strong> (7~14일)</li>
  <li>토큰은 <strong>HttpOnly Cookie</strong>에 저장 — localStorage는 XSS에 취약</li>
  <li><strong>비밀키는 환경변수</strong>로 관리, 코드에 절대 하드코딩하지 않기</li>
  <li>금전적 피해가 큰 서비스라면 <strong>RTR 전략</strong> 도입 고려</li>
  <li>로그아웃은 <strong>Redis 블랙리스트</strong> + 짧은 Access Token 조합으로 구현</li>
</ol>

<p>다음 글에서는 <strong>Spring Security + OAuth 2.0 소셜 로그인</strong> (Google, Kakao)을 다룰 예정이다.</p>

<hr />

<h2 id="references">References</h2>

<ul>
  <li><a href="https://docs.spring.io/spring-security/reference/servlet/architecture.html">Spring Security Reference — Servlet Architecture</a></li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc7519">RFC 7519 — JSON Web Token</a></li>
  <li><a href="https://github.com/jwtk/jjwt">JJWT GitHub</a></li>
  <li><a href="https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html">OWASP — JWT Cheat Sheet</a></li>
</ul>

<hr />

<h2 id="관련-포스트">관련 포스트</h2>

<ul>
  <li><a href="/spring/2026/03/15/spring-boot-jpa-basics/">Spring Boot + JPA 기초</a></li>
</ul>
]]></content:encoded>
        <pubDate>Wed, 01 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/spring/2026/04/01/spring-security-jwt/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/spring/2026/04/01/spring-security-jwt/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Spring</category>
        
        <category>Spring Security</category>
        
        <category>JWT</category>
        
        <category>Backend</category>
        
        
        <category>spring</category>
        
      </item>
    
      <item>
        <title>React Hooks 완전 정복 — useState부터 Custom Hook까지</title>
        <description>Class에서 Hooks로</description>
        <content:encoded><![CDATA[<h2 id="class에서-hooks로">Class에서 Hooks로</h2>

<p>React 16.8 이전에는 상태 관리와 생명주기 로직을 사용하려면 반드시 클래스 컴포넌트를 작성해야 했다.</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="kd">class</span> <span class="nc">Counter</span> <span class="kd">extends</span> <span class="nc">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
  <span class="nf">constructor</span><span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">(</span><span class="nx">props</span><span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">state</span> <span class="o">=</span> <span class="p">{</span> <span class="na">count</span><span class="p">:</span> <span class="mi">0</span> <span class="p">};</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">handleClick</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">handleClick</span><span class="p">.</span><span class="nf">bind</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="nf">componentDidMount</span><span class="p">()</span> <span class="p">{</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">title</span> <span class="o">=</span> <span class="s2">`클릭: </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">count</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nf">componentDidUpdate</span><span class="p">()</span> <span class="p">{</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">title</span> <span class="o">=</span> <span class="s2">`클릭: </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">count</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nf">handleClick</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nf">setState</span><span class="p">({</span> <span class="na">count</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">});</span>
  <span class="p">}</span>

  <span class="nf">render</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleClick</span><span class="si">}</span><span class="p">&gt;</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">count</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;;</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>문제는 명확했다:</p>

<ul>
  <li><strong>this 바인딩</strong>: 매번 <code class="language-plaintext highlighter-rouge">.bind(this)</code>를 하거나 화살표 함수를 써야 한다</li>
  <li><strong>로직 분산</strong>: 같은 관심사의 코드가 <code class="language-plaintext highlighter-rouge">componentDidMount</code>, <code class="language-plaintext highlighter-rouge">componentDidUpdate</code>, <code class="language-plaintext highlighter-rouge">componentWillUnmount</code>에 흩어진다</li>
  <li><strong>재사용 어려움</strong>: 상태 로직을 공유하려면 HOC나 render props 패턴이 필요한데 “wrapper hell”을 만든다</li>
</ul>

<p>Hooks는 이 문제를 <strong>함수 컴포넌트 안에서 상태와 생명주기를 사용할 수 있게</strong> 해서 해결했다.</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nf">Counter</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">count</span><span class="p">,</span> <span class="nx">setCount</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>

  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">title</span> <span class="o">=</span> <span class="s2">`클릭: </span><span class="p">${</span><span class="nx">count</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
  <span class="p">},</span> <span class="p">[</span><span class="nx">count</span><span class="p">]);</span>

  <span class="k">return</span> <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nf">setCount</span><span class="p">(</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span><span class="si">}</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">count</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>코드가 절반으로 줄었고, 관련 로직이 한 곳에 모였다.</p>

<hr />

<h2 id="usestate-상태-관리의-기본">useState: 상태 관리의 기본</h2>

<h3 id="기본-사용법">기본 사용법</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="kd">const</span> <span class="p">[</span><span class="nx">value</span><span class="p">,</span> <span class="nx">setValue</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="nx">initialValue</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="핵심-1-상태-업데이트는-비동기다">핵심 1: 상태 업데이트는 비동기다</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nf">Counter</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">count</span><span class="p">,</span> <span class="nx">setCount</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">handleClick</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">setCount</span><span class="p">(</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
    <span class="nf">setCount</span><span class="p">(</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
    <span class="nf">setCount</span><span class="p">(</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">count</span><span class="p">);</span> <span class="c1">// 여전히 0 — 즉시 반영되지 않는다</span>
  <span class="p">};</span>
  <span class="c1">// 결과: count는 1이 된다 (3이 아님!)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>세 번 호출해도 <code class="language-plaintext highlighter-rouge">count</code>는 같은 클로저 값(0)을 참조하므로 <code class="language-plaintext highlighter-rouge">setCount(0 + 1)</code>이 세 번 실행될 뿐이다.</p>

<h3 id="핵심-2-함수형-업데이트">핵심 2: 함수형 업데이트</h3>

<p>이전 상태를 기반으로 업데이트할 때는 <strong>함수형 업데이트</strong>를 사용한다:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="kd">const</span> <span class="nx">handleClick</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nf">setCount</span><span class="p">(</span><span class="nx">prev</span> <span class="o">=&gt;</span> <span class="nx">prev</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
  <span class="nf">setCount</span><span class="p">(</span><span class="nx">prev</span> <span class="o">=&gt;</span> <span class="nx">prev</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
  <span class="nf">setCount</span><span class="p">(</span><span class="nx">prev</span> <span class="o">=&gt;</span> <span class="nx">prev</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
  <span class="c1">// 결과: count는 3이 된다 ✅</span>
<span class="p">};</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">prev</code>는 항상 최신 상태를 보장하므로 연속 업데이트가 올바르게 누적된다.</p>

<h3 id="핵심-3-객체-상태는-불변하게">핵심 3: 객체 상태는 불변하게</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="kd">const</span> <span class="p">[</span><span class="nx">user</span><span class="p">,</span> <span class="nx">setUser</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">김도윤</span><span class="dl">'</span><span class="p">,</span> <span class="na">age</span><span class="p">:</span> <span class="mi">25</span> <span class="p">});</span>

<span class="c1">// ❌ 잘못된 방법 — 직접 변경</span>
<span class="nx">user</span><span class="p">.</span><span class="nx">age</span> <span class="o">=</span> <span class="mi">26</span><span class="p">;</span>
<span class="nf">setUser</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span> <span class="c1">// React가 변경을 감지하지 못함 (같은 참조)</span>

<span class="c1">// ✅ 올바른 방법 — 새 객체 생성</span>
<span class="nf">setUser</span><span class="p">(</span><span class="nx">prev</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="p">...</span><span class="nx">prev</span><span class="p">,</span> <span class="na">age</span><span class="p">:</span> <span class="mi">26</span> <span class="p">}));</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>React는 <strong>참조 비교</strong>(Object.is)로 상태 변경을 감지한다. 같은 객체 참조를 전달하면 리렌더링이 발생하지 않는다.</p>

<hr />

<h2 id="useeffect-부수-효과-관리">useEffect: 부수 효과 관리</h2>

<h3 id="기본-구조">기본 구조</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// 실행할 부수 효과</span>
  <span class="k">return </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// cleanup 함수 (선택)</span>
  <span class="p">};</span>
<span class="p">},</span> <span class="p">[</span><span class="nx">dependencies</span><span class="p">]);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>의존성 배열에 따라 실행 시점이 달라진다:</p>

<table>
  <thead>
    <tr>
      <th>의존성 배열</th>
      <th>실행 시점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>생략</td>
      <td>매 렌더링마다</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[]</code> (빈 배열)</td>
      <td>마운트 시 1번</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[a, b]</code></td>
      <td>a 또는 b가 변경될 때</td>
    </tr>
  </tbody>
</table>

<h3 id="함정-1-빈-의존성-배열에서-stale-closure">함정 1: 빈 의존성 배열에서 stale closure</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nf">Timer</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">count</span><span class="p">,</span> <span class="nx">setCount</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>

  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">id</span> <span class="o">=</span> <span class="nf">setInterval</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">count</span><span class="p">);</span> <span class="c1">// 항상 0 — stale closure!</span>
      <span class="nf">setCount</span><span class="p">(</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="c1">// 항상 0 + 1 = 1</span>
    <span class="p">},</span> <span class="mi">1000</span><span class="p">);</span>
    <span class="k">return </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nf">clearInterval</span><span class="p">(</span><span class="nx">id</span><span class="p">);</span>
  <span class="p">},</span> <span class="p">[]);</span> <span class="c1">// count를 의존성에 넣지 않았다</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">[]</code>을 전달하면 effect는 마운트 시점의 <code class="language-plaintext highlighter-rouge">count</code>(0)를 영원히 참조한다. 해결 방법:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c1">// 방법 1: 함수형 업데이트</span>
<span class="nf">setCount</span><span class="p">(</span><span class="nx">prev</span> <span class="o">=&gt;</span> <span class="nx">prev</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>

<span class="c1">// 방법 2: 의존성 배열에 추가 (단, 매번 재실행됨)</span>
<span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="p">...</span> <span class="p">},</span> <span class="p">[</span><span class="nx">count</span><span class="p">]);</span>

<span class="c1">// 방법 3: useRef로 최신 값 추적</span>
<span class="kd">const</span> <span class="nx">countRef</span> <span class="o">=</span> <span class="nf">useRef</span><span class="p">(</span><span class="nx">count</span><span class="p">);</span>
<span class="nx">countRef</span><span class="p">.</span><span class="nx">current</span> <span class="o">=</span> <span class="nx">count</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="함정-2-객체배열-의존성">함정 2: 객체/배열 의존성</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nf">UserProfile</span><span class="p">({</span> <span class="nx">userId</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">user</span><span class="p">,</span> <span class="nx">setUser</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
  
  <span class="c1">// ⚠️ options가 매 렌더링마다 새 객체 → effect 무한 실행</span>
  <span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span> <span class="na">includeDetails</span><span class="p">:</span> <span class="kc">true</span> <span class="p">};</span>

  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">fetchUser</span><span class="p">(</span><span class="nx">userId</span><span class="p">,</span> <span class="nx">options</span><span class="p">).</span><span class="nf">then</span><span class="p">(</span><span class="nx">setUser</span><span class="p">);</span>
  <span class="p">},</span> <span class="p">[</span><span class="nx">userId</span><span class="p">,</span> <span class="nx">options</span><span class="p">]);</span> <span class="c1">// options는 매번 새 참조!</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>객체와 배열은 매 렌더링마다 새 참조가 만들어진다. 해결:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="c1">// 방법 1: 의존성을 원시 값으로 분해</span>
<span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nf">fetchUser</span><span class="p">(</span><span class="nx">userId</span><span class="p">,</span> <span class="p">{</span> <span class="na">includeDetails</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}).</span><span class="nf">then</span><span class="p">(</span><span class="nx">setUser</span><span class="p">);</span>
<span class="p">},</span> <span class="p">[</span><span class="nx">userId</span><span class="p">]);</span>

<span class="c1">// 방법 2: useMemo로 참조 안정화</span>
<span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="nf">useMemo</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">includeDetails</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}),</span> <span class="p">[]);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="cleanup-함수의-중요성">Cleanup 함수의 중요성</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">ws</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">WebSocket</span><span class="p">(</span><span class="dl">'</span><span class="s1">wss://api.example.com/feed</span><span class="dl">'</span><span class="p">);</span>
  <span class="nx">ws</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nf">setMessages</span><span class="p">(</span><span class="nx">prev</span> <span class="o">=&gt;</span> <span class="p">[...</span><span class="nx">prev</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">data</span><span class="p">]);</span>

  <span class="k">return </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">ws</span><span class="p">.</span><span class="nf">close</span><span class="p">();</span> <span class="c1">// 컴포넌트 언마운트 시 연결 종료</span>
  <span class="p">};</span>
<span class="p">},</span> <span class="p">[]);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>cleanup을 빠뜨리면 <strong>메모리 누수</strong>가 발생한다. 구독, 타이머, WebSocket, 이벤트 리스너는 반드시 cleanup에서 정리하자.</p>

<h3 id="흔한-실수-useeffect-안에서-상태-설정--무한-루프">흔한 실수: useEffect 안에서 상태 설정 → 무한 루프</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c1">// ❌ 무한 루프</span>
<span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nf">setCount</span><span class="p">(</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="c1">// 상태 변경 → 리렌더링 → effect 재실행 → ...</span>
<span class="p">});</span>

<span class="c1">// ❌ 미묘한 무한 루프</span>
<span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nf">setItems</span><span class="p">([...</span><span class="nx">items</span><span class="p">,</span> <span class="nx">newItem</span><span class="p">]);</span> <span class="c1">// items가 변경 → effect 재실행</span>
<span class="p">},</span> <span class="p">[</span><span class="nx">items</span><span class="p">]);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="usecallback과-usememo-최적화-그러나-신중하게">useCallback과 useMemo: 최적화, 그러나 신중하게</h2>

<h3 id="usememo--값-메모이제이션">useMemo — 값 메모이제이션</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="kd">const</span> <span class="nx">sortedList</span> <span class="o">=</span> <span class="nf">useMemo</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">[...</span><span class="nx">items</span><span class="p">].</span><span class="nf">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span><span class="p">.</span><span class="nx">price</span> <span class="o">-</span> <span class="nx">b</span><span class="p">.</span><span class="nx">price</span><span class="p">);</span>
<span class="p">},</span> <span class="p">[</span><span class="nx">items</span><span class="p">]);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">items</code>가 변경되지 않으면 정렬을 다시 수행하지 않는다.</p>

<h3 id="usecallback--함수-메모이제이션">useCallback — 함수 메모이제이션</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="kd">const</span> <span class="nx">handleSubmit</span> <span class="o">=</span> <span class="nf">useCallback</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">api</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/users</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
<span class="p">},</span> <span class="p">[]);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">useCallback(fn, deps)</code>는 사실 <code class="language-plaintext highlighter-rouge">useMemo(() =&gt; fn, deps)</code>와 동일하다. 함수 자체의 참조를 안정화한다.</p>

<h3 id="언제-써야-하는가">언제 써야 하는가</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="rouge-code"><pre><span class="c1">// ✅ 사용이 정당한 경우</span>
<span class="c1">// 1. React.memo로 감싼 자식에게 전달하는 콜백</span>
<span class="kd">const</span> <span class="nx">MemoChild</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nf">memo</span><span class="p">(({</span> <span class="nx">onClick</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">onClick</span><span class="si">}</span><span class="p">&gt;</span>Click<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;);</span>

<span class="kd">function</span> <span class="nf">Parent</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">handleClick</span> <span class="o">=</span> <span class="nf">useCallback</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">clicked</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">},</span> <span class="p">[]);</span>
  <span class="k">return</span> <span class="p">&lt;</span><span class="nc">MemoChild</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">handleClick</span><span class="si">}</span> <span class="p">/&gt;;</span>
<span class="p">}</span>

<span class="c1">// 2. useEffect 의존성에 들어가는 함수</span>
<span class="kd">const</span> <span class="nx">fetchData</span> <span class="o">=</span> <span class="nf">useCallback</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">api</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="s2">`/users/</span><span class="p">${</span><span class="nx">userId</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">},</span> <span class="p">[</span><span class="nx">userId</span><span class="p">]);</span>

<span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nf">fetchData</span><span class="p">().</span><span class="nf">then</span><span class="p">(</span><span class="nx">setData</span><span class="p">);</span>
<span class="p">},</span> <span class="p">[</span><span class="nx">fetchData</span><span class="p">]);</span>

<span class="c1">// 3. 비용이 큰 계산</span>
<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nf">useMemo</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nf">heavyComputation</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span> <span class="c1">// 수만 건 데이터 처리</span>
<span class="p">},</span> <span class="p">[</span><span class="nx">data</span><span class="p">]);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="남용하지-말-것">남용하지 말 것</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="c1">// ❌ 불필요한 useMemo — 단순 계산</span>
<span class="kd">const</span> <span class="nx">fullName</span> <span class="o">=</span> <span class="nf">useMemo</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="s2">`</span><span class="p">${</span><span class="nx">firstName</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">lastName</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="p">[</span><span class="nx">firstName</span><span class="p">,</span> <span class="nx">lastName</span><span class="p">]);</span>
<span class="c1">// 그냥 이렇게 쓰면 된다:</span>
<span class="kd">const</span> <span class="nx">fullName</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">firstName</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">lastName</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>

<span class="c1">// ❌ 불필요한 useCallback — memo된 자식에게 전달하지 않는 콜백</span>
<span class="kd">const</span> <span class="nx">handleClick</span> <span class="o">=</span> <span class="nf">useCallback</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nf">setCount</span><span class="p">(</span><span class="nx">c</span> <span class="o">=&gt;</span> <span class="nx">c</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
<span class="p">},</span> <span class="p">[]);</span>
<span class="c1">// 자식이 React.memo가 아니면 어차피 리렌더링된다</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>useMemo/useCallback 자체도 비용이 있다.</strong> 의존성 배열 비교, 이전 값 캐싱 등의 오버헤드가 발생한다. 단순한 연산에 적용하면 오히려 성능이 나빠진다. <strong>“측정 후 최적화”</strong>가 원칙이다.</p>

<hr />

<h2 id="useref-dom-접근과-뮤터블-값">useRef: DOM 접근과 뮤터블 값</h2>

<h3 id="dom-접근">DOM 접근</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nf">TextInput</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">inputRef</span> <span class="o">=</span> <span class="nf">useRef</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">focusInput</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">inputRef</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nf">focus</span><span class="p">();</span>
  <span class="p">};</span>

  <span class="k">return </span><span class="p">(</span>
    <span class="p">&lt;&gt;</span>
      <span class="p">&lt;</span><span class="nt">input</span> <span class="na">ref</span><span class="p">=</span><span class="si">{</span><span class="nx">inputRef</span><span class="si">}</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="p">/&gt;</span>
      <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">focusInput</span><span class="si">}</span><span class="p">&gt;</span>포커스<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
    <span class="p">&lt;/&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="렌더링과-무관한-뮤터블-값-저장">렌더링과 무관한 뮤터블 값 저장</h3>

<p><code class="language-plaintext highlighter-rouge">useRef</code>는 <code class="language-plaintext highlighter-rouge">.current</code>를 변경해도 <strong>리렌더링을 트리거하지 않는다</strong>. 이 특성을 활용해 렌더링 사이에 값을 유지하면서도 리렌더링을 피할 수 있다.</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nf">StopWatch</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">elapsed</span><span class="p">,</span> <span class="nx">setElapsed</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">intervalRef</span> <span class="o">=</span> <span class="nf">useRef</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">start</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">intervalRef</span><span class="p">.</span><span class="nx">current</span> <span class="o">=</span> <span class="nf">setInterval</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nf">setElapsed</span><span class="p">(</span><span class="nx">prev</span> <span class="o">=&gt;</span> <span class="nx">prev</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
    <span class="p">},</span> <span class="mi">1000</span><span class="p">);</span>
  <span class="p">};</span>

  <span class="kd">const</span> <span class="nx">stop</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">clearInterval</span><span class="p">(</span><span class="nx">intervalRef</span><span class="p">.</span><span class="nx">current</span><span class="p">);</span>
  <span class="p">};</span>

  <span class="k">return </span><span class="p">(</span>
    <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">span</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">elapsed</span><span class="si">}</span>초<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">start</span><span class="si">}</span><span class="p">&gt;</span>시작<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">stop</span><span class="si">}</span><span class="p">&gt;</span>정지<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="이전-상태-값-추적">이전 상태 값 추적</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nf">usePrevious</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">ref</span> <span class="o">=</span> <span class="nf">useRef</span><span class="p">();</span>
  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">ref</span><span class="p">.</span><span class="nx">current</span> <span class="o">=</span> <span class="nx">value</span><span class="p">;</span>
  <span class="p">});</span>
  <span class="k">return</span> <span class="nx">ref</span><span class="p">.</span><span class="nx">current</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nf">Counter</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">count</span><span class="p">,</span> <span class="nx">setCount</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">prevCount</span> <span class="o">=</span> <span class="nf">usePrevious</span><span class="p">(</span><span class="nx">count</span><span class="p">);</span>
  <span class="k">return</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>현재: <span class="si">{</span><span class="nx">count</span><span class="si">}</span>, 이전: <span class="si">{</span><span class="nx">prevCount</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="custom-hook-로직-재사용의-핵심">Custom Hook: 로직 재사용의 핵심</h2>

<p>Custom Hook은 <code class="language-plaintext highlighter-rouge">use</code>로 시작하는 함수로, 내부에서 다른 Hook을 조합해 재사용 가능한 상태 로직을 캡슐화한다.</p>

<h3 id="usefetch--api-호출-추상화">useFetch — API 호출 추상화</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nf">useFetch</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">data</span><span class="p">,</span> <span class="nx">setData</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">loading</span><span class="p">,</span> <span class="nx">setLoading</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">error</span><span class="p">,</span> <span class="nx">setError</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>

  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">controller</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AbortController</span><span class="p">();</span>
    <span class="nf">setLoading</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>

    <span class="nf">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span> <span class="na">signal</span><span class="p">:</span> <span class="nx">controller</span><span class="p">.</span><span class="nx">signal</span> <span class="p">})</span>
      <span class="p">.</span><span class="nf">then</span><span class="p">(</span><span class="nx">res</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">res</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span><span class="s2">`HTTP </span><span class="p">${</span><span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">();</span>
      <span class="p">})</span>
      <span class="p">.</span><span class="nf">then</span><span class="p">(</span><span class="nx">setData</span><span class="p">)</span>
      <span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">err</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">if </span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">name</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">AbortError</span><span class="dl">'</span><span class="p">)</span> <span class="nf">setError</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
      <span class="p">})</span>
      <span class="p">.</span><span class="k">finally</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nf">setLoading</span><span class="p">(</span><span class="kc">false</span><span class="p">));</span>

    <span class="k">return </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">controller</span><span class="p">.</span><span class="nf">abort</span><span class="p">();</span> <span class="c1">// cleanup: 컴포넌트 언마운트 시 요청 취소</span>
  <span class="p">},</span> <span class="p">[</span><span class="nx">url</span><span class="p">]);</span>

  <span class="k">return</span> <span class="p">{</span> <span class="nx">data</span><span class="p">,</span> <span class="nx">loading</span><span class="p">,</span> <span class="nx">error</span> <span class="p">};</span>
<span class="p">}</span>

<span class="c1">// 사용</span>
<span class="kd">function</span> <span class="nf">UserList</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="na">data</span><span class="p">:</span> <span class="nx">users</span><span class="p">,</span> <span class="nx">loading</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">useFetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/users</span><span class="dl">'</span><span class="p">);</span>

  <span class="k">if </span><span class="p">(</span><span class="nx">loading</span><span class="p">)</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>로딩 중...<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;;</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>에러: <span class="si">{</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;;</span>
  <span class="k">return</span> <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">users</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="nx">u</span> <span class="o">=&gt;</span> <span class="p">&lt;</span><span class="nt">li</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">u</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">u</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;)</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="uselocalstorage--localstorage와-상태-동기화">useLocalStorage — localStorage와 상태 동기화</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nf">useLocalStorage</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">initialValue</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">value</span><span class="p">,</span> <span class="nx">setValue</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">stored</span> <span class="o">=</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nf">getItem</span><span class="p">(</span><span class="nx">key</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">stored</span> <span class="o">!==</span> <span class="kc">null</span> <span class="p">?</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">stored</span><span class="p">)</span> <span class="p">:</span> <span class="nx">initialValue</span><span class="p">;</span>
  <span class="p">});</span>

  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">localStorage</span><span class="p">.</span><span class="nf">setItem</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">value</span><span class="p">));</span>
  <span class="p">},</span> <span class="p">[</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">]);</span>

  <span class="k">return</span> <span class="p">[</span><span class="nx">value</span><span class="p">,</span> <span class="nx">setValue</span><span class="p">];</span>
<span class="p">}</span>

<span class="c1">// 사용</span>
<span class="kd">function</span> <span class="nf">Settings</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">theme</span><span class="p">,</span> <span class="nx">setTheme</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useLocalStorage</span><span class="p">(</span><span class="dl">'</span><span class="s1">theme</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">light</span><span class="dl">'</span><span class="p">);</span>
  <span class="k">return </span><span class="p">(</span>
    <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nf">setTheme</span><span class="p">(</span><span class="nx">theme</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">light</span><span class="dl">'</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">dark</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">light</span><span class="dl">'</span><span class="p">)</span><span class="si">}</span><span class="p">&gt;</span>
      현재 테마: <span class="si">{</span><span class="nx">theme</span><span class="si">}</span>
    <span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="usedebounce--입력-디바운스">useDebounce — 입력 디바운스</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
</pre></td><td class="rouge-code"><pre><span class="kd">function</span> <span class="nf">useDebounce</span><span class="p">(</span><span class="nx">value</span><span class="p">,</span> <span class="nx">delay</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">debounced</span><span class="p">,</span> <span class="nx">setDebounced</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="nx">value</span><span class="p">);</span>

  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">timer</span> <span class="o">=</span> <span class="nf">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nf">setDebounced</span><span class="p">(</span><span class="nx">value</span><span class="p">),</span> <span class="nx">delay</span><span class="p">);</span>
    <span class="k">return </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nf">clearTimeout</span><span class="p">(</span><span class="nx">timer</span><span class="p">);</span>
  <span class="p">},</span> <span class="p">[</span><span class="nx">value</span><span class="p">,</span> <span class="nx">delay</span><span class="p">]);</span>

  <span class="k">return</span> <span class="nx">debounced</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// 사용: 검색 입력 디바운스</span>
<span class="kd">function</span> <span class="nf">SearchBar</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">query</span><span class="p">,</span> <span class="nx">setQuery</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="dl">''</span><span class="p">);</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">results</span><span class="p">,</span> <span class="nx">setResults</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">([]);</span>
  <span class="kd">const</span> <span class="nx">debouncedQuery</span> <span class="o">=</span> <span class="nf">useDebounce</span><span class="p">(</span><span class="nx">query</span><span class="p">,</span> <span class="mi">300</span><span class="p">);</span>

  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">debouncedQuery</span><span class="p">)</span> <span class="p">{</span>
      <span class="nf">searchApi</span><span class="p">(</span><span class="nx">debouncedQuery</span><span class="p">).</span><span class="nf">then</span><span class="p">(</span><span class="nx">setResults</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">},</span> <span class="p">[</span><span class="nx">debouncedQuery</span><span class="p">]);</span>

  <span class="k">return</span> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">query</span><span class="si">}</span> <span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">e</span> <span class="o">=&gt;</span> <span class="nf">setQuery</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">)</span><span class="si">}</span> <span class="p">/&gt;;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Custom Hook 설계 원칙:</p>

<ol>
  <li><strong>하나의 관심사에 집중</strong> — useFetch는 API 호출만, useLocalStorage는 로컬 저장소만</li>
  <li><strong>반환 값은 사용처에 맞게</strong> — 단일 값이면 값 자체를, 여러 값이면 객체로</li>
  <li><strong>cleanup을 잊지 말 것</strong> — 타이머, 구독, 요청 취소</li>
  <li><strong>Hook 규칙을 준수</strong> — 내부에서 다른 Hook을 조건부로 호출하지 않기</li>
</ol>

<hr />

<h2 id="hook의-규칙과-그-이유">Hook의 규칙과 그 이유</h2>

<h3 id="규칙-1-최상위에서만-호출">규칙 1: 최상위에서만 호출</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="c1">// ❌ 조건문 안에서 호출</span>
<span class="k">if </span><span class="p">(</span><span class="nx">isLoggedIn</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">user</span><span class="p">,</span> <span class="nx">setUser</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span> <span class="c1">// 금지!</span>
<span class="p">}</span>

<span class="c1">// ❌ 반복문 안에서 호출</span>
<span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">item</span> <span class="k">of</span> <span class="nx">items</span><span class="p">)</span> <span class="p">{</span>
  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span> <span class="c1">// 금지!</span>
<span class="p">}</span>

<span class="c1">// ✅ 항상 컴포넌트/Hook 최상위에서 호출</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">user</span><span class="p">,</span> <span class="nx">setUser</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">items</span><span class="p">,</span> <span class="nx">setItems</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">([]);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>이유:</strong> React는 Hook을 <strong>호출 순서</strong>로 식별한다. 내부적으로 Hook 상태를 배열(또는 연결 리스트)로 관리하며, 매 렌더링마다 같은 순서로 호출되어야 올바른 상태를 매칭할 수 있다. 조건부 호출은 순서를 깨뜨린다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>// 첫 렌더링:  useState(0) → useEffect → useState('')
//              Hook #0       Hook #1      Hook #2
//
// isLoggedIn이 false가 되면:
// 두번째 렌더링: useEffect → useState('')
//                Hook #0(!!!)  Hook #1(!!!)
// → Hook #0이 useState가 아닌 useEffect와 매칭 → 💥
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="규칙-2-react-함수-안에서만-호출">규칙 2: React 함수 안에서만 호출</h3>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="c1">// ❌ 일반 함수에서 호출</span>
<span class="kd">function</span> <span class="nf">formatData</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">formatted</span><span class="p">,</span> <span class="nx">setFormatted</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span> <span class="c1">// 금지!</span>
<span class="p">}</span>

<span class="c1">// ✅ React 컴포넌트에서 호출</span>
<span class="kd">function</span> <span class="nf">DataDisplay</span><span class="p">({</span> <span class="nx">data</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">formatted</span><span class="p">,</span> <span class="nx">setFormatted</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// ✅ Custom Hook에서 호출</span>
<span class="kd">function</span> <span class="nf">useFormattedData</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">formatted</span><span class="p">,</span> <span class="nx">setFormatted</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">formatted</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>ESLint 플러그인 <code class="language-plaintext highlighter-rouge">eslint-plugin-react-hooks</code>를 설치하면 이 규칙 위반을 빌드 시점에 잡아준다:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>npm <span class="nb">install </span>eslint-plugin-react-hooks <span class="nt">--save-dev</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="p">{</span><span class="w">
  </span><span class="nl">"plugins"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"react-hooks"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"rules"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"react-hooks/rules-of-hooks"</span><span class="p">:</span><span class="w"> </span><span class="s2">"error"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"react-hooks/exhaustive-deps"</span><span class="p">:</span><span class="w"> </span><span class="s2">"warn"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="마무리">마무리</h2>

<p>Hook은 단순한 API 변경이 아니라 <strong>React의 사고방식 자체를 바꾼 전환점</strong>이었다. 생명주기 중심에서 동기화 중심으로, 상속 기반에서 합성 기반으로의 전환이다.</p>

<p>핵심 정리:</p>

<table>
  <thead>
    <tr>
      <th>Hook</th>
      <th>용도</th>
      <th>주의점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">useState</code></td>
      <td>상태 관리</td>
      <td>비동기 업데이트, 객체 불변성</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">useEffect</code></td>
      <td>부수 효과</td>
      <td>의존성 배열, cleanup, stale closure</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">useCallback</code></td>
      <td>함수 메모이제이션</td>
      <td>React.memo와 함께 써야 의미 있음</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">useMemo</code></td>
      <td>값 메모이제이션</td>
      <td>측정 후 적용, 단순 계산에 남용 금지</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">useRef</code></td>
      <td>DOM 접근 / 뮤터블 값</td>
      <td>변경해도 리렌더링 안 됨</td>
    </tr>
    <tr>
      <td>Custom Hook</td>
      <td>로직 재사용</td>
      <td>use 접두사, Hook 규칙 준수</td>
    </tr>
  </tbody>
</table>

<p>클래스 컴포넌트를 억지로 Hook으로 바꿀 필요는 없다. 하지만 새 코드를 작성한다면 Hook이 기본이다. 공식 문서에서도 함수 컴포넌트 + Hook을 권장하고 있으며, React의 최신 기능(Server Components, Suspense 등)도 함수 컴포넌트를 전제로 설계되고 있다.</p>
]]></content:encoded>
        <pubDate>Wed, 01 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/react/2026/04/01/react-hooks-deep-dive/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/react/2026/04/01/react-hooks-deep-dive/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>React</category>
        
        <category>JavaScript</category>
        
        <category>Frontend</category>
        
        
        <category>react</category>
        
      </item>
    
      <item>
        <title>Kotlin Coroutines 실전 가이드</title>
        <description>Kotlin의 코루틴은 비동기 프로그래밍을 동기 코드처럼 작성할 수 있게 해주는 경량 동시성 프레임워크다. 이 글에서는 기본 개념부터 실전 패턴까지 정리한다.</description>
        <content:encoded><![CDATA[<p>Kotlin의 코루틴은 비동기 프로그래밍을 <strong>동기 코드처럼</strong> 작성할 수 있게 해주는 경량 동시성 프레임워크다. 이 글에서는 기본 개념부터 실전 패턴까지 정리한다.</p>

<p><br /></p>

<h2 id="1-coroutine-기본-개념">1. Coroutine 기본 개념</h2>

<h3 id="11-suspend-함수">1.1 suspend 함수</h3>

<p><code class="language-plaintext highlighter-rouge">suspend</code> 키워드는 함수가 <strong>일시 중단(suspend)</strong> 될 수 있음을 표시한다. suspend 함수는 코루틴 내부 또는 다른 suspend 함수에서만 호출할 수 있다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">suspend</span> <span class="k">fun</span> <span class="nf">fetchUserData</span><span class="p">(</span><span class="n">userId</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">User</span> <span class="p">{</span>
    <span class="c1">// 네트워크 호출 — 스레드를 블로킹하지 않고 일시 중단</span>
    <span class="k">return</span> <span class="n">apiService</span><span class="p">.</span><span class="nf">getUser</span><span class="p">(</span><span class="n">userId</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>일반 함수와의 차이:</p>
<ul>
  <li>일반 함수: 호출하면 완료될 때까지 스레드를 점유</li>
  <li>suspend 함수: 중간에 실행을 양보(suspend)하고, 나중에 재개(resume)할 수 있음</li>
</ul>

<h3 id="12-launch--fire-and-forget">1.2 launch — Fire and Forget</h3>

<p><code class="language-plaintext highlighter-rouge">launch</code>는 결과를 반환하지 않는 코루틴을 시작한다. 반환 타입은 <code class="language-plaintext highlighter-rouge">Job</code>이다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">job</span><span class="p">:</span> <span class="nc">Job</span> <span class="p">=</span> <span class="nf">launch</span> <span class="p">{</span>
        <span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"World!"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"Hello,"</span><span class="p">)</span>
    <span class="n">job</span><span class="p">.</span><span class="nf">join</span><span class="p">()</span> <span class="c1">// 코루틴 완료 대기</span>
<span class="p">}</span>
<span class="c1">// 출력:</span>
<span class="c1">// Hello,</span>
<span class="c1">// World!</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="13-asyncawait--결과-반환">1.3 async/await — 결과 반환</h3>

<p><code class="language-plaintext highlighter-rouge">async</code>는 결과를 반환하는 코루틴을 시작한다. 반환 타입은 <code class="language-plaintext highlighter-rouge">Deferred&lt;T&gt;</code>이며, <code class="language-plaintext highlighter-rouge">await()</code>로 결과를 받는다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">deferred</span><span class="p">:</span> <span class="nc">Deferred</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">async</span> <span class="p">{</span>
        <span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span>
        <span class="mi">42</span>
    <span class="p">}</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"계산 결과: ${deferred.await()}"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>빌더</th>
      <th>반환 타입</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">launch</code></td>
      <td><code class="language-plaintext highlighter-rouge">Job</code></td>
      <td>결과가 필요 없는 비동기 작업</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">async</code></td>
      <td><code class="language-plaintext highlighter-rouge">Deferred&lt;T&gt;</code></td>
      <td>결과를 반환하는 비동기 작업</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">runBlocking</code></td>
      <td><code class="language-plaintext highlighter-rouge">T</code></td>
      <td>메인 함수/테스트에서 코루틴 진입점</td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<h2 id="2-coroutinescope-coroutinecontext-dispatcher">2. CoroutineScope, CoroutineContext, Dispatcher</h2>

<h3 id="21-coroutinescope">2.1 CoroutineScope</h3>

<p>모든 코루틴은 <strong>CoroutineScope</strong> 안에서 실행된다. Scope는 코루틴의 생명주기를 관리한다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="kd">class</span> <span class="nc">UserRepository</span> <span class="p">{</span>
    <span class="c1">// 자체 스코프 — 필요 시 전체 취소 가능</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">scope</span> <span class="p">=</span> <span class="nc">CoroutineScope</span><span class="p">(</span><span class="nc">SupervisorJob</span><span class="p">()</span> <span class="p">+</span> <span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">IO</span><span class="p">)</span>

    <span class="k">fun</span> <span class="nf">fetchUsers</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">scope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">users</span> <span class="p">=</span> <span class="n">apiService</span><span class="p">.</span><span class="nf">getUsers</span><span class="p">()</span>
            <span class="c1">// ...</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">clear</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">scope</span><span class="p">.</span><span class="nf">cancel</span><span class="p">()</span> <span class="c1">// 소속 코루틴 전체 취소</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Android에서는 <code class="language-plaintext highlighter-rouge">viewModelScope</code>, <code class="language-plaintext highlighter-rouge">lifecycleScope</code> 등 미리 정의된 스코프를 활용한다.</p>

<h3 id="22-coroutinecontext">2.2 CoroutineContext</h3>

<p>CoroutineContext는 코루틴의 실행 환경을 정의하는 <strong>불변 요소 집합</strong>이다.</p>

<p>주요 요소:</p>
<ul>
  <li><strong>Job</strong>: 코루틴의 생명주기 관리</li>
  <li><strong>Dispatcher</strong>: 어떤 스레드에서 실행할지</li>
  <li><strong>CoroutineName</strong>: 디버깅용 이름</li>
  <li><strong>CoroutineExceptionHandler</strong>: 예외 처리기</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">context</span> <span class="p">=</span> <span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">IO</span> <span class="p">+</span> <span class="nc">CoroutineName</span><span class="p">(</span><span class="s">"data-loader"</span><span class="p">)</span> <span class="p">+</span> <span class="n">exceptionHandler</span>
<span class="nf">launch</span><span class="p">(</span><span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// IO 디스패처에서 "data-loader"라는 이름으로 실행</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="23-dispatcher-종류">2.3 Dispatcher 종류</h3>

<table>
  <thead>
    <tr>
      <th>Dispatcher</th>
      <th>스레드 풀</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Dispatchers.Main</code></td>
      <td>메인(UI) 스레드</td>
      <td>UI 업데이트, 가벼운 작업</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Dispatchers.IO</code></td>
      <td>공유 스레드 풀 (64개)</td>
      <td>네트워크, DB, 파일 I/O</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Dispatchers.Default</code></td>
      <td>CPU 코어 수만큼</td>
      <td>CPU 집약적 연산 (정렬, JSON 파싱)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Dispatchers.Unconfined</code></td>
      <td>호출 스레드 → 재개 스레드</td>
      <td>특수 케이스, 테스트</td>
    </tr>
  </tbody>
</table>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nf">launch</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">IO</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">data</span> <span class="p">=</span> <span class="nf">fetchFromNetwork</span><span class="p">()</span>        <span class="c1">// IO 스레드</span>
    <span class="nf">withContext</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">Main</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">updateUI</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>                    <span class="c1">// 메인 스레드로 전환</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="3-structured-concurrency-패턴-및-job-계층-구조">3. Structured Concurrency 패턴 및 Job 계층 구조</h2>

<h3 id="31-structured-concurrency-원칙">3.1 Structured Concurrency 원칙</h3>

<p>Kotlin 코루틴의 핵심 설계 철학은 <strong>Structured Concurrency</strong>다:</p>

<ol>
  <li>모든 코루틴은 <strong>부모 스코프</strong> 안에서 실행된다.</li>
  <li>부모가 취소되면 <strong>자식 코루틴도 모두 취소</strong>된다.</li>
  <li>자식이 실패하면 <strong>부모에게 전파</strong>된다.</li>
  <li>부모는 <strong>모든 자식이 완료될 때까지</strong> 완료되지 않는다.</li>
</ol>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>       <span class="c1">// 부모</span>
    <span class="nf">launch</span> <span class="p">{</span>                      <span class="c1">// 자식 1</span>
        <span class="nf">delay</span><span class="p">(</span><span class="mi">2000L</span><span class="p">)</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"자식 1 완료"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nf">launch</span> <span class="p">{</span>                      <span class="c1">// 자식 2</span>
        <span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"자식 2 완료"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="c1">// runBlocking은 두 자식이 모두 완료될 때까지 대기</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="32-job-계층-구조">3.2 Job 계층 구조</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>runBlocking (Job)
├── launch (Job - 자식 1)
│   └── launch (Job - 손자 1)
└── launch (Job - 자식 2)
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>취소 전파</strong>: 부모 Job 취소 → 모든 자식 취소</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">parentJob</span> <span class="p">=</span> <span class="nf">launch</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">child1</span> <span class="p">=</span> <span class="nf">launch</span> <span class="p">{</span>
            <span class="nf">repeat</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="p">-&gt;</span>
                <span class="nf">println</span><span class="p">(</span><span class="s">"자식 1: $i"</span><span class="p">)</span>
                <span class="nf">delay</span><span class="p">(</span><span class="mi">500L</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="kd">val</span> <span class="py">child2</span> <span class="p">=</span> <span class="nf">launch</span> <span class="p">{</span>
            <span class="nf">repeat</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="p">-&gt;</span>
                <span class="nf">println</span><span class="p">(</span><span class="s">"자식 2: $i"</span><span class="p">)</span>
                <span class="nf">delay</span><span class="p">(</span><span class="mi">300L</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="nf">delay</span><span class="p">(</span><span class="mi">1300L</span><span class="p">)</span>
    <span class="n">parentJob</span><span class="p">.</span><span class="nf">cancel</span><span class="p">()</span>  <span class="c1">// 자식 1, 자식 2 모두 취소됨</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"부모 취소 완료"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="33-coroutinescope-vs-supervisorscope">3.3 coroutineScope vs supervisorScope</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c1">// coroutineScope: 자식 하나가 실패하면 나머지도 취소</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">failFast</span><span class="p">()</span> <span class="p">=</span> <span class="nf">coroutineScope</span> <span class="p">{</span>
    <span class="nf">launch</span> <span class="p">{</span> <span class="k">throw</span> <span class="nc">RuntimeException</span><span class="p">(</span><span class="s">"실패!"</span><span class="p">)</span> <span class="p">}</span>  <span class="c1">// 전체 스코프 취소</span>
    <span class="nf">launch</span> <span class="p">{</span> <span class="nf">delay</span><span class="p">(</span><span class="nc">Long</span><span class="p">.</span><span class="nc">MAX_VALUE</span><span class="p">)</span> <span class="p">}</span>             <span class="c1">// 같이 취소됨</span>
<span class="p">}</span>

<span class="c1">// supervisorScope: 자식 실패가 형제에게 전파되지 않음</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">failIsolated</span><span class="p">()</span> <span class="p">=</span> <span class="nf">supervisorScope</span> <span class="p">{</span>
    <span class="nf">launch</span> <span class="p">{</span> <span class="k">throw</span> <span class="nc">RuntimeException</span><span class="p">(</span><span class="s">"실패!"</span><span class="p">)</span> <span class="p">}</span>  <span class="c1">// 이것만 실패</span>
    <span class="nf">launch</span> <span class="p">{</span>
        <span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"나는 계속 실행됨"</span><span class="p">)</span>              <span class="c1">// 정상 실행</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="4-실전-예제">4. 실전 예제</h2>

<h3 id="41-여러-api-병렬-호출-asyncawait">4.1 여러 API 병렬 호출 (async/await)</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="k">suspend</span> <span class="k">fun</span> <span class="nf">loadDashboard</span><span class="p">(</span><span class="n">userId</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">Dashboard</span> <span class="p">=</span> <span class="nf">coroutineScope</span> <span class="p">{</span>
    <span class="c1">// 세 API를 병렬로 호출</span>
    <span class="kd">val</span> <span class="py">userDeferred</span> <span class="p">=</span> <span class="nf">async</span> <span class="p">{</span> <span class="n">userApi</span><span class="p">.</span><span class="nf">getUser</span><span class="p">(</span><span class="n">userId</span><span class="p">)</span> <span class="p">}</span>
    <span class="kd">val</span> <span class="py">ordersDeferred</span> <span class="p">=</span> <span class="nf">async</span> <span class="p">{</span> <span class="n">orderApi</span><span class="p">.</span><span class="nf">getOrders</span><span class="p">(</span><span class="n">userId</span><span class="p">)</span> <span class="p">}</span>
    <span class="kd">val</span> <span class="py">recommendsDeferred</span> <span class="p">=</span> <span class="nf">async</span> <span class="p">{</span> <span class="n">recommendApi</span><span class="p">.</span><span class="nf">getRecommendations</span><span class="p">(</span><span class="n">userId</span><span class="p">)</span> <span class="p">}</span>

    <span class="c1">// 모든 결과를 모아서 반환</span>
    <span class="nc">Dashboard</span><span class="p">(</span>
        <span class="n">user</span> <span class="p">=</span> <span class="n">userDeferred</span><span class="p">.</span><span class="nf">await</span><span class="p">(),</span>
        <span class="n">orders</span> <span class="p">=</span> <span class="n">ordersDeferred</span><span class="p">.</span><span class="nf">await</span><span class="p">(),</span>
        <span class="n">recommendations</span> <span class="p">=</span> <span class="n">recommendsDeferred</span><span class="p">.</span><span class="nf">await</span><span class="p">()</span>
    <span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>순차 실행 시 3초 걸리는 작업이 병렬로 <strong>1초</strong>에 완료된다 (각 API가 1초라고 가정).</p>

<h3 id="42-타임아웃-처리-withtimeout">4.2 타임아웃 처리 (withTimeout)</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="k">suspend</span> <span class="k">fun</span> <span class="nf">fetchWithTimeout</span><span class="p">():</span> <span class="nc">String</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">try</span> <span class="p">{</span>
        <span class="nf">withTimeout</span><span class="p">(</span><span class="mi">3000L</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// 3초 안에 완료되지 않으면 TimeoutCancellationException</span>
            <span class="n">slowApi</span><span class="p">.</span><span class="nf">fetchData</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">TimeoutCancellationException</span><span class="p">)</span> <span class="p">{</span>
        <span class="s">"기본값 (타임아웃)"</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// null 반환 버전</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">fetchOrNull</span><span class="p">():</span> <span class="nc">String</span><span class="p">?</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nf">withTimeoutOrNull</span><span class="p">(</span><span class="mi">3000L</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">slowApi</span><span class="p">.</span><span class="nf">fetchData</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="43-재시도-패턴">4.3 재시도 패턴</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="k">suspend</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="nf">retry</span><span class="p">(</span>
    <span class="n">times</span><span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">3</span><span class="p">,</span>
    <span class="n">initialDelay</span><span class="p">:</span> <span class="nc">Long</span> <span class="p">=</span> <span class="mi">100L</span><span class="p">,</span>
    <span class="n">factor</span><span class="p">:</span> <span class="nc">Double</span> <span class="p">=</span> <span class="mf">2.0</span><span class="p">,</span>
    <span class="n">block</span><span class="p">:</span> <span class="k">suspend</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">T</span>
<span class="p">):</span> <span class="nc">T</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="py">currentDelay</span> <span class="p">=</span> <span class="n">initialDelay</span>
    <span class="nf">repeat</span><span class="p">(</span><span class="n">times</span> <span class="p">-</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">try</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nf">block</span><span class="p">()</span>
        <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">Exception</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">println</span><span class="p">(</span><span class="s">"재시도 ${it + 1}/$times — ${e.message}"</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="nf">delay</span><span class="p">(</span><span class="n">currentDelay</span><span class="p">)</span>
        <span class="n">currentDelay</span> <span class="p">=</span> <span class="p">(</span><span class="n">currentDelay</span> <span class="p">*</span> <span class="n">factor</span><span class="p">).</span><span class="nf">toLong</span><span class="p">()</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nf">block</span><span class="p">()</span> <span class="c1">// 마지막 시도 — 실패 시 예외 전파</span>
<span class="p">}</span>

<span class="c1">// 사용</span>
<span class="kd">val</span> <span class="py">result</span> <span class="p">=</span> <span class="nf">retry</span><span class="p">(</span><span class="n">times</span> <span class="p">=</span> <span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">apiService</span><span class="p">.</span><span class="nf">getUser</span><span class="p">(</span><span class="s">"user-123"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="5-flow-기초">5. Flow 기초</h2>

<p>Flow는 <strong>비동기 데이터 스트림</strong>이다. RxJava의 Observable과 유사하지만 코루틴 기반이다.</p>

<h3 id="51-cold-stream">5.1 Cold Stream</h3>

<p>Flow는 <strong>cold stream</strong>이다 — <code class="language-plaintext highlighter-rouge">collect</code>가 호출될 때까지 실행되지 않는다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">import</span> <span class="nn">kotlinx.coroutines.flow.*</span>

<span class="k">fun</span> <span class="nf">numberFlow</span><span class="p">():</span> <span class="nc">Flow</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">5</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">delay</span><span class="p">(</span><span class="mi">100L</span><span class="p">)</span>
        <span class="nf">emit</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>  <span class="c1">// 값 방출</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
    <span class="nf">numberFlow</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-&gt;</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"수신: $value"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="52-flow-연산자">5.2 Flow 연산자</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
    <span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">10</span><span class="p">).</span><span class="nf">asFlow</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">it</span> <span class="p">%</span> <span class="mi">2</span> <span class="p">==</span> <span class="mi">0</span> <span class="p">}</span>           <span class="c1">// 짝수만</span>
        <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span> <span class="p">*</span> <span class="n">it</span> <span class="p">}</span>                    <span class="c1">// 제곱</span>
        <span class="p">.</span><span class="nf">take</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>                            <span class="c1">// 처음 3개만</span>
        <span class="p">.</span><span class="nf">collect</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="n">it</span><span class="p">)</span> <span class="p">}</span>            <span class="c1">// 4, 16, 36</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>주요 연산자 정리:</p>

<table>
  <thead>
    <tr>
      <th>연산자</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">map</code></td>
      <td>각 값 변환</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">filter</code></td>
      <td>조건에 맞는 값만 통과</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">take</code></td>
      <td>처음 N개만</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">drop</code></td>
      <td>처음 N개 건너뛰기</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">onEach</code></td>
      <td>각 값에 대해 부수 효과 실행</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">flatMapConcat</code></td>
      <td>각 값을 새 Flow로 변환 후 순차 연결</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">flatMapMerge</code></td>
      <td>각 값을 새 Flow로 변환 후 병렬 수집</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">combine</code></td>
      <td>두 Flow의 최신 값 조합</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">zip</code></td>
      <td>두 Flow의 값을 1:1 매칭</td>
    </tr>
  </tbody>
</table>

<h3 id="53-stateflow와-sharedflow">5.3 StateFlow와 SharedFlow</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="kd">class</span> <span class="nc">UserViewModel</span> <span class="p">:</span> <span class="nc">ViewModel</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// StateFlow — 항상 최신 값을 가지고 있음 (LiveData 대체)</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">_uiState</span> <span class="p">=</span> <span class="nc">MutableStateFlow</span><span class="p">(</span><span class="nc">UiState</span><span class="p">.</span><span class="nc">Loading</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">uiState</span><span class="p">:</span> <span class="nc">StateFlow</span><span class="p">&lt;</span><span class="nc">UiState</span><span class="p">&gt;</span> <span class="p">=</span> <span class="n">_uiState</span><span class="p">.</span><span class="nf">asStateFlow</span><span class="p">()</span>

    <span class="c1">// SharedFlow — 이벤트 전달 (일회성 이벤트)</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">_events</span> <span class="p">=</span> <span class="nc">MutableSharedFlow</span><span class="p">&lt;</span><span class="nc">Event</span><span class="p">&gt;()</span>
    <span class="kd">val</span> <span class="py">events</span><span class="p">:</span> <span class="nc">SharedFlow</span><span class="p">&lt;</span><span class="nc">Event</span><span class="p">&gt;</span> <span class="p">=</span> <span class="n">_events</span><span class="p">.</span><span class="nf">asSharedFlow</span><span class="p">()</span>

    <span class="k">fun</span> <span class="nf">loadUser</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">viewModelScope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
            <span class="n">_uiState</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="nc">UiState</span><span class="p">.</span><span class="nc">Loading</span>
            <span class="k">try</span> <span class="p">{</span>
                <span class="kd">val</span> <span class="py">user</span> <span class="p">=</span> <span class="n">userRepository</span><span class="p">.</span><span class="nf">getUser</span><span class="p">(</span><span class="n">id</span><span class="p">)</span>
                <span class="n">_uiState</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="nc">UiState</span><span class="p">.</span><span class="nc">Success</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
            <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">Exception</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">_uiState</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="nc">UiState</span><span class="p">.</span><span class="nc">Error</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">message</span><span class="p">)</span>
                <span class="n">_events</span><span class="p">.</span><span class="nf">emit</span><span class="p">(</span><span class="nc">Event</span><span class="p">.</span><span class="nc">ShowSnackbar</span><span class="p">(</span><span class="s">"로드 실패"</span><span class="p">))</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="6-코루틴-예외-처리">6. 코루틴 예외 처리</h2>

<h3 id="61-launch-vs-async-예외-전파-차이">6.1 launch vs async 예외 전파 차이</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
    <span class="c1">// launch: 예외가 즉시 부모로 전파됨</span>
    <span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="nf">launch</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="nc">RuntimeException</span><span class="p">(</span><span class="s">"launch 에러"</span><span class="p">)</span>
        <span class="c1">// → 부모 코루틴까지 전파, try-catch로 잡을 수 없음</span>
    <span class="p">}</span>

    <span class="c1">// async: await() 호출 시 예외 발생</span>
    <span class="kd">val</span> <span class="py">deferred</span> <span class="p">=</span> <span class="nf">async</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="nc">RuntimeException</span><span class="p">(</span><span class="s">"async 에러"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">try</span> <span class="p">{</span>
        <span class="n">deferred</span><span class="p">.</span><span class="nf">await</span><span class="p">()</span>  <span class="c1">// 여기서 예외 발생</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">RuntimeException</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"잡았다: ${e.message}"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="62-coroutineexceptionhandler">6.2 CoroutineExceptionHandler</h3>

<p><code class="language-plaintext highlighter-rouge">launch</code>의 예외를 잡기 위해 <strong>CoroutineExceptionHandler</strong>를 사용한다. 이 핸들러는 <strong>루트 코루틴</strong>에만 적용된다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">handler</span> <span class="p">=</span> <span class="nc">CoroutineExceptionHandler</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">exception</span> <span class="p">-&gt;</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"예외 발생: ${exception.message}"</span><span class="p">)</span>
    <span class="c1">// 로깅, 알림 등 처리</span>
<span class="p">}</span>

<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">scope</span> <span class="p">=</span> <span class="nc">CoroutineScope</span><span class="p">(</span><span class="nc">SupervisorJob</span><span class="p">()</span> <span class="p">+</span> <span class="n">handler</span><span class="p">)</span>

    <span class="n">scope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="nc">RuntimeException</span><span class="p">(</span><span class="s">"문제 발생!"</span><span class="p">)</span>
        <span class="c1">// → handler에서 처리됨</span>
    <span class="p">}</span>

    <span class="n">scope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
        <span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"나는 정상 실행"</span><span class="p">)</span>  <span class="c1">// SupervisorJob 덕분에 영향 없음</span>
    <span class="p">}</span>

    <span class="nf">delay</span><span class="p">(</span><span class="mi">2000L</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="63-supervisorjob">6.3 SupervisorJob</h3>

<p><code class="language-plaintext highlighter-rouge">SupervisorJob</code>은 자식의 실패가 다른 자식에게 전파되지 않게 한다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">scope</span> <span class="p">=</span> <span class="nc">CoroutineScope</span><span class="p">(</span><span class="nc">SupervisorJob</span><span class="p">()</span> <span class="p">+</span> <span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">IO</span><span class="p">)</span>

<span class="n">scope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
    <span class="c1">// 자식 1 — 실패해도 자식 2에 영향 없음</span>
    <span class="k">throw</span> <span class="nc">RuntimeException</span><span class="p">(</span><span class="s">"자식 1 실패"</span><span class="p">)</span>
<span class="p">}</span>

<span class="n">scope</span><span class="p">.</span><span class="nf">launch</span> <span class="p">{</span>
    <span class="c1">// 자식 2 — 정상 실행 계속</span>
    <span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"자식 2 완료"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>일반 Job vs SupervisorJob 비교:</strong></p>

<table>
  <thead>
    <tr>
      <th>특성</th>
      <th>Job</th>
      <th>SupervisorJob</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>자식 실패 시</td>
      <td>다른 자식 모두 취소</td>
      <td>실패한 자식만 취소</td>
    </tr>
    <tr>
      <td>부모 영향</td>
      <td>부모도 취소</td>
      <td>부모 유지</td>
    </tr>
    <tr>
      <td>사용 시점</td>
      <td>전체가 하나의 작업일 때</td>
      <td>독립적인 작업들을 관리할 때</td>
    </tr>
  </tbody>
</table>

<h3 id="64-실전-예외-처리-패턴">6.4 실전 예외 처리 패턴</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="k">sealed</span> <span class="kd">class</span> <span class="nc">Result</span><span class="p">&lt;</span><span class="k">out</span> <span class="nc">T</span><span class="p">&gt;</span> <span class="p">{</span>
    <span class="kd">data class</span> <span class="nc">Success</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;(</span><span class="kd">val</span> <span class="py">data</span><span class="p">:</span> <span class="nc">T</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Result</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;()</span>
    <span class="kd">data class</span> <span class="nc">Error</span><span class="p">(</span><span class="kd">val</span> <span class="py">exception</span><span class="p">:</span> <span class="nc">Throwable</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Result</span><span class="p">&lt;</span><span class="nc">Nothing</span><span class="p">&gt;()</span>
<span class="p">}</span>

<span class="k">suspend</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="nf">safeApiCall</span><span class="p">(</span><span class="n">block</span><span class="p">:</span> <span class="k">suspend</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">T</span><span class="p">):</span> <span class="nc">Result</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">try</span> <span class="p">{</span>
        <span class="nc">Result</span><span class="p">.</span><span class="nc">Success</span><span class="p">(</span><span class="nf">block</span><span class="p">())</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">CancellationException</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="n">e</span>  <span class="c1">// 취소 예외는 반드시 재던져야 함!</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">Exception</span><span class="p">)</span> <span class="p">{</span>
        <span class="nc">Result</span><span class="p">.</span><span class="nc">Error</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 사용</span>
<span class="kd">val</span> <span class="py">result</span> <span class="p">=</span> <span class="nf">safeApiCall</span> <span class="p">{</span> <span class="n">apiService</span><span class="p">.</span><span class="nf">getUser</span><span class="p">(</span><span class="n">userId</span><span class="p">)</span> <span class="p">}</span>
<span class="k">when</span> <span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">is</span> <span class="nc">Result</span><span class="p">.</span><span class="nc">Success</span> <span class="p">-&gt;</span> <span class="nf">showUser</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">data</span><span class="p">)</span>
    <span class="k">is</span> <span class="nc">Result</span><span class="p">.</span><span class="nc">Error</span> <span class="p">-&gt;</span> <span class="nf">showError</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">exception</span><span class="p">.</span><span class="n">message</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>주의</strong>: <code class="language-plaintext highlighter-rouge">CancellationException</code>을 삼키면 코루틴 취소가 작동하지 않는다. 반드시 재던져야 한다.</p>
</blockquote>

<p><br /></p>

<h2 id="정리">정리</h2>

<table>
  <thead>
    <tr>
      <th>개념</th>
      <th>핵심</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">suspend</code></td>
      <td>일시 중단 가능한 함수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">launch</code> / <code class="language-plaintext highlighter-rouge">async</code></td>
      <td>코루틴 빌더 (fire-and-forget / 결과 반환)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Dispatcher</code></td>
      <td>실행 스레드 제어 (Main, IO, Default)</td>
    </tr>
    <tr>
      <td>Structured Concurrency</td>
      <td>부모-자식 관계로 생명주기 관리</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Flow</code></td>
      <td>비동기 cold stream</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SupervisorJob</code></td>
      <td>자식 실패 격리</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CoroutineExceptionHandler</code></td>
      <td>루트 코루틴 예외 처리</td>
    </tr>
  </tbody>
</table>

<p>코루틴은 단순히 스레드를 대체하는 도구가 아니라, <strong>구조화된 동시성</strong>을 통해 안전하고 관리 가능한 비동기 코드를 작성하게 해주는 설계 패러다임이다.</p>

<p><br /></p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://kotlinlang.org/docs/coroutines-overview.html">Coroutines overview — Kotlin Documentation</a></li>
  <li><a href="https://kotlinlang.org/docs/coroutines-guide.html">Coroutines guide — Kotlin Documentation</a></li>
  <li><a href="https://kotlinlang.org/docs/flow.html">Asynchronous Flow — Kotlin Documentation</a></li>
  <li><a href="https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html">Coroutine context and dispatchers — Kotlin Documentation</a></li>
  <li><a href="https://kotlinlang.org/docs/exception-handling.html">Coroutine exceptions handling — Kotlin Documentation</a></li>
  <li><a href="https://www.youtube.com/watch?v=a3agLJQ6DJUk">KotlinConf 2019 — Coroutines! — Roman Elizarov</a></li>
  <li><a href="https://github.com/Kotlin/kotlinx.coroutines">kotlinx.coroutines — GitHub</a></li>
</ul>
]]></content:encoded>
        <pubDate>Wed, 01 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/kotlin/2026/04/01/kotlin-coroutines-guide/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/kotlin/2026/04/01/kotlin-coroutines-guide/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Kotlin</category>
        
        <category>Coroutines</category>
        
        <category>Async</category>
        
        <category>Concurrency</category>
        
        
        <category>kotlin</category>
        
      </item>
    
      <item>
        <title>Kotlin 고급 문법</title>
        <description>Kotlin 고급 문법</description>
        <content:encoded><![CDATA[<h2 id="kotlin-고급-문법">Kotlin 고급 문법</h2>

<p>Kotlin의 <a href="/kotlin/2023/06/20/kotlin-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95/">기본 문법</a>에 이어, 실무와 코딩 테스트에서 자주 활용되는 고급 문법을 정리한다.</p>

<p><br /></p>

<h3 id="1-lambda">1. Lambda</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">square</span><span class="p">:</span> <span class="p">(</span><span class="nc">Int</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Int</span> <span class="p">=</span> <span class="p">{</span> <span class="n">number</span> <span class="p">-&gt;</span> <span class="n">number</span> <span class="p">*</span> <span class="n">number</span> <span class="p">}</span>

<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="nf">square</span><span class="p">(</span><span class="mi">5</span><span class="p">))</span> <span class="c1">// 25</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>람다식은 value처럼 다룰 수 있는 익명 함수이다. 변수에 저장하거나 함수의 인자로 전달할 수 있다.</p>

<h4 id="축약-표현">축약 표현</h4>

<p>파라미터가 하나인 경우 <code class="language-plaintext highlighter-rouge">it</code> 키워드로 축약할 수 있다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">double</span><span class="p">:</span> <span class="p">(</span><span class="nc">Int</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Int</span> <span class="p">=</span> <span class="p">{</span> <span class="n">it</span> <span class="p">*</span> <span class="mi">2</span> <span class="p">}</span>
<span class="kd">val</span> <span class="py">names</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="s">"Alice"</span><span class="p">,</span> <span class="s">"Bob"</span><span class="p">,</span> <span class="s">"Charlie"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">lengths</span> <span class="p">=</span> <span class="n">names</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">length</span> <span class="p">}</span> <span class="c1">// [5, 3, 7]</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="여러-줄-람다">여러 줄 람다</h4>

<p>마지막 표현식이 반환값이 된다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">processName</span><span class="p">:</span> <span class="p">(</span><span class="nc">String</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">String</span> <span class="p">=</span> <span class="p">{</span> <span class="n">name</span> <span class="p">-&gt;</span>
    <span class="kd">val</span> <span class="py">trimmed</span> <span class="p">=</span> <span class="n">name</span><span class="p">.</span><span class="nf">trim</span><span class="p">()</span>
    <span class="kd">val</span> <span class="py">upper</span> <span class="p">=</span> <span class="n">trimmed</span><span class="p">.</span><span class="nf">uppercase</span><span class="p">()</span>
    <span class="n">upper</span> <span class="c1">// 이 값이 반환됨</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h3 id="2-higher-order-functions-고차-함수">2. Higher-Order Functions (고차 함수)</h3>

<p>함수를 매개변수로 받거나 함수를 반환하는 함수이다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nf">operate</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="n">operation</span><span class="p">:</span> <span class="p">(</span><span class="nc">Int</span><span class="p">,</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Int</span><span class="p">):</span> <span class="nc">Int</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nf">operation</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">sum</span> <span class="p">=</span> <span class="nf">operate</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="p">-&gt;</span> <span class="n">x</span> <span class="p">+</span> <span class="n">y</span> <span class="p">}</span>
    <span class="kd">val</span> <span class="py">product</span> <span class="p">=</span> <span class="nf">operate</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="p">-&gt;</span> <span class="n">x</span> <span class="p">*</span> <span class="n">y</span> <span class="p">}</span>
    <span class="nf">println</span><span class="p">(</span><span class="n">sum</span><span class="p">)</span>     <span class="c1">// 7</span>
    <span class="nf">println</span><span class="p">(</span><span class="n">product</span><span class="p">)</span> <span class="c1">// 12</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>마지막 파라미터가 람다인 경우, 괄호 밖으로 뺄 수 있다 (trailing lambda).</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c1">// 아래 두 표현은 동일</span>
<span class="nf">listOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nf">filter</span><span class="p">({</span> <span class="n">it</span> <span class="p">&gt;</span> <span class="mi">1</span> <span class="p">})</span>
<span class="nf">listOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">it</span> <span class="p">&gt;</span> <span class="mi">1</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h3 id="3-extension-functions-확장-함수">3. Extension Functions (확장 함수)</h3>

<p>기존 클래스를 수정하지 않고 새로운 함수를 추가할 수 있다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nc">String</span><span class="p">.</span><span class="nf">addExclamation</span><span class="p">():</span> <span class="nc">String</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span> <span class="p">+</span> <span class="s">"!"</span>
<span class="p">}</span>

<span class="k">fun</span> <span class="nc">Int</span><span class="p">.</span><span class="nf">isEven</span><span class="p">():</span> <span class="nc">Boolean</span> <span class="p">=</span> <span class="k">this</span> <span class="p">%</span> <span class="mi">2</span> <span class="p">==</span> <span class="mi">0</span>

<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"Hello"</span><span class="p">.</span><span class="nf">addExclamation</span><span class="p">())</span> <span class="c1">// Hello!</span>
    <span class="nf">println</span><span class="p">(</span><span class="mi">4</span><span class="p">.</span><span class="nf">isEven</span><span class="p">())</span>               <span class="c1">// true</span>
    <span class="nf">println</span><span class="p">(</span><span class="mi">7</span><span class="p">.</span><span class="nf">isEven</span><span class="p">())</span>               <span class="c1">// false</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="실용적인-예시">실용적인 예시</h4>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="nf">List</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;.</span><span class="nf">secondOrNull</span><span class="p">():</span> <span class="nc">T</span><span class="p">?</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">size</span> <span class="p">&gt;=</span> <span class="mi">2</span><span class="p">)</span> <span class="k">this</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">else</span> <span class="k">null</span>
<span class="p">}</span>

<span class="k">fun</span> <span class="nc">String</span><span class="p">.</span><span class="nf">toSlug</span><span class="p">():</span> <span class="nc">String</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nf">lowercase</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="nc">Regex</span><span class="p">(</span><span class="s">"[^a-z0-9\\s-]"</span><span class="p">),</span> <span class="s">""</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="nc">Regex</span><span class="p">(</span><span class="s">"\\s+"</span><span class="p">),</span> <span class="s">"-"</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">trim</span><span class="p">(</span><span class="sc">'-'</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h3 id="4-scope-functions-범위-함수">4. Scope Functions (범위 함수)</h3>

<p>Kotlin은 객체의 컨텍스트 내에서 코드 블록을 실행하는 5개의 scope function을 제공한다.</p>

<table>
  <thead>
    <tr>
      <th>함수</th>
      <th>객체 참조</th>
      <th>반환값</th>
      <th>사용 사례</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">let</code></td>
      <td><code class="language-plaintext highlighter-rouge">it</code></td>
      <td>람다 결과</td>
      <td>null 체크 후 실행</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">run</code></td>
      <td><code class="language-plaintext highlighter-rouge">this</code></td>
      <td>람다 결과</td>
      <td>객체 설정 + 결과 계산</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">with</code></td>
      <td><code class="language-plaintext highlighter-rouge">this</code></td>
      <td>람다 결과</td>
      <td>객체의 여러 메서드 호출</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">apply</code></td>
      <td><code class="language-plaintext highlighter-rouge">this</code></td>
      <td>객체 자체</td>
      <td>객체 초기화/설정</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">also</code></td>
      <td><code class="language-plaintext highlighter-rouge">it</code></td>
      <td>객체 자체</td>
      <td>부수 효과 (로깅 등)</td>
    </tr>
  </tbody>
</table>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
</pre></td><td class="rouge-code"><pre><span class="kd">data class</span> <span class="nc">Person</span><span class="p">(</span>
    <span class="kd">var</span> <span class="py">name</span><span class="p">:</span> <span class="nc">String</span> <span class="p">=</span> <span class="s">""</span><span class="p">,</span>
    <span class="kd">var</span> <span class="py">age</span><span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="kd">var</span> <span class="py">email</span><span class="p">:</span> <span class="nc">String</span> <span class="p">=</span> <span class="s">""</span>
<span class="p">)</span>

<span class="c1">// let: null-safe 처리</span>
<span class="kd">val</span> <span class="py">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">?</span> <span class="p">=</span> <span class="s">"Kotlin"</span>
<span class="n">name</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"Name length: ${it.length}"</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// apply: 객체 초기화</span>
<span class="kd">val</span> <span class="py">person</span> <span class="p">=</span> <span class="nc">Person</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="n">name</span> <span class="p">=</span> <span class="s">"DooDoo"</span>
    <span class="k">this</span><span class="p">.</span><span class="n">age</span> <span class="p">=</span> <span class="mi">25</span>
    <span class="k">this</span><span class="p">.</span><span class="n">email</span> <span class="p">=</span> <span class="s">"doodoo@example.com"</span>
<span class="p">}</span>

<span class="c1">// also: 디버깅/로깅</span>
<span class="kd">val</span> <span class="py">numbers</span> <span class="p">=</span> <span class="nf">mutableListOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">also</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="s">"Before: $it"</span><span class="p">)</span> <span class="p">}</span>
    <span class="p">.</span><span class="nf">apply</span> <span class="p">{</span> <span class="nf">add</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span> <span class="p">}</span>
    <span class="p">.</span><span class="nf">also</span> <span class="p">{</span> <span class="nf">println</span><span class="p">(</span><span class="s">"After: $it"</span><span class="p">)</span> <span class="p">}</span>

<span class="c1">// run: 객체 설정 + 결과</span>
<span class="kd">val</span> <span class="py">greeting</span> <span class="p">=</span> <span class="n">person</span><span class="p">.</span><span class="nf">run</span> <span class="p">{</span>
    <span class="s">"Hello, $name! You are $age years old."</span>
<span class="p">}</span>

<span class="c1">// with: 여러 속성 접근</span>
<span class="kd">val</span> <span class="py">info</span> <span class="p">=</span> <span class="nf">with</span><span class="p">(</span><span class="n">person</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
    <span class="nf">println</span><span class="p">(</span><span class="n">age</span><span class="p">)</span>
    <span class="s">"$name ($age)"</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h3 id="5-data-class--destructuring">5. Data Class &amp; Destructuring</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="kd">data class</span> <span class="nc">User</span><span class="p">(</span><span class="kd">val</span> <span class="py">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="kd">val</span> <span class="py">age</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="kd">val</span> <span class="py">email</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>

<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">user</span> <span class="p">=</span> <span class="nc">User</span><span class="p">(</span><span class="s">"DooDoo"</span><span class="p">,</span> <span class="mi">25</span><span class="p">,</span> <span class="s">"doodoo@example.com"</span><span class="p">)</span>

    <span class="c1">// 자동 생성: toString, equals, hashCode, copy</span>
    <span class="nf">println</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>                        <span class="c1">// User(name=DooDoo, age=25, email=doodoo@example.com)</span>
    <span class="kd">val</span> <span class="py">older</span> <span class="p">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">copy</span><span class="p">(</span><span class="n">age</span> <span class="p">=</span> <span class="mi">26</span><span class="p">)</span>      <span class="c1">// name, email은 유지</span>

    <span class="c1">// Destructuring</span>
    <span class="kd">val</span> <span class="p">(</span><span class="py">name</span><span class="p">,</span> <span class="py">age</span><span class="p">,</span> <span class="py">email</span><span class="p">)</span> <span class="p">=</span> <span class="n">user</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"$name is $age years old"</span><span class="p">)</span>   <span class="c1">// DooDoo is 25 years old</span>

    <span class="c1">// Map에서도 활용</span>
    <span class="kd">val</span> <span class="py">map</span> <span class="p">=</span> <span class="nf">mapOf</span><span class="p">(</span><span class="s">"a"</span> <span class="n">to</span> <span class="mi">1</span><span class="p">,</span> <span class="s">"b"</span> <span class="n">to</span> <span class="mi">2</span><span class="p">)</span>
    <span class="k">for</span> <span class="p">((</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> <span class="k">in</span> <span class="n">map</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"$key -&gt; $value"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h3 id="6-sealed-class">6. Sealed Class</h3>

<p>상속 가능한 클래스들의 집합을 제한할 때 사용한다. <code class="language-plaintext highlighter-rouge">when</code> 표현식에서 <code class="language-plaintext highlighter-rouge">else</code> 분기가 필요 없어진다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">sealed</span> <span class="kd">class</span> <span class="nc">Result</span><span class="p">&lt;</span><span class="k">out</span> <span class="nc">T</span><span class="p">&gt;</span> <span class="p">{</span>
    <span class="kd">data class</span> <span class="nc">Success</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;(</span><span class="kd">val</span> <span class="py">data</span><span class="p">:</span> <span class="nc">T</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Result</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;()</span>
    <span class="kd">data class</span> <span class="nc">Error</span><span class="p">(</span><span class="kd">val</span> <span class="py">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Result</span><span class="p">&lt;</span><span class="nc">Nothing</span><span class="p">&gt;()</span>
    <span class="kd">object</span> <span class="nc">Loading</span> <span class="p">:</span> <span class="nc">Result</span><span class="p">&lt;</span><span class="nc">Nothing</span><span class="p">&gt;()</span>
<span class="p">}</span>

<span class="k">fun</span> <span class="nf">handleResult</span><span class="p">(</span><span class="n">result</span><span class="p">:</span> <span class="nc">Result</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;)</span> <span class="p">{</span>
    <span class="k">when</span> <span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">is</span> <span class="nc">Result</span><span class="p">.</span><span class="nc">Success</span> <span class="p">-&gt;</span> <span class="nf">println</span><span class="p">(</span><span class="s">"Data: ${result.data}"</span><span class="p">)</span>
        <span class="k">is</span> <span class="nc">Result</span><span class="p">.</span><span class="nc">Error</span>   <span class="p">-&gt;</span> <span class="nf">println</span><span class="p">(</span><span class="s">"Error: ${result.message}"</span><span class="p">)</span>
        <span class="k">is</span> <span class="nc">Result</span><span class="p">.</span><span class="nc">Loading</span> <span class="p">-&gt;</span> <span class="nf">println</span><span class="p">(</span><span class="s">"Loading..."</span><span class="p">)</span>
        <span class="c1">// else 불필요 - 컴파일러가 모든 케이스를 확인</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h3 id="7-collection-operations">7. Collection Operations</h3>

<p>Kotlin의 컬렉션 API는 함수형 프로그래밍 스타일의 다양한 연산을 제공한다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">numbers</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>

<span class="c1">// 기본 변환</span>
<span class="kd">val</span> <span class="py">doubled</span> <span class="p">=</span> <span class="n">numbers</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span> <span class="p">*</span> <span class="mi">2</span> <span class="p">}</span>           <span class="c1">// [2, 4, 6, ..., 20]</span>
<span class="kd">val</span> <span class="py">evens</span> <span class="p">=</span> <span class="n">numbers</span><span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">it</span> <span class="p">%</span> <span class="mi">2</span> <span class="p">==</span> <span class="mi">0</span> <span class="p">}</span>     <span class="c1">// [2, 4, 6, 8, 10]</span>
<span class="kd">val</span> <span class="py">sum</span> <span class="p">=</span> <span class="n">numbers</span><span class="p">.</span><span class="nf">reduce</span> <span class="p">{</span> <span class="n">acc</span><span class="p">,</span> <span class="n">n</span> <span class="p">-&gt;</span> <span class="n">acc</span> <span class="p">+</span> <span class="n">n</span> <span class="p">}</span>  <span class="c1">// 55</span>

<span class="c1">// 그룹화 &amp; 분할</span>
<span class="kd">val</span> <span class="py">grouped</span> <span class="p">=</span> <span class="n">numbers</span><span class="p">.</span><span class="nf">groupBy</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">it</span> <span class="p">%</span> <span class="mi">2</span> <span class="p">==</span> <span class="mi">0</span><span class="p">)</span> <span class="s">"even"</span> <span class="k">else</span> <span class="s">"odd"</span> <span class="p">}</span>
<span class="c1">// {odd=[1, 3, 5, 7, 9], even=[2, 4, 6, 8, 10]}</span>

<span class="kd">val</span> <span class="p">(</span><span class="py">small</span><span class="p">,</span> <span class="py">large</span><span class="p">)</span> <span class="p">=</span> <span class="n">numbers</span><span class="p">.</span><span class="nf">partition</span> <span class="p">{</span> <span class="n">it</span> <span class="p">&lt;=</span> <span class="mi">5</span> <span class="p">}</span>
<span class="c1">// small=[1,2,3,4,5], large=[6,7,8,9,10]</span>

<span class="c1">// flatMap</span>
<span class="kd">val</span> <span class="py">nested</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span> <span class="nf">listOf</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span> <span class="nf">listOf</span><span class="p">(</span><span class="mi">5</span><span class="p">))</span>
<span class="kd">val</span> <span class="py">flat</span> <span class="p">=</span> <span class="n">nested</span><span class="p">.</span><span class="nf">flatMap</span> <span class="p">{</span> <span class="n">it</span> <span class="p">}</span> <span class="c1">// [1, 2, 3, 4, 5]</span>

<span class="c1">// 체이닝</span>
<span class="kd">val</span> <span class="py">result</span> <span class="p">=</span> <span class="n">numbers</span>
    <span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">it</span> <span class="p">%</span> <span class="mi">2</span> <span class="p">==</span> <span class="mi">0</span> <span class="p">}</span>
    <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span> <span class="p">*</span> <span class="n">it</span> <span class="p">}</span>
    <span class="p">.</span><span class="nf">sortedDescending</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">take</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
<span class="c1">// [100, 64, 36]</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h3 id="8-coroutines-기초">8. Coroutines 기초</h3>

<p>Kotlin 코루틴은 비동기 프로그래밍을 순차적인 코드처럼 작성할 수 있게 해준다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="k">import</span> <span class="nn">kotlinx.coroutines.*</span>

<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">=</span> <span class="nf">runBlocking</span> <span class="p">{</span>
    <span class="c1">// launch: 결과를 반환하지 않는 코루틴</span>
    <span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="nf">launch</span> <span class="p">{</span>
        <span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"World!"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"Hello,"</span><span class="p">)</span>
    <span class="n">job</span><span class="p">.</span><span class="nf">join</span><span class="p">()</span>

    <span class="c1">// async: 결과를 반환하는 코루틴</span>
    <span class="kd">val</span> <span class="py">deferred1</span> <span class="p">=</span> <span class="nf">async</span> <span class="p">{</span> <span class="nf">fetchUserName</span><span class="p">()</span> <span class="p">}</span>
    <span class="kd">val</span> <span class="py">deferred2</span> <span class="p">=</span> <span class="nf">async</span> <span class="p">{</span> <span class="nf">fetchUserAge</span><span class="p">()</span> <span class="p">}</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"${deferred1.await()} is ${deferred2.await()} years old"</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">fetchUserName</span><span class="p">():</span> <span class="nc">String</span> <span class="p">{</span>
    <span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span> <span class="c1">// 네트워크 호출 시뮬레이션</span>
    <span class="k">return</span> <span class="s">"DooDoo"</span>
<span class="p">}</span>

<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">fetchUserAge</span><span class="p">():</span> <span class="nc">Int</span> <span class="p">{</span>
    <span class="nf">delay</span><span class="p">(</span><span class="mi">1000L</span><span class="p">)</span>
    <span class="k">return</span> <span class="mi">25</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="주요-개념">주요 개념</h4>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">suspend</code></strong>: 일시 중단 가능한 함수를 표시하는 키워드</li>
  <li><strong><code class="language-plaintext highlighter-rouge">launch</code></strong>: 결과를 반환하지 않는 코루틴 빌더 (<code class="language-plaintext highlighter-rouge">Job</code> 반환)</li>
  <li><strong><code class="language-plaintext highlighter-rouge">async</code></strong>: 결과를 반환하는 코루틴 빌더 (<code class="language-plaintext highlighter-rouge">Deferred&lt;T&gt;</code> 반환)</li>
  <li><strong><code class="language-plaintext highlighter-rouge">runBlocking</code></strong>: 코루틴이 완료될 때까지 현재 스레드를 차단</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="c1">// 구조화된 동시성 (Structured Concurrency)</span>
<span class="k">suspend</span> <span class="k">fun</span> <span class="nf">loadData</span><span class="p">()</span> <span class="p">=</span> <span class="nf">coroutineScope</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">users</span> <span class="p">=</span> <span class="nf">async</span> <span class="p">{</span> <span class="nf">fetchUsers</span><span class="p">()</span> <span class="p">}</span>
    <span class="kd">val</span> <span class="py">posts</span> <span class="p">=</span> <span class="nf">async</span> <span class="p">{</span> <span class="nf">fetchPosts</span><span class="p">()</span> <span class="p">}</span>
    <span class="c1">// 둘 다 완료될 때까지 대기</span>
    <span class="nf">processData</span><span class="p">(</span><span class="n">users</span><span class="p">.</span><span class="nf">await</span><span class="p">(),</span> <span class="n">posts</span><span class="p">.</span><span class="nf">await</span><span class="p">())</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="dispatchers">Dispatchers</h4>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c1">// Dispatchers 종류</span>
<span class="nf">launch</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">Main</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* UI 작업 */</span> <span class="p">}</span>
<span class="nf">launch</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">IO</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* 네트워크, DB I/O */</span> <span class="p">}</span>
<span class="nf">launch</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">Default</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* CPU 집약 작업 */</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="exception-handling">Exception Handling</h4>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">handler</span> <span class="p">=</span> <span class="nc">CoroutineExceptionHandler</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">exception</span> <span class="p">-&gt;</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"예외 처리: $exception"</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">val</span> <span class="py">job</span> <span class="p">=</span> <span class="nc">CoroutineScope</span><span class="p">(</span><span class="nc">Dispatchers</span><span class="p">.</span><span class="nc">IO</span> <span class="p">+</span> <span class="n">handler</span><span class="p">).</span><span class="nf">launch</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="nc">RuntimeException</span><span class="p">(</span><span class="s">"에러 발생"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="flow-기초">Flow 기초</h4>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nf">numberFlow</span><span class="p">():</span> <span class="nc">Flow</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">flow</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">5</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">delay</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span>
        <span class="nf">emit</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// collect</span>
<span class="nf">numberFlow</span><span class="p">().</span><span class="nf">collect</span> <span class="p">{</span> <span class="n">value</span> <span class="p">-&gt;</span> <span class="nf">println</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h3 id="9-inline-functions--reified-types">9. Inline Functions &amp; Reified Types</h3>

<p><code class="language-plaintext highlighter-rouge">inline</code> 키워드는 함수 호출 오버헤드를 줄이고, <code class="language-plaintext highlighter-rouge">reified</code>는 제네릭 타입 정보를 런타임에 유지한다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">inline</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="k">reified</span> <span class="nc">T</span><span class="p">&gt;</span> <span class="nf">List</span><span class="p">&lt;*&gt;.</span><span class="nf">filterByType</span><span class="p">():</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="n">filterIsInstance</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;()</span>
<span class="p">}</span>

<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">mixed</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">Any</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s">"hello"</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">,</span> <span class="s">"world"</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">strings</span> <span class="p">=</span> <span class="n">mixed</span><span class="p">.</span><span class="n">filterByType</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;()</span> <span class="c1">// [hello, world]</span>
    <span class="kd">val</span> <span class="py">ints</span> <span class="p">=</span> <span class="n">mixed</span><span class="p">.</span><span class="n">filterByType</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">&gt;()</span>       <span class="c1">// [1, 3]</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://kotlinlang.org/docs/home.html">Kotlin 공식 문서</a></li>
  <li><a href="https://kotlinlang.org/docs/coroutines-guide.html">Kotlin Coroutines Guide</a></li>
</ul>
]]></content:encoded>
        <pubDate>Wed, 01 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/kotlin/2026/04/01/kotlin-advanced-syntax/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/kotlin/2026/04/01/kotlin-advanced-syntax/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Kotlin</category>
        
        
        <category>kotlin</category>
        
      </item>
    
      <item>
        <title>Kubernetes 핵심 개념 — Pod부터 Deployment까지</title>
        <description>왜 Kubernetes인가</description>
        <content:encoded><![CDATA[<h2 id="왜-kubernetes인가">왜 Kubernetes인가</h2>

<p><a href="/infra/2026/03/20/docker-getting-started/">이전 Docker 포스트</a>에서 컨테이너를 만들고 <code class="language-plaintext highlighter-rouge">docker-compose</code>로 멀티 컨테이너 환경을 구성하는 법을 다뤘다. 로컬 개발이나 소규모 서비스에서는 충분하지만, <strong>프로덕션 환경</strong>에서는 금방 한계에 부딪힌다.</p>

<ul>
  <li>컨테이너가 죽으면 누가 다시 띄우는가?</li>
  <li>트래픽이 급증하면 컨테이너를 어떻게 늘리는가?</li>
  <li>새 버전을 배포할 때 다운타임 없이 교체할 수 있는가?</li>
  <li>서버가 여러 대일 때 어떤 서버에 컨테이너를 배치할 것인가?</li>
</ul>

<p>Kubernetes(이하 k8s)는 이 문제들을 <strong>자동으로</strong> 해결해 주는 컨테이너 오케스트레이션 플랫폼이다.</p>

<table>
  <thead>
    <tr>
      <th>문제</th>
      <th>k8s가 제공하는 해법</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>컨테이너 장애</td>
      <td><strong>Self-healing</strong> — 컨테이너가 죽으면 자동 재시작</td>
    </tr>
    <tr>
      <td>트래픽 급증</td>
      <td><strong>Auto Scaling</strong> — HPA로 Pod 수를 자동 조절</td>
    </tr>
    <tr>
      <td>무중단 배포</td>
      <td><strong>Rolling Update</strong> — 순차적으로 새 버전 교체</td>
    </tr>
    <tr>
      <td>서버 분산 배치</td>
      <td><strong>Scheduling</strong> — 리소스 상태에 따라 최적 노드 배치</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="핵심-아키텍처">핵심 아키텍처</h2>

<h3 id="cluster">Cluster</h3>

<p>k8s의 최상위 단위. <strong>Control Plane</strong>(마스터)과 하나 이상의 <strong>Worker Node</strong>로 구성된다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre>┌─────────────────── Cluster ───────────────────┐
│                                                │
│  ┌──── Control Plane ────┐                     │
│  │  API Server           │                     │
│  │  etcd                 │                     │
│  │  Scheduler            │                     │
│  │  Controller Manager   │                     │
│  └───────────────────────┘                     │
│                                                │
│  ┌── Worker Node 1 ──┐  ┌── Worker Node 2 ──┐ │
│  │  kubelet           │  │  kubelet           │ │
│  │  kube-proxy        │  │  kube-proxy        │ │
│  │  [Pod] [Pod]       │  │  [Pod] [Pod]       │ │
│  └────────────────────┘  └────────────────────┘ │
└────────────────────────────────────────────────┘
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li><strong>API Server</strong>: 모든 요청의 진입점. <code class="language-plaintext highlighter-rouge">kubectl</code> 명령이 여기로 간다.</li>
  <li><strong>etcd</strong>: 클러스터 상태를 저장하는 분산 key-value 저장소.</li>
  <li><strong>Scheduler</strong>: 새 Pod를 어느 노드에 배치할지 결정.</li>
  <li><strong>Controller Manager</strong>: Desired State와 현재 상태를 비교해 차이를 메꾼다.</li>
</ul>

<h3 id="node">Node</h3>

<p>실제 컨테이너가 실행되는 물리/가상 머신. 각 노드에는 <strong>kubelet</strong>(Pod 관리 에이전트)과 <strong>kube-proxy</strong>(네트워크 규칙 관리)가 동작한다.</p>

<h3 id="pod">Pod</h3>

<p>k8s에서 <strong>배포 가능한 가장 작은 단위</strong>. 하나 이상의 컨테이너를 포함하며, 같은 Pod 안의 컨테이너들은 네트워크(localhost)와 스토리지를 공유한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">my-app</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">app</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">my-app:1.0</span>
      <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p>실무에서는 Pod를 직접 생성하는 일은 거의 없다. 항상 Deployment 같은 상위 오브젝트를 통해 관리한다.</p>
</blockquote>

<h3 id="namespace">Namespace</h3>

<p>클러스터 안에서 리소스를 <strong>논리적으로 격리</strong>하는 단위. 팀별, 환경별로 나눠 사용한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre>kubectl get namespaces
<span class="c"># NAME              STATUS   AGE</span>
<span class="c"># default           Active   10d</span>
<span class="c"># kube-system       Active   10d</span>
<span class="c"># production        Active   5d</span>
<span class="c"># staging           Active   5d</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="핵심-오브젝트">핵심 오브젝트</h2>

<h3 id="deployment">Deployment</h3>

<p><strong>Pod의 원하는 상태(Desired State)를 선언</strong>하면, k8s가 그 상태를 유지해 준다. Pod 개수, 이미지 버전 등을 명시하면 Controller가 알아서 관리한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">my-app</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>            <span class="c1"># Pod 3개를 유지</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">my-app</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">my-app</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">app</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">my-app:1.0</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">250m"</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">256Mi"</span>
            <span class="na">limits</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">500m"</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">512Mi"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Rolling Update</strong>는 Deployment의 기본 배포 전략이다. <code class="language-plaintext highlighter-rouge">image: my-app:2.0</code>으로 바꾸면 k8s가 새 Pod를 하나씩 띄우고 기존 Pod를 하나씩 종료한다. 다운타임이 없다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c"># 이미지 업데이트 → Rolling Update 자동 실행</span>
kubectl <span class="nb">set </span>image deployment/my-app <span class="nv">app</span><span class="o">=</span>my-app:2.0

<span class="c"># 배포 상태 확인</span>
kubectl rollout status deployment/my-app

<span class="c"># 문제 시 롤백</span>
kubectl rollout undo deployment/my-app
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="service">Service</h3>

<p>Pod는 생성/삭제될 때마다 IP가 바뀐다. <strong>Service는 Pod 집합에 대한 안정적인 네트워크 엔드포인트를 제공한다.</strong> Label selector로 대상 Pod를 지정하고, 고정 IP(ClusterIP)와 DNS 이름을 부여한다.</p>

<table>
  <thead>
    <tr>
      <th>타입</th>
      <th>접근 범위</th>
      <th>사용 시나리오</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>ClusterIP</strong> (기본)</td>
      <td>클러스터 내부만</td>
      <td>마이크로서비스 간 통신</td>
    </tr>
    <tr>
      <td><strong>NodePort</strong></td>
      <td>외부 (노드IP:포트)</td>
      <td>개발/테스트 환경</td>
    </tr>
    <tr>
      <td><strong>LoadBalancer</strong></td>
      <td>외부 (클라우드 LB)</td>
      <td>프로덕션 외부 노출</td>
    </tr>
  </tbody>
</table>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">my-app-service</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">my-app</span>          <span class="c1"># 이 label을 가진 Pod에 트래픽 전달</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>           <span class="c1"># Service가 받는 포트</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">8080</span>   <span class="c1"># Pod에 전달하는 포트</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="configmap--secret">ConfigMap &amp; Secret</h3>

<p><strong>환경 설정과 민감 정보를 코드에서 분리</strong>한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="c1"># ConfigMap — 일반 설정</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ConfigMap</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">app-config</span>
<span class="na">data</span><span class="pi">:</span>
  <span class="na">SPRING_PROFILES_ACTIVE</span><span class="pi">:</span> <span class="s2">"</span><span class="s">production"</span>
  <span class="na">LOG_LEVEL</span><span class="pi">:</span> <span class="s2">"</span><span class="s">INFO"</span>
<span class="nn">---</span>
<span class="c1"># Secret — 민감 정보 (Base64 인코딩)</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Secret</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">db-secret</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">Opaque</span>
<span class="na">data</span><span class="pi">:</span>
  <span class="na">DB_PASSWORD</span><span class="pi">:</span> <span class="s">cGFzc3dvcmQxMjM=</span>    <span class="c1"># echo -n 'password123' | base64</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Pod에서 참조:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">app</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">my-app:1.0</span>
      <span class="na">envFrom</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">configMapRef</span><span class="pi">:</span>
            <span class="na">name</span><span class="pi">:</span> <span class="s">app-config</span>
        <span class="pi">-</span> <span class="na">secretRef</span><span class="pi">:</span>
            <span class="na">name</span><span class="pi">:</span> <span class="s">db-secret</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p>⚠️ Secret은 Base64일 뿐 암호화가 아니다. 프로덕션에서는 Sealed Secrets, Vault 등 별도 암호화 솔루션을 사용해야 한다.</p>
</blockquote>

<hr />

<h2 id="자주-쓰는-kubectl-명령어">자주 쓰는 kubectl 명령어</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="c"># 클러스터 정보</span>
kubectl cluster-info
kubectl get nodes

<span class="c"># Pod 관리</span>
kubectl get pods                          <span class="c"># Pod 목록</span>
kubectl get pods <span class="nt">-o</span> wide                  <span class="c"># 노드 배치 정보 포함</span>
kubectl describe pod &lt;pod-name&gt;           <span class="c"># Pod 상세 정보</span>
kubectl logs &lt;pod-name&gt;                   <span class="c"># 로그 확인</span>
kubectl logs &lt;pod-name&gt; <span class="nt">-f</span>                <span class="c"># 실시간 로그</span>
kubectl <span class="nb">exec</span> <span class="nt">-it</span> &lt;pod-name&gt; <span class="nt">--</span> /bin/sh    <span class="c"># Pod에 접속</span>

<span class="c"># Deployment 관리</span>
kubectl apply <span class="nt">-f</span> deployment.yaml          <span class="c"># 리소스 생성/업데이트</span>
kubectl get deployments
kubectl scale deployment/my-app <span class="nt">--replicas</span><span class="o">=</span>5  <span class="c"># 스케일링</span>
kubectl delete deployment my-app

<span class="c"># 디버깅</span>
kubectl get events <span class="nt">--sort-by</span><span class="o">=</span><span class="s1">'.lastTimestamp'</span>
kubectl top pods                          <span class="c"># 리소스 사용량</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="실전-예제-spring-boot-앱-k8s-배포">실전 예제: Spring Boot 앱 k8s 배포</h2>

<p>Docker 포스트에서 만든 Spring Boot 앱을 k8s에 배포해 보자. 하나의 파일에 Deployment와 Service를 함께 정의한다.</p>

<h3 id="k8s-deploymentyaml">k8s-deployment.yaml</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
</pre></td><td class="rouge-code"><pre><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">spring-app</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">spring-app</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">spring-app</span>
  <span class="na">strategy</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">RollingUpdate</span>
    <span class="na">rollingUpdate</span><span class="pi">:</span>
      <span class="na">maxSurge</span><span class="pi">:</span> <span class="m">1</span>           <span class="c1"># 업데이트 시 최대 1개 추가 Pod</span>
      <span class="na">maxUnavailable</span><span class="pi">:</span> <span class="m">0</span>     <span class="c1"># 업데이트 중 사용 불가 Pod 0개 → 무중단</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">spring-app</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">spring-app</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">my-registry/spring-app:1.0</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">250m"</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">512Mi"</span>
            <span class="na">limits</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1000m"</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1Gi"</span>
          <span class="na">livenessProbe</span><span class="pi">:</span>
            <span class="na">httpGet</span><span class="pi">:</span>
              <span class="na">path</span><span class="pi">:</span> <span class="s">/actuator/health</span>
              <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
            <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">30</span>
            <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">10</span>
          <span class="na">readinessProbe</span><span class="pi">:</span>
            <span class="na">httpGet</span><span class="pi">:</span>
              <span class="na">path</span><span class="pi">:</span> <span class="s">/actuator/health/readiness</span>
              <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
            <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">10</span>
            <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">5</span>
          <span class="na">envFrom</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">configMapRef</span><span class="pi">:</span>
                <span class="na">name</span><span class="pi">:</span> <span class="s">app-config</span>
            <span class="pi">-</span> <span class="na">secretRef</span><span class="pi">:</span>
                <span class="na">name</span><span class="pi">:</span> <span class="s">db-secret</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">spring-app-service</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">LoadBalancer</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">spring-app</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">8080</span>
      <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>핵심 포인트:</strong></p>

<ul>
  <li><strong>livenessProbe</strong>: 컨테이너가 살아있는지 확인. 실패하면 재시작한다.</li>
  <li><strong>readinessProbe</strong>: 트래픽을 받을 준비가 되었는지 확인. 실패하면 Service에서 제외한다.</li>
  <li><strong>resources</strong>: 리소스 요청(requests)과 제한(limits)을 반드시 설정해야 Scheduler가 올바르게 배치한다.</li>
  <li><strong>maxUnavailable: 0</strong>: 업데이트 중에도 항상 3개 Pod가 가용하므로 무중단 배포가 보장된다.</li>
</ul>

<h3 id="배포-실행">배포 실행</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="c"># ConfigMap, Secret 먼저 생성</span>
kubectl apply <span class="nt">-f</span> configmap.yaml
kubectl apply <span class="nt">-f</span> secret.yaml

<span class="c"># Deployment + Service 배포</span>
kubectl apply <span class="nt">-f</span> k8s-deployment.yaml

<span class="c"># 확인</span>
kubectl get all <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>spring-app
<span class="c"># NAME                              READY   STATUS    RESTARTS   AGE</span>
<span class="c"># pod/spring-app-6d4f8b7c9-abc12   1/1     Running   0          30s</span>
<span class="c"># pod/spring-app-6d4f8b7c9-def34   1/1     Running   0          30s</span>
<span class="c"># pod/spring-app-6d4f8b7c9-ghi56   1/1     Running   0          30s</span>
<span class="c">#</span>
<span class="c"># NAME                         TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)</span>
<span class="c"># service/spring-app-service   LoadBalancer   10.96.123.45    34.56.78.90     80:31234/TCP</span>
<span class="c">#</span>
<span class="c"># NAME                         READY   UP-TO-DATE   AVAILABLE   AGE</span>
<span class="c"># deployment.apps/spring-app   3/3     3            3           30s</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="docker-compose-vs-kubernetes">Docker Compose vs Kubernetes</h2>

<table>
  <thead>
    <tr>
      <th>기준</th>
      <th>Docker Compose</th>
      <th>Kubernetes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>용도</strong></td>
      <td>로컬 개발, 단일 호스트</td>
      <td>프로덕션, 멀티 호스트</td>
    </tr>
    <tr>
      <td><strong>스케일링</strong></td>
      <td><code class="language-plaintext highlighter-rouge">docker-compose up --scale app=3</code> (수동)</td>
      <td>HPA로 자동 스케일링</td>
    </tr>
    <tr>
      <td><strong>Self-healing</strong></td>
      <td>없음 (restart 정책만 존재)</td>
      <td>Pod 자동 재시작 + 재배치</td>
    </tr>
    <tr>
      <td><strong>네트워킹</strong></td>
      <td>단일 호스트 내 브리지</td>
      <td>클러스터 전체 Service Discovery</td>
    </tr>
    <tr>
      <td><strong>배포 전략</strong></td>
      <td>없음 (stop → start)</td>
      <td>Rolling Update, Blue-Green, Canary</td>
    </tr>
    <tr>
      <td><strong>설정 관리</strong></td>
      <td><code class="language-plaintext highlighter-rouge">.env</code> 파일</td>
      <td>ConfigMap, Secret</td>
    </tr>
    <tr>
      <td><strong>학습 곡선</strong></td>
      <td>낮음</td>
      <td>높음</td>
    </tr>
    <tr>
      <td><strong>설정 파일</strong></td>
      <td><code class="language-plaintext highlighter-rouge">docker-compose.yml</code></td>
      <td>여러 YAML 매니페스트</td>
    </tr>
  </tbody>
</table>

<p>이 둘은 경쟁 관계가 아니라 <strong>보완 관계</strong>다. 로컬에서는 Docker Compose로 빠르게 개발하고, 프로덕션에서는 k8s로 운영하는 것이 일반적인 패턴이다.</p>

<hr />

<h2 id="언제-k8s를-쓰고-언제-과한가">언제 k8s를 쓰고, 언제 과한가</h2>

<h3 id="k8s가-적합한-경우">k8s가 적합한 경우</h3>

<ul>
  <li>마이크로서비스 아키텍처로 서비스가 5개 이상</li>
  <li>트래픽 변동이 크고 오토스케일링이 필요</li>
  <li>무중단 배포(Rolling Update, Canary)가 필수</li>
  <li>멀티 클라우드 또는 하이브리드 클라우드 환경</li>
  <li>팀 규모가 크고 여러 서비스를 독립적으로 배포</li>
</ul>

<h3 id="k8s가-과한-경우">k8s가 과한 경우</h3>

<ul>
  <li>모놀리식 앱 하나를 운영하는 경우</li>
  <li>팀원이 1-3명이고 운영 인력이 부족한 경우</li>
  <li>트래픽이 예측 가능하고 안정적인 경우</li>
  <li>단순히 “이력서에 넣고 싶어서” (가장 흔한 이유…)</li>
</ul>

<p><strong>대안 선택지:</strong></p>
<ul>
  <li>단일 서버: Docker Compose + Nginx</li>
  <li>서버리스: AWS Lambda, Google Cloud Run</li>
  <li>매니지드 PaaS: AWS ECS, Google App Engine</li>
  <li>간소화된 k8s: k3s (경량 Kubernetes)</li>
</ul>

<blockquote>
  <p>k8s를 도입하기 전에 “이 복잡도를 감당할 운영 역량이 있는가?”를 먼저 물어보자. 도구가 문제를 해결하는 것이 아니라, 도구를 운영할 수 있는 팀이 문제를 해결한다.</p>
</blockquote>

<hr />

<h2 id="마무리">마무리</h2>

<p>Kubernetes는 단순한 배포 도구가 아니라 <strong>선언적 인프라 관리 플랫폼</strong>이다. “컨테이너 3개를 유지하라”고 선언하면 k8s가 알아서 상태를 맞춰주는 것이 핵심 철학이다.</p>

<p>이번 포스트에서 다룬 내용을 정리하면:</p>

<ol>
  <li><strong>Pod</strong> — 배포의 최소 단위, 직접 생성하지 않는다</li>
  <li><strong>Deployment</strong> — Pod의 Desired State를 선언하고 Rolling Update를 관리</li>
  <li><strong>Service</strong> — Pod에 안정적인 네트워크 접근을 제공</li>
  <li><strong>ConfigMap/Secret</strong> — 설정과 민감 정보를 코드에서 분리</li>
</ol>

<p>다음 포스트에서는 Helm Chart를 활용한 패키지 관리와 Ingress를 통한 외부 라우팅을 다룰 예정이다.</p>

<hr />

<h2 id="관련-포스트">관련 포스트</h2>

<ul>
  <li><a href="/infra/2026/03/20/docker-getting-started/">Docker 시작하기</a></li>
</ul>
]]></content:encoded>
        <pubDate>Wed, 01 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/infra/2026/04/01/kubernetes-basics/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/infra/2026/04/01/kubernetes-basics/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Kubernetes</category>
        
        <category>Docker</category>
        
        <category>Infrastructure</category>
        
        <category>DevOps</category>
        
        
        <category>infra</category>
        
      </item>
    
      <item>
        <title>MySQL vs PostgreSQL — 백엔드 개발자가 알아야 할 차이</title>
        <description>라이선스와 아키텍처 차이</description>
        <content:encoded><![CDATA[<h2 id="라이선스와-아키텍처-차이">라이선스와 아키텍처 차이</h2>

<h3 id="라이선스">라이선스</h3>

<p>MySQL은 Oracle이 소유하고 있으며 <strong>듀얼 라이선스</strong> 정책을 따른다. Community Edition은 GPL v2로 무료이지만, 상용 라이선스가 별도로 존재한다. Oracle의 방향성에 따라 기능이 제한될 수 있다는 우려가 있어 MariaDB로 포크된 역사가 있다.</p>

<p>PostgreSQL은 <strong>PostgreSQL License</strong>(BSD 계열)로, 사실상 제한 없이 자유롭게 사용할 수 있다. 상용 제품에 포함해도 라이선스 비용이 없다. 기업 입장에서 법적 리스크가 가장 낮은 선택지다.</p>

<h3 id="아키텍처">아키텍처</h3>

<p>MySQL은 <strong>멀티스레드</strong> 기반이다. 하나의 프로세스 안에서 각 커넥션이 스레드로 처리된다. 메모리 사용 효율이 높고 커넥션 생성 비용이 낮다.</p>

<p>PostgreSQL은 <strong>멀티프로세스</strong> 기반이다. 각 커넥션마다 별도의 프로세스(backend process)가 생성된다. 프로세스 간 격리가 강해 안정적이지만, 커넥션 수가 많아지면 메모리 소모가 크다. 이 때문에 PostgreSQL에서는 <strong>PgBouncer</strong> 같은 커넥션 풀러를 사용하는 것이 일반적이다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c"># PostgreSQL 커넥션 풀러 — PgBouncer 설정 예시</span>
<span class="o">[</span>databases]
mydb <span class="o">=</span> <span class="nv">host</span><span class="o">=</span>127.0.0.1 <span class="nv">port</span><span class="o">=</span>5432 <span class="nv">dbname</span><span class="o">=</span>mydb

<span class="o">[</span>pgbouncer]
listen_port <span class="o">=</span> 6432
pool_mode <span class="o">=</span> transaction
max_client_conn <span class="o">=</span> 1000
default_pool_size <span class="o">=</span> 20
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="json-지원-비교">JSON 지원 비교</h2>

<p>두 데이터베이스 모두 JSON을 지원하지만, 깊이와 완성도에 차이가 있다.</p>

<h3 id="mysql의-json">MySQL의 JSON</h3>

<p>MySQL 5.7부터 <code class="language-plaintext highlighter-rouge">JSON</code> 타입을 지원한다. 내부적으로 바이너리 포맷으로 저장한다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">products</span> <span class="p">(</span>
    <span class="n">id</span> <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
    <span class="n">name</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span>
    <span class="n">attributes</span> <span class="n">JSON</span>
<span class="p">);</span>

<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">products</span> <span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">attributes</span><span class="p">)</span>
<span class="k">VALUES</span> <span class="p">(</span><span class="s1">'노트북'</span><span class="p">,</span> <span class="s1">'{"brand": "Samsung", "ram": 16, "storage": "512GB"}'</span><span class="p">);</span>

<span class="c1">-- JSON 필드 조회</span>
<span class="k">SELECT</span> <span class="n">name</span><span class="p">,</span> <span class="n">attributes</span><span class="o">-&gt;&gt;</span><span class="s1">'$.brand'</span> <span class="k">AS</span> <span class="n">brand</span>
<span class="k">FROM</span> <span class="n">products</span>
<span class="k">WHERE</span> <span class="n">attributes</span><span class="o">-&gt;&gt;</span><span class="s1">'$.ram'</span> <span class="o">=</span> <span class="s1">'16'</span><span class="p">;</span>

<span class="c1">-- MySQL 8.0+: Multi-Valued Index</span>
<span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">idx_tags</span> <span class="k">ON</span> <span class="n">products</span> <span class="p">((</span><span class="k">CAST</span><span class="p">(</span><span class="n">attributes</span><span class="o">-&gt;</span><span class="s1">'$.tags'</span> <span class="k">AS</span> <span class="nb">CHAR</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span> <span class="n">ARRAY</span><span class="p">)));</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="postgresql의-json">PostgreSQL의 JSON</h3>

<p>PostgreSQL은 <code class="language-plaintext highlighter-rouge">JSON</code>과 <code class="language-plaintext highlighter-rouge">JSONB</code> 두 가지 타입을 제공한다. <code class="language-plaintext highlighter-rouge">JSONB</code>는 바이너리 포맷으로 저장하며, 인덱싱과 연산 성능이 뛰어나다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">products</span> <span class="p">(</span>
    <span class="n">id</span> <span class="n">BIGSERIAL</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
    <span class="n">name</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span>
    <span class="n">attributes</span> <span class="n">JSONB</span>
<span class="p">);</span>

<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">products</span> <span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">attributes</span><span class="p">)</span>
<span class="k">VALUES</span> <span class="p">(</span><span class="s1">'노트북'</span><span class="p">,</span> <span class="s1">'{"brand": "Samsung", "ram": 16, "tags": ["ultrabook", "2024"]}'</span><span class="p">);</span>

<span class="c1">-- JSONB 연산자</span>
<span class="k">SELECT</span> <span class="n">name</span><span class="p">,</span> <span class="n">attributes</span><span class="o">-&gt;&gt;</span><span class="s1">'brand'</span> <span class="k">AS</span> <span class="n">brand</span>
<span class="k">FROM</span> <span class="n">products</span>
<span class="k">WHERE</span> <span class="n">attributes</span> <span class="o">@&gt;</span> <span class="s1">'{"ram": 16}'</span><span class="p">;</span>    <span class="c1">-- 포함 연산자</span>

<span class="c1">-- GIN 인덱스로 JSONB 전체 필드 인덱싱</span>
<span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">idx_attributes</span> <span class="k">ON</span> <span class="n">products</span> <span class="k">USING</span> <span class="n">GIN</span> <span class="p">(</span><span class="n">attributes</span><span class="p">);</span>

<span class="c1">-- JSONB 배열 요소 검색</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">products</span>
<span class="k">WHERE</span> <span class="n">attributes</span><span class="o">-&gt;</span><span class="s1">'tags'</span> <span class="o">?</span> <span class="s1">'ultrabook'</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>PostgreSQL의 <code class="language-plaintext highlighter-rouge">JSONB</code>가 <code class="language-plaintext highlighter-rouge">@&gt;</code>, <code class="language-plaintext highlighter-rouge">?</code>, <code class="language-plaintext highlighter-rouge">?|</code> 같은 연산자와 GIN 인덱스를 제공하기 때문에 JSON 활용이 빈번한 프로젝트에서는 PostgreSQL이 유리하다.</p>

<hr />

<h2 id="full-text-search-비교">Full-Text Search 비교</h2>

<h3 id="mysql">MySQL</h3>

<p>MySQL은 InnoDB 엔진에서 <code class="language-plaintext highlighter-rouge">FULLTEXT</code> 인덱스를 지원한다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">articles</span> <span class="p">(</span>
    <span class="n">id</span> <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
    <span class="n">title</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span>
    <span class="n">body</span> <span class="nb">TEXT</span><span class="p">,</span>
    <span class="n">FULLTEXT</span> <span class="k">INDEX</span> <span class="n">ft_idx</span> <span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
<span class="p">)</span> <span class="n">ENGINE</span><span class="o">=</span><span class="n">InnoDB</span><span class="p">;</span>

<span class="c1">-- 자연어 검색</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">articles</span>
<span class="k">WHERE</span> <span class="k">MATCH</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span> <span class="n">AGAINST</span><span class="p">(</span><span class="s1">'Redis 캐시 전략'</span> <span class="k">IN</span> <span class="k">NATURAL</span> <span class="k">LANGUAGE</span> <span class="k">MODE</span><span class="p">);</span>

<span class="c1">-- 불린 모드</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">articles</span>
<span class="k">WHERE</span> <span class="k">MATCH</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span> <span class="n">AGAINST</span><span class="p">(</span><span class="s1">'+Redis -Memcached'</span> <span class="k">IN</span> <span class="nb">BOOLEAN</span> <span class="k">MODE</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>한국어/중국어/일본어 등 CJK 문자는 기본적으로 N-gram 파서(<code class="language-plaintext highlighter-rouge">ngram</code>)를 설정해야 제대로 동작한다.</p>

<h3 id="postgresql">PostgreSQL</h3>

<p>PostgreSQL은 <code class="language-plaintext highlighter-rouge">tsvector</code>와 <code class="language-plaintext highlighter-rouge">tsquery</code>를 이용한 강력한 Full-Text Search를 내장하고 있다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">articles</span> <span class="p">(</span>
    <span class="n">id</span> <span class="n">BIGSERIAL</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
    <span class="n">title</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span>
    <span class="n">body</span> <span class="nb">TEXT</span><span class="p">,</span>
    <span class="n">search_vector</span> <span class="n">TSVECTOR</span>
<span class="p">);</span>

<span class="c1">-- tsvector 컬럼 자동 업데이트 트리거</span>
<span class="k">CREATE</span> <span class="k">FUNCTION</span> <span class="n">update_search_vector</span><span class="p">()</span> <span class="k">RETURNS</span> <span class="k">TRIGGER</span> <span class="k">AS</span> <span class="err">$$</span>
<span class="k">BEGIN</span>
    <span class="k">NEW</span><span class="p">.</span><span class="n">search_vector</span> <span class="p">:</span><span class="o">=</span> <span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="n">COALESCE</span><span class="p">(</span><span class="k">NEW</span><span class="p">.</span><span class="n">title</span><span class="p">,</span> <span class="s1">''</span><span class="p">)</span> <span class="o">||</span> <span class="s1">' '</span> <span class="o">||</span> <span class="n">COALESCE</span><span class="p">(</span><span class="k">NEW</span><span class="p">.</span><span class="n">body</span><span class="p">,</span> <span class="s1">''</span><span class="p">));</span>
    <span class="k">RETURN</span> <span class="k">NEW</span><span class="p">;</span>
<span class="k">END</span><span class="p">;</span>
<span class="err">$$</span> <span class="k">LANGUAGE</span> <span class="n">plpgsql</span><span class="p">;</span>

<span class="k">CREATE</span> <span class="k">TRIGGER</span> <span class="n">trg_search_vector</span>
    <span class="k">BEFORE</span> <span class="k">INSERT</span> <span class="k">OR</span> <span class="k">UPDATE</span> <span class="k">ON</span> <span class="n">articles</span>
    <span class="k">FOR</span> <span class="k">EACH</span> <span class="k">ROW</span> <span class="k">EXECUTE</span> <span class="k">FUNCTION</span> <span class="n">update_search_vector</span><span class="p">();</span>

<span class="c1">-- GIN 인덱스</span>
<span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">idx_search</span> <span class="k">ON</span> <span class="n">articles</span> <span class="k">USING</span> <span class="n">GIN</span> <span class="p">(</span><span class="n">search_vector</span><span class="p">);</span>

<span class="c1">-- 검색</span>
<span class="k">SELECT</span> <span class="n">title</span><span class="p">,</span> <span class="n">ts_rank</span><span class="p">(</span><span class="n">search_vector</span><span class="p">,</span> <span class="n">query</span><span class="p">)</span> <span class="k">AS</span> <span class="n">rank</span>
<span class="k">FROM</span> <span class="n">articles</span><span class="p">,</span> <span class="n">to_tsquery</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="s1">'Redis &amp; 캐시'</span><span class="p">)</span> <span class="n">query</span>
<span class="k">WHERE</span> <span class="n">search_vector</span> <span class="o">@@</span> <span class="n">query</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">rank</span> <span class="k">DESC</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>PostgreSQL의 FTS는 랭킹, 하이라이팅, 사전(dictionary) 커스터마이징 등 더 세밀한 제어가 가능하다. 다만 한국어 형태소 분석을 위해서는 별도 확장(예: <code class="language-plaintext highlighter-rouge">textsearch_ko</code>)이 필요하다.</p>

<hr />

<h2 id="트랜잭션과-mvcc-구현-차이">트랜잭션과 MVCC 구현 차이</h2>

<p>두 데이터베이스 모두 MVCC(Multi-Version Concurrency Control)를 지원하지만 구현 방식이 다르다.</p>

<h3 id="mysql-innodb">MySQL (InnoDB)</h3>

<p>InnoDB는 <strong>Undo Log</strong> 기반 MVCC를 사용한다. 행이 수정되면 이전 버전을 Undo Log에 보관하고, 다른 트랜잭션이 이전 버전을 읽어야 할 때 Undo Log를 참조한다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c1">-- MySQL 기본 격리 수준: REPEATABLE READ</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'transaction_isolation'</span><span class="p">;</span>
<span class="c1">-- 'REPEATABLE-READ'</span>

<span class="c1">-- InnoDB는 REPEATABLE READ에서도 Phantom Read를 방지한다 (Gap Lock)</span>
<span class="k">START</span> <span class="n">TRANSACTION</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">orders</span> <span class="k">WHERE</span> <span class="n">amount</span> <span class="o">&gt;</span> <span class="mi">1000</span> <span class="k">FOR</span> <span class="k">UPDATE</span><span class="p">;</span>
<span class="c1">-- Gap Lock으로 범위 내 INSERT 차단</span>
<span class="k">COMMIT</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>MySQL의 REPEATABLE READ는 Gap Lock 덕분에 Phantom Read를 사실상 방지하지만, 이로 인해 동시성이 떨어질 수 있다.</p>

<h3 id="postgresql-1">PostgreSQL</h3>

<p>PostgreSQL은 <strong>테이블 내부에 다중 버전을 직접 저장</strong>하는 방식이다. 수정 시 기존 행을 삭제 표시하고 새 행을 삽입한다. 이 때문에 <code class="language-plaintext highlighter-rouge">VACUUM</code>으로 죽은 행(dead tuple)을 주기적으로 정리해야 한다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="c1">-- PostgreSQL 기본 격리 수준: READ COMMITTED</span>
<span class="k">SHOW</span> <span class="n">default_transaction_isolation</span><span class="p">;</span>
<span class="c1">-- 'read committed'</span>

<span class="c1">-- SERIALIZABLE 격리 수준 사용</span>
<span class="k">BEGIN</span> <span class="n">TRANSACTION</span> <span class="k">ISOLATION</span> <span class="k">LEVEL</span> <span class="k">SERIALIZABLE</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="k">SUM</span><span class="p">(</span><span class="n">balance</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">accounts</span> <span class="k">WHERE</span> <span class="n">branch</span> <span class="o">=</span> <span class="s1">'gangnam'</span><span class="p">;</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span> <span class="o">-</span> <span class="mi">1000</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">COMMIT</span><span class="p">;</span>
<span class="c1">-- 직렬화 충돌 시 에러 발생 → 애플리케이션에서 재시도 필요</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>PostgreSQL의 SERIALIZABLE은 SSI(Serializable Snapshot Isolation) 알고리즘을 사용하며, Lock 대신 충돌 감지 방식이라 동시성이 높다.</p>

<hr />

<h2 id="성능-비교">성능 비교</h2>

<h3 id="mysql이-유리한-경우">MySQL이 유리한 경우</h3>

<ul>
  <li><strong>단순 읽기 중심 워크로드</strong>: 단순 SELECT, 기본 키 조회가 많은 서비스</li>
  <li><strong>높은 커넥션 수</strong>: 멀티스레드 구조로 많은 동시 접속 처리에 유리</li>
  <li><strong>복제(Replication)</strong>: 읽기 복제본 구성이 간단하고 안정적</li>
</ul>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c1">-- MySQL: 기본 키 조회는 매우 빠르다</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">users</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">42</span><span class="p">;</span>
<span class="c1">-- Clustered Index 구조로 PK 조회 = 데이터 직접 접근</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="postgresql이-유리한-경우">PostgreSQL이 유리한 경우</h3>

<ul>
  <li><strong>복잡한 쿼리</strong>: 서브쿼리, CTE, 윈도우 함수 등 복잡한 분석 쿼리</li>
  <li><strong>쓰기 중심 워크로드</strong>: MVCC 구현 특성상 쓰기 동시성이 높음</li>
  <li><strong>확장성</strong>: 사용자 정의 타입, 함수, 연산자 등 확장이 자유로움</li>
</ul>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="c1">-- PostgreSQL: CTE + 윈도우 함수 조합</span>
<span class="k">WITH</span> <span class="n">monthly_sales</span> <span class="k">AS</span> <span class="p">(</span>
    <span class="k">SELECT</span>
        <span class="n">DATE_TRUNC</span><span class="p">(</span><span class="s1">'month'</span><span class="p">,</span> <span class="n">order_date</span><span class="p">)</span> <span class="k">AS</span> <span class="k">month</span><span class="p">,</span>
        <span class="n">category</span><span class="p">,</span>
        <span class="k">SUM</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total</span>
    <span class="k">FROM</span> <span class="n">orders</span>
    <span class="k">GROUP</span> <span class="k">BY</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span>
<span class="p">)</span>
<span class="k">SELECT</span>
    <span class="k">month</span><span class="p">,</span>
    <span class="n">category</span><span class="p">,</span>
    <span class="n">total</span><span class="p">,</span>
    <span class="n">LAG</span><span class="p">(</span><span class="n">total</span><span class="p">)</span> <span class="n">OVER</span> <span class="p">(</span><span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">category</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="k">month</span><span class="p">)</span> <span class="k">AS</span> <span class="n">prev_month</span><span class="p">,</span>
    <span class="n">ROUND</span><span class="p">((</span><span class="n">total</span> <span class="o">-</span> <span class="n">LAG</span><span class="p">(</span><span class="n">total</span><span class="p">)</span> <span class="n">OVER</span> <span class="p">(</span><span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">category</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="k">month</span><span class="p">))</span>
        <span class="o">/</span> <span class="n">LAG</span><span class="p">(</span><span class="n">total</span><span class="p">)</span> <span class="n">OVER</span> <span class="p">(</span><span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">category</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="k">month</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="k">AS</span> <span class="n">growth_pct</span>
<span class="k">FROM</span> <span class="n">monthly_sales</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">category</span><span class="p">,</span> <span class="k">month</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="실무-선택-기준">실무 선택 기준</h2>

<table>
  <thead>
    <tr>
      <th>기준</th>
      <th>MySQL</th>
      <th>PostgreSQL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>단순 CRUD 서비스</td>
      <td>적합</td>
      <td>적합</td>
    </tr>
    <tr>
      <td>복잡한 분석 쿼리</td>
      <td>보통</td>
      <td>우수</td>
    </tr>
    <tr>
      <td>JSON 활용 비중 높음</td>
      <td>보통</td>
      <td>우수</td>
    </tr>
    <tr>
      <td>지리 데이터(GIS)</td>
      <td>제한적</td>
      <td>PostGIS로 우수</td>
    </tr>
    <tr>
      <td>레거시/기존 인프라</td>
      <td>MySQL이 이미 있다면 유지</td>
      <td>—</td>
    </tr>
    <tr>
      <td>라이선스 민감</td>
      <td>주의 필요</td>
      <td>자유</td>
    </tr>
    <tr>
      <td>커뮤니티/생태계</td>
      <td>매우 큼</td>
      <td>크고 빠르게 성장 중</td>
    </tr>
  </tbody>
</table>

<p><strong>실무에서의 판단 기준</strong>: 기존에 MySQL 인프라가 갖춰져 있고 단순 CRUD가 대부분이라면 MySQL을 유지한다. 새 프로젝트를 시작하고, 복잡한 쿼리나 JSON 활용이 예상되며, 라이선스에 민감하다면 PostgreSQL을 선택한다.</p>

<hr />

<h2 id="spring-boot-설정">Spring Boot 설정</h2>

<h3 id="mysql-설정">MySQL 설정</h3>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c1">// build.gradle</span>
<span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-data-jpa'</span>
    <span class="n">runtimeOnly</span> <span class="s1">'com.mysql:mysql-connector-j'</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="c1"># application.yml</span>
<span class="na">spring</span><span class="pi">:</span>
  <span class="na">datasource</span><span class="pi">:</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">jdbc:mysql://localhost:3306/mydb?useSSL=false&amp;serverTimezone=Asia/Seoul&amp;characterEncoding=UTF-8</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">root</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">password</span>
    <span class="na">driver-class-name</span><span class="pi">:</span> <span class="s">com.mysql.cj.jdbc.Driver</span>
  <span class="na">jpa</span><span class="pi">:</span>
    <span class="na">hibernate</span><span class="pi">:</span>
      <span class="na">ddl-auto</span><span class="pi">:</span> <span class="s">validate</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">hibernate</span><span class="pi">:</span>
        <span class="na">dialect</span><span class="pi">:</span> <span class="s">org.hibernate.dialect.MySQLDialect</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="postgresql-설정">PostgreSQL 설정</h3>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c1">// build.gradle</span>
<span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-data-jpa'</span>
    <span class="n">runtimeOnly</span> <span class="s1">'org.postgresql:postgresql'</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="c1"># application.yml</span>
<span class="na">spring</span><span class="pi">:</span>
  <span class="na">datasource</span><span class="pi">:</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">jdbc:postgresql://localhost:5432/mydb</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">postgres</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">password</span>
    <span class="na">driver-class-name</span><span class="pi">:</span> <span class="s">org.postgresql.Driver</span>
  <span class="na">jpa</span><span class="pi">:</span>
    <span class="na">hibernate</span><span class="pi">:</span>
      <span class="na">ddl-auto</span><span class="pi">:</span> <span class="s">validate</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">hibernate</span><span class="pi">:</span>
        <span class="na">dialect</span><span class="pi">:</span> <span class="s">org.hibernate.dialect.PostgreSQLDialect</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="정리">정리</h2>

<p>MySQL과 PostgreSQL은 모두 검증된 RDBMS이며, “어느 것이 더 좋다”는 단정은 무의미하다. 중요한 것은 프로젝트의 요구사항, 팀의 숙련도, 기존 인프라를 종합적으로 고려해 선택하는 것이다. 최근 업계 트렌드로는 PostgreSQL의 채택이 빠르게 늘고 있지만, MySQL이 여전히 거대한 생태계를 가지고 있다는 점도 무시할 수 없다.</p>
]]></content:encoded>
        <pubDate>Wed, 01 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/database/2026/04/01/mysql-vs-postgresql/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/database/2026/04/01/mysql-vs-postgresql/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>MySQL</category>
        
        <category>PostgreSQL</category>
        
        <category>Database</category>
        
        <category>Backend</category>
        
        
        <category>database</category>
        
      </item>
    
      <item>
        <title>Data Representation - Integer</title>
        <description>Integers, or whole number from elemental mathematics, are the most common and
fundamental numbers used in the computers. It’s represented as
fixed-point numbers, contrast to floating-point numbers in the machine.
Today we are going to learn a whole bunch of way to encode it.</description>
        <content:encoded><![CDATA[<p>Integers, or <em>whole number</em> from elemental mathematics, are the most common and
fundamental numbers used in the computers. It’s represented as
<em>fixed-point numbers</em>, contrast to <em>floating-point numbers</em> in the machine.
Today we are going to learn a whole bunch of way to encode it.</p>

<p>There are mainly two properties to make a integer representation different:</p>

<ol>
  <li>
    <p><strong>Size, of the number of bits used</strong>.
usually the power of 2. e.g. 8-bit, 16-bit, 32-bit, 64-bit.</p>
  </li>
  <li>
    <p><strong>Signed or unsigned</strong>.
there are also multiple schemas to encode a signed integers.</p>
  </li>
</ol>

<p>We are also gonna use the below terminologies throughout the post:</p>

<ul>
  <li><em>MSB</em>: Most Significant Bit</li>
  <li><em>LSB</em>: Least Significant Bit</li>
</ul>

<h2 id="prerequisite---printf-recap">Prerequisite - <code class="language-plaintext highlighter-rouge">printf</code> Recap</h2>

<p>We will quickly recap the integers subset of usages of <code class="language-plaintext highlighter-rouge">printf</code>.
Basically, we used <em>format specifier</em> to interpolate values into strings:</p>

<h3 id="format-specifier"><a href="http://www.cplusplus.com/reference/cstdio/printf/">Format Specifier</a></h3>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">%[flags][width][.precision][length]specifier</code></p>
</blockquote>

<ul>
  <li><code class="language-plaintext highlighter-rouge">specifier</code>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">d</code>, <code class="language-plaintext highlighter-rouge">i</code> : signed decimal</li>
      <li><code class="language-plaintext highlighter-rouge">u</code> : unsigned decimal</li>
      <li><code class="language-plaintext highlighter-rouge">c</code> : char</li>
      <li><code class="language-plaintext highlighter-rouge">p</code>: pointer addr</li>
      <li><code class="language-plaintext highlighter-rouge">x</code> / <code class="language-plaintext highlighter-rouge">X</code> : lower/upper unsigned hex</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">length</code>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">l</code> : long (at least 32)</li>
      <li><code class="language-plaintext highlighter-rouge">ll</code> : long long (at least 64)</li>
      <li><code class="language-plaintext highlighter-rouge">h</code> : short (usually 16)</li>
      <li><code class="language-plaintext highlighter-rouge">hh</code> : short short (usually 8)</li>
    </ul>
  </li>
</ul>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">using</span> <span class="k">namespace</span> <span class="n">std</span><span class="p">;</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>  <span class="p">{</span>
  <span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Size of int = "</span><span class="o">&lt;&lt;</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="n">endl</span><span class="p">;</span>
  <span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Size of long = "</span> <span class="o">&lt;&lt;</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">long</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="n">endl</span><span class="p">;</span>
  <span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Size of long long = "</span> <span class="o">&lt;&lt;</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">long</span> <span class="kt">long</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">Output</span> <span class="n">in</span> <span class="mi">32</span> <span class="n">bit</span> <span class="n">gcc</span> <span class="n">compiler</span><span class="o">:</span> <span class="mi">4</span> <span class="mi">4</span> <span class="mi">8</span>
<span class="n">Output</span> <span class="n">in</span> <span class="mi">64</span> <span class="n">bit</span> <span class="n">gcc</span> <span class="n">compiler</span><span class="o">:</span> <span class="mi">4</span> <span class="mi">8</span> <span class="mi">8</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="inttypesh-from-c99"><a href="http://www.qnx.com/developers/docs/6.5.0/index.jsp?topic=%2Fcom.qnx.doc.dinkum_en_c99%2Finttypes.html"><code class="language-plaintext highlighter-rouge">inttypes.h</code> from C99</a></h3>

<p>Also in <a href="https://en.cppreference.com/w/c/types/integer">cppreference.com</a></p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre><span class="c1">// signed int (d or i)</span>
<span class="cp">#define PRId8     "hhd"
#define PRId16    "hd"
#define PRId32    "ld"
#define PRId64    "lld"
</span>
<span class="c1">// unsigned int (u)</span>
<span class="cp">#define PRIu8     "hhu"
#define PRIu16    "hu"
#define PRIu32    "u"
#define PRIu64    "llu"
</span>
<span class="c1">// unsigned hex</span>
<span class="cp">#define PRIx8     "hhx"
#define PRIx16    "hx"
#define PRIx32    "x"
#define PRIx64    "llx"
</span>
<span class="c1">// uintptr_t (64 bit machine word len)</span>
<span class="cp">#define PRIxPTR   "llx"
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="unsigned-integers">Unsigned Integers</h2>

<p>The conversion between unsigned integers and binaries are trivial.
Here, we can represent 8 bits (i.e. a <em>byte</em>) as a <em>hex pair</em>, e.g.
<code class="language-plaintext highlighter-rouge">255 == 0xff == 0b11111111</code>.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="cp">#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="c1">    // uintN_t</span><span class="cp">
#include</span> <span class="cpf">&lt;inttypes.h&gt;</span><span class="c1">  // PRI macros</span><span class="cp">
</span>
<span class="kt">uint8_t</span> <span class="n">u8</span> <span class="o">=</span> <span class="mi">255</span><span class="p">;</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"0x%02"</span> <span class="n">PRIx8</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">u8</span><span class="p">);</span> <span class="c1">// 0xff</span>
<span class="n">printf</span><span class="p">(</span>  <span class="s">"%"</span>   <span class="n">PRId8</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">u8</span><span class="p">);</span> <span class="c1">// 255</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="signed-integers">Signed Integers</h2>

<p>Signed integers are more complicated. We need to cut those bits to halves
to represent both positive and negative integers somehow.</p>

<p>There are four well-known schemas to encode it, according to
<a href="https://en.wikipedia.org/wiki/Signed_number_representations">signed number representation of wikipedia</a>.</p>

<h3 id="sign-magnitude-원码">Sign magnitude 원码</h3>

<p>It’s also called <em>“sign and magnitude”</em>. From the name we can see how straightforward it is:
it’s basically put one bit (often the <em>MSB</em>) as the <em>sign bit</em> to represent <em>sign</em> and the remaining bits indicating
the magnitude (or absolute value), e.g.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>  <span class="n">binary</span>   <span class="o">|</span> <span class="n">sign</span><span class="o">-</span><span class="n">magn</span> <span class="o">|</span>  <span class="kt">unsigned</span>
<span class="o">-----------|-----------|------------</span>
<span class="mi">0</span> <span class="mo">000</span> <span class="mo">0000</span> <span class="o">|</span>    <span class="o">+</span><span class="mi">0</span>     <span class="o">|</span>     <span class="mi">0</span>
<span class="mi">0</span> <span class="mi">111</span> <span class="mi">1111</span> <span class="o">|</span>    <span class="mi">127</span>    <span class="o">|</span>    <span class="mi">127</span>
<span class="p">...</span>
<span class="mi">1</span> <span class="mo">000</span> <span class="mo">0000</span> <span class="o">|</span>    <span class="o">-</span><span class="mi">0</span>     <span class="o">|</span>    <span class="mi">128</span>
<span class="mi">1</span> <span class="mi">111</span> <span class="mi">1111</span> <span class="o">|</span>   <span class="o">-</span><span class="mi">127</span>    <span class="o">|</span>    <span class="mi">255</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>It was used in early computer (IBM 7090) and now mainly used in the
<em>significand</em> part in floating-point number</p>

<p>Pros:</p>
<ul>
  <li>simple and natural for human</li>
</ul>

<p>Cons:</p>
<ul>
  <li>2 ways to represent zeros (<code class="language-plaintext highlighter-rouge">+0</code> and <code class="language-plaintext highlighter-rouge">-0</code>)</li>
  <li>not as good for machine
    <ul>
      <li>add/sub/cmp require knowing the sign
        <ul>
          <li>complicate CPU ALU design; potentially more cycles</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h3 id="ones-complement-반码"><a href="https://en.wikipedia.org/wiki/Ones%27_complement">Ones’ complement</a> 반码</h3>

<p>It forms a negative integer by applying a <em>bitwise NOT</em>
i.e. <em>complement</em> of its positive counterpart.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre>  <span class="n">binary</span>   <span class="o">|</span>  <span class="mx">1s</span> <span class="n">comp</span>  <span class="o">|</span>  <span class="kt">unsigned</span>
<span class="o">-----------|-----------|------------</span>
<span class="mo">0000</span> <span class="mo">0000</span>  <span class="o">|</span>     <span class="mi">0</span>     <span class="o">|</span>     <span class="mi">0</span>
<span class="mo">0000</span> <span class="mo">0001</span>  <span class="o">|</span>     <span class="mi">1</span>     <span class="o">|</span>     <span class="mi">1</span>
<span class="p">...</span>
<span class="mo">0111</span> <span class="mi">1111</span>  <span class="o">|</span>    <span class="mi">127</span>    <span class="o">|</span>    <span class="mi">127</span>
<span class="mi">1000</span> <span class="mo">0000</span>  <span class="o">|</span>   <span class="o">-</span><span class="mi">127</span>    <span class="o">|</span>    <span class="mi">128</span>
<span class="p">...</span>
<span class="mi">1111</span> <span class="mi">1110</span>  <span class="o">|</span>    <span class="o">-</span><span class="mi">1</span>     <span class="o">|</span>    <span class="mi">254</span>
<span class="mi">1111</span> <span class="mi">1111</span>  <span class="o">|</span>    <span class="o">-</span><span class="mi">0</span>     <span class="o">|</span>    <span class="mi">255</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>N.B. <em>MSB</em> can still be signified by MSB.</p>

<p>It’s referred to as <em>ones’</em> complement because the negative can be formed
by subtracting the positive <strong>from</strong> <em>ones</em>: <code class="language-plaintext highlighter-rouge">1111 1111 (-0)</code></p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>  <span class="mi">1111</span> <span class="mi">1111</span>       <span class="o">-</span><span class="mi">0</span>
<span class="o">-</span> <span class="mo">0111</span> <span class="mi">1111</span>       <span class="mi">127</span>
<span class="o">---------------------</span>
  <span class="mi">1000</span> <span class="mo">0000</span>      <span class="o">-</span><span class="mi">127</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The benefits of the complement nature is that adding becomes simple,
except we need to do an <em>end-around carry</em> to add resulting carry
back to get the correct result.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>  <span class="mo">0111</span> <span class="mi">1111</span>       <span class="mi">127</span>
<span class="o">+</span> <span class="mi">1000</span> <span class="mo">0001</span>      <span class="o">-</span><span class="mi">126</span>
<span class="o">---------------------</span>
<span class="mi">1</span> <span class="mo">0000</span> <span class="mo">0000</span>        <span class="mi">0</span>
          <span class="mi">1</span>       <span class="o">+</span><span class="mi">1</span>     <span class="o">&lt;-</span> <span class="n">add</span> <span class="n">carry</span> <span class="s">"1"</span> <span class="n">back</span>
<span class="o">---------------------</span>
  <span class="mo">0000</span> <span class="mo">0001</span>        <span class="mi">1</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Pros:</p>
<ul>
  <li>Arithmetics on machine are fast.</li>
</ul>

<p>Cons:</p>
<ul>
  <li>still 2 zeros!</li>
</ul>

<h3 id="twos-complement-보码"><a href="https://en.wikipedia.org/wiki/Two%27s_complement">Two’s complement</a> 보码</h3>

<p>Most of the current architecture adopted this, including x86, MIPS, ARM, etc.
It differs from one’s complement by one.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre>  <span class="n">binary</span>   <span class="o">|</span>  <span class="mx">2s</span> <span class="n">comp</span>  <span class="o">|</span>  <span class="kt">unsigned</span>
<span class="o">-----------|-----------|------------</span>
<span class="mo">0000</span> <span class="mo">0000</span>  <span class="o">|</span>     <span class="mi">0</span>     <span class="o">|</span>     <span class="mi">0</span>
<span class="mo">0000</span> <span class="mo">0001</span>  <span class="o">|</span>     <span class="mi">1</span>     <span class="o">|</span>     <span class="mi">1</span>
<span class="p">...</span>
<span class="mo">0111</span> <span class="mi">1111</span>  <span class="o">|</span>    <span class="mi">127</span>    <span class="o">|</span>    <span class="mi">127</span>
<span class="mi">1000</span> <span class="mo">0000</span>  <span class="o">|</span>   <span class="o">-</span><span class="mi">128</span>    <span class="o">|</span>    <span class="mi">128</span>
<span class="mi">1000</span> <span class="mo">0001</span>  <span class="o">|</span>   <span class="o">-</span><span class="mi">127</span>    <span class="o">|</span>    <span class="mi">129</span>
<span class="p">...</span>
<span class="mi">1111</span> <span class="mi">1110</span>  <span class="o">|</span>    <span class="o">-</span><span class="mi">2</span>     <span class="o">|</span>    <span class="mi">254</span>
<span class="mi">1111</span> <span class="mi">1111</span>  <span class="o">|</span>    <span class="o">-</span><span class="mi">1</span>     <span class="o">|</span>    <span class="mi">255</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>N.B. <em>MSB</em> can still be signified by MSB.</p>

<p>It’s referred to as <em>two’s</em> complement because the negative can be formed
by subtracting the positive <strong>from</strong> <code class="language-plaintext highlighter-rouge">2 ** N</code> (congruent to <code class="language-plaintext highlighter-rouge">0000 0000 (+0)</code>),
where <code class="language-plaintext highlighter-rouge">N</code> is the number of bits.</p>

<p>E.g., for a <code class="language-plaintext highlighter-rouge">uint8_t</code>, the <em>sum</em> of any number and its two’s complement would
be <code class="language-plaintext highlighter-rouge">256 (1 0000 0000)</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="mi">1</span> <span class="mo">0000</span> <span class="mo">0000</span>       <span class="mi">256</span>  <span class="o">=</span> <span class="mi">2</span> <span class="o">**</span> <span class="mi">8</span>
<span class="o">-</span> <span class="mo">0111</span> <span class="mi">1111</span>       <span class="mi">127</span>
<span class="o">---------------------</span>
  <span class="mi">1000</span> <span class="mo">0001</span>      <span class="o">-</span><span class="mi">127</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Because of this, arithmetics become really easier, for any number <code class="language-plaintext highlighter-rouge">x</code> e.g. <code class="language-plaintext highlighter-rouge">127</code>
we can get its two’s complement by:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">~x =&gt; 1000 0000</code> bitwise NOT (like ones’ complement)</li>
  <li><code class="language-plaintext highlighter-rouge">+1 =&gt; 1000 0001</code> add 1 (the one differed from ones’ complement)</li>
</ol>

<p>Pros:</p>
<ul>
  <li>fast machine arithmetics</li>
  <li>only 1 zero!</li>
  <li>the minimal negative is <code class="language-plaintext highlighter-rouge">-128</code></li>
</ul>

<p>Cons:</p>
<ul>
  <li>asymmetric range: <code class="language-plaintext highlighter-rouge">-128</code> to <code class="language-plaintext highlighter-rouge">127</code> for 8-bit</li>
</ul>

<h3 id="offset-binary-이동码"><a href="https://en.wikipedia.org/wiki/Offset_binary">Offset binary</a> 이동码</h3>

<p>It’s also called <em>excess-K</em> or <em>biased representation</em>, where <code class="language-plaintext highlighter-rouge">K</code> is
the <em>biasing value</em> (the new <code class="language-plaintext highlighter-rouge">0</code>), e.g. in <em>excess-128</em>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre>  <span class="n">binary</span>   <span class="o">|</span>  <span class="n">K</span> <span class="o">=</span> <span class="mi">128</span>  <span class="o">|</span>  <span class="kt">unsigned</span>
<span class="o">-----------|-----------|------------</span>
<span class="mo">0000</span> <span class="mo">0000</span>  <span class="o">|</span>   <span class="o">-</span><span class="mi">128</span><span class="p">(</span><span class="o">-</span><span class="n">K</span><span class="p">)</span><span class="o">|</span>     <span class="mi">0</span>
<span class="mo">0000</span> <span class="mo">0001</span>  <span class="o">|</span>   <span class="o">-</span><span class="mi">127</span>    <span class="o">|</span>     <span class="mi">1</span>
<span class="p">...</span>
<span class="mo">0111</span> <span class="mi">1111</span>  <span class="o">|</span>    <span class="o">-</span><span class="mi">1</span>     <span class="o">|</span>    <span class="mi">127</span>
<span class="mi">1000</span> <span class="mo">0000</span>  <span class="o">|</span>     <span class="mi">0</span>     <span class="o">|</span>    <span class="mi">128</span>  <span class="p">(</span><span class="n">K</span><span class="p">)</span>
<span class="mi">1000</span> <span class="mo">0001</span>  <span class="o">|</span>     <span class="mi">1</span>     <span class="o">|</span>    <span class="mi">129</span>
<span class="p">...</span>
<span class="mi">1111</span> <span class="mi">1111</span>  <span class="o">|</span>    <span class="mi">127</span>    <span class="o">|</span>    <span class="mi">255</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>It’s now mainly used for the <em>exponent</em> part of floating-point number.</p>

<h2 id="type-conversion--printf">Type Conversion &amp; <code class="language-plaintext highlighter-rouge">printf</code></h2>

<p>This might be a little bit off topic, but I want to note down what I observed
from experimenting. Basically, <code class="language-plaintext highlighter-rouge">printf</code> would not perform an implicit type
conversion but merely <em>interpret</em> the bits arrangement of your arguments as you
told it.</p>

<ul>
  <li><em>UB!</em> stands for <em>undefined behaviors</em></li>
</ul>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="kt">uint8_t</span> <span class="n">u8</span> <span class="o">=</span> <span class="mb">0b10000000</span><span class="p">;</span> <span class="c1">// 128</span>
 <span class="kt">int8_t</span> <span class="n">s8</span> <span class="o">=</span> <span class="mb">0b10000000</span><span class="p">;</span> <span class="c1">// -128</span>

<span class="n">printf</span><span class="p">(</span><span class="s">"%"</span><span class="n">PRIu8</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">u8</span><span class="p">);</span>          <span class="c1">// 128</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%"</span><span class="n">PRId8</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">u8</span><span class="p">);</span>          <span class="c1">// 128</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%"</span><span class="n">PRId8</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="p">(</span><span class="kt">int8_t</span><span class="p">)</span><span class="n">u8</span><span class="p">);</span>  <span class="c1">// -128</span>

<span class="n">printf</span><span class="p">(</span><span class="s">"%"</span><span class="n">PRId8</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">s8</span><span class="p">);</span>          <span class="c1">// -128</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%"</span><span class="n">PRIu8</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">s8</span><span class="p">);</span>          <span class="c1">// 4294967168</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%"</span><span class="n">PRId8</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)</span><span class="n">s8</span><span class="p">);</span> <span class="c1">// 128</span>

<span class="n">printf</span><span class="p">(</span><span class="s">"%"</span><span class="n">PRIxPTR</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">s8</span><span class="p">);</span>             <span class="c1">// ffffff80</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%"</span><span class="n">PRIxPTR</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="p">(</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">s8</span><span class="p">);</span>  <span class="c1">// ffffffffffffff80</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="char--ascii">Char &amp; <a href="https://en.wikipedia.org/wiki/ASCII">ASCII</a></h2>

<p>Traditionally, <code class="language-plaintext highlighter-rouge">char</code> is represented in the computer as 8 bits as well. And
really, ASCII is only defined between <code class="language-plaintext highlighter-rouge">0</code> and <code class="language-plaintext highlighter-rouge">127</code> and require 7 bits.
(8-bit Extended ASCII is not quite well popularized and supported.)</p>

<p>It’s more complicated in extension such as <em>Unicode</em> nowadays, but we’ll ignore
it for future posts dedicated for char and string representation.</p>

<p>So how is a <code class="language-plaintext highlighter-rouge">char</code> different with a <em>byte</em>?</p>

<p>Well, the answer is whether a <code class="language-plaintext highlighter-rouge">char</code> is a <code class="language-plaintext highlighter-rouge">signed char</code> (backed by <code class="language-plaintext highlighter-rouge">int8_t</code>)
or a <code class="language-plaintext highlighter-rouge">unsigned char</code> (backed by <code class="language-plaintext highlighter-rouge">uint8_t</code>) is… <em>implementation-defined</em>.
And most systems made it <em>signed</em> since most types (e.g. <code class="language-plaintext highlighter-rouge">int</code>) were signed
by default.</p>

<p>N.B. <code class="language-plaintext highlighter-rouge">int</code> is standard-defined to be equivalent to <code class="language-plaintext highlighter-rouge">signed int</code>. This is
not the case of <code class="language-plaintext highlighter-rouge">char</code>.</p>

<p>That’s why you often see such <code class="language-plaintext highlighter-rouge">typedef</code> such as:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">typedef</span> <span class="kt">unsigned</span> <span class="kt">char</span> <span class="n">Byte_t</span><span class="p">;</span>
<span class="k">typedef</span> <span class="kt">uint8_t</span> <span class="n">byte_t</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>to emphasize the nature of byte should be just plain, unsigned, bits.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/Integer_(computer_science)">Wikipedia - Integer (computer science)</a></li>
  <li><a href="https://www3.ntu.edu.sg/home/ehchua/programming/java/datarepresentation.html">NTU - Data Representation</a></li>
</ul>

<hr />

<h2 id="관련-포스트">관련 포스트</h2>

<ul>
  <li><a href="/cs/2026/04/01/data-representation-float/">부동소수점 IEEE 754 완전 정복</a></li>
</ul>
]]></content:encoded>
        <pubDate>Wed, 01 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/cs/2026/04/01/data-representation-integer/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/cs/2026/04/01/data-representation-integer/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>CS</category>
        
        <category>CS Fundamentals</category>
        
        
        <category>cs</category>
        
      </item>
    
      <item>
        <title>부동소수점(Floating Point) 표현 — IEEE 754 완전 정복</title>
        <description>이전 글(Data Representation - Integer)에서 정수의 표현 방식을 알아보았다. 정수는 fixed-point(고정소수점)으로 표현된다고 했는데, 이번에는 실수를 표현하는 방식인 부동소수점(floating-point) 을 다룬다.</description>
        <content:encoded><![CDATA[<p><a href="/cs/2026/04/01/data-representation-integer/">이전 글(Data Representation - Integer)</a>에서 정수의 표현 방식을 알아보았다. 정수는 <em>fixed-point</em>(고정소수점)으로 표현된다고 했는데, 이번에는 실수를 표현하는 방식인 <strong>부동소수점(floating-point)</strong> 을 다룬다.</p>

<p>핵심 주제는 현대 거의 모든 하드웨어가 채택한 <strong>IEEE 754</strong> 표준이다.</p>

<hr />

<h2 id="왜-부동소수점인가">왜 부동소수점인가?</h2>

<p>고정소수점(fixed-point)은 소수점의 위치가 고정되어 있다. 예를 들어 32비트 중 16비트를 정수부, 16비트를 소수부에 할당하면 표현 가능한 범위가 매우 좁다.</p>

<p>부동소수점은 <strong>소수점이 떠다닌다(float)</strong>. 과학적 표기법(scientific notation)과 같은 원리로, 극도로 큰 수와 극도로 작은 수를 같은 비트 수로 표현할 수 있다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>과학적 표기법:  -2.625 = -1.0101 × 2^1

                 ↑ sign   ↑ significand  ↑ exponent
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="ieee-754-구조">IEEE 754 구조</h2>

<p>IEEE 754는 부동소수점 수를 세 부분으로 나눈다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> [Sign] [Exponent (biased)] [Mantissa (fraction)]
</pre></td></tr></tbody></table></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>형식</th>
      <th>총 비트</th>
      <th>Sign</th>
      <th>Exponent</th>
      <th>Mantissa</th>
      <th>Bias</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>단정도 (Single, <code class="language-plaintext highlighter-rouge">float</code>)</strong></td>
      <td>32</td>
      <td>1</td>
      <td>8</td>
      <td>23</td>
      <td>127</td>
    </tr>
    <tr>
      <td><strong>배정도 (Double, <code class="language-plaintext highlighter-rouge">double</code>)</strong></td>
      <td>64</td>
      <td>1</td>
      <td>11</td>
      <td>52</td>
      <td>1023</td>
    </tr>
  </tbody>
</table>

<h3 id="각-필드의-의미">각 필드의 의미</h3>

<ol>
  <li><strong>Sign bit (부호 비트)</strong> — <code class="language-plaintext highlighter-rouge">0</code>이면 양수, <code class="language-plaintext highlighter-rouge">1</code>이면 음수</li>
  <li><strong>Exponent (지수)</strong> — <em>biased</em> 표현 사용. 실제 지수 = 저장된 값 - Bias</li>
  <li><strong>Mantissa (가수, fraction)</strong> — 정규화된 이진수의 소수 부분. 앞의 <code class="language-plaintext highlighter-rouge">1.</code>은 <em>implicit leading bit</em> 로 생략된다 (정규화된 수의 경우)</li>
</ol>

<p>즉, 값은 다음 공식으로 계산된다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>(-1)^sign × 1.mantissa × 2^(exponent - bias)
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="10진수--ieee-754-변환-예제">10진수 → IEEE 754 변환 예제</h2>

<h3 id="예제-1--675를-단정도32-bit로-변환">예제 1: <code class="language-plaintext highlighter-rouge">-6.75</code>를 단정도(32-bit)로 변환</h3>

<p><strong>Step 1. 부호 결정</strong></p>

<p>음수이므로 Sign = <code class="language-plaintext highlighter-rouge">1</code></p>

<p><strong>Step 2. 절대값을 이진수로 변환</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>6   = 110 (2진수)
0.75 → 0.75 × 2 = 1.5  → 1
       0.5  × 2 = 1.0  → 1
∴ 0.75 = .11
</pre></td></tr></tbody></table></code></pre></div></div>

<p>따라서 <code class="language-plaintext highlighter-rouge">6.75 = 110.11</code></p>

<p><strong>Step 3. 정규화 (Normalize)</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>110.11 = 1.1011 × 2^2
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Step 4. Exponent 계산 (biased)</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>실제 지수 = 2
biased exponent = 2 + 127 = 129 = 1000 0001
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Step 5. Mantissa 추출</strong></p>

<p><code class="language-plaintext highlighter-rouge">1.1011</code>에서 leading <code class="language-plaintext highlighter-rouge">1.</code>을 제거 → <code class="language-plaintext highlighter-rouge">1011</code></p>

<p>23비트로 패딩: <code class="language-plaintext highlighter-rouge">1011 0000 0000 0000 0000 000</code></p>

<p><strong>최종 결과:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>Sign | Exponent  | Mantissa
  1  | 1000 0001 | 1011 0000 0000 0000 0000 000

Hex: 0xC0D80000
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="예제-2-ieee-754-비트--10진수-역변환">예제 2: IEEE 754 비트 → 10진수 역변환</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>0 | 0111 1110 | 1000 0000 0000 0000 0000 000
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Sign = <code class="language-plaintext highlighter-rouge">0</code> → 양수</li>
  <li>Exponent = <code class="language-plaintext highlighter-rouge">0111 1110</code> = 126 → 실제 지수 = 126 - 127 = <strong>-1</strong></li>
  <li>Mantissa = <code class="language-plaintext highlighter-rouge">1.1</code> (implicit leading 1 복원)</li>
  <li>값 = <code class="language-plaintext highlighter-rouge">+1.1 × 2^(-1)</code> = <code class="language-plaintext highlighter-rouge">0.11</code> (2진) = <strong>0.75</strong></li>
</ol>

<hr />

<h2 id="c-코드로-확인하기">C 코드로 확인하기</h2>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp">
</span>
<span class="kt">void</span> <span class="nf">print_float_bits</span><span class="p">(</span><span class="kt">float</span> <span class="n">f</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">bits</span><span class="p">;</span>
    <span class="n">memcpy</span><span class="p">(</span><span class="o">&amp;</span><span class="n">bits</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">f</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">bits</span><span class="p">));</span>

    <span class="kt">uint32_t</span> <span class="n">sign</span>     <span class="o">=</span> <span class="p">(</span><span class="n">bits</span> <span class="o">&gt;&gt;</span> <span class="mi">31</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mi">1</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">exponent</span> <span class="o">=</span> <span class="p">(</span><span class="n">bits</span> <span class="o">&gt;&gt;</span> <span class="mi">23</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFF</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">mantissa</span> <span class="o">=</span> <span class="n">bits</span> <span class="o">&amp;</span> <span class="mh">0x7FFFFF</span><span class="p">;</span>

    <span class="n">printf</span><span class="p">(</span><span class="s">"값: %f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">f</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"  Sign:     %u</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">sign</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"  Exponent: %u (biased), %d (실제)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">exponent</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">exponent</span> <span class="o">-</span> <span class="mi">127</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"  Mantissa: 0x%06X</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">mantissa</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"  Hex:      0x%08X</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">bits</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">print_float_bits</span><span class="p">(</span><span class="o">-</span><span class="mi">6</span><span class="p">.</span><span class="mi">75</span><span class="n">f</span><span class="p">);</span>   <span class="c1">// 예제 1 검증</span>
    <span class="n">print_float_bits</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mi">75</span><span class="n">f</span><span class="p">);</span>    <span class="c1">// 예제 2 검증</span>
    <span class="n">print_float_bits</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="p">);</span>     <span class="c1">// 무한소수 케이스</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>출력:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre>값: -6.750000
  Sign:     1
  Exponent: 129 (biased), 2 (실제)
  Mantissa: 0x580000
  Hex:      0xC0D80000

값: 0.750000
  Sign:     0
  Exponent: 126 (biased), -1 (실제)
  Mantissa: 0x400000
  Hex:      0x3F400000

값: 0.100000
  Sign:     0
  Exponent: 123 (biased), -4 (실제)
  Mantissa: 0x4CCCCD
  Hex:      0x3DCCCCCD
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">0.1</code>의 mantissa가 <code class="language-plaintext highlighter-rouge">0x4CCCCD</code>인 것을 볼 수 있다. <code class="language-plaintext highlighter-rouge">0.1</code>은 이진수로 <code class="language-plaintext highlighter-rouge">0.0001100110011...</code>로 무한 반복소수이기 때문에, 23비트에서 잘려 나가면서 오차가 발생한다.</p>

<hr />

<h2 id="특수-값-special-values">특수 값 (Special Values)</h2>

<p>IEEE 754는 exponent와 mantissa의 특정 조합으로 특수한 값을 표현한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>  Exponent   |  Mantissa  |  의미
-------------|------------|------------------
  0000 0000  |  000...0   |  ±0 (부호 비트에 따라)
  0000 0000  |  ≠ 0       |  비정규화수 (denormalized)
  1111 1111  |  000...0   |  ±Infinity
  1111 1111  |  ≠ 0       |  NaN (Not a Number)
  그 외       |  any       |  정규화수 (normalized)
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="0과--0">+0과 -0</h3>

<p>IEEE 754는 <strong>양의 0</strong>과 <strong>음의 0</strong>을 구분한다. 대부분의 비교 연산에서 <code class="language-plaintext highlighter-rouge">+0 == -0</code>은 <code class="language-plaintext highlighter-rouge">true</code>지만, 비트 레벨에서는 다르다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="kt">float</span> <span class="n">pos_zero</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">neg_zero</span> <span class="o">=</span> <span class="o">-</span><span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="p">;</span>

<span class="n">printf</span><span class="p">(</span><span class="s">"%d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">pos_zero</span> <span class="o">==</span> <span class="n">neg_zero</span><span class="p">);</span> <span class="c1">// 1 (true)</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span> <span class="o">/</span> <span class="n">pos_zero</span><span class="p">);</span>     <span class="c1">// inf</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span> <span class="o">/</span> <span class="n">neg_zero</span><span class="p">);</span>     <span class="c1">// -inf  ← 부호가 다르다!</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="infinity">Infinity</h3>

<p>오버플로우 또는 0으로 나눌 때 발생한다. 산술 연산에서 전파된다:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="kt">float</span> <span class="n">inf</span> <span class="o">=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span> <span class="o">/</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="p">;</span>     <span class="c1">// +inf</span>
<span class="kt">float</span> <span class="n">neg_inf</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span> <span class="o">/</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="p">;</span> <span class="c1">// -inf</span>

<span class="n">printf</span><span class="p">(</span><span class="s">"%f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">inf</span> <span class="o">+</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="p">);</span>   <span class="c1">// inf</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">inf</span> <span class="o">+</span> <span class="n">neg_inf</span><span class="p">);</span> <span class="c1">// nan (inf - inf는 정의 불가)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="nan-not-a-number">NaN (Not a Number)</h3>

<p>정의 불가한 연산의 결과다. <strong>NaN은 자기 자신과도 같지 않다</strong> — 이것이 NaN 탐지의 핵심이다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="cp">#include</span> <span class="cpf">&lt;math.h&gt;</span><span class="cp">
</span>
<span class="kt">float</span> <span class="n">nan_val</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span> <span class="o">/</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="p">;</span>

<span class="n">printf</span><span class="p">(</span><span class="s">"%d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">nan_val</span> <span class="o">==</span> <span class="n">nan_val</span><span class="p">);</span>  <span class="c1">// 0 (false!)</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">isnan</span><span class="p">(</span><span class="n">nan_val</span><span class="p">));</span>      <span class="c1">// 1 (true)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Java에서도 동일한 동작을 한다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="kt">double</span> <span class="n">nan</span> <span class="o">=</span> <span class="nc">Double</span><span class="o">.</span><span class="na">NaN</span><span class="o">;</span>

<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">nan</span> <span class="o">==</span> <span class="n">nan</span><span class="o">);</span>           <span class="c1">// false</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="nc">Double</span><span class="o">.</span><span class="na">isNaN</span><span class="o">(</span><span class="n">nan</span><span class="o">));</span>    <span class="c1">// true</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="비정규화수-denormalized--subnormal-numbers">비정규화수 (Denormalized / Subnormal Numbers)</h3>

<p>Exponent가 전부 <code class="language-plaintext highlighter-rouge">0</code>이고 Mantissa가 <code class="language-plaintext highlighter-rouge">0</code>이 아닌 경우, <strong>implicit leading bit이 <code class="language-plaintext highlighter-rouge">0</code></strong> 이 된다. 이를 통해 0에 매우 가까운 아주 작은 수를 표현한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>값 = (-1)^sign × 0.mantissa × 2^(1 - bias)
</pre></td></tr></tbody></table></code></pre></div></div>

<p>단정도 기준으로 표현 가능한 가장 작은 양수:</p>

<ul>
  <li>정규화: <code class="language-plaintext highlighter-rouge">1.0 × 2^(-126)</code> ≈ <code class="language-plaintext highlighter-rouge">1.175e-38</code></li>
  <li>비정규화: <code class="language-plaintext highlighter-rouge">0.000...1 × 2^(-126)</code> ≈ <code class="language-plaintext highlighter-rouge">1.401e-45</code></li>
</ul>

<p>비정규화수 덕분에 0으로의 <em>gradual underflow</em> 가 가능하다.</p>

<hr />

<h2 id="정밀도-한계와-부동소수점-함정">정밀도 한계와 부동소수점 함정</h2>

<h3 id="01--02--03">0.1 + 0.2 != 0.3</h3>

<p>부동소수점의 가장 유명한 함정이다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="mf">0.1</span> <span class="o">+</span> <span class="mf">0.2</span><span class="o">);</span>           <span class="c1">// 0.30000000000000004</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="mf">0.1</span> <span class="o">+</span> <span class="mf">0.2</span> <span class="o">==</span> <span class="mf">0.3</span><span class="o">);</span>    <span class="c1">// false</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">printf</span><span class="p">(</span><span class="s">"%.20f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">1</span> <span class="o">+</span> <span class="mi">0</span><span class="p">.</span><span class="mi">2</span><span class="p">);</span>  <span class="c1">// 0.30000000000000004441</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%.20f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">3</span><span class="p">);</span>        <span class="c1">// 0.29999999999999998890</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>원인: <code class="language-plaintext highlighter-rouge">0.1</code>, <code class="language-plaintext highlighter-rouge">0.2</code>, <code class="language-plaintext highlighter-rouge">0.3</code> 모두 이진수로 무한반복소수이다. 유한 비트로 표현하면서 각각 미세한 오차가 발생하고, 이 오차들이 합산되면서 결과가 달라진다.</p>

<h3 id="큰-수와-작은-수의-덧셈">큰 수와 작은 수의 덧셈</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="kt">float</span> <span class="n">big</span>   <span class="o">=</span> <span class="mf">16777216.0f</span><span class="o">;</span>  <span class="c1">// 2^24, 정확히 표현 가능</span>
<span class="kt">float</span> <span class="n">small</span> <span class="o">=</span> <span class="mf">1.0f</span><span class="o">;</span>

<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">big</span> <span class="o">+</span> <span class="n">small</span> <span class="o">==</span> <span class="n">big</span><span class="o">);</span>  <span class="c1">// true!</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>단정도의 mantissa가 23비트이므로 <code class="language-plaintext highlighter-rouge">2^24</code>에 <code class="language-plaintext highlighter-rouge">1</code>을 더해도 mantissa에 담을 수 없어 반올림되어 사라진다. 이를 <strong>absorption</strong> 현상이라 한다.</p>

<h3 id="결합법칙이-성립하지-않는다">결합법칙이 성립하지 않는다</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="kt">double</span> <span class="n">a</span> <span class="o">=</span> <span class="mi">1</span><span class="n">e15</span><span class="o">,</span> <span class="n">b</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="n">e15</span><span class="o">,</span> <span class="n">c</span> <span class="o">=</span> <span class="mf">1.0</span><span class="o">;</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">((</span><span class="n">a</span> <span class="o">+</span> <span class="n">b</span><span class="o">)</span> <span class="o">+</span> <span class="n">c</span><span class="o">);</span>  <span class="c1">// 1.0</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">a</span> <span class="o">+</span> <span class="o">(</span><span class="n">b</span> <span class="o">+</span> <span class="n">c</span><span class="o">));</span>  <span class="c1">// 0.0  ← 다르다!</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>부동소수점 연산은 <code class="language-plaintext highlighter-rouge">(a + b) + c ≠ a + (b + c)</code>일 수 있다. 이것이 수치 계산 라이브러리가 덧셈 순서까지 신경 쓰는 이유다 (Kahan summation 등).</p>

<h3 id="정밀도-비교-epsilon-방식">정밀도 비교: epsilon 방식</h3>

<p>부동소수점 비교는 <code class="language-plaintext highlighter-rouge">==</code> 대신 <strong>epsilon 비교</strong>를 사용해야 한다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">double</span> <span class="no">EPSILON</span> <span class="o">=</span> <span class="mi">1</span><span class="n">e</span><span class="o">-</span><span class="mi">10</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">static</span> <span class="kt">boolean</span> <span class="nf">nearlyEqual</span><span class="o">(</span><span class="kt">double</span> <span class="n">a</span><span class="o">,</span> <span class="kt">double</span> <span class="n">b</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">Math</span><span class="o">.</span><span class="na">abs</span><span class="o">(</span><span class="n">a</span> <span class="o">-</span> <span class="n">b</span><span class="o">)</span> <span class="o">&lt;</span> <span class="no">EPSILON</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>단, 값의 크기에 따라 적절한 epsilon이 달라지므로, 더 정교한 비교가 필요할 때는 <strong>relative epsilon</strong> 또는 <strong>ULP(Unit in the Last Place)</strong> 기반 비교를 사용한다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c1">// Java의 Math.ulp 활용</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">boolean</span> <span class="nf">nearlyEqual</span><span class="o">(</span><span class="kt">double</span> <span class="n">a</span><span class="o">,</span> <span class="kt">double</span> <span class="n">b</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">Math</span><span class="o">.</span><span class="na">abs</span><span class="o">(</span><span class="n">a</span> <span class="o">-</span> <span class="n">b</span><span class="o">)</span> <span class="o">&lt;=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">max</span><span class="o">(</span><span class="nc">Math</span><span class="o">.</span><span class="na">ulp</span><span class="o">(</span><span class="n">a</span><span class="o">),</span> <span class="nc">Math</span><span class="o">.</span><span class="na">ulp</span><span class="o">(</span><span class="n">b</span><span class="o">));</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="단정도-vs-배정도-비교">단정도 vs 배정도 비교</h2>

<table>
  <thead>
    <tr>
      <th>속성</th>
      <th><code class="language-plaintext highlighter-rouge">float</code> (32-bit)</th>
      <th><code class="language-plaintext highlighter-rouge">double</code> (64-bit)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Mantissa 비트</td>
      <td>23</td>
      <td>52</td>
    </tr>
    <tr>
      <td>유효 십진 자릿수</td>
      <td>~7자리</td>
      <td>~15-16자리</td>
    </tr>
    <tr>
      <td>최대값</td>
      <td>~3.4 × 10^38</td>
      <td>~1.8 × 10^308</td>
    </tr>
    <tr>
      <td>최소 정규화 양수</td>
      <td>~1.2 × 10^-38</td>
      <td>~2.2 × 10^-308</td>
    </tr>
    <tr>
      <td>Exponent 범위</td>
      <td>-126 ~ +127</td>
      <td>-1022 ~ +1023</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="백엔드-개발자를-위한-실무-가이드">백엔드 개발자를 위한 실무 가이드</h2>

<h3 id="db-float-vs-decimal">DB: FLOAT vs DECIMAL</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c1">-- FLOAT/DOUBLE: IEEE 754 기반, 근사값 저장</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">products</span> <span class="p">(</span>
    <span class="n">price</span> <span class="nb">FLOAT</span>  <span class="c1">-- 절대 하지 마세요!</span>
<span class="p">);</span>

<span class="c1">-- DECIMAL: 고정소수점, 정확한 값 저장</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">products</span> <span class="p">(</span>
    <span class="n">price</span> <span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>  <span class="c1">-- 소수점 이하 2자리까지 정확</span>
<span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>금액(money)은 절대 FLOAT/DOUBLE로 저장하지 않는다.</strong> 반올림 오차가 누적되면 회계 장부에서 수 원~수십 원의 차이가 발생한다.</p>

<ul>
  <li><strong>FLOAT/DOUBLE</strong> — 과학 계산, 통계, 좌표(위도/경도) 등 <em>근사값이 허용되는</em> 경우에 사용</li>
  <li><strong>DECIMAL</strong> — 금액, 세율, 환율 등 <em>정확한 값이 필요한</em> 경우에 사용</li>
</ul>

<h3 id="java-double-vs-bigdecimal">Java: <code class="language-plaintext highlighter-rouge">double</code> vs <code class="language-plaintext highlighter-rouge">BigDecimal</code></h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c1">// 절대 이렇게 하면 안 된다</span>
<span class="kt">double</span> <span class="n">price</span> <span class="o">=</span> <span class="mf">0.1</span><span class="o">;</span>
<span class="kt">double</span> <span class="n">quantity</span> <span class="o">=</span> <span class="mi">3</span><span class="o">;</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">price</span> <span class="o">*</span> <span class="n">quantity</span><span class="o">);</span>  <span class="c1">// 0.30000000000000004</span>

<span class="c1">// BigDecimal 사용 (문자열 생성자 필수!)</span>
<span class="nc">BigDecimal</span> <span class="n">price</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BigDecimal</span><span class="o">(</span><span class="s">"0.1"</span><span class="o">);</span>
<span class="nc">BigDecimal</span> <span class="n">quantity</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BigDecimal</span><span class="o">(</span><span class="s">"3"</span><span class="o">);</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">price</span><span class="o">.</span><span class="na">multiply</span><span class="o">(</span><span class="n">quantity</span><span class="o">));</span>  <span class="c1">// 0.3</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>주의: <code class="language-plaintext highlighter-rouge">new BigDecimal(0.1)</code>은 이미 오차가 있는 double 값을 그대로 가져오므로 <strong>반드시 문자열 생성자</strong> <code class="language-plaintext highlighter-rouge">new BigDecimal("0.1")</code>을 사용해야 한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="k">new</span> <span class="nc">BigDecimal</span><span class="o">(</span><span class="mf">0.1</span><span class="o">));</span>
<span class="c1">// 0.1000000000000000055511151231257827021181583404541015625</span>

<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="k">new</span> <span class="nc">BigDecimal</span><span class="o">(</span><span class="s">"0.1"</span><span class="o">));</span>
<span class="c1">// 0.1</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="정리-타입-선택-가이드">정리: 타입 선택 가이드</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>금액 계산       → BigDecimal (Java), DECIMAL (DB)
과학/통계 계산  → double
ML/그래픽스     → float (메모리/속도 중시)
좌표(위경도)    → double (FLOAT도 가능하나 정밀도 주의)
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="마무리">마무리</h2>

<p>IEEE 754는 한정된 비트로 실수를 표현하기 위한 정교한 타협이다. 대부분의 경우 잘 작동하지만, <strong>정밀도의 한계</strong>를 이해하지 못하면 디버깅하기 어려운 버그를 만들게 된다.</p>

<p>핵심 정리:</p>

<ol>
  <li><strong>구조</strong>: Sign(1) + Exponent(biased) + Mantissa(implicit leading 1)</li>
  <li><strong>특수 값</strong>: ±0, ±Infinity, NaN — 각각 고유한 비트 패턴</li>
  <li><strong>함정</strong>: <code class="language-plaintext highlighter-rouge">0.1 + 0.2 != 0.3</code>, absorption, 결합법칙 깨짐</li>
  <li><strong>실무 원칙</strong>: 돈은 <code class="language-plaintext highlighter-rouge">BigDecimal</code>/<code class="language-plaintext highlighter-rouge">DECIMAL</code>, 비교는 epsilon 방식</li>
</ol>

<p>다음 글에서는 문자(Character)와 문자열(String) 인코딩 — ASCII, Unicode, UTF-8 — 을 다룰 예정이다.</p>

<hr />

<h2 id="references">References</h2>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/IEEE_754">IEEE 754 — Wikipedia</a></li>
  <li><a href="https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html">What Every Computer Scientist Should Know About Floating-Point Arithmetic</a></li>
  <li><a href="https://float.exposed/">Float Exposed — Interactive IEEE 754 Visualization</a></li>
  <li><a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/math/BigDecimal.html">Java BigDecimal — Oracle Docs</a></li>
</ul>

<hr />

<h2 id="관련-포스트">관련 포스트</h2>

<ul>
  <li><a href="/cs/2026/04/01/data-representation-integer/">정수 표현 방식 완전 정복</a></li>
</ul>
]]></content:encoded>
        <pubDate>Wed, 01 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/cs/2026/04/01/data-representation-float/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/cs/2026/04/01/data-representation-float/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>CS</category>
        
        <category>CS Fundamentals</category>
        
        
        <category>cs</category>
        
      </item>
    
      <item>
        <title>Trie 자료구조 — 원리와 구현</title>
        <description>Trie란</description>
        <content:encoded><![CDATA[<h2 id="trie란">Trie란</h2>

<p>검색 트리의 일종으로, 문자열을 저장하고 탐색하는데 효율적인 자료구조이다.
Trie는 문자열의 접두사(Prefix)를 이용하여 트리를 구성하므로, 특히나 문자열 검색에 유용하다.
이름은 re<strong>trie</strong>val(검색)에서 유래되었다.</p>

<p>예를 들어 <code class="language-plaintext highlighter-rouge">["apple", "app", "april", "bat", "ball"]</code>이라는 단어들을 저장한다고 하면, 공통 접두사를 공유하는 트리 구조가 만들어진다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre>        (root)
       /      \
      a         b
      |         |
      p         a
     / \        |  \
    p   r       t   l
    |   |           |
    l   i           l
    |   |
    e   l
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="trie의-특징">Trie의 특징</h3>

<ul>
  <li>각 노드는 하나의 문자를 저장한다</li>
  <li>루트 노드는 빈 문자열을 나타낸다</li>
  <li>각 노드는 해당 노드까지의 문자열(prefix)을 나타내며, 단어의 끝을 표시하는 플래그(<code class="language-plaintext highlighter-rouge">isEnd</code>)를 가진다</li>
  <li>자식 노드는 해당 문자 다음에 나타날 수 있는 문자를 저장하는 데 사용된다</li>
</ul>

<h3 id="시간-복잡도">시간 복잡도</h3>

<table>
  <thead>
    <tr>
      <th>연산</th>
      <th>시간 복잡도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>삽입</td>
      <td>O(L)</td>
    </tr>
    <tr>
      <td>검색</td>
      <td>O(L)</td>
    </tr>
    <tr>
      <td>접두사 검색</td>
      <td>O(L)</td>
    </tr>
    <tr>
      <td>삭제</td>
      <td>O(L)</td>
    </tr>
  </tbody>
</table>

<p>여기서 <code class="language-plaintext highlighter-rouge">L</code>은 문자열의 길이이다. HashMap으로 검색하면 O(L)이지만 접두사 기반 검색은 불가능하다. Trie는 접두사 검색까지 O(L)에 처리할 수 있다는 점이 핵심 장점이다.</p>

<h3 id="공간-복잡도">공간 복잡도</h3>

<p>최악의 경우 O(ALPHABET_SIZE × L × N)으로, N은 저장된 문자열의 수이다. 공통 접두사가 많을수록 공간 효율이 좋아진다.</p>

<p><br /></p>

<h2 id="trie-구현-java">Trie 구현 (Java)</h2>

<h3 id="노드-정의">노드 정의</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="kd">class</span> <span class="nc">TrieNode</span> <span class="o">{</span>
    <span class="nc">TrieNode</span><span class="o">[]</span> <span class="n">children</span><span class="o">;</span>
    <span class="kt">boolean</span> <span class="n">isEnd</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">TrieNode</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">children</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TrieNode</span><span class="o">[</span><span class="mi">26</span><span class="o">];</span> <span class="c1">// 소문자 알파벳만 고려</span>
        <span class="n">isEnd</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="trie-클래스">Trie 클래스</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
</pre></td><td class="rouge-code"><pre><span class="kd">class</span> <span class="nc">Trie</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">TrieNode</span> <span class="n">root</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">Trie</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">root</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TrieNode</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="c1">// 삽입</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">insert</span><span class="o">(</span><span class="nc">String</span> <span class="n">word</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">TrieNode</span> <span class="n">node</span> <span class="o">=</span> <span class="n">root</span><span class="o">;</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">char</span> <span class="n">c</span> <span class="o">:</span> <span class="n">word</span><span class="o">.</span><span class="na">toCharArray</span><span class="o">())</span> <span class="o">{</span>
            <span class="kt">int</span> <span class="n">idx</span> <span class="o">=</span> <span class="n">c</span> <span class="o">-</span> <span class="sc">'a'</span><span class="o">;</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">node</span><span class="o">.</span><span class="na">children</span><span class="o">[</span><span class="n">idx</span><span class="o">]</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">node</span><span class="o">.</span><span class="na">children</span><span class="o">[</span><span class="n">idx</span><span class="o">]</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TrieNode</span><span class="o">();</span>
            <span class="o">}</span>
            <span class="n">node</span> <span class="o">=</span> <span class="n">node</span><span class="o">.</span><span class="na">children</span><span class="o">[</span><span class="n">idx</span><span class="o">];</span>
        <span class="o">}</span>
        <span class="n">node</span><span class="o">.</span><span class="na">isEnd</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="c1">// 검색: 해당 단어가 존재하는지 확인</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">search</span><span class="o">(</span><span class="nc">String</span> <span class="n">word</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">TrieNode</span> <span class="n">node</span> <span class="o">=</span> <span class="n">findNode</span><span class="o">(</span><span class="n">word</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">node</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">node</span><span class="o">.</span><span class="na">isEnd</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="c1">// 접두사 검색: 해당 접두사로 시작하는 단어가 있는지 확인</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">startsWith</span><span class="o">(</span><span class="nc">String</span> <span class="n">prefix</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nf">findNode</span><span class="o">(</span><span class="n">prefix</span><span class="o">)</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">TrieNode</span> <span class="nf">findNode</span><span class="o">(</span><span class="nc">String</span> <span class="n">str</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">TrieNode</span> <span class="n">node</span> <span class="o">=</span> <span class="n">root</span><span class="o">;</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">char</span> <span class="n">c</span> <span class="o">:</span> <span class="n">str</span><span class="o">.</span><span class="na">toCharArray</span><span class="o">())</span> <span class="o">{</span>
            <span class="kt">int</span> <span class="n">idx</span> <span class="o">=</span> <span class="n">c</span> <span class="o">-</span> <span class="sc">'a'</span><span class="o">;</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">node</span><span class="o">.</span><span class="na">children</span><span class="o">[</span><span class="n">idx</span><span class="o">]</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
            <span class="o">}</span>
            <span class="n">node</span> <span class="o">=</span> <span class="n">node</span><span class="o">.</span><span class="na">children</span><span class="o">[</span><span class="n">idx</span><span class="o">];</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">node</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="사용-예시">사용 예시</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Trie</span> <span class="n">trie</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Trie</span><span class="o">();</span>

    <span class="n">trie</span><span class="o">.</span><span class="na">insert</span><span class="o">(</span><span class="s">"apple"</span><span class="o">);</span>
    <span class="n">trie</span><span class="o">.</span><span class="na">insert</span><span class="o">(</span><span class="s">"app"</span><span class="o">);</span>
    <span class="n">trie</span><span class="o">.</span><span class="na">insert</span><span class="o">(</span><span class="s">"april"</span><span class="o">);</span>

    <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">trie</span><span class="o">.</span><span class="na">search</span><span class="o">(</span><span class="s">"apple"</span><span class="o">));</span>     <span class="c1">// true</span>
    <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">trie</span><span class="o">.</span><span class="na">search</span><span class="o">(</span><span class="s">"app"</span><span class="o">));</span>       <span class="c1">// true</span>
    <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">trie</span><span class="o">.</span><span class="na">search</span><span class="o">(</span><span class="s">"ap"</span><span class="o">));</span>        <span class="c1">// false (삽입하지 않음)</span>
    <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">trie</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"ap"</span><span class="o">));</span>    <span class="c1">// true (접두사로는 존재)</span>
    <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">trie</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"bat"</span><span class="o">));</span>   <span class="c1">// false</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br /></p>

<h2 id="trie에서의-삭제">Trie에서의 삭제</h2>

<p>삭제는 삽입/검색보다 조금 더 복잡하다. 다른 단어의 접두사인 경우 노드 자체를 삭제하면 안 되기 때문이다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">delete</span><span class="o">(</span><span class="nc">String</span> <span class="n">word</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nf">delete</span><span class="o">(</span><span class="n">root</span><span class="o">,</span> <span class="n">word</span><span class="o">,</span> <span class="mi">0</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">delete</span><span class="o">(</span><span class="nc">TrieNode</span> <span class="n">node</span><span class="o">,</span> <span class="nc">String</span> <span class="n">word</span><span class="o">,</span> <span class="kt">int</span> <span class="n">depth</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">node</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>

    <span class="k">if</span> <span class="o">(</span><span class="n">depth</span> <span class="o">==</span> <span class="n">word</span><span class="o">.</span><span class="na">length</span><span class="o">())</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">node</span><span class="o">.</span><span class="na">isEnd</span><span class="o">)</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
        <span class="n">node</span><span class="o">.</span><span class="na">isEnd</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>
        <span class="k">return</span> <span class="nf">isEmpty</span><span class="o">(</span><span class="n">node</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kt">int</span> <span class="n">idx</span> <span class="o">=</span> <span class="n">word</span><span class="o">.</span><span class="na">charAt</span><span class="o">(</span><span class="n">depth</span><span class="o">)</span> <span class="o">-</span> <span class="sc">'a'</span><span class="o">;</span>
    <span class="kt">boolean</span> <span class="n">shouldDeleteChild</span> <span class="o">=</span> <span class="n">delete</span><span class="o">(</span><span class="n">node</span><span class="o">.</span><span class="na">children</span><span class="o">[</span><span class="n">idx</span><span class="o">],</span> <span class="n">word</span><span class="o">,</span> <span class="n">depth</span> <span class="o">+</span> <span class="mi">1</span><span class="o">);</span>

    <span class="k">if</span> <span class="o">(</span><span class="n">shouldDeleteChild</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">node</span><span class="o">.</span><span class="na">children</span><span class="o">[</span><span class="n">idx</span><span class="o">]</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
        <span class="k">return</span> <span class="o">!</span><span class="n">node</span><span class="o">.</span><span class="na">isEnd</span> <span class="o">&amp;&amp;</span> <span class="n">isEmpty</span><span class="o">(</span><span class="n">node</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">isEmpty</span><span class="o">(</span><span class="nc">TrieNode</span> <span class="n">node</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">for</span> <span class="o">(</span><span class="nc">TrieNode</span> <span class="n">child</span> <span class="o">:</span> <span class="n">node</span><span class="o">.</span><span class="na">children</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">child</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>삭제 로직의 핵심:</p>
<ol>
  <li>단어의 끝 노드에서 <code class="language-plaintext highlighter-rouge">isEnd = false</code>로 설정</li>
  <li>해당 노드에 자식이 없으면 노드를 삭제</li>
  <li>재귀적으로 올라가며 불필요한 노드들을 정리</li>
</ol>

<p><br /></p>

<h2 id="hashmap을-사용한-trie">HashMap을 사용한 Trie</h2>

<p>알파벳만 다루는 경우 배열이 효율적이지만, 유니코드 등 다양한 문자를 다뤄야 할 경우 <code class="language-plaintext highlighter-rouge">HashMap</code>을 사용할 수 있다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="kd">class</span> <span class="nc">TrieNode</span> <span class="o">{</span>
    <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">Character</span><span class="o">,</span> <span class="nc">TrieNode</span><span class="o">&gt;</span> <span class="n">children</span><span class="o">;</span>
    <span class="kt">boolean</span> <span class="n">isEnd</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">TrieNode</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">children</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;&gt;();</span>
        <span class="n">isEnd</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>이 방식은 메모리를 절약할 수 있지만, 배열 방식보다 접근 속도가 느릴 수 있다.</p>

<p><br /></p>

<h2 id="활용-사례">활용 사례</h2>

<p>Trie는 다음과 같은 상황에서 자주 사용된다:</p>

<ol>
  <li><strong>자동 완성(Autocomplete)</strong>: 입력한 접두사에 해당하는 모든 단어를 빠르게 검색</li>
  <li><strong>맞춤법 검사(Spell Checker)</strong>: 사전에 있는 단어인지 빠르게 확인</li>
  <li><strong>IP 라우팅(Longest Prefix Match)</strong>: 네트워크에서 가장 긴 접두사 매칭</li>
  <li><strong>문자열 정렬</strong>: Trie에 삽입 후 DFS로 사전순 정렬 가능</li>
</ol>

<h3 id="코딩-테스트-tip">코딩 테스트 Tip</h3>

<p>Trie 관련 대표 문제:</p>
<ul>
  <li><strong>LeetCode 208</strong> - Implement Trie (기본 구현)</li>
  <li><strong>LeetCode 211</strong> - Design Add and Search Words Data Structure (와일드카드 검색)</li>
  <li><strong>LeetCode 212</strong> - Word Search II (2D 보드에서 단어 검색, Trie + DFS)</li>
  <li><strong>백준 5052</strong> - 전화번호 목록 (접두사 판별)</li>
</ul>

<p><br /></p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/Trie">Wikipedia - Trie</a></li>
  <li><a href="https://www.geeksforgeeks.org/trie-insert-and-search/">GeeksforGeeks - Trie Data Structure</a></li>
</ul>

<hr />

<h2 id="관련-포스트">관련 포스트</h2>

<ul>
  <li><a href="/algorithm/2023/06/19/Segment-tree/">세그먼트 트리 — 구간 쿼리를 위한 트리 자료구조</a></li>
</ul>
]]></content:encoded>
        <pubDate>Wed, 01 Apr 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/algorithm/2026/04/01/trie/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/algorithm/2026/04/01/trie/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Algorithm</category>
        
        <category>Tree</category>
        
        
        <category>algorithm</category>
        
      </item>
    
      <item>
        <title>시스템 디자인: 캐싱 전략 (Cache-Aside, Write-Through, Write-Behind)</title>
        <description>캐싱이 필요한 이유</description>
        <content:encoded><![CDATA[<h2 id="캐싱이-필요한-이유">캐싱이 필요한 이유</h2>

<p>데이터베이스는 신뢰성과 일관성에 최적화된 저장소다. 하지만 모든 요청이 DB까지 도달하면 응답 지연이 발생하고, 트래픽이 몰리면 DB가 병목이 된다. <strong>캐시</strong>는 자주 접근하는 데이터를 메모리에 올려두어 응답 속도를 높이고 DB 부하를 줄이는 역할을 한다.</p>

<p>대표적인 인메모리 캐시 솔루션으로 <strong>Redis</strong>가 많이 사용된다. Redis를 활용한 구체적인 캐싱 패턴과 Cache Stampede 해결책은 <a href="/backend/2026/04/02/redis-caching-strategy/">Redis 캐싱 전략 완전 정복</a>에서 상세히 다룬다.</p>

<hr />

<h2 id="1-cache-aside-lazy-loading">1. Cache-Aside (Lazy Loading)</h2>

<p>가장 널리 사용되는 패턴이다. 애플리케이션이 직접 캐시를 관리한다.</p>

<h3 id="동작-흐름">동작 흐름</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre>읽기:
1. 캐시에서 데이터 조회 (Cache Hit?)
2. Hit → 캐시 데이터 반환
3. Miss → DB에서 조회 → 캐시에 저장 → 반환

쓰기:
1. DB에 데이터 쓰기
2. 캐시에서 해당 키 삭제 (invalidate)
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="spring-boot--redis-구현-예제">Spring Boot + Redis 구현 예제</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
</pre></td><td class="rouge-code"><pre><span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ProductService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ProductRepository</span> <span class="n">productRepository</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RedisTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Product</span><span class="o">&gt;</span> <span class="n">redisTemplate</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">KEY_PREFIX</span> <span class="o">=</span> <span class="s">"product:"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Duration</span> <span class="no">TTL</span> <span class="o">=</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">);</span>

    <span class="kd">public</span> <span class="nc">Product</span> <span class="nf">findById</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="no">KEY_PREFIX</span> <span class="o">+</span> <span class="n">id</span><span class="o">;</span>

        <span class="c1">// 1. 캐시 조회</span>
        <span class="nc">Product</span> <span class="n">cached</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">cached</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">cached</span><span class="o">;</span> <span class="c1">// Cache Hit</span>
        <span class="o">}</span>

        <span class="c1">// 2. Cache Miss → DB 조회</span>
        <span class="nc">Product</span> <span class="n">product</span> <span class="o">=</span> <span class="n">productRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">id</span><span class="o">)</span>
                <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">NoSuchElementException</span><span class="o">(</span><span class="s">"상품이 없습니다."</span><span class="o">));</span>

        <span class="c1">// 3. 캐시에 저장</span>
        <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">product</span><span class="o">,</span> <span class="no">TTL</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">product</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Transactional</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">update</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">,</span> <span class="nc">ProductUpdateRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Product</span> <span class="n">product</span> <span class="o">=</span> <span class="n">productRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">id</span><span class="o">)</span>
                <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">NoSuchElementException</span><span class="o">(</span><span class="s">"상품이 없습니다."</span><span class="o">));</span>
        <span class="n">product</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>

        <span class="c1">// DB 업데이트 후 캐시 무효화</span>
        <span class="n">redisTemplate</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="no">KEY_PREFIX</span> <span class="o">+</span> <span class="n">id</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="장단점">장단점</h3>

<table>
  <thead>
    <tr>
      <th>장점</th>
      <th>단점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>구현이 단순하고 직관적</td>
      <td>첫 요청은 항상 Cache Miss (Cold Start)</td>
    </tr>
    <tr>
      <td>실제로 요청된 데이터만 캐싱</td>
      <td>캐시 만료 전까지 stale 데이터 가능</td>
    </tr>
    <tr>
      <td>캐시 장애 시에도 DB에서 조회 가능</td>
      <td>애플리케이션에 캐시 로직이 침투</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="2-write-through">2. Write-Through</h2>

<p>데이터를 쓸 때 <strong>캐시와 DB에 동시에</strong> 쓰는 패턴이다.</p>

<h3 id="동작-흐름-1">동작 흐름</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>쓰기:
1. 캐시에 데이터 저장
2. 캐시가 동기적으로 DB에도 저장
→ 두 저장소가 항상 동기화됨

읽기:
1. 항상 캐시에서 읽기 (캐시에 최신 데이터 보장)
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="의사-코드">의사 코드</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kt">void</span> <span class="nf">saveProduct</span><span class="o">(</span><span class="nc">Product</span> <span class="n">product</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// 캐시와 DB에 동시 저장</span>
    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span>
        <span class="no">KEY_PREFIX</span> <span class="o">+</span> <span class="n">product</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">product</span><span class="o">,</span> <span class="no">TTL</span>
    <span class="o">);</span>
    <span class="n">productRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">product</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="nc">Product</span> <span class="nf">findById</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// 캐시에서 바로 조회 (항상 최신)</span>
    <span class="nc">Product</span> <span class="n">cached</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="no">KEY_PREFIX</span> <span class="o">+</span> <span class="n">id</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">cached</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">cached</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="c1">// Fallback (캐시 장애 등)</span>
    <span class="k">return</span> <span class="n">productRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">id</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="장단점-1">장단점</h3>

<table>
  <thead>
    <tr>
      <th>장점</th>
      <th>단점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>캐시 데이터 일관성 보장</td>
      <td><strong>쓰기 지연 증가</strong> (캐시 + DB 모두 기다림)</td>
    </tr>
    <tr>
      <td>읽기 시 항상 Cache Hit</td>
      <td>사용되지 않는 데이터까지 캐싱 (메모리 낭비)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="3-write-behind-write-back">3. Write-Behind (Write-Back)</h2>

<p>데이터를 <strong>캐시에만 먼저 쓰고</strong>, DB 반영은 <strong>비동기로 나중에</strong> 하는 패턴이다.</p>

<h3 id="동작-흐름-2">동작 흐름</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre>쓰기:
1. 캐시에 데이터 저장 (즉시 반환)
2. 백그라운드에서 일정 주기/조건에 따라 DB에 반영

읽기:
1. 캐시에서 읽기
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="개념-코드">개념 코드</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c1">// 쓰기 — 캐시에만 저장하고 큐에 등록</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">saveProduct</span><span class="o">(</span><span class="nc">Product</span> <span class="n">product</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="no">KEY_PREFIX</span> <span class="o">+</span> <span class="n">product</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">product</span><span class="o">);</span>
    <span class="n">writeQueue</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">product</span><span class="o">);</span> <span class="c1">// 비동기 처리 큐</span>
<span class="o">}</span>

<span class="c1">// 별도 스레드/스케줄러에서 주기적으로 DB 반영</span>
<span class="nd">@Scheduled</span><span class="o">(</span><span class="n">fixedDelay</span> <span class="o">=</span> <span class="mi">5000</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">flushToDatabase</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Product</span><span class="o">&gt;</span> <span class="n">batch</span> <span class="o">=</span> <span class="n">writeQueue</span><span class="o">.</span><span class="na">drain</span><span class="o">();</span>
    <span class="k">if</span> <span class="o">(!</span><span class="n">batch</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
        <span class="n">productRepository</span><span class="o">.</span><span class="na">saveAll</span><span class="o">(</span><span class="n">batch</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="장단점-2">장단점</h3>

<table>
  <thead>
    <tr>
      <th>장점</th>
      <th>단점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>쓰기 속도가 매우 빠름</strong></td>
      <td>캐시 장애 시 데이터 유실 위험</td>
    </tr>
    <tr>
      <td>DB 부하 분산 (배치 처리)</td>
      <td>구현 복잡도 높음</td>
    </tr>
    <tr>
      <td>짧은 시간에 같은 키를 여러 번 업데이트하면 마지막 값만 DB에 반영 (write 최적화)</td>
      <td>데이터 일관성 보장 어려움</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="어떤-전략을-언제-쓸까">어떤 전략을 언제 쓸까?</h2>

<table>
  <thead>
    <tr>
      <th>시나리오</th>
      <th>추천 전략</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>일반적인 읽기 중심 API</td>
      <td><strong>Cache-Aside</strong></td>
      <td>구현 간단, 필요한 데이터만 캐싱</td>
    </tr>
    <tr>
      <td>데이터 정합성이 중요한 서비스</td>
      <td><strong>Write-Through</strong></td>
      <td>캐시 = DB 동기화 보장</td>
    </tr>
    <tr>
      <td>쓰기가 매우 빈번한 서비스 (조회수, 좋아요)</td>
      <td><strong>Write-Behind</strong></td>
      <td>쓰기 성능 극대화, 배치 처리</td>
    </tr>
    <tr>
      <td>세션 스토어</td>
      <td><strong>Write-Through</strong></td>
      <td>세션 유실 방지</td>
    </tr>
    <tr>
      <td>랭킹/리더보드</td>
      <td><strong>Write-Behind</strong></td>
      <td>실시간 반영은 캐시, DB는 주기적 동기화</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="실무에서-흔히-하는-실수">실무에서 흔히 하는 실수</h2>

<ol>
  <li><strong>TTL 설정 누락</strong> — 캐시가 영원히 남아 메모리가 부족해진다</li>
  <li><strong>Cache Stampede</strong> — 인기 키의 TTL이 동시에 만료되어 DB에 요청이 몰린다 → TTL에 랜덤 값을 추가하자 (자세한 해결 전략은 <a href="/backend/2026/04/02/redis-caching-strategy/">Redis 캐싱 전략</a> 참고)</li>
  <li><strong>캐시 무효화 순서 실수</strong> — 캐시 삭제 후 DB 업데이트 vs DB 업데이트 후 캐시 삭제. 후자가 안전하다</li>
  <li><strong>직렬화 비용 무시</strong> — 큰 객체를 매번 JSON 직렬화/역직렬화하면 오히려 느려질 수 있다</li>
</ol>

<hr />

<h2 id="정리">정리</h2>

<p>캐싱은 시스템 성능을 크게 향상시킬 수 있지만, <strong>일관성과 복잡도 사이의 트레이드오프</strong>가 항상 존재한다. 서비스의 읽기/쓰기 비율, 데이터 정합성 요구 수준, 장애 허용 범위를 고려해서 적절한 전략을 선택하자. 대부분의 경우 <strong>Cache-Aside로 시작하고</strong>, 필요에 따라 Write-Through나 Write-Behind를 부분적으로 도입하는 것이 현실적인 접근이다.</p>
]]></content:encoded>
        <pubDate>Sat, 28 Mar 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/system-design/2026/03/28/caching-strategy/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/system-design/2026/03/28/caching-strategy/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>System Design</category>
        
        <category>Backend</category>
        
        <category>Redis</category>
        
        
        <category>system-design</category>
        
      </item>
    
      <item>
        <title>PostgreSQL 인덱스 제대로 이해하기</title>
        <description>인덱스가 왜 필요한가</description>
        <content:encoded><![CDATA[<h2 id="인덱스가-왜-필요한가">인덱스가 왜 필요한가</h2>

<p>100만 건의 주문 데이터에서 특정 사용자의 주문을 찾는다고 하자.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">orders</span> <span class="k">WHERE</span> <span class="n">user_id</span> <span class="o">=</span> <span class="mi">42</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>인덱스가 없으면 PostgreSQL은 <strong>Sequential Scan</strong> — 테이블의 모든 행을 처음부터 끝까지 읽는다. 행이 100만 개라면 100만 번 비교한다. 인덱스가 있으면 <strong>Index Scan</strong>으로 필요한 행만 빠르게 찾을 수 있다. 책의 목차와 같은 원리다.</p>

<hr />

<h2 id="b-tree-인덱스">B-Tree 인덱스</h2>

<p>PostgreSQL의 기본 인덱스 타입은 <strong>B-Tree(Balanced Tree)</strong>다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">idx_orders_user_id</span> <span class="k">ON</span> <span class="n">orders</span> <span class="p">(</span><span class="n">user_id</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>B-Tree의 특징:</p>
<ul>
  <li><strong>균형 트리</strong> 구조로 어떤 값을 찾든 동일한 깊이만큼만 탐색</li>
  <li>동등 비교(<code class="language-plaintext highlighter-rouge">=</code>), 범위 비교(<code class="language-plaintext highlighter-rouge">&lt;</code>, <code class="language-plaintext highlighter-rouge">&gt;</code>, <code class="language-plaintext highlighter-rouge">BETWEEN</code>), 정렬(<code class="language-plaintext highlighter-rouge">ORDER BY</code>)에 모두 효과적</li>
  <li><code class="language-plaintext highlighter-rouge">NULL</code> 값도 인덱스에 포함됨</li>
  <li>시간 복잡도: <strong>O(log n)</strong></li>
</ul>

<h3 id="인덱스-종류-비교">인덱스 종류 비교</h3>

<table>
  <thead>
    <tr>
      <th>타입</th>
      <th>용도</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>B-Tree</td>
      <td>범용 (기본값)</td>
      <td>대부분의 컬럼</td>
    </tr>
    <tr>
      <td>Hash</td>
      <td>동등 비교만</td>
      <td><code class="language-plaintext highlighter-rouge">WHERE status = 'ACTIVE'</code></td>
    </tr>
    <tr>
      <td>GIN</td>
      <td>배열, Full-text Search</td>
      <td>JSONB, tsvector</td>
    </tr>
    <tr>
      <td>GiST</td>
      <td>공간 데이터, 범위 타입</td>
      <td>PostGIS, tsrange</td>
    </tr>
    <tr>
      <td>BRIN</td>
      <td>물리적 정렬된 대용량 데이터</td>
      <td>시계열 데이터</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="explain-analyze로-쿼리-분석하기">EXPLAIN ANALYZE로 쿼리 분석하기</h2>

<p>인덱스를 만들었는데 정말 사용되고 있는 걸까? <strong>EXPLAIN ANALYZE</strong>로 확인할 수 있다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">EXPLAIN</span> <span class="k">ANALYZE</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">orders</span> <span class="k">WHERE</span> <span class="n">user_id</span> <span class="o">=</span> <span class="mi">42</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>인덱스가 없을 때:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre>Seq Scan on orders  (cost=0.00..18726.00 rows=523 width=64)
  (actual time=0.031..92.145 rows=487 loops=1)
  Filter: (user_id = 42)
  Rows Removed by Filter: 999513
Planning Time: 0.089 ms
Execution Time: 92.312 ms
</pre></td></tr></tbody></table></code></pre></div></div>

<p>인덱스 생성 후:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>Index Scan using idx_orders_user_id on orders  (cost=0.42..523.15 rows=523 width=64)
  (actual time=0.028..1.234 rows=487 loops=1)
  Index Cond: (user_id = 42)
Planning Time: 0.102 ms
Execution Time: 1.387 ms
</pre></td></tr></tbody></table></code></pre></div></div>

<p>92ms → 1.4ms로 <strong>약 66배</strong> 빨라졌다. 핵심 지표:</p>

<ul>
  <li><strong>Seq Scan → Index Scan</strong>: 인덱스가 사용되고 있음</li>
  <li><strong>actual time</strong>: 실제 소요 시간 (ms)</li>
  <li><strong>Rows Removed by Filter</strong>: Seq Scan에서 버려진 행 수 (높을수록 비효율)</li>
</ul>

<hr />

<h2 id="복합-인덱스-전략">복합 인덱스 전략</h2>

<p>여러 컬럼을 조합한 쿼리가 자주 사용된다면 <strong>복합 인덱스(Composite Index)</strong>를 고려하자.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c1">-- 특정 사용자의 최근 주문을 자주 조회하는 경우</span>
<span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">idx_orders_user_created</span>
    <span class="k">ON</span> <span class="n">orders</span> <span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">created_at</span> <span class="k">DESC</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c1">-- 이 쿼리에 최적화됨</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">orders</span>
<span class="k">WHERE</span> <span class="n">user_id</span> <span class="o">=</span> <span class="mi">42</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">created_at</span> <span class="k">DESC</span>
<span class="k">LIMIT</span> <span class="mi">10</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="복합-인덱스의-핵심-규칙-컬럼-순서">복합 인덱스의 핵심 규칙: 컬럼 순서</h3>

<p>복합 인덱스는 <strong>왼쪽 컬럼부터 순서대로</strong> 사용된다. 이를 <strong>Leftmost Prefix Rule</strong>이라 한다.</p>

<p>인덱스 <code class="language-plaintext highlighter-rouge">(user_id, status, created_at)</code>가 있을 때:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c1">-- ✅ 인덱스 사용됨</span>
<span class="k">WHERE</span> <span class="n">user_id</span> <span class="o">=</span> <span class="mi">42</span>
<span class="k">WHERE</span> <span class="n">user_id</span> <span class="o">=</span> <span class="mi">42</span> <span class="k">AND</span> <span class="n">status</span> <span class="o">=</span> <span class="s1">'PAID'</span>
<span class="k">WHERE</span> <span class="n">user_id</span> <span class="o">=</span> <span class="mi">42</span> <span class="k">AND</span> <span class="n">status</span> <span class="o">=</span> <span class="s1">'PAID'</span> <span class="k">AND</span> <span class="n">created_at</span> <span class="o">&gt;</span> <span class="s1">'2026-01-01'</span>

<span class="c1">-- ❌ 인덱스 사용 안 됨 (user_id 없이 중간 컬럼부터 시작)</span>
<span class="k">WHERE</span> <span class="n">status</span> <span class="o">=</span> <span class="s1">'PAID'</span>
<span class="k">WHERE</span> <span class="n">status</span> <span class="o">=</span> <span class="s1">'PAID'</span> <span class="k">AND</span> <span class="n">created_at</span> <span class="o">&gt;</span> <span class="s1">'2026-01-01'</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="컬럼-순서-결정-기준">컬럼 순서 결정 기준</h3>

<ol>
  <li><strong>동등 조건(<code class="language-plaintext highlighter-rouge">=</code>)에 사용되는 컬럼</strong> → 앞에 배치</li>
  <li><strong>범위 조건(<code class="language-plaintext highlighter-rouge">&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;</code>, <code class="language-plaintext highlighter-rouge">BETWEEN</code>)에 사용되는 컬럼</strong> → 뒤에 배치</li>
  <li><strong>카디널리티(고유 값 수)가 높은 컬럼</strong> → 앞에 배치</li>
</ol>

<hr />

<h2 id="인덱스를-걸면-안-되는-경우">인덱스를 걸면 안 되는 경우</h2>

<p>인덱스는 만능이 아니다. 오히려 성능을 떨어뜨리는 경우도 있다:</p>

<ul>
  <li><strong>쓰기가 매우 빈번한 테이블</strong>: INSERT/UPDATE/DELETE 시 인덱스도 함께 갱신되므로 오버헤드 발생. 동시 쓰기가 많다면 <a href="/backend/2026/04/06/database-transaction-isolation/">트랜잭션 격리 수준</a>도 함께 고려해야 한다</li>
  <li><strong>카디널리티가 극단적으로 낮은 컬럼</strong>: <code class="language-plaintext highlighter-rouge">gender</code>처럼 값이 2~3개뿐이면 Full Scan이 더 빠를 수 있음</li>
  <li><strong>테이블 크기가 작은 경우</strong>: 수천 건 이하라면 Seq Scan이 충분히 빠름</li>
</ul>

<hr />

<h2 id="실무-팁">실무 팁</h2>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c1">-- 사용되지 않는 인덱스 찾기</span>
<span class="k">SELECT</span> <span class="n">schemaname</span><span class="p">,</span> <span class="n">tablename</span><span class="p">,</span> <span class="n">indexname</span><span class="p">,</span> <span class="n">idx_scan</span>
<span class="k">FROM</span> <span class="n">pg_stat_user_indexes</span>
<span class="k">WHERE</span> <span class="n">idx_scan</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">pg_relation_size</span><span class="p">(</span><span class="n">indexrelid</span><span class="p">)</span> <span class="k">DESC</span><span class="p">;</span>

<span class="c1">-- 인덱스 크기 확인</span>
<span class="k">SELECT</span> <span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_indexes_size</span><span class="p">(</span><span class="s1">'orders'</span><span class="p">));</span>

<span class="c1">-- 테이블 + 인덱스 전체 크기</span>
<span class="k">SELECT</span> <span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_total_relation_size</span><span class="p">(</span><span class="s1">'orders'</span><span class="p">));</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>사용되지 않는 인덱스는 쓰기 성능만 떨어뜨리므로, 주기적으로 확인하고 정리하는 것이 좋다.</p>

<hr />

<h2 id="정리">정리</h2>

<ol>
  <li>인덱스는 읽기 성능을 높이지만 쓰기 성능을 낮춘다 — <strong>트레이드오프</strong>를 이해하자</li>
  <li><strong>EXPLAIN ANALYZE</strong>로 항상 실제 실행 계획을 확인하자</li>
  <li>복합 인덱스는 <strong>컬럼 순서</strong>가 핵심이다</li>
  <li>불필요한 인덱스는 정리하자 — 인덱스도 디스크와 메모리를 소비한다</li>
</ol>
]]></content:encoded>
        <pubDate>Wed, 25 Mar 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/database/2026/03/25/postgresql-index/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/database/2026/03/25/postgresql-index/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>PostgreSQL</category>
        
        <category>Database</category>
        
        <category>Backend</category>
        
        
        <category>database</category>
        
      </item>
    
      <item>
        <title>Docker 입문: 컨테이너로 개발 환경 통일하기</title>
        <description>Docker를 왜 쓰는가</description>
        <content:encoded><![CDATA[<h2 id="docker를-왜-쓰는가">Docker를 왜 쓰는가</h2>

<p>“제 컴퓨터에서는 되는데요?” — 개발자라면 한 번쯤 들어본 말이다.</p>

<p>팀원마다 OS, JDK 버전, 로컬 DB 설정이 다르면 동일한 코드가 다른 결과를 낳는다. Docker는 <strong>애플리케이션과 그 실행 환경을 하나의 패키지(컨테이너)로 묶어서</strong> 어디서든 동일하게 동작하도록 보장한다.</p>

<hr />

<h2 id="핵심-개념">핵심 개념</h2>

<h3 id="image-vs-container">Image vs Container</h3>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>Image</th>
      <th>Container</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>비유</td>
      <td>클래스</td>
      <td>인스턴스</td>
    </tr>
    <tr>
      <td>상태</td>
      <td>불변(Immutable)</td>
      <td>실행 중 변경 가능</td>
    </tr>
    <tr>
      <td>저장</td>
      <td>Docker Registry (Docker Hub 등)</td>
      <td>로컬 머신</td>
    </tr>
  </tbody>
</table>

<p>Image는 <strong>읽기 전용 템플릿</strong>이고, Container는 Image를 기반으로 생성된 <strong>실행 중인 프로세스</strong>다.</p>

<h3 id="layer-구조">Layer 구조</h3>

<p>Docker Image는 여러 개의 <strong>레이어</strong>로 구성된다. Dockerfile의 각 명령어(RUN, COPY 등)가 하나의 레이어를 생성하며, 변경되지 않은 레이어는 <strong>캐시</strong>를 활용해 빌드 속도를 높인다.</p>

<hr />

<h2 id="dockerfile-작성">Dockerfile 작성</h2>

<p>Spring Boot 애플리케이션을 컨테이너화하는 Dockerfile 예제:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="c"># 빌드 스테이지</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">eclipse-temurin:17-jdk-alpine</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> gradle/ gradle/</span>
<span class="k">COPY</span><span class="s"> gradlew build.gradle settings.gradle ./</span>
<span class="k">RUN </span>./gradlew dependencies <span class="nt">--no-daemon</span>
<span class="k">COPY</span><span class="s"> src/ src/</span>
<span class="k">RUN </span>./gradlew bootJar <span class="nt">--no-daemon</span>

<span class="c"># 실행 스테이지</span>
<span class="k">FROM</span><span class="s"> eclipse-temurin:17-jre-alpine</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> --from=builder /app/build/libs/*.jar app.jar</span>
<span class="k">EXPOSE</span><span class="s"> 8080</span>
<span class="k">ENTRYPOINT</span><span class="s"> ["java", "-jar", "app.jar"]</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>여기서 사용한 <strong>Multi-stage Build</strong>의 장점:</p>
<ul>
  <li>빌드에 필요한 JDK + Gradle은 최종 이미지에 포함되지 않는다</li>
  <li>최종 이미지에는 JRE + JAR만 들어가므로 이미지 크기가 훨씬 작다</li>
  <li>빌드 도구가 없으므로 보안 공격 표면(Attack Surface)도 줄어든다</li>
</ul>

<hr />

<h2 id="자주-쓰는-docker-명령어">자주 쓰는 Docker 명령어</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="c"># 이미지 빌드</span>
docker build <span class="nt">-t</span> my-app:1.0 <span class="nb">.</span>

<span class="c"># 컨테이너 실행</span>
docker run <span class="nt">-d</span> <span class="nt">-p</span> 8080:8080 <span class="nt">--name</span> my-app my-app:1.0

<span class="c"># 실행 중인 컨테이너 확인</span>
docker ps

<span class="c"># 로그 확인</span>
docker logs <span class="nt">-f</span> my-app

<span class="c"># 컨테이너 내부 접속</span>
docker <span class="nb">exec</span> <span class="nt">-it</span> my-app /bin/sh

<span class="c"># 컨테이너 정지 및 삭제</span>
docker stop my-app <span class="o">&amp;&amp;</span> docker <span class="nb">rm </span>my-app
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="docker-compose">Docker Compose</h2>

<p>실제 프로젝트에서는 애플리케이션 서버 하나만 띄우는 경우가 거의 없다. DB, Redis, 메시지 큐 등 여러 서비스를 함께 관리해야 한다. <strong>Docker Compose</strong>는 여러 컨테이너를 하나의 YAML 파일로 정의하고 한 번에 실행할 수 있게 해준다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
</pre></td><td class="rouge-code"><pre><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.8'</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">app</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">.</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8080:8080"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">SPRING_DATASOURCE_URL</span><span class="pi">:</span> <span class="s">jdbc:postgresql://db:5432/myapp</span>
      <span class="na">SPRING_DATASOURCE_USERNAME</span><span class="pi">:</span> <span class="s">postgres</span>
      <span class="na">SPRING_DATASOURCE_PASSWORD</span><span class="pi">:</span> <span class="s">secret</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="na">db</span><span class="pi">:</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">service_healthy</span>

  <span class="na">db</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:16-alpine</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">myapp</span>
      <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">postgres</span>
      <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">secret</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">5432:5432"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">pgdata:/var/lib/postgresql/data</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD-SHELL"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">pg_isready</span><span class="nv"> </span><span class="s">-U</span><span class="nv"> </span><span class="s">postgres"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">3s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">pgdata</span><span class="pi">:</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>주요 포인트:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">depends_on</code> + <code class="language-plaintext highlighter-rouge">healthcheck</code> — DB가 완전히 준비된 후 앱이 시작된다</li>
  <li><code class="language-plaintext highlighter-rouge">volumes</code> — 컨테이너가 삭제되어도 데이터가 유지된다</li>
  <li>서비스 이름(<code class="language-plaintext highlighter-rouge">db</code>)이 곧 내부 DNS 호스트명이 된다 (<code class="language-plaintext highlighter-rouge">jdbc:postgresql://db:5432/myapp</code>)</li>
</ul>

<p>실행은 단 한 줄:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker compose up <span class="nt">-d</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="dockerignore">.dockerignore</h2>

<p>빌드 컨텍스트에서 불필요한 파일을 제외하면 빌드 속도가 빨라지고 이미지 크기도 줄어든다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>.git
.gradle
build
*.md
.env
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="정리">정리</h2>

<table>
  <thead>
    <tr>
      <th>개념</th>
      <th>핵심</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Image</td>
      <td>불변 실행 환경 템플릿</td>
    </tr>
    <tr>
      <td>Container</td>
      <td>Image 기반 실행 인스턴스</td>
    </tr>
    <tr>
      <td>Dockerfile</td>
      <td>Image 빌드 레시피</td>
    </tr>
    <tr>
      <td>Docker Compose</td>
      <td>다중 컨테이너 오케스트레이션</td>
    </tr>
    <tr>
      <td>Volume</td>
      <td>데이터 영속성 보장</td>
    </tr>
  </tbody>
</table>

<p>Docker를 익히면 로컬 개발 환경 구축이 편해질 뿐 아니라, CI/CD 파이프라인과 Kubernetes로 나아가는 기반이 된다. 다음에는 GitHub Actions와 Docker를 연동한 CI/CD 파이프라인 구축을 다뤄볼 예정이다.</p>

<hr />

<h2 id="관련-포스트">관련 포스트</h2>

<ul>
  <li><a href="/infra/2026/04/01/kubernetes-basics/">Kubernetes 핵심 개념 — Pod부터 Deployment까지</a></li>
</ul>
]]></content:encoded>
        <pubDate>Fri, 20 Mar 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/infra/2026/03/20/docker-getting-started/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/infra/2026/03/20/docker-getting-started/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Docker</category>
        
        <category>DevOps</category>
        
        <category>Backend</category>
        
        
        <category>infra</category>
        
      </item>
    
      <item>
        <title>Spring Boot + JPA로 REST API 만들기</title>
        <description>들어가며</description>
        <content:encoded><![CDATA[<h2 id="들어가며">들어가며</h2>

<p>Spring Boot와 JPA를 사용하면 놀라울 정도로 적은 코드로 REST API를 만들 수 있다. 이번 글에서는 가장 기본적인 <strong>Entity → Repository → Service → Controller</strong> 계층 구조를 살펴보고, 간단한 게시글(Post) CRUD API를 구현해 본다.</p>

<hr />

<h2 id="프로젝트-세팅">프로젝트 세팅</h2>

<p><code class="language-plaintext highlighter-rouge">start.spring.io</code>에서 다음 의존성을 추가한다.</p>

<ul>
  <li><strong>Spring Web</strong> — REST Controller</li>
  <li><strong>Spring Data JPA</strong> — ORM</li>
  <li><strong>H2 Database</strong> — 개발용 인메모리 DB</li>
  <li><strong>Lombok</strong> — 보일러플레이트 제거</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">application.yml</code> 설정:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="na">spring</span><span class="pi">:</span>
  <span class="na">datasource</span><span class="pi">:</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">jdbc:h2:mem:testdb</span>
    <span class="na">driver-class-name</span><span class="pi">:</span> <span class="s">org.h2.Driver</span>
  <span class="na">jpa</span><span class="pi">:</span>
    <span class="na">hibernate</span><span class="pi">:</span>
      <span class="na">ddl-auto</span><span class="pi">:</span> <span class="s">create-drop</span>
    <span class="na">show-sql</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">hibernate</span><span class="pi">:</span>
        <span class="na">format_sql</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">h2</span><span class="pi">:</span>
    <span class="na">console</span><span class="pi">:</span>
      <span class="na">enabled</span><span class="pi">:</span> <span class="kc">true</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="1-entity">1. Entity</h2>

<p>Entity는 데이터베이스 테이블과 1:1로 매핑되는 클래스다. <code class="language-plaintext highlighter-rouge">@Entity</code> 어노테이션을 붙이면 JPA가 이 클래스를 기반으로 테이블을 자동 생성한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
</pre></td><td class="rouge-code"><pre><span class="nd">@Entity</span>
<span class="nd">@Getter</span>
<span class="nd">@NoArgsConstructor</span><span class="o">(</span><span class="n">access</span> <span class="o">=</span> <span class="nc">AccessLevel</span><span class="o">.</span><span class="na">PROTECTED</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Post</span> <span class="o">{</span>

    <span class="nd">@Id</span>
    <span class="nd">@GeneratedValue</span><span class="o">(</span><span class="n">strategy</span> <span class="o">=</span> <span class="nc">GenerationType</span><span class="o">.</span><span class="na">IDENTITY</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>

    <span class="nd">@Column</span><span class="o">(</span><span class="n">nullable</span> <span class="o">=</span> <span class="kc">false</span><span class="o">,</span> <span class="n">length</span> <span class="o">=</span> <span class="mi">100</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">title</span><span class="o">;</span>

    <span class="nd">@Column</span><span class="o">(</span><span class="n">columnDefinition</span> <span class="o">=</span> <span class="s">"TEXT"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">content</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nc">LocalDateTime</span> <span class="n">createdAt</span><span class="o">;</span>

    <span class="nd">@Builder</span>
    <span class="kd">public</span> <span class="nf">Post</span><span class="o">(</span><span class="nc">String</span> <span class="n">title</span><span class="o">,</span> <span class="nc">String</span> <span class="n">content</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">title</span> <span class="o">=</span> <span class="n">title</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">content</span> <span class="o">=</span> <span class="n">content</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">createdAt</span> <span class="o">=</span> <span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">update</span><span class="o">(</span><span class="nc">String</span> <span class="n">title</span><span class="o">,</span> <span class="nc">String</span> <span class="n">content</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">title</span> <span class="o">=</span> <span class="n">title</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">content</span> <span class="o">=</span> <span class="n">content</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>포인트 정리:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">@Id</code> + <code class="language-plaintext highlighter-rouge">@GeneratedValue</code> — 기본키 자동 생성</li>
  <li><code class="language-plaintext highlighter-rouge">@NoArgsConstructor(access = PROTECTED)</code> — JPA 스펙 요구사항 (기본 생성자 필요) + 외부 직접 생성 방지</li>
  <li><code class="language-plaintext highlighter-rouge">update()</code> 메서드로 <strong>Dirty Checking</strong> 활용 (트랜잭션 내에서 필드 값 변경 시 자동 UPDATE 쿼리)</li>
</ul>

<hr />

<h2 id="2-repository">2. Repository</h2>

<p>Spring Data JPA의 <code class="language-plaintext highlighter-rouge">JpaRepository</code>를 상속하면 기본 CRUD 메서드가 자동으로 제공된다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">PostRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Post</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Post</span><span class="o">&gt;</span> <span class="nf">findByTitleContaining</span><span class="o">(</span><span class="nc">String</span> <span class="n">keyword</span><span class="o">);</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">findByTitleContaining</code> 같은 <strong>메서드 이름 기반 쿼리</strong>를 사용하면 별도의 SQL 작성 없이도 검색 기능을 구현할 수 있다. 내부적으로 <code class="language-plaintext highlighter-rouge">LIKE '%keyword%'</code> 쿼리가 생성된다.</p>

<hr />

<h2 id="3-service">3. Service</h2>

<p>비즈니스 로직을 담당하는 계층이다. Controller에서 직접 Repository를 호출하지 않고 Service를 거치는 이유는 <strong>관심사 분리</strong>와 <strong>트랜잭션 관리</strong> 때문이다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
</pre></td><td class="rouge-code"><pre><span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="nd">@Transactional</span><span class="o">(</span><span class="n">readOnly</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">PostService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">PostRepository</span> <span class="n">postRepository</span><span class="o">;</span>

    <span class="c1">// 게시글 전체 조회</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Post</span><span class="o">&gt;</span> <span class="nf">findAll</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">postRepository</span><span class="o">.</span><span class="na">findAll</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="c1">// 게시글 단건 조회</span>
    <span class="kd">public</span> <span class="nc">Post</span> <span class="nf">findById</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">postRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">id</span><span class="o">)</span>
                <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">IllegalArgumentException</span><span class="o">(</span><span class="s">"해당 게시글이 없습니다. id="</span> <span class="o">+</span> <span class="n">id</span><span class="o">));</span>
    <span class="o">}</span>

    <span class="c1">// 게시글 생성</span>
    <span class="nd">@Transactional</span>
    <span class="kd">public</span> <span class="nc">Long</span> <span class="nf">save</span><span class="o">(</span><span class="nc">String</span> <span class="n">title</span><span class="o">,</span> <span class="nc">String</span> <span class="n">content</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Post</span> <span class="n">post</span> <span class="o">=</span> <span class="nc">Post</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
                <span class="o">.</span><span class="na">title</span><span class="o">(</span><span class="n">title</span><span class="o">)</span>
                <span class="o">.</span><span class="na">content</span><span class="o">(</span><span class="n">content</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">postRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">post</span><span class="o">).</span><span class="na">getId</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="c1">// 게시글 수정</span>
    <span class="nd">@Transactional</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">update</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">,</span> <span class="nc">String</span> <span class="n">title</span><span class="o">,</span> <span class="nc">String</span> <span class="n">content</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Post</span> <span class="n">post</span> <span class="o">=</span> <span class="n">findById</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
        <span class="n">post</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">title</span><span class="o">,</span> <span class="n">content</span><span class="o">);</span> <span class="c1">// Dirty Checking</span>
    <span class="o">}</span>

    <span class="c1">// 게시글 삭제</span>
    <span class="nd">@Transactional</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">delete</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Post</span> <span class="n">post</span> <span class="o">=</span> <span class="n">findById</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
        <span class="n">postRepository</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="n">post</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">@Transactional(readOnly = true)</code>를 클래스 레벨에 걸고, 쓰기 작업에만 <code class="language-plaintext highlighter-rouge">@Transactional</code>을 따로 붙이는 패턴이 일반적이다. 읽기 전용 트랜잭션은 Hibernate의 플러시 모드를 MANUAL로 설정하기 때문에 성능상 이점이 있다.</p>

<hr />

<h2 id="4-controller">4. Controller</h2>

<p>클라이언트의 HTTP 요청을 받아서 Service에 위임하고, 결과를 JSON으로 반환한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
</pre></td><td class="rouge-code"><pre><span class="nd">@RestController</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/api/posts"</span><span class="o">)</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">PostController</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">PostService</span> <span class="n">postService</span><span class="o">;</span>

    <span class="nd">@GetMapping</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Post</span><span class="o">&gt;</span> <span class="nf">findAll</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">postService</span><span class="o">.</span><span class="na">findAll</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/{id}"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">Post</span> <span class="nf">findById</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">postService</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@PostMapping</span>
    <span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="nf">save</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">PostSaveRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Long</span> <span class="n">id</span> <span class="o">=</span> <span class="n">postService</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getTitle</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getContent</span><span class="o">());</span>
        <span class="k">return</span> <span class="nc">ResponseEntity</span><span class="o">.</span><span class="na">status</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">CREATED</span><span class="o">).</span><span class="na">body</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@PutMapping</span><span class="o">(</span><span class="s">"/{id}"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="nf">update</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">,</span>
                                        <span class="nd">@RequestBody</span> <span class="nc">PostUpdateRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">postService</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">id</span><span class="o">,</span> <span class="n">request</span><span class="o">.</span><span class="na">getTitle</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getContent</span><span class="o">());</span>
        <span class="k">return</span> <span class="nc">ResponseEntity</span><span class="o">.</span><span class="na">ok</span><span class="o">().</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@DeleteMapping</span><span class="o">(</span><span class="s">"/{id}"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="nf">delete</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">postService</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
        <span class="k">return</span> <span class="nc">ResponseEntity</span><span class="o">.</span><span class="na">noContent</span><span class="o">().</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="전체-흐름-정리">전체 흐름 정리</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>Client → Controller → Service → Repository → Database
                         ↕
                      Entity (JPA 영속성 컨텍스트)
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li><strong>Controller</strong>: HTTP 요청/응답 처리</li>
  <li><strong>Service</strong>: 비즈니스 로직 + 트랜잭션 경계</li>
  <li><strong>Repository</strong>: 데이터 접근 추상화</li>
  <li><strong>Entity</strong>: 데이터베이스 테이블 매핑</li>
</ol>

<p>이 4계층 구조를 지키면 각 레이어의 역할이 명확해지고, 테스트 작성과 유지보수가 훨씬 수월해진다.</p>

<hr />

<h2 id="마무리">마무리</h2>

<p>이번 글에서는 Spring Boot + JPA의 가장 기본적인 패턴을 다뤘다. 실무에서는 여기에 <strong>DTO 변환</strong>, <strong>예외 처리(@ControllerAdvice)</strong>, <strong>Validation</strong>, <strong>페이징 처리</strong> 등이 추가된다. 또한 연관 관계가 복잡해지면 <a href="/spring/2026/04/04/jpa-n-plus-one-problem/">JPA N+1 문제</a>를 반드시 이해해야 한다. 다음 글에서는 이 API에 Spring Security를 적용하는 방법을 알아보겠다.</p>

<hr />

<h2 id="관련-포스트">관련 포스트</h2>

<ul>
  <li><a href="/spring/2026/04/01/spring-security-jwt/">Spring Security 6 + JWT 인증 구현</a></li>
</ul>
]]></content:encoded>
        <pubDate>Sun, 15 Mar 2026 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/spring/2026/03/15/spring-boot-jpa-basics/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/spring/2026/03/15/spring-boot-jpa-basics/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Spring</category>
        
        <category>Backend</category>
        
        <category>Java</category>
        
        
        <category>spring</category>
        
      </item>
    
      <item>
        <title>How to use React in Jekyll app</title>
        <description>How to Use React in Jekyll</description>
        <content:encoded><![CDATA[<h2 id="how-to-use-react-in-jekyll">How to Use React in Jekyll</h2>

<h3 id="create-react-app">Create react app</h3>
<p>Create react app in base directory that your Jekyll project.<br />
Use below code</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>npx create-react-app dev-react-pages
cd dev-react-pages
npm start
</pre></td></tr></tbody></table></code></pre></div></div>
<p>Then, you can see <code class="language-plaintext highlighter-rouge">dev-react-pages</code> application and will starting. dev-react-pages is only develop folder. 
Now, we will create <code class="language-plaintext highlighter-rouge">react-pages</code> that deployment only.<br />
<br /></p>

<h3 id="add-react-settings">Add react settings</h3>
<p>Add below lines to ignore node_modules when we commit.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>dev-react-pages/node_modules/
dev-react-pages/build/
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Also add below lines in Jekyll project <code class="language-plaintext highlighter-rouge">_config.yml</code>. Jekyll doesn’t have to know about react.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>exclude:
  - node_modules
  - dev-react-pages
</pre></td></tr></tbody></table></code></pre></div></div>
<p><br /></p>

<h3 id="make-your-react-webpage-uri">Make your react webpage uri</h3>
<p>Make your react project webpage uri to add below line. Thar will be <code class="language-plaintext highlighter-rouge">{your git-hub-pages-uri}\{react-uri}</code>.<br />
There is an example <a href="https://doodoo3804.github.io/react-pages/">https://doodoo3804.github.io/react-pages/</a></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>"homepage": "/react-pages/"
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="write-build-and-deployment-scrit">Write build and deployment scrit</h3>
<p>To deploy react page, build and copy results to deployment directory that we made first step and commit then.
There need some lines to build and deployment. To do this easily, we going to write some lines in react application <code class="language-plaintext highlighter-rouge">package.json</code>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre>"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject",
  
  // add here
    "predeploy": "npm run build",
    "purge": "rmdir /s /q ..\\react-pages\\static &amp;&amp; xcopy .\\build\\* ..\\react-pages\\ /E /y",
    "deploy": "npm run purge &amp;&amp; npm run frontmatter"
},
</pre></td></tr></tbody></table></code></pre></div></div>
<p>After that, <code class="language-plaintext highlighter-rouge">yarn run deploy</code> to build and copy results. Last step is check your react page and commit all results.</p>
]]></content:encoded>
        <pubDate>Fri, 04 Aug 2023 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/react/2023/08/04/how-to-use-react-in-jekyll-app/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/react/2023/08/04/how-to-use-react-in-jekyll-app/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>React</category>
        
        
        <category>react</category>
        
      </item>
    
      <item>
        <title>Minimum Spanning Tree</title>
        <description>MST 최소 신장 트리
신장 트리란 하나의 그래프가 있을 때 모든 노드를 포함하면서 사이클이 존재하지 않는 부분 그래프를 의미한다.
최소한의 비용으로 신장 트리를 찾는 것이 MST 알고리즘이다.</description>
        <content:encoded><![CDATA[<h2 id="mst-최소-신장-트리">MST 최소 신장 트리</h2>
<p>신장 트리란 하나의 그래프가 있을 때 모든 노드를 포함하면서 사이클이 존재하지 않는 부분 그래프를 의미한다.<br />
최소한의 비용으로 신장 트리를 찾는 것이 MST 알고리즘이다.</p>

<h2 id="mst-구현">MST 구현</h2>
<h3 id="1-그루스칼kruskal">1. 그루스칼(Kruskal)</h3>
<blockquote>
  <p>그리디 알고리즘의 일종으로 분류</p>
</blockquote>

<p>모든 간선에 대하여 정렬을 수행한 뒤에 가장 거리가 짧은 간선부터 집합에 포함<br />
사이클을 발생하는 경우 집합에서 제외한다.<br />
일종의 트리 구조이므로 최종적으로 만들어지는 신장 트리에 포함되는 간선의 개수가 <code class="language-plaintext highlighter-rouge">노드의 개수 - 1</code>과 같다.<br />
<br />
<b>그루스칼 알고리즘 구현</b><br />
[ with <b><code class="language-plaintext highlighter-rouge">C++</code></b> ]</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
</pre></td><td class="rouge-code"><pre><span class="c1">// 정점이 7개인 그래프로 예시</span>
<span class="k">const</span> <span class="kt">int</span> <span class="n">V</span> <span class="o">=</span> <span class="mi">7</span><span class="p">;</span>
<span class="n">vector</span><span class="o">&lt;</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">pair</span><span class="o">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="o">&gt;&gt;&gt;</span><span class="n">graph</span><span class="p">;</span>

<span class="k">struct</span> <span class="nc">Edge</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">src</span><span class="p">,</span> <span class="n">dest</span><span class="p">,</span> <span class="n">weight</span><span class="p">;</span>
<span class="p">};</span>

<span class="c1">// 노드 x의 부모를 찾는 함수</span>
<span class="kt">int</span> <span class="nf">find</span><span class="p">(</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;&amp;</span> <span class="n">parent</span><span class="p">,</span> <span class="kt">int</span> <span class="n">x</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">parent</span><span class="p">[</span><span class="n">x</span><span class="p">]</span> <span class="o">!=</span> <span class="n">x</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">parent</span><span class="p">[</span><span class="n">x</span><span class="p">]</span> <span class="o">=</span> <span class="n">find</span><span class="p">(</span><span class="n">parent</span><span class="p">,</span> <span class="n">parent</span><span class="p">[</span><span class="n">x</span><span class="p">]);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">parent</span><span class="p">[</span><span class="n">x</span><span class="p">];</span>
<span class="p">}</span>

<span class="c1">// 두 노드가 속한 집합을 합치는 함수</span>
<span class="kt">void</span> <span class="nf">merge</span><span class="p">(</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;&amp;</span><span class="n">parent</span><span class="p">,</span> <span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;&amp;</span> <span class="n">rank</span><span class="p">,</span> <span class="kt">int</span> <span class="n">u</span><span class="p">,</span> <span class="kt">int</span> <span class="n">v</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">u</span> <span class="o">=</span> <span class="n">find</span><span class="p">(</span><span class="n">parent</span><span class="p">,</span> <span class="n">u</span><span class="p">);</span>
    <span class="n">v</span> <span class="o">=</span> <span class="n">find</span><span class="p">(</span><span class="n">parent</span><span class="p">,</span> <span class="n">v</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">rank</span><span class="p">[</span><span class="n">u</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">rank</span><span class="p">[</span><span class="n">v</span><span class="p">])</span> <span class="p">{</span>
        <span class="n">swap</span><span class="p">(</span><span class="n">u</span><span class="p">,</span> <span class="n">v</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="n">parent</span><span class="p">[</span><span class="n">u</span><span class="p">]</span> <span class="o">=</span> <span class="n">v</span><span class="p">;</span>
    <span class="k">if</span><span class="p">(</span><span class="n">rank</span><span class="p">[</span><span class="n">u</span><span class="p">]</span> <span class="o">==</span> <span class="n">rank</span><span class="p">[</span><span class="n">v</span><span class="p">])</span> <span class="p">{</span>
        <span class="n">rank</span><span class="p">[</span><span class="n">v</span><span class="p">]</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 그루스칼 알고리즘</span>
<span class="n">vector</span><span class="o">&lt;</span><span class="n">Edge</span><span class="o">&gt;</span> <span class="n">kruskal</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// 그래프의 간선을 모두 추출한 뒤, 가중치를 기준으로 오름차순으로 정렬</span>
    <span class="n">vector</span><span class="o">&lt;</span><span class="n">Edge</span><span class="o">&gt;</span> <span class="n">edges</span><span class="p">;</span>
    <span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="n">parent</span><span class="p">(</span><span class="n">V</span><span class="p">);</span>
    <span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="n">rank</span><span class="p">(</span><span class="n">V</span><span class="p">);</span>

    <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">V</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 맨 처음 부모는 자기 자신으로 초기화</span>
        <span class="n">parent</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">i</span><span class="p">;</span>
        <span class="n">rank</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
        <span class="k">for</span> <span class="p">(</span><span class="k">auto</span><span class="o">&amp;</span> <span class="n">edge</span> <span class="o">:</span> <span class="n">graph</span><span class="p">[</span><span class="n">i</span><span class="p">])</span> <span class="p">{</span>
            <span class="n">edges</span><span class="p">.</span><span class="n">push_back</span><span class="p">({</span><span class="n">i</span><span class="p">,</span> <span class="n">edge</span><span class="p">.</span><span class="n">first</span><span class="p">,</span> <span class="n">edge</span><span class="p">.</span><span class="n">second</span><span class="p">});</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="c1">// 가중치를 기준으로 정렬</span>
    <span class="n">sort</span><span class="p">(</span><span class="n">edges</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">edges</span><span class="p">.</span><span class="n">end</span><span class="p">(),</span> <span class="p">[](</span><span class="n">Edge</span> <span class="n">x</span><span class="p">,</span> <span class="n">Edge</span> <span class="n">y</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">x</span><span class="p">.</span><span class="n">weight</span> <span class="o">&lt;</span> <span class="n">y</span><span class="p">.</span><span class="n">weight</span><span class="p">;</span>
    <span class="p">});</span>

    <span class="c1">// 간선을 하나씩 선택하며, 그루스칼 알고리즘 적용</span>
    <span class="n">vector</span><span class="o">&lt;</span><span class="n">Edge</span><span class="o">&gt;</span> <span class="n">result</span><span class="p">;</span>
    <span class="k">for</span> <span class="p">(</span><span class="k">auto</span><span class="o">&amp;</span> <span class="n">edge</span> <span class="o">:</span> <span class="n">edges</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">int</span> <span class="n">u</span> <span class="o">=</span> <span class="n">edge</span><span class="p">.</span><span class="n">src</span><span class="p">,</span> <span class="n">v</span> <span class="o">=</span> <span class="n">edge</span><span class="p">.</span><span class="n">dest</span><span class="p">,</span> <span class="n">weight</span> <span class="o">=</span> <span class="n">edge</span><span class="p">.</span><span class="n">weight</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">find</span><span class="p">(</span><span class="n">parent</span><span class="p">,</span> <span class="n">u</span><span class="p">)</span> <span class="o">!=</span> <span class="n">find</span><span class="p">(</span><span class="n">parent</span><span class="p">,</span> <span class="n">v</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">merge</span><span class="p">(</span><span class="n">parent</span><span class="p">,</span> <span class="n">rank</span><span class="p">,</span> <span class="n">u</span><span class="p">,</span> <span class="n">v</span><span class="p">);</span>
            <span class="n">result</span><span class="p">.</span><span class="n">push_back</span><span class="p">(</span><span class="n">edge</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">vector</span><span class="o">&lt;</span><span class="n">Edge</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">kruskal</span><span class="p">();</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>
]]></content:encoded>
        <pubDate>Wed, 12 Jul 2023 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/algorithm/2023/07/12/minimun-spanning-tree-kruskal/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/algorithm/2023/07/12/minimun-spanning-tree-kruskal/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Algorithm</category>
        
        <category>Graph</category>
        
        
        <category>algorithm</category>
        
      </item>
    
      <item>
        <title>Floyd Warshall</title>
        <description>플로이드-워셜 알고리즘이란
그래프에서 모든 노드 쌍 간의 최단 경로를 찾는 알고리즘
다익스트라와 다르게 음수 가중치를 가진 그래프에서도 동작한다. 모든 지점에서 다른 모든 지점까지의 최단 경로를 모두 구해야 하는 경우에 사용할 수 있다.
DP(Dynamic Programming)를 기반으로 동작한다.
점화식은 아래와 같다.
1
D_ab = min(D_ab, D_ak + D_kb)

A에서 B로 가는 최소 비용과 A에서 K를 거쳐 B로 가는 비용을 비교하여 더 작은 값으로 갱신
다익스트라 알고리즘에 비해서 구현이 쉽다.</description>
        <content:encoded><![CDATA[<h2 id="플로이드-워셜-알고리즘이란">플로이드-워셜 알고리즘이란</h2>
<p>그래프에서 모든 노드 쌍 간의 최단 경로를 찾는 알고리즘
다익스트라와 다르게 음수 가중치를 가진 그래프에서도 동작한다. 모든 지점에서 다른 모든 지점까지의 최단 경로를 모두 구해야 하는 경우에 사용할 수 있다.
<b>DP(Dynamic Programming)</b>를 기반으로 동작한다.
점화식은 아래와 같다.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>D_ab = min(D_ab, D_ak + D_kb)
</pre></td></tr></tbody></table></code></pre></div></div>
<p>A에서 B로 가는 최소 비용과 A에서 K를 거쳐 B로 가는 비용을 비교하여 더 작은 값으로 갱신
다익스트라 알고리즘에 비해서 구현이 쉽다.</p>

<hr />
<p>플로이드-워셜 구현
—
[ with <b><code class="language-plaintext highlighter-rouge">C++</code></b> ]</p>

<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
</pre></td><td class="rouge-code"><pre><span class="cp">#define INF 2134567890
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// 그래프 예시</span>
    <span class="n">vector</span><span class="o">&lt;</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;&gt;</span> <span class="n">graph</span> <span class="o">=</span> <span class="p">{</span>
        <span class="p">{</span><span class="mi">0</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="n">INF</span><span class="p">,</span> <span class="mi">10</span><span class="p">},</span>
        <span class="p">{</span><span class="n">INF</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="n">INF</span><span class="p">},</span>
        <span class="p">{</span><span class="n">INF</span><span class="p">,</span> <span class="n">INF</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">},</span>
        <span class="p">{</span><span class="n">INF</span><span class="p">,</span> <span class="n">INF</span><span class="p">,</span> <span class="n">INF</span><span class="p">,</span> <span class="mi">0</span><span class="p">},</span>
    <span class="p">};</span>

    <span class="c1">// 그래프의 인접 행렬을 dist로 복사</span>
    <span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="n">graph</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
    <span class="n">vector</span><span class="o">&lt;</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;&gt;</span><span class="n">dist</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="n">n</span><span class="p">));</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">dist</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">graph</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">j</span><span class="p">];</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="c1">// 플로이드 워셜 알고리즘</span>
    <span class="c1">// k : 중간 거치는 노드</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">k</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// (i j) vs (i k j)</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">dist</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">k</span><span class="p">]</span> <span class="o">!=</span> <span class="n">INF</span> <span class="o">&amp;&amp;</span> <span class="n">dist</span><span class="p">[</span><span class="n">k</span><span class="p">][</span><span class="n">j</span><span class="p">]</span> <span class="o">!=</span> <span class="n">INF</span> <span class="o">&amp;&amp;</span> <span class="n">dist</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">k</span><span class="p">]</span> <span class="o">+</span> <span class="n">dist</span><span class="p">[</span><span class="n">k</span><span class="p">][</span><span class="n">j</span><span class="p">]</span> <span class="o">&lt;</span> <span class="n">dist</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">j</span><span class="p">])</span> <span class="p">{</span>
                    <span class="n">dist</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">dist</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">k</span><span class="p">]</span> <span class="o">+</span> <span class="n">dist</span><span class="p">[</span><span class="n">k</span><span class="p">][</span><span class="n">j</span><span class="p">];</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>
]]></content:encoded>
        <pubDate>Tue, 11 Jul 2023 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/algorithm/2023/07/11/floyd-warshall/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/algorithm/2023/07/11/floyd-warshall/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Algorithm</category>
        
        <category>Graph</category>
        
        
        <category>algorithm</category>
        
      </item>
    
      <item>
        <title>Kotlin vs Java</title>
        <description>개발자들은 평생동안 코드를 쓰는 것 보다 읽는 것에 더 많은 시간을 할애한다. 코틀린은 가독성에 초점을 뒀다. 「이사코바」</description>
        <content:encoded><![CDATA[<blockquote>
  <p>개발자들은 평생동안 코드를 쓰는 것 보다 읽는 것에 더 많은 시간을 할애한다. 코틀린은 가독성에 초점을 뒀다. 「이사코바」</p>
</blockquote>

<p>많은 서비스의 Backend 시스템은 자바로 이루어져 있습니다. 몇몇 회사와 개발자들이 Backend에 코틀린을 사용하려는 모습을 보이고 있습니다. 코틀린은 자바와 완벽하게 호환이 되므로 대체될 수 있습니다. 또 구글에서 코틀린을 1 언어로 채택하고 있는 걸 보면 코틀린은 여러 면에서 유리합니다.</p>

<h2 id="why-we-should-learn-kotlin">Why we should learn kotlin?</h2>

<h3 id="정적-타입">정적 타입</h3>
<p>동적 타입 언어에서 발생할 수 있는 런타임 오류를 어느 정도 예방이 가능하다. <strong>NPE</strong>(NullPointException)을 컴파일 단계에서 예방할 수 있습니다.</p>

<h3 id="간결성">간결성</h3>
<p>자바에 비해서 코드가 간결하고 직관적입니다. 그렇기에 배우기 쉽습니다. (<del>물론 자바에 비해서..</del>) Jetbrain 에서 만든 언어이기 때문에 Intellij IDEA에 적용하기 좋습니다. 일례로 자바로 작성된 코드를 코틀린 파일에 붙여 넣으면 IDEA에서 자동으로 코틀린으로 변환해줍니다.</p>

<h2 id="kotlin-vs-java">Kotlin vs Java</h2>
<p><img src="https://kruschecompany.com/wp-content/uploads/2022/01/overview-2048x1603.png" alt="Kotlin vs Java 기능 비교" /></p>
<h3 id="변수-상수">변수 상수</h3>

<p>Java<br />
- 변수 : 그냥 선언<br />
- 상수 : <strong>final</strong>을 사용함</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nc">String</span> <span class="n">str</span> <span class="o">=</span> <span class="s">""</span><span class="o">;</span>
<span class="kd">final</span> <span class="nc">String</span> <span class="n">str</span> <span class="o">=</span> <span class="s">""</span><span class="o">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><br />
kotlin<br />
- 변수 : <strong>var</strong>(variable)로 선언<br />
- 상수 : <strong>val</strong>(value)로 선언</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="kd">var</span> <span class="py">str</span> <span class="p">=</span> <span class="s">""</span>
<span class="kd">val</span> <span class="py">str</span> <span class="p">=</span> <span class="s">""</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="객체-초기화">객체 초기화</h3>
<p>java<br />
- <strong>new</strong>로 객체를 초기화하고 초기화된 객체를 이용하여 초기 작업을 진행합니다.<br />
여러 객체를 생성해야 하는 상황이라면 불편해지고, 가독성이 떨어집니다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nc">Intent</span> <span class="n">testIntent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Intent</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="nc">SecondActivity</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="n">testIntent</span><span class="o">.</span><span class="na">putExtra</span><span class="o">(</span><span class="s">"ext1"</span><span class="o">,</span> <span class="mi">1</span><span class="o">);</span>
<span class="n">testIntent</span><span class="o">.</span><span class="na">putExtra</span><span class="o">(</span><span class="s">"ext2"</span><span class="o">,</span> <span class="mi">2</span><span class="o">);</span>
<span class="n">testIntent</span><span class="o">.</span><span class="na">putExtra</span><span class="o">(</span><span class="s">"ext3"</span><span class="o">,</span> <span class="s">"3"</span><span class="o">);</span>
<span class="n">testIntent</span><span class="o">.</span><span class="na">putExtra</span><span class="o">(</span><span class="s">"ext4"</span><span class="o">,</span> <span class="s">"4"</span><span class="o">);</span>
<span class="n">testIntent</span><span class="o">.</span><span class="na">putExtra</span><span class="o">(</span><span class="s">"ext5"</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>kotlin<br />
- <strong>apply block</strong>을 이용하여 초기화 작업을 수행합니다. 초기화된 객체 자신을 <code class="language-plaintext highlighter-rouge">this</code>로 칭하기 때문에 따로 선언하지 않고 초기화 작업을 진행해 줄 수 있습니다. 여러 객체를 생성하더라도 block으로 감싸서 진행하기 때문에 가독성이 좋고, 코드를 관리하기 수월합니다.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">testIntent</span> <span class="p">=</span> <span class="nc">Intent</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nc">SecondActivity</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="nf">putExtra</span><span class="p">(</span><span class="s">"ext1"</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
    <span class="nf">putExtra</span><span class="p">(</span><span class="s">"ext2"</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
    <span class="nf">putExtra</span><span class="p">(</span><span class="s">"ext3"</span><span class="p">,</span> <span class="s">"3"</span><span class="p">)</span>
    <span class="nf">putExtra</span><span class="p">(</span><span class="s">"ext4"</span><span class="p">,</span> <span class="s">"4"</span><span class="p">)</span>
    <span class="nf">putExtra</span><span class="p">(</span><span class="s">"ext5"</span><span class="p">,</span> <span class="k">false</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="lambda">Lambda</h3>
<p>kotlin<br />
자바에서는 람다를 사용할 때 매개변수를 사용했지만, 코틀린에서는 이를 생략하고 <code class="language-plaintext highlighter-rouge">it</code>이라는 암묵적 변수로 작성할 수 있습니다. (<del>예시가 좀 잘못됐다…</del>) 아래 2번 코드의 3줄 모두 같은 기능을 수행합니다.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="c1">// 1</span>
<span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">name</span> <span class="p">:</span> <span class="nc">String</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">user</span> <span class="p">=</span> <span class="nf">listOf</span> <span class="p">{</span>
        <span class="n">user1</span> <span class="p">=</span> <span class="nc">User</span><span class="p">(</span><span class="s">"A"</span><span class="p">),</span>
        <span class="n">user2</span> <span class="p">=</span> <span class="nc">User</span><span class="p">(</span><span class="s">"B"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="kd">val</span> <span class="py">selectUser</span> <span class="p">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">filter</span> <span class="p">{</span><span class="n">it</span><span class="p">.</span><span class="n">name</span> <span class="p">==</span> <span class="s">"A"</span><span class="p">}</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"Selected user is ${selectUser.joinToString(separator = "</span><span class="p">,</span> <span class="s">") { it.name }}"</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// 2</span>
<span class="n">people</span><span class="p">.</span><span class="nf">maxBy</span><span class="p">(</span><span class="nc">Person</span><span class="o">::</span><span class="n">age</span><span class="p">)</span>
<span class="n">people</span><span class="p">.</span><span class="nf">maxBy</span><span class="p">(</span><span class="n">p</span> <span class="p">-&gt;</span> <span class="n">p</span><span class="p">.</span><span class="n">age</span><span class="p">)</span>
<span class="n">people</span><span class="p">.</span><span class="nf">maxBy</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="n">age</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="npenullpointexception">NPE(NullPointException)</h3>
<p>java<br />
- 자바는 기본적으로 Nullable로 선언됩니다. <code class="language-plaintext highlighter-rouge">@Nullable</code>과 <code class="language-plaintext highlighter-rouge">@NonNull</code>을 사용하여 Null타입을 구분합니다.<br />
- 자바 변수에 Null이 들어오는 경우를 체크하고 NPE를 방지하고 싶다면 <code class="language-plaintext highlighter-rouge">if</code>를 사용합니다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nd">@Nullable</span> <span class="nc">String</span> <span class="n">strNullable</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
<span class="nd">@NonNull</span> <span class="nc">String</span> <span class="n">strNonNull</span> <span class="o">=</span> <span class="s">""</span><span class="o">;</span>

<span class="k">if</span> <span class="o">(</span><span class="n">strNullable</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">strNullable</span><span class="o">.</span><span class="na">split</span><span class="o">(</span><span class="s">"/"</span><span class="o">);</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>kotlin<br />
- <code class="language-plaintext highlighter-rouge">?</code>를 사용하여 null이 가능함을 구분할 수 있습니다.<br />
- 코틀린에서는 NPE를 방지하기 위해 <code class="language-plaintext highlighter-rouge">?</code>를 사용합니다. 만약 Null이라면 <code class="language-plaintext highlighter-rouge">split()</code>은 실행되지 않습니다.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="kd">var</span> <span class="py">strNullable</span><span class="p">:</span> <span class="nc">String</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span>
<span class="kd">var</span> <span class="py">strNonNull</span><span class="p">:</span> <span class="nc">String</span> <span class="p">=</span> <span class="s">""</span>

<span class="n">strNullable</span><span class="o">?.</span><span class="nf">split</span><span class="p">(</span><span class="s">"/"</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="생성자">생성자</h3>
<p>java<br />
- getter setter 등 Lombok의 <code class="language-plaintext highlighter-rouge">@</code>(Annotation)을 많이 선언해 주어야 하는 불편함이 있습니다.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="nd">@Getter</span>
<span class="nd">@Builder</span>
<span class="nd">@AllArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">User</span> <span class="o">{</span>
  <span class="nd">@NotNull</span><span class="o">(</span><span class="n">message</span> <span class="o">=</span> <span class="s">"name is required"</span><span class="o">)</span>
  <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>
  <span class="nd">@Nullable</span>
  <span class="kd">private</span> <span class="nc">String</span> <span class="n">lastName</span><span class="o">;</span>  
 
  <span class="kd">public</span> <span class="nf">User</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
    <span class="k">this</span><span class="o">.</span><span class="na">lastName</span> <span class="o">=</span> <span class="s">"NO LASTNAME"</span><span class="o">;</span>
  <span class="o">}</span>
<span class="o">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p>kotlin
- 코틀린은 @(Annotation)이 내장되어 있어 Lombok을 사용하지 않습니다. <code class="language-plaintext highlighter-rouge">var</code>를 통해 선언하는 것으로 DTO로 사용할 수 있습니다.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="kd">class</span> <span class="nc">User</span><span class="p">(</span>
    <span class="kd">var</span> <span class="py">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
    <span class="kd">var</span> <span class="py">lastName</span> <span class="p">:</span> <span class="nc">String</span><span class="p">?</span> <span class="p">=</span> <span class="s">"NO LASTNAME"</span><span class="p">,</span>
<span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>
<p>자바와 코틀린은 완벽 호환이 된다고는 하지만 많은 부분 코틀린이 더 간결한 방식으로 다릅니다. 위에서 다 다루지 못한 내용들이 많이 있지만 다음 글들을 통해서 확인하면 좋습니다.<br />
<a href="https://doodoo3804.github.io/2023/06/20/kotlin-%EA%B8%B0%EB%B3%B8/">「Kotlin 기본 문법」</a>
<!-- [「Kotlin 고급 문법」](https://doodoo3804.github.io/2023/06/20/kotlin-%EA%B8%B0%EB%B3%B8/) --></p>

<h2 id="참고">참고</h2>
<ol>
  <li>백엔드 개발자의 코틀린 입문기 - 코틀린이 얼마나 좋길래? 자바에서 옮겨가도 될까?<a href="https://seolin.tistory.com/146">https://seolin.tistory.com/146</a></li>
  <li>Kotlin vs Java: strengths, weaknesses and when to use which <a href="https://kruschecompany.com/kotlin-vs-java/">https://kruschecompany.com/kotlin-vs-java/</a></li>
  <li>[Kotlin] Kotlin vs. Java - 코틀린, 자바 차이점 비교 <a href="https://dev-imaec.tistory.com/36">https://dev-imaec.tistory.com/36</a></li>
  <li>Java vs Kotlin 비교 / 안드로이드 앱 개발 승자는??? <a href="https://mondayless.tistory.com/25">https://mondayless.tistory.com/25</a></li>
  <li>[kotlin vs java ] 코틀린과 자바의 차이, 코틀린의 장점 <a href="https://juhi.tistory.com/72">https://juhi.tistory.com/72</a></li>
  <li>Kotlin으로 프로젝트 하기 <a href="https://brunch.co.kr/@purpledev/5">https://brunch.co.kr/@purpledev/5</a></li>
</ol>
]]></content:encoded>
        <pubDate>Wed, 21 Jun 2023 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/kotlin/2023/06/21/kotlin-vs-Java/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/kotlin/2023/06/21/kotlin-vs-Java/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Kotlin</category>
        
        <category>Java</category>
        
        
        <category>kotlin</category>
        
      </item>
    
      <item>
        <title>Kotlin 기본 문법</title>
        <description>Kotlin 기본 문법
1. 함수
1
2
3
4
5
6
fun helloWorld() : Unit {
    println(&quot;Hello World&quot;)
}
fun add (a : Int, b : Int) : Int {
    return a + b
}

함수에서 return 값이 없는 경우면 Unit을 작성하지만 생략해도 가능하다.
return 값이 있다면 그 타입을 : 뒤에 작성한다.</description>
        <content:encoded><![CDATA[<h2 id="kotlin-기본-문법">Kotlin 기본 문법</h2>
<h3 id="1-함수">1. 함수</h3>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nf">helloWorld</span><span class="p">()</span> <span class="p">:</span> <span class="nc">Unit</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"Hello World"</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">add</span> <span class="p">(</span><span class="n">a</span> <span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="n">b</span> <span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">a</span> <span class="p">+</span> <span class="n">b</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p>함수에서 return 값이 없는 경우면 <code class="language-plaintext highlighter-rouge">Unit</code>을 작성하지만 생략해도 가능하다.<br />
return 값이 있다면 그 타입을 <code class="language-plaintext highlighter-rouge">:</code> 뒤에 작성한다.</p>

<h3 id="2-var-vs-val">2. var vs val</h3>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">a</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">10</span>
<span class="kd">var</span> <span class="py">b</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">9</span>
<span class="kd">var</span> <span class="py">c</span> <span class="p">=</span> <span class="mi">100</span>
<span class="kd">var</span> <span class="py">s</span> <span class="p">=</span> <span class="s">"String"</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">val</code>은 value로 상수를 의미, 변하지 않는 값이다. <code class="language-plaintext highlighter-rouge">var</code>는 variable로 변수를 의미 변하는 값이다.<br />
kotlin에서는 변수의 타입을 자동으로 추론하기 때문에 <code class="language-plaintext highlighter-rouge">c</code>에서 처럼 <code class="language-plaintext highlighter-rouge">: Int</code>를 적지 않아도 가능하다. 이는 Int 에서만 적용되는 것이 아니라 모두 적용된다.(s의 경우)</p>

<h3 id="3-string-template">3. String template</h3>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">name</span> <span class="p">=</span> <span class="s">"doodoo"</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"my name is ${name}"</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p>큰 따옴표(““)안에 변수를 넣으려면 <code class="language-plaintext highlighter-rouge">${변수 이름}</code> 이렇게 사용하면 된다.</p>

<h3 id="4-조건식">4. 조건식</h3>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nf">maxBy1</span> <span class="p">(</span><span class="n">a</span> <span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="n">b</span> <span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">a</span> <span class="p">&gt;</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">a</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">b</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">fun</span> <span class="nf">maxBy2</span> <span class="p">(</span><span class="n">a</span> <span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="n">b</span> <span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">a</span> <span class="p">&gt;</span> <span class="n">b</span><span class="p">)</span> <span class="n">a</span> <span class="k">else</span> <span class="n">b</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p>maxBy1 함수는 아래 maxBy2 처럼 간결하게 적을 수 있다.</p>

<h3 id="5-when">5. when</h3>
<p>Java에서의 <code class="language-plaintext highlighter-rouge">switch</code></p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">fun</span> <span class="nf">checkNum</span><span class="p">(</span><span class="n">score</span> <span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">when</span> <span class="p">(</span><span class="n">score</span><span class="p">)</span> <span class="p">{</span>
        <span class="mi">0</span> <span class="p">-&gt;</span> <span class="nf">println</span><span class="p">(</span><span class="s">"0"</span><span class="p">)</span>
        <span class="mi">1</span><span class="p">,</span><span class="mi">2</span> <span class="p">-&gt;</span> <span class="nf">println</span><span class="p">(</span><span class="s">"1 or 2"</span><span class="p">)</span>
        <span class="k">else</span> <span class="p">-&gt;</span> <span class="nf">println</span><span class="p">(</span><span class="s">"other scores"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="kd">var</span> <span class="py">b</span> <span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="k">when</span><span class="p">(</span><span class="n">score</span><span class="p">)</span> <span class="p">{</span>
        <span class="mi">0</span> <span class="p">-&gt;</span> <span class="mi">0</span>
        <span class="k">else</span> <span class="p">-&gt;</span> <span class="mi">1</span>
    <span class="p">}</span>
    <span class="k">when</span><span class="p">(</span><span class="n">score</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">in</span> <span class="mi">90</span><span class="o">..</span><span class="mi">100</span> <span class="p">-&gt;</span> <span class="nf">println</span><span class="p">(</span><span class="s">"great"</span><span class="p">)</span>
        <span class="k">in</span> <span class="mi">50</span><span class="o">..</span><span class="mi">80</span> <span class="p">-&gt;</span> <span class="nf">println</span><span class="p">(</span><span class="s">"not bad"</span><span class="p">)</span>
        <span class="k">else</span> <span class="p">-&gt;</span> <span class="nf">println</span><span class="p">(</span><span class="s">"try hard"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p>변수 할당에서 <code class="language-plaintext highlighter-rouge">when</code>을 사용할 수 있다. 이 경우에는 함수에서 사용한 것과는 다르게 <code class="language-plaintext highlighter-rouge">else</code>로 기본 값을 반드시 써줘야 한다.<br />
<code class="language-plaintext highlighter-rouge">when</code>에서 범위를 설정하려면 <code class="language-plaintext highlighter-rouge">in 90..100</code> 처럼 사용하면 된다.</p>

<h3 id="6-expression-vs-statement">6. Expression vs Statement</h3>
<p><strong>Expression</strong>은 변수를 어떤 값을 할당하거나 변환하는 것, 즉 값을 <strong>만들어</strong> 내는 것이다. kotlin에서 모든 함수는 Expression이다.<br />
<strong>Statement</strong>는 값을 만들지 않고 문장을 실행하는 것(?)</p>

<h3 id="7-array-vs-list">7. Array vs List</h3>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">array</span> <span class="p">=</span> <span class="nf">arrayOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">list</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>

<span class="kd">val</span> <span class="py">array2</span> <span class="p">=</span> <span class="nf">arrayOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s">"s"</span><span class="p">,</span> <span class="mf">3.14f</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">list2</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s">"s"</span><span class="p">,</span> <span class="mf">3.14f</span><span class="p">)</span>

<span class="kd">val</span> <span class="py">arrayList</span> <span class="p">=</span> <span class="n">arrayListOf</span><span class="p">&lt;</span><span class="nc">Int</span><span class="p">&gt;()</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p><strong>list</strong>는 interface로 get만 존재하므로 <code class="language-plaintext highlighter-rouge">list[0] = 3</code>와 같이 값을 변경하는 것은 불가능하다. 반면 <strong>array</strong>는 가능<br />
<strong>arrayList</strong>는 add remove 등으로 변경이 가능하다.</p>

<h3 id="8-for-vs-while">8. for vs while</h3>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">10</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..</span><span class="mi">10</span> <span class="n">step</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="k">in</span> <span class="mi">10</span> <span class="n">downTo</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">val</span> <span class="py">students</span> <span class="p">=</span> <span class="nf">arrayListOf</span><span class="p">(</span><span class="s">"a"</span><span class="p">,</span> <span class="s">"b"</span><span class="p">,</span> <span class="s">"c"</span><span class="p">)</span>
<span class="k">for</span> <span class="p">((</span><span class="n">index</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span> <span class="k">in</span> <span class="n">students</span><span class="p">.</span><span class="nf">withIndex</span><span class="p">())</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"$index $name"</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">var</span> <span class="py">index</span> <span class="p">=</span> <span class="mi">0</span>
<span class="k">while</span><span class="p">(</span><span class="n">index</span> <span class="p">&lt;</span> <span class="mi">5</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="n">index</span><span class="p">)</span>
    <span class="n">index</span> <span class="p">++</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p>첫 번째 for문은 1부터 10까지<br />
두 번째 for문은 2간격으로 1부터 10까지<br />
세 번째 for문은 10부터 1까지<br />
네 번째 for문은 <code class="language-plaintext highlighter-rouge">withIndex()</code>를 사용하여 index와 value를 둘 다 가져올 수 있다. <em>(python 에서의 <code class="language-plaintext highlighter-rouge">enumerate</code>와 동일)</em></p>

<h3 id="9-nonnull-과-nullable">9. NonNull 과 Nullable</h3>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="kd">var</span> <span class="py">name</span> <span class="p">:</span> <span class="nc">String</span> <span class="p">=</span> <span class="s">"doodoo"</span>
<span class="kd">var</span> <span class="py">nullName</span> <span class="p">:</span> <span class="nc">String</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span>

<span class="kd">var</span> <span class="py">nullNameInUpperCase</span> <span class="p">=</span> <span class="n">nullName</span><span class="o">?.</span><span class="nf">toUpperCase</span><span class="p">()</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p>String은 NonNull 타입인데 <code class="language-plaintext highlighter-rouge">?</code>를 사용하면 null을 할당 / null이 될 수 있다.<br />
초기 변수 할당이 아닌 때에도 nullable 타입을 할당하려고 하면 <code class="language-plaintext highlighter-rouge">?</code>를 사용하면 된다.
<br /></p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="kd">val</span> <span class="py">lastName</span> <span class="p">:</span> <span class="nc">String</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span>
<span class="kd">val</span> <span class="py">fullName</span> <span class="p">:</span> <span class="n">name</span> <span class="p">+</span> <span class="s">" "</span> <span class="p">+</span> <span class="p">(</span><span class="n">lastName</span> <span class="o">?:</span> <span class="s">"No LastName"</span><span class="p">)</span>

<span class="kd">val</span> <span class="py">email</span> <span class="p">:</span> <span class="nc">String</span> <span class="p">?=</span> <span class="s">"doodoo3804@gmail.com"</span>
<span class="n">email</span><span class="o">?.</span><span class="nf">let</span> <span class="p">{</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"my email is ${email}"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">?:</code> elvis expression<br />
null인 경우의 default 값을 적어준다.<br />
보통 <code class="language-plaintext highlighter-rouge">?.let</code>을 사용하여 null이 아닌 경우의 함수를 짠다.
<code class="language-plaintext highlighter-rouge">!!</code> null이 절대로 될 수 없는 경우에 사용</p>

<h3 id="10-class">10. class</h3>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="k">open</span> <span class="kd">class</span> <span class="nc">Human</span> <span class="k">constructor</span><span class="p">(</span><span class="kd">val</span> <span class="py">name</span> <span class="p">:</span> <span class="nc">String</span> <span class="p">=</span> <span class="s">"default name"</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">constructor</span><span class="p">(</span><span class="n">name</span> <span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">age</span> <span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">:</span> <span class="k">this</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">age</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nf">init</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"walk"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">open</span> <span class="k">fun</span> <span class="nf">eating</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"eat"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nc">NotHuman</span> <span class="p">:</span> <span class="nc">Human</span><span class="p">(){</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">eating</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">super</span><span class="p">.</span><span class="nf">eating</span><span class="p">()</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"not eating"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">human1</span> <span class="p">=</span> <span class="nc">Human</span><span class="p">(</span><span class="s">"doodoo"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">human2</span> <span class="p">=</span> <span class="nc">Human</span><span class="p">(</span><span class="s">"doodoos"</span><span class="p">,</span> <span class="mi">99</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">notHuman</span> <span class="p">=</span> <span class="nc">NotHuman</span><span class="p">()</span>
<span class="p">}</span>

</pre></td></tr></tbody></table></code></pre></div></div>
<p>kotlin의 class는 기본적으로 <strong>final class</strong>이다. 그렇기 때문에 같은 파일 내에 있더라도 override하여 사용할 수 없다. 상속하여 사용하기 위해서는 <code class="language-plaintext highlighter-rouge">open</code> 예약어를 사용한다. class내부의 <code class="language-plaintext highlighter-rouge">fun</code>도 같은 문제가 있어 <code class="language-plaintext highlighter-rouge">open</code>을 사용한다. 상속받은 parameter 역시 사용 가능하다.<br />
<br />
<strong>주생성자</strong> :  name 이라는 String을 할당하기 위해 사용 class 이름 옆에 constructor를 작성한다. 이는 생략 가능<br />
<strong>부생성자</strong> : class 내부에 constructor를 선언, 부생성자는 주생성자에게서 <code class="language-plaintext highlighter-rouge">this</code>를 통해 값을 받아와야만 한다.<br />
<strong>init</strong> : 생성되었을 때 자동으로 실행하는 부분<br />
<strong>super</strong> : 만약 부모로 부터 상속 받아 사용해야 하는 경우 사용한다.<br />
<strong>open</strong> : 상속을 받기 위한 부분에 사용하는 예약어</p>
]]></content:encoded>
        <pubDate>Tue, 20 Jun 2023 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/kotlin/2023/06/20/kotlin-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/kotlin/2023/06/20/kotlin-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Kotlin</category>
        
        
        <category>kotlin</category>
        
      </item>
    
      <item>
        <title>Segment tree</title>
        <description>세그먼트 트리란
주어진 쿼리에 대해 빠르게 응답하기 위해 만들어진 자료구조이다.
따라서 많은 쿼리가 반복되는 상황에 유리하다.</description>
        <content:encoded><![CDATA[<h2 id="세그먼트-트리란">세그먼트 트리란</h2>
<p>주어진 쿼리에 대해 빠르게 응답하기 위해 만들어진 자료구조이다.
<br />따라서 많은 쿼리가 반복되는 상황에 유리하다.
<br /></p>

<h3 id="세그먼트-트리의-전체-크기">세그먼트 트리의 전체 크기</h3>
<p>크기가 N인 배열에 대해</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>트리의 높이 - ceil(log2(N))
세그먼트 트리의 크기 - 1 &lt;&lt; (트리의 높이 + 1)
</pre></td></tr></tbody></table></code></pre></div></div>
<p><br /></p>
<h3 id="세그먼트-트리생성">세그먼트 트리생성</h3>
<p>세그먼트 트리는 full binary tree에 가깝기에 배열에 모든 값들이 꽉차서 올 가능성이 매우 높다.
<br />포인터보다는 배열을 사용하여 작성한다.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>         1
       ⁄   ∖
     2       3
    ⁄  ∖    ⁄  ∖
  4     5  6    7
</pre></td></tr></tbody></table></code></pre></div></div>
<p>루트 노드 = 1로 생각한다.
<br />이때 루트 노드의 왼쪽은 2번, 오른쪽은 3번이 된다.
<br />2번 노드의 왼쪽은 4번, 오른쪽은 5번이 된다.
<br />3번 노드의 왼쪽은 6번, 오른쪽은 7번이 된다…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>|현재 노드가 node라면|
노드의 왼쪽 자식 배열 번호 : node * 2
노드의 오른쪽 자식 배열 번호 : node * 2 + 1
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="세그먼트-트리-구현">세그먼트 트리 구현</h2>
<p>[ with <b><code class="language-plaintext highlighter-rouge">C++</code></b> ]
<br />아래 코드에서 tree 배열은 세그먼트 트리가 만들어지는 배열
<br />arr 배열은 처음에 입력받아 생성된 배열을 의미한다.</p>
<h3 id="1-초기화-과정-init"><b>1. 초기화 과정 (init)</b></h3>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="kt">long</span> <span class="kt">long</span> <span class="nf">init</span><span class="p">(</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">long</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">arr</span><span class="p">,</span> <span class="n">vector</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">long</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">tree</span><span class="p">,</span> <span class="kt">int</span> <span class="n">node</span><span class="p">,</span> <span class="kt">int</span> <span class="n">start</span><span class="p">,</span> <span class="kt">int</span> <span class="n">end</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">start</span> <span class="o">==</span> <span class="n">end</span><span class="p">)</span> <span class="k">return</span> <span class="n">tree</span><span class="p">[</span><span class="n">node</span><span class="p">]</span> <span class="o">=</span> <span class="n">arr</span><span class="p">[</span><span class="n">start</span><span class="p">];</span>
    <span class="kt">int</span> <span class="n">mid</span> <span class="o">=</span> <span class="p">(</span><span class="n">end</span> <span class="o">+</span> <span class="n">start</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">tree</span><span class="p">[</span><span class="n">node</span><span class="p">]</span> <span class="o">=</span> <span class="n">init</span><span class="p">(</span><span class="n">arr</span><span class="p">,</span> <span class="n">tree</span><span class="p">,</span> <span class="n">node</span> <span class="o">*</span> <span class="mi">2</span><span class="p">,</span> <span class="n">start</span><span class="p">,</span> <span class="n">mid</span><span class="p">)</span> <span class="o">+</span> <span class="n">init</span><span class="p">(</span><span class="n">arr</span><span class="p">,</span> <span class="n">tree</span><span class="p">,</span> <span class="n">node</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">mid</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">end</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="2-갱신-과정-update"><b>2. 갱신 과정 (update)</b></h3>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="kt">void</span> <span class="nf">update</span><span class="p">(</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">long</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">tree</span><span class="p">,</span> <span class="kt">int</span> <span class="n">node</span><span class="p">,</span> <span class="kt">int</span> <span class="n">start</span><span class="p">,</span> <span class="kt">int</span> <span class="n">end</span><span class="p">,</span> <span class="kt">int</span> <span class="n">index</span><span class="p">,</span> <span class="kt">long</span> <span class="kt">long</span> <span class="n">diff</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">start</span> <span class="o">&lt;=</span> <span class="n">index</span> <span class="o">&amp;&amp;</span> <span class="n">index</span> <span class="o">&lt;=</span> <span class="n">end</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
    <span class="n">tree</span><span class="p">[</span><span class="n">node</span><span class="p">]</span> <span class="o">+=</span> <span class="n">diff</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">start</span> <span class="o">!=</span> <span class="n">end</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">int</span> <span class="n">mid</span> <span class="o">=</span> <span class="p">(</span><span class="n">start</span> <span class="o">+</span> <span class="n">end</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
        <span class="n">update</span><span class="p">(</span><span class="n">tree</span><span class="p">,</span> <span class="n">node</span> <span class="o">*</span> <span class="mi">2</span><span class="p">,</span> <span class="n">start</span><span class="p">,</span> <span class="n">mid</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="n">diff</span><span class="p">);</span>
        <span class="n">update</span><span class="p">(</span><span class="n">tree</span><span class="p">,</span> <span class="n">node</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">mid</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">end</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="n">diff</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="3-합-과정-sum"><b>3. 합 과정 (sum)</b></h3>
<p>이 부분은 <code class="language-plaintext highlighter-rouge">쿼리</code>에 따라 달라질 수 있다.</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="kt">long</span> <span class="kt">long</span> <span class="nf">sum</span><span class="p">(</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">long</span> <span class="kt">long</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">tree</span><span class="p">,</span> <span class="kt">int</span> <span class="n">node</span><span class="p">,</span> <span class="kt">int</span> <span class="n">start</span><span class="p">,</span> <span class="kt">int</span> <span class="n">end</span><span class="p">,</span> <span class="kt">int</span> <span class="n">left</span><span class="p">,</span> <span class="kt">int</span> <span class="n">right</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">left</span> <span class="o">&gt;</span> <span class="n">end</span> <span class="o">||</span> <span class="n">right</span> <span class="o">&lt;</span> <span class="n">start</span><span class="p">)</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">left</span> <span class="o">&lt;=</span> <span class="n">start</span> <span class="o">&amp;&amp;</span> <span class="n">end</span> <span class="o">&lt;=</span> <span class="n">right</span><span class="p">)</span> <span class="k">return</span> <span class="n">tree</span><span class="p">[</span><span class="n">node</span><span class="p">];</span>
    <span class="kt">int</span> <span class="n">mid</span> <span class="o">=</span> <span class="p">(</span><span class="n">start</span> <span class="o">+</span> <span class="n">end</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">sum</span><span class="p">(</span><span class="n">tree</span><span class="p">,</span> <span class="n">node</span> <span class="o">*</span> <span class="mi">2</span><span class="p">,</span> <span class="n">start</span><span class="p">,</span> <span class="n">mid</span><span class="p">,</span> <span class="n">left</span><span class="p">,</span> <span class="n">right</span><span class="p">)</span> <span class="o">+</span> <span class="n">sum</span><span class="p">(</span><span class="n">tree</span><span class="p">,</span> <span class="n">node</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">mid</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">left</span><span class="p">,</span> <span class="n">right</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="관련-포스트">관련 포스트</h2>

<ul>
  <li><a href="/algorithm/2026/04/01/trie/">Trie 자료구조 — 문자열 검색에 최적화된 트리 구조</a></li>
</ul>
]]></content:encoded>
        <pubDate>Mon, 19 Jun 2023 00:00:00 +0900</pubDate>
        <link>https://doodoo3804.github.io/algorithm/2023/06/19/Segment-tree/</link>
        <guid isPermaLink="true">https://doodoo3804.github.io/algorithm/2023/06/19/Segment-tree/</guid>
        <author>doodoo3804@gmail.com (DoYoon Kim)</author>
        
        <category>Algorithm</category>
        
        <category>Tree</category>
        
        
        <category>algorithm</category>
        
      </item>
    
  </channel>
</rss>
