<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://skysoo1111.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://skysoo1111.github.io/" rel="alternate" type="text/html" /><updated>2026-03-27T06:47:36+00:00</updated><id>https://skysoo1111.github.io/feed.xml</id><title type="html">soo’s blog</title><subtitle>Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.</subtitle><entry><title type="html">디자인 패턴 정리 - Spring 환경에서의 Singleton, Strategy, Factory 외 주요 패턴 실전 가이드</title><link href="https://skysoo1111.github.io/java/2026/03/27/design-patterns-in-spring-singleton-strategy-factory.html" rel="alternate" type="text/html" title="디자인 패턴 정리 - Spring 환경에서의 Singleton, Strategy, Factory 외 주요 패턴 실전 가이드" /><published>2026-03-27T01:00:00+00:00</published><updated>2026-03-27T01:00:00+00:00</updated><id>https://skysoo1111.github.io/java/2026/03/27/design-patterns-in-spring-singleton-strategy-factory</id><content type="html" xml:base="https://skysoo1111.github.io/java/2026/03/27/design-patterns-in-spring-singleton-strategy-factory.html"><![CDATA[<h2 id="디자인-패턴이란">디자인 패턴이란?</h2>

<p>디자인 패턴(Design Pattern)은 소프트웨어 설계에서 반복적으로 등장하는 문제에 대한 검증된 해결책이다. 1994년 Erich Gamma 외 3인(Gang of Four, GoF)이 저서 <em>“Design Patterns: Elements of Reusable Object-Oriented Software”</em>에서 23가지 패턴을 정리했다.</p>

<p>GoF 패턴은 세 가지로 분류된다.</p>

<table>
  <thead>
    <tr>
      <th>분류</th>
      <th>설명</th>
      <th>대표 패턴</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>생성(Creational)</strong></td>
      <td>객체 생성 방식을 추상화</td>
      <td>Singleton, Factory Method, Abstract Factory, Builder, Prototype</td>
    </tr>
    <tr>
      <td><strong>구조(Structural)</strong></td>
      <td>클래스/객체의 구성 방식</td>
      <td>Proxy, Decorator, Adapter, Facade, Composite</td>
    </tr>
    <tr>
      <td><strong>행동(Behavioral)</strong></td>
      <td>객체 간 책임 분배와 알고리즘</td>
      <td>Strategy, Observer, Template Method, Command, Chain of Responsibility</td>
    </tr>
  </tbody>
</table>

<p>Spring Framework 자체가 GoF 패턴의 집약체다. Spring을 제대로 이해하려면 이 패턴들을 반드시 알아야 한다.</p>

<hr />

<h2 id="1-싱글톤-패턴-singleton">1. 싱글톤 패턴 (Singleton)</h2>

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

<p>하나의 클래스에 인스턴스가 단 하나만 존재하도록 보장하는 패턴이다.</p>

<p><strong>전통적인 싱글톤 구현 (권장하지 않음):</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">DatabaseConnection</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="nc">DatabaseConnection</span> <span class="n">instance</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nf">DatabaseConnection</span><span class="o">()</span> <span class="o">{}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">synchronized</span> <span class="nc">DatabaseConnection</span> <span class="nf">getInstance</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">instance</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">instance</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DatabaseConnection</span><span class="o">();</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">instance</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>이 방식은 멀티스레드 환경에서 이중 검사(double-checked locking)나 <code class="language-plaintext highlighter-rouge">synchronized</code> 처리 없이는 안전하지 않다.</p>

<h3 id="spring-bean과-싱글톤">Spring Bean과 싱글톤</h3>

<p>Spring의 IoC 컨테이너는 기본적으로 모든 Bean을 <strong>싱글톤 스코프</strong>로 관리한다. 개발자가 직접 싱글톤 패턴을 구현할 필요가 없다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="c1">// @Scope("singleton") 은 기본값이므로 생략 가능</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserService</span> <span class="o">{</span>
    <span class="c1">// Spring ApplicationContext 당 하나의 인스턴스만 생성됨</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Spring이 지원하는 Bean 스코프:</p>

<table>
  <thead>
    <tr>
      <th>스코프</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">singleton</code></td>
      <td>ApplicationContext 당 1개 (기본값)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">prototype</code></td>
      <td>요청할 때마다 새 인스턴스 생성</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">request</code></td>
      <td>HTTP 요청 당 1개</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">session</code></td>
      <td>HTTP 세션 당 1개</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">application</code></td>
      <td>ServletContext 당 1개</td>
    </tr>
  </tbody>
</table>

<h3 id="싱글톤-빈의-스레드-안전성-주의점">싱글톤 빈의 스레드 안전성 주의점</h3>

<p>Spring 싱글톤 Bean은 <strong>스레드 안전을 보장하지 않는다</strong>. 여러 요청이 동시에 같은 Bean 인스턴스를 사용하기 때문에 가변 상태(mutable state)가 있으면 경쟁 조건(race condition)이 발생한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 잘못된 예 - 공유 가변 상태</span>
<span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CounterService</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="c1">// 여러 스레드가 동시에 접근 -&gt; 문제</span>

    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">increment</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="o">++</span><span class="n">count</span><span class="o">;</span> <span class="c1">// race condition!</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// 올바른 예 - 무상태(stateless) 유지</span>
<span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CalculatorService</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">add</span><span class="o">(</span><span class="kt">int</span> <span class="n">a</span><span class="o">,</span> <span class="kt">int</span> <span class="n">b</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span><span class="o">;</span> <span class="c1">// 로컬 변수만 사용 -&gt; 스레드 안전</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// 불가피하게 상태가 필요한 경우</span>
<span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">StatefulCounterService</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AtomicInteger</span> <span class="n">count</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AtomicInteger</span><span class="o">(</span><span class="mi">0</span><span class="o">);</span> <span class="c1">// 스레드 안전</span>

    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">increment</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">count</span><span class="o">.</span><span class="na">incrementAndGet</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>모범 사례:</strong> Spring Bean은 가능하면 <strong>무상태(stateless)</strong>로 설계한다.</p>

<hr />

<h2 id="2-전략-패턴-strategy">2. 전략 패턴 (Strategy)</h2>

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

<p>알고리즘 군을 정의하고 각각을 캡슐화하여 교환 가능하게 만드는 패턴이다. 클라이언트와 알고리즘을 분리하여 런타임에 알고리즘을 교체할 수 있다.</p>

<h3 id="spring-di--인터페이스로-구현">Spring DI + 인터페이스로 구현</h3>

<p>Spring의 의존성 주입(DI)은 전략 패턴을 매우 자연스럽게 구현할 수 있게 해준다.</p>

<p><strong>결제 수단 선택 예시:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 전략 인터페이스</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">PaymentStrategy</span> <span class="o">{</span>
    <span class="kt">void</span> <span class="nf">pay</span><span class="o">(</span><span class="kt">int</span> <span class="n">amount</span><span class="o">);</span>
    <span class="nc">String</span> <span class="nf">getType</span><span class="o">();</span>
<span class="o">}</span>

