<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>신강희 | Backend Developer Blog</title>
        <link>https://KKangHHee.github.io/blog</link>
        <description>신강희 | Backend Developer Blog</description>
        <lastBuildDate>Wed, 10 Dec 2025 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>ko</language>
        <item>
            <title><![CDATA[이메일 발송 API 응답 속도 개선: Spring Event와 비동기 처리]]></title>
            <link>https://KKangHHee.github.io/blog/spring-event-async-email-optimization</link>
            <guid>https://KKangHHee.github.io/blog/spring-event-async-email-optimization</guid>
            <pubDate>Wed, 10 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[SMTP 동기 호출로 인해 2.5초 걸리던 인증 API를]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>SMTP 동기 호출로 인해 2.5초 걸리던 인증 API를 <br>
Spring Event와 @Async를 활용한 비동기 처리로 <strong>응답 시간을 0.2초로 단축</strong>하고, 처리량을 향상시킨 과정입니다.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-문제-상황동기-블로킹-방식">1. 문제 상황(동기 블로킹 방식)<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#1-%EB%AC%B8%EC%A0%9C-%EC%83%81%ED%99%A9%EB%8F%99%EA%B8%B0-%EB%B8%94%EB%A1%9C%ED%82%B9-%EB%B0%A9%EC%8B%9D" class="hash-link" aria-label="1. 문제 상황(동기 블로킹 방식)에 대한 직접 링크" title="1. 문제 상황(동기 블로킹 방식)에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="초기-코드">초기 코드<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#%EC%B4%88%EA%B8%B0-%EC%BD%94%EB%93%9C" class="hash-link" aria-label="초기 코드에 대한 직접 링크" title="초기 코드에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class="">처음에는 <strong>저장-발송</strong>에만 초점을 맞춰 아래와 같은 방식으로 구현했습니다.</li>
</ul>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">VerificationService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createAndSendCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">VerificationType</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 1. 인증 코드 생성 및 Redis 저장 (0.1초)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">String</span><span class="token plain"> code </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateRandomCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        redisService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">saveCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 2. 이메일 발송 - SMTP 서버 응답 대기</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        emailService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sendVerificationCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><strong>문제점</strong></p>
<ul>
<li class=""><code>JavaMailSender.send()</code> 메서드는 Sync-Blocking방식<!-- -->
<ol>
<li class="">SMTP 서버가 응답할 때까지 API 요청 스레드가 대기(블로킹)</li>
<li class="">사용자 대기 시간 증가 (UX 악화)</li>
<li class="">동시 요청 시 처리량 감소</li>
<li class="">SMTP 서버 장애 시 API 전체가 영향</li>
</ol>
</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-핵심-개념-정리">2. 핵심 개념 정리<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#2-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC" class="hash-link" aria-label="2. 핵심 개념 정리에 대한 직접 링크" title="2. 핵심 개념 정리에 대한 직접 링크" translate="no">​</a></h2>
<blockquote>
<p>해결에 앞서 필요한 <strong>동기/비동기</strong>, <strong>블로킹/논블로킹</strong>, 그리고 <strong>ThreadPool</strong>의 개념을 먼저 정리하겠습니다.</p>
</blockquote>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-동기-vs-비동기-제어권의-관점">1) 동기 vs 비동기 (제어권의 관점)<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#1-%EB%8F%99%EA%B8%B0-vs-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%A0%9C%EC%96%B4%EA%B6%8C%EC%9D%98-%EA%B4%80%EC%A0%90" class="hash-link" aria-label="1) 동기 vs 비동기 (제어권의 관점)에 대한 직접 링크" title="1) 동기 vs 비동기 (제어권의 관점)에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class="">
<p><strong>Sync-Blocking</strong>: 호출한 스레드가 작업이 끝날 때까지 제어권을 잃고 대기</p>
</li>
<li class="">
<p><strong>Async-NonBlocking</strong>: 작업을 다른 스레드에 위임하고 즉시 제어권을 반환</p>
</li>
<li class="">
<p>시간이 걸리는 작업의 경우, <strong>Async-NonBlocking</strong>방식을 통해 작업을 던지고 제어권을 바로 받아야 합니다.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-threadpool이란">2) ThreadPool이란?<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#2-threadpool%EC%9D%B4%EB%9E%80" class="hash-link" aria-label="2) ThreadPool이란?에 대한 직접 링크" title="2) ThreadPool이란?에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class="">요청마다 새로운 스레드를 생성하면,</li>
<li class="">스레드 생성 비용 + 컨텍스트 스위칭 비용으로 인해 성능 저하가 발생</li>
</ul>
<p>→ 이를 해결하기 위해 <strong>미리 만든 스레드를 재사용하는 구조가 ThreadPool</strong></p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="장점">장점:<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#%EC%9E%A5%EC%A0%90" class="hash-link" aria-label="장점:에 대한 직접 링크" title="장점:에 대한 직접 링크" translate="no">​</a></h4>
<ol>
<li class=""><strong>느슨한 결합</strong>: Service는 이메일을 어떻게 보내는지 몰라도 됨</li>
<li class=""><strong>확장성</strong>: 다른 작업에 대해 리스너만 하나 더 만들면 끝</li>
<li class=""><strong>장애 격리</strong>: 이메일 실패해도 코드 생성은 성공</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-event-driven-architecture-적용">3) Event-Driven Architecture 적용<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#3-event-driven-architecture-%EC%A0%81%EC%9A%A9" class="hash-link" aria-label="3) Event-Driven Architecture 적용에 대한 직접 링크" title="3) Event-Driven Architecture 적용에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class="">
<p>Spring의 <code>ApplicationEventPublisher</code>를 사용하여 비즈니스 로직(인증 코드 생성)과 부가 로직(이메일 발송)을 분리합니다.</p>
</li>
<li class="">
<p><strong>발행(Publish):</strong></p>
<blockquote>
<p>서비스 계층에서 특정 이벤트를 <code>publishEvent()</code>를 통해 트리거 합니다.<br>
이벤트 처리 여부와 상관없이 즉시 응답을 반환합니다.</p>
</blockquote>
</li>
<li class="">
<p><strong>구독(Subscribe):</strong></p>
<blockquote>
<p><code>@EventListener</code>를 통해 특정 이벤트가 트리거 되면 이에 대한 처리를 합니다.</p>
</blockquote>
</li>
<li class="">
<p><strong>비동기화(Async):</strong></p>
<blockquote>
<p>리스너 메서드에 <code>@Async</code>를 통해 비동기 작업임을 명시하고, <br>
커스텀하여, 메인 스레드가 아닌 별도의 워커 스레드로 처리함을 명시합니다.</p>
</blockquote>
</li>
<li class="">
<p><strong>Before:</strong> Service가 직접 이메일 발송</p>
</li>
<li class="">
<p><strong>After:</strong> Service는 "인증 코드 생성됨" 이벤트만 발행</p>
</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-해결과정">3. 해결과정<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#3-%ED%95%B4%EA%B2%B0%EA%B3%BC%EC%A0%95" class="hash-link" aria-label="3. 해결과정에 대한 직접 링크" title="3. 해결과정에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-이벤트-클래스">1) 이벤트 클래스<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#1-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%81%B4%EB%9E%98%EC%8A%A4" class="hash-link" aria-label="1) 이벤트 클래스에 대한 직접 링크" title="1) 이벤트 클래스에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>우리가 처리할 작업의 트리거가 될 Event를 먼저 생성합니다.</p>
</blockquote>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">VerificationCodeCreatedEvent</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">extends</span><span class="token plain"> </span><span class="token class-name">ApplicationEvent</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token class-name">String</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token class-name">String</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token class-name">VerificationType</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token class-name">VerificationCodeCreatedEvent</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">Object</span><span class="token plain"> source</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">String</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">String</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">VerificationType</span><span class="token plain"> type</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">super</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">source</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">email </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">code </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// getters</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-이벤트-발행-service-계층">2) 이벤트 발행 (Service 계층)<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#2-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B0%9C%ED%96%89-service-%EA%B3%84%EC%B8%B5" class="hash-link" aria-label="2) 이벤트 발행 (Service 계층)에 대한 직접 링크" title="2) 이벤트 발행 (Service 계층)에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>비즈니스 작업을 처리하던 기존의 Sevice계층에서는 <br>
이제 이벤트 처리에 대한 내용을 알 필요가 없습니다.<br>
단지, 이벤트를 던지기만 합니다.</p>
</blockquote>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token annotation punctuation" style="color:#393A34">@Service</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token annotation punctuation" style="color:#393A34">@RequiredArgsConstructor</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">VerificationService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token class-name">ApplicationEventPublisher</span><span class="token plain"> eventPublisher</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token class-name">RedisService</span><span class="token plain"> redisService</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createAndSendCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">VerificationType</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 1. Redis에 인증 코드 저장 (동기)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">String</span><span class="token plain"> code </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateRandomCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        redisService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">saveCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 2. 이벤트 발행 (비동기)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        eventPublisher</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">publishEvent</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">VerificationCodeCreatedEvent</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-이벤트-리스너-비동기-처리">3) 이벤트 리스너 (비동기 처리)<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#3-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A6%AC%EC%8A%A4%EB%84%88-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC" class="hash-link" aria-label="3) 이벤트 리스너 (비동기 처리)에 대한 직접 링크" title="3) 이벤트 리스너 (비동기 처리)에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p><code>@Async()</code>어노테션에 추가로 <code>ThreadPoolTaskExecutor</code>를 설정하여 메인 스레드가 아닌 워커 스레드를 할당 합니다.</p>
</blockquote>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token annotation punctuation" style="color:#393A34">@Component</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">VerificationCodeEventListener</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token class-name">EmailService</span><span class="token plain"> emailService</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token annotation punctuation" style="color:#393A34">@Async</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"emailTaskExecutor"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 별도 스레드 풀에서 실행</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token annotation punctuation" style="color:#393A34">@EventListener</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">handleVerificationCodeCreated</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">VerificationCodeCreatedEvent</span><span class="token plain"> event</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            emailService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sendVerificationCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getEmail</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getType</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">Exception</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token comment" style="color:#999988;font-style:italic">// 실패 시 재시도 로직 또는 알림 처리</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-threadpooltaskexecutor-설정">4) ThreadPoolTaskExecutor 설정<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#4-threadpooltaskexecutor-%EC%84%A4%EC%A0%95" class="hash-link" aria-label="4) ThreadPoolTaskExecutor 설정에 대한 직접 링크" title="4) ThreadPoolTaskExecutor 설정에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class="">
<p><code>CorePoolSize</code> (기본 스레드 수)</p>
</li>
<li class="">
<p><code>MaxPoolSize</code> (최대 스레드 수)</p>
</li>
<li class="">
<p><code>QueueCapacity</code> (대기 큐 크기)</p>
</li>
<li class="">
<p><code>RejectedExecutionHandler</code>:</p>
<ul>
<li class="">최대 스레드까지 다 쓰고 큐도 꽉 찼을 때 어떻게 할지 결정합니다.</li>
<li class=""><code>AbortPolicy</code> (기본값): 예외를 던지고 작업을 버립니다.</li>
<li class=""><code>CallerRunsPolicy</code>: 큐가 꽉 차면 이벤트를 발행한 메인 스레드가 직접 처리합니다.</li>
</ul>
</li>
<li class="">
<p><code>ThreadNamePrefix</code>:</p>
<ul>
<li class="">디버깅을 위한 네이밍입니다.</li>
</ul>
</li>
</ul>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token annotation punctuation" style="color:#393A34">@Configuration</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token annotation punctuation" style="color:#393A34">@EnableAsync</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">AsyncConfig</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token annotation punctuation" style="color:#393A34">@Bean</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">name </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"emailTaskExecutor"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token class-name">Executor</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">emailTaskExecutor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">ThreadPoolTaskExecutor</span><span class="token plain"> executor </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">ThreadPoolTaskExecutor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        executor</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setCorePoolSize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 기본 스레드 수</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        executor</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setMaxPoolSize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">         </span><span class="token comment" style="color:#999988;font-style:italic">// 최대 스레드 수</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        executor</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setQueueCapacity</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">100</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">     </span><span class="token comment" style="color:#999988;font-style:italic">// 대기 큐 크기</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        executor</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setThreadNamePrefix</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"email-async-"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        executor</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setRejectedExecutionHandler</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">ThreadPoolExecutor</span><span class="token class-name punctuation" style="color:#393A34">.</span><span class="token class-name">CallerRunsPolicy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 큐가 꽉 찼을 때: 호출한 스레드가 직접 실행</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        executor</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">initialize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> executor</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="threadpool-설정">ThreadPool 설정<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#threadpool-%EC%84%A4%EC%A0%95" class="hash-link" aria-label="ThreadPool 설정에 대한 직접 링크" title="ThreadPool 설정에 대한 직접 링크" translate="no">​</a></h4>
<ul>
<li class="">이메일 발송은 CPU 연산보다 SMTP 서버와의 I/O 대기 시간이 긴 작업이기 때문에</li>
<li class="">스레드 수를 크게 잡지 않고 초기 설정 단계이므로 값을 최소화했습니다.</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="왜-threadpooltaskexecutor를-별도로-관리해야-하는가">왜 ThreadPoolTaskExecutor를 별도로 관리해야 하는가?<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#%EC%99%9C-threadpooltaskexecutor%EB%A5%BC-%EB%B3%84%EB%8F%84%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80" class="hash-link" aria-label="왜 ThreadPoolTaskExecutor를 별도로 관리해야 하는가?에 대한 직접 링크" title="왜 ThreadPoolTaskExecutor를 별도로 관리해야 하는가?에 대한 직접 링크" translate="no">​</a></h4>
<blockquote>
<p><code>ThreadPool</code>의 설정은 단순히 몇 개의 스레드를 돌릴지의 문제가 아닌 자원의 배분에 대한 설정입니다.<br>
단순히 <code>@Async</code>만 사용하고 설정을 생략하면, <br>
스프링은 기본적으로 <code>SimpleAsyncTaskExecutor</code>를 사용합니다.</p>
</blockquote>
<ul>
<li class="">
<p><strong><code>SimpleAsyncTaskExecutor</code>의 위험성</strong></p>
<ul>
<li class="">요청마다 새로운 스레드 생성</li>
<li class="">스레드 재사용 X</li>
<li class="">트래픽 증가 시, <strong>OOM</strong>이 발생 가능</li>
</ul>
</li>
<li class="">
<p><strong>ThreadPool을 별도로 관리하여</strong></p>
<ul>
<li class="">메인 스레드와 워커 스레드를 구분하여, 장애 격리</li>
<li class=""><code>CorePoolSize</code>와 <code>MaxPoolSize</code>를 통해 알맞는 자원 할당</li>
<li class=""><code>QueueCapacity</code>를 통해 대기 작업을 안전하게 보관</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-성능-측정">5) 성능 측정<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#5-%EC%84%B1%EB%8A%A5-%EC%B8%A1%EC%A0%95" class="hash-link" aria-label="5) 성능 측정에 대한 직접 링크" title="5) 성능 측정에 대한 직접 링크" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="측정-방법">측정 방법<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#%EC%B8%A1%EC%A0%95-%EB%B0%A9%EB%B2%95" class="hash-link" aria-label="측정 방법에 대한 직접 링크" title="측정 방법에 대한 직접 링크" translate="no">​</a></h4>
<ul>
<li class="">테스트 환경: 로컬 Docker + Redis + Gmail SMTP / JMeter</li>
<li class="">JMeter로 동시 사용자 50명, 각 10회 요청</li>
<li class="">API: POST /api/auth/email/verification</li>
</ul>
<table><thead><tr><th>항목</th><th>Before (동기)</th><th>After (비동기)</th><th>개선율</th></tr></thead><tbody><tr><td><strong>평균 응답시간</strong></td><td>2.5초</td><td>0.2초</td><td><strong>92% ↓</strong></td></tr><tr><td><strong>95 percentile</strong></td><td>3.2초</td><td>0.3초</td><td><strong>90% ↓</strong></td></tr><tr><td><strong>처리량 (TPS)</strong></td><td>10 req/s</td><td>100+ req/s</td><td><strong>10배 ↑</strong></td></tr><tr><td><strong>동시 처리 가능 수</strong></td><td>10명</td><td>100명+</td><td><strong>10배 ↑</strong></td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="보완점">보완점<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#%EB%B3%B4%EC%99%84%EC%A0%90" class="hash-link" aria-label="보완점에 대한 직접 링크" title="보완점에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-이벤트-발행-시-트랜잭션-문제">1) 이벤트 발행 시, 트랜잭션 문제<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#1-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B0%9C%ED%96%89-%EC%8B%9C-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%AC%B8%EC%A0%9C" class="hash-link" aria-label="1) 이벤트 발행 시, 트랜잭션 문제에 대한 직접 링크" title="1) 이벤트 발행 시, 트랜잭션 문제에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token annotation punctuation" style="color:#393A34">@Transactional</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createAndSendCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">VerificationType</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name">String</span><span class="token plain"> code </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateRandomCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    redisService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">saveCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 1) DB 저장</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    eventPublisher</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">publishEvent</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">VerificationCodeCreatedEvent</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<ul>
<li class="">이벤트 발행 직후, 예외 발생 시 롤백</li>
<li class="">작업 1)이 무시되어, 코드가 없는 이메일이 발송</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="해결책">해결책<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#%ED%95%B4%EA%B2%B0%EC%B1%85" class="hash-link" aria-label="해결책에 대한 직접 링크" title="해결책에 대한 직접 링크" translate="no">​</a></h4>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token annotation punctuation" style="color:#393A34">@Component</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">VerificationCodeEventListener</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token class-name">EmailService</span><span class="token plain"> emailService</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token annotation punctuation" style="color:#393A34">@Async</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"emailTaskExecutor"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 별도 스레드 풀에서 실행</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token annotation punctuation" style="color:#393A34">@TransactionalEventListener</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">phase </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token class-name">TransactionPhase</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">AFTER_COMMIT</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">handleVerificationCodeCreated</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">VerificationCodeCreatedEvent</span><span class="token plain"> event</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            emailService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sendVerificationCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getEmail</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getType</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">Exception</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token comment" style="color:#999988;font-style:italic">// 실패 시 재시도 로직 또는 알림 처리</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<ul>
<li class="">단순히 <code>@EventListener</code>어노테이션이 아닌,</li>
<li class=""><code>@TransactionalEventListener</code>을 사용하여 이메일 발송이 DB 저장보다 먼저 실행되는 문제를 방지<!-- -->
<ul>
<li class="">트랜잭션이 커밋된 이후에만 이벤트를 처리하기 때문</li>
</ul>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="주의점">주의점<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#%EC%A3%BC%EC%9D%98%EC%A0%90" class="hash-link" aria-label="주의점에 대한 직접 링크" title="주의점에 대한 직접 링크" translate="no">​</a></h4>
<ul>
<li class=""><code>@Async</code>나 <code>@Transactional</code>은 Spring AOP 기반으로 동작</li>
<li class=""><strong>자가 호출</strong> 시 동작하지 않음<!-- -->
<ul>
<li class="">동일 클래스 내의 메서드 호출 시,</li>
<li class="">프록시 객체를 거치지 않아 <code>@Async</code>가 동작하지 않으므로,</li>
<li class="">반드시 리스너 클래스를 별도의 <code>@Component</code>로 분리하여 빈 주입을 통해 호출해야 함</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-재시도-로직-없음">2) 재시도 로직 없음<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#2-%EC%9E%AC%EC%8B%9C%EB%8F%84-%EB%A1%9C%EC%A7%81-%EC%97%86%EC%9D%8C" class="hash-link" aria-label="2) 재시도 로직 없음에 대한 직접 링크" title="2) 재시도 로직 없음에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>SMTP 서버 장애에 대한 대응이 없음</p>
</blockquote>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="해결책-1">해결책<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#%ED%95%B4%EA%B2%B0%EC%B1%85-1" class="hash-link" aria-label="해결책에 대한 직접 링크" title="해결책에 대한 직접 링크" translate="no">​</a></h4>
<p><code>handleVerificationCodeCreated</code>에 반복문 + <code>try-catch</code>를 사용하여 시도횟수 및 실패횟수 카운팅 및 재시도 로직 추가 필요</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-메세지-유실-방지">3) 메세지 유실 방지<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#3-%EB%A9%94%EC%84%B8%EC%A7%80-%EC%9C%A0%EC%8B%A4-%EB%B0%A9%EC%A7%80" class="hash-link" aria-label="3) 메세지 유실 방지에 대한 직접 링크" title="3) 메세지 유실 방지에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>현재 구조는 메인 서버가 죽으면 이벤트가 초기화<br>
[이벤트 발행] → [메모리 큐] → [서버 셧다운] → 이벤트 유실</p>
</blockquote>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="해결책-2">해결책<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#%ED%95%B4%EA%B2%B0%EC%B1%85-2" class="hash-link" aria-label="해결책에 대한 직접 링크" title="해결책에 대한 직접 링크" translate="no">​</a></h4>
<ul>
<li class="">RabbitMQ, Kafka 같은 메시지 브로커 사용</li>
<li class="">Dead Letter Queue(DLQ)로 실패한 메시지 별도 관리</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="참고-자료">참고 자료<a href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C" class="hash-link" aria-label="참고 자료에 대한 직접 링크" title="참고 자료에 대한 직접 링크" translate="no">​</a></h4>
<ul>
<li class=""><a href="https://docs.spring.io/spring-framework/reference/core/beans/context-introduction.html#context-functionality-events" target="_blank" rel="noopener noreferrer" class="">Spring Framework - Event Publishing</a></li>
<li class=""><a href="https://docs.spring.io/spring-framework/reference/integration/scheduling.html" target="_blank" rel="noopener noreferrer" class="">Spring Framework - Task Execution and Scheduling</a></li>
</ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[레디스는 싱글스레드인데 왜 동시성 제어가 필요할까?]]></title>
            <link>https://KKangHHee.github.io/blog/redis-concurrency</link>
            <guid>https://KKangHHee.github.io/blog/redis-concurrency</guid>
            <pubDate>Tue, 21 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[프로젝트 GitHub]]></description>
            <content:encoded><![CDATA[<p><a href="https://github.com/KKangHHee/redis-concurrency-test" target="_blank" rel="noopener noreferrer" class="">프로젝트 GitHub</a></p>
<p>Redis는 <strong>싱글 스레드 기반</strong>으로 동작합니다.<br>
<!-- -->그런데 Redis를 사용할 때 <strong>동시성 제어</strong> 이야기가 나오는 걸 보고 의문이 생겼습니다.</p>
<blockquote>
<p>"싱글 스레드인데, 왜 Race Condition이 생기지?"</p>
</blockquote>
<p>이 글에서는 이전 프로젝트에서 발견한 의문점을 바탕으로<br>
<!-- -->✔ Redis의 싱글 스레드 구조<br>
<!-- -->✔ 동시성 문제가 발생하는 근본 원인<br>
<!-- -->✔ Docker Compose로 검증한 Before/After 비교<br>
<!-- -->✔ 안전하게 사용하는 방법을 기록합니다.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-redis란-무엇인가">1. Redis란 무엇인가?<a href="https://kkanghhee.github.io/blog/redis-concurrency#1-redis%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80" class="hash-link" aria-label="1. Redis란 무엇인가?에 대한 직접 링크" title="1. Redis란 무엇인가?에 대한 직접 링크" translate="no">​</a></h2>
<p>Redis(Remote Dictionary Server)는 <strong>메모리 기반 Key-Value 저장소</strong>입니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-핵심-특징">✔ 핵심 특징<a href="https://kkanghhee.github.io/blog/redis-concurrency#-%ED%95%B5%EC%8B%AC-%ED%8A%B9%EC%A7%95" class="hash-link" aria-label="✔ 핵심 특징에 대한 직접 링크" title="✔ 핵심 특징에 대한 직접 링크" translate="no">​</a></h3>
<table><thead><tr><th>특징</th><th>설명</th></tr></thead><tbody><tr><td>In-Memory</td><td>RAM 기반으로 디스크보다 매우 빠름</td></tr><tr><td>Key-Value 구조</td><td>단순한 데이터 모델로 빠른 접근</td></tr><tr><td>싱글 스레드</td><td>이벤트 루프 기반으로 한 번에 하나의 명령만 처리</td></tr><tr><td>다양한 자료구조</td><td>String, Hash, List, Set, Sorted Set 등 지원</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-주요-사용처">✔ 주요 사용처<a href="https://kkanghhee.github.io/blog/redis-concurrency#-%EC%A3%BC%EC%9A%94-%EC%82%AC%EC%9A%A9%EC%B2%98" class="hash-link" aria-label="✔ 주요 사용처에 대한 직접 링크" title="✔ 주요 사용처에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class=""><strong>캐시</strong>: DB 부하 감소 (조회 성능 향상)</li>
<li class=""><strong>세션 관리</strong>: 로그인 상태 유지</li>
<li class=""><strong>인증 코드</strong>: 이메일/SMS 인증번호 저장</li>
<li class=""><strong>실시간 카운팅</strong>: 조회수, 좋아요 등</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-redis의-싱글-스레드-모델">2. Redis의 싱글 스레드 모델<a href="https://kkanghhee.github.io/blog/redis-concurrency#2-redis%EC%9D%98-%EC%8B%B1%EA%B8%80-%EC%8A%A4%EB%A0%88%EB%93%9C-%EB%AA%A8%EB%8D%B8" class="hash-link" aria-label="2. Redis의 싱글 스레드 모델에 대한 직접 링크" title="2. Redis의 싱글 스레드 모델에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-redis-내부-동작-방식">✔ Redis 내부 동작 방식<a href="https://kkanghhee.github.io/blog/redis-concurrency#-redis-%EB%82%B4%EB%B6%80-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D" class="hash-link" aria-label="✔ Redis 내부 동작 방식에 대한 직접 링크" title="✔ Redis 내부 동작 방식에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">┌─────────────┐</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ Client A    │───┐</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">└─────────────┘   │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                  ▼</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">┌─────────────┐   ┌──────────────────┐</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ Client B    │─▶│  Command Queue   │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">└─────────────┘   └──────────────────┘</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                          │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">┌─────────────┐           ▼</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ Client C    │───▶┌──────────────┐</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">└─────────────┘     │ Event Loop   │ ← 싱글 스레드</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    │ (한 번에 1개) │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    └──────────────┘</span><br></span></code></pre></div></div>
<blockquote>
<p>내부적으로 이벤트 루프 기반 싱글 스레드로, 명령을 처리</p>
</blockquote>
<ol>
<li class="">클라이언트 요청이 큐에 쌓임</li>
<li class="">Redis는 한 번에 하나의 명령만 처리(각 명령은 원자적)</li>
</ol>
<p>→ 따라서 <code>INCR</code>, <code>HINCRBY</code> 같은 <strong>단일 명령</strong>이 중요</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-그럼-왜-동시성-문제가-생길까">3. 그럼 왜 동시성 문제가 생길까?<a href="https://kkanghhee.github.io/blog/redis-concurrency#3-%EA%B7%B8%EB%9F%BC-%EC%99%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EA%B0%80-%EC%83%9D%EA%B8%B8%EA%B9%8C" class="hash-link" aria-label="3. 그럼 왜 동시성 문제가 생길까?에 대한 직접 링크" title="3. 그럼 왜 동시성 문제가 생길까?에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="문제-애플리케이션-레벨의-분리된-연산">문제: "애플리케이션 레벨의 분리된 연산"<a href="https://kkanghhee.github.io/blog/redis-concurrency#%EB%AC%B8%EC%A0%9C-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%A0%88%EB%B2%A8%EC%9D%98-%EB%B6%84%EB%A6%AC%EB%90%9C-%EC%97%B0%EC%82%B0" class="hash-link" aria-label="문제: &quot;애플리케이션 레벨의 분리된 연산&quot;에 대한 직접 링크" title="문제: &quot;애플리케이션 레벨의 분리된 연산&quot;에 대한 직접 링크" translate="no">​</a></h3>
<p>Redis 자체는 싱글 스레드이므로 각 명령은 원자적으로 처리됩니다.
하지만 <strong>비즈니스 로직이 GET → Application 계산 → SET 순서로 분리</strong>되어 있다면, 이 사이에는 다른 서버의 요청이 끼어들 수 있는 <strong>Race Window가 발생</strong>합니다.</p>
<ul>
<li class="">비교 프로젝트를 통해 알아보도록 하겠습니다.</li>
<li class="">환경은 이메일을 키로 하여, 동일 환경에서 Read-Modify-Write와 HINCRBY와 같은 단일 처리로 나눠서 비교하였습니다.</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="잘못된-구조-before">잘못된 구조 (Before)<a href="https://kkanghhee.github.io/blog/redis-concurrency#%EC%9E%98%EB%AA%BB%EB%90%9C-%EA%B5%AC%EC%A1%B0-before" class="hash-link" aria-label="잘못된 구조 (Before)에 대한 직접 링크" title="잘못된 구조 (Before)에 대한 직접 링크" translate="no">​</a></h4>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token constant" style="color:#36acaa">GET</span><span class="token plain"> count</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">+</span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token constant" style="color:#36acaa">SET</span><span class="token plain"> count</span><br></span></code></pre></div></div>
<ul>
<li class="">여러 서버 인스턴스가 동시에 실행되면</li>
<li class="">같은 키의 값을 바꾼다 할때,</li>
</ul>
<p><img decoding="async" loading="lazy" alt="image" src="https://kkanghhee.github.io/assets/images/app.before-90d95ff5d508bd196ce46998b3297830.png" width="1391" height="286" class="img_ev3q"></p>
<blockquote>
<p>Docker Compose로 <strong>2대 서버 + 12번 병렬 요청</strong>을 보냈을 때: Lost Update 발생</p>
</blockquote>
<ul>
<li class="">Redis는 싱글 스레드지만 애플리케이션은 멀티 인스턴스</li>
<li class="">그래서 <strong>RMW</strong> 구조는 <strong>Lost Update</strong> 문제 발생 가능</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="해결-redis-원자-연산-사용">해결: Redis 원자 연산 사용<a href="https://kkanghhee.github.io/blog/redis-concurrency#%ED%95%B4%EA%B2%B0-redis-%EC%9B%90%EC%9E%90-%EC%97%B0%EC%82%B0-%EC%82%AC%EC%9A%A9" class="hash-link" aria-label="해결: Redis 원자 연산 사용에 대한 직접 링크" title="해결: Redis 원자 연산 사용에 대한 직접 링크" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="redis의-원자-연산-명령">Redis의 원자 연산 명령<a href="https://kkanghhee.github.io/blog/redis-concurrency#redis%EC%9D%98-%EC%9B%90%EC%9E%90-%EC%97%B0%EC%82%B0-%EB%AA%85%EB%A0%B9" class="hash-link" aria-label="Redis의 원자 연산 명령에 대한 직접 링크" title="Redis의 원자 연산 명령에 대한 직접 링크" translate="no">​</a></h4>
<table><thead><tr><th>명령어</th><th>용도</th><th>예시</th></tr></thead><tbody><tr><td><code>INCR</code> / <code>INCRBY</code></td><td>숫자 증가</td><td>조회수, 좋아요 카운트</td></tr><tr><td><code>HINCRBY</code></td><td>Hash 내부 값 증가</td><td>사용자별 시도 횟수 관리</td></tr><tr><td><code>SETNX</code></td><td>최초 1회만 설정</td><td>분산 락 구현</td></tr><tr><td><code>Lua Script</code></td><td>여러 명령을 하나로 묶음</td><td>복잡한 조건부 로직</td></tr></tbody></table>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="선택-기준">선택 기준<a href="https://kkanghhee.github.io/blog/redis-concurrency#%EC%84%A0%ED%83%9D-%EA%B8%B0%EC%A4%80" class="hash-link" aria-label="선택 기준에 대한 직접 링크" title="선택 기준에 대한 직접 링크" translate="no">​</a></h4>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">단순 증가만 필요 → INCR / INCRBY</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">여러 항목 관리 → HINCRBY (Hash 구조)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">조건부 복잡 로직 → Lua Script</span><br></span></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="해결-구조-after">해결 구조 (after)<a href="https://kkanghhee.github.io/blog/redis-concurrency#%ED%95%B4%EA%B2%B0-%EA%B5%AC%EC%A1%B0-after" class="hash-link" aria-label="해결 구조 (after)에 대한 직접 링크" title="해결 구조 (after)에 대한 직접 링크" translate="no">​</a></h4>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">Long</span><span class="token plain"> currentAttempts </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">opsForHash</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">increment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"attemptCount"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<ul>
<li class="">Redis 내부에서 단일 명령으로 처리:
HINCRBY email<!-- -->:attempts<!-- --> <a href="mailto:user@example.com" target="_blank" rel="noopener noreferrer" class="">user@example.com</a> 1</li>
</ul>
<p><img decoding="async" loading="lazy" alt="image" src="https://kkanghhee.github.io/assets/images/app.after-01ae3fd87643c7cb178161786925d48c.png" width="1435" height="445" class="img_ev3q"></p>
<blockquote>
<p>Docker Compose로 <strong>2대 서버 + 12번 병렬 요청</strong>을 보냈을 때: 정확한 카운팅</p>
</blockquote>
<ul>
<li class=""><strong>단일 명령</strong>이므로 Redis의 싱글 스레드 특성상 절대 중간에 끼어들 수 없음</li>
<li class="">분산 환경(여러 서버)에서도 안전</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-정리">4. 정리<a href="https://kkanghhee.github.io/blog/redis-concurrency#4-%EC%A0%95%EB%A6%AC" class="hash-link" aria-label="4. 정리에 대한 직접 링크" title="4. 정리에 대한 직접 링크" translate="no">​</a></h2>
<table><thead><tr><th>항목</th><th>내용</th></tr></thead><tbody><tr><td><strong>문제</strong></td><td>Redis는 싱글 스레드지만, 애플리케이션의 분리된 연산에서 Race Condition 발생</td></tr><tr><td><strong>원인</strong></td><td>Read-Modify-Write은 3단계가 분리되어 있음</td></tr><tr><td><strong>해결</strong></td><td>HINCRBY 같은 원자 연산 사용</td></tr><tr><td><strong>검증</strong></td><td>Docker Compose로 2대 서버 환경에서 12번 요청 테스트 완료</td></tr></tbody></table>
<ul>
<li class="">Redis를 사용한다고 해서 자동으로 동시성 문제가 해결되는 건 아닙니다.</li>
<li class="">Read-Modify-Write 방식으로 구현하면 여전히 Race Condition이 발생할 수 있습니다.</li>
<li class="">트랜잭션 처리처럼 잘 묶어서 올바르게 사용하는 게 중요합니다.</li>
</ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[이메일 인증에서 Redis가 강점을 갖는 이유: TTL과 원자 연산]]></title>
            <link>https://KKangHHee.github.io/blog/redis-email-verification</link>
            <guid>https://KKangHHee.github.io/blog/redis-email-verification</guid>
            <pubDate>Fri, 10 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA["이메일 인증 코드는 왜 DB가 아닌 Redis에 저장할까?"]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"이메일 인증 코드는 왜 DB가 아닌 Redis에 저장할까?"</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-이메일-인증의-요구사항">1. 이메일 인증의 요구사항<a href="https://kkanghhee.github.io/blog/redis-email-verification#1-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D%EC%9D%98-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD" class="hash-link" aria-label="1. 이메일 인증의 요구사항에 대한 직접 링크" title="1. 이메일 인증의 요구사항에 대한 직접 링크" translate="no">​</a></h2>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="비즈니스-요구사항">비즈니스 요구사항<a href="https://kkanghhee.github.io/blog/redis-email-verification#%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD" class="hash-link" aria-label="비즈니스 요구사항에 대한 직접 링크" title="비즈니스 요구사항에 대한 직접 링크" translate="no">​</a></h4>
<table><thead><tr><th>항목</th><th>요구사항</th><th>이유</th></tr></thead><tbody><tr><td><strong>빠른 응답</strong></td><td>빠른 응답</td><td>사용자 경험 (UX)</td></tr><tr><td><strong>자동 만료</strong></td><td>5~10분 후 삭제</td><td>보안과 메모리 절약</td></tr><tr><td><strong>시도 제한</strong></td><td>5회 초과 시 차단</td><td>무차별 대입 공격 방지</td></tr><tr><td><strong>일시성</strong></td><td>인증 완료 후 즉시 삭제</td><td>개인정보 최소 보관</td></tr></tbody></table>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="db-vs-redis-비교">DB vs Redis 비교<a href="https://kkanghhee.github.io/blog/redis-email-verification#db-vs-redis-%EB%B9%84%EA%B5%90" class="hash-link" aria-label="DB vs Redis 비교에 대한 직접 링크" title="DB vs Redis 비교에 대한 직접 링크" translate="no">​</a></h4>
<table><thead><tr><th>기준</th><th>MySQL/PostgreSQL</th><th>Redis</th></tr></thead><tbody><tr><td><strong>읽기 속도</strong></td><td>상대적으로 느림(디스크 I/O)</td><td>상대적으로 빠름(메모리)</td></tr><tr><td><strong>TTL 지원</strong></td><td>❌</td><td>✅ (자동 만료)</td></tr><tr><td><strong>동시성</strong></td><td>Row Lock 필요</td><td>원자 연산 기본 지원</td></tr><tr><td><strong>영속성</strong></td><td>장기 보관</td><td>RDB/AOF로 백업 가능</td></tr></tbody></table>
<ul>
<li class=""><strong>일시적 데이터 + 빠른 속도 + 자동 만료</strong>에서는 <strong>Redis가 압도적 우위</strong></li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-redis-ttl">2. Redis TTL<a href="https://kkanghhee.github.io/blog/redis-email-verification#2-redis-ttl" class="hash-link" aria-label="2. Redis TTL에 대한 직접 링크" title="2. Redis TTL에 대한 직접 링크" translate="no">​</a></h2>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="db로-구현-시">DB로 구현 시<a href="https://kkanghhee.github.io/blog/redis-email-verification#db%EB%A1%9C-%EA%B5%AC%ED%98%84-%EC%8B%9C" class="hash-link" aria-label="DB로 구현 시에 대한 직접 링크" title="DB로 구현 시에 대한 직접 링크" translate="no">​</a></h4>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> email_codes </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    email </span><span class="token keyword" style="color:#00009f">VARCHAR</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">255</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    code </span><span class="token keyword" style="color:#00009f">VARCHAR</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    expired_at </span><span class="token keyword" style="color:#00009f">TIMESTAMP</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<ul>
<li class="">조회할 때마다 <code>expired_at &gt; NOW()</code> 조건 체크</li>
<li class="">배치 작업으로 수동 삭제 필요 (코드 + 스케줄러 설정)</li>
<li class="">만료된 데이터가 즉시 삭제되지 않음 (디스크 공간 낭비)</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="redis의-경우">redis의 경우<a href="https://kkanghhee.github.io/blog/redis-email-verification#redis%EC%9D%98-%EA%B2%BD%EC%9A%B0" class="hash-link" aria-label="redis의 경우에 대한 직접 링크" title="redis의 경우에 대한 직접 링크" translate="no">​</a></h4>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 인증 코드 저장 + 10분 TTL 설정</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">opsForValue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">set</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"email:code:"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">TimeUnit</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">MINUTES</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="redis가-빠른-이유">Redis가 빠른 이유<a href="https://kkanghhee.github.io/blog/redis-email-verification#redis%EA%B0%80-%EB%B9%A0%EB%A5%B8-%EC%9D%B4%EC%9C%A0" class="hash-link" aria-label="Redis가 빠른 이유에 대한 직접 링크" title="Redis가 빠른 이유에 대한 직접 링크" translate="no">​</a></h4>
<ul>
<li class="">
<p>MySQL:
요청 → 디스크 I/O → 인덱스 검색 → Row Lock → 결과 반환</p>
</li>
<li class="">
<p>Redis:
요청 → 메모리 해시 조회 → 결과 반환</p>
</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-redis를-통한-이메일-인증-구현">3. Redis를 통한 이메일 인증 구현<a href="https://kkanghhee.github.io/blog/redis-email-verification#3-redis%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84" class="hash-link" aria-label="3. Redis를 통한 이메일 인증 구현에 대한 직접 링크" title="3. Redis를 통한 이메일 인증 구현에 대한 직접 링크" translate="no">​</a></h2>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="데이터-구조-설계">데이터 구조 설계<a href="https://kkanghhee.github.io/blog/redis-email-verification#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B5%AC%EC%A1%B0-%EC%84%A4%EA%B3%84" class="hash-link" aria-label="데이터 구조 설계에 대한 직접 링크" title="데이터 구조 설계에 대한 직접 링크" translate="no">​</a></h4>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">Key: email:verification:{email}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Fields:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  - code: "A1B2C3"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  - attempts: 3</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  - created_at: "2025-01-27T10:30:00"</span><br></span></code></pre></div></div>
<p><strong>Hash 구조의 장점</strong></p>
<ul>
<li class=""><strong>데이터 응집도</strong>: 같은 이메일의 모든 정보를 한 곳에 관리</li>
<li class=""><strong>부분 업데이트</strong>: <code>code</code>는 그대로 두고 <code>attempts</code>만 증가 가능</li>
<li class=""><strong>메모리 효율</strong>: 작은 Hash는 Ziplist로 압축되어 메모리 절약</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="코드-구현java">코드 구현(java)<a href="https://kkanghhee.github.io/blog/redis-email-verification#%EC%BD%94%EB%93%9C-%EA%B5%AC%ED%98%84java" class="hash-link" aria-label="코드 구현(java)에 대한 직접 링크" title="코드 구현(java)에 대한 직접 링크" translate="no">​</a></h4>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token annotation punctuation" style="color:#393A34">@Service</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token annotation punctuation" style="color:#393A34">@RequiredArgsConstructor</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">EmailVerificationService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token class-name">String</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">KEY_PREFIX</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"email:verification:"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 키 구조</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">int</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">MAX_ATTEMPTS</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 최대 횟수</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">int</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">CODE_EXPIRY_MINUTES</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 만료 시간</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token class-name">RedisTemplate</span><span class="token plain"> redisTemplate</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 1) 인증 코드 생성 및 저장(hash)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token class-name">String</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">String</span><span class="token plain"> code </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateRandomCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">String</span><span class="token plain"> key </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">KEY_PREFIX</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">Map</span><span class="token plain"> data </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">HashMap</span><span class="token generics punctuation" style="color:#393A34">&lt;</span><span class="token generics punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        data</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"code"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        data</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"attempts"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        data</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"created_at"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">LocalDateTime</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toString</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">opsForHash</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">putAll</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">expire</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">CODE_EXPIRY_MINUTES</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">TimeUnit</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">MINUTES</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 2) 인증 코드 검증(Redis 자체)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">boolean</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">verify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">String</span><span class="token plain"> inputCode</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">String</span><span class="token plain"> key </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">KEY_PREFIX</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 1. 키 존재 여부 확인</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token class-name">Boolean</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">TRUE</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">equals</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">hasKey</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">CodeExpiredException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"인증 코드가 만료되었거나 존재하지 않습니다."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 2. 시도 횟수 증가</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">Long</span><span class="token plain"> attempts </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">opsForHash</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">increment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"attempts"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 3. 최대 시도 횟수 체크</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">attempts </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">MAX_ATTEMPTS</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">TooManyAttemptsException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token string" style="color:#e3116c">"인증 시도 횟수가 초과되었습니다."</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 4. 코드 일치 확인</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">String</span><span class="token plain"> storedCode </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">opsForHash</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"code"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">storedCode </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">CodeExpiredException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"인증 코드가 만료되었습니다."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">boolean</span><span class="token plain"> isValid </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> storedCode</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">equals</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">inputCode</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 5. 인증 성공 시 즉시 삭제</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">isValid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">delete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> isValid</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 3) 재발급 시, 이전 코드 무효화</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token class-name">String</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">regenerateCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">String</span><span class="token plain"> key </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">KEY_PREFIX</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">delete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">email</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 4) IP당 1분에 2번 요청 가능</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">checkRateLimit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token plain"> ip</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">String</span><span class="token plain"> key </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"rate:limit:"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> ip</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">Long</span><span class="token plain"> requests </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">opsForValue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">increment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">requests </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 첫 요청 시 TTL 설정</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">expire</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">TimeUnit</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">MINUTES</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">requests </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RateLimitExceededException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"너무 많은 요청입니다. 1분 후 다시 시도하세요."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">         </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 5) 인증코드 생성</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token class-name">String</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateRandomCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token class-name">RandomStringUtils</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">randomAlphanumeric</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">6</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toUpperCase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="주의점-1-만료된-키에-대한-increment">주의점 1) 만료된 키에 대한 <code>increment</code><a href="https://kkanghhee.github.io/blog/redis-email-verification#%EC%A3%BC%EC%9D%98%EC%A0%90-1-%EB%A7%8C%EB%A3%8C%EB%90%9C-%ED%82%A4%EC%97%90-%EB%8C%80%ED%95%9C-increment" class="hash-link" aria-label="주의점-1-만료된-키에-대한-increment에 대한 직접 링크" title="주의점-1-만료된-키에-대한-increment에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name">Long</span><span class="token plain"> attempts </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">opsForHash</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">increment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"attempts"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<ul>
<li class="">인증 코드가 만료되어도 계속 검증 요청이 올 경우,</li>
<li class="">위의 코드에서는 새로 키를 생성</li>
</ul>
<p><strong>결과:</strong></p>
<ul>
<li class=""><code>code</code> 필드는 없고 <code>attempts</code> 필드만 있는 <strong>키</strong> 생성</li>
<li class="">TTL도 없어서 <strong>영원히 메모리에 남음</strong> (메모리 누수 발생)</li>
</ul>
<p><strong>해결</strong></p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 키 존재 여부 먼저 체크</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token class-name">Boolean</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">TRUE</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">equals</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">redisTemplate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">hasKey</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">CodeExpiredException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"인증 코드가 만료되었습니다."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 이후 increment 실행</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="주의점-2-hash-vs-string-구조-비교">주의점 2) Hash vs String 구조 비교<a href="https://kkanghhee.github.io/blog/redis-email-verification#%EC%A3%BC%EC%9D%98%EC%A0%90-2-hash-vs-string-%EA%B5%AC%EC%A1%B0-%EB%B9%84%EA%B5%90" class="hash-link" aria-label="주의점 2) Hash vs String 구조 비교에 대한 직접 링크" title="주의점 2) Hash vs String 구조 비교에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class="">
<p><strong>Hash 구조의 경우</strong></p>
<ul>
<li class="">데이터 응집도 좋음 (한 곳에 모든 정보)</li>
<li class="">코드와 시도 횟수는 같은 생명 주기</li>
<li class="">부분 업데이트 효율적</li>
<li class=""><strong>개별 필드에 다른 TTL을 걸 수 없음</strong></li>
</ul>
</li>
<li class="">
<p><strong>String 구조의 경우</strong></p>
<ul>
<li class="">필드마다 <strong>다른 TTL</strong>이 필요할 때만 고려</li>
</ul>
</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-정리">4. 정리<a href="https://kkanghhee.github.io/blog/redis-email-verification#4-%EC%A0%95%EB%A6%AC" class="hash-link" aria-label="4. 정리에 대한 직접 링크" title="4. 정리에 대한 직접 링크" translate="no">​</a></h2>
<blockquote>
<p>데이터의 생명 주기가 짧은 데이터(인증 코드)의 경우 redis가 최적임</p>
</blockquote>
<ul>
<li class=""><strong>운영 효율성:</strong>
별도의 Batch 작업이나 스케줄러 없이도 TTL을 통해 만료 데이터를 자동 정리함</li>
<li class=""><strong>성능 최적화:</strong>
인증 과정에서 발생하는 빈번한 쓰기/읽기 작업이 메모리 내에서 처리되어 DB 부하가 없음</li>
<li class=""><strong>보안:</strong>
원자적 연산을 활용해 분산 환경에서도 정확한 카운팅 가능</li>
<li class=""><strong>비용 절감:</strong>
일시적인 데이터를 위해 RDB의 커넥션과 저장 공간을 낭비하지 않음</li>
</ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[복잡한 검색 쿼리, JPA vs MyBatis 성능 비교와 하이브리드 전략]]></title>
            <link>https://KKangHHee.github.io/blog/jpa-mybatis-hybrid-strategy</link>
            <guid>https://KKangHHee.github.io/blog/jpa-mybatis-hybrid-strategy</guid>
            <pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate>
            <description><![CDATA["관리자 페이지의 복잡한 검색 기능 어떻게 구현할까?]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"관리자 페이지의 복잡한 검색 기능 어떻게 구현할까?</p>
</blockquote>
<p>이 글에서는 다양한 필터 조건을 가진 검색 기능을 구현하며 <strong>순수 JPA → Native Query → JPA Specification → MyBatis</strong>로 변환한 과정과 각 방식의 성능을 실측한 결과를 공유합니다.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-프로젝트-요구사항">1. 프로젝트 요구사항<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#1-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD" class="hash-link" aria-label="1. 프로젝트 요구사항에 대한 직접 링크" title="1. 프로젝트 요구사항에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="관리자-페이지의-고객-검색-기능">관리자 페이지의 고객 검색 기능<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EA%B4%80%EB%A6%AC%EC%9E%90-%ED%8E%98%EC%9D%B4%EC%A7%80%EC%9D%98-%EA%B3%A0%EA%B0%9D-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5" class="hash-link" aria-label="관리자 페이지의 고객 검색 기능에 대한 직접 링크" title="관리자 페이지의 고객 검색 기능에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>비즈니스 요구사항:</strong></p>
<ul>
<li class="">다양한 필터 조건 지원<!-- -->
<ul>
<li class="">기간 필터 (가입일, 최종 로그인)</li>
<li class="">계정 상태 (활성/비활성/잠김)</li>
<li class="">권한별 필터</li>
</ul>
</li>
<li class="">키워드 검색 범위<!-- -->
<ul>
<li class="">전체 검색 (이메일, 이름, 소속, 부서, 연락처)</li>
<li class="">개별 필드 검색</li>
</ul>
</li>
<li class="">번호 기반 페이지네이션 (1, 2, 3... 페이지)</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="기술적-요구사항">기술적 요구사항<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD" class="hash-link" aria-label="기술적 요구사항에 대한 직접 링크" title="기술적 요구사항에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>성능 목표:</strong></p>
<ul>
<li class="">따로 제한은 없었으나 느린 것을 피하고자 함</li>
</ul>
<p><strong>유지보수성:</strong></p>
<ul>
<li class="">새로운 검색 조건 추가 용이</li>
<li class="">쿼리 가독성 확보</li>
<li class="">테스트 가능한 구조</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-구현-과정">2. 구현 과정<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#2-%EA%B5%AC%ED%98%84-%EA%B3%BC%EC%A0%95" class="hash-link" aria-label="2. 구현 과정에 대한 직접 링크" title="2. 구현 과정에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1단계-순수-jpa-repository">1단계: 순수 JPA Repository<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#1%EB%8B%A8%EA%B3%84-%EC%88%9C%EC%88%98-jpa-repository" class="hash-link" aria-label="1단계: 순수 JPA Repository에 대한 직접 링크" title="1단계: 순수 JPA Repository에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">UserRepository</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">extends</span><span class="token plain"> </span><span class="token class-name">JpaRepository</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name">Page</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">findByClientCompanyNameContaining</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token plain"> companyName</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">Pageable</span><span class="token plain"> pageable</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name">Page</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">findByLastLoginAtBetween</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">LocalDateTime</span><span class="token plain"> start</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">LocalDateTime</span><span class="token plain"> end</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">Pageable</span><span class="token plain"> pageable</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="문제점">문제점<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EB%AC%B8%EC%A0%9C%EC%A0%90" class="hash-link" aria-label="문제점에 대한 직접 링크" title="문제점에 대한 직접 링크" translate="no">​</a></h4>
<ul>
<li class="">필터링 조건 마다<!-- -->
<ul>
<li class=""><strong>Repository 메서드 증가</strong> (조건 N개 → 2^N개)</li>
<li class=""><strong>Service의 분기 처리 복잡</strong></li>
<li class=""><strong>가독성과 유지 보수성의 저하</strong></li>
</ul>
</li>
</ul>
<hr>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2단계-native-query">2단계: Native Query<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#2%EB%8B%A8%EA%B3%84-native-query" class="hash-link" aria-label="2단계: Native Query에 대한 직접 링크" title="2단계: Native Query에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>@Query 어노테이션 사용</p>
</blockquote>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="문제점-1">문제점<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EB%AC%B8%EC%A0%9C%EC%A0%90-1" class="hash-link" aria-label="문제점에 대한 직접 링크" title="문제점에 대한 직접 링크" translate="no">​</a></h4>
<ul>
<li class="">Native Query를 사용하여,<!-- -->
<ul>
<li class="">
<p>DB 종속 (MySQL 전용)</p>
</li>
<li class="">
<p>동적 조건 처리 (IS NULL OR) 비효율</p>
</li>
<li class="">
<p>실행 계획 최적화 어려움</p>
<ul>
<li class=""><strong>실행 계획 분석:</strong></li>
</ul>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">EXPLAIN</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> u</span><span class="token punctuation" style="color:#393A34">.</span><span class="token operator" style="color:#393A34">*</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">user</span><span class="token plain"> u</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> :keyword </span><span class="token operator" style="color:#393A34">IS</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">NULL</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">OR</span><span class="token plain"> u</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">email </span><span class="token operator" style="color:#393A34">LIKE</span><span class="token plain"> CONCAT</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'%'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> :keyword</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'%'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<ul>
<li class=""><strong>문제점:</strong>
<ul>
<li class="">MySQL 옵티마이저는 IS NULL OR 조건을 최적화하지 못함<!-- -->
<ul>
<li class="">인덱스를 사용하지 못하고 Full Table Scan 발생</li>
<li class="">데이터가 늘어날수록 성능 저하</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3단계-jpa-specification">3단계: JPA Specification<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#3%EB%8B%A8%EA%B3%84-jpa-specification" class="hash-link" aria-label="3단계: JPA Specification에 대한 직접 링크" title="3단계: JPA Specification에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>Repository 확장:</strong></p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// Specification 사용을 위해 상속만 추가</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">UserRepository</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">extends</span><span class="token plain"> </span><span class="token class-name">JpaRepository</span><span class="token generics punctuation" style="color:#393A34">&lt;</span><span class="token generics class-name">User</span><span class="token generics punctuation" style="color:#393A34">,</span><span class="token generics"> </span><span class="token generics class-name">Long</span><span class="token generics punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">JpaSpecificationExecutor</span><span class="token generics punctuation" style="color:#393A34">&lt;</span><span class="token generics class-name">User</span><span class="token generics punctuation" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><strong>Specification 구현:</strong></p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">UserSpecification</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token class-name">Specification</span><span class="token generics punctuation" style="color:#393A34">&lt;</span><span class="token generics class-name">User</span><span class="token generics punctuation" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">filterClients</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token class-name">LocalDate</span><span class="token plain"> startDate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token class-name">LocalDate</span><span class="token plain"> endDate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token class-name">String</span><span class="token plain"> range</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token class-name">String</span><span class="token plain"> keyword</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">root</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> criteriaBuilder</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token class-name">List</span><span class="token generics punctuation" style="color:#393A34">&lt;</span><span class="token generics class-name">Predicate</span><span class="token generics punctuation" style="color:#393A34">&gt;</span><span class="token plain"> predicates </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">ArrayList</span><span class="token generics punctuation" style="color:#393A34">&lt;</span><span class="token generics punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="장점">장점<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EC%9E%A5%EC%A0%90" class="hash-link" aria-label="장점에 대한 직접 링크" title="장점에 대한 직접 링크" translate="no">​</a></h4>
<ul>
<li class=""><strong>Fetch Join을 동적으로 제어하여 N+1 문제를 해결</strong></li>
<li class=""><strong>ORM 일관성 유지</strong></li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="단점">단점<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EB%8B%A8%EC%A0%90" class="hash-link" aria-label="단점에 대한 직접 링크" title="단점에 대한 직접 링크" translate="no">​</a></h4>
<ul>
<li class="">Java와 SQL 로직 혼재</li>
<li class=""><strong>복잡한 쿼리 작성의 어려움</strong></li>
<li class=""><strong>쿼리 튜닝의 어려움</strong>
<ul>
<li class="">실제 실행되는 SQL을 보려면 로그 확인 필요</li>
<li class="">실행 계획 분석이 어려움</li>
<li class="">인덱스 힌트 사용 불가</li>
</ul>
</li>
</ul>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="성능-테스트-결과">성능 테스트 결과<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B2%B0%EA%B3%BC" class="hash-link" aria-label="성능 테스트 결과에 대한 직접 링크" title="성능 테스트 결과에 대한 직접 링크" translate="no">​</a></h4>
<p><strong>테스트 환경:</strong></p>
<ul>
<li class="">
<p>JMeter (10명 동시 접속, 50회 요청| <code>GET /admin/clients</code>)</p>
</li>
<li class="">
<p>데이터: User 1,050명 (Admin 50 + Client 1,000)</p>
<table><thead><tr><th>시나리오</th><th>평균 응답시간(ms)</th><th>최대(ms)</th><th>처리량(TPS)</th></tr></thead><tbody><tr><td>Search (키워드 검색)</td><td>20.99</td><td>77.33</td><td>55.93</td></tr><tr><td>Date Filter (날짜 필터)</td><td>15.86</td><td>44.00</td><td>55.96</td></tr><tr><td>Basic (기본 페이지네이션)</td><td>39.99</td><td>81.67</td><td>55.64</td></tr><tr><td>Complex Filter (복합 조건)</td><td>20.95</td><td>75.00</td><td>55.30</td></tr><tr><td><strong>평균</strong></td><td><strong>24.45</strong></td><td><strong>83.67</strong></td><td><strong>221.77</strong></td></tr></tbody></table>
</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-mybatis-도입">3. MyBatis 도입<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#3-mybatis-%EB%8F%84%EC%9E%85" class="hash-link" aria-label="3. MyBatis 도입에 대한 직접 링크" title="3. MyBatis 도입에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="도입-배경">도입 배경<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EB%8F%84%EC%9E%85-%EB%B0%B0%EA%B2%BD" class="hash-link" aria-label="도입 배경에 대한 직접 링크" title="도입 배경에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>왜 MyBatis인가?</strong></p>
<ol>
<li class="">SQL과 Java 코드 완전 분리 → 가독성 향상</li>
<li class="">복잡한 동적 쿼리 작성 용이</li>
<li class="">쿼리 튜닝 및 실행 계획 분석 편리</li>
<li class="">필요한 컬럼만 선택적으로 조회 가능</li>
</ol>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="초기-mybatis-구현문제점-포함">초기 MyBatis 구현(문제점 포함)<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EC%B4%88%EA%B8%B0-mybatis-%EA%B5%AC%ED%98%84%EB%AC%B8%EC%A0%9C%EC%A0%90-%ED%8F%AC%ED%95%A8" class="hash-link" aria-label="초기 MyBatis 구현(문제점 포함)에 대한 직접 링크" title="초기 MyBatis 구현(문제점 포함)에 대한 직접 링크" translate="no">​</a></h4>
<div class="language-xml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-xml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token prolog" style="color:#999988;font-style:italic">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token doctype punctuation" style="color:#393A34;font-style:italic">&lt;!</span><span class="token doctype doctype-tag" style="color:#999988;font-style:italic">DOCTYPE</span><span class="token doctype" style="color:#999988;font-style:italic"> </span><span class="token doctype name" style="color:#999988;font-style:italic">mapper</span><span class="token doctype" style="color:#999988;font-style:italic"> </span><span class="token doctype name" style="color:#999988;font-style:italic">PUBLIC</span><span class="token doctype" style="color:#999988;font-style:italic"> </span><span class="token doctype string" style="color:#e3116c;font-style:italic">"-//mybatis.org//DTD Mapper 3.0//EN"</span><span class="token doctype" style="color:#999988;font-style:italic"></span><br></span><span class="token-line" style="color:#393A34"><span class="token doctype" style="color:#999988;font-style:italic">        </span><span class="token doctype string" style="color:#e3116c;font-style:italic">"http://mybatis.org/dtd/mybatis-3-mapper.dtd"</span><span class="token doctype punctuation" style="color:#393A34;font-style:italic">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">mapper</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">namespace</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">com.example.mapper.UserMapper</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">select</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">id</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">searchClients</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">resultType</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">ClientDto</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        SELECT</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            u.user_id,</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            u.email,</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            u.name,</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            u.last_login_at,</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            c.phone_number,</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            co.company_name</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        FROM user u</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        LEFT JOIN clients c ON u.client_id = c.client_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        LEFT JOIN company co ON c.company_id = co.company_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        WHERE u.enabled = true</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">&lt;!-- 날짜 필터 --&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">if</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">startDate != null and endDate != null</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            AND u.last_login_at BETWEEN #{startDate} AND #{endDate}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">if</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">&lt;!-- 키워드 검색 - 문제점: choose 중첩 --&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">if</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">keyword != null and keyword != ''</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">choose</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">when</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">range == 'all'</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    AND (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        u.email LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        OR u.name LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        OR co.company_name LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        OR c.phone_number LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    )</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">when</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">when</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">range == 'email'</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    AND u.email LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">when</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">when</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">range == 'name'</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    AND u.name LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">when</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">when</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">range == 'company'</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    AND co.company_name LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">when</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">when</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">range == 'phone'</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    AND c.phone_number LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">when</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">choose</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">if</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        ORDER BY u.created_at DESC</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        LIMIT #{offset}, #{size}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">select</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">&lt;!-- COUNT 쿼리 - 문제점: 불필요한 JOIN --&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">select</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">id</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">countFilteredClients</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">resultType</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">long</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        SELECT COUNT(*)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        FROM user u</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        LEFT JOIN clients c ON u.client_id = c.client_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        LEFT JOIN company co ON c.company_id = co.company_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        WHERE u.enabled = true</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">if</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">startDate != null and endDate != null</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            AND u.last_login_at BETWEEN #{startDate} AND #{endDate}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">if</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">if</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">keyword != null and keyword != ''</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">choose</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">when</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">range == 'all'</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    AND (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        u.email LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        OR u.name LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        OR co.company_name LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        OR c.phone_number LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    )</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">when</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token comment" style="color:#999988;font-style:italic">&lt;!-- ... 동일한 choose 반복 --&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">choose</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">if</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">select</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">mapper</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><br></span></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="초기-mybatis-성능-테스트">초기 MyBatis 성능 테스트<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EC%B4%88%EA%B8%B0-mybatis-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8" class="hash-link" aria-label="초기 MyBatis 성능 테스트에 대한 직접 링크" title="초기 MyBatis 성능 테스트에 대한 직접 링크" translate="no">​</a></h4>
<table><thead><tr><th>시나리오</th><th>평균 응답시간(ms)</th><th>최대(ms)</th><th>처리량(TPS)</th></tr></thead><tbody><tr><td>Search</td><td>24.04</td><td>108</td><td>59.15</td></tr><tr><td>Date Filter</td><td>23.39</td><td>92</td><td>59.34</td></tr><tr><td>Basic</td><td>23.85</td><td><strong>473</strong></td><td>57.59</td></tr><tr><td>Complex Filter</td><td>23.46</td><td>112</td><td>59.32</td></tr><tr><td><strong>평균</strong></td><td><strong>24.56</strong></td><td><strong>473</strong></td><td><strong>66.70</strong></td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="문제-분석">문제 분석<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EB%AC%B8%EC%A0%9C-%EB%B6%84%EC%84%9D" class="hash-link" aria-label="문제 분석에 대한 직접 링크" title="문제 분석에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class="">
<p><strong>choose 중첩으로 인한 SQL 캐시 비효율</strong></p>
</li>
<li class="">
<p>최대 응답시간 편차 큼 (473ms)</p>
</li>
<li class="">
<p><strong>COUNT 쿼리의 불필요한 JOIN</strong></p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">COUNT</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic"># 4) 3단계 조인 후 연산</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">user</span><span class="token plain"> u </span><span class="token comment" style="color:#999988;font-style:italic"># 1) user 테이블 스캔</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LEFT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">JOIN</span><span class="token plain"> clients c </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> u</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">client_id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">client_id </span><span class="token comment" style="color:#999988;font-style:italic"># 2) 임시 테이블 생성</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LEFT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">JOIN</span><span class="token plain"> company co </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">company_id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> co</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">company_id </span><span class="token comment" style="color:#999988;font-style:italic"># 3) 임시 테이블 생성)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><br></span></code></pre></div></div>
</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-mybatis-최적화">4. MyBatis 최적화<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#4-mybatis-%EC%B5%9C%EC%A0%81%ED%99%94" class="hash-link" aria-label="4. MyBatis 최적화에 대한 직접 링크" title="4. MyBatis 최적화에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="개선-1-동적-sql-단순화">개선 1: 동적 SQL 단순화<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EA%B0%9C%EC%84%A0-1-%EB%8F%99%EC%A0%81-sql-%EB%8B%A8%EC%88%9C%ED%99%94" class="hash-link" aria-label="개선 1: 동적 SQL 단순화에 대한 직접 링크" title="개선 1: 동적 SQL 단순화에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>Before (choose 중첩) → After (OR 조건 통합):</strong></p>
<div class="language-xml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-xml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">if</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">keyword != null and keyword != ''</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    AND (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        (#{range} = 'all' AND (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            u.email LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            OR u.name LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            OR co.company_name LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            OR c.phone_number LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        ))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        OR (#{range} = 'email' AND u.email LIKE CONCAT('%', #{keyword}, '%'))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        OR (#{range} = 'name' AND u.name LIKE CONCAT('%', #{keyword}, '%'))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        OR (#{range} = 'company' AND co.company_name LIKE CONCAT('%', #{keyword}, '%'))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        OR (#{range} = 'phone' AND c.phone_number LIKE CONCAT('%', #{keyword}, '%'))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    )</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">if</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><br></span></code></pre></div></div>
<p><strong>효과:</strong></p>
<ul>
<li class=""><strong>Before:</strong> range 값에 따라 5개의 다른 SQL 생성</li>
<li class=""><strong>After:</strong> 항상 동일한 SQL 템플릿 생성 &gt; 실행 계획 재사용성 증가</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="개선-2-count-쿼리-최적화">개선 2: COUNT 쿼리 최적화<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EA%B0%9C%EC%84%A0-2-count-%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94" class="hash-link" aria-label="개선 2: COUNT 쿼리 최적화에 대한 직접 링크" title="개선 2: COUNT 쿼리 최적화에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>Before (JOIN 기반) → After (EXISTS 서브쿼리 기반)</strong></p>
<div class="language-xml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-xml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">select</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">id</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">countFilteredClients</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">resultType</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">long</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    SELECT COUNT(*)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    FROM user u</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    WHERE u.enabled = true</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">if</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">startDate != null and endDate != null</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        AND u.last_login_at BETWEEN #{startDate} AND #{endDate}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">if</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">if</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">test</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">keyword != null and keyword != ''</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        AND (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            (#{range} = 'all' AND (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                u.email LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                OR u.name LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                OR EXISTS (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    SELECT 1 FROM clients c</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    JOIN company co ON c.company_id = co.company_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    WHERE c.client_id = u.client_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    AND co.company_name LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                )</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                OR EXISTS (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    SELECT 1 FROM clients c</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    WHERE c.client_id = u.client_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    AND c.phone_number LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                )</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            ))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            OR (#{range} = 'email' AND u.email LIKE CONCAT('%', #{keyword}, '%'))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            OR (#{range} = 'name' AND u.name LIKE CONCAT('%', #{keyword}, '%'))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            OR (#{range} = 'company' AND EXISTS (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                SELECT 1 FROM clients c</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                JOIN company co ON c.company_id = co.company_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                WHERE c.client_id = u.client_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                AND co.company_name LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            ))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            OR (#{range} = 'phone' AND EXISTS (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                SELECT 1 FROM clients c</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                WHERE c.client_id = u.client_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                AND c.phone_number LIKE CONCAT('%', #{keyword}, '%')</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            ))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        )</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">if</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">select</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="exists-vs-join-성능-비교">EXISTS vs JOIN 성능 비교<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#exists-vs-join-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90" class="hash-link" aria-label="EXISTS vs JOIN 성능 비교에 대한 직접 링크" title="EXISTS vs JOIN 성능 비교에 대한 직접 링크" translate="no">​</a></h3>
<ol>
<li class="">
<p><strong>JOIN 방식:</strong></p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">COUNT</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">user</span><span class="token plain"> u</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LEFT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">JOIN</span><span class="token plain"> clients c </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> u</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">client_id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">client_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LEFT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">JOIN</span><span class="token plain"> company co </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">company_id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> co</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">company_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> co</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">company_name </span><span class="token operator" style="color:#393A34">LIKE</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'%ABC%'</span><br></span></code></pre></div></div>
<p><strong>실행 과정:</strong></p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"> 1. user 테이블 전체 스캔</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> 2. clients 테이블과 조인 (1,000건)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> 3. company 테이블과 조인 (1,000건)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> 4. WHERE 조건 필터링</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> 5. COUNT(*) 계산</span><br></span></code></pre></div></div>
</li>
<li class="">
<p><strong>EXISTS 방식:</strong></p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">COUNT</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">user</span><span class="token plain"> u</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">EXISTS</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> clients c</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">JOIN</span><span class="token plain"> company co </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">company_id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> co</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">company_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">client_id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> u</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">client_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">AND</span><span class="token plain"> co</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">company_name </span><span class="token operator" style="color:#393A34">LIKE</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'%ABC%'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p><strong>실행 과정:</strong></p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">1. user 테이블 전체 스캔</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">2. 각 행마다 EXISTS 서브쿼리 실행</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    2.1. 조건 만족 시 즉시 TRUE 반환 (조기 종료)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    2.2. 첫 번째 매칭 행 발견 시 더 이상 검색 안 함</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">3. COUNT(*) 계산</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">→ 필요한 만큼만 조인</span><br></span></code></pre></div></div>
</li>
</ol>
<p><strong>성능 차이:</strong></p>
<ul>
<li class="">JOIN 방식: 1,000건 조인 → 1,000건 처리</li>
<li class="">EXISTS 방식: 조기 종료 → 평균 150건 처리</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-최종-성능-비교">5. 최종 성능 비교<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#5-%EC%B5%9C%EC%A2%85-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90" class="hash-link" aria-label="5. 최종 성능 비교에 대한 직접 링크" title="5. 최종 성능 비교에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="mybatis-최적화-후-테스트-결과">MyBatis 최적화 후 테스트 결과<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#mybatis-%EC%B5%9C%EC%A0%81%ED%99%94-%ED%9B%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B2%B0%EA%B3%BC" class="hash-link" aria-label="MyBatis 최적화 후 테스트 결과에 대한 직접 링크" title="MyBatis 최적화 후 테스트 결과에 대한 직접 링크" translate="no">​</a></h3>
<table><thead><tr><th>시나리오</th><th>평균 응답시간(ms)</th><th>최대(ms)</th><th>처리량(TPS)</th></tr></thead><tbody><tr><td>Search</td><td>17.16</td><td>48</td><td>68.95</td></tr><tr><td>Date Filter</td><td>15.89</td><td>32</td><td>68.99</td></tr><tr><td>Basic</td><td>17.23</td><td>54</td><td>68.64</td></tr><tr><td>Complex Filter</td><td>17.11</td><td>48</td><td>69.02</td></tr><tr><td><strong>평균</strong></td><td><strong>15.51</strong></td><td><strong>54</strong></td><td><strong>273.34</strong></td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="전체-비교">전체 비교<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EC%A0%84%EC%B2%B4-%EB%B9%84%EA%B5%90" class="hash-link" aria-label="전체 비교에 대한 직접 링크" title="전체 비교에 대한 직접 링크" translate="no">​</a></h3>
<p><img decoding="async" loading="lazy" alt="image" src="https://kkanghhee.github.io/assets/images/mybatis_%EC%84%B1%EB%8A%A5-d41760d7fc75b2bd65b18a50a5df9eb3.png" width="1653" height="993" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="개선-효과">개선 효과<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EA%B0%9C%EC%84%A0-%ED%9A%A8%EA%B3%BC" class="hash-link" aria-label="개선 효과에 대한 직접 링크" title="개선 효과에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>MyBatis 개선 전 대비:</strong></p>
<ul>
<li class="">평균 응답시간 <strong>36.8% 감소</strong> (24.56 → 15.51ms)</li>
<li class="">최대 응답시간 <strong>88.5% 감소</strong> (473 → 54ms)</li>
<li class="">처리량 <strong>3.9배 증가</strong> (66.7 → 273 TPS)</li>
</ul>
<p><strong>JPA Specification 대비:</strong></p>
<ul>
<li class="">평균 응답시간 <strong>36.5% 감소</strong> (24.45 → 15.51ms)</li>
<li class="">최대 응답시간 <strong>35.5% 감소</strong> (83.67 → 54ms)</li>
<li class="">Basic 요청 <strong>2.3배 빠름</strong> (39.99 → 17.23ms)</li>
<li class="">처리량 <strong>23% 향상</strong> (221.77 → 273 TPS)</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="최종-아키텍처-설계혼합">최종 아키텍처 설계(혼합)<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%EC%B5%9C%EC%A2%85-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%84%A4%EA%B3%84%ED%98%BC%ED%95%A9" class="hash-link" aria-label="최종 아키텍처 설계(혼합)에 대한 직접 링크" title="최종 아키텍처 설계(혼합)에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>JPA를 사용할 때:</strong></p>
<ul>
<li class="">단순 CRUD 작업</li>
<li class="">엔티티 간 관계 활용이 중요한 경우</li>
<li class="">트랜잭션 내에서 엔티티 변경 추적이 필요한 경우</li>
<li class="">2~3개 이하의 조건 조합</li>
</ul>
<p><strong>MyBatis를 사용할 때:</strong></p>
<ul>
<li class="">Window Function 및 복잡한 서브쿼리</li>
<li class="">다중 테이블 조인 (3개 이상)</li>
<li class="">성능 최적화가 중요한 대용량 조회</li>
<li class="">쿼리 튜닝이 필요한 경우</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="6-정리">6. 정리<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#6-%EC%A0%95%EB%A6%AC" class="hash-link" aria-label="6. 정리에 대한 직접 링크" title="6. 정리에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="핵심-요약">핵심 요약<a href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy#%ED%95%B5%EC%8B%AC-%EC%9A%94%EC%95%BD" class="hash-link" aria-label="핵심 요약에 대한 직접 링크" title="핵심 요약에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>최종 전략:</strong></p>
<ul>
<li class=""><strong>JPA</strong>: 단순 CRUD, 엔티티 관계 활용</li>
<li class=""><strong>MyBatis</strong>: 복잡한 검색, 통계, 성능 최적화</li>
</ul>
<p><strong>성능 개선:</strong></p>
<ul>
<li class="">평균 응답시간 <strong>36.5% 감소</strong></li>
<li class="">최대 응답시간 <strong>88.5% 감소</strong></li>
<li class="">처리량 <strong>23% 향상</strong></li>
</ul>
<p><strong>참고 자료</strong></p>
<ul>
<li class=""><a href="https://mybatis.org/mybatis-3/" target="_blank" rel="noopener noreferrer" class="">MyBatis Documentation</a></li>
<li class=""><a href="https://dev.mysql.com/doc/refman/8.0/en/subquery-optimization.html" target="_blank" rel="noopener noreferrer" class="">MySQL Performance Tuning - EXISTS vs JOIN</a></li>
</ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Spring Boot에서 Session 인증을 커스텀하는 이유와 실전 구현]]></title>
            <link>https://KKangHHee.github.io/blog/spring-boot-custom-session-authentication</link>
            <guid>https://KKangHHee.github.io/blog/spring-boot-custom-session-authentication</guid>
            <pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate>
            <description><![CDATA["왜 Spring Security의 기본 Form Login을 쓰지 않고 직접 구현할까?"]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"왜 Spring Security의 기본 Form Login을 쓰지 않고 직접 구현할까?"</p>
</blockquote>
<p>이 글에서는 <strong>Admin/Customer 통합 환경</strong>에서 <strong>JSON 기반 로그인 + 중복 로그인 제어</strong>를 구현하며 겪었던 <strong>표준 방식으로는 안 되는 이유</strong>와 <strong>어떻게 우회했는지</strong>를 기록합니다.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-왜-커스텀-세션-인증">1. 왜 커스텀 세션 인증?<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#1-%EC%99%9C-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%84%B8%EC%85%98-%EC%9D%B8%EC%A6%9D" class="hash-link" aria-label="1. 왜 커스텀 세션 인증?에 대한 직접 링크" title="1. 왜 커스텀 세션 인증?에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="프로젝트-요구사항">프로젝트 요구사항<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD" class="hash-link" aria-label="프로젝트 요구사항에 대한 직접 링크" title="프로젝트 요구사항에 대한 직접 링크" translate="no">​</a></h3>
<table><thead><tr><th>항목</th><th>요구사항</th><th>표준 FormLogin 가능 여부</th></tr></thead><tbody><tr><td><strong>JSON 기반 로그인</strong></td><td><code>{"email": "...", "password": "..."}</code></td><td>복잡</td></tr><tr><td><strong>중복 로그인 제어</strong></td><td>동일 계정 1명만 로그인 허용</td><td>가능</td></tr><tr><td><strong>계정 잠금 처리</strong></td><td>5회 실패 시 계정 잠김</td><td>커스텀 필요</td></tr><tr><td><strong>첫 로그인 체크</strong></td><td>첫 로그인 시 비밀번호 변경 강제</td><td>커스텀 필요</td></tr><tr><td><strong>타입별 응답</strong></td><td>Admin/Customer별 다른 응답</td><td>커스텀 필요</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="표준-formlogin의-한계">표준 FormLogin의 한계<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#%ED%91%9C%EC%A4%80-formlogin%EC%9D%98-%ED%95%9C%EA%B3%84" class="hash-link" aria-label="표준 FormLogin의 한계에 대한 직접 링크" title="표준 FormLogin의 한계에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>Spring Security의 기본 FormLogin 필터 체인:</strong></p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">UsernamePasswordAuthenticationFilter</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    → AuthenticationManager</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    → UserDetailsService (사용자 조회)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    → SuccessHandler / FailureHandler</span><br></span></code></pre></div></div>
<p><strong>문제점:</strong></p>
<ol>
<li class=""><strong>필터 단계에서는 비즈니스 로직 주입이 복잡</strong>
<ul>
<li class="">필터 기반 인증은 커스터마이징이 어렵고, 특히 AuthenticationFailureHandler만으로는 DB 상태 변경(잠금 등)을 처리하기 직관적이지 않음.</li>
</ul>
</li>
<li class=""><strong>실패 카운트를 어디서 관리할까?</strong>
<ul>
<li class="">필터는 Stateless하므로 DB 업데이트 로직을 끼워넣기 애매함</li>
</ul>
</li>
<li class=""><strong>첫 로그인 체크는 인증 성공 이후에 판단해야 함</strong>
<ul>
<li class="">필터 체인에서는 순서상 어색함</li>
</ul>
</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="해결-방안-controller--sessionauthenticationstrategy-조합">해결 방안: Controller + SessionAuthenticationStrategy 조합<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#%ED%95%B4%EA%B2%B0-%EB%B0%A9%EC%95%88-controller--sessionauthenticationstrategy-%EC%A1%B0%ED%95%A9" class="hash-link" aria-label="해결 방안: Controller + SessionAuthenticationStrategy 조합에 대한 직접 링크" title="해결 방안: Controller + SessionAuthenticationStrategy 조합에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>필터를 우회하여 Controller에서 비즈니스 로직을 선행 처리하되,
인증 결과는 Security의 표준 컴포넌트를 이용해 세션 시스템에 등록
수동으로 Authentication 객체를 생성하고,
SecurityContextRepository를 통해 세션에 영속화하는 일련의 과정을 코드로 구현</p>
</blockquote>
<p><strong>[기존] Filter Chain 방식</strong></p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">Request → Filter → AuthenticationManager → Response</span><br></span></code></pre></div></div>
<p><strong>[개선] Controller 방식</strong></p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">Request → Controller → Service (비즈니스 로직)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    → 수동으로 AuthenticationManager 호출</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    → SessionAuthenticationStrategy 호출</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    → SecurityContext 저장 → Response</span><br></span></code></pre></div></div>
<blockquote>
<p><strong>Spring Security의 세션 정책을 그대로 활용하는 방안</strong></p>
</blockquote>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-인증-구현">2. 인증 구현<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#2-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84" class="hash-link" aria-label="2. 인증 구현에 대한 직접 링크" title="2. 인증 구현에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="전체-인증-플로우">전체 인증 플로우<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#%EC%A0%84%EC%B2%B4-%EC%9D%B8%EC%A6%9D-%ED%94%8C%EB%A1%9C%EC%9A%B0" class="hash-link" aria-label="전체 인증 플로우에 대한 직접 링크" title="전체 인증 플로우에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">1. [Client] Request</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">2. [Controller] 요청 수신</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">3. [LoginService] 검증</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		3.1 사용자 확인</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		3.2 계정 활성 여부</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		3.3 비밀번호 검증</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		3.4 첫 로그인 검증</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">4. [performSecurityAuthentication]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		4.1 AuthenticationManager.authenticate() 호출</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		4.2 SessionAuthenticationStrategy.onAuthentication() 호출</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">				4.2.1) 중복 세션 제어 | 세션 고정 공격 방지 |</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">				4.2.2) SessionRegistry 등록</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		4.3 SecurityContext 생성 및 설정</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		4.4 HttpSession에 SecurityContext 저장</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">5. [Response]</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="단계별-설명">단계별 설명<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#%EB%8B%A8%EA%B3%84%EB%B3%84-%EC%84%A4%EB%AA%85" class="hash-link" aria-label="단계별 설명에 대한 직접 링크" title="단계별 설명에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>1단계: 사용자 요청 수신</strong></p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token annotation punctuation" style="color:#393A34">@PostMapping</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/login"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token class-name">ResponseEntity</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">login</span><span class="token punctuation" style="color:#393A34">(</span><span class="token annotation punctuation" style="color:#393A34">@RequestBody</span><span class="token plain"> </span><span class="token class-name">LoginDto</span><span class="token plain"> req</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// Controller에서 직접 처리</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><strong>2단계: 비즈니스 검증 (LoginService)</strong></p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">isLocked</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">AccountLockedException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"계정이 잠겼습니다."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">isFirstLogin</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">FirstLoginResponse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"비밀번호를 변경하세요."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">passwordEncoder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">matches</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getPassword</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getPassword</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">incrementFailCount</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getFailCount</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">lock</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    userRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">BadCredentialsException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"아이디 또는 비밀번호 틀렸습니다."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><strong>3단계: Spring Security 인증 실행</strong></p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// AuthenticationManager를 수동으로 호출</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name">UsernamePasswordAuthenticationToken</span><span class="token plain"> authToken </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">UsernamePasswordAuthenticationToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getEmail</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getPassword</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getAuthorities</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name">Authentication</span><span class="token plain"> authentication </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> authenticationManager</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">authenticate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">authToken</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><strong>4단계: 세션 정책 적용</strong></p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// SessionAuthenticationStrategy 수동 호출</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">sessionAuthenticationStrategy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">onAuthentication</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">authentication</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><strong>5단계: SecurityContext 저장</strong></p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name">SecurityContext</span><span class="token plain"> context </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> securityContextHolderStrategy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">createEmptyContext</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setAuthentication</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">authentication</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name">SecurityContextHolder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setContext</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">securityContextRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">saveContext</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-핵심-컴포넌트-구현">3. 핵심 컴포넌트 구현<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#3-%ED%95%B5%EC%8B%AC-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B5%AC%ED%98%84" class="hash-link" aria-label="3. 핵심 컴포넌트 구현에 대한 직접 링크" title="3. 핵심 컴포넌트 구현에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="각-컴포넌트의-역할"><strong>각 컴포넌트의 역할</strong><a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#%EA%B0%81-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-%EC%97%AD%ED%95%A0" class="hash-link" aria-label="각-컴포넌트의-역할에 대한 직접 링크" title="각-컴포넌트의-역할에 대한 직접 링크" translate="no">​</a></h3>
<table><thead><tr><th>컴포넌트</th><th>역할</th><th>실무 의미</th></tr></thead><tbody><tr><td><code>HttpSessionEventPublisher</code></td><td>세션 생성/삭제 이벤트를 Spring Security에 전달</td><td>로그아웃/타임아웃 시 SessionRegistry 자동 정리</td></tr><tr><td><code>SessionRegistry</code></td><td>세션 및 사용자 상태 관리</td><td>Admin UI에서 "현재 접속 중인 사용자" 목록 확인 가능</td></tr><tr><td><code>SessionAuthenticationStrategy</code></td><td>세션 정책 체인</td><td>중복 로그인 제어 + 세션 고정 공격 방지 + 세션 등록</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-user-entity">1) User Entity<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#1-user-entity" class="hash-link" aria-label="1) User Entity에 대한 직접 링크" title="1) User Entity에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token annotation punctuation" style="color:#393A34">@Entity</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token annotation punctuation" style="color:#393A34">@Table</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">name </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"users"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token annotation punctuation" style="color:#393A34">@EqualsAndHashCode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">of </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"email"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> callSuper </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token annotation punctuation" style="color:#393A34">@NoArgsConstructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">access </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token class-name">AccessLevel</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">PROTECTED</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">User</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">UserDetails</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">//...</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="equalshashcode를-오버라이딩-하는-이유"><code>equals</code>/<code>hashCode</code>를 오버라이딩 하는 이유<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#equalshashcode%EB%A5%BC-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0" class="hash-link" aria-label="equalshashcode를-오버라이딩-하는-이유에 대한 직접 링크" title="equalshashcode를-오버라이딩-하는-이유에 대한 직접 링크" translate="no">​</a></h4>
<ul>
<li class="">
<p>Spring Security의 <code>SessionRegistry</code>는 <strong>동일 사용자의 세션을 추적</strong>하기 위해</p>
</li>
<li class="">
<p>내부적으로 <code>Map&lt;Object, Set&lt;SessionInformation&gt;&gt;</code>을 사용하기 때문에 <code>equals</code>/<code>hashCode</code>를 오버라이딩해 해야 함</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// SessionRegistryImpl 내부</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token class-name">ConcurrentMap</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> principals </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">ConcurrentHashMap</span><span class="token generics punctuation" style="color:#393A34">&lt;</span><span class="token generics punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">registerNewSession</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token plain"> sessionId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">Object</span><span class="token plain"> principal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name">Set</span><span class="token plain"> sessions </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> principals</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">principal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 여기서 principal.equals()로 동일 사용자 판단</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
</li>
<li class="">
<p><code>equals</code>를 오버라이딩하지 않으면: <strong>객체 참조 비교</strong>로 동작</p>
</li>
<li class="">
<p>SessionRegistry에 같은 사용자가 여러 번 등록되어, 중복 로그인 제어 X</p>
</li>
</ul>
<hr>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-loginservice">2) LoginService<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#2-loginservice" class="hash-link" aria-label="2) LoginService에 대한 직접 링크" title="2) LoginService에 대한 직접 링크" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-로그인-메인-로직">1) 로그인 메인 로직<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#1-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%A9%94%EC%9D%B8-%EB%A1%9C%EC%A7%81" class="hash-link" aria-label="1) 로그인 메인 로직에 대한 직접 링크" title="1) 로그인 메인 로직에 대한 직접 링크" translate="no">​</a></h4>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token annotation punctuation" style="color:#393A34">@Transactional</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token class-name">LoginResponse</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">login</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">LoginDto</span><span class="token plain"> req</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">HttpServletRequest</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                </span><span class="token class-name">HttpServletResponse</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 1. 사용자 조회</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">User</span><span class="token plain"> user </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> userRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findByEmail</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getEmail</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">orElseThrow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">UsernameNotFoundException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"사용자를 찾을 수 없습니다."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 2. 계정 잠김 체크</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">isLocked</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">AccountLockedException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"계정이 잠겼습니다. 관리자에게 문의하세요."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 3. 계정 활성화 체크</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">isEnabled</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">DisabledException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"비활성화된 계정입니다."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 4. 비밀번호 검증</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">passwordEncoder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">matches</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getPassword</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getPassword</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">incrementFailCount</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            userRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">isLocked</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">AccountLockedException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token string" style="color:#e3116c">"5회 실패로 계정이 잠겼습니다. 관리자에게 문의하세요."</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">BadCredentialsException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token class-name">String</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">format</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"아이디 또는 비밀번호가 틀렸습니다. (남은 시도: %d회)"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                              </span><span class="token number" style="color:#36acaa">5</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getFailCount</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 5. 첫 로그인 체크</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">isFirstLogin</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token class-name">LoginResponse</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">firstLogin</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"비밀번호를 변경해야 합니다."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 6. Spring Security 인증 실행</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token function" style="color:#d73a49">performSecurityAuthentication</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 7. 로그인 성공 처리</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">resetFailCount</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        userRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token class-name">LoginResponse</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">success</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-spring-security-인증-수행">2) Spring Security 인증 수행<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#2-spring-security-%EC%9D%B8%EC%A6%9D-%EC%88%98%ED%96%89" class="hash-link" aria-label="2) Spring Security 인증 수행에 대한 직접 링크" title="2) Spring Security 인증 수행에 대한 직접 링크" translate="no">​</a></h4>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">performSecurityAuthentication</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">LoginDto</span><span class="token plain"> req</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">User</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                                </span><span class="token class-name">HttpServletRequest</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                                </span><span class="token class-name">HttpServletResponse</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 1. AuthenticationManager를 통한 인증</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">UsernamePasswordAuthenticationToken</span><span class="token plain"> authToken </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">UsernamePasswordAuthenticationToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getEmail</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getPassword</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getAuthorities</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">Authentication</span><span class="token plain"> authentication </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> authenticationManager</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">authenticate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">authToken</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 2. SessionAuthenticationStrategy 호출 (세션 정책 적용) → SessionRegistry 등록</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        sessionAuthenticationStrategy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">onAuthentication</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">authentication</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 3. SecurityContext 생성 및 설정</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">SecurityContext</span><span class="token plain"> context </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> securityContextHolderStrategy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">createEmptyContext</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setAuthentication</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">authentication</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">SecurityContextHolder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setContext</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 4. SecurityContextRepository에 저장 (HttpSession에 저장됨)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        securityContextRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">saveContext</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<hr>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-sessionconfig---세션-정책-설정">3) SessionConfig - 세션 정책 설정<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#3-sessionconfig---%EC%84%B8%EC%85%98-%EC%A0%95%EC%B1%85-%EC%84%A4%EC%A0%95" class="hash-link" aria-label="3) SessionConfig - 세션 정책 설정에 대한 직접 링크" title="3) SessionConfig - 세션 정책 설정에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>1) HttpSessionEventPublisher</strong></p>
<blockquote>
<p>세션 생성/삭제 이벤트를 Spring Security에 전달</p>
</blockquote>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token class-name">HttpSessionEventPublisher</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">httpSessionEventPublisher</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">HttpSessionEventPublisher</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token annotation punctuation" style="color:#393A34">@Override</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">sessionCreated</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">HttpSessionEvent</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token keyword" style="color:#00009f">super</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sessionCreated</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token annotation punctuation" style="color:#393A34">@Override</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">sessionDestroyed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">HttpSessionEvent</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token keyword" style="color:#00009f">super</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sessionDestroyed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><strong>2) 서블릿 리스너에 등록 (정상 동작 보장)</strong></p>
<blockquote>
<p><strong>(1)HttpSessionEventPublisher</strong>을 ServletListener에 등록해야 로그아웃 시 SessionRegistry에서 세션이 제거됨
하지 않을 경우, 세션이 누적</p>
</blockquote>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token class-name">ServletListenerRegistrationBean</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token function" style="color:#d73a49">httpSessionEventPublisherRegistration</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">ServletListenerRegistrationBean</span><span class="token generics punctuation" style="color:#393A34">&lt;</span><span class="token generics punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">httpSessionEventPublisher</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><strong>3) SessionRegistry</strong></p>
<blockquote>
<p>세션 및 사용자 상태 관리 핵심 컴포넌트</p>
</blockquote>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token class-name">SessionRegistry</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">sessionRegistry</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">SessionRegistryImpl</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token annotation punctuation" style="color:#393A34">@Override</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">registerNewSession</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token plain"> sessionId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">Object</span><span class="token plain"> principal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token keyword" style="color:#00009f">super</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">registerNewSession</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sessionId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> principal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token annotation punctuation" style="color:#393A34">@Override</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">removeSessionInformation</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">String</span><span class="token plain"> sessionId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token class-name">SessionInformation</span><span class="token plain"> info </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getSessionInformation</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sessionId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token keyword" style="color:#00009f">super</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">removeSessionInformation</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sessionId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token class-name">String</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">extractPrincipalName</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">Object</span><span class="token plain"> principal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">principal </span><span class="token keyword" style="color:#00009f">instanceof</span><span class="token plain"> </span><span class="token class-name">User</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">User</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> principal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getEmail</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> principal</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toString</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><strong>4) SessionAuthenticationStrategy</strong></p>
<blockquote>
<p>인증 성공 시 작동하는 세션 정책</p>
</blockquote>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token class-name">SessionAuthenticationStrategy</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">sessionAuthenticationStrategy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token class-name">SessionRegistry</span><span class="token plain"> sessionRegistry</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 3-1) 동시 로그인 개수 제한</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">ConcurrentSessionControlAuthenticationStrategy</span><span class="token plain"> concurrentControl </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">ConcurrentSessionControlAuthenticationStrategy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sessionRegistry</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        concurrentControl</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setMaximumSessions</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 동일 계정 1명만</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        concurrentControl</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setExceptionIfMaximumExceeded</span><span class="token punctuation" style="color:#393A34">(</span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 기존 세션 만료</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 3-2) 세션 고정 공격 방지 (세션 ID 재발급)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">SessionFixationProtectionStrategy</span><span class="token plain"> sessionFixation </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">SessionFixationProtectionStrategy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        sessionFixation</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setMigrateSessionAttributes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 3-3) SessionRegistry에 세션 등록</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">RegisterSessionAuthenticationStrategy</span><span class="token plain"> registerSession </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RegisterSessionAuthenticationStrategy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sessionRegistry</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 3가지 전략을 Composite로 통합</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">CompositeSessionAuthenticationStrategy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">Arrays</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">asList</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            concurrentControl</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            sessionFixation</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            registerSession</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<hr>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-securityfilterchain-설정">4) SecurityFilterChain 설정<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#4-securityfilterchain-%EC%84%A4%EC%A0%95" class="hash-link" aria-label="4) SecurityFilterChain 설정에 대한 직접 링크" title="4) SecurityFilterChain 설정에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>컨트롤러는 "로그인 시 딱 한 번 실행"되어 "세션 생성 및 전략 수동 호출"함
필터는 "로그인 이후 API"에 "세션 유효성 검증 및 정책 담당"</p>
</blockquote>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token annotation punctuation" style="color:#393A34">@Bean</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token class-name">SecurityFilterChain</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">securityFilterChain</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">HttpSecurity</span><span class="token plain"> http</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">throws</span><span class="token plain"> </span><span class="token class-name">Exception</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    http</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">cors</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cors </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> cors</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">configurationSource</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">corsConfigurationSource</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">csrf</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">csrf </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> csrf</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">disable</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// REST API는 CSRF 불필요</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// SecurityContext 저장 방식 설정</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">securityContext</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">context </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> context</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">securityContextRepository</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">securityContextRepository</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">requireExplicitSave</span><span class="token punctuation" style="color:#393A34">(</span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 명시적 저장 (Controller에서 수동 저장)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 세션 관리 설정</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sessionManagement</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">session </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> session</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sessionFixation</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">changeSessionId</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 세션 고정 공격 방지</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sessionCreationPolicy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">SessionCreationPolicy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">IF_REQUIRED</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">maximumSessions</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 동일 계정 1명만</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">maxSessionsPreventsLogin</span><span class="token punctuation" style="color:#393A34">(</span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 기존 세션 만료 방식</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sessionRegistry</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">sessionRegistry</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">expiredSessionStrategy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">customSessionExpiredStrategy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 만료 시 처리</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 예외 처리</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">exceptionHandling</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ex </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> ex</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">authenticationEntryPoint</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">authenticationDeniedHandler</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 401</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">accessDeniedHandler</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">authorizationDeniedHandler</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 403</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 커스텀 필터 추가</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">addFilterBefore</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">sessionExpiredFilter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">SecurityContextPersistenceFilter</span><span class="token punctuation" style="color:#393A34">.</span><span class="token keyword" style="color:#00009f">class</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">addFilterBefore</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">improvedLogoutFilter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">LogoutFilter</span><span class="token punctuation" style="color:#393A34">.</span><span class="token keyword" style="color:#00009f">class</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> http</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">build</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><strong>주요 설정 설명</strong></p>
<ul>
<li class="">
<p><strong>requireExplicitSave</strong></p>
<ul>
<li class="">기본값(false)은 SecurityContextPersistenceFilter가 자동 저장</li>
<li class="">true로 설정 시 Controller에서 수동으로 saveContext() 호출 필요</li>
<li class="">true로 변경하지 않으면, SecurityContext가 두 번 저장됨</li>
</ul>
</li>
<li class="">
<p><strong>sessionFixation().changeSessionId()</strong></p>
<ul>
<li class="">세션 고정 공격(Session Fixation) 방지</li>
<li class="">로그인 성공 시 세션 ID를 새로 발급하여 공격자가 탈취한 세션 ID 무효화</li>
</ul>
</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-동작-검증">4. 동작 검증<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#4-%EB%8F%99%EC%9E%91-%EA%B2%80%EC%A6%9D" class="hash-link" aria-label="4. 동작 검증에 대한 직접 링크" title="4. 동작 검증에 대한 직접 링크" translate="no">​</a></h2>
<p><strong>1) 정상 로그인</strong></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">POST /api/login</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">{</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  "email": "user@example.com",</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  "password": "password123"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">→ 200 OK</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">{</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  "status": "SUCCESS",</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  "user": {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    "email": "user@example.com",</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    "type": "CUSTOMER"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre></div></div>
<p><strong>2) 5회 실패 → 계정 잠김</strong></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># 1~4회 실패</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">→ 400 Bad Request</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">{</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  "error": "아이디 또는 비밀번호가 틀렸습니다. (남은 시도: 4회)"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># 5회 실패</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">→ 423 Locked</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">{</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  "error": "5회 실패로 계정이 잠겼습니다."</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre></div></div>
<p><strong>3) 첫 로그인</strong></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">POST /api/login</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">{</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  "email": "newuser@example.com",</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  "password": "temp123"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">→ 200 OK</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">{</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  "status": "FIRST_LOGIN",</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  "message": "비밀번호를 변경해야 합니다."</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre></div></div>
<p><strong>4) 중복 로그인 제어</strong></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># 사용자 A: 로그인 성공 (Session ID: abc123)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># 사용자 B: 같은 계정으로 로그인 시도</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">→ 사용자 A의 세션이 만료됨</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">→ 사용자 A가 API 호출 시 401 Unauthorized 응답</span><br></span></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-정리">5. 정리<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#5-%EC%A0%95%EB%A6%AC" class="hash-link" aria-label="5. 정리에 대한 직접 링크" title="5. 정리에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="핵심-요약">핵심 요약<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#%ED%95%B5%EC%8B%AC-%EC%9A%94%EC%95%BD" class="hash-link" aria-label="핵심 요약에 대한 직접 링크" title="핵심 요약에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>JSON 기반 로그인 + 계정 잠금 + 첫 로그인 체크 + ROLE별 응답<br>
<!-- -->이 4가지를 FormLogin 필터에서 처리하기는 너무 복잡해습니다.</p>
</blockquote>
<p><strong>Controller 방식으로 전환한 후:</strong></p>
<ul>
<li class="">비즈니스 로직이 Service 레이어에 명확히 분리됨</li>
<li class="">Spring Security의 세션 정책은 그대로 활용</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="트레이드오프">트레이드오프<a href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication#%ED%8A%B8%EB%A0%88%EC%9D%B4%EB%93%9C%EC%98%A4%ED%94%84" class="hash-link" aria-label="트레이드오프에 대한 직접 링크" title="트레이드오프에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class="">
<p><strong>장점:</strong></p>
<ul>
<li class="">비즈니스 로직 자유도 높음</li>
<li class="">코드 가독성 좋음</li>
</ul>
</li>
<li class="">
<p><strong>단점:</strong></p>
<ul>
<li class="">Spring Security의 "자동화"를 포기</li>
<li class=""><code>SessionAuthenticationStrategy</code> 수동 호출 필요</li>
<li class=""><code>SecurityContext</code> 수동 저장 필요</li>
</ul>
</li>
</ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[기록 블로그]]></title>
            <link>https://KKangHHee.github.io/blog/index</link>
            <guid>https://KKangHHee.github.io/blog/index</guid>
            <pubDate>Thu, 13 Jul 2000 00:00:00 GMT</pubDate>
            <description><![CDATA[궁금한거 정리해놓은 곳]]></description>
            <content:encoded><![CDATA[<p>안녕하세요!<br>
<strong>신입 개발자 신강희</strong>입니다 😊</p>
<blockquote>
<p>✨ <em>“기록은 가장 강력한 성장 도구다”</em></p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-게시글-목록">📚 게시글 목록<a href="https://kkanghhee.github.io/blog/index#-%EA%B2%8C%EC%8B%9C%EA%B8%80-%EB%AA%A9%EB%A1%9D" class="hash-link" aria-label="📚 게시글 목록에 대한 직접 링크" title="📚 게시글 목록에 대한 직접 링크" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-redis--동시성">🔴 Redis &amp; 동시성<a href="https://kkanghhee.github.io/blog/index#-redis--%EB%8F%99%EC%8B%9C%EC%84%B1" class="hash-link" aria-label="🔴 Redis &amp; 동시성에 대한 직접 링크" title="🔴 Redis &amp; 동시성에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class=""><a class="" href="https://kkanghhee.github.io/blog/redis-concurrency">레디스는 싱글스레드인데 왜 동시성 제어가 필요할까?</a></li>
<li class=""><a class="" href="https://kkanghhee.github.io/blog/redis-email-verification">이메일 인증에서 Redis가 강점을 갖는 이유: TTL과 원자 연산</a></li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-spring--backend">🟢 Spring &amp; Backend<a href="https://kkanghhee.github.io/blog/index#-spring--backend" class="hash-link" aria-label="🟢 Spring &amp; Backend에 대한 직접 링크" title="🟢 Spring &amp; Backend에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class=""><a class="" href="https://kkanghhee.github.io/blog/spring-boot-custom-session-authentication">Spring Boot에서 Session 인증을 커스텀하는 이유와 실전 구현</a></li>
<li class=""><a class="" href="https://kkanghhee.github.io/blog/jpa-mybatis-hybrid-strategy">복잡한 검색 쿼리, JPA vs MyBatis 성능 비교와 하이브리드 전략</a></li>
<li class=""><a class="" href="https://kkanghhee.github.io/blog/spring-event-async-email-optimization">이메일 발송 API 응답 속도 개선: Spring Event와 비동기 처리</a></li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-docker--infra">🟣 Docker / Infra<a href="https://kkanghhee.github.io/blog/index#-docker--infra" class="hash-link" aria-label="🟣 Docker / Infra에 대한 직접 링크" title="🟣 Docker / Infra에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class=""><em>(새로운 글을 준비 중이에요 🚀)</em></li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-about-me">📌 About Me<a href="https://kkanghhee.github.io/blog/index#-about-me" class="hash-link" aria-label="📌 About Me에 대한 직접 링크" title="📌 About Me에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class="">🎓 컴퓨터정보공학 전공</li>
<li class="">💻 백엔드 개발자 지망</li>
<li class="">📈 꾸준히 성장 중</li>
</ul>
<hr>]]></content:encoded>
        </item>
    </channel>
</rss>