<span class="c1">// 신용카드 전략</span>
<span class="nd">@Component</span><span class="o">(</span><span class="s">"creditCard"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CreditCardPayment</span> <span class="kd">implements</span> <span class="nc">PaymentStrategy</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">pay</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="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="s">"신용카드로 "</span> <span class="o">+</span> <span class="n">amount</span> <span class="o">+</span> <span class="s">"원 결제"</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">getType</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="s">"creditCard"</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// PayPal 전략</span>
<span class="nd">@Component</span><span class="o">(</span><span class="s">"paypal"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">PayPalPayment</span> <span class="kd">implements</span> <span class="nc">PaymentStrategy</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">pay</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="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="s">"PayPal로 "</span> <span class="o">+</span> <span class="n">amount</span> <span class="o">+</span> <span class="s">"원 결제"</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">getType</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="s">"paypal"</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// 카카오페이 전략</span>
<span class="nd">@Component</span><span class="o">(</span><span class="s">"kakaoPay"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">KakaoPayPayment</span> <span class="kd">implements</span> <span class="nc">PaymentStrategy</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">pay</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="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="s">"카카오페이로 "</span> <span class="o">+</span> <span class="n">amount</span> <span class="o">+</span> <span class="s">"원 결제"</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">getType</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="s">"kakaoPay"</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Context - 전략을 사용하는 서비스</span>
<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">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">PaymentStrategy</span><span class="o">&gt;</span> <span class="n">paymentStrategies</span><span class="o">;</span>

    <span class="c1">// Spring이 PaymentStrategy 구현체들을 Map으로 자동 주입</span>
    <span class="c1">// key: Bean 이름, value: Bean 인스턴스</span>
    <span class="kd">public</span> <span class="nf">OrderService</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">PaymentStrategy</span><span class="o">&gt;</span> <span class="n">paymentStrategies</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">paymentStrategies</span> <span class="o">=</span> <span class="n">paymentStrategies</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">processPayment</span><span class="o">(</span><span class="nc">String</span> <span class="n">paymentMethod</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="nc">PaymentStrategy</span> <span class="n">strategy</span> <span class="o">=</span> <span class="n">paymentStrategies</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">paymentMethod</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">strategy</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"지원하지 않는 결제 수단: "</span> <span class="o">+</span> <span class="n">paymentMethod</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="n">strategy</span><span class="o">.</span><span class="na">pay</span><span class="o">(</span><span class="n">amount</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>새로운 결제 수단을 추가할 때 <code class="language-plaintext highlighter-rouge">OrderService</code>를 수정하지 않고 새 <code class="language-plaintext highlighter-rouge">@Component</code>만 추가하면 된다. <strong>OCP(개방-폐쇄 원칙)</strong>를 자연스럽게 따른다.</p>

<p><strong>Kotlin 버전:</strong></p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">PaymentStrategy</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">pay</span><span class="p">(</span><span class="n">amount</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span>
<span class="p">}</span>

<span class="nd">@Component</span><span class="p">(</span><span class="s">"toss"</span><span class="p">)</span>
<span class="kd">class</span> <span class="nc">TossPayment</span> <span class="p">:</span> <span class="nc">PaymentStrategy</span> <span class="p">{</span>
    <span class="k">override</span> <span class="k">fun</span> <span class="nf">pay</span><span class="p">(</span><span class="n">amount</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">=</span> <span class="nf">println</span><span class="p">(</span><span class="s">"토스로 ${amount}원 결제"</span><span class="p">)</span>
<span class="p">}</span>

<span class="nd">@Service</span>
<span class="kd">class</span> <span class="nc">OrderService</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">paymentStrategies</span><span class="p">:</span> <span class="nc">Map</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">PaymentStrategy</span><span class="p">&gt;</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">processPayment</span><span class="p">(</span><span class="n">method</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">paymentStrategies</span><span class="p">[</span><span class="n">method</span><span class="p">]</span><span class="o">?.</span><span class="nf">pay</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span>
            <span class="o">?:</span> <span class="k">throw</span> <span class="nc">IllegalArgumentException</span><span class="p">(</span><span class="s">"지원하지 않는 결제 수단: $method"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="3-팩토리-패턴-factory-method--abstract-factory">3. 팩토리 패턴 (Factory Method / Abstract Factory)</h2>

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

<ul>
  <li><strong>Factory Method</strong>: 객체 생성 인터페이스를 정의하고 서브클래스가 어떤 클래스를 인스턴스화할지 결정</li>
  <li><strong>Abstract Factory</strong>: 관련 객체들의 집합을 생성하는 인터페이스 제공</li>
</ul>

<h3 id="spring의-beanfactory와-factorybean">Spring의 BeanFactory와 FactoryBean</h3>

<p>Spring의 <code class="language-plaintext highlighter-rouge">BeanFactory</code>는 팩토리 패턴의 대표적인 구현체다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// @Bean 메서드 = Factory Method 패턴</span>
<span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DataSourceConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">DataSource</span> <span class="nf">dataSource</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">DataSourceBuilder</span><span class="o">.</span><span class="na">create</span><span class="o">()</span>
            <span class="o">.</span><span class="na">driverClassName</span><span class="o">(</span><span class="s">"com.mysql.cj.jdbc.Driver"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">url</span><span class="o">(</span><span class="s">"jdbc:mysql://localhost:3306/mydb"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">username</span><span class="o">(</span><span class="s">"user"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">password</span><span class="o">(</span><span class="s">"password"</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>
</code></pre></div></div>

<p><strong>FactoryBean 인터페이스로 복잡한 객체 생성:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">RedisConnectionFactoryBean</span> <span class="kd">implements</span> <span class="nc">FactoryBean</span><span class="o">&lt;</span><span class="nc">RedisConnectionFactory</span><span class="o">&gt;</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">host</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">port</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">RedisConnectionFactoryBean</span><span class="o">(</span><span class="nc">String</span> <span class="n">host</span><span class="o">,</span> <span class="kt">int</span> <span class="n">port</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">host</span> <span class="o">=</span> <span class="n">host</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">port</span> <span class="o">=</span> <span class="n">port</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">RedisConnectionFactory</span> <span class="nf">getObject</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">LettuceConnectionFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LettuceConnectionFactory</span><span class="o">(</span><span class="n">host</span><span class="o">,</span> <span class="n">port</span><span class="o">);</span>
        <span class="n">factory</span><span class="o">.</span><span class="na">afterPropertiesSet</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="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">getObjectType</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">RedisConnectionFactory</span><span class="o">.</span><span class="na">class</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>조건부 Bean 생성 - Abstract Factory 응용:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">NotificationService</span> <span class="o">{</span>
    <span class="kt">void</span> <span class="nf">send</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">,</span> <span class="nc">String</span> <span class="n">recipient</span><span class="o">);</span>
<span class="o">}</span>

<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmailNotificationService</span> <span class="kd">implements</span> <span class="nc">NotificationService</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">send</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">,</span> <span class="nc">String</span> <span class="n">recipient</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 이메일 발송 로직</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SmsNotificationService</span> <span class="kd">implements</span> <span class="nc">NotificationService</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">send</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">,</span> <span class="nc">String</span> <span class="n">recipient</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// SMS 발송 로직</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">NotificationConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="nd">@ConditionalOnProperty</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"notification.channel"</span><span class="o">,</span> <span class="n">havingValue</span> <span class="o">=</span> <span class="s">"email"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">NotificationService</span> <span class="nf">emailNotificationService</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">EmailNotificationService</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="nd">@ConditionalOnProperty</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"notification.channel"</span><span class="o">,</span> <span class="n">havingValue</span> <span class="o">=</span> <span class="s">"sms"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">NotificationService</span> <span class="nf">smsNotificationService</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">SmsNotificationService</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="4-템플릿-메서드-패턴-template-method">4. 템플릿 메서드 패턴 (Template Method)</h2>

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

<p>알고리즘의 뼈대(skeleton)를 상위 클래스에서 정의하고, 세부 구현을 하위 클래스에 위임하는 패턴이다. 공통 흐름은 상위 클래스에서 처리하고, 변하는 부분만 오버라이드한다.</p>

<h3 id="spring의-xxxtemplate-클래스들">Spring의 xxxTemplate 클래스들</h3>

<p>Spring의 <code class="language-plaintext highlighter-rouge">JdbcTemplate</code>, <code class="language-plaintext highlighter-rouge">RestTemplate</code>, <code class="language-plaintext highlighter-rouge">TransactionTemplate</code>은 모두 템플릿 메서드 패턴을 활용한다.</p>

<p><strong>JdbcTemplate:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Repository</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserRepository</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">JdbcTemplate</span> <span class="n">jdbcTemplate</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">UserRepository</span><span class="o">(</span><span class="nc">JdbcTemplate</span> <span class="n">jdbcTemplate</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">jdbcTemplate</span> <span class="o">=</span> <span class="n">jdbcTemplate</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="nf">findAll</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// JdbcTemplate이 Connection 획득, PreparedStatement 생성,</span>
        <span class="c1">// 예외 처리, 리소스 반환 등 보일러플레이트를 처리</span>
        <span class="c1">// 개발자는 SQL과 RowMapper만 제공</span>
        <span class="k">return</span> <span class="n">jdbcTemplate</span><span class="o">.</span><span class="na">query</span><span class="o">(</span>
            <span class="s">"SELECT id, name, email FROM users"</span><span class="o">,</span>
            <span class="o">(</span><span class="n">rs</span><span class="o">,</span> <span class="n">rowNum</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span>
                <span class="n">rs</span><span class="o">.</span><span class="na">getLong</span><span class="o">(</span><span class="s">"id"</span><span class="o">),</span>
                <span class="n">rs</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="s">"name"</span><span class="o">),</span>
                <span class="n">rs</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="s">"email"</span><span class="o">)</span>
            <span class="o">)</span>
        <span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">Optional</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</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">jdbcTemplate</span><span class="o">.</span><span class="na">query</span><span class="o">(</span>
            <span class="s">"SELECT id, name, email FROM users WHERE id = ?"</span><span class="o">,</span>
            <span class="o">(</span><span class="n">rs</span><span class="o">,</span> <span class="n">rowNum</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="n">rs</span><span class="o">.</span><span class="na">getLong</span><span class="o">(</span><span class="s">"id"</span><span class="o">),</span> <span class="n">rs</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="s">"name"</span><span class="o">),</span> <span class="n">rs</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="s">"email"</span><span class="o">)),</span>
            <span class="n">id</span>
        <span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">findFirst</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>직접 구현해보는 템플릿 메서드:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 추상 템플릿 클래스</span>
<span class="kd">public</span> <span class="kd">abstract</span> <span class="kd">class</span> <span class="nc">DataExportTemplate</span> <span class="o">{</span>

    <span class="c1">// 템플릿 메서드 - 알고리즘 골격</span>
    <span class="kd">public</span> <span class="kd">final</span> <span class="kt">void</span> <span class="nf">export</span><span class="o">(</span><span class="nc">String</span> <span class="n">destination</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="n">data</span> <span class="o">=</span> <span class="n">fetchData</span><span class="o">();</span>       <span class="c1">// 추상 메서드</span>
        <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="n">processed</span> <span class="o">=</span> <span class="n">process</span><span class="o">(</span><span class="n">data</span><span class="o">);</span> <span class="c1">// 훅 메서드 (선택적 오버라이드)</span>
        <span class="n">write</span><span class="o">(</span><span class="n">processed</span><span class="o">,</span> <span class="n">destination</span><span class="o">);</span>          <span class="c1">// 추상 메서드</span>
        <span class="n">notifyComplete</span><span class="o">();</span>                       <span class="c1">// 공통 구현</span>
    <span class="o">}</span>

    <span class="kd">protected</span> <span class="kd">abstract</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="nf">fetchData</span><span class="o">();</span>
    <span class="kd">protected</span> <span class="kd">abstract</span> <span class="kt">void</span> <span class="nf">write</span><span class="o">(</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="n">data</span><span class="o">,</span> <span class="nc">String</span> <span class="n">destination</span><span class="o">);</span>

    <span class="c1">// 훅 메서드 - 기본 구현 제공, 필요 시 오버라이드</span>
    <span class="kd">protected</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="nf">process</span><span class="o">(</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="n">data</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">data</span><span class="o">;</span> <span class="c1">// 기본은 그대로 반환</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">notifyComplete</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="s">"내보내기 완료"</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// 구체 구현</span>
<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CsvDataExport</span> <span class="kd">extends</span> <span class="nc">DataExportTemplate</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">public</span> <span class="nf">CsvDataExport</span><span class="o">(</span><span class="nc">UserRepository</span> <span class="n">userRepository</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">userRepository</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="nf">fetchData</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;(</span><span class="n">userRepository</span><span class="o">.</span><span class="na">findAll</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">write</span><span class="o">(</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="n">data</span><span class="o">,</span> <span class="nc">String</span> <span class="n">destination</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// CSV 파일 작성 로직</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="5-프록시-패턴-proxy">5. 프록시 패턴 (Proxy)</h2>

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

<p>실제 객체에 대한 접근을 제어하기 위해 대리 객체(proxy)를 두는 패턴이다. 프록시는 실제 객체와 동일한 인터페이스를 구현하며, 요청을 실제 객체에 위임하기 전후에 추가 로직을 수행할 수 있다.</p>

<h3 id="spring-aop와-transactional">Spring AOP와 @Transactional</h3>

<p>Spring AOP는 프록시 패턴의 대표적인 활용 사례다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// @Transactional은 Spring이 프록시를 생성하여 트랜잭션 관리</span>
<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="nd">@Transactional</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">placeOrder</span><span class="o">(</span><span class="nc">OrderRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Spring이 생성한 프록시가 실제 메서드 호출 전후에:</span>
        <span class="c1">// 1. 트랜잭션 시작 (begin)</span>
        <span class="c1">// 2. 메서드 실행</span>
        <span class="c1">// 3. 성공 시 커밋 (commit)</span>
        <span class="c1">// 4. 예외 시 롤백 (rollback)</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">from</span><span class="o">(</span><span class="n">request</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="n">inventoryService</span><span class="o">.</span><span class="na">decreaseStock</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">getItems</span><span class="o">());</span>
        <span class="n">paymentService</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">getTotalAmount</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>중요 주의사항 - self-invocation 문제:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><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="nd">@Transactional</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">outerMethod</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">innerMethod</span><span class="o">();</span> <span class="c1">// 같은 클래스 내부 호출 -&gt; 프록시 우회 -&gt; @Transactional 무효!</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">innerMethod</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// 이 트랜잭션 설정은 outerMethod()에서 직접 호출 시 적용되지 않음</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>커스텀 AOP 구현:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><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">ExecutionTime</span> <span class="o">{}</span>

<span class="nd">@Aspect</span>
<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ExecutionTimeAspect</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Logger</span> <span class="n">log</span> <span class="o">=</span> <span class="nc">LoggerFactory</span><span class="o">.</span><span class="na">getLogger</span><span class="o">(</span><span class="nc">ExecutionTimeAspect</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="nd">@Around</span><span class="o">(</span><span class="s">"@annotation(ExecutionTime)"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">measureExecutionTime</span><span class="o">(</span><span class="nc">ProceedingJoinPoint</span> <span class="n">pjp</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Throwable</span> <span class="o">{</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">String</span> <span class="n">methodName</span> <span class="o">=</span> <span class="n">pjp</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="k">try</span> <span class="o">{</span>
            <span class="nc">Object</span> <span class="n">result</span> <span class="o">=</span> <span class="n">pjp</span><span class="o">.</span><span class="na">proceed</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">"{} 실행 시간: {}ms"</span><span class="o">,</span> <span class="n">methodName</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="k">return</span> <span class="n">result</span><span class="o">;</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">methodName</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="k">throw</span> <span class="n">e</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// 사용</span>
<span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ReportService</span> <span class="o">{</span>

    <span class="nd">@ExecutionTime</span>
    <span class="kd">public</span> <span class="nc">Report</span> <span class="nf">generateReport</span><span class="o">(</span><span class="nc">Long</span> <span class="n">reportId</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 실행 시간 자동 측정</span>
        <span class="k">return</span> <span class="nf">buildReport</span><span class="o">(</span><span class="n">reportId</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="6-옵저버-패턴-observer">6. 옵저버 패턴 (Observer)</h2>

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

<p>객체의 상태 변화를 다른 객체들에게 자동으로 알리는 패턴이다. 발행-구독(Publish-Subscribe) 구조로도 알려져 있다.</p>

<h3 id="spring의-applicationevent">Spring의 ApplicationEvent</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 이벤트 클래스 정의</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserRegisteredEvent</span> <span class="kd">extends</span> <span class="nc">ApplicationEvent</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="kd">public</span> <span class="nf">UserRegisteredEvent</span><span class="o">(</span><span class="nc">Object</span> <span class="n">source</span><span class="o">,</span> <span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="n">source</span><span class="o">);</span>
        <span class="k">this</span><span class="o">.</span><span class="na">user</span> <span class="o">=</span> <span class="n">user</span><span class="o">;</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="o">{</span>
        <span class="k">return</span> <span class="n">user</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 이벤트 발행자 (Subject/Publisher)</span>
<span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserService</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">ApplicationEventPublisher</span> <span class="n">eventPublisher</span><span class="o">;</span>

    <span class="nd">@Transactional</span>
    <span class="kd">public</span> <span class="nc">User</span> <span class="nf">register</span><span class="o">(</span><span class="nc">UserRegisterRequest</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="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">name</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getName</span><span class="o">())</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>

        <span class="nc">User</span> <span class="n">savedUser</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="c1">// 이벤트 발행 - 리스너들은 알아서 처리</span>
        <span class="n">eventPublisher</span><span class="o">.</span><span class="na">publishEvent</span><span class="o">(</span><span class="k">new</span> <span class="nc">UserRegisteredEvent</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">savedUser</span><span class="o">));</span>

        <span class="k">return</span> <span class="n">savedUser</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 이벤트 리스너 (Observer/Subscriber) - 관심사 분리</span>
<span class="nd">@Component</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">NotificationEventListener</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">EmailService</span> <span class="n">emailService</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">SlackService</span> <span class="n">slackService</span><span class="o">;</span>

    <span class="c1">// 동기 처리</span>
    <span class="nd">@EventListener</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">sendWelcomeEmail</span><span class="o">(</span><span class="nc">UserRegisteredEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">emailService</span><span class="o">.</span><span class="na">sendWelcomeEmail</span><span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getUser</span><span class="o">().</span><span class="na">getEmail</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="c1">// 비동기 처리</span>
    <span class="nd">@EventListener</span>
    <span class="nd">@Async</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">sendSlackNotification</span><span class="o">(</span><span class="nc">UserRegisteredEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">slackService</span><span class="o">.</span><span class="na">notify</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">getUser</span><span class="o">().</span><span class="na">getName</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="c1">// 트랜잭션 커밋 후 처리 (DB 저장이 확정된 후 실행)</span>
    <span class="nd">@TransactionalEventListener</span><span class="o">(</span><span class="n">phase</span> <span class="o">=</span> <span class="nc">TransactionPhase</span><span class="o">.</span><span class="na">AFTER_COMMIT</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">sendAnalyticsEvent</span><span class="o">(</span><span class="nc">UserRegisteredEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">analyticsService</span><span class="o">.</span><span class="na">track</span><span class="o">(</span><span class="s">"user_registered"</span><span class="o">,</span> <span class="n">event</span><span class="o">.</span><span class="na">getUser</span><span class="o">().</span><span class="na">getId</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>Spring 이벤트의 장점:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">UserService</code>는 이메일/슬랙/분석 서비스를 직접 의존하지 않음</li>
  <li>새로운 처리 로직 추가 시 기존 코드 수정 불필요 (OCP)</li>
  <li><code class="language-plaintext highlighter-rouge">@Async</code>로 비동기 처리 가능</li>
  <li><code class="language-plaintext highlighter-rouge">@TransactionalEventListener</code>로 트랜잭션 경계와 연동 가능</li>
</ul>

<hr />

<h2 id="7-패턴-선택-가이드">7. 패턴 선택 가이드</h2>

<p>어떤 패턴을 언제 사용해야 하는지 정리했다.</p>

<table>
  <thead>
    <tr>
      <th>상황</th>
      <th>적용 패턴</th>
      <th>Spring 적용 방법</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>전역 상태 / 공유 자원 관리</td>
      <td>Singleton</td>
      <td><code class="language-plaintext highlighter-rouge">@Component</code> (기본 스코프)</td>
    </tr>
    <tr>
      <td>여러 알고리즘 중 런타임 선택</td>
      <td>Strategy</td>
      <td>인터페이스 + DI, <code class="language-plaintext highlighter-rouge">Map&lt;String, Strategy&gt;</code></td>
    </tr>
    <tr>
      <td>복잡한 객체 생성 캡슐화</td>
      <td>Factory Method</td>
      <td><code class="language-plaintext highlighter-rouge">@Bean</code> 메서드, <code class="language-plaintext highlighter-rouge">FactoryBean</code></td>
    </tr>
    <tr>
      <td>공통 흐름 + 세부 구현 분리</td>
      <td>Template Method</td>
      <td><code class="language-plaintext highlighter-rouge">JdbcTemplate</code>, 추상 클래스 확장</td>
    </tr>
    <tr>
      <td>횡단 관심사 (로깅, 트랜잭션)</td>
      <td>Proxy</td>
      <td><code class="language-plaintext highlighter-rouge">@Aspect</code>, <code class="language-plaintext highlighter-rouge">@Transactional</code></td>
    </tr>
    <tr>
      <td>이벤트 기반 느슨한 결합</td>
      <td>Observer</td>
      <td><code class="language-plaintext highlighter-rouge">ApplicationEvent</code>, <code class="language-plaintext highlighter-rouge">@EventListener</code></td>
    </tr>
    <tr>
      <td>환경별 다른 구현체 사용</td>
      <td>Abstract Factory</td>
      <td><code class="language-plaintext highlighter-rouge">@ConditionalOnProperty</code></td>
    </tr>
  </tbody>
</table>

<h3 id="패턴-조합-예시">패턴 조합 예시</h3>

<p>실제 프로젝트에서는 여러 패턴을 함께 사용한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Strategy + Observer + Template Method 조합</span>
<span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">PaymentFacade</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">PaymentStrategy</span><span class="o">&gt;</span> <span class="n">strategies</span><span class="o">;</span>   <span class="c1">// Strategy</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ApplicationEventPublisher</span> <span class="n">eventPublisher</span><span class="o">;</span>  <span class="c1">// Observer</span>

    <span class="nd">@Transactional</span>                                           <span class="c1">// Proxy (AOP)</span>
    <span class="kd">public</span> <span class="nc">PaymentResult</span> <span class="nf">pay</span><span class="o">(</span><span class="nc">PaymentRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">PaymentStrategy</span> <span class="n">strategy</span> <span class="o">=</span> <span class="n">strategies</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getMethod</span><span class="o">());</span> <span class="c1">// Strategy 선택</span>
        <span class="nc">PaymentResult</span> <span class="n">result</span> <span class="o">=</span> <span class="n">strategy</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>               <span class="c1">// Template Method</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">isSuccess</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">eventPublisher</span><span class="o">.</span><span class="na">publishEvent</span><span class="o">(</span>                                 <span class="c1">// Observer 발행</span>
                <span class="k">new</span> <span class="nf">PaymentCompletedEvent</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">result</span><span class="o">)</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="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="마치며">마치며</h2>

<p>Spring Framework를 사용하면서 무심코 쓰던 <code class="language-plaintext highlighter-rouge">@Transactional</code>, <code class="language-plaintext highlighter-rouge">@EventListener</code>, <code class="language-plaintext highlighter-rouge">JdbcTemplate</code> 등이 모두 GoF 디자인 패턴의 구현임을 알 수 있다. 패턴을 코드로 외우려 하기보다는 <strong>“어떤 문제를 해결하기 위한 패턴인가”</strong>를 이해하는 것이 중요하다.</p>

<p>Spring이 제공하는 패턴 구현체들을 잘 활용하면 직접 패턴을 구현하는 수고 없이도 유지보수성 높은 코드를 작성할 수 있다.</p>]]></content><author><name></name></author><category term="Java" /><category term="java" /><category term="spring" /><category term="design-pattern" /><category term="singleton" /><category term="strategy" /><category term="factory" /><category term="proxy" /><category term="observer" /><category term="template-method" /><category term="gof" /><summary type="html"><![CDATA[디자인 패턴이란?]]></summary></entry><entry><title type="html">DB 락 완전 가이드 - 비관적 락과 낙관적 락, 데드락 해결 전략</title><link href="https://skysoo1111.github.io/database/2026/03/27/database-lock-pessimistic-optimistic.html" rel="alternate" type="text/html" title="DB 락 완전 가이드 - 비관적 락과 낙관적 락, 데드락 해결 전략" /><published>2026-03-27T00:00:00+00:00</published><updated>2026-03-27T00:00:00+00:00</updated><id>https://skysoo1111.github.io/database/2026/03/27/database-lock-pessimistic-optimistic</id><content type="html" xml:base="https://skysoo1111.github.io/database/2026/03/27/database-lock-pessimistic-optimistic.html"><![CDATA[<h2 id="개요">개요</h2>

<p>다중 사용자 환경에서 동일한 데이터에 여러 트랜잭션이 동시 접근하면 <strong>데이터 불일치</strong>가 발생할 수 있다. 이를 제어하는 핵심 메커니즘이 바로 <strong>DB 락(Lock)</strong>이다.</p>

<h3 id="갱신-분실lost-update-문제">갱신 분실(Lost Update) 문제</h3>

<p>락의 필요성을 이해하려면 먼저 갱신 분실 문제를 알아야 한다.</p>

<p><strong>시나리오: 재고 관리 시스템</strong></p>

<ol>
  <li>관리자 A와 B가 재고 10개인 상품을 동시 조회</li>
  <li>A가 2개 판매 → 재고 8로 수정 및 커밋</li>
  <li>B가 조회 시점 기준(10개)으로 5개 판매 → 재고 5로 수정 및 커밋</li>
  <li>결과: 최종 재고는 3이어야 하나 <strong>5가 됨</strong> (A의 변경 사항 유실)</li>
</ol>

<p>이처럼 <code class="language-plaintext highlighter-rouge">READ_COMMITTED</code>나 <code class="language-plaintext highlighter-rouge">REPEATABLE_READ</code> 같은 표준 격리 수준만으로는 <strong>여러 요청에 걸친 갱신 분실을 막을 수 없다</strong>. DB 락이 필요한 이유다.</p>

<hr />

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

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

<p><strong>“데이터 경합이 빈번하게 발생할 것이라고 비관적으로 가정하고, 동시 수정을 원천적으로 차단한다.”</strong></p>

<p>트랜잭션이 데이터를 읽는 시점에 DB 레벨의 락을 획득하고, 다른 트랜잭션이 해당 데이터에 접근하지 못하도록 막는다.</p>

<p><strong>동작 흐름:</strong></p>
<ol>
  <li>트랜잭션 A가 데이터를 읽으며 배타적 락(X-Lock) 획득</li>
  <li>트랜잭션 B가 동일 데이터 접근 시도 → 대기 상태</li>
  <li>트랜잭션 A가 수정 완료 후 커밋 → 락 해제</li>
  <li>트랜잭션 B가 락 획득 후 갱신된 데이터로 작업 시작</li>
</ol>

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

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 배타적 락 (쓰기 전용, 다른 트랜잭션의 읽기/쓰기 차단)</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">product</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">-- 공유 락 (읽기 허용, 쓰기만 차단)</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">product</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">SHARE</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="jpa-구현-예시">JPA 구현 예시</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ProductRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Product</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="o">{</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 p from Product p where p.id = :id"</span><span class="o">)</span>
    <span class="nc">Optional</span><span class="o">&lt;</span><span class="nc">Product</span><span class="o">&gt;</span> <span class="nf">findByIdForUpdate</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>

<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">@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">id</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">findByIdForUpdate</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="nl">EntityNotFoundException:</span><span class="o">:</span><span class="k">new</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>
</code></pre></div></div>

<h3 id="잠금-타임아웃-설정">잠금 타임아웃 설정</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><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="o">})</span>
<span class="nd">@Query</span><span class="o">(</span><span class="s">"select p from Product p where p.id = :id"</span><span class="o">)</span>
<span class="nc">Optional</span><span class="o">&lt;</span><span class="nc">Product</span><span class="o">&gt;</span> <span class="nf">findByIdForUpdate</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>
</code></pre></div></div>

<p>3초 이내에 락을 획득하지 못하면 <code class="language-plaintext highlighter-rouge">LockTimeoutException</code>이 발생한다.</p>

<h3 id="lockmodetype-종류">LockModeType 종류</h3>

<table>
  <thead>
    <tr>
      <th>모드</th>
      <th>설명</th>
      <th>DB 락</th>
      <th>SQL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PESSIMISTIC_WRITE</code></td>
      <td>배타적 쓰기 락</td>
      <td>Exclusive Lock</td>
      <td><code class="language-plaintext highlighter-rouge">SELECT ... FOR UPDATE</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PESSIMISTIC_READ</code></td>
      <td>공유 읽기 락</td>
      <td>Shared Lock</td>
      <td><code class="language-plaintext highlighter-rouge">SELECT ... FOR SHARE</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PESSIMISTIC_FORCE_INCREMENT</code></td>
      <td>배타적 락 + 버전 강제 증가</td>
      <td>Exclusive Lock + Version</td>
      <td><code class="language-plaintext highlighter-rouge">SELECT ... FOR UPDATE</code></td>
    </tr>
  </tbody>
</table>

<h3 id="장단점">장단점</h3>

<p><strong>장점:</strong></p>
<ul>
  <li>충돌을 사전에 방지하여 데이터 일관성을 강력하게 보장</li>
  <li>충돌 발생 즉시 감지 가능 (락 대기 중 인지)</li>
  <li>충돌 비용이 큰 금융 거래에 적합</li>
</ul>

<p><strong>단점:</strong></p>
<ul>
  <li>락 보유 시간 동안 다른 트랜잭션이 대기 → 동시 처리 성능 저하</li>
  <li>교착 상태(Deadlock) 발생 위험</li>
  <li>웹 환경에서 HTTP 요청 간 락 유지 불가</li>
</ul>

<hr />

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

<h3 id="개념과-동작-원리-1">개념과 동작 원리</h3>

<p><strong>“데이터 충돌이 거의 발생하지 않을 것이라 가정하고, 수정 시점에 변경 여부만 확인한다.”</strong></p>

<p>DB 레벨의 락을 사용하지 않고, <strong>버전(Version) 컬럼</strong>을 활용해 애플리케이션 레벨에서 충돌을 감지한다. Compare-And-Swap(CAS) 메커니즘과 유사하다.</p>

<p><strong>동작 흐름:</strong></p>
<ol>
  <li>트랜잭션 A와 B가 데이터(version=1)를 동시 조회 (락 없음)</li>
  <li>트랜잭션 A가 먼저 커밋 → <code class="language-plaintext highlighter-rouge">UPDATE ... WHERE version=1</code> 성공 → version=2</li>
  <li>트랜잭션 B가 커밋 시도 → <code class="language-plaintext highlighter-rouge">UPDATE ... WHERE version=1</code> 실패 (이미 version=2)</li>
  <li><strong>0 rows affected</strong> → <code class="language-plaintext highlighter-rouge">OptimisticLockException</code> 발생 → 롤백</li>
</ol>

<h3 id="version-기반-구현">@Version 기반 구현</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><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">// Integer 또는 Timestamp도 가능</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">decreaseStock</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="k">if</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">stock</span> <span class="o">&lt;</span> <span class="n">quantity</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"재고가 부족합니다."</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">this</span><span class="o">.</span><span class="na">stock</span> <span class="o">-=</span> <span class="n">quantity</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">@Version</code> 필드는 JPA가 자동으로 관리한다. 엔티티 수정 시 자동으로 버전을 증가시키고 WHERE 절에 포함시킨다.</p>

<h3 id="커밋-시-생성되는-sql">커밋 시 생성되는 SQL</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">UPDATE</span> <span class="n">product</span>
<span class="k">SET</span> <span class="n">stock</span> <span class="o">=</span> <span class="mi">8</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이면 OptimisticLockException 발생</span>
</code></pre></div></div>

<h3 id="jpa-구현-예시-1">JPA 구현 예시</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Repository - 별도 어노테이션 없이 @Version만으로 동작</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ProductRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Product</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="o">{</span>
    <span class="c1">// 기본 findById 사용 가능</span>
<span class="o">}</span>

<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">@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="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="c1">// 트랜잭션 커밋 시 버전 체크 자동 수행</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="예외-처리-및-재시도-로직">예외 처리 및 재시도 로직</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><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">@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="k">try</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="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="k">catch</span> <span class="o">(</span><span class="nc">ObjectOptimisticLockingFailureException</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">warn</span><span class="o">(</span><span class="s">"동시성 충돌 발생 - productId: {}"</span><span class="o">,</span> <span class="n">productId</span><span class="o">);</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">ConcurrencyConflictException</span><span class="o">(</span>
                <span class="s">"재고가 다른 사용자에 의해 수정되었습니다. 다시 시도해주세요."</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>충돌이 잦은 환경에서는 재시도(Retry) 로직을 별도로 구현하거나, Spring Retry를 활용할 수 있다.</p>

<h3 id="장단점-1">장단점</h3>

<p><strong>장점:</strong></p>
<ul>
  <li>DB 직접 락 미사용으로 높은 동시 처리 성능</li>
  <li>교착 상태(Deadlock) 발생 없음</li>
  <li>상태 비저장(stateless) 방식으로 웹 애플리케이션, 마이크로서비스 환경에 적합</li>
  <li>HTTP 요청 간 경계를 넘어서도 충돌 감지 가능</li>
</ul>

<p><strong>단점:</strong></p>
<ul>
  <li>충돌 발생 시 전체 트랜잭션 롤백</li>
  <li>충돌이 빈번한 환경에서는 반복 롤백 비용으로 오히려 성능 저하</li>
  <li><code class="language-plaintext highlighter-rouge">@Modifying @Query</code> 같은 벌크 업데이트는 버전 관리를 우회함</li>
</ul>

<hr />

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

<table>
  <thead>
    <tr>
      <th>특징</th>
      <th>비관적 락</th>
      <th>낙관적 락</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>철학</td>
      <td>충돌 방지 (Prevent)</td>
      <td>충돌 감지 (Detect)</td>
    </tr>
    <tr>
      <td>잠금 시점</td>
      <td>읽기 시</td>
      <td>쓰기/커밋 시</td>
    </tr>
    <tr>
      <td>잠금 주체</td>
      <td>DB 트랜잭션</td>
      <td>애플리케이션</td>
    </tr>
    <tr>
      <td>구현 방식</td>
      <td>DB 락 (<code class="language-plaintext highlighter-rouge">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>높음</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td>HTTP 요청 간 적용</td>
      <td>불가</td>
      <td>가능</td>
    </tr>
    <tr>
      <td>적합 환경</td>
      <td>높은 충돌 환경</td>
      <td>낮은 충돌 환경</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>오해 주의:</strong> “비관적 락이 항상 느리고 낙관적 락이 항상 빠르다”는 잘못된 믿음이다. 충돌이 빈번한 환경에서는 반복 롤백 비용보다 락 대기 비용이 더 저렴할 수 있다.</p>
</blockquote>

<hr />

<h2 id="데드락-deadlock">데드락 (Deadlock)</h2>

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

<p>두 개 이상의 트랜잭션이 서로 상대방의 락 해제를 기다리며 <strong>무한정 대기하는 상태</strong>다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>트랜잭션 A: Lock(Item 1) 획득 → Lock(Item 2) 대기 중...
트랜잭션 B: Lock(Item 2) 획득 → Lock(Item 1) 대기 중...
→ 서로를 기다리며 영원히 대기 (Deadlock)
</code></pre></div></div>

<h3 id="발생-조건-4가지">발생 조건 4가지</h3>

<p>데드락은 다음 4가지 조건이 <strong>동시에 만족</strong>될 때 발생한다.</p>

<ol>
  <li><strong>상호 배제 (Mutual Exclusion)</strong>: 자원은 한 번에 하나의 트랜잭션만 사용 가능</li>
  <li><strong>점유와 대기 (Hold and Wait)</strong>: 자원을 보유한 상태에서 다른 자원을 요청하며 대기</li>
  <li><strong>비선점 (No Preemption)</strong>: 다른 트랜잭션이 보유한 자원을 강제로 빼앗을 수 없음</li>
  <li><strong>순환 대기 (Circular Wait)</strong>: 트랜잭션들이 순환적 의존 관계를 형성</li>
</ol>

<h3 id="예방-및-해결-전략">예방 및 해결 전략</h3>

<p><strong>예방 (Prevention):</strong></p>
<ul>
  <li>4가지 조건 중 하나를 제거</li>
  <li><strong>항상 동일한 순서로 락 획득</strong> → 순환 대기 조건 제거
    <ul>
      <li>예: 여러 테이블 업데이트 시 항상 id 오름차순으로 락 획득</li>
    </ul>
  </li>
</ul>

<p><strong>회피 (Avoidance):</strong></p>
<ul>
  <li>자원 할당 전 데드락 가능성을 예측하고 회피</li>
  <li>은행원 알고리즘(Banker’s Algorithm) 활용</li>
</ul>

<p><strong>탐지 및 복구 (Detection and Recovery):</strong></p>
<ul>
  <li>DB가 자동으로 데드락을 감지하고 하나의 트랜잭션을 희생(Victim)으로 선택하여 롤백</li>
  <li>MySQL에서 데드락 확인:</li>
</ul>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SHOW</span> <span class="n">ENGINE</span> <span class="n">INNODB</span> <span class="n">STATUS</span><span class="p">;</span>
<span class="c1">-- LATEST DETECTED DEADLOCK 섹션 확인</span>
</code></pre></div></div>

<p><strong>타임아웃 설정:</strong></p>
<ul>
  <li>락 대기 타임아웃을 설정하여 무한 대기 방지</li>
  <li><code class="language-plaintext highlighter-rouge">jakarta.persistence.lock.timeout</code> 힌트 활용</li>
</ul>

<hr />

<h2 id="실무에서의-선택-기준">실무에서의 선택 기준</h2>

<h3 id="비관적-락이-적합한-시나리오">비관적 락이 적합한 시나리오</h3>

<p><strong>충돌이 자주 발생하거나 충돌 비용이 높을 때</strong></p>

<table>
  <thead>
    <tr>
      <th>시나리오</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>인기 상품 재고 차감</td>
      <td>동시 구매 요청이 집중됨</td>
    </tr>
    <tr>
      <td>항공권/공연 티켓 좌석 예약</td>
      <td>좌석 중복 예약 절대 불가</td>
    </tr>
    <tr>
      <td>금융 거래 (계좌 이체, 출금)</td>
      <td>충돌 시 롤백 비용이 매우 큼</td>
    </tr>
  </tbody>
</table>

<h3 id="낙관적-락이-적합한-시나리오">낙관적 락이 적합한 시나리오</h3>

<p><strong>충돌 가능성이 낮고 읽기 위주일 때</strong></p>

<table>
  <thead>
    <tr>
      <th>시나리오</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>상품 설명/이름 수정</td>
      <td>동시 수정 확률이 낮음</td>
    </tr>
    <tr>
      <td>사용자 프로필 변경</td>
      <td>본인만 수정하는 경우가 대부분</td>
    </tr>
    <tr>
      <td>위키/문서 협업 도구</td>
      <td>충돌 시 사용자에게 재시도 안내 가능</td>
    </tr>
  </tbody>
</table>

<h3 id="하이브리드-접근법">하이브리드 접근법</h3>

<p>두 방식을 혼용하는 전략도 유효하다.</p>

<blockquote>
  <p><strong>예시: 이커머스 주문 흐름</strong></p>
  <ul>
    <li>상품 조회 / 장바구니 담기 → 낙관적 락 (충돌 거의 없음)</li>
    <li>최종 결제 / 재고 차감 → 비관적 락 (충돌 허용 불가)</li>
  </ul>
</blockquote>

<p>최선의 선택은 애플리케이션의 <strong>데이터 접근 패턴</strong>과 <strong>비즈니스 요구사항</strong>에 대한 깊은 이해에서 비롯된다.</p>]]></content><author><name></name></author><category term="Database" /><category term="lock" /><category term="pessimistic-lock" /><category term="optimistic-lock" /><category term="jpa" /><category term="database" /><category term="concurrency" /><category term="deadlock" /><summary type="html"><![CDATA[개요]]></summary></entry><entry><title type="html">배포 전략 완전 가이드 - 카나리, 블루그린, 롤링, 리니어 배포</title><link href="https://skysoo1111.github.io/devops/2026/03/26/deployment-strategies-canary-blue-green-rolling-linear.html" rel="alternate" type="text/html" title="배포 전략 완전 가이드 - 카나리, 블루그린, 롤링, 리니어 배포" /><published>2026-03-26T15:00:00+00:00</published><updated>2026-03-26T15:00:00+00:00</updated><id>https://skysoo1111.github.io/devops/2026/03/26/deployment-strategies-canary-blue-green-rolling-linear</id><content type="html" xml:base="https://skysoo1111.github.io/devops/2026/03/26/deployment-strategies-canary-blue-green-rolling-linear.html"><![CDATA[<h2 id="개요">개요</h2>

<p>서비스를 중단 없이 안전하게 배포하는 것은 현대 소프트웨어 운영의 핵심 과제다. 배포 전략은 “새 버전을 어떻게, 얼마나 빠르게, 얼마나 안전하게 트래픽에 노출할 것인가”를 결정하는 방식이다. 잘못된 배포 전략은 전체 서비스 장애로 이어질 수 있고, 반대로 적절한 전략은 빠른 배포 주기와 높은 안정성을 동시에 달성하게 해준다.</p>

<p>이 글에서는 가장 널리 쓰이는 4가지 배포 전략인 <strong>카나리(Canary)</strong>, <strong>블루그린(Blue-Green)</strong>, <strong>롤링(Rolling)</strong>, <strong>리니어(Linear)</strong> 배포를 개념부터 실전까지 정리한다.</p>

<hr />

<h2 id="배포-전략-비교-요약">배포 전략 비교 요약</h2>

<table>
  <thead>
    <tr>
      <th>전략</th>
      <th>다운타임</th>
      <th>롤백 속도</th>
      <th>리소스 비용</th>
      <th>트래픽 제어</th>
      <th>위험도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>롤링(Rolling)</td>
      <td>없음</td>
      <td>느림</td>
      <td>낮음</td>
      <td>제한적</td>
      <td>중간</td>
    </tr>
    <tr>
      <td>블루그린(Blue-Green)</td>
      <td>없음</td>
      <td>빠름</td>
      <td>높음(2배)</td>
      <td>이진(0%/100%)</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td>카나리(Canary)</td>
      <td>없음</td>
      <td>빠름</td>
      <td>중간</td>
      <td>세밀함</td>
      <td>매우 낮음</td>
    </tr>
    <tr>
      <td>리니어(Linear)</td>
      <td>없음</td>
      <td>중간</td>
      <td>중간</td>
      <td>단계적</td>
      <td>낮음</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="1-롤링-배포-rolling-deployment">1. 롤링 배포 (Rolling Deployment)</h2>

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

<p>롤링 배포는 인스턴스(파드)를 <strong>점진적으로 교체</strong>하는 방식이다. 기존 버전의 인스턴스를 하나씩 종료하면서 새 버전의 인스턴스로 대체한다. Kubernetes의 기본 배포 전략이 바로 롤링 업데이트다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>배포 시작:
[v1] [v1] [v1] [v1]   ← 전체 v1

배포 중간:
[v2] [v2] [v1] [v1]   ← 절반 교체

배포 완료:
[v2] [v2] [v2] [v2]   ← 전체 v2
</code></pre></div></div>

<h3 id="kubernetes-설정-예시">Kubernetes 설정 예시</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><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">4</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">maxUnavailable</span><span class="pi">:</span> <span class="m">1</span>    <span class="c1"># 동시에 종료할 수 있는 최대 파드 수</span>
      <span class="na">maxSurge</span><span class="pi">:</span> <span class="m">1</span>          <span class="c1"># 동시에 추가 생성할 수 있는 최대 파드 수</span>
  <span class="na">template</span><span class="pi">:</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">my-app</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">my-app:v2</span>
</code></pre></div></div>

<h3 id="장단점">장단점</h3>

<p><strong>장점</strong></p>
<ul>
  <li>추가 인프라 비용이 없다 (기존 리소스 내에서 교체)</li>
  <li>배포 중 서비스가 유지된다 (제로 다운타임)</li>
  <li>설정이 단순하고 대부분의 컨테이너 오케스트레이터에서 기본 지원한다</li>
</ul>

<p><strong>단점</strong></p>
<ul>
  <li>배포 중 v1과 v2가 동시에 운영되므로 <strong>API 하위 호환성</strong>이 반드시 보장되어야 한다</li>
  <li>문제 발생 시 롤백이 느리다 (다시 순차적으로 교체해야 함)</li>
  <li>배포 진행 중에는 트래픽 비율을 정밀하게 제어하기 어렵다</li>
</ul>

<h3 id="적합한-상황">적합한 상황</h3>
<ul>
  <li>빠른 배포보다 단순함을 우선할 때</li>
  <li>API 호환성이 완전히 보장된 패치/마이너 업데이트</li>
  <li>리소스 비용이 제한된 환경</li>
</ul>

<hr />

<h2 id="2-블루그린-배포-blue-green-deployment">2. 블루그린 배포 (Blue-Green Deployment)</h2>

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

<p>블루그린 배포는 <strong>동일한 환경을 두 벌</strong> 운영하는 방식이다. 현재 프로덕션은 “블루(Blue)”, 새 버전은 “그린(Green)” 환경에 배포한다. 준비가 완료되면 로드 밸런서의 트래픽을 블루에서 그린으로 한 번에 전환한다.</p>

<pre><code class="language-mermaid">graph LR
    LB[로드 밸런서]
    subgraph Blue[Blue - 현재 버전 v1]
        B1[v1 인스턴스]
        B2[v1 인스턴스]
    end
    subgraph Green[Green - 신규 버전 v2]
        G1[v2 인스턴스]
        G2[v2 인스턴스]
    end

    LB --&gt;|트래픽 100%| Blue
    LB -.-&gt;|대기중| Green
</code></pre>

<p><strong>전환 후:</strong></p>

<pre><code class="language-mermaid">graph LR
    LB[로드 밸런서]
    subgraph Blue[Blue - 구버전 v1 대기]
        B1[v1 인스턴스]
        B2[v1 인스턴스]
    end
    subgraph Green[Green - 신규 버전 v2]
        G1[v2 인스턴스]
        G2[v2 인스턴스]
    end

    LB -.-&gt;|대기 롤백용| Blue
    LB --&gt;|트래픽 100%| Green
</code></pre>

<h3 id="aws-환경-예시">AWS 환경 예시</h3>

<p>AWS Elastic Beanstalk, CodeDeploy, 또는 ALB의 Target Group 교체로 구현 가능하다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ALB 리스너 규칙을 그린으로 전환</span>
aws elbv2 modify-listener <span class="se">\</span>
  <span class="nt">--listener-arn</span> <span class="nv">$LISTENER_ARN</span> <span class="se">\</span>
  <span class="nt">--default-actions</span> <span class="nv">Type</span><span class="o">=</span>forward,TargetGroupArn<span class="o">=</span><span class="nv">$GREEN_TARGET_GROUP_ARN</span>

<span class="c"># 검증 후 블루 환경 정리 또는 유지</span>
</code></pre></div></div>

<h3 id="kubernetes에서-구현">Kubernetes에서 구현</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 서비스의 selector만 바꿔서 트래픽 전환</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">my-app-service</span>
<span class="na">spec</span><span class="pi">:</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="na">version</span><span class="pi">:</span> <span class="s">green</span>   <span class="c1"># blue → green 으로 변경</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>
</code></pre></div></div>

<h3 id="장단점-1">장단점</h3>

<p><strong>장점</strong></p>
<ul>
  <li>트래픽 전환이 즉각적이다 (배포 순간 100% 전환)</li>
  <li>문제 발생 시 로드 밸런서 설정만 되돌리면 즉시 롤백된다</li>
  <li>구버전 환경이 그대로 남아있어 비교 테스트가 용이하다</li>
  <li>v1/v2 동시 운영 기간이 없어 호환성 걱정이 줄어든다</li>
</ul>

<p><strong>단점</strong></p>
<ul>
  <li>동일한 인프라를 두 벌 운영하므로 <strong>비용이 약 2배</strong>다</li>
  <li>데이터베이스 스키마 변경이 있을 때 처리가 복잡해진다</li>
  <li>100% 전환이기 때문에 신규 버전에 버그가 있으면 전체 사용자가 영향을 받는다</li>
</ul>

<h3 id="적합한-상황-1">적합한 상황</h3>
<ul>
  <li>즉각적인 롤백이 필수인 금융/결제 시스템</li>
  <li>정기 점검 없이 배포해야 하는 24/7 서비스</li>
  <li>프로덕션과 동일한 환경에서 최종 검증이 필요한 경우</li>
</ul>

<hr />

<h2 id="3-카나리-배포-canary-deployment">3. 카나리 배포 (Canary Deployment)</h2>

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

<p>카나리 배포는 신규 버전을 <strong>소수의 트래픽에만 먼저 노출</strong>하고, 문제가 없으면 점진적으로 비율을 높이는 전략이다. 이름의 유래는 탄광 안전 도구였던 카나리아 새(독가스에 민감하여 위험을 먼저 감지)에서 왔다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1단계: 신규 버전에 5% 트래픽 노출
[v2] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1] [v1]
 5%                                                                                                95%

2단계: 메트릭 정상 → 25%로 확대
[v2][v2][v2][v2][v2] [v1][v1][v1][v1][v1][v1][v1][v1][v1][v1][v1][v1][v1][v1][v1]
      25%                                     75%

3단계: 메트릭 정상 → 100%
[v2][v2][v2][v2][v2][v2][v2][v2][v2][v2][v2][v2][v2][v2][v2][v2][v2][v2][v2][v2]
                                   100%
</code></pre></div></div>

<h3 id="argo-rollouts를-사용한-카나리-구현">Argo Rollouts를 사용한 카나리 구현</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">argoproj.io/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Rollout</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">10</span>
  <span class="na">strategy</span><span class="pi">:</span>
    <span class="na">canary</span><span class="pi">:</span>
      <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">setWeight</span><span class="pi">:</span> <span class="m">5</span>       <span class="c1"># 1단계: 5% 트래픽</span>
      <span class="pi">-</span> <span class="na">pause</span><span class="pi">:</span> <span class="pi">{</span><span class="nv">duration</span><span class="pi">:</span> <span class="nv">10m</span><span class="pi">}</span>   <span class="c1"># 10분 관찰</span>
      <span class="pi">-</span> <span class="na">setWeight</span><span class="pi">:</span> <span class="m">25</span>      <span class="c1"># 2단계: 25%</span>
      <span class="pi">-</span> <span class="na">pause</span><span class="pi">:</span> <span class="pi">{</span><span class="nv">duration</span><span class="pi">:</span> <span class="nv">10m</span><span class="pi">}</span>
      <span class="pi">-</span> <span class="na">setWeight</span><span class="pi">:</span> <span class="m">50</span>      <span class="c1"># 3단계: 50%</span>
      <span class="pi">-</span> <span class="na">pause</span><span class="pi">:</span> <span class="pi">{}</span>          <span class="c1"># 수동 승인 대기</span>
      <span class="pi">-</span> <span class="na">setWeight</span><span class="pi">:</span> <span class="m">100</span>     <span class="c1"># 4단계: 전체 전환</span>

      <span class="c1"># 자동 롤백 조건 (Prometheus 메트릭 기반)</span>
      <span class="na">analysis</span><span class="pi">:</span>
        <span class="na">templates</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">templateName</span><span class="pi">:</span> <span class="s">success-rate</span>
        <span class="na">args</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">service-name</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">my-app</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">argoproj.io/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">AnalysisTemplate</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">success-rate</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">metrics</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">success-rate</span>
    <span class="na">interval</span><span class="pi">:</span> <span class="s">1m</span>
    <span class="na">successCondition</span><span class="pi">:</span> <span class="s">result[0] &gt;= </span><span class="m">0.95</span>   <span class="c1"># 성공률 95% 이상</span>
    <span class="na">failureLimit</span><span class="pi">:</span> <span class="m">3</span>
    <span class="na">provider</span><span class="pi">:</span>
      <span class="na">prometheus</span><span class="pi">:</span>
        <span class="na">address</span><span class="pi">:</span> <span class="s">http://prometheus:9090</span>
        <span class="na">query</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">sum(rate(http_requests_total{status=~"2.."}[5m]))</span>
          <span class="s">/</span>
          <span class="s">sum(rate(http_requests_total[5m]))</span>
</code></pre></div></div>

<h3 id="istio를-활용한-트래픽-분할">Istio를 활용한 트래픽 분할</h3>

<p>서비스 메시(Istio)를 사용하면 쿠버네티스 파드 수 비율이 아닌 <strong>정확한 트래픽 비율</strong>로 제어할 수 있다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.istio.io/v1alpha3</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">VirtualService</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">http</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">route</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">destination</span><span class="pi">:</span>
        <span class="na">host</span><span class="pi">:</span> <span class="s">my-app</span>
        <span class="na">subset</span><span class="pi">:</span> <span class="s">v1</span>
      <span class="na">weight</span><span class="pi">:</span> <span class="m">95</span>
    <span class="pi">-</span> <span class="na">destination</span><span class="pi">:</span>
        <span class="na">host</span><span class="pi">:</span> <span class="s">my-app</span>
        <span class="na">subset</span><span class="pi">:</span> <span class="s">v2</span>
      <span class="na">weight</span><span class="pi">:</span> <span class="m">5</span>
</code></pre></div></div>

<h3 id="장단점-2">장단점</h3>

<p><strong>장점</strong></p>
<ul>
  <li>신규 버전의 문제를 소수 사용자에게만 노출한다 (위험 최소화)</li>
  <li>실제 프로덕션 트래픽으로 성능·안정성을 검증한다</li>
  <li>문제 발생 시 트래픽 비율을 0%로 즉시 되돌릴 수 있다</li>
  <li>A/B 테스트나 기능 플래그와 결합하면 사용자 세그먼트 제어도 가능하다</li>
</ul>

<p><strong>단점</strong></p>
<ul>
  <li>카나리와 기존 버전이 함께 운영되므로 <strong>API 호환성</strong>이 필요하다</li>
  <li>모니터링 및 분석 인프라가 갖춰져 있어야 효과적이다</li>
  <li>배포 완료까지 시간이 걸린다</li>
  <li>구성이 블루그린·롤링보다 복잡하다</li>
</ul>

<h3 id="적합한-상황-2">적합한 상황</h3>
<ul>
  <li>트래픽이 많고 안정성이 중요한 대형 서비스</li>
  <li>빠른 실험과 데이터 기반 의사결정이 필요한 팀</li>
  <li>Prometheus, Datadog 등 메트릭 인프라가 이미 갖춰진 환경</li>
</ul>

<hr />

<h2 id="4-리니어-배포-linear-deployment">4. 리니어 배포 (Linear Deployment)</h2>

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

<p>리니어 배포는 카나리 배포의 변형으로, 트래픽을 <strong>일정한 간격으로 균등하게</strong> 증가시키는 전략이다. “선형 배포”라고도 부른다. AWS CodeDeploy의 <code class="language-plaintext highlighter-rouge">Linear10PercentEvery1Minute</code> 같은 설정이 대표적이다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>시작:    [v2  5%] [v1 95%]
1분 후:  [v2 15%] [v1 85%]
2분 후:  [v2 25%] [v1 75%]
3분 후:  [v2 35%] [v1 65%]
4분 후:  [v2 45%] [v1 55%]
5분 후:  [v2 55%] [v1 45%]
...
완료:    [v2 100%]
</code></pre></div></div>

<p>카나리가 단계별로 사람이 승인하거나 특정 메트릭을 확인하며 진행하는 반면, 리니어는 <strong>자동으로 일정한 속도로 증가</strong>한다.</p>

<h3 id="aws-codedeploy-설정-예시">AWS CodeDeploy 설정 예시</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"deploymentConfigName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"my-linear-config"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"trafficRoutingConfig"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TimeBasedLinear"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"timeBasedLinear"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"linearPercentage"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
      </span><span class="nl">"linearInterval"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>위 설정은 매 1분마다 10%씩 트래픽을 증가시켜 10분 만에 완전 전환한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># SAM/CloudFormation Lambda 함수 배포 예시</span>
<span class="na">DeploymentPreference</span><span class="pi">:</span>
  <span class="na">Type</span><span class="pi">:</span> <span class="s">Linear10PercentEvery1Minute</span>  <span class="c1"># 매 분 10%씩 증가</span>
  <span class="na">Alarms</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="kt">!Ref</span> <span class="s">AliasErrorMetricGreaterThanZeroAlarm</span>
  <span class="na">Hooks</span><span class="pi">:</span>
    <span class="na">PreTraffic</span><span class="pi">:</span> <span class="kt">!Ref</span> <span class="s">PreTrafficHookFunction</span>
    <span class="na">PostTraffic</span><span class="pi">:</span> <span class="kt">!Ref</span> <span class="s">PostTrafficHookFunction</span>
</code></pre></div></div>

<h3 id="카나리-vs-리니어-차이">카나리 vs 리니어 차이</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>카나리(Canary)</th>
      <th>리니어(Linear)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>트래픽 증가 방식</td>
      <td>단계적, 비균등 (5% → 25% → 50% → 100%)</td>
      <td>균등 간격 (10% → 20% → 30% …)</td>
    </tr>
    <tr>
      <td>진행 방식</td>
      <td>수동 승인 또는 분석 기반</td>
      <td>자동, 시간 기반</td>
    </tr>
    <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="장단점-3">장단점</h3>

<p><strong>장점</strong></p>
<ul>
  <li>예측 가능하고 일정한 배포 속도를 보장한다</li>
  <li>카나리보다 설정이 단순하다</li>
  <li>자동화된 점진적 노출로 문제를 조기에 감지한다</li>
  <li>배포 완료 시간을 예측할 수 있다</li>
</ul>

<p><strong>단점</strong></p>
<ul>
  <li>트래픽 증가 속도를 상황에 맞게 유연하게 조정하기 어렵다</li>
  <li>카나리처럼 특정 비율에서 오랜 시간 관찰하는 것이 불가능하다</li>
  <li>배포 속도가 카나리보다 느릴 수 있다</li>
</ul>

<h3 id="적합한-상황-3">적합한 상황</h3>
<ul>
  <li>AWS Lambda, API Gateway 같은 서버리스 환경</li>
  <li>자동화된 배포 파이프라인을 구성할 때</li>
  <li>카나리보다 단순한 점진적 배포가 필요한 경우</li>
</ul>

<hr />

<h2 id="배포-전략-선택-가이드">배포 전략 선택 가이드</h2>

<pre><code class="language-mermaid">flowchart TD
    A[배포 전략 선택] --&gt; B{다운타임 허용 가능?}
    B --&gt;|예| C[In-place / 재시작]
    B --&gt;|아니오| D{롤백 속도가 최우선?}
    D --&gt;|예, 즉각 롤백 필요| E{트래픽을 단계적으로\n제어하고 싶은가?}
    E --&gt;|아니오| F[블루그린 배포]
    E --&gt;|예| G{메트릭 인프라가\n갖춰져 있는가?}
    G --&gt;|예| H[카나리 배포]
    G --&gt;|아니오| I[리니어 배포]
    D --&gt;|느려도 괜찮음| J{추가 리소스 비용\n허용 가능?}
    J --&gt;|아니오| K[롤링 배포]
    J --&gt;|예| F
</code></pre>

<h3 id="실무-권장-조합">실무 권장 조합</h3>

<p><strong>소규모 팀 / 스타트업</strong></p>
<ul>
  <li>개발/스테이징: 롤링 배포</li>
  <li>프로덕션: 블루그린 배포</li>
</ul>

<p><strong>중규모 팀 / 성장 단계</strong></p>
<ul>
  <li>개발: 롤링 배포</li>
  <li>스테이징: 블루그린 배포</li>
  <li>프로덕션: 카나리 배포 (Argo Rollouts + Prometheus)</li>
</ul>

<p><strong>대규모 팀 / 엔터프라이즈</strong></p>
<ul>
  <li>모든 환경: 카나리 배포</li>
  <li>서버리스 함수: 리니어 배포 (AWS CodeDeploy)</li>
  <li>Feature Flag와 결합하여 코드 배포와 기능 출시를 분리</li>
</ul>

<hr />

<h2 id="데이터베이스-마이그레이션과-배포-전략">데이터베이스 마이그레이션과 배포 전략</h2>

<p>배포 전략에서 가장 까다로운 부분 중 하나는 <strong>DB 스키마 변경</strong>이다. v1과 v2가 동시에 운영되는 시간이 발생하는 롤링·카나리·리니어 배포에서는 특히 중요하다.</p>

<h3 id="expand-contract-패턴">Expand-Contract 패턴</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1단계 (Expand): 신규 컬럼 추가 (NULL 허용)
  → v1, v2 모두 동작 가능

2단계 (Migrate): 데이터 마이그레이션 실행
  → 배포와 별개로 진행

3단계 (배포): v2 배포
  → v2는 신규 컬럼 사용

4단계 (Contract): 구버전 컬럼 제거
  → v1이 완전히 사라진 후 별도 배포로 진행
</code></pre></div></div>

<p>이 패턴을 따르면 배포 중 구버전과 신버전의 DB 호환성을 유지할 수 있다.</p>

<hr />

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

<table>
  <thead>
    <tr>
      <th>전략</th>
      <th>핵심 특징</th>
      <th>추천 환경</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>롤링</td>
      <td>단순, 낮은 비용, 느린 롤백</td>
      <td>호환성 보장된 업데이트, 소규모 서비스</td>
    </tr>
    <tr>
      <td>블루그린</td>
      <td>즉각 롤백, 높은 비용, 100% 전환</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>과 <strong>충분한 모니터링</strong>이 전제되어야 배포 전략이 실질적인 안전망이 된다.</p>]]></content><author><name></name></author><category term="DevOps" /><category term="deployment" /><category term="canary" /><category term="blue-green" /><category term="rolling" /><category term="linear" /><category term="kubernetes" /><category term="devops" /><category term="cicd" /><summary type="html"><![CDATA[개요]]></summary></entry><entry><title type="html">LangChain 생태계 주간 업데이트 - Prompt Caching, 실행 추적 강화, 배포 관리 개선</title><link href="https://skysoo1111.github.io/ai/2026/03/21/langchain-ecosystem-weekly-update.html" rel="alternate" type="text/html" title="LangChain 생태계 주간 업데이트 - Prompt Caching, 실행 추적 강화, 배포 관리 개선" /><published>2026-03-21T01:00:00+00:00</published><updated>2026-03-21T01:00:00+00:00</updated><id>https://skysoo1111.github.io/ai/2026/03/21/langchain-ecosystem-weekly-update</id><content type="html" xml:base="https://skysoo1111.github.io/ai/2026/03/21/langchain-ecosystem-weekly-update.html"><![CDATA[<h2 id="개요">개요</h2>

<p>2026년 3월 셋째 주, LangChain 생태계에 주목할 만한 업데이트가 쏟아졌다. Anthropic 프롬프트 캐싱 미들웨어 도입, LangSmith 트레이싱 정확도 개선, LangGraph 런타임 실행 정보 추가, 그리고 CLI 배포 관리 기능 강화까지. 에이전트 운영 비용을 줄이고 관찰가능성(Observability)을 높이는 방향으로 생태계 전반이 성숙해가고 있다.</p>

<p>이번 글에서는 각 릴리스의 핵심 변경사항과 실전에서의 의미를 정리한다.</p>

<h2 id="1-langchain-anthropic-140---프롬프트-캐싱으로-토큰-비용-절감">1. langchain-anthropic 1.4.0 - 프롬프트 캐싱으로 토큰 비용 절감</h2>

<p>3월 17일 릴리스된 langchain-anthropic 1.4.0의 핵심은 <strong>AnthropicPromptCachingMiddleware</strong>다.</p>

<h3 id="무엇이-바뀌었나">무엇이 바뀌었나</h3>

<p>시스템 메시지와 도구 정의(tool definitions)에 명시적 캐싱이 자동 적용된다. 에이전트가 매번 동일한 시스템 프롬프트와 도구 스키마를 보내더라도, Anthropic API 레벨에서 캐시 히트가 발생하여 <strong>토큰 비용이 크게 줄어든다</strong>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">langchain_anthropic</span> <span class="kn">import</span> <span class="n">ChatAnthropic</span>

<span class="c1"># 1.4.0부터 cache_control을 최상위 파라미터로 직접 전달 가능
</span><span class="n">llm</span> <span class="o">=</span> <span class="n">ChatAnthropic</span><span class="p">(</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"claude-sonnet-4-5-20250514"</span><span class="p">,</span>
    <span class="n">cache_control</span><span class="o">=</span><span class="p">{</span><span class="s">"type"</span><span class="p">:</span> <span class="s">"ephemeral"</span><span class="p">}</span>  <span class="c1"># Anthropic 네이티브 파라미터로 직접 위임
</span><span class="p">)</span>
</code></pre></div></div>

<p>기존에는 LangChain 내부에서 <code class="language-plaintext highlighter-rouge">cache_control</code>을 별도로 핸들링했지만, Anthropic API가 이를 네이티브로 지원하게 되면서 중간 처리 코드가 제거되었다. 더 깔끔하고 예측 가능한 동작이다.</p>

<h3 id="실전에서의-의미">실전에서의 의미</h3>

<p>에이전트 시스템에서 시스템 프롬프트는 보통 수천 토큰에 달하고, 도구 정의가 10개 이상이면 스키마만으로도 상당한 토큰을 소비한다. 반복 호출이 많은 프로덕션 환경에서 이 캐싱은 <strong>월 단위로 수십 퍼센트의 비용 절감</strong> 효과를 가져올 수 있다.</p>

<h2 id="2-langchain-core-1220--langchain-1213---langsmith-트레이싱-정확도-강화">2. LangChain Core 1.2.20 &amp; LangChain 1.2.13 - LangSmith 트레이싱 정확도 강화</h2>

<p>3월 18~19일에 걸쳐 릴리스된 langchain-core 1.2.20과 langchain 1.2.13은 <strong>관찰가능성</strong> 측면에서 중요한 개선을 담고 있다.</p>

<h3 id="호출-파라미터-트레이싱-수정">호출 파라미터 트레이싱 수정</h3>

<p>이전 버전에서는 <code class="language-plaintext highlighter-rouge">init_chat_model</code>로 생성한 모델에 <code class="language-plaintext highlighter-rouge">thinking</code> 파라미터나 <code class="language-plaintext highlighter-rouge">bind_tools</code>를 적용했을 때, LangSmith 트레이스에서 실제 호출 파라미터가 누락되는 문제가 있었다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">langchain.chat_models</span> <span class="kn">import</span> <span class="n">init_chat_model</span>

<span class="n">model</span> <span class="o">=</span> <span class="n">init_chat_model</span><span class="p">(</span>
    <span class="s">"anthropic:claude-haiku-4-5"</span><span class="p">,</span>
    <span class="n">thinking</span><span class="o">=</span><span class="p">{</span><span class="s">"type"</span><span class="p">:</span> <span class="s">"enabled"</span><span class="p">,</span> <span class="s">"budget_tokens"</span><span class="p">:</span> <span class="mi">5_000</span><span class="p">}</span>
<span class="p">).</span><span class="n">bind_tools</span><span class="p">([</span><span class="n">get_weather</span><span class="p">])</span>

<span class="c1"># 1.2.20 이전: LangSmith에서 temperature, thinking budget 등이 보이지 않았음
# 1.2.20 이후: 모든 invocation params가 메타데이터로 정확히 기록됨
</span><span class="n">model</span><span class="p">.</span><span class="n">invoke</span><span class="p">(</span><span class="s">"Hello!"</span><span class="p">,</span> <span class="n">temperature</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="create_agent-langsmith-통합-메타데이터">create_agent LangSmith 통합 메타데이터</h3>

<p><code class="language-plaintext highlighter-rouge">create_agent</code>와 <code class="language-plaintext highlighter-rouge">init_chat_model</code>에 LangSmith 통합 메타데이터가 자동 추가되었다. 에이전트의 초기화 설정, 사용 모델, 바인딩된 도구 정보가 LangSmith 대시보드에서 자동으로 표시된다.</p>

<h3 id="실전에서의-의미-1">실전에서의 의미</h3>

<p>프로덕션 에이전트 디버깅에서 <strong>“이 호출에 어떤 파라미터가 전달되었는가”</strong>는 가장 빈번하게 확인하는 정보다. 이 수정으로 thinking 모드 사용 여부, temperature 설정, 도구 바인딩 상태를 트레이스만으로 즉시 파악할 수 있게 되었다.</p>

<h2 id="3-langgraph-113---런타임-execution-info-추가">3. LangGraph 1.1.3 - 런타임 Execution Info 추가</h2>

<p>3월 18일 릴리스된 LangGraph 1.1.3은 런타임 레벨에서 <strong>실행 정보(Execution Info)</strong>를 추적하는 기능을 추가했다.</p>

<h3 id="무엇이-바뀌었나-1">무엇이 바뀌었나</h3>

<p>그래프 실행 중 각 노드의 실행 상태, 타이밍, 메타데이터를 런타임에서 직접 조회할 수 있다. 이전에는 LangSmith를 통해서만 확인할 수 있었던 실행 세부 정보를 코드 레벨에서 접근 가능해졌다.</p>

<h3 id="함께-수정된-사항">함께 수정된 사항</h3>

<ul>
  <li><strong>checkpoint-postgres 3.0.5</strong>: DB 커넥션 재사용 버그 수정. 장시간 운영 시 커넥션 풀 고갈 문제가 해결되었다.</li>
  <li><strong>SDK Python 0.3.12</strong>: 직렬화(serialization) 알파 지원 추가.</li>
</ul>

<h3 id="실전에서의-의미-2">실전에서의 의미</h3>

<p>LangGraph 기반 에이전트의 각 노드가 얼마나 걸렸는지, 어디서 병목이 발생하는지를 외부 모니터링 도구 없이도 파악할 수 있다. 특히 복잡한 멀티 노드 그래프에서 성능 튜닝 시 유용하다.</p>

<h2 id="4-langgraph-cli-0419---배포-리비전-관리">4. LangGraph CLI 0.4.19 - 배포 리비전 관리</h2>

<p>3월 20일 릴리스된 LangGraph CLI 0.4.19는 <code class="language-plaintext highlighter-rouge">deploy revisions list</code> 명령어를 추가했다.</p>

<h3 id="사용법">사용법</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>langgraph deploy revisions list &lt;deployment-id&gt;
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Revision ID                           Status    Created At                 
------------------------------------  --------  ---------------------------
fe05f163-58cf-4783-a918-0c78a8fab0c7  BUILDING  2026-03-12T20:47:14.626436Z
03c32fcf-544f-4517-a016-c7a7243b898d  DEPLOYED  2026-03-12T20:30:09.199275Z
29605810-29cc-4110-ac05-d43e564827d0  DEPLOYED  2026-03-12T19:56:36.232427Z
</code></pre></div></div>

<p>특정 배포의 리비전 히스토리를 테이블 형태로 조회할 수 있다. 각 리비전의 ID, 상태(BUILDING/DEPLOYED), 생성 시각을 확인 가능하다.</p>

<h3 id="실전에서의-의미-3">실전에서의 의미</h3>

<p>프로덕션 에이전트 배포에서 <strong>롤백 판단</strong>과 <strong>배포 이력 추적</strong>이 CLI 한 줄로 가능해졌다. 문제 발생 시 이전 리비전으로의 롤백 결정을 빠르게 내릴 수 있다.</p>

<h2 id="한-주를-관통하는-키워드-프로덕션-성숙도">한 주를 관통하는 키워드: 프로덕션 성숙도</h2>

<p>이번 주 업데이트들을 관통하는 공통 주제는 <strong>프로덕션 환경에서의 성숙도</strong>다.</p>

<table>
  <thead>
    <tr>
      <th>영역</th>
      <th>업데이트</th>
      <th>효과</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>비용</td>
      <td>Anthropic Prompt Caching Middleware</td>
      <td>반복 호출 토큰 비용 절감</td>
    </tr>
    <tr>
      <td>관찰가능성</td>
      <td>LangSmith 트레이싱 파라미터 수정</td>
      <td>디버깅 정확도 향상</td>
    </tr>
    <tr>
      <td>런타임</td>
      <td>LangGraph Execution Info</td>
      <td>코드 레벨 성능 모니터링</td>
    </tr>
    <tr>
      <td>운영</td>
      <td>CLI deploy revisions list</td>
      <td>배포 이력 관리 및 롤백</td>
    </tr>
  </tbody>
</table>

<p>Deep Agents의 등장이 “에이전트를 만드는 것”에 집중했다면, 이번 주 업데이트들은 “에이전트를 운영하는 것”에 집중하고 있다. LangChain 생태계가 프로토타입 단계를 넘어 프로덕션 운영 도구로 자리잡아 가는 흐름이 뚜렷하다.</p>

<h2 id="버전-요약">버전 요약</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>langchain          1.2.13   (2026-03-19)
langchain-core     1.2.20   (2026-03-18)
langchain-anthropic 1.4.0   (2026-03-17)
langgraph          1.1.3    (2026-03-18)
langgraph CLI      0.4.19   (2026-03-20)
</code></pre></div></div>

<h2 id="참고-자료">참고 자료</h2>

<ul>
  <li><a href="https://github.com/langchain-ai/langgraph/releases/tag/1.1.3">LangGraph 1.1.3 Release</a></li>
  <li><a href="https://github.com/langchain-ai/langgraph/releases/tag/cli%3D%3D0.4.19">LangGraph CLI 0.4.19 Release</a></li>
  <li><a href="https://github.com/langchain-ai/langchain/releases/tag/langchain-anthropic%3D%3D1.4.0">langchain-anthropic 1.4.0 Release</a></li>
  <li><a href="https://github.com/langchain-ai/langchain/releases/tag/langchain-core%3D%3D1.2.20">langchain-core 1.2.20 Release</a></li>
  <li><a href="https://github.com/langchain-ai/langchain/releases/tag/langchain%3D%3D1.2.13">LangChain 1.2.13 Release</a></li>
</ul>]]></content><author><name></name></author><category term="AI" /><category term="langchain" /><category term="langgraph" /><category term="langsmith" /><category term="anthropic" /><category term="prompt-caching" /><category term="ai-agent" /><category term="python" /><summary type="html"><![CDATA[개요]]></summary></entry><entry><title type="html">Claude Code 커스텀 명령어 가이드: Git 워크플로우 자동화</title><link href="https://skysoo1111.github.io/devops/2026/03/20/claude-code-custom-commands-git-workflow-automation.html" rel="alternate" type="text/html" title="Claude Code 커스텀 명령어 가이드: Git 워크플로우 자동화" /><published>2026-03-20T15:00:00+00:00</published><updated>2026-03-20T15:00:00+00:00</updated><id>https://skysoo1111.github.io/devops/2026/03/20/claude-code-custom-commands-git-workflow-automation</id><content type="html" xml:base="https://skysoo1111.github.io/devops/2026/03/20/claude-code-custom-commands-git-workflow-automation.html"><![CDATA[<blockquote>
  <p>📖 예상 읽기 시간: 5분</p>
</blockquote>

<h2 id="개요">개요</h2>

<p>이 문서는 <code class="language-plaintext highlighter-rouge">~/.claude/commands</code>에 정의된 두 가지 커스텀 명령어인 <strong>commit.md</strong>와 <strong>push.md</strong>를 소개합니다. 이 명령어들은 Claude Code에서 <code class="language-plaintext highlighter-rouge">/commit</code>, <code class="language-plaintext highlighter-rouge">/push</code>로 호출되어 반복적인 Git 작업을 자동화합니다.</p>

<hr />

<h2 id="1-commitmd---스마트-커밋-자동화">1. commit.md - 스마트 커밋 자동화</h2>

<h3 id="기능-요약">기능 요약</h3>

<p><code class="language-plaintext highlighter-rouge">/commit</code> 명령어는 변경된 파일을 분석하고, 빌드를 검증한 후, 일관된 형식의 커밋 메시지를 자동 생성합니다.</p>

<h3 id="실행-흐름">실행 흐름</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status → 서브모듈 처리 → git diff 분석 → 파일 선택 확인 → 빌드 검증 → 커밋
</code></pre></div></div>

<h3 id="핵심-기능">핵심 기능</h3>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>변경사항 자동 분석</strong></td>
      <td><code class="language-plaintext highlighter-rouge">git status</code>와 <code class="language-plaintext highlighter-rouge">git diff</code>로 변경 내용을 파악</td>
    </tr>
    <tr>
      <td><strong>서브모듈 우선 처리</strong></td>
      <td>grpc-idl 같은 서브모듈이 있으면 먼저 커밋</td>
    </tr>
    <tr>
      <td><strong>설정 파일 필터링</strong></td>
      <td><code class="language-plaintext highlighter-rouge">.yml</code>, <code class="language-plaintext highlighter-rouge">.yaml</code>, <code class="language-plaintext highlighter-rouge">.xml</code>, <code class="language-plaintext highlighter-rouge">.log</code> 파일은 포함 여부를 사용자에게 확인</td>
    </tr>
    <tr>
      <td><strong>빌드 검증 필수</strong></td>
      <td><code class="language-plaintext highlighter-rouge">./gradlew compileKotlin compileTestKotlin</code> 성공 시에만 커밋 진행</td>
    </tr>
    <tr>
      <td><strong>커밋 메시지 자동 생성</strong></td>
      <td>브랜치명 기반으로 일관된 형식의 메시지 작성</td>
    </tr>
  </tbody>
</table>

<h3 id="커밋-메시지-형식">커밋 메시지 형식</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>feat({브랜치명}): 커밋내용 요약
- 상세 커밋 내용
</code></pre></div></div>

<p>브랜치명에서 prefix는 자동 제거됩니다.</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">feature/news</code> → <code class="language-plaintext highlighter-rouge">news</code></li>
  <li><code class="language-plaintext highlighter-rouge">bugfix/login-issue</code> → <code class="language-plaintext highlighter-rouge">login-issue</code>
<img src="https://velog.velcdn.com/images/skysoo/post/89e7689a-a0f4-4c57-a590-eaa6882f69d5/image.png" alt="" />
<img src="https://velog.velcdn.com/images/skysoo/post/c1e8acdf-3e48-4c59-a712-d61234146270/image.png" alt="" /></li>
</ul>

<h2 id="결과물">결과물</h2>
<p><img src="https://velog.velcdn.com/images/skysoo/post/bd7febc7-02ce-467f-87a5-6b83dba8aab9/image.png" alt="" /></p>

<h2 id="commitmd">commit.md</h2>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code>변경된 파일들을 분석하고 자동으로 커밋해줘:

<span class="gs">**중요**</span>: 4번과 7번 과정을 제외한 나머지 과정(1~3, 5~6)은 사용자에게 질문 없이 자동으로 진행
<span class="gs">**중요**</span>: TodoWrite 도구 사용 금지 - 진행 상황은 최종 결과 테이블로만 표시
<span class="p">
1.</span> git status로 변경/추가된 파일 확인
<span class="p">2.</span> 서브모듈(예: grpc-idl)에 변경 사항이 있으면 서브모듈 디렉토리에서 먼저 커밋
<span class="p">3.</span> git diff로 변경 내용 분석
<span class="p">4.</span> <span class="gs">**파일 선택 확인**</span>: 변경된 파일 중 <span class="sb">`.yml`</span>, <span class="sb">`.yaml`</span>, <span class="sb">`.xml`</span>, <span class="sb">`.log`</span> 파일이 있으면 커밋에 포함할지 사용자에게 질문
<span class="p">5.</span> <span class="gs">**빌드 확인 (필수)**</span>: <span class="sb">`./gradlew compileKotlin compileTestKotlin`</span> 실행하여 빌드 성공 확인
<span class="p">   -</span> 빌드 실패 시 오류를 먼저 수정하고 다시 빌드 확인
<span class="p">6.</span> 빌드 성공 후 변경 내용을 기반으로 명확한 커밋 메시지 작성
<span class="p">7.</span> git add 및 git commit 실행 (서브모듈 포인터 포함, 4번에서 제외한 파일은 커밋에서 제외)

브랜치명은 <span class="sb">`git branch --show-current`</span>로 확인하고, <span class="sb">`feature/`</span>, <span class="sb">`bugfix/`</span>, <span class="sb">`hotfix/`</span> 등의 prefix는 제외한 실제 이름만 사용해서 아래 커밋 메세지 형식을 토대로 커밋 메세지를 작성해줘.
예: <span class="sb">`feature/news`</span> → <span class="sb">`news`</span>, <span class="sb">`bugfix/login-issue`</span> → <span class="sb">`login-issue`</span>

커밋 메시지 형식:
</code></pre></div></div>
<p>feat({브랜치명}): 커밋내용 요약</p>
<ul>
  <li>상세 커밋 내용
```</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&gt; subModule이 없는 프로젝트라면 2번 과정은 제외하는 것이 깔끔하다

---

## 2. push.md - 다중 브랜치 배포 자동화

### 기능 요약

`/push {브랜치명}` 명령어는 작업 브랜치를 dev, qa, qc 환경 브랜치에 순차적으로 머지하고 푸시합니다.

### 실행 흐름

</code></pre></div></div>
<p>브랜치 확인 → 작업 브랜치 푸시 → dev 머지/빌드/푸시 → qa 머지/빌드/푸시 → qc 머지/빌드/푸시 → 원래 브랜치 복귀</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
### 핵심 기능

| 기능 | 설명 |
|------|------|
| **브랜치 자동 감지** | 인자 미지정 시 현재 브랜치 사용 여부 확인 |
| **순차적 환경 배포** | dev → qa → qc 순서로 안전하게 진행 |
| **각 단계별 빌드 검증** | 머지 후 빌드 실패 시 자동 롤백 |
| **충돌 감지 및 알림** | 머지 충돌 발생 시 작업 중단 및 사용자 알림 |
| **진행 상황 시각화** | ✅ 성공 / ❌ 실패 / 🔄 진행 중 표시 |

### 안전장치

- 푸시할 커밋이 없으면 작업 즉시 종료
- 빌드 실패 시 `git reset --hard`로 롤백
- 충돌 시 수동 해결 요청

---

## 3. 왜 Git 워크플로우 자동화가 필요한가?

### 수동 작업의 문제점

개발자가 하루에 수행하는 Git 작업을 생각해보면, 커밋 한 번에 5~10분, 다중 브랜치 배포에 15~30분이 소요됩니다. 이 과정에서 여러 문제가 발생합니다.

**휴먼 에러 위험**: 빌드 확인 없이 커밋, 잘못된 브랜치에 푸시, 설정 파일 실수로 포함 등의 실수가 빈번합니다.

**일관성 부재**: 팀원마다 다른 커밋 메시지 스타일, 배포 순서 혼란 등이 발생합니다.

**컨텍스트 스위칭 비용**: 코딩 → Git 작업 → 코딩으로 돌아오는 과정에서 집중력이 분산됩니다.

### 자동화의 가치

| 측면 | 수동 작업 | 자동화 후 |
|------|----------|----------|
| **커밋 소요 시간** | 5~10분 | 1~2분 |
| **다중 브랜치 배포** | 15~30분 | 3~5분 |
| **빌드 검증 누락** | 가끔 발생 | 0% (강제 검증) |
| **커밋 메시지 일관성** | 팀원마다 상이 | 100% 통일 |

---

## 4. 이 명령어들의 장점

### 품질 보장

빌드 검증이 워크플로우에 내장되어 있어 컴파일 에러가 있는 코드가 커밋되거나 배포되는 것을 원천 차단합니다. 이는 CI/CD 파이프라인 실패를 사전에 예방합니다.

### 운영 안정성

push.md는 dev → qa → qc 순서를 강제하고, 각 단계마다 빌드를 검증합니다. 실패 시 자동 롤백되므로 환경이 오염되지 않습니다.

### 개발자 경험 향상

반복적이고 지루한 작업을 AI에게 위임하여 개발자는 비즈니스 로직에 집중할 수 있습니다. 특히 서브모듈 처리나 다중 브랜치 배포 같은 복잡한 작업에서 효과가 큽니다.

### 팀 표준화

커밋 메시지 형식과 배포 절차가 명령어에 정의되어 있어 팀 전체가 동일한 방식으로 작업하게 됩니다. 신규 팀원 온보딩 시간도 단축됩니다.
![](https://velog.velcdn.com/images/skysoo/post/6ebb49e5-e9ca-48e6-a350-1b220e3db387/image.png)
![](https://velog.velcdn.com/images/skysoo/post/c216caf1-ca6c-4f15-9f5b-d8b22b9bafe0/image.png)
![](https://velog.velcdn.com/images/skysoo/post/9f2a266e-a114-4814-a65e-88f328fe8aac/image.png)

## push.md
```markdown
브랜치를 dev, qa, qc에 머지하고 푸시해줘:

**사용법**: /push {브랜치명}
**예시**: /push feature/news

**주의**: news 레포지토리만 push 대상 (grpc-idl 서브모듈 제외)

**중요**: 0단계(브랜치 확인)를 제외한 나머지 과정(1~3단계)은 사용자에게 질문 없이 자동으로 진행
**중요**: TodoWrite 도구 사용 금지 - 진행 상황은 최종 결과 테이블로만 표시

## 실행 순서

### 0단계: 브랜치 확인 (ARGUMENTS가 비어있는 경우)
`$ARGUMENTS`가 비어있거나 지정되지 않은 경우:
1. `git branch --show-current`로 현재 브랜치 확인
2. AskUserQuestion 도구를 사용하여 사용자에게 질문:
   - 질문: "브랜치가 지정되지 않았습니다. 현재 브랜치 '{현재브랜치명}'를 사용하시겠습니까?"
   - 옵션: "예, 현재 브랜치 사용" / "아니오, 취소"
3. 사용자가 승인하면 현재 브랜치를 `$ARGUMENTS`로 사용
4. 사용자가 거부하면 작업 중단

### 1단계: 작업 브랜치 변경사항 확인 및 푸시
1. `$ARGUMENTS` 브랜치로 체크아웃
2. `git log origin/$ARGUMENTS..$ARGUMENTS`로 원격에 푸시되지 않은 커밋 확인
3. **푸시할 커밋이 있는 경우**:
</code></pre></div></div>
<p>git push origin $ARGUMENTS</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>4. **푸시할 커밋이 없는 경우** (원격과 동일):
   - "작업 브랜치에 푸시할 변경사항이 없습니다. 작업을 종료합니다." 메시지 출력
   - **작업 즉시 종료** (2단계로 진행하지 않음)

### 2단계: dev, qa, qc 브랜치 순차 처리
각 브랜치(dev → qa → qc)에 대해 아래 작업 수행:

1. **브랜치 업데이트**
</code></pre></div></div>
<p>git checkout {브랜치}
   git fetch origin {브랜치}
   git reset –hard origin/{브랜치}</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
2. **머지 수행**
</code></pre></div></div>
<p>git merge $ARGUMENTS –no-edit</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   - 충돌 발생 시 사용자에게 알리고 중단

3. **빌드 확인 (필수)**
</code></pre></div></div>
<p>./gradlew compileKotlin compileTestKotlin</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   - 빌드 실패 시 해당 브랜치 작업 중단하고 사용자에게 알림
   - `git merge --abort` 또는 `git reset --hard origin/{브랜치}`로 롤백

4. **푸시**
</code></pre></div></div>
<p>git push origin {브랜치}</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
### 3단계: 원래 브랜치로 복귀
</code></pre></div></div>
<p>git checkout $ARGUMENTS</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
## 에러 처리
- 머지 충돌: 충돌 내용 표시하고 수동 해결 요청
- 빌드 실패: 해당 브랜치 롤백 후 다음 브랜치로 자동 진행
- 푸시 실패: 원인 분석 후 사용자에게 알림


## 진행 상황 표시
각 단계마다 진행 상황을 명확히 표시:
- ✅ 성공한 브랜치
- ❌ 실패한 브랜치
- 🔄 진행 중인 브랜치
</code></pre></div></div>

<hr />

<h2 id="5-개선-방향-제안">5. 개선 방향 제안</h2>

<h3 id="단기-개선-사항">단기 개선 사항</h3>

<p><strong>테스트 실행 옵션 추가</strong>: 현재는 컴파일만 검증하지만, 단위 테스트 실행 옵션을 추가하면 품질을 더 높일 수 있습니다.</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># 제안: commit.md에 추가</span>
5-1. (선택) <span class="sb">`./gradlew test`</span> 실행 - 사용자 옵션으로 제공
</code></pre></div></div>

<p><strong>커밋 타입 다양화</strong>: 현재 <code class="language-plaintext highlighter-rouge">feat</code>만 사용하는데, 변경 내용에 따라 <code class="language-plaintext highlighter-rouge">fix</code>, <code class="language-plaintext highlighter-rouge">refactor</code>, <code class="language-plaintext highlighter-rouge">docs</code> 등을 자동 판별하면 좋겠습니다.</p>

<p><strong>Dry-run 모드</strong>: 실제 실행 전에 어떤 작업이 수행될지 미리 보여주는 모드가 있으면 안전성이 높아집니다.</p>

<h3 id="중기-개선-사항">중기 개선 사항</h3>

<p><strong>선택적 브랜치 배포</strong>: push.md에서 dev, qa, qc 중 원하는 브랜치만 선택할 수 있는 옵션이 필요합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/push feature/news --only dev,qa
</code></pre></div></div>

<p><strong>롤백 명령어 추가</strong>: 배포 후 문제 발생 시 빠르게 롤백할 수 있는 <code class="language-plaintext highlighter-rouge">/rollback</code> 명령어를 고려해볼 수 있습니다.</p>

<p><strong>Slack/Teams 알림 연동</strong>: 배포 완료나 실패 시 팀 채널에 자동 알림을 보내면 협업 효율이 높아집니다.</p>

<h3 id="장기-개선-사항">장기 개선 사항</h3>

<p><strong>PR 자동 생성 연동</strong>: 커밋 후 GitHub/GitLab PR을 자동으로 생성하는 <code class="language-plaintext highlighter-rouge">/pr</code> 명령어와의 연계를 고려할 수 있습니다.</p>

<p><strong>배포 이력 관리</strong>: 언제 어떤 브랜치가 어디에 배포되었는지 로그를 남기면 트러블슈팅에 도움이 됩니다.</p>

<p><strong>커스텀 훅 지원</strong>: 팀별로 특수한 요구사항(예: 특정 파일 변경 시 추가 검증)을 플러그인 형태로 추가할 수 있는 구조면 확장성이 높아집니다.</p>

<hr />

<h2 id="6-활용-팁">6. 활용 팁</h2>

<h3 id="commitmd-활용">commit.md 활용</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 일반적인 사용</span>
/commit

<span class="c"># 설정 파일이 많이 변경된 경우</span>
<span class="c"># → 자동으로 .yml, .yaml 파일 포함 여부를 물어봄</span>
</code></pre></div></div>

<h3 id="pushmd-활용">push.md 활용</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 브랜치 지정</span>
/push feature/test

<span class="c"># 현재 브랜치 사용 (확인 질문 표시)</span>
/push
</code></pre></div></div>

<h3 id="권장-워크플로우">권장 워크플로우</h3>

<ol>
  <li>기능 개발 완료</li>
  <li><code class="language-plaintext highlighter-rouge">/commit</code> 실행 → 빌드 검증 + 커밋</li>
  <li><code class="language-plaintext highlighter-rouge">/push</code> 실행 → dev/qa/qc 순차 배포</li>
  <li>진행 상황 테이블로 결과 확인</li>
</ol>

<hr />

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

<p>commit.md와 push.md는 반복적인 Git 작업을 자동화하여 개발 생산성을 높이고, 빌드 검증을 강제하여 품질을 보장합니다. 특히 다중 환경 브랜치를 운영하는 팀에서 배포 실수를 줄이는 데 큰 도움이 됩니다.</p>

<p>이 명령어들은 시작점일 뿐입니다. 팀의 워크플로우에 맞게 지속적으로 개선하면서, 더 많은 반복 작업을 자동화해 나가시기 바랍니다.</p>]]></content><author><name></name></author><category term="DevOps" /><category term="claude" /><category term="claude-code" /><category term="claude-code-커스텀" /><category term="commands" /><category term="commit" /><category term="custom-commands" /><category term="git" /><category term="git-워크플로우-자동화" /><category term="push" /><category term="자동화" /><summary type="html"><![CDATA[📖 예상 읽기 시간: 5분]]></summary></entry><entry><title type="html">Webflux + Coroutine 에서의 Scope 동작</title><link href="https://skysoo1111.github.io/kotlin/2026/03/20/webflux-coroutine-scope-behavior.html" rel="alternate" type="text/html" title="Webflux + Coroutine 에서의 Scope 동작" /><published>2026-03-20T15:00:00+00:00</published><updated>2026-03-20T15:00:00+00:00</updated><id>https://skysoo1111.github.io/kotlin/2026/03/20/webflux-coroutine-scope-behavior</id><content type="html" xml:base="https://skysoo1111.github.io/kotlin/2026/03/20/webflux-coroutine-scope-behavior.html"><![CDATA[<blockquote>
  <p>원문: <a href="https://velog.io/@skysoo/Webflux-Coroutine-%EC%97%90%EC%84%9C%EC%9D%98-Scope-%EB%8F%99%EC%9E%91">https://velog.io/@skysoo/Webflux-Coroutine-%EC%97%90%EC%84%9C%EC%9D%98-Scope-%EB%8F%99%EC%9E%91</a></p>
</blockquote>

<h1 id="coroutine-scope">coroutine scope</h1>
<p>coroutine scope란 위에서 언급했 듯이 coroutine context를 포함한 coroutine을 실행하는 범위를 뜻한다.</p>

<h2 id="의문점-webflux--kotlin-coroutine-같이-사용하면-coroutine-scope는-어떻게-될까">의문점: Webflux + kotlin coroutine 같이 사용하면 coroutine scope는 어떻게 될까?</h2>
<p>springboot + kotlin 프로젝트로 구성도 많이하고 webflux + coroutine은 자연스런 구성일 것이다.
근데 개발하면서 coroutine scope를 별도로 지정해서 사용하지 않았는데, 위 구성시에는 내부적으로 어떻게 구성될까?</p>
<ul>
  <li>Webflux는 스레드풀이 아니라 NIO 이벤트 루프 기반 동작인데, kotlin은 기본적으로 스레드풀 위에서 동작한다.</li>
  <li>조금 더 깊게 들어가면 둘 다 스레드를 효율적으로 사용하기 위함이지만 webflux는 java.nio를 직접 다루고 kotlin은 java.io에 의존한다.</li>
</ul>

<p>그럼 다시 본론으로 돌아가서 webflux + coroutine을 같이 사용하면 coroutine scope는 어떻게 되나?</p>
<ul>
  <li>Spring이 요청 단위로 CoroutineScope를 제공하고 Dispatcher는 Reactor 이벤트 루프 전용 Dispatcher를 사용한다.</li>
  <li>즉 kotlin의 전용 Dispatcher가 아닌 Reactor의 Scheduler가 만든 전용 Dispatcher 위에서 동작한다.</li>
  <li>이유는 위에서 언급한 바와 같이 kotlin은 스레드풀 위에서 동작하고 Reactor는 netty 이벤트 루프를 이용하므로 그 근간부터 다르다.
    <blockquote>
      <p>val reactorDispatcher = Schedulers.parallel().asCoroutineDispatcher()</p>
    </blockquote>
  </li>
</ul>

<p>만약 reactor를 kotlin의 Dispatcher를 사용한다면 오히려 컨텍스트 단절이 생기면서 비효율적일 것이다.</p>

<table>
  <thead>
    <tr>
      <th>Dispatcher</th>
      <th>소속</th>
      <th>스레드 종류</th>
      <th>언제 쓰나</th>
      <th>예시 Thread 이름</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Dispatchers.Default</code></td>
      <td>Kotlin 표준</td>
      <td>CPU-bound 풀</td>
      <td>CPU 연산</td>
      <td><code class="language-plaintext highlighter-rouge">DefaultDispatcher-worker-1</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Dispatchers.IO</code></td>
      <td>Kotlin 표준</td>
      <td>I/O용 풀 (확장형)</td>
      <td>블로킹 I/O</td>
      <td><code class="language-plaintext highlighter-rouge">DefaultDispatcher-worker-5</code></td>
    </tr>
    <tr>
      <td><strong>Reactor Dispatcher</strong> <em>(Spring WebFlux 내부)</em></td>
      <td>Reactor (Netty 기반)</td>
      <td>이벤트 루프</td>
      <td>Non-blocking 요청 처리</td>
      <td><code class="language-plaintext highlighter-rouge">reactor-http-nio-2</code></td>
    </tr>
  </tbody>
</table>

<h2 id="내부-동작">내부 동작</h2>
<p>[HTTP 요청] → Reactor pipeline 시작
   ↓
Reactor Context -&gt; CoroutineContext (bridge)
   ↓
suspend 함수 실행 (실제 스레드는 reactor-http-nio-#)
   ↓
suspend -&gt; resume 시점도 동일 이벤트 루프에서 스케줄됨</p>

<h2 id="coroutine-suspend-시점에-netty-scheduler-동작">coroutine suspend 시점에 Netty Scheduler 동작</h2>
<p>suspend(함수 일시중단)의 기능을 kotlin은 coroutination이 reactor는 scheduler가 대신한다.
| 시점             | 이벤트 루프 스레드 상태          | 코루틴 상태      |
| ————– | ———————- | ———– |
| <code class="language-plaintext highlighter-rouge">suspend</code> 진입 전 | 코루틴 실행 중               | Active      |
| <code class="language-plaintext highlighter-rouge">suspend</code> 시    | subscribe 등록 → 스레드 반환  | Suspended   |
| I/O 완료 이벤트 수신  | Reactor가 resume 요청 스케줄 | Resuming    |
| resume 이후      | 다시 같은 Reactor 스레드에서 실행 | Active (재개) |</p>

<h1 id="결론">결론</h1>
<p>webflux + coroutine을 사용하여 내부적으로 suspend 함수를 사용하더라도 CoroutineDispatcher를 감싼 형태의 Reactor 전용 dispatcher를 사용하고 그 근간은 netty의 이벤트 루프를 사용하여 동작된다.</p>]]></content><author><name></name></author><category term="Kotlin" /><category term="kotlin" /><category term="coroutine" /><category term="webflux" /><category term="spring" /><category term="reactor" /><category term="netty" /><summary type="html"><![CDATA[원문: https://velog.io/@skysoo/Webflux-Coroutine-%EC%97%90%EC%84%9C%EC%9D%98-Scope-%EB%8F%99%EC%9E%91]]></summary></entry><entry><title type="html">LangChain Deep Agents 완전 가이드 - 계획하고 기억하고 위임하는 AI 에이전트의 등장</title><link href="https://skysoo1111.github.io/ai/2026/03/20/langchain-deep-agents-guide.html" rel="alternate" type="text/html" title="LangChain Deep Agents 완전 가이드 - 계획하고 기억하고 위임하는 AI 에이전트의 등장" /><published>2026-03-20T01:00:00+00:00</published><updated>2026-03-20T01:00:00+00:00</updated><id>https://skysoo1111.github.io/ai/2026/03/20/langchain-deep-agents-guide</id><content type="html" xml:base="https://skysoo1111.github.io/ai/2026/03/20/langchain-deep-agents-guide.html"><![CDATA[<h2 id="개요">개요</h2>

<p>LangChain이 Deep Agents를 공식 릴리스했다. 단순한 도구 호출 루프를 넘어, 스스로 계획을 세우고 컨텍스트를 관리하며 서브에이전트에게 작업을 위임하는 구조화된 에이전트 런타임이다. GitHub 공개 5시간 만에 9.9k 스타를 달성할 만큼 커뮤니티의 반응이 뜨거운데, 왜 이렇게 주목받는지 핵심 아키텍처와 실전 코드를 함께 살펴본다.</p>

<h2 id="기존-에이전트의-한계와-deep-agents의-해법">기존 에이전트의 한계와 Deep Agents의 해법</h2>

<p>기존 LLM 에이전트의 근본적인 문제는 세 가지로 압축된다. 복잡한 작업을 체계적으로 분해하지 못하고, 긴 대화에서 컨텍스트가 넘쳐 중요한 정보를 잃어버리며, 하나의 에이전트가 모든 것을 처리하려다 품질이 떨어진다.</p>

<p>Deep Agents는 이 문제를 4가지 아키텍처 컴포넌트로 해결한다.</p>

<ul>
  <li><strong>상세 시스템 프롬프트</strong>: Claude Code의 프롬프트 설계 방식을 참고한 정교한 기본 프롬프트</li>
  <li><strong>계획 도구(write_todos)</strong>: 복잡한 태스크를 단계별로 분해하는 todo list 기반 계획</li>
  <li><strong>서브에이전트 생성</strong>: 독립된 컨텍스트에서 하위 작업을 수행하는 에이전트 위임</li>
  <li><strong>가상 파일시스템</strong>: 대용량 컨텍스트를 파일로 저장하고 필요할 때만 읽어오는 메모리 관리</li>
</ul>

<p>핵심은 <strong>컨텍스트 격리</strong>다. 서브에이전트가 별도의 컨텍스트 윈도우에서 실행되므로, 메인 에이전트의 컨텍스트가 오염되지 않는다. 연구, 코딩, 분석처럼 수십 단계가 필요한 장기 자율 작업에서 이 설계가 결정적인 차이를 만든다.</p>

<h2 id="5분-만에-시작하는-deep-agents-실전-코드">5분 만에 시작하는 Deep Agents 실전 코드</h2>

<p><code class="language-plaintext highlighter-rouge">pip install deepagents</code>로 바로 설치할 수 있다. LangGraph 위에 구축되어 있어 기존 LangChain 생태계와 자연스럽게 호환된다.</p>

<p><strong>Before: 기존 LangChain 에이전트</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">langchain_anthropic</span> <span class="kn">import</span> <span class="n">ChatAnthropic</span>
<span class="kn">from</span> <span class="nn">langgraph.prebuilt</span> <span class="kn">import</span> <span class="n">create_react_agent</span>

<span class="n">llm</span> <span class="o">=</span> <span class="n">ChatAnthropic</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s">"claude-sonnet-4-5-20250514"</span><span class="p">)</span>
<span class="n">agent</span> <span class="o">=</span> <span class="n">create_react_agent</span><span class="p">(</span><span class="n">llm</span><span class="p">,</span> <span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="n">search_tool</span><span class="p">,</span> <span class="n">write_tool</span><span class="p">])</span>

<span class="c1"># 단순 도구 호출 루프 - 복잡한 작업에서 컨텍스트 유실 발생
</span><span class="n">result</span> <span class="o">=</span> <span class="n">agent</span><span class="p">.</span><span class="n">invoke</span><span class="p">({</span><span class="s">"messages"</span><span class="p">:</span> <span class="p">[(</span><span class="s">"user"</span><span class="p">,</span> <span class="s">"3개 기술 블로그의 최신 트렌드를 분석해서 보고서를 작성해"</span><span class="p">)]})</span>
</code></pre></div></div>

<p><strong>After: Deep Agents</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">deepagents</span> <span class="kn">import</span> <span class="n">create_deep_agent</span>
<span class="kn">from</span> <span class="nn">langchain_anthropic</span> <span class="kn">import</span> <span class="n">ChatAnthropic</span>

<span class="n">llm</span> <span class="o">=</span> <span class="n">ChatAnthropic</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s">"claude-sonnet-4-5-20250514"</span><span class="p">)</span>

<span class="n">agent</span> <span class="o">=</span> <span class="n">create_deep_agent</span><span class="p">(</span>
    <span class="n">llm</span><span class="p">,</span>
    <span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="n">search_tool</span><span class="p">,</span> <span class="n">write_tool</span><span class="p">],</span>
    <span class="c1"># 서브에이전트 생성, todo 계획, 파일시스템 접근이 자동 포함
</span><span class="p">)</span>

<span class="c1"># 에이전트가 스스로 계획을 세우고, 서브에이전트에게 각 블로그 분석을 위임
</span><span class="n">result</span> <span class="o">=</span> <span class="n">agent</span><span class="p">.</span><span class="n">invoke</span><span class="p">({</span><span class="s">"messages"</span><span class="p">:</span> <span class="p">[(</span><span class="s">"user"</span><span class="p">,</span> <span class="s">"3개 기술 블로그의 최신 트렌드를 분석해서 보고서를 작성해"</span><span class="p">)]})</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">create_deep_agent()</code>가 반환하는 것은 컴파일된 LangGraph 그래프다. 즉, 도구 호출을 지원하는 모든 LLM과 호환되고, 기존 LangGraph 워크플로우에 그대로 통합할 수 있다.</p>

<p>서브에이전트에 커스텀 도구를 추가하고 싶다면 다음과 같이 설정한다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">agent</span> <span class="o">=</span> <span class="n">create_deep_agent</span><span class="p">(</span>
    <span class="n">llm</span><span class="p">,</span>
    <span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="n">search_tool</span><span class="p">,</span> <span class="n">write_tool</span><span class="p">],</span>
    <span class="n">sub_agent_tools</span><span class="o">=</span><span class="p">[</span><span class="n">code_executor</span><span class="p">,</span> <span class="n">db_query_tool</span><span class="p">],</span>
    <span class="n">max_sub_agents</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span>
    <span class="n">filesystem_enabled</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
<span class="p">)</span>

<span class="c1"># 스트리밍으로 에이전트의 계획 수립 과정을 실시간 관찰
</span><span class="k">for</span> <span class="n">event</span> <span class="ow">in</span> <span class="n">agent</span><span class="p">.</span><span class="n">stream</span><span class="p">({</span><span class="s">"messages"</span><span class="p">:</span> <span class="p">[(</span><span class="s">"user"</span><span class="p">,</span> <span class="s">"프로젝트 코드베이스를 분석하고 리팩토링 계획을 작성해"</span><span class="p">)]}):</span>
    <span class="k">print</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="nvidia-파트너십과-엔터프라이즈-확장">NVIDIA 파트너십과 엔터프라이즈 확장</h2>

<p>LangChain은 Deep Agents 릴리스와 함께 NVIDIA와의 전략적 파트너십도 발표했다. 핵심 결과물인 <strong>AI-Q Blueprint</strong>는 Deep Agents 위에 구축된 프로덕션급 딥 리서치 시스템으로, 딥 리서치 벤치마크 1위를 달성했다.</p>

<p>이 파트너십이 시사하는 점은 명확하다. Deep Agents가 단순한 오픈소스 실험이 아니라, 엔터프라이즈 프로덕션을 목표로 설계되었다는 것이다. LangGraph 런타임의 상태 관리, Deep Agents의 태스크 계획과 서브에이전트 기능, NVIDIA의 병렬 실행 인프라가 결합되면서, 프로토타입에서 프로덕션까지의 간극이 좁혀지고 있다.</p>

<p>성능 면에서도 주목할 만하다. Claude Sonnet 4.5 기반 Deep Agents가 Terminal Bench 2.0에서 <strong>42.65%</strong>를 기록했는데, 이는 동일 모델의 Claude Code와 동등한 수준이다. 오픈소스 프레임워크가 상용 제품과 대등한 성능을 보여준 셈이다.</p>

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

<ul>
  <li><strong>Deep Agents는 계획-메모리-위임의 세 축</strong>으로 기존 도구 호출 에이전트의 근본적 한계를 해결한다.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">create_deep_agent()</code> 한 줄</strong>로 시작할 수 있고, LangGraph와 완전 호환되어 기존 코드를 크게 바꿀 필요가 없다.</li>
  <li><strong>컨텍스트 격리</strong>가 핵심 설계 원칙이다. 서브에이전트가 독립된 컨텍스트에서 동작하므로 장기 작업에서도 품질이 유지된다.</li>
  <li><strong>NVIDIA 파트너십</strong>으로 엔터프라이즈 프로덕션 경로가 열렸고, 벤치마크에서 상용 도구급 성능을 입증했다.</li>
  <li>MIT 라이선스 오픈소스이므로 지금 바로 <code class="language-plaintext highlighter-rouge">pip install deepagents</code>로 시작해볼 수 있다.</li>
</ul>

<h2 id="참고-자료">참고 자료</h2>

<ul>
  <li><a href="https://blog.langchain.com/deep-agents/">Deep Agents - LangChain 공식 블로그</a></li>
  <li><a href="https://www.marktechpost.com/2026/03/15/langchain-releases-deep-agents-a-structured-runtime-for-planning-memory-and-context-isolation-in-multi-step-ai-agents/">LangChain Releases Deep Agents - MarkTechPost</a></li>
  <li><a href="https://github.com/langchain-ai/deepagents">langchain-ai/deepagents - GitHub</a></li>
  <li><a href="https://blog.langchain.com/nvidia-enterprise/">LangChain x NVIDIA Enterprise Agentic AI Platform</a></li>
  <li><a href="https://awesomeagents.ai/news/langchain-deep-agents-release/">LangChain Deep Agents Release - Awesome Agents</a></li>
</ul>]]></content><author><name></name></author><category term="AI" /><category term="langchain" /><category term="deep-agents" /><category term="langgraph" /><category term="ai-agent" /><category term="python" /><category term="nvidia" /><summary type="html"><![CDATA[개요]]></summary></entry><entry><title type="html">tmux를 활용한 Claude Code 사용법 - 터미널 멀티플렉서로 AI 코딩 극대화하기</title><link href="https://skysoo1111.github.io/dev%20tools/2026/03/19/tmux-claude-code-usage-guide.html" rel="alternate" type="text/html" title="tmux를 활용한 Claude Code 사용법 - 터미널 멀티플렉서로 AI 코딩 극대화하기" /><published>2026-03-19T06:30:00+00:00</published><updated>2026-03-19T06:30:00+00:00</updated><id>https://skysoo1111.github.io/dev%20tools/2026/03/19/tmux-claude-code-usage-guide</id><content type="html" xml:base="https://skysoo1111.github.io/dev%20tools/2026/03/19/tmux-claude-code-usage-guide.html"><![CDATA[<h2 id="개요">개요</h2>

<p>Claude Code는 터미널에서 동작하는 AI 코딩 에이전트다. 그런데 하나의 터미널 세션에서 하나의 에이전트만 돌리고 있다면, Claude Code의 잠재력을 절반도 쓰지 못하고 있는 셈이다. tmux와 결합하면 여러 에이전트를 병렬로 실행하고, 세션을 영구 유지하며, 원격에서도 작업을 이어갈 수 있다. 이 글에서는 tmux로 Claude Code 활용도를 극대화하는 다섯 가지 접근법을 정리한다.</p>

<h2 id="1-agent-teams-공식-지원되는-멀티-에이전트-협업">1. Agent Teams: 공식 지원되는 멀티 에이전트 협업</h2>

<p>Anthropic은 공식 문서에서 Claude Code의 <strong>Agent Teams</strong> 기능을 안내하고 있다. 핵심은 <code class="language-plaintext highlighter-rouge">teammateMode</code> 설정을 <code class="language-plaintext highlighter-rouge">tmux</code>로 지정하는 것이다. 이렇게 하면 각 에이전트가 tmux의 별도 pane에서 실행되어, 한 화면에서 여러 에이전트의 작업 상황을 실시간으로 모니터링할 수 있다.</p>

<p>예를 들어 한 에이전트는 백엔드 API를 구현하고, 다른 에이전트는 프론트엔드 컴포넌트를 작성하며, 세 번째 에이전트는 테스트 코드를 생성하는 식이다. split-pane 모드로 시각적으로 배치하면 각 에이전트의 진행 상황을 한눈에 파악할 수 있어 오케스트레이션이 훨씬 수월해진다. 단순히 여러 터미널 탭을 여는 것과는 차원이 다른 구조화된 협업이 가능하다.</p>

<h2 id="2-영구-세션으로-작업-맥락-유지하기">2. 영구 세션으로 작업 맥락 유지하기</h2>

<p><a href="https://www.devas.life/how-to-run-claude-code-in-a-tmux-popup-window-with-persistent-sessions/">devas.life의 가이드</a>에서 소개하는 방법은 실용적이면서도 우아하다. 작업 디렉토리를 MD5 해싱하여 폴더별로 고유한 tmux 세션을 자동 생성하고, <code class="language-plaintext highlighter-rouge">display-popup</code>과 <code class="language-plaintext highlighter-rouge">attach-session</code>을 결합해 팝업 창 형태로 Claude Code를 띄운다.</p>

<p>이 접근법의 장점은 프로젝트마다 독립된 세션이 유지된다는 것이다. A 프로젝트에서 작업하다가 B 프로젝트로 전환한 뒤 다시 A로 돌아와도, Claude Code가 이전 대화 맥락을 그대로 갖고 있다. 팝업 형태이므로 필요할 때만 불러오고 닫을 수 있어 화면 공간도 효율적으로 사용할 수 있다. 여러 프로젝트를 동시에 관리하는 개발자에게 특히 유용하다.</p>

<h2 id="3-vps--tmux로-어디서든-접속-가능한-ai-코딩-환경">3. VPS + tmux로 어디서든 접속 가능한 AI 코딩 환경</h2>

<p>로컬 머신에 종속되지 않는 환경이 필요하다면, VPS에서 Claude Code를 tmux와 함께 운용하는 방법이 있다. <a href="https://medium.com/@0xmega/claude-code-on-a-vps-the-complete-setup-security-tmux-mobile-access-2d214f5a0b3b">Medium의 가이드</a>는 세션 생성부터 detach/attach 워크플로우, 그리고 <strong>Tailscale + Termius</strong> 조합으로 모바일에서도 접근하는 방법까지 다룬다.</p>

<p>이 구성의 진짜 가치는 <strong>연속성</strong>에 있다. 사무실에서 시작한 Claude Code 세션을 퇴근길 스마트폰에서 확인하고, 집에서 노트북으로 이어받을 수 있다. tmux 세션은 SSH 연결이 끊어져도 서버에서 계속 살아있으므로, 장시간 걸리는 리팩토링이나 마이그레이션 작업을 맡겨두고 나중에 결과를 확인하는 것도 가능하다.</p>

<h2 id="4-amux-수십-개-에이전트를-병렬로-무인-실행">4. amux: 수십 개 에이전트를 병렬로 무인 실행</h2>

<p>개인 프로젝트 수준을 넘어 대규모 병렬 실행이 필요하다면, <a href="https://github.com/mixpeek/amux">amux</a>를 주목할 만하다. tmux 기반으로 수십 개의 Claude Code 에이전트를 동시에 실행하는 오픈소스 멀티플렉서다.</p>

<p>웹 대시보드로 전체 에이전트 상태를 한눈에 파악할 수 있고, 에이전트가 중단되면 자동으로 재시작하는 자가 치유(self-healing) 감시 기능도 갖추고 있다. REST API를 통한 오케스트레이션도 지원하므로, CI/CD 파이프라인이나 자동화 스크립트와 연동하기에 좋다. 모노레포에서 여러 모듈을 동시에 수정하거나, 대량의 반복적 코드 변환 작업을 처리할 때 위력을 발휘한다.</p>

<h2 id="5-claude-tmux-세션-관리에-특화된-tui-도구">5. claude-tmux: 세션 관리에 특화된 TUI 도구</h2>

<p><a href="https://github.com/nielsgroen/claude-tmux">claude-tmux</a>는 tmux 내에서 여러 Claude Code 세션을 효율적으로 관리하는 데 초점을 맞춘 도구다. TUI(Terminal User Interface)로 세션 상태를 확인하고, 퍼지 필터링 검색으로 원하는 세션을 빠르게 찾을 수 있다.</p>

<p>특히 <strong>git worktree</strong> 지원이 돋보인다. 브랜치마다 별도의 worktree를 만들고 각각에 Claude Code 세션을 붙이면, 여러 기능 브랜치를 동시에 개발하면서도 서로 간섭 없이 작업할 수 있다. 코드 리뷰를 하면서 동시에 다른 브랜치에서 개발을 진행하는 워크플로우가 자연스럽게 가능해진다.</p>

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

<ul>
  <li><strong>Agent Teams</strong>(공식 기능)로 여러 에이전트를 split-pane에 배치하면 구조화된 멀티 에이전트 협업이 가능하다.</li>
  <li><strong>영구 세션 + 팝업</strong> 패턴으로 프로젝트별 대화 맥락을 유지하면서 화면 공간을 절약할 수 있다.</li>
  <li><strong>VPS + tmux</strong> 조합은 장소에 구애받지 않는 연속적인 AI 코딩 환경을 제공한다.</li>
  <li><strong>amux</strong>는 대규모 병렬 에이전트 실행과 자동화에, <strong>claude-tmux</strong>는 세션 관리와 git worktree 기반 브랜치별 작업에 각각 강점이 있다.</li>
  <li>tmux는 단순한 터미널 분할 도구가 아니라, Claude Code의 활용 범위를 근본적으로 확장하는 인프라다.</li>
</ul>

<h2 id="참고-자료">참고 자료</h2>

<ul>
  <li><a href="https://code.claude.com/docs/en/agent-teams">Orchestrate teams of Claude Code sessions - Anthropic 공식 문서</a></li>
  <li><a href="https://www.devas.life/how-to-run-claude-code-in-a-tmux-popup-window-with-persistent-sessions/">How to run Claude Code in a Tmux popup window with persistent sessions - devas.life</a></li>
  <li><a href="https://medium.com/@0xmega/claude-code-on-a-vps-the-complete-setup-security-tmux-mobile-access-2d214f5a0b3b">Claude Code On a VPS: The Complete Setup - Medium</a></li>
  <li><a href="https://github.com/mixpeek/amux">amux - Claude Code agent multiplexer - GitHub</a></li>
  <li><a href="https://github.com/nielsgroen/claude-tmux">claude-tmux: Manage Claude Code within tmux - GitHub</a></li>
</ul>]]></content><author><name></name></author><category term="Dev Tools" /><category term="tmux" /><category term="claude-code" /><category term="productivity" /><category term="terminal" /><category term="ai-coding" /><summary type="html"><![CDATA[개요]]></summary></entry></feed>