<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>riverpower6_g.log</title>
        <link>https://velog.io/</link>
        <description>기록</description>
        <lastBuildDate>Sat, 25 Apr 2026 06:20:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>riverpower6_g.log</title>
            <url>https://velog.velcdn.com/images/riverpower6_g/profile/f1c12b9b-5e32-48a6-88ac-f5fef735c0e5/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. riverpower6_g.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/riverpower6_g" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[PCCP 응시 후기]]></title>
            <link>https://velog.io/@riverpower6_g/PCCP-%EC%9D%91%EC%8B%9C-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@riverpower6_g/PCCP-%EC%9D%91%EC%8B%9C-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sat, 25 Apr 2026 06:20:18 GMT</pubDate>
            <description><![CDATA[<p>&nbsp;지난 2026년 4월 19일, <strong>PCCP</strong> 시험을 응시하였다.</p>
<p>&nbsp;프로그래머스 데브코스 과정 수료 혜택 중 프로그래머스 인증시험 1회 무료 응시권이 주어졌고, 필자는 그 중 <strong>PCCP</strong>를 선택하였다.
&nbsp;&nbsp;실제 코딩테스트 경험도 추가하고 싶었고, 현재 내 코딩테스트 실력 및 알고리즘 활용도가 어느 정도까지 올라와있는지 확인해보고 싶었다.</p>
<p>&nbsp;이번 포스팅에는 간단하게 <strong>PCCP 응시 후기</strong>를 남겨보고자 한다.</p>
<p><br><br></p>
<h2 id="🎯-시험-준비">🎯 시험 준비</h2>
<hr>
<p>&nbsp;먼저 필자는 몇 달 전부터, 코딩테스트를 하루에 한 문제씩 풀고 있다.
&nbsp;&nbsp;이전까지는 <strong>프로그래머스</strong>에 제공된 코딩테스트를 해결하며 실력을 키웠고, 다시 하루에 한 문제씩 풀기 시작한 시점부터는 <strong>백준</strong>에 제공된 코딩테스트 문제들을 풀이하고 있다.</p>
<p>&nbsp;개인적으로 코딩테스트 문제들을 풀이할 때 적용하는 몇 가지 시간 관련 규칙이 있다. 코딩테스트에만 시간을 전부 투자할 수는 없기 때문에 개인적으로 세워둔 규칙이다.</p>
<p>&nbsp;<strong>첫 번째로, 문제 풀이 시작 10분 전까지는 알고리즘을 반드시 도출해야한다.</strong>
&nbsp;&nbsp;만약 10분이 지났는데도 알고리즘을 도출하지 못하면, 해당 문제에서 사용되는 알고리즘을 <strong>문제 분류, AI</strong> 등을 통해 먼저 확인한다. <strong>&#39;해당 시간을 지나면 웬만해서는 해당 문제 풀이 방법을 도출할 수 없다&#39;</strong>는 지론이다.</p>
<p>&nbsp;<strong>두 번째로, 문제 풀이 시간은 1시간을 넘기지 않는다.</strong>
&nbsp;&nbsp;물론 문제 난이도에 따라 다르겠지만, 난이도가 그 정도로 높다고 판단되는 문제가 아니라면 문제 풀이에 1시간 이상을 소요하지 않는 편이다.
&nbsp;&nbsp;만약 1시간이 지났음에도 문제를 풀이하지 못하거나 반례를 찾아내지 못하면, 마찬가지로 AI를 통해 풀이 방법, 반례 등을 확인해본다.</p>
<p>&nbsp;중요한 것은 해당 문제 풀이 방식을 이해하고, 이후 <strong>비슷한 유형의 문제가 출제되었을 때 사용 알고리즘 및 풀이 방식을 떠올릴 수 있어야 한다</strong>는 것이다. 그러므로 풀이를 확인하더라도 코드를 하나하나 확인하고, <strong>해당 코드를 보지 않고 직접 구현해보는 방식</strong>으로 코딩테스트를 준비하고 있다.</p>
<p>&nbsp;이후 다시 확인해보는 게 좋을 것 같은 문제들은 즐겨찾기에 걸어두고, 특정 문제 유형에 대한 공통적인 알고리즘 혹은 재확인이 필요할 것 같은 알고리즘 등은 <a href="https://www.notion.so/30b3723e4a97802a91ebd6008f684f7a?source=copy_link">개인 노션 페이지</a>에 정리하여, 추후 재확인할 수 있도록 하고 있다.
&nbsp;&nbsp;정리를 해둬야만 한다고 판단될 경우에만 기록하기 때문에, <strong>알고리즘이 추가되는 빈도는 낮은 편</strong>이다.</p>
<p>&nbsp;그럼에도, 아직 어려운 단계까지 풀이 가능한 수준은 아니다. 현재 <strong>프로그래머스 Level 2<del>3, 백준 Gold 3</del>1</strong> 정도가 풀이 가능한 최대 선인 것 같다. 당연하게도 문제 난이도에 따라 풀이 성공률이 많이 낮아진다.</p>
<p>&nbsp;그렇게 다음과 같이 프로그래머스 및 백준 코딩테스트를 준비한 상태에서 <strong>PCCP</strong>에 응시하였다.</p>
<p align="center"><img src="https://velog.velcdn.com/images/riverpower6_g/post/b8c15c05-e47e-46fb-a277-27a26bde5902/image.png" width="70%"></p>

<p>$$
\small{\text{〈 프로그래머스 〉}}
$$</p>
<p align="center"><img src="https://velog.velcdn.com/images/riverpower6_g/post/1ef94924-3fdf-4e31-8e10-43e7f08f1e5b/image.jpeg" width="70%"></p>

<p>$$
\small{\text{〈 백준 〉}}
$$</p>
<p><br><br></p>
<h2 id="🔎-pccp-체감-난이도">🔎 PCCP 체감 난이도</h2>
<hr>
<p>&nbsp;문제는 총 <strong>4문제</strong>가 주어지며, 제한 시간은 <strong>2시간</strong>이다.<br><br></p>
<p>&nbsp;<strong>1, 2번 문제</strong>는 그렇게 난이도가 높지는 않았다. 개인적인 체감 난이도는 둘 다 <strong>백준 Silver 3 ~ 1</strong> 정도였다.</p>
<p>&nbsp;<strong>1번 문제</strong>는 처음 보는 유형이라 처음에는 좀 당황했는데, 그래도 어떻게 점화식을 잘 찾아낼 수 있었다. 점화식만 찾아내면 이후 구현은 간단하게 할 수 있는 수준이었다. 문제 풀이에는 약 <strong>15분</strong> 정도가 소요됐다.</p>
<p>&nbsp;<strong>2번 문제</strong>는 개인적으로는 <strong>1번 문제보다 쉬웠다.</strong> 기존에 풀어본 다른 코딩테스트 문제들과 비슷한 유형의 문제였다. 당황했던 1번 문제와 달리 침착하게 구현할 수 있었으며, 마찬가지로 문제 풀이에는 약 <strong>15분</strong> 정도가 소요됐다.<br><br></p>
<p>&nbsp;<strong>3번 문제</strong>부터는 확실히 난이도 상승이 체감되었다. 체감 난이도는 백준 <strong>Gold 3 ~ 1</strong> 정도였다.</p>
<p>&nbsp;<strong>알고리즘과 풀이 방식</strong>은 비교적 빨리 찾아낼 수 있었다. 기본적으로 백준 Gold 문제를 좀 풀어본 사람들은 충분히 떠올릴 수 있는 방식이었다. 다만, 실제 구현에서 어려움을 겪었다.
&nbsp;&nbsp;구현 섹션을 약 <strong>3~4 섹션</strong>으로 나누었는데, 그 중 마지막 섹션 구현 도중, 앞에서 진행해둔 구현과 뭔가 틀어진 것을 느꼈다. 기존에 떠올려둔 구현 방식을 마지막 섹션에서 그대로 구현하기에 어려움이 존재했고, 이 과정에서 자료구조를 여러 번 변경했다. 그렇게 구현이 틀어지면서 이것저것 변경하다보니, 문제 풀이를 마무리한 시점에서 <strong>변경된 자료구조로 인한 시간복잡도 변경과 반례 및 엣지 케이스</strong> 등을 고려할 시간이 없었다. 기본으로 주어진 테스트케이스 통과만 확인한 후 문제 풀이를 마무리하였다. 문제 풀이에는 약 <strong>1시간 정도</strong>가 소요됐다.<br><br></p>
<p>&nbsp;<strong>4번 문제</strong>는 확실히 가장 난이도가 높았다. <strong>&#39;2시간을 모두 해당 문제 풀이에 소요해도 풀 수 있을지 확신할 수 없다&#39;</strong>는 생각이 들었다. 체감 난이도는 백준 <strong>Gold 1 ~ Platinum</strong> 수준이었다.</p>
<p>&nbsp;기본적으로 점화식을 알아내야만 풀 수 있는 문제였다. 문제 지문이 꽤나 길기도 했고, 문제 풀이 시작 시점에 남은 시간이 <strong>20~30분</strong> 정도밖에 없었기에, 점화식을 구해보다가 제한 시간이 끝나버렸다.
&nbsp;&nbsp;개인적으로는 완전히 <strong>처음 보는 유형</strong>이었고, 문제 복잡도도 상당해보였다. 점화식을 어찌저찌 찾아낸다고해도 <strong>&#39;코드로 구현할 수 있었을까?&#39;</strong> 에 대한 의문에는 여전히 물음표가 남아있다.</p>
<blockquote>
<p><em>※ 참고로, 필자는 백준 Platinum 문제 풀이 경험이 없으므로, 해당 난이도 평가는 개인적인 체감에 가깝다.</em></p>
</blockquote>
<p><br><br></p>
<h2 id="🪪-응시-결과">🪪 응시 결과</h2>
<hr>
<p><strong>PCCP</strong> 시험의 경우, 제한 시간이 종료되면 응시 결과를 <a href="https://certi.programmers.co.kr/result/issuance">해당 페이지</a>를 통해 거의 바로 확인할 수 있다.</p>
<p>&nbsp;취득 레벨은 <strong>Lv.0 ~ Lv.5</strong>로 구분되어 있으며, 총점 <strong>1,000점</strong>을 기준으로 성적이 구분된다.
&nbsp;&nbsp;성적 유효 기간은 <strong>취득일로부터 2년</strong>이다.</p>
<p>&nbsp;필자의 경우, <strong>650점</strong>을 획득하여 <strong>Lv.3</strong> 자격을 취득했다.</p>
<p align="center"><img src="https://velog.velcdn.com/images/riverpower6_g/post/ef51cc47-3a04-4b0b-977a-f1b5f6037553/image.jpg" width="80%"></p>

<p>$$
\small{\text{〈 PCCP 성적표 〉}}
$$</p>
<br>

<h2 id="💬-회고">💬 회고</h2>
<hr>
<p>&nbsp;<strong>응시 전 목표는 Lv.4 취득</strong>이었기에, 다소 아쉬움이 남는다.
&nbsp;&nbsp;문제 풀이 결과를 확인할 수 없기 때문에 확신할 수는 없지만, <strong>3번</strong> 문제가 <strong>일부 반례로 인해 부분 점수 획득</strong> 처리되었을 것으로 예상된다. 그렇게 <strong>2.5 Solve</strong>가 아닐까 예상해본다.</p>
<p>&nbsp;채점표가 따로 공개되어있지 않으므로 확신할 수는 없지만, <strong>3 Solve</strong>를 했더라도 문제 난이도 분포 상 <strong>Lv.4 취득</strong>은 어렵지 않았을까 생각된다.
&nbsp;&nbsp;물론 만약 <strong>1 Solve당 250점</strong>을 받는 구조라면, <strong>3 Solve 시 총 750점</strong>을 취득하여 <strong>Lv.4</strong> 달성이 가능할 것이다. 만약 그런 기준이라면... 더더욱 아쉬울 것 같다.</p>
<p>&nbsp;그래도 <strong>첫 실전 코딩테스트</strong> 치고는 완전 나쁜 결과는 아니라고 생각한다. 또한, <strong>실전 경험</strong>을 쌓을 수 있는 <strong>값진 경험</strong>이었다.
&nbsp;&nbsp;앞으로도 꾸준히 코딩테스트를 풀어나갈 생각이므로, 점점 난이도를 올려가며 더욱 더 다양한 문제 유형을 경험해봐야겠다.</p>
<p>&nbsp;이후 기회가 된다면, 다음에는 <strong>최소 Lv.4 취득</strong>을 목표로 도전해보고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cache]]></title>
            <link>https://velog.io/@riverpower6_g/Spring-Cache</link>
            <guid>https://velog.io/@riverpower6_g/Spring-Cache</guid>
            <pubDate>Mon, 23 Feb 2026 07:15:20 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp;<a href="https://velog.io/@riverpower6_g/Cache%EC%9D%98-%EC%9D%B4%ED%95%B4">이전 포스팅</a>을 통해, 캐시에 대한 이해는 어느정도 가닥이 잡혔다.
&nbsp;&nbsp;이제 본격적으로 스프링에서 캐시를 사용하기 위해 필요한 개념들을 하나하나 자세하게 살펴보자.
<br><br><br></p>
<h1 id="1-spring-cache-추상화-인터페이스">1. Spring Cache 추상화 인터페이스</h1>
<hr>
<ul>
<li>스프링은 캐시를 동일한 방식으로 다룰 수 있도록 <strong>Cache Abstraction</strong>을 제공</li>
</ul>
<h3 id="1️⃣-cache">1️⃣ Cache</h3>
<ul>
<li><p><strong>캐시 한 덩어리</strong></p>
<ul>
<li>일반적으로 <strong>cacheName</strong> 하나</li>
</ul>
</li>
<li><p><strong>핵심 메서드</strong></p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>get(Object key)</code></td>
<td>키로 조회, 값이 <strong>없으면 null</strong> 반환. <strong>캐시 hit/miss 판정</strong> 가능</td>
</tr>
<tr>
<td><code>get(Object key, Class&lt;T&gt; type)</code></td>
<td><strong>타입 지정</strong> 조회</td>
</tr>
<tr>
<td><code>get(Object key, Callable&lt;T&gt; valueLoader)</code></td>
<td><strong>캐시 미스 시 Callable 실행 후 값 저장</strong>. <strong>동시성</strong> 제어 핵심.</td>
</tr>
<tr>
<td><code>put(Object key, Object value)</code></td>
<td>캐시에 값 <strong>저장</strong></td>
</tr>
<tr>
<td><code>putIfAbsent(Object key, Object value)</code></td>
<td>이미 값이 <strong>없을 때만 저장</strong></td>
</tr>
<tr>
<td><code>evict(Object key)</code></td>
<td>특정 키 <strong>삭제</strong></td>
</tr>
<tr>
<td><code>clear()</code></td>
<td>캐시 <strong>전체 삭제</strong></td>
</tr>
<tr>
<td><br></td>
<td></td>
</tr>
</tbody></table>
</li>
</ul>
<p>📌 <code>get(Object key, Callable&lt;T&gt; valueLoader</code> 사용 예시</p>
<pre><code class="language-java">Cache cache = cacheManager.getCache(&quot;user&quot;);
User user = cache.get(userId, () -&gt; userRepository.findById(userId));</code></pre>
<ul>
<li>캐시에 <strong>&quot;user&quot;</strong> 값이 존재하면 즉시 반환</li>
<li>존재하지 않으면 <strong>Callable 실행</strong><ul>
<li><strong>결과를 캐시에 저장 후 반환</strong></li>
</ul>
</li>
<li><strong>캐시가 없는 키</strong>에 대해 여러 스레드가 <strong>동시에</strong> Callable 함수를 실행하지 않도록 <strong>동시성 제어 가능</strong><br>

</li>
</ul>
<h3 id="2️⃣-cachemanager">2️⃣ CacheManager</h3>
<ul>
<li><p><strong>여러 개의 Cache를 관리, 생성, 조회하는 역할</strong></p>
</li>
<li><p><strong>핵심 메서드</strong></p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>getCache(String name)</code></td>
<td><strong>이름</strong>으로 Cache 인스턴스 반환</td>
</tr>
<tr>
<td><code>getCacheNames()</code></td>
<td>등록된 Cache 이름 <strong>목록</strong> 반환</td>
</tr>
<tr>
<td><br></td>
<td></td>
</tr>
</tbody></table>
</li>
<li><p><strong>구현체 예시</strong></p>
<ul>
<li><strong>CaffeineCacheManager</strong> -&gt; <strong>JVM 메모리</strong> 기반 <strong>로컬</strong> 캐시</li>
<li><strong>RedisCacheManager</strong> -&gt; <strong>분산</strong> 캐시</li>
<li><strong>SimpleCacheManager</strong> -&gt; 여러 Cache를 <strong>직접</strong> 등록 가능<br>

</li>
</ul>
</li>
</ul>
<h3 id="3️⃣-cacheresolver">3️⃣ CacheResolver</h3>
<ul>
<li><p>호출에서 <strong>어떤</strong> 캐시를 쓸지 결정</p>
</li>
<li><p>일반적으로 <strong>cacheName</strong>으로 찾은 후 반환</p>
</li>
<li><p><strong>멀티테넌시, 환경별 캐시, 요청별 캐시</strong> 선택 시 <strong>커스텀</strong> 가능</p>
<ul>
<li><strong>멀티테넌시</strong> -&gt; <strong>하나</strong>의 애플리케이션이나 시스템 인스턴스를 <strong>여러 독립 고객(tenant)</strong>이 공유하면서도, <strong>각 고객의 데이터와 설정은 격리</strong>하는 구조</li>
</ul>
</li>
<li><p><strong>핵심 메서드</strong></p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>Collection&lt;? extends Cache&gt; resolveCaches(CacheOperationInvocationContext&lt;?&gt; context)</code></td>
<td><strong>호출 컨텍스트</strong>에서 캐시 결정</td>
</tr>
<tr>
<td><br></td>
<td></td>
</tr>
</tbody></table>
</li>
</ul>
<h3 id="4️⃣-keygenerator">4️⃣ KeyGenerator</h3>
<ul>
<li><p><strong>캐시 키 생성 정책</strong></p>
</li>
<li><p>기본적으로는 <strong>메서드 파라미터</strong> 기반</p>
</li>
<li><p><strong>성능 및 정합성</strong> 문제로 인해, 정말 간단한 경우가 아니라면 대부분 <strong>커스텀 키</strong> 사용</p>
</li>
<li><p>사용 예시</p>
<pre><code class="language-java">@Override
public Object generate(Object target, Method method, Object... params) {
  return method.getName() + &quot;#&quot; + Arrays.deepHashCode(params);
}</code></pre>
</li>
<li><p><strong>메서드 이름 + 파라미터 Hash</strong>로 키 생성</p>
</li>
<li><p><strong>멀티테넌시, 특정 필드만</strong> Key로 하고 싶을 때 커스텀</p>
<br>

</li>
</ul>
<h3 id="5️⃣-cacheerrorhandler">5️⃣ CacheErrorHandler</h3>
<ul>
<li><p>캐시 백엔드(Redis 등) <strong>장애 발생 시의 동작</strong> 정의</p>
</li>
<li><p>기본적으로는 <strong>예외를 던짐</strong></p>
</li>
<li><p><strong>캐시 장애 시 DB 조회로 폴백</strong>하는 방식으로 주로 커스텀</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>handleCacheGetError(RuntimeException exception, Cache cache, Object key)</code></td>
<td><strong>get</strong> 시 예외 처리</td>
</tr>
<tr>
<td><code>handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value)</code></td>
<td><strong>put</strong> 시 예외 처리</td>
</tr>
<tr>
<td><code>handleCacheEvictError(RuntimeException exception, Cache cache, Object key)</code></td>
<td><strong>evict</strong> 시 예외 처리</td>
</tr>
<tr>
<td><code>handleCacheClearError(RuntimeException exception, Cache cache)</code></td>
<td><strong>clear</strong> 시 예외 처리</td>
</tr>
<tr>
<td><br></td>
<td></td>
</tr>
</tbody></table>
</li>
<li><p>사용 예시</p>
<pre><code class="language-java">@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
  log.error(&quot;Redis 에러: DB 폴백&quot;, exception);
  // DB 조회
}</code></pre>
<br>

</li>
</ul>
<h1 id="2-enablecaching">2. @EnableCaching</h1>
<hr>
<ul>
<li><p>내부적으로 <code>CachingConfigurationSelector</code> 를 통해 캐시 인프라 구성을 <strong>import</strong>하여 <strong>어노테이션 기반 캐싱 활성화</strong></p>
</li>
<li><p>기본적으로 <strong>Proxy</strong> 기반이므로, 주의할 점 존재</p>
<ul>
<li><p><strong>Self-Invocation</strong> -&gt; <strong>같은 클래스 내부</strong>에서 <strong>내부 메서드 호출</strong> 시, <strong>프록시를 거치지 않으므로</strong> 캐시 미동작</p>
<pre><code class="language-java">public class CacheMain {
    @Cacheable(&quot;user&quot;)
    public User getUser(Long id) { ... }

    public User getUser2(Long id) {
        return getUser(id);    // 내부 호출, 프록시를 안 거침 -&gt; 캐시 미적용
    }
}</code></pre>
</li>
<li><p><strong>final/private 메서드</strong> -&gt; 프록시가 <strong>가로채지 못하는 경우</strong>가 많음</p>
<br>
## 🌊호출 흐름
</li>
</ul>
</li>
<li><p><code>@Cacheable</code> 기준, 호출 흐름</p>
<pre><code>클라이언트 요청
  ↓
Service (프록시)
  ↓ (Advisor / Interceptor 가로채기)
CacheInterceptor
  ├─ CacheOperation 조회 (OperationSource)
  ├─ CacheManager로 Cache 조회
  ├─ Cache Hit → 바로 반환
  └─ Cache Miss → 실제 Service 메서드 실행 → 결과 캐시에 저장 → 반환</code></pre></li>
<li><p>메서드 호출이 <strong>비즈니스 서비스 로직에 도달하기 이전</strong>, <strong>CacheInterceptor</strong>가 먼저 <strong>Hit/Miss</strong> 체크 수행</p>
<br>
## 🫘 인프라 빈 관점</li>
<li><p><strong>프록시 기반 캐시</strong>는 스프링 내부에서 다음과 같은 구조로 이해 가능</p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Advisor</strong></td>
<td>어떤 메서드에 캐시 어드바이스를 적용할지 결정 ( <code>@Cacheable</code> , <code>@CachePut</code> , <code>@CacheEvict</code> 등 )</td>
</tr>
<tr>
<td><strong>Interceptor (=Advice)</strong></td>
<td>실제 캐시 동작 수행 ( <code>CacheInterceptor</code> )</td>
</tr>
<tr>
<td><strong>OperationSource</strong></td>
<td><code>@Cacheable</code> 등의 어노테이션을 <code>CacheOperation</code> 객체로 변환 -&gt; <strong>캐시 동작 정의</strong> 생성</td>
</tr>
<tr>
<td><strong>CacheManager / KeyGenerator / CacheResolver / ErrorHandler</strong></td>
<td><strong>실제 캐시 조회, 키 생성, 선택, 예외 처리</strong> 등을 담당</td>
</tr>
<tr>
<td><br></td>
<td></td>
</tr>
<tr>
<td>### 1️⃣ Advisor</td>
<td></td>
</tr>
</tbody></table>
</li>
<li><p><strong>AOP</strong>에서는 <strong>어떤 시점에 어떤 Advice를 적용</strong>할지 결정</p>
</li>
<li><p><strong>스프링 캐시</strong>에서는 <strong>CacheOperationSource, CacheInterceptor</strong>를 묶어서 <strong>Advice</strong>로 등록</p>
</li>
<li><p>메서드에 <code>@Cacheable</code> 이 있으면 <strong>CacheInterceptor 실행</strong>을 알려주는 <strong>중간 관리자</strong> 역할</p>
<br>
### 2️⃣ OperationSource</li>
<li><p><code>@Cacheable</code> , <code>@CachePut</code> , <code>@CacheEvict</code> 같은 어노테이션을 내부적으로 <strong>CacheOperation</strong> 객체로 변환</p>
</li>
<li><p>다음과 같은 코드를</p>
<pre><code class="language-java">@Cacheable(value = &quot;users&quot;, key = &quot;#id&quot;)
public User find(Long id)</code></pre>
<p>다음과 같은 구조로 변환</p>
<pre><code>CacheOperation:
- cacheName: users
- key: #id
- condition: null
- unless: null</code></pre></li>
<li><p>어노테이션을 기계가 이해할 수 있는 <strong>메타데이터</strong>로 바꿔주는 역할</p>
<br>
### 3️⃣ Interceptor</li>
<li><p><strong>실제 캐시 로직 수행자</strong></p>
</li>
<li><p><strong>동작 흐름</strong></p>
<pre><code>1. 캐시 키 생성
 2. CacheManager에서 캐시 가져오기
 3. 캐시에 값이 있는지 체크
   → 있으면 바로 반환 (메서드 미실행)
   → 없으면 실제 메서드 실행
 4. 결과를 캐시에 저장
 5. 반환</code></pre><br>
# 3. 스프링 캐시 어노테이션
</li>
</ul>
<hr>
<h2 id="1️⃣-cacheable">1️⃣ @Cacheable</h2>
<pre><code class="language-java">@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Cacheable {

    // cacheNames: 사용할 캐시 이름들 지정
    String[] cacheNames() default {};
    // value: cacheNames와 동일.cacheNames의 별칭
    String[] value() default {};

    // key: 캐시 키를 SpEL로 지정
    String key() default &quot;&quot;;

    // keyGenerator: KeyGenerator 빈 이름 지정
    String keyGenerator() default &quot;&quot;;

    // cacheManager: CacheManager 빈 이름 지정(Resolver 대신 직접 지정하는 느낌)
    String cacheManager() default &quot;&quot;;

    // cacheResolver: CacheResolver 빈 이름 지정(여러 캐시에서 동적 선택 시)
    String cacheResolver() default &quot;&quot;;

    // condition: 캐시 적용 여부(SpEL). true일 때만 캐시 로직 동작
    String condition() default &quot;&quot;;

    // unless: 캐시 저장 여부(SpEL). true면 put 스킵
    // 일반적으로 &#39;결과 기반&#39;으로 평가
    String unless() default &quot;&quot;;

    // sync: stampede 방지용 동기화 모드(같은 key에 대한 동시 로딩 방지)
    boolean sync() default false;
}</code></pre>
<ul>
<li><p><strong>조회 캐싱의 기본</strong></p>
<ul>
<li><strong>캐시에 값이 있으면 메서드 실행 없이 캐시 값을 반환하고, 없으면 메서드 실행 후 캐시에 저장</strong></li>
</ul>
</li>
<li><p><code>cacheNames</code></p>
<ul>
<li>사실상 <strong>데이터 구조 및 정책 단위</strong></li>
<li><strong>TTL, size, serializer</strong>를 <strong>cacheName별로 다르게</strong> 주는 게 일반적</li>
</ul>
</li>
<li><p><code>key</code></p>
<ul>
<li>키를 <strong>SpEL로 직접</strong> 만들면 강력하지만, 문자열 키를 남발하게 되어 <strong>충돌 가능성과 관리 난이도 증가</strong></li>
<li>일반적으로 <strong>&#39;규칙화된 prefix + 식별자&#39;</strong> 정도만 <strong>SpEL</strong>로 사용<ul>
<li>복잡해지면 <strong>KeyGenerator</strong> 구현</li>
</ul>
</li>
</ul>
</li>
<li><p><code>condition</code></p>
<ul>
<li><strong>사전 조건</strong></li>
<li>일반적으로 <strong>메서드 실행 전</strong> 평가</li>
<li><code>true</code> 일 때만 캐시 로직 적용</li>
<li><code>false</code> 면 캐시 무시 후 <strong>메서드 호출</strong></li>
</ul>
</li>
<li><p><code>unless</code></p>
<ul>
<li><strong>사후 거부 조건(veto)</strong></li>
<li><strong>메서드 실행 후</strong> 평가</li>
<li><strong>반환값</strong>( <code>#result</code> )을 보고 <strong>캐싱하지 않을 결과 결정</strong></li>
<li><code>true</code> 일 시 캐시에 <strong>저장하지 않음</strong></li>
</ul>
</li>
<li><p><code>sync=true</code></p>
<ul>
<li><p><strong>Stampede 방지</strong>에 매우 중요</p>
</li>
<li><p>캐시 구현체가 <strong>동시 로딩</strong>을 잘 지원해야 좋은 효과 발휘</p>
</li>
<li><p>모든 기능과 조합 시 제약이 생길 수 있어, <strong>핵심 키 몇 개</strong>에만 제한적으로 적용하는 경우가 많음</p>
<ul>
<li><code>unless</code> 와는 같이 사용 불가능</li>
</ul>
<br>

</li>
</ul>
</li>
</ul>
<h2 id="2️⃣-cacheput">2️⃣ @CachePut</h2>
<pre><code class="language-java">@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface CachePut {
    String[] cacheNames() default {};
    String[] value() default {};
    String key() default &quot;&quot;;
    String keyGenerator() default &quot;&quot;;
    String cacheManager() default &quot;&quot;;
    String cacheResolver() default &quot;&quot;;
    String condition() default &quot;&quot;;
    String unless() default &quot;&quot;;
}</code></pre>
<ul>
<li><p><strong>항상 메서드 실행 후 캐시 갱신</strong></p>
<ul>
<li><strong>쓰기 후 캐시 갱신</strong>을 메서드와 묶을 때 유용</li>
</ul>
</li>
<li><p><strong>DB 트랜잭션과의 순서 및 실패</strong>를 조심해야 함</p>
<ul>
<li><p><strong>안정성</strong> 측면에서 <code>@CacheEvict</code> 가 더 선호되기도 함</p>
<br>
## 3️⃣ @CacheEvict
```java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface CacheEvict {
String[] cacheNames() default {};
String[] value() default {};
String key() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
String condition() default "";

<p>// allEntries=true 시, 해당 캐시의 모든 엔트리 drop
boolean allEntries() default false;</p>
<p>// beforeInvocation=true 시 메서드 실행 전에 evict
// 따라서, 메서드가 예외를 던져도 evict 수행
boolean beforeInvocation() default false;
}
```</p>
</li>
</ul>
</li>
<li><p><strong>메서드 실행 전/후에 캐시 항목 제거(무효화)</strong></p>
</li>
<li><p><code>allEntries=true</code> 는 매우 위험할 수 있으므로, <strong>정말 필요할 때만</strong> 사용</p>
</li>
<li><p><code>beforeInvocation=true</code> 는 <strong>실패하더라도 반드시 캐시를 무효화해야 하는 상황</strong>에만 사용</p>
<br>
## 4️⃣ @Caching
```java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Caching {
  Cacheable[] cacheable() default {};
  CachePut[] put() default {};
  CacheEvict[] evict() default {};
}
```</li>
<li><p><strong>여러 캐시 동작을 한 메서드에 조합</strong></p>
</li>
<li><p><strong>1번</strong> 업데이트에서</p>
<ul>
<li>A 캐시는 evict</li>
<li>B 캐시는 put</li>
<li>둘을 <strong>동시에</strong> 해야되는 상황 같은 때 사용<br>
## 5️⃣ @CacheConfig
```java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
String[] cacheNames() default {};
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}
```</li>
</ul>
</li>
<li><p><strong>클래스 레벨 기본값</strong></p>
<ul>
<li>클래스에 <strong>공통 설정</strong>(cacheNames, keyGenerator, cacheManager, cacheResovler ...)을 걸어 <strong>중복 제거</strong></li>
</ul>
</li>
<li><p><code>@CacheConfig</code> 만 붙인다고 캐시가 켜지는 게 아니고, 오직 <strong>기본값만</strong> 제공</p>
</li>
<li><p><strong>코드 예시</strong></p>
<pre><code class="language-java">@CacheConfig(cacheNames = &quot;user&quot;, keyGenerator = &quot;userKeyGenerator&quot;)
public class UserService {
  @Cacheable    // 메서드별 적용
  public User get(long id) { ... }
}</code></pre>
<br> 
# 4. SpEL과 평가 컨텍스트
</li>
</ul>
<hr>
<ul>
<li><strong>스프링 캐시 어노테이션</strong>은 <strong>SpEL</strong>을 사용해 다음을 제어 가능<ul>
<li><strong>key</strong>: <strong>캐시 키를 어떻게</strong> 만들 것인가</li>
<li><strong>condition</strong>: 캐시 동작을 적용할 것인가 (<strong>사전 조건</strong>)</li>
<li><strong>unless</strong>: 결과를 보고, 캐시 저장을 거부할 것인가 (<strong>사후 거부 조건</strong>)<br>
## 🔭 어노테이션별 평가 시점 차이

</li>
</ul>
</li>
</ul>
<h3 id="1️⃣-cacheable-평가-순서">1️⃣ @Cacheable 평가 순서</h3>
<p align="center"><img src="https://velog.velcdn.com/images/riverpower6_g/post/75511de6-fb8b-4bd6-a1df-fe0152879dfb/image.png" width="45%"></p>

<ul>
<li>코드<pre><code class="language-java">@Cacheable(
  cacheNames = &quot;items&quot;,
  key = &quot;#id&quot;,
  condition = &quot;#id &gt; 0&quot;,        // (1) 먼저 평가
  unless = &quot;#result == null&quot;    // (2) 메서드 실행 후 평가
)
public Product find(long id) {
   return productRepository.findById(id).orElse(null);
}</code></pre>
<ul>
<li><strong>Cache Hit</strong>인 경우<ul>
<li>메서드 비호출</li>
<li>따라서 <code>#result</code> 자체가 없음</li>
<li>즉, <code>unless</code> 는 <strong>Cache Hit 판단 조건에서 제외</strong><br>
### 2️⃣ @CachePut 평가 순서

</li>
</ul>
</li>
</ul>
</li>
</ul>
<p align="center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/6cbb0c7f-c436-4ba7-842c-f792ef3412d6/image.png" width="45%"/></p>

<ul>
<li>코드<pre><code class="language-java">@CachePut(
  cacheNames = &quot;products&quot;,
  key = &quot;#result.id&quot;,                // 생성 후 id 생기는 경우 유용
  condition = &quot;#result != null&quot;,    // 결과 기반 condition 가능
  unless = &quot;#result.delete == true&quot;    // 결과에 따라 캐시 put 거부
)
public Product updateProduct(ProductUpdateDto response) {
   return productService.update(response);
}</code></pre>
</li>
<li><code>@CachePut</code> 은 메서드가 <strong>항상</strong> 실행</li>
<li><code>condition</code> , <code>unless</code> 모두 사용 및 제어 가능</li>
<li><code>@CachePut</code> 에서는 <code>condition</code> 또한 <strong>메서드 호출 후</strong> 평가되며, 여기서 <code>#result</code> 참조 또한 가능<br>
### 3️⃣ @CacheEvict 평가 순서
<p align="center"><img src="https://velog.velcdn.com/images/riverpower6_g/post/047b8e74-444a-41b4-942c-09663c27a306/image.png" width="45%"></p>


</li>
</ul>
<ul>
<li>코드 ( <code>beforeInvocation = false</code> )<pre><code class="language-java">@CacheEvict(cacheNames = &quot;products&quot;, key = &quot;#id&quot;)
public void deleteProduct(Long id) {
   productRepository.deleteById(id);    // 성공 후 evict
}</code></pre>
<ul>
<li>메서드 <strong>성공 후</strong> 무효화</li>
<li><code>#result</code> 사용 가능</li>
</ul>
</li>
</ul>
<p align="center"><img src="https://velog.velcdn.com/images/riverpower6_g/post/8a173d6f-a112-43e2-8417-529ba0eddcd2/image.png" width="45%"></p>

<ul>
<li>코드 ( <code>beforeInvocation = true</code> )<pre><code class="language-java">@CacheEvict(cacheNames = &quot;products&quot;, key = &quot;#id&quot;, beforeInvocation = true)
public void deleteProduct(Long id) {
   productRepository.deleteById(id);    // 실행 전 evict
}</code></pre>
<ul>
<li>메서드 <strong>실행 전</strong> 무효화</li>
<li>아직 결과가 없기 때문에 <code>#result</code> 사용 불가<br>
## 🔖 자주 사용되는 값

</li>
</ul>
</li>
</ul>
<p><strong>1.</strong> <code>#root.methodName</code></p>
<ul>
<li>호출된 메서드 이름<pre><code class="language-java">key = &quot;#root.methodName + &#39;:&#39; + #id&quot;</code></pre>
<br>

</li>
</ul>
<p><strong>2.</strong> <code>#root.method</code></p>
<ul>
<li>호출된 메서드<pre><code class="language-java">key = &quot;#root.method.name + &#39;:&#39; + #id&quot;</code></pre>
<br>

</li>
</ul>
<p><strong>3.</strong> <code>#root.target</code></p>
<ul>
<li>대상 객체 (빈 인스턴스)<pre><code class="language-java">key = &quot;#root.targetClass.simpleName + &#39;:&#39; + #id&quot;</code></pre>
<br>

</li>
</ul>
<p><strong>4.</strong> <code>#root.targetClass</code></p>
<ul>
<li>대상 클래스<pre><code class="language-java">key = &quot;#root.targetClass.name + &#39;:&#39; + #id&quot;</code></pre>
<br>

</li>
</ul>
<p><strong>5.</strong> <code>#root.args</code></p>
<ul>
<li>인자 배열<pre><code class="language-java">key = &quot;#root.args[0]&quot;</code></pre>
<br>

</li>
</ul>
<p><strong>6.</strong> <code>#root.caches</code></p>
<ul>
<li>현재 적용 대상 캐시 컬렉션<pre><code class="language-java">key = &quot;#root.caches[0].name + &#39;:&#39; + #id&quot;</code></pre>
<br>

</li>
</ul>
<p><strong>7. 파라미터 이름 기반 참조</strong> ( <code>#id</code> , <code>#request.userId</code> )</p>
<pre><code class="language-java">   key = &quot;#id&quot;
    key = &quot;#request.userId + &#39;:&#39; + #request.page&quot;</code></pre>
<ul>
<li><code>-parameters</code> 컴파일 옵션 필요<br>

</li>
</ul>
<p><strong>8. 인덱스 기반 참조</strong> ( <code>#a0</code> , <code>#p0</code> , <code>#root.args[0]</code> )</p>
<ul>
<li><strong>파라미터 이름을 못 가져오는 상황</strong>에서도 안전한 방식</li>
<li><strong>스프링 공식 문서</strong>에 따르면, 파라미터 이름이 없을 시 <code>#a0</code> , <code>#p0</code> 같은 <strong>인덱스 접근</strong>을 쓰라고 안내<ul>
<li><a href="https://docs.spring.io/spring-framework/reference/integration/cache/annotations.html">Spring Cache 공식 문서</a><pre><code class="language-java">key = &quot;#a0&quot;        // 첫 번째 인자
key = &quot;#p1&quot;        // 두 번째 인자</code></pre>
<br>

</li>
</ul>
</li>
</ul>
<p><strong>9.</strong> <code>#result</code></p>
<ul>
<li><p>메서드 결과값</p>
</li>
<li><p><code>@Cacheable</code> : <code>unless</code> 에서 사용 가능</p>
</li>
<li><p><code>@CachePut</code> : <code>key</code> , <code>condition</code> , <code>unless</code> 에서 결과 기반으로 사용 가능</p>
</li>
<li><p><code>@CacheEvict</code> : <code>beforeInvocation=false</code> 일 때 사용 가능</p>
<br>
## 📒 코드 예시
### 1️⃣ 입력 조건 + null 결과 제외
```java
@Service
@RequiredArgsConstructor
public class ProductService {

<p>  private final ProductRepository productRepository;</p>
<p>  / **</p>
<pre><code>* id가 0 이하면 캐시 미사용 (condition)
* 조회 결과가 null이면 캐시에 미저장 (unless)
*/</code></pre><p>  @Cacheable(</p>
<pre><code>  cacheNames = &quot;products&quot;,
  key = &quot;#id&quot;,
  condition = &quot;#id != null &amp;&amp; #id &gt; 0&quot;,
  unless = &quot;#result == null&quot;</code></pre><p>  )
  public Product findById(Long id) {</p>
<pre><code>  // 캐시 miss일 때만 실행
  return findByIdFromDb(id);</code></pre><p>  }</p>
<p>  private Product findByIdFromDb(Long id) {</p>
<pre><code>  return productRepository.findById(id).orElse(null);</code></pre><p>  }
}</p>
<pre><code>&lt;br&gt;
</code></pre></li>
</ul>
<h3 id="2️⃣-검색-캐시-문자열-정규화--페이지-제한">2️⃣ 검색 캐시 (문자열 정규화 + 페이지 제한)</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ProductSearchService {

    private final ProductSearchRepository productSearchRepository;

    /**
     * 검색 캐시는 폭발하기 쉬움
     * page가 너무 크면 캐시 제외 (condition)
     * 결과가 비어있으면 캐시 저장 제외 (unless)
     * keyword는 trim과 lowercase로 키 정규화
     */
    @Cacheable(
        cacheNames = &quot;searchProducts&quot;,
        key = &quot;&#39;keyword=&#39; + #keyword.trim().toLowerCase() + &#39;:page=&#39; + #page + &#39;:size=&#39; + #size&quot;,
        condition = &quot;#page &gt;= 0 &amp;&amp; #page &lt; 20 &amp;&amp; #size &lt;= 100&quot;,
        unless = &quot;#result == null || #result.isEmpty()&quot;
    )
    public List&lt;Product&gt; searchProduct(String keyword, int page, int size) {
        return searchProductFromDb(keyword, page, size);
    }

    private List&lt;Product&gt; searchProductFromDb(String keyword, int page, int size) {
        return productSearchRepository.search(keyword, page, size);
    }
}</code></pre>
<br>

<h3 id="3️⃣-cacheput에서-결과-기반-키조건-사용">3️⃣ @CachePut에서 결과 기반 키/조건 사용</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ProudctService {

    private final ProductRepository productRepository;

    /**
     * @CachePut은 항상 메서드 실행
     * 저장/수정 후 반환된 결과(result)를 기준으로 캐시에 저장
     */
    @CachePut(
        cacheNames = &quot;products&quot;,
        key = &quot;#result.id&quot;,                // 결과 id 값으로 키 추출
        condition = &quot;#result != null&quot;,    // 결과가 있어야만 put
        unless = &quot;#result.notSale&quot;        // notSale 상태면 put 금지
    )
    public Product save(ProductCreateDto request) {
        return saveProduct(request);
    }

    private Product saveProduct(ProductCreateDto request) {
        return productRepository.save(request.id, request.notSale);
    }
}</code></pre>
<br>

<h3 id="4️⃣-cacheevict---beforeinvocation-차이">4️⃣ @CacheEvict - beforeInvocation 차이</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;

    /**
     * 기본값 (beforeInvocation = false)
     * 메서드가 성공해야만 그 후에 캐시 제거
     * 예외 발생 시 캐시가 삭제되지 않음
       - 트랜잭션 롤백 가능성이 존재할 때, 일관성 측면에서 더 안전한 편
     */
    @CacheEvict(cacheNames = &quot;products&quot;, key = &quot;#id&quot;)
    public void deleteProduct(Long id) {
        deleteProductFromDb(id);
    }

    /**
     * beforeInvocation = false
     * 메서드 호출 전, 미리 캐시 제거
     * 예외가 발생하더라도 그 전에 캐시는 제거된 상태
     */
    @CacheEvict(cacheNames = &quot;products&quot;, key = &quot;#id&quot;, beforeInvocation = true)
    public void deleteProductForce(Long id) {
        deleteProductFromDb(id);
    }

    private void deleteProductFromDb(Long id) {
        productRepository.delete(id);
    }
}</code></pre>
<br>

<h1 id="5-키-설계">5. 키 설계</h1>
<hr>
<pre><code class="language-javascript">캐시는 &quot;같은 키면 같은 값&quot; 이라는 강한 가정을 전제로 동작</code></pre>
<ul>
<li><p>키가 잘못되면 아래와 같은 문제 발생 가능</p>
<p><strong>1. 잘못된 데이터 반환(오염)</strong>: <strong>서로 다른 요청</strong>이 <strong>같은 키</strong>로 매핑(충돌)</p>
</li>
<li><p><em>2. 정보 누출*</em>: 권한/테넌트별 <strong>결과가 다름</strong>에도, 키에 <strong>scope</strong>가 없어서 <strong>남의 데이터 노출 가능</strong></p>
</li>
<li><p><em>3. 캐시 무용지물*</em>: <strong>동일 요청</strong>임에도 키가 계속 달라져 <strong>Cache Miss</strong> 발생</p>
</li>
<li><p><em>4. 운영 통제 불가*</em>: 키의 prefix나 버전이 없으면 롤백/배포 때 <strong>전체</strong> 캐시를 <strong>제거</strong>해야 함</p>
<br>

</li>
</ul>
<h2 id="📌-키-설계-규칙">📌 키 설계 규칙</h2>
<h3 id="1-짧고-결정적">1. 짧고 결정적</h3>
<ul>
<li><strong>같은 입력</strong>이면 항상 <strong>같은 키</strong></li>
<li>리스트 검색에서, 리스트 파라미터가 있으면 <strong>정규화 필수</strong><ul>
<li><strong>정렬, 중복 제거, 공백 제거 및 소문자 통일</strong></li>
</ul>
</li>
<li><strong>키 정규화</strong> 예시:<ul>
<li>입력<pre><code class="language-java">[solo, male]
[male, solo]</code></pre>
</li>
<li>정규화<pre><code class="language-java">[male, solo]</code></pre>
</li>
<li>정규화된 키<pre><code class="language-java">games:tags=male,solo</code></pre>
<br>
### 2. 충돌 방지</li>
</ul>
</li>
<li><strong>결과를 바꾸는 모든 요소는 키에 포함</strong><ul>
<li>아닐 시 <strong>오염</strong> 발생</li>
</ul>
</li>
<li>필드가 <strong>결과에 영향</strong>을 주는지 항상 체크<ul>
<li><strong>page, pageSize, orderBy</strong> 등<br>
### 3. 버전</li>
</ul>
</li>
<li>캐시는 <strong>스키마나 응답, 직렬화 방식이 바뀌면, 이전 캐시가 틀린 값</strong>이 될 가능성 존재<ul>
<li><strong>캐시</strong>는 원본 DB가 아닌 <strong>복제본</strong></li>
<li>버전 업데이트 배포 후, <strong>같은 요청의 응답 구조가 달라질 수 있음</strong></li>
</ul>
</li>
<li><strong>버저닝</strong>을 통해서 이를 관리 가능<ul>
<li><code>playon:v2:partyDetail:{partyId}</code></li>
</ul>
</li>
<li><strong>장점</strong><ul>
<li>배포 직후 <strong>이전 캐시의 자동 무효화</strong></li>
</ul>
</li>
<li><strong>단점</strong><ul>
<li>배포 직후 <strong>Cold Start</strong> 문제 발생</li>
<li>이는 <strong>TTL 조절, 워밍업, 스태거 배포</strong> 등의 방법으로 완화 가능<br>
### 4. 멀티테넌시/권한/사용자별 결과는 Key Scope로 강제</li>
</ul>
</li>
<li>캐시는 기본적으로 <strong>같은 키는 같은 값</strong>으로 반환</li>
<li><strong>테넌시/권한/사용자</strong>에 따라 달라 결과가 달라짐에도 같은 키를 사용하면, <strong>다른 사람의 데이터를 그대로 재사용</strong>하는 심각한 버그 발생<ul>
<li>이는 단순한 캐시 버그를 넘어선 <strong>데이터 유출</strong> 문제</li>
</ul>
</li>
<li>다음과 같은 <strong>캐시 스코프</strong>를 포함<ul>
<li><strong>테넌트</strong>별 데이터 분리 : <code>tenantId</code></li>
<li><strong>권한</strong>별 노출 필드 차이 : <code>role</code></li>
<li><strong>사용자</strong>별 결과 차이 : <code>userId</code><br>
### 5. 키 길이 제한 관리
```
📑 Redis 사용 중이라고 가정
```</li>
</ul>
</li>
<li><strong>Redis</strong>는 긴 키도 저장 가능하지만, 다음과 같은 문제 발생 가능</li>
</ul>
<p><strong>1. 네트워크 비용 증가</strong></p>
<ul>
<li>Redis는 요청 시마다 <strong>키 문자열을 그대로</strong> 전송<ul>
<li><strong>QPS가 높을수록 네트워크 트래픽 급격히 증가</strong><ul>
<li><strong>QPS : 초당 처리 요청 수</strong> ( Query Per Second )</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><strong>2. 메모리 오버헤드</strong></p>
<ul>
<li><strong>Redis는 키 자체도 메모리에 저장</strong></li>
<li>키가 길어지면, 특히 필터 검색이 많은 API에서 <strong>메모리 낭비 급증</strong></li>
</ul>
<p><strong>3. 필터 폭발 문제 발생 및 운영 난이도 증가</strong></p>
<ul>
<li><strong>필터</strong> 수가 많아질수록 키에 포함되는 <strong>디멘전 증가</strong><ul>
<li>이로 인해 <strong>키 길이 및 캐시 엔트리 수 급증</strong></li>
<li>이를 해결하기 위해 <strong>필터 정규화</strong> 후 <strong>해시로 압축</strong>하는 방식 주로 사용</li>
<li>반드시 <strong>정규화 후</strong> 해싱</li>
<li><strong>SHA-256</strong> 알고리즘을 사용하면 <strong>낮은 충돌 확률, 고정된 길이 보장 가능</strong><br>

</li>
</ul>
</li>
</ul>
<h2 id="🔑-키-생성-방식-3단계">🔑 키 생성 방식 3단계</h2>
<pre><code class="language-text">🎯 키 설계 규칙을 단계적으로 고도화해가며, 캐시 키 설계 규칙을 확장해보자.</code></pre>
<br>

<h3 id="1️⃣-전역-keygenerator">1️⃣ 전역 KeyGenerator</h3>
<pre><code class="language-java">​@Bean
public KeyGenerator globalKeyGenerator() {
    return (target, method, params) -&gt;
        method.getName() + &quot;::&quot; + Arrays.deepToString(params);
}</code></pre>
<ul>
<li><strong>매우 단순한</strong> 형태의 <code>KeyGenerator</code> 구현 방식</li>
<li><strong>장점</strong><ul>
<li><strong>빠른</strong> 구현</li>
<li><strong>직관적인</strong> 규칙<br>
```
🔎 현재 키 설계 원칙 관점에서는 여러 문제 존재
```</li>
</ul>
</li>
<li><em>1. 키가 결정적이지 않음*</em></li>
<li>필터에 <strong>컬렉션</strong>이 포함할 경우, 순서가 매번 변경될 수 있음<ul>
<li><strong>같은 의미</strong>의 요청임에도 <strong>서로 다른 키</strong> 생성</li>
<li>이로 인해 <strong>Cache Miss</strong> 발생 가능성 증가</li>
</ul>
</li>
<li><strong>DTO</strong>의 <code>toString()</code> 포맷 등이 바뀌면 <strong>키 변경</strong></li>
</ul>
<p><strong>2. 충돌을 방지하지 못함</strong></p>
<ul>
<li><code>method.getName()</code> 의 경우, <strong>동일한 메서드명(오버로드 등)으로 인한 충돌</strong> 위험 존재</li>
<li><code>Arrays.deepToString()</code> 의 경우, <strong>서로 다른 파라미터</strong>가 <strong>동일한 문자열</strong>이 나오는 등 <strong>충돌</strong> 위험 존재</li>
</ul>
<p><strong>3. 버전별 캐시 무효화 어려움</strong></p>
<ul>
<li><strong>응답 스키마나 정규화 로직이 변경</strong>되면, <strong>잘못된 이전 캐시값</strong> 사용 가능</li>
</ul>
<p><strong>4. 캐시 스코프 누락 가능성 존재</strong></p>
<ul>
<li><strong>user, tenant, role</strong> 등 캐시 스코프 정보가 파라미터에 포함되지 않으면 <strong>키에 미반영</strong></li>
<li>이로 인해 <strong>잘못된 캐시 재사용</strong>이나 <strong>정보 누출 위험</strong> 발생</li>
</ul>
<p><strong>5. 키 길이가 길어질 수 있음</strong></p>
<ul>
<li>필터가 많아지면 키가 길어져 <strong>네트워크 및 메모리 비용 증가</strong><br>
### 2️⃣ 전역 정규화 + 해시로 압축

</li>
</ul>
<pre><code class="language-java">​@Bean
public KeyGenerator hashKeyGenerator() {
    return (target, method, params) -&gt; {
        String signature = method.getDeclaringClass().getSimpleName() + &quot;#&quot; + method.getName();

        String normalized = Canonicalizer.canonicalize(params);        // 컬렉션 정규화
        String digest = sha256Hex(signature + &quot;:&quot; + normalized);

        return signature + &quot;:&quot; + digest;
    }
}

static class Canonicalizer {
    static String canonicalize(Object... params) {
        if (params == null || params.length == 0) {
            return &quot;[]&quot;;
        }

        return Arrays.stream(params)
                     .map(Canonicalizer::normalize)
                     .collect(Collectors.joining(&quot;,&quot;, &quot;[&quot;, &quot;]&quot;));
    }

    // 정규화
    static String normalize(Object p) {
        if (p == null) {
            return &quot;null&quot;;
        }

        if (p instanceof CharSequence s) {
            return s.toString().trim();
        }

        if (p instanceof Number || p instanceof Boolean) {
            return String.valueOf(p);
        }

        if (p instanceof Enum&lt;?&gt; e) {
            return e.name();
        }

        // Map -&gt; key 기준으로 정렬
        if (p instanceof Map&lt;?, ?&gt; m) {
            return m.entrySet().stream()
                    .map(e -&gt; normalize(e.getKey()) + &quot;=&quot; + normalize(e.getValue()))
                    .sorted()
                    .collect(Collectors.joining(&quot;&amp;&quot;, &quot;{&quot;, &quot;}&quot;));
        }

        // Set -&gt; 정렬
        if (p instanceof Set&lt;?&gt; s) {
            return s.stream()
                    .map(Canonicalizer::normalize)
                    .sorted()
                    .collect(Collectors.joining(&quot;,&quot;, &quot;S[&quot;, &quot;]&quot;));
        }

        // List -&gt; 값 순서 유지
        if (p instanceof List&lt;?&gt; l) {
            return l.stream()
                    .map(Canonicalizer::normalize)
                    .collect(Collectors.joining(&quot;,&quot;, &quot;L[&quot;, &quot;]&quot;));
        }

        return p.toString().trim();
    }
}</code></pre>
<ul>
<li><strong>전역 KeyGenerator를 유지</strong>하되, <strong>결정적 키 생성</strong>과 <strong>키 길이 제한 문제</strong> 해결<ul>
<li><strong>같은 의미면 같은 키 생성</strong></li>
<li><strong>컬렉션 정규화</strong> 후 SHA-256 알고리즘을 통해 <strong>고정 길이 키 생성</strong><br>

</li>
</ul>
</li>
</ul>
<pre><code>🔎 여전히 남아있는 문제</code></pre><p><strong>1. 도메인별 키 정책 차이</strong></p>
<ul>
<li>일부 <strong>List</strong> 는 <strong>순서 유지</strong> 필요, 일부는 <strong>정렬 후 키 생성</strong>이 효율적일 수 있음</li>
<li><strong>기본값</strong> 필드는 키에서 <strong>제외</strong> 가능</li>
</ul>
<p><strong>2. 캐시 스코프 누락 가능성 존재</strong></p>
<ul>
<li>캐시 스코프 존재 시, 여전히 누락 가능성 존재</li>
</ul>
<p><strong>3. 버전별 캐시 무효화 어려움</strong></p>
<ul>
<li><strong>전역 KeyGenerator</strong>만으로는 <strong>특정 캐시 버전</strong> 관리 불가능</li>
</ul>
<p><strong>4. DTO 구조 변경에 따른 문제</strong></p>
<ul>
<li>필드 추가나 포맷 변경 등의 <strong>구조 변경</strong> 시, <strong>키 변경</strong> 가능</li>
<li><strong>동일한 의미</strong>임에도 <strong>불필요한 Cache Miss</strong> 발생<br>
### 3️⃣ 전역 KeyGenerator 최소화 + 키 중앙 집중화</li>
<li>간단한 키는 <strong>@CacheConfig + KeyGenerator</strong> 방식을 사용</li>
<li>복잡한 키의 경우, <code>Prefix</code> 는 <code>Redis</code> 설정으로, 정규화 등의 <strong>키 정책</strong>은 <code>CacheKeys</code> 로 중앙집중화</li>
<li>이를 통해 <strong>전역 KeyGenerator 방식의 한계를 해결</strong><br>

</li>
</ul>
<p><strong>1. Prefix는 RedisCacheConfiguration으로 등록</strong></p>
<pre><code class="language-java">​@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
    return RedisCacheConfiguration.defaultCacheConfig()
                .computePrefixWith(cacheName -&gt; &quot;playon:&quot; + cacheName + &quot;::&quot;);
}</code></pre>
<ul>
<li><strong>Prefix</strong> 설정을 통해 <strong>캐시 그룹 단위</strong>로 <strong>분석 및 삭제</strong> 가능</li>
<li><strong>환경 분리</strong>가 필요하다면, <strong>Environment</strong>를 통해 <strong>profile</strong>을 받아서 <strong>prefix</strong>에 추가<br>

</li>
</ul>
<p><strong>2. CacheNames에 캐시 이름 상수화</strong></p>
<pre><code class="language-java">​@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CacheNames {
    public static final String PARTY_LIST = &quot;partyList&quot;;
    public static final String PARTY_DETAIL = &quot;partyDetail&quot;;
}</code></pre>
<ul>
<li>사용할 <strong>cacheNames</strong>들을 <strong>중앙 집중적</strong>으로 관리 가능<br>

</li>
</ul>
<p><strong>3. CacheKeys로 도메인별 키 정책을 캐시별로 명시</strong></p>
<pre><code class="language-java">​@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CacheKeys {
    private static final String VERSION = &quot;v1&quot;;

    public static String partyListKey(
        int page, int pageSize, String orderBy, boolean isMacSupported, String partyAt,
        Long appId, Collection&lt;String&gt; genres, Collection&lt;String&gt; tags
    ) {
        // 1. 정규화 진행
        Set&lt;String&gt; g = normalize(genres);
        Set&lt;String&gt; t = normalize(tags);

        // 2. 필터 및 디멘전 추가
        String payload =
            &quot;page=&quot; + page +
            &quot;&amp;size=&quot; + pageSize +
            &quot;&amp;orderBy=&quot; + trim(orderBy) +
            &quot;&amp;mac=&quot; + isMacSupported +
            &quot;&amp;partyAt=&quot; + trim(partyAt) +
            &quot;&amp;appId=&quot; + (appId == null ? &quot;&quot; : appId) +
            &quot;&amp;genres=&quot; + String.join(&quot;,&quot;, g) +
            &quot;&amp;tags=&quot; + String.join(&quot;,&quot;, t);

        // 3. 해시로 압축
        return VERSION + &quot;:&quot; + sha256Hex(payload);
    }

    // 컬렉션 -&gt; 정렬을 통한 정규화
    private static Set&lt;String&gt; normalize(Collection&lt;String&gt; c) {
        TreeSet&lt;String&gt; ts = new TreeSet&lt;&gt;();

        if (c == null) {
            return ts;
        }

        for (String v : c) {
            String trimV = trim(v);

            if (!trimV.isEmpty()) {
                ts.add(trimV);
            }
        }

        return ts;
    }

    // SHA-256 알고리즘을 통한 해싱
    private static String sha256Hex(String s) {
        try {
            byte[] digest = MessageDigest.getInstance(&quot;SHA-256&quot;)
                            .digest(s.getBytes(StandardCharsets.UTF_8));

            StringBuilder sb = new StringBuilder(digest.length * 2);

            for (byte b : digest) {
                sb.append(String.format(&quot;%02x&quot;, b));
            }

            return sb.toString();
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    // 빈 값 체크 및 공백 제거 + 대소문자 통일
    private static String trim(String s) {
        return s == null ? &quot;&quot; : s.trim().toLowerCase(Locale.ROOT);
    }
}</code></pre>
<ul>
<li><strong>결정성 준수</strong><ul>
<li><strong>각</strong> 캐시에 맞게 <strong>정규화</strong> 진행</li>
</ul>
</li>
<li><strong>충돌 방지</strong><ul>
<li><strong>결과를 바꾸는 필드</strong>를 반드시 포함</li>
</ul>
</li>
<li><strong>버전 관리</strong><ul>
<li><strong>버전 변경 시, 이전 캐시 자동 무효화</strong></li>
</ul>
</li>
<li><strong>캐시 스코프</strong><ul>
<li>현재 코드에는 추가하지 않았지만, <strong>캐시 스코프 존재 시 필요한 캐시에만 포함</strong></li>
</ul>
</li>
<li><strong>키 길이 제한 준수</strong><ul>
<li><strong>payload</strong>가 아무리 길어지더라도, <strong>SHA-256</strong> 해싱을 통해 <strong>고정 길이</strong>로 제한<br>

</li>
</ul>
</li>
</ul>
<p><strong>4. @Cacheable에서 CacheKeys 호출</strong></p>
<pre><code class="language-java">​@Cacheable(
    cacheNames = CacheNames.PARTY_LIST,
    key = &quot;T(패키지명.CacheKeys).partyListKey(#page, #pageSize, #orderBy, #isMacSupported, #partyAt, #request.appId(), #request.genres(), #request.tags())&quot;
)
public PartyListResponse list(int page, int pageSize, String orderBy, boolean isMacSupported, String partyAt, GetAllPartiesRequest request) {
    // 구현 생략
}</code></pre>
<ul>
<li><strong>캐시를 사용할 메서드</strong>에서, 어노테이션의 각 필드에 <strong>구현 클래스 사용</strong></li>
<li>결과적으로 다음과 같은 키 저장<ul>
<li><code>playon:partyList::v1:{sha256hex}</code></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Cache의 이해]]></title>
            <link>https://velog.io/@riverpower6_g/Cache%EC%9D%98-%EC%9D%B4%ED%95%B4</link>
            <guid>https://velog.io/@riverpower6_g/Cache%EC%9D%98-%EC%9D%B4%ED%95%B4</guid>
            <pubDate>Fri, 20 Feb 2026 06:59:45 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp;백엔드 개발자에게 가장 필요한 능력이자, 가장 관심있게 봐야할 부분이 무엇일까? 필자는 <strong>&#39;성능 최적화&#39;</strong> 가 반드시 그 중 하나라고 생각한다.</p>
<p>&nbsp;지금까지 학습을 하고 프로젝트를 진행해가면서, 이러한 <strong>&#39;성능 최적화&#39;</strong> 를 위해 여러 시도를 했다. 대표적으로 <strong>&#39;쿼리 튜닝&#39;</strong> 및 <strong>&#39;인덱싱&#39;</strong> 이 있다.</p>
<p>&nbsp;이러한 작업들을 어느 정도 만족스럽게 마무리한 이후, <strong>성능을 더욱 극대화</strong>하고 싶다는 생각이 들어 찾아보던 중, <strong>캐싱</strong>을 적용하면 이러한 목적을 달성할 수 있다는 것을 알게 되었다.</p>
<p>&nbsp;당연히 캐시의 개념과 목적 정도는 알고 있었지만, 이를 더욱 자세하게 이해하고 코드로 녹여내는 것은 별개의 일이다. 당장 내 프로젝트에 캐싱을 적용하고 싶어도, 그러기 위해서는 캐시에 대한 개념과 확실한 이해가 필요하다.</p>
<p>&nbsp;그렇기에, 당분간 캐시의 개념부터 시작해, 스프링에서 캐시를 사용하기 위해 알아야할 지식들을 <strong>점차적으로</strong> 포스팅할 예정이다.
&nbsp;&nbsp;이번 포스팅은 캐시를 이해하기 위한, <strong>캐시의 개념</strong>에 대한 내용이다.
<br><br><br></p>
<h1 id="1-캐시란">1. 캐시란?</h1>
<hr>
<h2 id="캐시">캐시</h2>
<ul>
<li><strong>비싼 계산 및 조회 결과</strong>를 더 <strong>빠른 저장소</strong>에 잠시 저장해, <strong>같은 요청</strong>이 다시 오면 원본 대신 그 값을 <strong>재활용</strong>하여 돌려주는 것</li>
<li>이를 통해 <strong>지연시간(latency)를 감소시키고, 처리량(throughput)을 증가</strong></li>
<li>캐시는 원 저장소가 아닌 성능을 위한 <strong>복제본</strong>이므로 데이터 <strong>일관성 문제</strong>가 항상 존재<ul>
<li>캐시에 남아있는 값이 <strong>최신 값이 아닐 수 있음</strong><br>
## 캐시 핵심 요소

</li>
</ul>
</li>
</ul>
<h3 id="1️⃣-key-설계">1️⃣ Key 설계</h3>
<ul>
<li><p>캐시는 기본적으로 <code>key -&gt; value</code> 맵</p>
<ul>
<li>성능을 가르는 건 <strong>키 설계</strong>가 절반이라고 봐도 무방</li>
</ul>
</li>
<li><p>키는 <strong>정규화</strong> 필수</p>
<ul>
<li>ex) <code>tags</code> 를 <strong>정렬</strong>해서 키에 추가</li>
</ul>
</li>
<li><p>키 길이는 너무 길면 네트워크/메모리 측면에서 비효율적</p>
<ul>
<li><strong>sha256</strong> 같은 <strong>해시</strong>를 통해 줄이기도 함<br>
### 2️⃣ Value (저장 전략)</li>
</ul>
</li>
<li><p>실제 <strong>캐싱 대상 데이터</strong></p>
<ul>
<li>캐시에 <strong>무엇을 저장</strong>할지에 대한 설계 영역</li>
<li>단순 DTO뿐 아니라 <strong>저장 단위 + 표현 방식 + 크기 전략</strong>까지 모두 포함</li>
</ul>
</li>
<li><p><strong>직렬화 전략</strong> 선택 필요 (분산 캐시에서는 필수)</p>
<ul>
<li><strong>JSON</strong> -&gt; 디버깅이 쉽지만 크기가 큼</li>
<li><strong>Kryo</strong> -&gt; 빠르고 작지만 설정이 필요</li>
<li><strong>JDK Serialization</strong> -&gt; 구현이 간단하지만 성능 및 보안 이슈 존재</li>
<li><strong>트래픽</strong>이 많아질수록, <strong>직렬화 비용</strong>이 <strong>병목</strong>이 될 가능성 존재</li>
</ul>
</li>
<li><p><strong>크기 관리</strong> 중요</p>
<ul>
<li>크기가 큰 객체는 <strong>네트워크 및 메모리 부담 증가</strong></li>
<li>따라서 <strong>모두 다 캐싱</strong>하는 방법은 <strong>안티패턴</strong></li>
</ul>
</li>
<li><p><strong>캐싱 단위</strong> 선택</p>
<ul>
<li><strong>통째로 캐싱(Object-level caching)</strong><ul>
<li>단순하고 빠름</li>
<li><strong>일부</strong> 필드만 바뀌어도 <strong>전체 무효화</strong> 필요<pre><code class="language-java">party:123 -&gt; PartyDetailResponse</code></pre>
</li>
</ul>
</li>
<li><strong>부분 캐싱(Fragment/Field caching)</strong><ul>
<li>변경 범위를 최소화</li>
<li><strong>조립 비용이 증가</strong>하고 <strong>일관성 관리 어려움</strong><pre><code class="language-java">party:123:game
party:123:member
party:123:total</code></pre>
</li>
</ul>
</li>
</ul>
</li>
<li><p>캐시 객체는 가급적 <strong>불변</strong>으로 설계</p>
<ul>
<li><strong>참조 객체 수정</strong>은 <strong>캐시 오염</strong>으로 번질 수 있음<br>
### 3️⃣ TTL (Time To Live)</li>
</ul>
</li>
<li><p><strong>자동 만료 시간</strong></p>
</li>
<li><p>무효화가 어려운 데이터도 시간 단위로 해결 가능</p>
</li>
<li><p>TTL이 너무 길면 <strong>stale</strong> 리스크 증가</p>
<ul>
<li>데이터 <strong>정합성 문제</strong> 발생 가능</li>
</ul>
</li>
<li><p>TTL이 너무 짧으면 <strong>cache miss</strong> 확률이 증가하여 효과 감소</p>
</li>
<li><p><strong>Redis</strong> 기반 Spring Cache는 <strong>엔트리 TTL</strong>을 지원하며, <strong>고정 Duration</strong> 및 <strong>엔트리별 TTL 함수</strong>도 지원</p>
<br>
### 4️⃣ Eviction 정책</li>
<li><p>캐시가 무한대로 커지면 메모리 및 성능 문제 발생</p>
<ul>
<li>정책 필요</li>
</ul>
</li>
<li><p>Caffeine 같은 <strong>로컬 캐시</strong>는 보통, <strong>maximumSize, expireAfterWrite, expireAfterAccess</strong> 같은 정책을 조합</p>
<br>
## 캐시 용어
</li>
<li><p><strong>Cache Hit / Miss</strong></p>
<ul>
<li>키로 조회했을 때 값이 존재하면 Hit, 없으면 Miss</li>
</ul>
</li>
<li><p><strong>TTL(Time To Live)</strong></p>
<ul>
<li>값이 캐시에 살아있는 최대 시간</li>
<li><strong>생성</strong> 시점 기준, <strong>고정</strong>된 만료 시간</li>
</ul>
</li>
<li><p><strong>TTI(Time To Idle)</strong></p>
<ul>
<li>마지막 접근 이후 유휴 시간이 일정 시간 지나면 만료</li>
<li>값을 <strong>조회</strong>하면 만료 시간 <strong>리셋</strong></li>
</ul>
</li>
<li><p><strong>Eviction</strong></p>
<ul>
<li>용량 제한 및 정책에 의해 일부 키를 제거</li>
<li>예시 알고리즘 -&gt; LRU, LFU, FIFO, Random</li>
</ul>
</li>
<li><p><strong>Warmup / Preload</strong></p>
<ul>
<li>캐시를 미리 채워서 <strong>Cold Start</strong>를 완화</li>
</ul>
</li>
<li><p><strong>Negative Caching</strong></p>
<ul>
<li><strong>NULL</strong>(없는 값)도 <strong>캐싱</strong>해서 침투 방지</li>
</ul>
</li>
<li><p><strong>Jitter</strong></p>
<ul>
<li><strong>TTL</strong>에 <strong>랜덤 편차</strong>를 주어 <strong>동시 만료</strong> 방지</li>
</ul>
</li>
<li><p><strong>Stampede / Thundering herd</strong></p>
<ul>
<li>특정 키가 <strong>만료되는 순간</strong>, <strong>방대한 요청이 동시에 Cache Miss 발생</strong></li>
<li>이로 인한 DB 과부하 발생
<br><br><h1 id="2-캐시-전략">2. 캐시 전략</h1>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="1️⃣-cache-aside">1️⃣ Cache-Aside</h2>
<pre><code>애플리케이션이 캐시 조회
- Cache Hit -&gt; 반환
- Cache Miss -&gt; DB 조회
  - DB 결과를 캐시에 저장
  - 클라이언트에 반환</code></pre><ul>
<li><strong>읽기:</strong><ul>
<li>캐시 조회 -&gt; Miss 시 DB 조회 -&gt; 캐시에 추가 후 반환</li>
</ul>
</li>
<li><strong>쓰기:</strong><ul>
<li>DB에 저장 -&gt; 캐시 Evict(무효화) 또는 Update(갱신)</li>
</ul>
</li>
<li>가장 흔한 방식</li>
<li><code>@Cacheable</code> 어노테이션은 기본적으로, 이 모델을 <strong>메서드 단위</strong>로 편하게 쓰는 느낌</li>
<li><strong>장점</strong><ul>
<li>간단한 구현</li>
<li>필요한 데이터만 캐싱</li>
<li>메모리가 효율적</li>
</ul>
</li>
<li><strong>단점</strong><ul>
<li><strong>첫 요청</strong> 시 <strong>Cold Miss</strong></li>
<li><strong>캐시 만료</strong> 시 <strong>Stampede</strong> 위험성 존재<br>
## 2️⃣ Read-Through
```
App -> Cache -> DB
```</li>
</ul>
</li>
<li><strong>캐시가 DB 접근</strong>을 대신해서 수행<ul>
<li><strong>애플리케이션은 캐시만 호출</strong></li>
</ul>
</li>
<li><strong>Cache Miss</strong> 시, <strong>DB</strong>에서 데이터를 가져와 자동으로 <strong>저장</strong></li>
<li><strong>장점</strong><ul>
<li>단순한 코드</li>
<li>데이터 <strong>접근 경로</strong>를 <strong>캐시</strong>로 통일</li>
</ul>
</li>
<li><strong>단점</strong><ul>
<li><strong>캐시 시스템 복잡</strong></li>
<li>특정 DB <strong>로직 제어의 어려움</strong><br>
## 3️⃣ Write-Through
```</li>
</ul>
</li>
</ul>
<ol>
<li>데이터 쓰기 요청</li>
<li>캐시에 기록</li>
<li>DB에 기록
```</li>
</ol>
<ul>
<li><strong>쓰기 시점에 캐시와 DB를 함께 갱신</strong></li>
<li><strong>장점</strong><ul>
<li>캐시와 DB 간의 <strong>일관성</strong>을 항상 유지</li>
<li><strong>읽기</strong> 시 Cache Miss 확률 <strong>적음</strong></li>
</ul>
</li>
<li><strong>단점</strong><ul>
<li><strong>쓰기 비용 증가</strong></li>
<li>잘 <strong>안 쓰이는</strong> 데이터도 캐시에 적재<br>
## 4️⃣ Write-Behind (Write-Back)
```</li>
</ul>
</li>
</ul>
<ol>
<li>캐시에 기록</li>
<li>즉시 응답</li>
<li>배치/비동기로 DB에 반영
```</li>
</ol>
<ul>
<li><strong>캐시에 먼저 쓰고, DB에는 비동기로 나중에 반영</strong></li>
<li><strong>로그, 통계</strong> 등에서 주로 사용</li>
<li><strong>장점</strong><ul>
<li><strong>쓰기 성능 향상</strong></li>
<li><strong>배치</strong> 처리로 <strong>DB 부하 감소</strong> 가능</li>
</ul>
</li>
<li><strong>단점</strong><ul>
<li><strong>장애</strong> 시, 데이터 유실로 인한 <strong>정합성 이슈</strong></li>
<li>따라서, <strong>강한 일관성을 보장하기 어려움</strong><br>
## 5️⃣ Refresh-Ahead</li>
</ul>
</li>
<li>만료 이전에 <strong>미리 갱신</strong><ul>
<li><strong>TTL 만료 이전</strong>, 백그라운드 갱신을 통해 <strong>히트율과 안정성을 높임</strong></li>
</ul>
</li>
<li><strong>장점</strong><ul>
<li><strong>Cache Stampede</strong> 방지</li>
<li><strong>일정한 응답 속도</strong></li>
</ul>
</li>
<li><strong>단점</strong><ul>
<li><strong>불필요한</strong> 갱신 가능성 존재<br>
## 6️⃣ Stale-While-Revalidate
```</li>
</ul>
</li>
</ul>
<ol>
<li>캐시 만료</li>
<li>기존 값 응답</li>
<li>비동기 갱신
```</li>
</ol>
<ul>
<li>만료 시에도 <strong>오래된 값 먼저 응답</strong><ul>
<li>동시에 <strong>백그라운드 갱신</strong></li>
</ul>
</li>
<li>데이터의 <strong>정합성 보다는</strong>, <strong>가용성</strong>과 <strong>지연</strong>을 우선시하는 선택<ul>
<li>따라서, 확실한 <strong>데이터 정합성 보장 불가</strong></li>
</ul>
</li>
<li><strong>Cloudflare</strong> 같은 <strong>대규모 CDN</strong>에서 많이 사용</li>
<li><strong>장점</strong><ul>
<li><strong>사용자 지연 최소화</strong></li>
<li><strong>고트래픽 환경</strong>에 적합</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>‼️ 문제 상황</strong></p>
</blockquote>
<pre><code>TTL 만료
-&gt; 사용자 요청 1만 건이 동시에 발생
-&gt; DB 조회 폭주
-&gt; 지연/장애</code></pre><ul>
<li>이를 해결할 방법으로, 보통 2가지 선택지 존재</li>
</ul>
<ol>
<li><strong>최신성 보장</strong><ul>
<li>TTL이 만료되었으므로 <strong>DB에서 조회</strong></li>
<li>그동안 <strong>사용자 대기</strong></li>
<li><strong>트래픽이 많으면 장애 위험 존재</strong></li>
</ul>
</li>
<li><strong>Stale 허용</strong><ul>
<li>일단 <strong>예전 값 응답</strong></li>
<li>백그라운드에서 <strong>새로 갱신</strong></li>
<li>이로 인한 <strong>사용자 지연은 0에 수렴</strong></li>
</ul>
</li>
</ol>
<blockquote>
<p>📌 <strong>대규모 트래픽 시스템</strong>은 보통 <strong>Stale 허용</strong> 방식을 선택</p>
</blockquote>
<ul>
<li>사용자는 <strong>느린 최신 데이터보다</strong>, <strong>빠르고 조금 지난 데이터</strong>를 더 선호한다는 철학</li>
<li>특히 <strong>트래픽 폭주</strong> 상황에서는, 잠깐의 Stale 데이터보다 잠깐의 <strong>장애 발생이 훨씬 크리티컬</strong></li>
<li><strong>상품 목록, 통계, 조회수</strong> 등에서 사용 가능<ul>
<li>조금의 <strong>시간적 차이</strong>가 존재해도 <strong>치명적이지 않은 데이터</strong></li>
</ul>
</li>
<li><strong>계좌 잔액, 재고 수량, 결제 상태</strong> 등에서는 사용하면 안됨<ul>
<li><strong>강한 일관성</strong>이 필요한 부분이기 때문<br>

</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 <strong>일반적으로 정합성 유지를 위해 다음과 같이 설계</strong></p>
</blockquote>
<pre><code>Soft TTL -&gt; 60초 (Stale 허용)
Hard TTL -&gt; 5분 (Deadline)</code></pre><ul>
<li><strong>Soft TTL</strong> 초과 -&gt; <strong>Stale 응답 + 백그라운드 갱신</strong></li>
<li><strong>Hard TTL</strong> 초과 -&gt; 무조건 <strong>DB 조회</strong></li>
<li>이러한 방식으로 <strong>너무 오래된 데이터</strong>는 방지</li>
</ul>
<br>

<h3 id="캐시-전략-요약">캐시 전략 요약</h3>
<table>
<thead>
<tr>
<th>패턴</th>
<th>읽기 성능</th>
<th>쓰기 성능</th>
<th>일관성</th>
<th>복잡도</th>
</tr>
</thead>
<tbody><tr>
<td>Cache-Aside</td>
<td>높음</td>
<td>보통</td>
<td>보통</td>
<td>낮음</td>
</tr>
<tr>
<td>Read-Through</td>
<td>높음</td>
<td>보통</td>
<td>보통</td>
<td>중간</td>
</tr>
<tr>
<td>Write-Through</td>
<td>높음</td>
<td>낮음</td>
<td>높음</td>
<td>중간</td>
</tr>
<tr>
<td>Write-Behind</td>
<td>높음</td>
<td>매우 높음</td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td>Refresh-Ahead</td>
<td>매우 높음</td>
<td>보통</td>
<td>보통</td>
<td>높음</td>
</tr>
<tr>
<td><br></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[LLM 과 RAG]]></title>
            <link>https://velog.io/@riverpower6_g/1232</link>
            <guid>https://velog.io/@riverpower6_g/1232</guid>
            <pubDate>Fri, 21 Nov 2025 03:01:32 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; 최근 <strong>데브코스 단기 심화</strong> 과정을 수료하며 여러 새로운 기술들과 개념들을 학습하고 있다. 약 한 달 여의 기간 동안 <strong>카프카</strong>를 비롯해 <strong>DDD(헥사고날 아키텍쳐)</strong> 와 <strong>MSA</strong>, <strong>스프링 배치</strong>, <strong>엘라스틱 서치</strong> 등의 개념들을 배우고 실습을 진행했다.</p>
<p>&nbsp; 현재는 이러한 기술들을 적용한 새로운 프로젝트를 진행하고 있으며, 필자는 현재 이 중 <strong>카프카, 엘라스틱 서치</strong>를 사용하고 있다. 이후 <strong>MS 분리 및 헥사고날 아키텍쳐</strong>로 마이그레이션을 진행할 예정이다.</p>
<p>&nbsp; 1차 스프린트가 종료되고 새로운 개념을 추가로 배웠는데, 해당 내용이 바로 <strong>AI 및 LLM과 RAG</strong>에 관한 내용이다. 이 지식들을 기반으로 프로젝트에 <strong>AI</strong> 기능을 추가해나갈 예정이다.</p>
<p>&nbsp; 원할하게 프로그램에에 해당 기능을 추가하기 위해서는 당연히, 적용하려는 <strong>AI에 대한 개념</strong>이 확실하게 잡혀있어야 한다. 강의를 통해 배운 내용을 토대로, 해당 개념을 정리해두기로 결정했다.
<br><br></p>
<h1 id="🤖-인공-지능">🤖 인공 지능</h1>
<hr>
<h3 id="1-약-인공지능">1. 약 인공지능</h3>
<ul>
<li><strong>특정한 작업을 수행</strong>할 수 있도록 설계되어 있는 <strong>AI</strong></li>
<li>ChatGPT, 스팸 필터링, 얼굴 인식, 체스 바둑 등</li>
<li>범위를 벗어나면 작업 수행 <strong>불가능</strong></li>
</ul>
<br>

<h3 id="2-강-인공지능">2. 강 인공지능</h3>
<ul>
<li>사람이 가질 수 있는 지능, 지성 등을 <strong>컴퓨터의 정보처리 능력</strong>으로 구현한 시스템<ul>
<li>사람처럼 <strong>다양한 범위</strong>의 문제를 해결할 수 있는 범용 AI</li>
</ul>
</li>
<li>아직 <strong>구현되지 않은</strong> 기술<ul>
<li>현재는 이론적으로만 존재</li>
<li>아직 연구 단계</li>
</ul>
</li>
</ul>
<br>

<h1 id="🪧-ai의-작동-방식">🪧 AI의 작동 방식</h1>
<hr>
<h2 id="1-규칙-기반-ai">1. 규칙 기반 AI</h2>
<ul>
<li>사람이 명시적으로 정의한 <strong>규칙에 따라 작동하는 AI</strong><ul>
<li>ex) 나이에 따른 할인 적용</li>
</ul>
</li>
<li><strong>예측 가능한 범위 내</strong>에서 <strong>명확하고 정확한 결과</strong> 제공<ul>
<li>예측을 범어난 경우, 이러한 것들이 어려움</li>
</ul>
</li>
</ul>
<br>

<h2 id="2-학습-기반-ai">2. 학습 기반 AI</h2>
<ul>
<li>데이터로부터 <strong>스스로 학습하는 AI</strong><ul>
<li>패턴을 찾아냄</li>
<li>ex) ChatGPT 같은 <strong>생성형 AI</strong></li>
</ul>
</li>
<li>굉장히 많은 데이터를 필요로 하며, 이를 처리하고 학습해야 하기 때문에 <strong>물리적인 리소스</strong> 많이 사용</li>
</ul>
<br>

<h2 id="3-머신-러닝">3. 머신 러닝</h2>
<ul>
<li><strong>데이터에서 패턴을 학습</strong>하여 명시적 규칙 없이 <strong>스스로 예측/분류</strong> 등을 수행하는 모델</li>
<li>텍스트 분류, 이미지 분류, 예측 모델, 추천 시스템 등 <strong>다양한 패턴 학습 문제</strong>를 해결<ul>
<li><strong>ex) 키워드 추출</strong><ul>
<li>정해진 패턴에 따라 키워드를 추출</li>
</ul>
</li>
<li><strong>결과를 나열</strong><ul>
<li>정보를 전달해주기만 함</li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<h2 id="4-생성형-ai">4. 생성형 AI</h2>
<ul>
<li>질문에 대한 대답을 넘어서서 맥락을 이해하고 부연 설명을 추가<ul>
<li>머신 러닝 중에서도 <strong>새로운 데이터를 생성하는 모델</strong>에 속하는 분야</li>
</ul>
</li>
</ul>
<br>

<h2 id="5-정통적-ai-vs-생성형-ai">5. 정통적 AI vs 생성형 AI</h2>
<h3 id="1️⃣-정통적-ai">1️⃣ 정통적 AI</h3>
<ul>
<li><strong>분석</strong>을 통해 일어날 일을 <strong>예측</strong></li>
<li><strong>특정 작업</strong>을 위해서 훈련이 되어있으므로, 그 작업에 대한 수행만 가능<ul>
<li>다른 작업을 위해서는 반드시 해당 작업에 대한 <strong>훈련이 다시 필요</strong></li>
</ul>
</li>
<li><strong>입력 구조가 고정</strong><ul>
<li>ex) 정형화된 데이터, 특정 포맷의 이미지</li>
<li>자연어처럼 <strong>자유로운 입력</strong>을 직접 다루는 건 <strong>불가능</strong></li>
</ul>
</li>
</ul>
<h3 id="2️⃣-생성형-ai">2️⃣ 생성형 AI</h3>
<ul>
<li>존재하지 않는 텍스트, 이미지 등을 <strong>생성 가능</strong></li>
<li>하나의 모델만으로 질문, 답변, 요약, 답변, 코드 생성 등 다양한 작업 가능</li>
<li>자연어로 구성된 문장만으로 작업을 수행 가능</li>
</ul>
<br>

<h1 id="🦖-llmlarge-language-model">🦖 LLM(Large Language Model)</h1>
<hr>
<h2 id="1-llm의-기본-원리">1. LLM의 기본 원리</h2>
<ul>
<li>대규모 언어 모델</li>
<li>학습에 사용한 데이터의 패턴, 통계적 관계를 바탕으로 <strong>다음 단어를 예측</strong></li>
<li>크게 2가지의 학습 단계로 구분<ul>
<li>그 외 다양한 단계들도 존재</li>
</ul>
</li>
<li><strong>하나의 문장</strong>을 <strong>여러 개의 토큰</strong>으로 분리 (토크나이징)<ul>
<li>각각의 토큰은 <strong>고유한 ID</strong>로 변환</li>
</ul>
</li>
<li><strong>Attention Mechanism</strong> → 어떤 문장이 주어졌을 때 토큰 간 관계를 파악하여 문맥을 파악</li>
<li><strong>Temperature</strong> 값이 낮으면 토큰 다음 값이 확률이 낮으면 포함하지 않고, 높으면 토큰 다음 값의 확률이 낮아도 포함<ul>
<li>ex) <strong>OpenAI Default → 0.8</strong></li>
</ul>
</li>
<li>확률을 통한 대답 생성을 수행</li>
</ul>
<br>

<h2 id="2-llm의-학습-단계">2. LLM의 학습 단계</h2>
<h3 id="1️⃣-사전-학습">1️⃣ 사전 학습</h3>
<ul>
<li>웹 페이지, 논문,공개된 코드 등 텍스트 데이터 활용</li>
<li>다음 데이터 예측 과제 제공</li>
<li>많은 컴퓨팅 자원 필요</li>
<li>모델이 커질수록 원천 데이터 양도 증가</li>
<li>데이터 중에는 부정확한 정보도 포함</li>
<li>데이터 규모가 커지면서 오류가 포함될 가능성 또한 존재</li>
</ul>
<h3 id="2️⃣-미세-조정">2️⃣ 미세 조정</h3>
<ul>
<li>사전 학습 이후 다시 한 번 학습시키는 과정<ul>
<li><strong>강화 학습 (RLHF - Reignforcement Learning Human Feedback)</strong> 이후 이를 반영해 미세 조정</li>
</ul>
</li>
</ul>
<br>

<h2 id="3-llm의-구조적-한계">3. LLM의 구조적 한계</h2>
<ul>
<li><strong>LLM</strong>은 정확한 정보를 저장하고 검색하지 않고, 통계적으로 가장 그럴듯한 문장을 만들어내는 시스템</li>
<li><strong>좋은 성능</strong>과 동시에 <strong>근본적인 한계점</strong> 존재</li>
<li>이를 <strong>환각(Hallucination) 효과</strong>라고 함</li>
</ul>
<br>

<h1 id="👁️-환각hallucination">👁️ 환각(Hallucination)</h1>
<hr>
<h2 id="1-환각hallucination">1. 환각(Hallucination)</h2>
<ul>
<li><strong>LLM</strong>이 사실이 아닌 정보를 마치 <strong>사실인 양 자신있게 포함</strong>시키는 것<ul>
<li><strong>LLM</strong>은 질문을 받으면 항상 무언가를 생성</li>
<li>학습했던 패턴을 조합하여 <strong>그럴듯한 대답을 생성</strong></li>
<li>하지만 사실 여부를 판단하지는 않으므로 정확하지 않은 정보 포함 가능</li>
<li>또한 <strong>LLM</strong>은 본인이 <strong>학습한 시점까지</strong>의 데이터만을 가지고 있음</li>
</ul>
</li>
<li><strong>모르는 정보의 경우, 모른다고 이야기하지 않고 아는 지식의 조합을 통해 대답을 생성</strong><ul>
<li>이로 인해 소비자는 정확하지 않은 정보를 정확한 정보인 양 전달 받는 경우가 있음</li>
<li><strong>버전이 올라가면서</strong> 없는 정보는 검색을 해보기 때문에, <strong>비교적 적은 할루시네이션</strong> 효과 발생 (아예 발생하지 않지는 않음)</li>
</ul>
</li>
<li><strong>Temperature</strong> 값이 높을수록 이러한 환각 효과가 더 자주 발생</li>
<li><strong>맥락의 제한</strong>이 존재할 경우에도 발생<ul>
<li><strong>윈도우 길이의 한계</strong>가 있기 떄문에 이 경우 오해 발생</li>
</ul>
</li>
<li>학습 데이터의 <strong>편향과 오류</strong>가 존재하기 때문에, 이로 인해 환각 발생<ul>
<li><strong>의도적으로 왜곡</strong>된 데이터도 존재하므로 위험성 또한 존재</li>
</ul>
</li>
<li><strong>기술적인 면</strong>에서 또한 이런 할루시네이션 현상이 존재<ul>
<li><strong>존재하지도 않는</strong> 라이브러리 임포트</li>
<li>그 외 존재하지 않거나 <strong>Deprecated</strong>된 클래스, 메서드, 잘못된 오버라이딩 등</li>
<li><strong>최신 버전</strong>일 <strong>*<em>경우 모델이 *</em>아직 학습하지 못했을</strong> 가능성이 있으므로, 더 자주 발생</li>
</ul>
</li>
<li>이러한 환각 효과는 <strong>LLM</strong>의 구조적인 영향이므로 <strong>완전히 없앨 수는 없음</strong><ul>
<li>이를 줄여가는 것이 최선책</li>
</ul>
</li>
</ul>
<br>

<h2 id="2-hallucination-감소-방법">2. Hallucination 감소 방법</h2>
<ul>
<li><p><strong>구체적인 프롬프트</strong> 제공</p>
<ul>
<li><p>ex)</p>
<pre><code class="language-markdown">Spring Boot 3.4 버전에서,
RestTemplate 대신 WebClient를 사용하는 방법을 예제 코드와 함께 설명해줘.

모르면 모른다고 대답하고, 추측하지 말고 확실한 정보만 제공해</code></pre>
</li>
</ul>
</li>
<li><p><strong>RAG (Retrieval Augmented Generation)</strong></p>
<ul>
<li><strong>검색</strong>과 <strong>생성</strong>을 결합한 접근 방법</li>
</ul>
</li>
<li><p>적절한 <strong>Temperature</strong> 생성</p>
<ul>
<li><strong>Temperature</strong>가 <strong>높을수록</strong> 할루시네이션 효과 발생 확률 증가</li>
<li>그러므로, 적절한 <strong>Temperature</strong> 설정이 중요</li>
<li>ex) 사실적 정보가 중요하면 <strong>Temperature</strong> 값을 감소</li>
</ul>
</li>
</ul>
<br>

<h1 id="🔩-프롬프트-엔지니어링">🔩 프롬프트 엔지니어링</h1>
<hr>
<h2 id="1-프롬프트-엔지니어링">1. 프롬프트 엔지니어링</h2>
<ul>
<li><strong>프롬프트를 설계하고 최적화하는 과정</strong><ul>
<li><strong>프롬프트</strong> → 원하는 결과를 얻기 위해서 입력하는 텍스트</li>
</ul>
</li>
<li>이렇게 <strong>최적화한</strong> 프롬프트를 통해 원하고자 하는 보다 정확한 결과를 얻고자 하는 것</li>
</ul>
<br>

<h2 id="2-프롬프트-엔지니어링의-3원칙">2. 프롬프트 엔지니어링의 3원칙</h2>
<ul>
<li>프롬프트 엔지니어링의 기본 원칙은 크게 3가지가 존재</li>
</ul>
<h3 id="1️⃣-명확성"><strong>1️⃣ 명확성</strong></h3>
<ul>
<li>프롬프트를 작성할 거면 모호한 표현은 피하고 원하는 표현을 구체적으로 작성</li>
</ul>
<h3 id="2️⃣-맥락-제공"><strong>2️⃣ 맥락 제공</strong></h3>
<ul>
<li><strong>LLM</strong>이 필요한 정보와 맥락을 충분히 제공해야 함</li>
</ul>
<h3 id="3️⃣-구체화"><strong>3️⃣ 구체화</strong></h3>
<ul>
<li>복잡한 작업의 경우 단계별로 나누거나 정확한 정보를 제공해야 함</li>
</ul>
<br>

<h2 id="3-프롬프트-템플릿">3. 프롬프트 템플릿</h2>
<ul>
<li><strong>미리 정의된 프롬프트</strong>에 변수를 넣을 수 있도록 만들어둔 틀<ul>
<li>이를 통해 검증된 프롬프트를 <strong>표준화</strong></li>
</ul>
</li>
<li>ex1)
<img src="https://velog.velcdn.com/images/riverpower6_g/post/4e3ccaa1-847b-4ed9-9290-6de42af2ad60/image.png" alt=""></li>
</ul>
<ul>
<li>ex2)
<img src="https://velog.velcdn.com/images/riverpower6_g/post/896ea998-9d66-4998-9c6b-19b4c07dfefd/image.png" alt=""></li>
</ul>
<ul>
<li>크게 <strong>4가지 세션</strong>으로 구성</li>
</ul>
<h3 id="1️⃣-system-message"><strong>1️⃣ System Message</strong></h3>
<ul>
<li><strong>LLM</strong>의 <strong>역할 및 기본 동작 방식</strong> 정의</li>
</ul>
<h3 id="2️⃣-context"><strong>2️⃣ Context</strong></h3>
<ul>
<li><strong>도메인 특화 정보</strong></li>
<li><strong>검색된 문서</strong>의 내용</li>
</ul>
<h3 id="3️⃣-user-input"><strong>3️⃣ User Input</strong></h3>
<ul>
<li><strong>사용자 입력</strong> 세션</li>
<li>사용자의 <strong>질문 및 요청</strong> 포함</li>
</ul>
<h3 id="4️⃣-instructions"><strong>4️⃣ Instructions</strong></h3>
<ul>
<li><strong>구체적인 명령</strong></li>
</ul>
<blockquote>
<p>혹은 <strong>Instructions</strong>와 <strong>Output Format</strong>을 분리 가능</p>
</blockquote>
<br>

<h2 id="4-프롬프트-템플릿-주의사항">4. 프롬프트 템플릿 주의사항</h2>
<ul>
<li><strong>변수 주입</strong><ul>
<li><strong>프롬프트 인젝션</strong>을 고려<ul>
<li><strong>프롬프트 인젝션</strong> → 악의적인 사용자가 시스템 지시사항을 무시하거나 변경시키는 것</li>
</ul>
</li>
</ul>
</li>
<li><strong>토큰 제한</strong><ul>
<li><strong>LLM이 처리할 수 있는 토큰의 총량</strong></li>
<li>토큰을 제한하여 <strong>시간당 사용할 수 있는 토큰</strong> 개수 부여</li>
<li><strong>문장이 길수록</strong> 토큰 사용량 <strong>증가</strong></li>
</ul>
</li>
<li><strong>템플릿 관리 (=버전 관리)</strong><ul>
<li><strong>프롬프트를 관리할 수 있고 변경 이력을 추적할 수 있도록 설계</strong></li>
<li>템플릿에 <strong>변수 지정</strong> 및 <strong>런타임 시점</strong>에 실제 값을 <strong>바인딩</strong></li>
</ul>
</li>
</ul>
<br>

<h1 id="🔎-ragretrieval-argmented-generation">🔎 RAG(Retrieval Argmented Generation)</h1>
<hr>
<h2 id="1-rag">1. RAG</h2>
<ul>
<li><strong>검색 증강 생성 (Retrieval Argmented Generation)</strong><ul>
<li><strong>LLM</strong>이 답변을 생성하기 전에 먼저 <strong>검색과 자료</strong>를 참고하고, <strong>그 이후</strong> 답변을 생성</li>
</ul>
</li>
<li>LLM은 <strong>한계점</strong>이 존재<ul>
<li><strong>학습 시점 데이터 활용</strong></li>
<li><strong>비공개 정보 활용 불가</strong></li>
<li><strong>전문 도메인 지식 부재</strong></li>
<li><strong>모델 학습의 요구 비용</strong></li>
</ul>
</li>
<li>모델 자체를 <strong>재학습시키지 않고도</strong> 외부 지식을 활용할 수 있는 방법<ul>
<li>사용자 질문 → 관련 문서 및 정보 검색 → 프롬프트에 포함 → LLM에 전달</li>
<li>이를 통해 환각 효과를 <strong>크게 감소</strong><ul>
<li><strong>신뢰할 수 있는 결과 제공</strong></li>
<li>물론 <strong>환각 효과를 완전히 없애는 것은</strong> <strong>현재로선 불가능</strong></li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<h2 id="2-rag-단계도">2. RAG 단계도</h2>
<ul>
<li><strong>준비 단계 (Indexing)</strong><ul>
<li><strong>임베딩</strong>을 통해 데이터를 <strong>벡터로 변환</strong></li>
<li>벡터는 <strong>벡터 DB</strong>에 저장</li>
</ul>
</li>
<li><strong>검색 (Retrieval)</strong><ul>
<li>벡터 DB의 벡터와 <strong>유사</strong>한 문서를 검색<ul>
<li><strong>유사도 검색</strong></li>
</ul>
</li>
<li><strong>유사도가 높은</strong> 몇 문서들을 <strong>Chunk</strong>로 가져옴<ul>
<li>생성 시의 <strong>근거 자료</strong></li>
</ul>
</li>
</ul>
</li>
<li><strong>생성 (Creation)</strong><ul>
<li><strong>이전의 Chunk</strong>들을 기반으로 답변을 생성</li>
</ul>
</li>
</ul>
<br>

<h2 id="3-벡터-저장소">3. 벡터 저장소</h2>
<ul>
<li><strong>텍스트</strong>를 임베딩 <strong>벡터</strong>로 변환하여 <strong>저장</strong>하고, 필요할 때 <strong>의미적으로 유사한</strong> 데이터를 빠르게 검색할 수 있도록 도와주는 <strong>저장소</strong><ul>
<li><strong>고차원 공간</strong>에 배치된 벡터 간 거리를 계산하여 <strong>의미적으로 가까운 정보</strong> 획득</li>
<li>ex) <strong>Pincone, Weaviate, Chroma</strong></li>
</ul>
</li>
<li><strong>일바적인 RDB</strong>로는 벡터 저장을 효율적으로 하기 어려움<ul>
<li>유사도 기반으로 빠르게 검사하기 어려움</li>
</ul>
</li>
</ul>
<br>

<h2 id="4-청킹">4. 청킹</h2>
<ul>
<li><strong>긴 문서를 작은 조각으로 나누는 과정</strong><ul>
<li><strong>임베딩 모델</strong>에는 <strong>토큰 제한</strong>이 있으므로, 나눠서 처리할 필요가 있음</li>
</ul>
</li>
<li>섹션 별로 <strong>나눠서</strong> 임베딩 할 시 각 세션에 <strong>구체적</strong>인 내용이 비교적 잘 드러날 수 있음</li>
<li><strong>고정 길이 방식</strong><ul>
<li><strong>몇 글자마다 잘라서</strong> 청크를 나누는 방식</li>
<li>문단이나 문장의 경계를 무시</li>
<li>문맥상 문제가 발생할 수 있으므로, 임베딩 시 원하는 결과가 담기지 않을 확률 존재</li>
</ul>
</li>
<li><strong>문장/문단 경계 방식</strong><ul>
<li>자연스러운 단위로 청크를 나누는 방식</li>
<li><strong>자연스러운 문맥을 고려</strong>하여 임베딩하므로, 비교적 정교한 결과 제공</li>
</ul>
</li>
<li><strong>청크</strong>를 일정 부분 <strong>겹치게 만들면(Overwrap)</strong>, 문맥 설치 방지 가능<ul>
<li><strong>200 ~ 500</strong> 토큰 정도 오버랩이 적당하다고 알려져는 있으나, <strong>비즈니스마다 달라질 수 있음</strong></li>
</ul>
</li>
</ul>
<br>

<h1 id="💾-임베딩embedding">💾 임베딩(Embedding)</h1>
<hr>
<h2 id="1-임베딩embedding">1. 임베딩(Embedding)</h2>
<ul>
<li><strong>텍스트 데이터</strong>를 <strong>고차원 벡터</strong>로 변환<ul>
<li>단어나 문장의 의미를 <strong>숫자</strong>로 변환</li>
<li><strong>벡터</strong>는 간략하게, 여러 개의 <strong>숫자</strong>로 이루어진 <strong>배열 정보</strong></li>
</ul>
</li>
<li><strong>의미가 비슷한</strong> 단어들은 비교적 <strong>비슷한 벡터</strong>로 치환<ul>
<li>이 <strong>벡터 간의 거리</strong>를 통해 단어 간의 <strong>유사도 측정</strong> 가능</li>
</ul>
</li>
<li><strong>임베딩</strong>은 <strong>딥러닝 모델</strong>을 통해 <strong>학습</strong></li>
<li><strong>임베딩 추론 예시</strong>
<img src="https://velog.velcdn.com/images/riverpower6_g/post/88e8edf6-fa82-491e-9f50-049cb30809f5/image.png" alt=""></li>
</ul>
<ul>
<li><p>이러한 벡터 값들을 통해 유사도 검색을 진행 가능</p>
</li>
<li><p><strong>유사도 보존</strong></p>
<ul>
<li><strong>단어 간의 의미 관계</strong>를 <strong>벡터 공간</strong>에서 근사적으로 보존</li>
</ul>
</li>
<li><p><strong>문맥 의존성</strong></p>
<ul>
<li><strong>같은 단어</strong>도 <strong>문맥에 따라 다른</strong> 벡터를 가질 수 있음</li>
<li>ex) <strong>과일</strong> 사과 / <strong>잘못</strong>이라는 의미의 사과</li>
</ul>
</li>
<li><p><strong>다국어 지원</strong></p>
<ul>
<li>여러 언으의 단어들을 동일한 단어로 취급 가능</li>
</ul>
</li>
<li><p><strong>관계 표현 능력</strong></p>
<ul>
<li>임베딩 된 단어 간의 산술 연산이 가능</li>
<li>이로 인한 단어들의 관계 표현도 가능</li>
</ul>
</li>
<li><p><strong>불변성</strong></p>
<ul>
<li>짧든 길든, 임베딩 결과는 모델이 표현 가능한 같은 차원의 벡터로 차원의 벡터로 치환<ul>
<li>1개의 단어든 100개의 단어든 모두 동일한 차원의 벡터로 치환</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>임베딩 모델</strong></p>
<ul>
<li><strong>임베딩</strong>을 수행하는 <strong>모델</strong><ul>
<li><strong>Open AI → 텍스트 임베딩 3</strong></li>
<li><strong>구글 → Gecko</strong></li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<h2 id="2-임베딩-유사도">2. 임베딩 유사도</h2>
<h3 id="1️⃣-코사인-유사도">1️⃣ 코사인 유사도</h3>
<ul>
<li>두 벡터가 <strong>가리키는 방향이 얼마나 비슷한지</strong> 측정</li>
<li><strong>스프링 AI</strong>가 <strong>Default</strong>로 사용</li>
<li>장점<ul>
<li><strong>벡터의 크기에 영향을 받지 않고 방향만 영향을 받음</strong></li>
<li><strong>짧은 단어</strong>일수록 <strong>효과적인</strong> 반영 가능</li>
</ul>
</li>
</ul>
<h3 id="2️⃣-유클리디안-거리">2️⃣ 유클리디안 거리</h3>
<ul>
<li><strong>두 벡터 사이의 직선 거리 측정</strong><ul>
<li>두 점 사이의 거리를 구하는 <strong>피타고라스 정리</strong>를 <strong>고차원</strong>으로 확장한 것</li>
</ul>
</li>
<li><strong>벡터 크기에 영향을 받음</strong></li>
</ul>
<br>

<h2 id="3-인덱싱">3. 인덱싱</h2>
<ul>
<li><strong>벡터, 원본 텍스트 및</strong> 그 <strong>메타데이터</strong>들을 벡터 저장소에 저장하는 행위</li>
</ul>
<br>

<h2 id="4-스프링-ai">4. 스프링 AI</h2>
<ul>
<li><strong>임베딩</strong>과 <strong>Vector Store</strong> 기능을 이용해 RAG <strong>구조를 구현</strong></li>
<li><strong>문서 길이가 길면 토큰 길이 제한</strong>에 걸리고, 너무 <strong>없으면 유사도가 낮으므로</strong> <strong>적절한 개수</strong>로 가져오는 것이 중요</li>
<li><strong>가장 간단한 RAG → 챗봇</strong></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[레디스 클러스터]]></title>
            <link>https://velog.io/@riverpower6_g/%EB%A0%88%EB%94%94%EC%8A%A4-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0</link>
            <guid>https://velog.io/@riverpower6_g/%EB%A0%88%EB%94%94%EC%8A%A4-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0</guid>
            <pubDate>Tue, 21 Oct 2025 09:58:18 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; 이전에 <strong>레디스 레플리케이션</strong>에 관한 실습 내용을 남겼다. 실습을 하고 기록을 남기면서도 느꼈지만, <strong>확장</strong>을 위한 과정이기 때문에 반드시 알아야 하는 내용이었다.
&nbsp;&nbsp; 그렇게 <strong>레플리케이션</strong> 내용을 마무리하고 나니, 연달아 중요한 내용이 나왔다. <strong>레디스 클러스터</strong>라는 기능이다.</p>
<p>&nbsp; <strong>레디스 클러스터</strong>는 일종의 <strong>샤딩</strong>으로, <strong>노드별 슬롯 분배</strong> 뿐만 아니라, 내부 채널인 <strong>클러스터 포트(기본 포트 + 10,000)</strong>를 통해 <strong>클러스터 노드간의 메시지 교환, 슬롯 이동 등</strong>을 지원한다.
&nbsp;&nbsp; <strong>클러스터 내 각 샤드</strong>는 일반 레디스 노드와 마찬가지로 <strong>1개 이상의 마스터</strong>와 <strong>0개 이상의 레플리카</strong>를 가지고 있으며, 클러스터 노드 또한 <strong>마스터-레플리카</strong> 구조로 구성된다.</p>
<p>&nbsp; 실습을 위한 <strong>레디스 클러스터</strong> 관련 내용은 이 정도만 알고 있어도 큰 어려움이 없을 것이다. 이제 실습을 진행해보자.</p>
<blockquote>
<p>📌 본 실습은 <strong>Windows</strong> 환경에서 <strong>WSL</strong> 을 통해 <strong>경량 가상 머신을 이용한 Linux 환경</strong>에서 진행</p>
</blockquote>
<p><br><br></p>
<h2 id="⌨️-실습-진행">⌨️ 실습 진행</h2>
<br>

<h2 id="1-레디스-클러스터-실습용-conf-파일-생성">1. 레디스 클러스터 실습용 conf 파일 생성</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/91738df0-12d0-4052-a680-2066650e3a68/image.png" width="1000" height="1000"/></div>

<ul>
<li>클러스터용 디렉토리 <code>cluster</code> 로 이동</li>
<li>기존에 받아놨던 <code>redis.conf</code> 를 복사하여 <code>redis-cluster.conf</code> 생성<br>

</li>
</ul>
<h2 id="2-redis-clusterconf-파일-수정">2. redis-cluster.conf 파일 수정</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/644b3dbb-ffe7-4278-9ccd-20de70d9f7d3/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>cluster-enabled yes</code> 를 통해 <strong>클러스터 모드</strong> 사용 허용<br>

</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/56ccebbf-a6c0-43e9-9743-827360b7c171/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>requirepass {passwd}</code> 로 <strong>비밀번호 설정</strong><br>

</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/0b79fb7d-6731-436f-8928-b67baa2aa229/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>enable-debug-command yes</code> 를 통해 <strong>디버그 허용</strong><br>

</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/65dd013e-41db-4822-add8-8545a72d883d/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>bind 0.0.0.0</code> 을 통해 <strong>외부 컨테이너(서비스) 접근 허용</strong><br>

</li>
</ul>
<h2 id="3-docker-composeyml-작성">3. docker-compose.yml 작성</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/8a30dc23-b58b-431a-a852-92e23cb239a6/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>docker-compose.yml</code> 생성 및 작성</li>
<li>네트워크는 <code>redis_network</code> 로 지정<ul>
<li>부모 디렉토리가 <strong>cluster</strong>이므로, <strong>cluster_redis_network</strong> 라는 네트워크 생성 및 공유<br>

</li>
</ul>
</li>
</ul>
<h2 id="4-docker-compose-이용해-6개의-node-서비스-시작">4. docker-compose 이용해 6개의 node 서비스 시작</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/dfe0f176-31c0-43b2-be68-c1b9b20b6498/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>6개의 컨테이너</strong> 생성 확인<br>

</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/7c6b53a8-f543-4218-80fc-f7ad6b5640e2/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>docker-compose ps</code> 명령어로 <strong>docker-compose</strong>로 실행중인 서비스 확인<br>

</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/27a66d01-ba2b-4b9a-b4a3-24c48855d4b3/image.png" width="1000" height="1000"/></div>

<ul>
<li>컨테이너 실행 즉시 상태는 <strong>슬롯 미할당 상태</strong><br>

</li>
</ul>
<h2 id="5-클러스터-생성-및-레플리카-1개씩-지정">5. 클러스터 생성 및 레플리카 1개씩 지정</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/11881b3e-1331-4a18-9d19-d0624d7996fc/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>redis-cli --cluster create [{ip(container_name)/port}..] --cluster-replica 1 -a {passwd}</code> 명령어 사용<ul>
<li><strong>클러스터 생성</strong><ul>
<li>클러스터로 사용할 노드들 등록</li>
</ul>
</li>
<li>각 클러스터 노드 중 <strong>1대씩</strong>을 <strong>레플리카</strong>로 설정</li>
<li><strong>해당 컨테이너 접속</strong>을 위한 비밀번호 제출<br>

</li>
</ul>
</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/c109b57f-4b1a-4324-a28a-d2148bc8f50c/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>총 16384개의 슬롯</strong> 생성 확인
<br><br><br></li>
</ul>
<h2 id="🎯-트러블-슈팅---인증-실패-발생">🎯 트러블 슈팅 - 인증 실패 발생</h2>
<br>

<h2 id="ts1-1-인증을-했음에도-noauth인증-실패-발생">TS1-1. 인증을 했음에도 NOAUTH(인증 실패) 발생</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/48726c70-af2b-455f-957f-9980c2ce2587/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>-a {passwd}</code> 로 <strong>비밀번호 입력</strong>을 했음에도 불구하고, <strong>인증 실퍠</strong> 발생<br>

</li>
</ul>
<h2 id="ts1-2-해결---redis-clusterconf-파일에서-masterauth-지정">TS1-2. 해결 - redis-cluster.conf 파일에서 masterauth 지정</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/1b627fbf-a43e-4c85-bc6a-7345be2dc472/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>클러스터</strong> 생성 및 분배는 문제 없지만, <strong>레플리케이션</strong> 작업에서 문제 발생</li>
<li><strong>마스터-레플리카</strong> 간의 레플리케이션에서는 <strong>마스터 데이터 동기화</strong>를 위해 <strong>레플리카에서 마스터의 비밀번호 인증이 필요</strong>하기 때문</li>
<li>따라서, <code>redis-cluster.conf</code> 파일에 <code>masterauth {passwd}</code> 지시자를 지정하여 <strong>레플리케이션 인증 문제 해결</strong><br>

</li>
</ul>
<h2 id="ts1-3-확인---각-클러스터-노드-정보-할당-확인">TS1-3. 확인 - 각 클러스터 노드 정보 할당 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/205548d4-95dc-4709-8eee-1b946b579c05/image.png" width="1000" height="1000"/></div>

<ul>
<li>각 클러스터 노드에 <strong>epoch 번호, ip</strong> 등이 잘 할당됨을 확인<br>

</li>
</ul>
<h2 id="ts1-4-확인---마스터-레플리카-간-데이터-동기화-확인">TS1-4. 확인 - 마스터-레플리카 간 데이터 동기화 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/7ec553d2-98d1-4177-957f-a41139a188ae/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>클러스터 마스터-레플리카 노드</strong> 간의 <strong>레플리케이션</strong> 동기화 완료 확인
<br><br><br></li>
</ul>
<h2 id="6-마스터-노드-레플리케이션-정보-확인">6. 마스터 노드 레플리케이션 정보 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/d1fc9978-51ed-4d0e-af99-4ce776640b22/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>cluster-node-1</code> <strong>마스터</strong> 노드로 클라이언트 접속</li>
<li><code>info replication</code> 으로 레플리케이션 정보 확인</li>
<li><strong>레플리카 1대</strong> 존재 확인 및 <strong>레플리카 노드 관련 정보</strong> 확인 가능<br>

</li>
</ul>
<h2 id="7-레플리카-노드-레플리케이션-정보-확인">7. 레플리카 노드 레플리케이션 정보 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/91362bbd-958c-47bb-991d-d62033a5294a/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>cluster-node-6</code> <strong>레플리카</strong> 노드로 클라이언트 접속</li>
<li><code>info replication</code> 으로 레플리케이션 정보 확인</li>
<li><strong>레플리카</strong> 노드라는 것과 <strong>마스터 노드 관련 정보</strong> 확인 가능<br>

</li>
</ul>
<h2 id="8-클러스터-노드-정보-확인">8. 클러스터 노드 정보 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/1ef698cb-b8ed-47e4-b4c3-427db0663ed5/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>cluster nodes</code> 로 클러스터 노드 정보 확인</li>
<li>현재 총 <strong>3대의 마스터, 3대의 레플리카</strong>가 존재함을 확인<ul>
<li><strong>슬롯</strong> 또한 다 분배되어있음을 확인<br>

</li>
</ul>
</li>
</ul>
<h2 id="9-클러스터-샤드-정보-확인">9. 클러스터 샤드 정보 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/684a0139-11d8-45c6-b8ec-3081e3fa35e4/image.png" width="1000" height="1000"/></div>

<ul>
<li>현재 <strong>클러스터 샤드</strong> 정보 확인</li>
<li>사진에는 나오지 않았지만, <strong>총 3개의 샤드 존재</strong><br>

</li>
</ul>
<h2 id="10-클러스터-노드-8개로-확장">10. 클러스터 노드 8개로 확장</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/ab092625-7adb-4585-9a03-017bf7dcacd2/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>docker-compose down</code> 후 <code>docer-compose up --scale node=8</code> 로 노드를 <strong>8개로 확장</strong><br>

</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/9115f01a-75aa-4757-b4ea-23371145bd34/image.png" width="1000" height="1000"/></div>

<ul>
<li>기존 노드들은 <strong>정상 상태</strong>이지만 <code>node-7</code> 과 <code>node-8</code> 은 <strong>슬롯 미할당 상태</strong>임을 확인<br>

</li>
</ul>
<h2 id="11-새로-생성된-노드7의-현재-노드-정보-확인">11. 새로 생성된 노드7의 현재 노드 정보 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/47877c28-2b78-45c4-8aa3-2ba72fb1a5d7/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>cluster-node-7</code> 컨테이너 접속 후 <code>cluster nodes</code> 로 <strong>노드 정보 확인</strong></li>
<li>현재 노드는 <strong>마스터</strong>이지만 <strong>슬롯 미할당 상태</strong>임을 확인<ul>
<li>원래대로면 <code>connected</code> 옆에 <strong>할당된 슬롯 번호들</strong>이 있어야 함</li>
<li>8번 항목 참고<br>

</li>
</ul>
</li>
</ul>
<h2 id="12-노드7을-기존-노드1의-클러스터에-추가">12. 노드7을 기존 노드1의 클러스터에 추가</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/9ad43b2f-85db-41c3-8d31-869a4584a33c/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>--cluster add-node {addedNode} {orgNode}</code> 명령어로 <strong>기존 클러스터</strong>에 <strong>레디스 노드 추가</strong><ul>
<li><strong>새로운 노드</strong>가 <strong>클러스터에 추가</strong><br>

</li>
</ul>
</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/799f7c59-90b5-465a-914f-2d7edc18a71b/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>7번 노드</strong>가 정상적으로 <strong>클러스터에 등록</strong>됨을 확인 가능</li>
<li>하지만 여전히 <strong>8번 노드</strong>는 클러스터에 등록되지 않음<ul>
<li><code>&lt;search&gt; Got no slots in CLUSTER SLOTS</code> -&gt; 아직 클러스터에 속하지 않았다는 의미</li>
<li>해당 노드는 현재 <strong>자신 외 다른 노드나 슬롯 정보를 알 수 없음</strong></li>
<li>따라서 <code>CLUSTER SLOTS</code> 명령 시 <strong>빈 배열 반환</strong></li>
<li>그래서 <strong>슬롯 정보가 없다는 메시지</strong>를 출력하는 것<br>

</li>
</ul>
</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/0af1e711-b034-45ba-b609-18b19db309a0/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>7번 마스터 노드</strong>가 클러스터에 추가<ul>
<li>하지만 여전히 <strong>슬롯은 미할당 상태</strong><br>

</li>
</ul>
</li>
</ul>
<h2 id="13-노드8을-노드7의-클러스터에-추가">13. 노드8을 노드7의 클러스터에 추가</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/4874b8e2-5ba8-457e-8196-6af2695b7126/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>8번 노드</strong>를 <strong>7번 노드</strong>에 추가</li>
<li><code>--cluster-slave</code> 지시자를 추가하여 <strong>8번 노드</strong>가 <strong>7번 노드의 레플리카</strong>임을 지시<br>

</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/a374fdcc-29af-4f0d-9e13-0ca6085f6221/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>8번 레플리카 노드</strong>가 클러스터에 추가</li>
<li><strong>7번-8번</strong>노드 간의 레플리케이션 정상 동작<ul>
<li><strong>데이터 동기화 완료</strong> 확인<br>

</li>
</ul>
</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/be8b6bcc-9d1b-43a9-81ec-d5dd460b1e83/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>7번 노드</strong>에서 <code>info replication</code> 으로 <strong>레플리케이션 정보</strong> 확인 가능<br>

</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/7cc75f9a-b4dd-4454-81cc-e840b78a66dc/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>7번 노드</strong>에서 <code>cluster info</code> 로 클러스터 정보 확인</li>
<li><code>cluster_known_nodes:8</code> 을 보아, 노드는 <strong>8개</strong>로 확장됨이 확인</li>
<li>하지만 <code>cluster_size:3</code> 을 보면, 현재 클러스터의 <strong>샤드</strong>는 <strong>4개</strong>로 확장되지 않고 여전히 기존의 <strong>3개</strong>임을 확인 가능<br>

</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/b8bbc80d-4cd0-4e4a-ad85-cbdf6d0ec654/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>7번 노드</strong>에서 <code>cluster nodes</code> 로 <strong>클러스터 내 노드 정보</strong> 확인</li>
<li>클러스터에 총 <strong>8개</strong>의 노드가 존재<ul>
<li>하지만 여전히 <strong>7번 마스터 노드</strong>에는 <strong>슬롯 미할당</strong><br>

</li>
</ul>
</li>
</ul>
<h2 id="14-노드7에-슬롯을-할당하고-샤드-슬롯-재분배">14. 노드7에 슬롯을 할당하고 샤드 슬롯 재분배</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/ffc1e54f-06d9-4d2a-9fcb-ad4110d75101/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>reshard {desIP/desPort}</code> 를 통해 <strong>샤드 슬롯 재분배</strong> 진행</li>
<li>총 <strong>3가지</strong> 질문에 대답<ul>
<li><strong>How many slots do you want to move (from 1 to 16384)?</strong> : <ul>
<li><strong>대상 노드</strong>에 분배할 <strong>슬롯의 양</strong></li>
<li>존재하는 슬롯 개수 <strong>16384</strong>를 분배될 샤드 <strong>4개</strong>로 나눈 <strong>4096</strong>으로 지정</li>
</ul>
</li>
<li><strong>What is the receiving node ID?</strong> :<ul>
<li><strong>분배받을 노드 ID</strong></li>
<li><code>cluster nodes</code> 에서 확인한 <strong>7번 노드의 ID</strong> 입력</li>
</ul>
</li>
<li><strong>Please enter all the source node IDs</strong> :<ul>
<li>노드에 슬롯을 분배하기 위해 <strong>슬롯을 일부 가져올 노드의 ID</strong></li>
<li><strong>all</strong> 을 입력하여 <strong>나머지 모든 노드 슬롯 재분배</strong><br>

</li>
</ul>
</li>
</ul>
</li>
</ul>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/278f4734-3ce1-4afd-ae68-8dae2593d9dc/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>cluster nodes</code> 를 통해 <strong>클러스터 내 노드 정보</strong> 확인</li>
<li>기존 노드들에서 <strong>4096 / 3</strong> 만큼의 슬롯이 차감됨을 확인</li>
<li><strong>7번 노드</strong>는 나머지 3개의 노드들로부터 가져온 슬롯들을 분배 받음을 확인 가능<ul>
<li><code>0-1364</code> <code>5461-6826</code> <code>10923-12287</code><br>

</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<p>Reference :</p>
<blockquote>
<ul>
<li>실전 레디스 : 기초, 실전, 고급 단계별로 배우는 레디스 핵심 가이드</li>
</ul>
</blockquote>
</blockquote>
<ul>
<li>ChatGPT ( <a href="https://chatgpt.com/">https://chatgpt.com/</a> )</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[레디스 마스터-레플리카 레플리케이션]]></title>
            <link>https://velog.io/@riverpower6_g/%EB%A0%88%EB%94%94%EC%8A%A4-%EB%A7%88%EC%8A%A4%ED%84%B0-%EB%A0%88%ED%94%8C%EB%A6%AC%EC%B9%B4-%EB%A0%88%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-7h6ldnn6</link>
            <guid>https://velog.io/@riverpower6_g/%EB%A0%88%EB%94%94%EC%8A%A4-%EB%A7%88%EC%8A%A4%ED%84%B0-%EB%A0%88%ED%94%8C%EB%A6%AC%EC%B9%B4-%EB%A0%88%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-7h6ldnn6</guid>
            <pubDate>Sun, 19 Oct 2025 14:49:01 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; 최근 <strong>레디스</strong> 개념 및 원리, 핵심 기능 등을 확실하게 이해하고 <strong>자바 스프링</strong>에 녹여넣을 수 있도록, <strong>실전 레디스 : 기초, 실전, 고급 단계별로 배우는 레디스 핵심 가이드</strong> 라는 도서를 읽고 있다.</p>
<p>&nbsp;본 도서를 읽으면서 <strong>스냅샷, AOF</strong> 등의 영속성이나 레디스의 각종 <strong>자료형과 명령어</strong>, <strong>보안</strong> 기능이나 <strong>트랜잭션</strong>처럼 일관 처리(레디스의 경우에는 롤백은 불가능) 등 여러 개념들을 알게 되었다.</p>
<p>&nbsp;그렇게 책을 읽으며 실습하던 중, <strong>마스터-레플리카</strong>(마스터-슬레이브 구조와 동일하지만, 최근에는 <strong>슬레이브</strong>라는 단어가 <strong>노예</strong>라는 말을 뜻하기 때문에 사용하지 않으려는 추세라고 함) 레플리케이션 파트에 돌입했다.
&nbsp;&nbsp; 해당 파트는 <strong>자바 스프링 백엔드 개발자</strong>로서의 스택을 쌓고 있는 필자가 <strong>레디스를 사용한다면 반드시 알아둬야 할 부분</strong>이라고 생각하여, 진행한 실습을 블로그에 정리해두기로 결정했다.</p>
<blockquote>
<p>📌 본 실습은 <strong>Windows</strong> 환경에서 <strong>WSL</strong> 을 통해 <strong>경량 가상 머신을 이용한 Linux 환경</strong>에서 진행</p>
</blockquote>
<p><br><br></p>
<h2 id="⌨️-실습-진행">⌨️ 실습 진행</h2>
<br>

<h2 id="1-wget을-통한-redisconf-설치">1. wget을 통한 redis.conf 설치</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/ee6b741f-011c-483b-a763-366c380f956b/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>wsl</code> 환경에서 <code>redisDocker</code> 라는 프로젝트 디렉토리 생성</li>
<li><code>redis-server --version</code> 을 통해, 설치된 레디스의 버전을 확인<ul>
<li>노트북에서 <code>apt install redis</code> 로 설치한 레디스 버전이 <strong>7.4.1</strong>임을 확인</li>
<li>데스크탑에서는 <strong>8.2.2</strong> 버전이 설치된 것으로 보아, 최신 버전으로 설치되지는 않았음을 확인</li>
<li>사실상 <strong>레디스 7 버전</strong>부터는 기능 사용에 큰 차이가 없으므로, 그대로 진행</li>
</ul>
</li>
<li><code>wget https://raw.githubusercontent.com/redis/redis/{version}/redis.conf</code> 로 <strong>레디스 기본값</strong>으로 사용되는 <code>redis.conf</code> 파일 다운로드<ul>
<li><strong>레디스 버전(7.4.1)</strong>을 <code>{version}</code> 에 적용<br></li>
</ul>
</li>
<li><code>ls</code> 명령어로 <code>redis.conf</code> 파일이 제대로 다운로드 되었음을 확인<br>

</li>
</ul>
<h2 id="2-기존-설정-파일을-복사하여-마스터용-conf-파일과-레플리카용-conf-파일-생성">2. 기존 설정 파일을 복사하여 마스터용 conf 파일과 레플리카용 conf 파일 생성</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/806ed73a-83d5-4ab5-96d4-b25a9134024b/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>cp {src} {dest}</code> 명령어를 통해 기존 <code>redis.conf</code> 파일을 그대로 복사</li>
<li><code>ls</code> 명령어로 복사 확인<br>

</li>
</ul>
<h2 id="3-마스터용-conf-파일에-requirepass로-비밀번호-지정">3. 마스터용 conf 파일에 requirepass로 비밀번호 지정</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/7fc0bfca-4e09-4dc1-ae2e-8be82906d36c/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>vi redis-master.conf</code> 로 문서 편집</li>
<li><code>requirepass {passwd}</code> 를 추가하여, <code>Auth</code> 보안 인증에 사용할 <strong>패스워드</strong> 적용<ul>
<li>이후부터 <strong>마스터</strong> 클라이언트에서 명령어를 사용하려면 <strong>해당 패스워드를 통한 인증 필요</strong><br>

</li>
</ul>
</li>
</ul>
<h2 id="4-마스터용-conf-파일에-debug-command-허용">4. 마스터용 conf 파일에 debug-command 허용</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/9e9df0a9-2b88-4565-a761-52d14fd25234/image.png" width="1000" height="1000"/></div>

<ul>
<li>이후 <code>debug</code> 기능을 사용하기 위해, <code>enable-debug-command</code> 를 변경<ul>
<li><strong>no -&gt; yes</strong><br>

</li>
</ul>
</li>
</ul>
<h2 id="5-레플리카용-conf-파일에-masterauth-지정">5. 레플리카용 conf 파일에 masterauth 지정</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/e13860fe-e538-49c6-9a5c-7ac9b067e284/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>vi redis-replica.conf</code> 로 문서 편집</li>
<li><code>masterauth {passwd}</code> 를 추가하여, <strong>마스터 노드 접근을 위해 필요한 인증 비밀번호</strong> 추가<ul>
<li><code>redis-master.conf</code> 에서 설정한 비밀번호와 동일<br>

</li>
</ul>
</li>
</ul>
<h2 id="6-레플리카용-conf-파일에-replicaof-지정">6. 레플리카용 conf 파일에 replicaof 지정</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/9487ae5c-edc3-4dc2-90bf-cf8b21b493fe/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>replicaof {master-name} {port}</code> 지정<ul>
<li>추후 마스터 컨테이너 이름을 <code>redis-master</code> 로 지정할 예정이므로 해당 이름 지정</li>
<li>레디스 기본 포트인 <code>6379</code> 지정<br>

</li>
</ul>
</li>
</ul>
<h2 id="7-docker-composeyml-파일-작성">7. docker-compose.yml 파일 작성</h2>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/21c5ccff-a9f8-4de4-8cb7-b56265e5ac26/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>로컬 환경</strong>에서 <strong>여러 컨테이너 기반 서비스</strong>를 동시에 구성하기 위해서 <code>docker-compose</code> 사용<ul>
<li><code>docker-compose</code> -&gt; <strong>여러 컨테이너</strong>를 정의하고 <strong>한꺼번에</strong> 실행/관리할 수 있는 도구</li>
</ul>
</li>
<li><code>vi docker-compose.yml</code> 명령어로 파일 편집 및 파일 생성<ul>
<li><code>services:</code> -&gt; <strong>사용할 서비스</strong> 명시</li>
<li><code>master:</code> , <code>replica:</code> -&gt; <strong>각 서비스명</strong>, <code>container_name:</code> 을 명시하지 않으면 <strong>컨테이너 이름</strong>이 <strong>{프로젝트명}_{서비스명}_{번호}</strong> 형식으로 설정</li>
<li><code>image:</code> -&gt; <strong>사용할 컨테이너 이미지</strong></li>
<li><code>ports:</code> -&gt; <strong>사용할 포트</strong><ul>
<li><strong>{로컬 포트}:{컨테이너 포트}</strong></li>
<li><strong>로컬에서 접속할 포트:컨테이너 내부에서 서비스가 리스닝하는 포트</strong></li>
</ul>
</li>
<li><code>volumes:</code> -&gt; <strong>볼륨 마운트를 통한 파일/디렉토리 동기화</strong><ul>
<li><strong>{동기화 할 파일 및 경로}:{컨테이너에 동기화되어 저장될 파일 및 경로}</strong></li>
<li><code>$PWD</code> 로 현재 <code>docker-compose.yml</code> 이 있는 디렉토리를 <strong>절대 경로</strong>로 치환</li>
</ul>
</li>
<li><code>command:</code> -&gt; <strong>컨테이너 시작 시 실행할 명령어</strong><ul>
<li><code>redis-server {사용 설정 파일 및 경로}</code> -&gt; <strong>컨테이너 시작 시 마운트된 볼륨 설정 파일</strong>을 기반으로 레디스 서버 시작</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><br><br><br></p>
<h2 id="🎯-트러블-슈팅---레디스-서버-포트-충돌">🎯 트러블 슈팅 - 레디스 서버 포트 충돌</h2>
<br>

<h2 id="ts1-1-포트-충돌-발생">TS1-1. 포트 충돌 발생</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/9dfc0ca4-4bc2-4d1f-a7be-49c3a7cf1fb9/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>6379</strong> 포트가 사용중이라는 메시지와 함께 에러 발생<br>

</li>
</ul>
<h2 id="ts1-2-레디스-서버-종료-시에도-자동-재시작">TS1-2. 레디스 서버 종료 시에도 자동 재시작</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/f8e0e587-b118-440e-8149-7b12c21a0f8a/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>sudo lsof -i :6379</code> 명령어로 현재 <strong>6379</strong> 포트를 사용하고 있는 프로세스 목록 추출</li>
<li><code>sudo kill {PID}</code> 명령어로 해당 프로세스 <strong>강제 종료</strong></li>
<li>새로운 <strong>PID</strong>로 레디스 서버가 다시 띄워져있는 것을 확인<br>

</li>
</ul>
<h2 id="ts1-3-systemd-내부의-레디스-서비스-파일-확인">TS1-3. systemd 내부의 레디스 서비스 파일 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/10ded3a8-76ae-4131-907c-3dc5bcdcda15/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>/etc/systemd/system/redis.service</code> 파일을 확인<br>

</li>
</ul>
<h2 id="ts1-4-레디스-서비스-파일의-restart-옵션-변경">TS1-4. 레디스 서비스 파일의 Restart 옵션 변경</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/2eba193c-8814-4eda-b044-90107e176cce/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>Restart</code> 옵션은 <strong>서버 종료 시 자동 재시작 여부</strong><ul>
<li><code>Restart=always</code> 가 초기값으로 설정되어 있음</li>
<li><code>Restart=no</code> 로 변경하여 <strong>서버 종료 시 자동 재시작 거부</strong><br>

</li>
</ul>
</li>
</ul>
<h2 id="ts1-5-해결---레디스-서버-자동-재시작-미진행-확인">TS1-5. 해결 - 레디스 서버 자동 재시작 미진행 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/5d44f417-f428-451d-9041-1a86aabb93bd/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>systemd</strong> 변경 적용을 위해 <code>sudo systemctl daemon-reload</code> 진행</li>
<li>레디스 설정을 변경했으므로, <code>sudo systemctl restart redis</code> 로 <strong>레디스 서버도 재시작</strong></li>
<li><code>sudo systemctl status redis-server</code> 로 현재 레디스 서버 상태 확인<ul>
<li><code>Active: active (running)</code> 확인 (동작 중)</li>
</ul>
</li>
<li>해당 프로세스 확인 및 강제종료</li>
<li><strong>6379</strong> 포트 사용 프로세스 재확인<ul>
<li>사용되지 않음을 확인!
<br><br><br></li>
</ul>
</li>
</ul>
<h2 id="8-docker-compose-up-정상-실행-확인">8. docker-compose up 정상 실행 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/b6e826ee-e3da-48e5-809c-b074ef8ab2c2/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>docker-compose up</code> 명령어로 <code>docker-compose.yml</code> 파일 기반 <strong>도커 엔진 실행</strong><ul>
<li><strong>도커 엔진</strong>은 디렉터리명을 <strong>프로젝트명</strong>으로 사용하여, 각 <strong>서비스</strong>에 대한 컨테이너 생성 및 실행<br>

</li>
</ul>
</li>
</ul>
<h2 id="9-docker-desktop-에서-컨테이너-생성-확인">9. Docker Desktop 에서 컨테이너 생성 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/66b276ad-e6e9-4160-9516-010caac2dadb/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>redisDocker</code> 의 <code>docker-compose.yml</code> 파일을 이용했으므로, 해당 디렉토리명을 <strong>프로젝트명</strong>으로 삼아 각 서비스 컨테이너 생성 및 실행<ul>
<li>지정해준 <strong>컨테이너명과 포트</strong> 등이 잘 적용되어 서버가 실행되고 있음을 확인
<br><br><br></li>
</ul>
</li>
</ul>
<h2 id="🎯-트러블-슈팅---마스터-레플리카-커넥션-거부">🎯 트러블 슈팅 - 마스터-레플리카 커넥션 거부</h2>
<br>

<h2 id="ts2-1-레플리케이션에서-마스터-레플리카-간의-커넥션-거부">TS2-1. 레플리케이션에서 마스터-레플리카 간의 커넥션 거부</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/9579196f-71ac-4d6f-b365-9c7036122750/image.png" width="1000" height="1000"/></div>

<ul>
<li>각 서비스는 잘 실행되었으나 커넥션 관련 에러 발생<ul>
<li><code>ERROR condition on socket for SYNC: Connection refused</code></li>
<li><strong>RDB</strong>파일 동기화를 위한 소켓 연결 자체가 거절되었다고 유추됨<br>

</li>
</ul>
</li>
</ul>
<h2 id="ts2-2-마스터용-conf-파일에서-bind-지시자-확인">TS2-2. 마스터용 conf 파일에서 bind 지시자 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/123a79b8-9b0e-466a-9646-79ecf03691da/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>bind 127.0.0.1</code> 확인</li>
<li>로컬과의 연결만 허용하는 것이기 때문에, <strong>외부 컨테이너</strong>에서의 연결은 비허용<ul>
<li>따라서, 마스터-레플리카 간의 연결은 <strong>서로 다른 컨테이너(서비스)</strong>이기 때문에 연결 비허용</li>
<li>둘은 <strong>같은 네트워크</strong> 상에 있음에도 불구하고, <code>127.0.0.1</code> 은 <strong>각 컨테이너의 내부 주소</strong>이므로 <strong>컨테이너 자신을 제외한 외부 컨테이너는 접근 불가능</strong><br>

</li>
</ul>
</li>
</ul>
<h2 id="ts2-3-해결---마스터용-conf-파일-bind-지시자-수정">TS2-3. 해결 - 마스터용 conf 파일 bind 지시자 수정</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/4a51498b-5b9e-4e76-8631-f037bf4e828f/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>bind 0.0.0.0</code> 으로 수정하여 <strong>외부 컨테이너의 접속 허용</strong><br>

</li>
</ul>
<h2 id="ts2-4-해결---마스터용-conf-파일-protected-mode-지시자-확인">TS2-4. 해결 - 마스터용 conf 파일 protected-mode 지시자 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/908f396f-7c30-4d37-9b2d-8bde859ba7b0/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>protected-mode yes</code> 인 경우, <strong>보안 강화 모드</strong>로 작동하기 때문에, 기본적으로 <strong>외부 컨테이너 접속</strong>을 차단<ul>
<li><strong>컨테이너 내부</strong> 접속이거나 <strong>requirepass 혹은 ACL</strong>이 설정되어 있는 경우 동작 허용</li>
<li>그 외의 경우 <strong>연결 모두 거부</strong></li>
<li>즉, <strong>외부 연결을 오픈</strong>했는데 <strong>비밀번호도 없는 경우</strong>에는 연결 거부</li>
<li><code>protected-mode yes</code> 가 기본값</li>
</ul>
</li>
<li>현재 <code>bind 0.0.0.0</code> 으로 <strong>외부 컨테이너 접속 허용</strong> 상태이므로 <code>protected-mode yes</code> 설정을 통해, <strong>반드시 인증을 진행해야만 접속</strong>할 수 있도록 설정
<br><br><br></li>
</ul>
<h2 id="10-마스터-레플리카-정상-연결-및-데이터-복사-확인">10. 마스터-레플리카 정상 연결 및 데이터 복사 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/4eaf88e6-1ce0-4045-aa26-a48e98f7d810/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>마스터-레플리카</strong> 간의 연결이 제대로 성사되었음을 확인</li>
<li><strong>RDB 동기화</strong>가 성공적으로 되었음을 확인<ul>
<li><code>Synchronization with replica 172.18.0.3:6379 succeeded</code></li>
<li><strong>레플리카</strong>는 기본적으로 <strong>마스터의 스냅샷</strong>인 <strong>RDB</strong> 파일을 <strong>비동기적으로 복사</strong>하는 방식으로 <strong>데이터 동기화 진행</strong></li>
<li><strong>AOF 파일</strong>을 통해서도 레플리케이션이 <strong>가능하지만</strong>, 기본적으로 <strong>RDB 스냅샷 파일</strong>을 통한 레플리케이션 진행<br>

</li>
</ul>
</li>
</ul>
<h2 id="11-마스터에서-레플리카로-핑을-보내는지-확인">11. 마스터에서 레플리카로 핑을 보내는지 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/fe26e10a-c1d5-4105-a6c7-83e5a5e11d81/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>레플리카 쉘</strong>에 접속 후, 레디스 클라이언트에서 <code>monitor</code> 명령</li>
<li>기본값인 <strong>10초 간격</strong>으로 마스터인 <code>172.18.0.2:6379</code> 로부터 <code>ping</code> 이 호출되고 있음을 확인 가능<br>

</li>
</ul>
<h2 id="12-마스터에서-레플리케이션-관련-정보-확인">12. 마스터에서 레플리케이션 관련 정보 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/5d493c08-e703-4d7c-be4d-a8238021961d/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>마스터</strong> 클라이언트 접속 후 <code>info replication</code> 명령 수행<ul>
<li><strong>인증 없이</strong> 명령 불가능하므로, <code>auth {passwd}</code> 를 사용하여 인증 수행</li>
</ul>
</li>
<li><code>role:master</code> 를 통해 <strong>마스터</strong> 노드임을 확인 가능</li>
<li><code>connected_slaves:1</code> 을 통해, <strong>연결된 레플리카</strong>가 <strong>1대</strong>임을 확인<ul>
<li>해당 레플리카 관련 정보 또한 표시</li>
</ul>
</li>
<li>그 외, <strong>페일오버</strong> 전략 등 <strong>레플리케이션 관련 정보</strong>들을 확인 가능<ul>
<li><strong>페일오버</strong> : <strong>마스터 노드 연결 문제</strong> 발생 시, 보유하고 있는 레플리카로 <strong>마스터 역할을 승격</strong> 작업</li>
<li><code>no-failover</code> 이므로, 페일오버 전략은 따로 설정되지 않음<br>

</li>
</ul>
</li>
</ul>
<h2 id="13-레플리카에서-레플리케이션-관련-정보-확인">13. 레플리카에서 레플리케이션 관련 정보 확인</h2>
<hr>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/a3bad123-722b-4424-8c58-7f7f5480e5d0/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>레플리카</strong> 클라이언트 접속 후 <code>info replication</code> 명령 수행</li>
<li><code>role:slave</code> 를 통해 <strong>레플리카</strong> 노드임을 확인 가능</li>
<li>해당 레플리카의 <strong>마스터 관련 정보</strong>들 확인 가능<ul>
<li><strong>호스트, 포트, 상호 연결 상태</strong> 등</li>
</ul>
</li>
<li><code>slave_read_only:1</code> 을 통해 <strong>레플리카</strong>가 읽기 전용 모드인지 확인 가능<ul>
<li><strong>1</strong>은 읽기 전용, <strong>0</strong>은 읽기/쓰기 모두 가능</li>
</ul>
</li>
<li>마찬가지로 <strong>페일오버</strong> 전략 또한 확인 가능</li>
</ul>
<hr>
<blockquote>
<p>Reference :</p>
<blockquote>
<ul>
<li>실전 레디스 : 기초, 실전, 고급 단계별로 배우는 레디스 핵심 가이드</li>
</ul>
</blockquote>
</blockquote>
<ul>
<li>ChatGPT ( <a href="https://chatgpt.com/">https://chatgpt.com/</a> )</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[레디스 최악의 사례 7가지]]></title>
            <link>https://velog.io/@riverpower6_g/%EB%A0%88%EB%94%94%EC%8A%A4-%EC%B5%9C%EC%95%85%EC%9D%98-%EC%82%AC%EB%A1%80-7%EA%B0%80%EC%A7%80</link>
            <guid>https://velog.io/@riverpower6_g/%EB%A0%88%EB%94%94%EC%8A%A4-%EC%B5%9C%EC%95%85%EC%9D%98-%EC%82%AC%EB%A1%80-7%EA%B0%80%EC%A7%80</guid>
            <pubDate>Fri, 17 Oct 2025 13:52:14 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; 최근 <strong>레디스</strong>에 대한 개념을 확실하게 이해하고 개발에 사용하기 위해서, <strong>실전 레디스 : 기초, 실전, 고급 단계별로 배우는 레디스 핵심 가이드</strong> 도서를 읽으며 내용을 이해하고, 실습하고 있다.</p>
<p>&nbsp; 여러 개념들을 알아가던 중, 굉장히 눈에띄는 문구가 있었다.</p>
<blockquote>
<p><strong>레디스 최악의 사례 7가지</strong></p>
</blockquote>
<p>&nbsp; 이 얼마나 폭력적인 문장인가. 딱 봐도 <strong>레디스 기술 사용</strong>에 있어서 굉장히 중요한 요소임을 강조하는 듯하다.</p>
<p>&nbsp; 그런 의미에서, 해당 내용을 블로그에 정리해두기로 결정했다.
<br><br></p>
<h2 id="1️⃣-비밀번호-미설정">1️⃣ 비밀번호 미설정</h2>
<hr>
<ul>
<li>많은 경우, 레디스에서 <strong>비밀번호를 설정하지 않음</strong></li>
<li>비밀번호를 따로 설정하지 않는다면, 해당 레디스에 접속한 <strong>어떤 사람이든지</strong> 레디스 명령어 사용 가능<ul>
<li><strong>레디스 설정</strong>을 바꾸거나, <strong>데이터 삭제</strong>, <strong>디도스 공격</strong> 등 일어나서는 안 되는 일들이 벌어질 수 있음</li>
<li><strong>해킹</strong>에 매우 취약</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 <strong>반드시 비밀번호를 설정하자</strong></p>
</blockquote>
<pre><code class="language-bash"># redis.conf
requirepass {passwd}</code></pre>
<ul>
<li>사용하는 <code>conf</code> 설정 파일에서 <code>requirepass</code> 를 반드시 지정<ul>
<li>해당 비밀번호를 통해 인증해야만, 레디스 명령어 사용 가능<br>

</li>
</ul>
</li>
</ul>
<pre><code class="language-bash"># redis-cli 에서 명령어 실행하려면
AUTH {passwd}</code></pre>
<ul>
<li><code>redis-cli</code> 등 클라이언트에서 <strong>명령어를 실행</strong>하기 위해서는, 반드시 <code>AUTH</code> 를 통해 <strong>인증이 완료</strong>되어야 함<ul>
<li>비밀번호 없이는 레디스 명령어 사용 금지<br>

</li>
</ul>
</li>
</ul>
<h2 id="2️⃣-keys-명령어-사용-금지">2️⃣ KEYS 명령어 사용 금지</h2>
<hr>
<ul>
<li><code>KEYS {pattern}</code> : <strong>레디스 서버</strong>에서 사용 중인 <strong>키</strong>들을 전부 확인하는 명령어</li>
<li>키의 개수가 많아지면 많아질수록 <strong>성능에 큰 영향</strong></li>
</ul>
<blockquote>
<p>🤔 <strong><em>WHY</em></strong></p>
</blockquote>
<ul>
<li>레디스는 기본적으로 <strong>단일 스레드</strong> 구조<ul>
<li><code>KEYS</code> 실행 중 다른 작업들은 전부 <strong>블로킹</strong></li>
</ul>
</li>
<li><code>KEYS</code> 의 시간복잡도는 <code>O(N)</code></li>
<li>최대 <strong>2의 32승</strong>개의 키를 저장 가능<ul>
<li>최악의 경우 <strong>2의 32승</strong>의 시간복잡도</li>
</ul>
</li>
<li><strong>운영 환경</strong>에서 잘못 사용한다면, 한 번의 <code>KEYS</code> 명령으로 <strong>레디스 서버</strong> 전체가 <strong>몇 초 ~ 수십 초</strong>간 블로킹될 가능성 존재</li>
</ul>
<blockquote>
<p>💡 <strong><code>SCAN</code> 명령어를 사용하자</strong></p>
</blockquote>
<pre><code class="language-bash">SCAN 0 MATCH user:* COUNT 100</code></pre>
<ul>
<li><code>SCAN</code> 명령어는 기본적으로 <strong>비동기적</strong> 조회<ul>
<li>서버를 <strong>블로킹</strong>하지 않음</li>
</ul>
</li>
<li><code>SCAN</code> 명령어와 <strong>인덱스</strong>, <strong>카운트 개수</strong>를 여러 번 호출하여, 전체 키를 <strong>천천히 순회</strong></li>
<li><strong>배치 처리</strong>와 유사<br>

</li>
</ul>
<h2 id="3️⃣-select---번호-기반-db-사용">3️⃣ SELECT - 번호 기반 DB 사용</h2>
<hr>
<ul>
<li><code>SELECT {번호}</code> 명령을 사용하면 <strong>레디스 내의 각자 다른 데이터베이스</strong>를 사용할 수 있음<ul>
<li>ex) <code>SELECT 0</code> , <code>SELECT 1</code></li>
<li><strong>0-based-index</strong></li>
</ul>
</li>
<li>하지만 이런 <strong>번호 기반 데이터베이스</strong>들은 <strong>완전 부리된 데이터베이스</strong>가 아님<ul>
<li><strong>논리적</strong>으로만 다른 데이터베이스</li>
</ul>
</li>
</ul>
<blockquote>
<p>🤔 <strong><em>WHY</em></strong></p>
</blockquote>
<ul>
<li>같은 <strong>키:값</strong>이 <strong>DB 0, DB 1</strong>에서 각각 <strong>별도로 존재</strong>할 수 있지만, 내부적으로는 <strong>동일한 레디스 인스턴스 자원(CPU, 메모리, 이벤트 루프 등)을 공유</strong><ul>
<li><strong>DB 0</strong>에서 <code>KEYS</code> 명령을 사용하면, 이 명령은 <strong>레디스 인스턴스의 이벤트 루프를 블로킹</strong>하므로, <strong>같은 인스턴스 내 데이터베이스 또한 모두 블로킹</strong></li>
<li>즉, 독립적인 것처럼 보여도 <strong>실제 격리/스케일링 측면에서는 전혀 독립적이지 않음</strong></li>
</ul>
</li>
<li><strong>기술적인 면</strong>에서 또한, <strong>클러스터 환경</strong>에서는 번호 기반 데이터베이스 <strong>미지원</strong><ul>
<li><strong>단일 노드</strong>에서만 정상 동작 가능</li>
<li><strong>확장 불가</strong></li>
</ul>
</li>
<li>즉, <strong>확장성, 안정성</strong> 측면에서 <strong>심각한 제약</strong> 초래</li>
<li>레디스 창시자 <strong>Salvatore Sanfilippo</strong> 또한, <strong>&quot;레디스에서 만든 최악의 디자인 실수&quot;</strong> 라고 평가할 정도</li>
</ul>
<blockquote>
<p>💡 <strong>번호 기반 DB 대신, 레디스 인스턴스를 새로 띄워 분리하여 운영하자</strong></p>
</blockquote>
<ul>
<li>레디스는 <strong>인스턴스 하나당 메모리 오버헤드가 낮음</strong></li>
<li>따라서 <strong>레디스 서버</strong>를 따로 띄워 <strong>인스턴스</strong>를 추가하고 <strong>클러스터 버스</strong>라는 내부 채널 안에서 노드 간 통신을 하는 <strong>레디스 클러스터</strong> 방식을 사용하면, <strong>샤딩</strong>을 통한 데이터 분리 및 <code>MOVED</code>  명령을 통한 <strong>해당 데이터 소유 캐시 노드로의 자동 리다이렉션</strong> 또한 가능<br>

</li>
</ul>
<h2 id="4️⃣-hgetall-lrange-같은-무제한-반환-주의">4️⃣ HGETALL, LRANGE 같은 무제한 반환 주의</h2>
<hr>
<ul>
<li>레디스 명령어 중 일부는 <strong>모든 데이터를 한 번에 반환</strong><ul>
<li><code>HGETALL</code> : <strong>Hash</strong> 자료형의 모든 <strong>field/value</strong> 반환</li>
<li><code>LRANGE 0 -1</code> : <strong>List</strong> 자료형의 모든 요소 반환<ul>
<li>레디스에서 오프셋에 사용되는 <code>0</code> 은 시작 인덱스이며, <code>-1</code> 은 마지막 인덱스</li>
</ul>
</li>
<li><code>SMEMBERS</code> : <strong>Set</strong> 자료형의 모든 요소 반환</li>
<li><code>ZRANGE 0 -1</code> : <strong>Sorted Set</strong> 자료형의 모든 요소 반환</li>
<li>etc)...</li>
</ul>
</li>
</ul>
<blockquote>
<p>🤔 <strong><em>WHY</em></strong></p>
</blockquote>
<ul>
<li><strong>매우 큰 데이터 구조</strong><ul>
<li><strong>Hash</strong> : 필드 수 최대 <strong>2의 32승</strong>, 값 최대 <strong>512MB</strong></li>
<li><strong>List</strong> : 요소 수 최대 <strong>2의 32승</strong>, 값 최대 <strong>512MB</strong></li>
<li><strong>Set / Sorted Set</strong> : 요소 수 매우 많을 수 있음</li>
</ul>
</li>
<li><strong>사용 데이터가 누적</strong>된다면 기존 예상보다 <strong>훨씬 큰 데이터 구조</strong>가 될 가능성 존재</li>
<li>이러한 데이터를 <strong>한꺼번에 모두</strong> 가져온다면, 최악의 경우<strong>레디스 서버</strong> 자체가 다운</li>
</ul>
<blockquote>
<p>💡 <strong>크기를 먼저 체크한 후, 필요한 범위 내에서만 조회하자</strong></p>
</blockquote>
<ul>
<li><strong>데이터 크기 확인</strong><ul>
<li><strong>Hash</strong> : <code>HLEN {key}</code></li>
<li><strong>List</strong> : <code>LLEN {key}</code></li>
<li><strong>Set</strong> : <code>SCARD {key}</code></li>
<li><strong>Sorted Set</strong> : <code>ZCARD {key}</code></li>
</ul>
</li>
<li><strong>필요한 만큼만 부분 조회</strong><ul>
<li><strong>List</strong> : <code>LRANGE {startIdx} {endIdx}</code></li>
<li>그 외 : <code>{Xxx}SCAN</code> ...<br>

</li>
</ul>
</li>
</ul>
<h2 id="5️⃣-하나의-커넥션-당-하나의-명령은-비효율적">5️⃣ 하나의 커넥션 당 하나의 명령은 비효율적</h2>
<hr>
<ul>
<li>대부분의 <strong>네트워크 통신</strong> 사용 기술에서 공통적으로 <strong>명시되는 명제</strong></li>
<li>레디스는 <strong>지속 연결</strong> 활용을 전제로 설계<ul>
<li><strong>레디스 클러스터</strong> 사용 시 :<ul>
<li><strong>OSS Cluster API</strong> -&gt; <strong>클라이언트</strong>가 노드별 연결 유지</li>
<li><strong>Redis Enterprise</strong> -&gt; <strong>프록시</strong>를 통한 연결 관리, 클러스터 복잡성은 숨김 처리</li>
</ul>
</li>
</ul>
</li>
<li><strong>매 연결 생성 및 해제 시 오버헤드 발생</strong><ul>
<li><strong>지속 연결</strong> 활용 불가능</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 <strong>지속 연결을 활용하자</strong></p>
</blockquote>
<ul>
<li><strong>연결</strong>은 열어둔 채 <strong>여러 명령을 수행</strong></li>
<li>필요 시 <strong>커넥션 풀</strong> 활용</li>
<li><strong>REST 스타일</strong> 연결 패턴은 피할 것<ul>
<li><strong>요청마다 연결 및 해제</strong>하기 때문<br>

</li>
</ul>
</li>
</ul>
<h2 id="6️⃣-hot-keys-문제">6️⃣ Hot Keys 문제</h2>
<hr>
<ul>
<li>레디스는 <strong>자주 접근되는 핵심 데이터</strong>를 저장할 때 효율 상승</li>
<li>그렇지만 <strong>특정 몇 개의 키만</strong> 반복해서 접근하게 되면 <strong>Hot Keys</strong> 문제 발생</li>
</ul>
<blockquote>
<p>🤔 <strong><em>WHY</em></strong></p>
</blockquote>
<ul>
<li><strong>레디스 클러스터</strong> 구조에서는 <strong>키가 데이터 저장 위치를 결정</strong><ul>
<li><strong>키 해싱</strong>을 통해 <strong>슬롯 위치</strong>를 판단하고, 해당 슬롯을 <strong>어느 샤드</strong>에 분배할지 결정</li>
</ul>
</li>
<li><strong>하나의 키에 요청 집중</strong> 시 :<ul>
<li><strong>해당 특정 노드</strong>에 <strong>모든 트래픽 집중</strong></li>
<li>클러스터 내의 <strong>나머지 노드</strong>는 미활용</li>
<li><strong>병목 현상</strong> 발생</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 <strong>작은 수의 자주 쓰는 키 집중을 피하자</strong></p>
</blockquote>
<ul>
<li>설계 단계에서 <strong>Hot Key</strong> 패턴을 방지<ul>
<li><strong>키 분산 설계</strong></li>
<li><strong>동일 데이터를 여러 키에 분산 저장</strong></li>
<li>즉, <strong>서로 다른 샤드</strong>에 배치</li>
<li>etc)...<br>

</li>
</ul>
</li>
</ul>
<h2 id="7️⃣-레디스를-주-db-로-사용할-때-주의">7️⃣ 레디스를 주 DB 로 사용할 때 주의</h2>
<hr>
<ul>
<li>레디스를 <strong>주 DB</strong>로 사용하는 경우, 다운되면 <strong>전체 애플리케이션 중단</strong></li>
<li>단순한 <strong>레디스 기본 설정</strong>으로는 안정적인 주 DB 역할은 <strong>기대하기 어려움</strong></li>
</ul>
<blockquote>
<p>💡 <strong>적절한 HA와 Durability를 설정하자</strong></p>
</blockquote>
<ul>
<li><strong>고가용성 (High Availability)</strong><ul>
<li><strong>OSS Redis</strong> -&gt; <strong>Redis Sentinel</strong> 설정<ul>
<li><strong>모니터링</strong>을 통해 <strong>마스터 서버 정상 연결 유무 확인</strong></li>
<li><strong>페일오버</strong>를 통해 <strong>마스터 서버 장애 시 레플리카를 마스터로 자동 승격</strong></li>
<li><strong>클라이언트</strong>에게 <strong>승격된 마스터 IP</strong>를 마스터 서버로 알림</li>
<li>이러한 과정을 통해, <strong>한 대의 레디스 서버</strong>가 죽더라도 <strong>서비스 미중단</strong></li>
</ul>
</li>
</ul>
</li>
<li><strong>영속성 (Durability / Persistence)</strong><ul>
<li><strong>OSS Redis</strong> -&gt; <strong>RDB 스냅샷</strong> 또는 <strong>AOF 파일</strong> 사용<ul>
<li>해당 파일들이 일종의 <strong>백업 DB 파일</strong></li>
<li>레디스는 기본적으로 <strong>인-메모리</strong> 방식이므로 휘발성 메모리(RAM)를 사용하지만, 해당 파일들로부터 <strong>데이터를 불러오는 것이 가능</strong><br>

</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>📌 <strong>Redis Enterprise</strong> 를 사용하는 경우, 별도의 설정 없이도 <strong>멀티 노드 클러스터, 복구, 백업</strong>이 모두 자동화되므로 <strong>바로 주 DB로 사용 가능</strong></p>
<hr>
<blockquote>
<p>Reference :</p>
<blockquote>
<ul>
<li><a href="https://redis.io/blog/7-redis-worst-practices">https://redis.io/blog/7-redis-worst-practices</a></li>
</ul>
</blockquote>
</blockquote>
<ul>
<li>실전 레디스 : 기초, 실전, 고급 단계별로 배우는 레디스 핵심 가이드</li>
<li>ChatGPT ( <a href="https://chatgpt.com/">https://chatgpt.com/</a> )</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Q&A - @Scheduled, @Async 과  ThreadPool]]></title>
            <link>https://velog.io/@riverpower6_g/QA-Scheduled-Async-%EA%B3%BC-ThreadPool</link>
            <guid>https://velog.io/@riverpower6_g/QA-Scheduled-Async-%EA%B3%BC-ThreadPool</guid>
            <pubDate>Wed, 15 Oct 2025 14:47:23 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&nbsp;본 포스트는 <strong>학습 중 생긴 궁금증 해결 및 아이디어 검증</strong>에 관한 내용을 <strong>정리 및 공유</strong>용도가 아닌 <strong>기록</strong> 용도로 작성하였습니다.
<strong>학습 Q&amp;A 태그</strong> 포스팅은 동일하게 아래와 같이 진행될 예정입니다.<br></p>
<blockquote>
<ul>
<li>학습 중 생긴 <strong>여러 아이디어가 실현 가능한지</strong> 확인한 결과를 보관</li>
</ul>
</blockquote>
</blockquote>
<ul>
<li>학습 중 생긴 <strong>궁금증을 해소한 과정 및 결과</strong>를 보관</li>
<li><strong>추후 개발</strong>에 해당 아이디어들을 <strong>참고</strong>하기 위한 <strong>기록</strong> 용도이므로 구조가 자유롭고 다소 두서없을 수 있으며, 자유롭게 작성</li>
</ul>
<p><br><br></p>
<h3 id="q1-scheduled-메서드는-동기로-실행될까-비동기로-실행될까">Q1. <em>@Scheduled</em> 메서드는 동기로 실행될까, 비동기로 실행될까?</h3>
<hr>
<h3 id="a1-동기-로-실행">A1. <em>동기</em> 로 실행</h3>
<ul>
<li>기본적으로 <code>@Scheduled</code> 메서드는 <strong>동기</strong>로 실행
<br><br></li>
</ul>
<h3 id="q2-scheduled-메서드를-사용할-때의-기본-스레드풀-타입-과-생성-전략-은-무엇일까">Q2. <em>@Scheduled</em> 메서드를 사용할 때의 기본 <em>스레드풀 타입</em> 과 <em>생성 전략</em> 은 무엇일까?</h3>
<hr>
<h3 id="a2-defaultmanagedtaskscheduler-사용">A2. <em>DefaultManagedTaskScheduler</em> 사용</h3>
<ul>
<li>별도의 설정이 없다면, <code>DefaultManagedTaskScheduler</code> 를 생성</li>
<li>내부적으로 <code>SingleThreadScheduledExecutor</code> 를 <strong>스레드풀</strong>로 사용<ul>
<li><strong>오직 하나의 스레드만</strong> 생성</li>
<li><strong>해당 스레드만</strong> 스레드풀에 저장 후 재사용</li>
</ul>
</li>
<li><strong>모든 @Scheduled 메서드</strong>는 해당 스레드풀에서 관리<br></li>
<li>따라서 <code>@Scheduled</code> 메서드는 기본적으로 <strong>동기</strong>로 실행되므로, <strong>한 @Scheduled 메서드가 실행 중</strong>이면 <strong>완료시까지 다른 메서드들은 실행되지 못함</strong><ul>
<li><code>@Scheduled</code> 메서드가 <strong>비슷한 시간에 여러 개 작성</strong>되어 있을 경우, 문제가 발생할 수 있음
<br><br></li>
</ul>
</li>
</ul>
<h3 id="q3-scheduled-용-스레드풀을-만들-때-반환-타입-은">Q3. <em>@Scheduled</em> 용 스레드풀을 만들 때 <em>반환 타입</em> 은?</h3>
<hr>
<h3 id="a3-taskscheduler-인터페이스-혹은-threadpooltaskscheduler">A3. <em>TaskScheduler</em> 인터페이스, 혹은 <em>ThreadPoolTaskScheduler</em></h3>
<ul>
<li><code>@Scheduled</code> 메서드는 내부적으로 <code>ScheduledAnnotationBeanPostProcessor</code> -&gt; <code>TaskScheduler</code> 인터페이스를 기준으로 동작</li>
<li><code>ThreadPoolTaskScheduler</code> 는 <code>TaskScheduler</code> 인터페이스의 <strong>구현체</strong></li>
<li>보통 <code>ThreadPoolTaskScheduler</code> 타입으로 반환<ul>
<li>빈 등록 시 <strong>인터페이스 타입으로 반환</strong>하는 것이 일반적</li>
<li>하지만 <code>TaskScheduler</code> 는 기능이 제한적</li>
<li><code>ThreadPoolTaskScheduler</code> 를 사용하면 <strong>구체 클래스 설정 편리</strong><br>

</li>
</ul>
</li>
</ul>
<blockquote>
<p><code>@Scheduled</code> 메서드가 자동으로 등록하는 <strong>스레드풀 빈 이름</strong></p>
</blockquote>
<ul>
<li><code>@Scheduled</code> 메서드는 <code>taskScheduler</code> 라는 이름의 스레드풀 빈을 먼저 탐색 후 사용</li>
<li><strong>메서드명</strong>을 <code>taskScheduler</code> 로 설정하여 빈으로 등록하면, <code>@Scheduled</code> 메서드에서 이 <strong>스레드풀 빈</strong>을 자동으로 <code>DI</code> 받아서 사용</li>
</ul>
<p><br><br></p>
<h3 id="q4-async-메서드의-기본-스레드풀-타입-과-생성-전략-은-무엇일까">Q4. <em>@Async</em> 메서드의 <em>기본 스레드풀 타입</em> 과 <em>생성 전략</em> 은 무엇일까?</h3>
<hr>
<h3 id="a4-simpleasynctaskexecutor-사용">A4. <em>SimpleAsyncTaskExecutor</em> 사용</h3>
<ul>
<li>별도의 설정이 없다면, <code>SimpleAsyncTaskExecutor</code> 를 <strong>기본 스레드풀</strong>로 지정</li>
<li><strong>매번 새로운 스레드</strong>를 생성<ul>
<li>즉, 스레드를 <strong>재사용하지 않음</strong></li>
<li>요청이 많을 경우, 그만큼의 스레드를 <strong>새로 생성</strong>하여 작업하므로 성능 저하 또는 <strong>OOM</strong> 발생 가능</li>
<li>매 스레드는 <strong>작업 종료 시 반납</strong>되어 <code>GC</code> 에 의해 처리가 되기는 함
<br><br></li>
</ul>
</li>
</ul>
<h3 id="q5-async-용-스레드풀을-만들-때-반환-타입-은">Q5. <em>@Async</em> 용 스레드풀을 만들 때 <em>반환 타입</em> 은?</h3>
<hr>
<h3 id="a5-executor-사용">A5. <em>Executor</em> 사용</h3>
<ul>
<li><code>@Async</code> 메서드는 내부적으로 <code>AsyncExecutionAspectSupport</code> -&gt; <code>Executor</code> 인터페이스를 기준으로 동작</li>
<li>보통 <code>Executor</code> 타입으로 반환<ul>
<li>일반적으로 <strong>빈 등록 시 인터페이스 타입으로 반환</strong></li>
<li>해당 인터페이스 타입 그대로 반환해도 충분<br>

</li>
</ul>
</li>
</ul>
<blockquote>
<p><code>@Async</code> 메서드가 자동으로 등록하는 <strong>스레드풀 빈 이름</strong></p>
</blockquote>
<ul>
<li><code>@Async</code> 메서드는 <code>taskExecutor</code> 라는 이름의 스레드풀 빈을 먼저 탐색 후 사용</li>
<li><strong>메서드명</strong>을 <code>taskExecutor</code> 로 설정하여 빈으로 등록하면, <code>@Async</code> 메서드에서 이 <strong>스레드풀 빈</strong>을 자동으로 <code>DI</code> 받아서 사용
<br><br></li>
</ul>
<h3 id="q6-async-스레드풀에는-queuesize-를-정의하고-scheduled-스레드풀에는-queuesize-및-maxpoolsize-를-정의하지-않는-이유">Q6. <em>@Async</em> 스레드풀에는 <em>QueueSize</em> 를 정의하고, <em>@Scheduled</em> 스레드풀에는 <em>QueueSize</em> 및 <em>MaxPoolSize</em> 를 정의하지 않는 이유</h3>
<hr>
<h3 id="a6-자체-관리-및-예측-가능-하기-때문">A6. <em>자체 관리</em> 및 <em>예측 가능</em> 하기 때문</h3>
<ul>
<li><strong>사용자 요청</strong>으로 인한 <code>@Async</code> 메서드와는 달리, <code>@Scheduled</code> 메서드 실행 수는 개발자가 <strong>충분히 예측 가능</strong></li>
<li>따라서 <code>CorePoolSize</code> 만 설정해줘도 충분<ul>
<li>문제 발생 시 <code>CorePoolSize</code> 만 조금씩 증가</li>
</ul>
</li>
<li><code>QueueSize</code> 의 경우, <code>ThreadPoolTaskScheduler</code> 는 내부적으로 <code>ScheduledThreadPoolExecutor</code> 사용<ul>
<li><code>ScheduledThreadPoolExecutor</code> 는 <strong>대기 큐를 자체적으로관리</strong> <br>

</li>
</ul>
</li>
</ul>
<blockquote>
<p>🎯 따라서 별도의 큐 설정은 불필요하며, <code>setPoolSize()</code> 로 풀 사이즈만 조정해주면 된다.</p>
</blockquote>
<p><br><br></p>
<h1 id="💡-관련-토픽-아이디어">💡 관련 토픽 아이디어</h1>
<hr>
<h3 id="📌-async-메서드용-커넥션풀_과-_scheduled-메서드용-커넥션풀-을-직접-지정하여-사용">📌 <em>@Async</em> 메서드용 <em>커넥션풀_과 _@Scheduled</em> 메서드용 <em>커넥션풀</em> 을 직접 지정하여 사용</h3>
<br>

<pre><code class="language-java">@Configuration
@EnableAsync
@EnableScheduling
public class ThreadPoolConfig {

    private final int core = Runtime.getRuntime().availableProcessors();

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(core);
        executor.setMaxPoolSize(core * 10);
        executor.setQueueCapacity(1000);
        executor.setThreadNamePrefix(&quot;async-&quot;);

        return executor;
    }

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(core * 2);
        scheduler.setThreadNamePrefix(&quot;scheduler-&quot;);

        return scheduler;
    }
}</code></pre>
<ul>
<li><code>taskExecutor</code> 빈은 <code>@Async</code> 메서드에서 자동으로 사용</li>
<li><code>@Async</code> 메서드는 일반적으로 <strong>I/O 바운드</strong> 작업일 확률이 높으므로, <code>maxPoolSize</code> 를 <strong>core * 10</strong> 으로 설정<ul>
<li>일반적으로 <strong>코어 수 * 10</strong> 까지는 안전하게 실행 가능</li>
<li>그럼에도, <strong>스레드풀 관련 작업은 모니터링 및 성능 테스트 진행 권장</strong></li>
</ul>
</li>
<li><code>queueCapacity</code> 는 <strong>1,000</strong> 으로 설정<ul>
<li>해당 큐에는 <strong>스레드가 바쁠 때 대기할 작업</strong>들을 보관</li>
<li><strong>I/O 바운드</strong> 작업일 확률이 높으므로, 어느정도 넉넉하게 <strong>1,000</strong>개로 설정</li>
<li>마찬가지로 <strong>모니터링 및 성능 테스트 권장</strong></li>
</ul>
</li>
<li><code>taskScheduler</code> 빈은 <code>@Scheduled</code> 메서드에 자동으로 사용</li>
<li><code>@Scheduled</code> 메서드는 <strong>예약 작업</strong>이므로, 개발자가 어느 정도 실행될지 <strong>예상 가능</strong><ul>
<li><code>corePoolSize</code> 를 <strong>core</strong> 수 만큼, 혹은 넉넉하게 <strong>core * 2</strong> 로 설정</li>
<li><code>@Scheduled</code> 로 설정한 작업이 많다면 그에 비례해 <strong>성능 테스트 및 부하 테스트</strong> 진행해가며 조율</li>
</ul>
</li>
<li><code>ThreadNamePrefix</code> 를 <strong>async-, scheduler-</strong>로 구분<ul>
<li>디버깅 편리
<br><br></li>
</ul>
</li>
</ul>
<blockquote>
<p>📌 각 용도의 스레드풀을 <strong>명시적으로 지정하는 방법</strong></p>
</blockquote>
<pre><code class="language-java">// ...
public class ThreadPoolConfig implements AsyncConfigurer, SchedulingConfigurer {
    // ...

    // @Async 전용 스레드풀 지정
    @Override
    public Executor getAsyncExecutor() {
        return taskExecutor();
    }

    // @Scheduled 전용 스레드풀 지정
    @Override
    public void configureTasks(ScheduledTaskRegistrar registrar) {
        registrar.setScheduler(taskScheduler());    // taskScheduler() 반환 타입을 Executor 로 변경
    }
}</code></pre>
<ul>
<li>각 인터페이스를 구현</li>
<li><code>getAsyncExecutor()</code> 에서 생성한 스레드풀 반환<ul>
<li><code>@Async</code> 메서드 전용 스레드풀로 사용</li>
</ul>
</li>
<li><code>configureTasks()</code> 에서 스케쥴러에 사용할 스레드풀 지정<ul>
<li><code>taskScheduler()</code> 반환 타입을 기존 <code>ThreadPoolTaskScheduler</code> -&gt; <code>Executor</code> 로 변경<ul>
<li><strong>사용될 스레드풀</strong>을 지정해주기 위함</li>
</ul>
</li>
<li><code>registrar.setScheduler</code> 에서 해당 스레드풀 사용하도록<br><br></li>
</ul>
</li>
</ul>
<pre><code>‼️ 처음 방식으로 충분히 구현할 수 있으므로, 추천하는 방식은 아님</code></pre><p><br><br></p>
<blockquote>
<p>💡 설정에 사용될 <strong>코어 수 제한</strong> 등의 값들은 <strong>설정 파일</strong>과 <code>@ConfigurationProperties</code> 조합을 이용해 설정해두는 것이 효율적일 수 있다.</p>
</blockquote>
<br>

<ul>
<li>ex)<pre><code class="language-yaml">custom:
thread-pool:
  async:
    core: #{T(java.lang.Runtime).getRuntime().availableProcessors()}
    max: #{T(java.lang.Runtime).getRuntime().availableProcessors() * 10}
    queue-capacity: 1000
  scheduler:
    pool: #{T(java.lang.Runtime).getRuntime().availableProcessors() * 2}</code></pre>
</li>
<li><code>SpEL</code> 식을 이용해서 <strong>동적으로 CPU 코어 수 환산</strong></li>
</ul>
<pre><code class="language-java">@Getter
@Setter
@Component
@ConfigurationProperties(prefix = &quot;custom.thread-pool&quot;)
public class ThreadPoolProperties {
    private Async async = new Async();
    private Scheduler scheduler = new Scheduler();

    @Getter
    @Setter
    public static class Async {
        private int core;
        private int max;
        private int queueCapacity;
    }

    @Getter
    @Setter
    public static class Scheduler {
        private int pool;
    }
}</code></pre>
<ul>
<li>설정 파일을 읽어 <strong>프로퍼티 빈으로 등록</strong></li>
<li>이후 <code>ThreadPoolConfig</code> 에서 해당 빈을 주입 받아서 사용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[@Modifying + 더티 체킹]]></title>
            <link>https://velog.io/@riverpower6_g/Modifying-%EB%8D%94%ED%8B%B0-%EC%B2%B4%ED%82%B9</link>
            <guid>https://velog.io/@riverpower6_g/Modifying-%EB%8D%94%ED%8B%B0-%EC%B2%B4%ED%82%B9</guid>
            <pubDate>Sat, 16 Aug 2025 06:47:45 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; <code>JPA</code> 에 대한 학습을 하던 중, <code>@Modifying</code> 어노테이션을 마주쳤다.</p>
<p>&nbsp; 기존에 <code>@Modifying</code> 어노테이션을 사용하여 <code>DB</code> 삭제 작업을 처리해본 경험이 있다. 생각해보면, 이 때 동작 방식이나 원리를 확실하게 이해하지 못하고</p>
<p>&#39;이걸 사용하면 <code>DB</code> 수정/삭제 쿼리를 날릴 수 있다&#39;</p>
<p>정도만 알고 사용했다.</p>
<p>&nbsp; <code>@Modifying</code> 에 대해 알게 되면서, 궁금한 게 생겼다.</p>
<blockquote>
<p>_ <strong>JPA 더티체킹</strong> 과 <code>@Modifying</code> 이 <strong>함께</strong> 사용되면 어떻게 처리될까?_</p>
</blockquote>
<p>&nbsp; <code>@Modifying</code> 에 관한 간단한 정리와 더불어, 해당 부분에 대해서도 추가로 작성해보고자 한다.</p>
<hr>
<h1 id="1️⃣-벌크-연산">1️⃣ 벌크 연산</h1>
<ul>
<li><strong>여러</strong> 데이터를 <strong>하나의 쿼리</strong>로 <strong>일괄 변경</strong>하는 것</li>
<li><code>update</code> , <code>delete</code> 가 이에 해당<br>

</li>
</ul>
<h2 id="❓-필요한-이유">❓ 필요한 이유</h2>
<ul>
<li><code>JPA</code> 는 <strong>더티 체킹</strong>에 의한 <strong>데이터 변경</strong>을 기본 전략으로 사용<ul>
<li>이러한 전략은 데이터의 변경이 없을 경우, <strong>불필요한 변경 쿼리를 발생시키지 않음</strong></li>
</ul>
</li>
<li>하지만 변경할 엔티티가 <strong>여러 개인 경우</strong>, 해당 엔티티에 대한 <strong>변경 쿼리</strong>를 <strong>각각 1번씩 실행</strong><ul>
<li>변경 데이터가 <strong>N개일 경우</strong> : 총 <code>N+1</code> 만큼의 쿼리가 실행 (SELECT 1번 + 변경 쿼리 N번)</li>
<li><strong>네트워크 전송량</strong>의 증가로 인한 <strong>성능 저하 발생 가능</strong></li>
</ul>
</li>
<li><strong>벌크 연산</strong>을 사용하면, <strong>단 1번의 추가 쿼리</strong>로 <strong>여러 데이터 변경 가능</strong><ul>
<li><strong>영속성 컨텍스트</strong>와 <code>DB</code> <strong>상태 동기화</strong> 작업 필요<br>

</li>
</ul>
</li>
</ul>
<hr>
<h1 id="2️⃣-modifying">2️⃣ <code>@Modifying</code></h1>
<ul>
<li><code>JPA</code> 에서 사용 가능한 <strong>벌크 연산 어노테이션</strong></li>
<li><code>@Query</code> 와 함께 사용함으로 <strong>벌크 연산 구현 가능</strong></li>
<li><strong>더티 체킹</strong>을 거치지 않고, <strong>바로</strong> <code>DB</code> 쿼리 요청<br>

</li>
</ul>
<h2 id="✍️-learn-with">✍️ Learn With</h2>
<h3 id="1️⃣-query-어노테이션">1️⃣ <code>@Query</code> 어노테이션</h3>
<ul>
<li><code>@Query</code> 어노테이션은 <code>JPA</code> 의 <strong>조회용</strong> <strong>JPQL 생성</strong> 어노테이션<ul>
<li><code>JPA</code> 의 <strong>데이터 변경 전략</strong>은 앞서 말했듯, <strong>더티 체킹</strong>에 의한 변경</li>
<li>따라서 <strong>직접적으로 사용되는</strong> 쿼리는 일반적으로 <strong>조회용 쿼리</strong></li>
</ul>
</li>
<li>단순히 <code>@Query</code> 에 <code>update</code> 나 <code>delete</code> 쿼리를 등록할 경우, <strong>쿼리 자체가 비실행</strong><ul>
<li><code>@Query</code> 는 <code>JPQL</code> 을 항상 <strong>생성</strong></li>
<li><code>@Modifying</code> 이 없다면, <code>JPQL</code> 을 <strong>조회 쿼리처럼 사용</strong>하려고 시도</li>
<li>오류 발생</li>
</ul>
</li>
<li><code>@Modifying</code> 어노테이션은 <code>@Query</code> 와 함께 사용함으로, 해당 <code>JPQL</code> 쿼리는 <strong>DB 변경 쿼리</strong>임을 표시</li>
<li><strong>반환 타입</strong><ul>
<li><code>void</code> : <strong>변경 쿼리만 실행</strong></li>
<li><code>int</code> : <strong>변경 쿼리 적용 개수</strong> 반환<br>

</li>
</ul>
</li>
</ul>
<h3 id="2️⃣-속성">2️⃣ 속성</h3>
<ul>
<li><code>clearAutomatically</code><ul>
<li><code>@Modifying(clearAutomatically = true)</code> 같이 사용</li>
<li>쿼리 실행 <strong>후</strong>, <strong>영속성 컨텍스트 초기화</strong> 자동 실행</li>
<li><code>== em.clear()</code></li>
</ul>
</li>
<li><code>flushAutomatically</code><ul>
<li><code>@Modifying(flushAutomatically = true)</code> 같이 사용</li>
<li>쿼리 실행 <strong>전</strong>, <strong>영속성 컨텍스트 동기화</strong> 자동 실행</li>
<li><code>== em.flush()</code><br>

</li>
</ul>
</li>
</ul>
<h3 id="3️⃣-hibernate-와-flush">3️⃣ Hibernate 와 Flush</h3>
<ul>
<li><code>Hibernate</code> 는 <code>em.flush()</code> 가 호출되면, <strong>쓰기 지연 버퍼</strong>에 저장된 쿼리를 <code>SQL</code> 로 변환 후 <code>DB</code> 로 요청</li>
<li><code>Hibernate</code> 에는 <strong>실행 전, 자동 플러시</strong>를 실행하는 몇 시점이 존재<br>

</li>
</ul>
<p><strong>1. 명시적 플러시 호출</strong></p>
<ul>
<li><code>em.flush()</code><br></li>
</ul>
<p><strong>2. 트랜잭션 커밋 시점</strong></p>
<ul>
<li><code>@Transactional</code> 내의 <strong>모든 로직 실행 후</strong>, <code>tx.commit()</code> 직전<br><br></li>
</ul>
<p><strong>3. JPQL 등 쿼리 실행 직전</strong></p>
<ul>
<li><code>DB</code> 쿼리 실행 시 <strong>영속성 컨텍스트</strong>와 <code>DB</code> 상태 <strong>정합성 유지</strong>를 위해<br><br></li>
</ul>
<p><strong>4. em.persist() 시점</strong></p>
<ul>
<li><code>GeneratedValue(strategy = GenerationType.IDENTITY)</code> 전략 한정)<ul>
<li><strong>나머지 전략</strong>은 마찬가지로 <strong>쓰기 지연 버퍼</strong>에 작업을 쌓고, 바로 <strong>Flush</strong>는 실행하지 않음</li>
</ul>
</li>
<li><strong>엔티티</strong>에는 <code>ID</code> 값 필드가 기본적으로 존재</li>
<li>해당 전략은 <code>INSERT</code> 후에야 <code>ID</code> 가 <code>DB</code> 에 의해 자동으로 생성되는 전략</li>
<li>따라서 <code>INSERT</code> 실행 후 <code>ID</code> 를 가져와야, <strong>엔티티 생성 시 초기화 가능</strong><br>

</li>
</ul>
<blockquote>
<p>💡 <code>Hibernate</code> 의 <code>Flush</code> 모드</p>
</blockquote>
<ol>
<li><code>FlushModeType.AUTO</code> :<ul>
<li><code>Hibernate</code> 기본값</li>
<li>쿼리 실행 <strong>전</strong> <code>flush</code></li>
</ul>
</li>
<li><code>FlushModeType.COMMIT</code> :<ul>
<li>트랜잭션 <strong>커밋 시에만</strong> <code>flush</code></li>
</ul>
</li>
<li><code>FlushModeType.MANUAL</code> :<ul>
<li>직접 <code>flush</code> <strong>호출해야만</strong> <code>flush</code><br>
</li>
</ul>
</li>
</ol>
<ul>
<li>사용 방법</li>
</ul>
<ol>
<li><p><code>EntityManager</code> 단위</p>
<pre><code class="language-java">EntityManager em;

em.setFlushMode(FlushModeType.COMMIT);    // COMMIT 모드로 변경</code></pre>
</li>
<li><p><strong>트랜잭션</strong> 단위</p>
<pre><code class="language-java">@Transactional(flushMode = FlushModeType.COMMIT)</code></pre>
</li>
<li><p><strong>전역 설정</strong></p>
<pre><code class="language-yaml">spring:
jpa:
  properties:
    jakarta.persistence.flushMode: COMMIT</code></pre>
<br>

</li>
</ol>
<blockquote>
<p>📌 기본 모드가 <code>AUTO</code> 이므로, <code>flushAutomatically</code> 속성을 사용하지 않아도 <strong>쿼리 실행 전</strong> <code>flush</code> 호출</p>
</blockquote>
<br>

<hr>
<h1 id="3️⃣-더티-체킹-➕-modifying">3️⃣ 더티 체킹 ➕ <code>@Modifying</code></h1>
<blockquote>
<p>🔗 &#39;사실상 이번 정리글 작성의 이유&#39;<br></p>
</blockquote>
<ul>
<li>그렇다면 <strong>더티 체킹</strong>과 <code>@Modifying</code> 을 함께 사용하면 어떻게 동작할까? 간단한 테스트 코드와 함께 확인해보자.</li>
</ul>
<br>

<ul>
<li><p><strong>최초 코드</strong></p>
<pre><code class="language-java">@SpringBootTest
@Transactional
public class TempTest {

  @Autowired
  EntityManager em;

  @Autowired
  Repo repo;

  @Test
  @Rollback(value = false)    // DB 확인을 위한 롤백 미실시
  void t1() {
      Member member = new Member();
      member.setName(&quot;A&quot;);
      repo.save(member);

      // == member
      Member findMember = repo.findById(1L).get();
      findMember.setName(&quot;B&quot;);

      repo.update(&quot;C&quot;, findMember.getId());
      em.flush();
      em.clear();
      System.out.println(&quot;===================&quot;);

      Member dbMember = repo.findById(1L).get();
      System.out.println(&quot;findMember.getName() = &quot; + findMember.getName());
      System.out.println(&quot;dbMember.getName() = &quot; + dbMember.getName());
  }
}
</code></pre>
</li>
</ul>
<p>interface Repo extends JpaRepository&lt;Member, Long&gt; {</p>
<pre><code>@Modifying
@Query(&quot;update Member m set m.name = :name where m.id = :id&quot;)
void update(@Param(&quot;name&quot;) String name, @Param(&quot;id&quot;) Long id);</code></pre><p>}</p>
<pre><code>- __벌크연산__을 수행하기 위한 `Repo`
  - `id` 에 해당하는 `Member` 의 __이름 변경__ 쿼리 실행
- 이름이 __&quot;A&quot;__ 인 `member` 등록
- `member` 의 이름을 __&quot;B&quot;__ 로 변경
  - `JPA` __더티체킹 대기__
- `repo.update()` 실행
  - `member` 의 이름을 __&quot;C&quot;__ 로 변경
- `em.flush()` 로 __영속성 컨텍스트 동기화(더티 체킹 업데이트 실행)__ 및 `em.clear()` 로 __영속성 컨텍스트 초기화__
- __1차 캐시__가 아닌 `DB` 에서 __최신화된 Member 조회__
- __영속성 컨텍스트 멤버__의 이름과 __최신화된 멤버__의 이름 출력
&lt;br&gt;

## 1️⃣ 최초 코드 실행
- __이름__만 변경하는 최초 코드 실행
- __실행 결과__
&lt;div style=&quot;text-align:center&quot;&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/riverpower6_g/post/78eb91f6-1e9b-4365-a49f-5a52346089ae/image.png&quot; width=&quot;1000&quot; height=&quot;1000&quot;/&gt;&lt;/div&gt;
- `INSERT` 1번 + `UPDATE` 2번 + `SELECT` 1번
  - `em.clear()` 로 인한 __1번의 추가__ `SELECT`
- `name=&#39;B&#39;` __업데이트__ 실행 후 `name=&#39;C&#39;` __업데이트__ 실행
  - __더티 체킹 업데이트__ 먼저 실행
  - __벌크 연산__ 추가 실행
- `dbMember.getName()` 결과 : __&quot;C&quot;__
&lt;br&gt;

### 📌 결과
&lt;div style=&quot;text-align:center&quot;&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/riverpower6_g/post/1f727df3-adac-48a7-a24b-8acf327e373f/image.png&quot; width=&quot;1000&quot; height=&quot;1000&quot;/&gt;&lt;/div&gt;

- `NAME` 컬럼이 __&quot;C&quot;__ 로 잘 반영
&lt;br&gt;

## 2️⃣ 벌크 연산 이전, 다른 필드 변경 코드 실행
- `setTempCnt` 로 __이름이 아닌__ 필드 변경
- `repo.update()` 호출 이전에 `findMember.setTempCnt(100)` 실행
```java
// ...

Member member = new Member();
member.setName(&quot;A&quot;);
// setTempCnt 필드 추가
member.setTempCnt(0);
repo.save(member);

Member findMember = repo.findById(1L).get();
findMember.setName(&quot;B&quot;);
// update 호출 전, setTempCnt 필드 변경
findMember.setTempCnt(100);

repo.update(&quot;C&quot;, findMember.getId());
em.flush();
em.clear();

// ...</code></pre><ul>
<li><strong>실행 결과</strong><div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/62ada3a1-2a71-4f9f-bfe5-49bc8cfa6265/image.png" width="1000" height="1000"/></div></li>
<li><code>INSERT</code> 1번 + <code>UPDATE</code> 2번 + <code>SELECT</code> 1번<ul>
<li><code>em.clear()</code> 로 인한 <strong>1번의 추가</strong> <code>SELECT</code></li>
</ul>
</li>
<li><strong>1번</strong> 실행 결과와 마찬가지로, <strong>더티 체킹 업데이트 이후 벌크 연산 수행</strong></li>
<li><code>dbMember.getName()</code> 결과 : <strong>&quot;C&quot;</strong><br>

</li>
</ul>
<h3 id="📌-결과">📌 결과</h3>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/9337b5f5-fd7a-4ffb-a7eb-3d39b57b811a/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>NAME</code> 컬럼은 <strong>&quot;C&quot;</strong> , <code>TEMP_CNT</code> 컬럼은 <strong>100</strong>이 잘 반영<br>
## 3️⃣ 벌크 연산 이후, 다른 필드 변경 코드 실행</li>
<li><code>repo.update()</code> 호출 이후에 <code>findMember.setTempCnt(100)</code> 실행<pre><code class="language-java">// ...
</code></pre>
</li>
</ul>
<p>Member findMember = repo.findById(1L).get();
findMember.setName(&quot;B&quot;);</p>
<p>repo.update(&quot;C&quot;, findMember.getId());
// update 호출 후, setTempCnt 필드 변경
findMember.setTempCnt(100);
em.flush();
em.clear();</p>
<p>// ...</p>
<p>```</p>
<ul>
<li><strong>실행 결과</strong><div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/a6dfe4de-da85-4708-9640-980b96ac7c58/image.png" width="1000" height="1000"/></div></li>
<li><code>INSERT</code> 1번 + <code>UPDATE</code> 3번 + <code>SELECT</code> 1번<ul>
<li><code>em.clear()</code> 로 인한 <strong>1번의 추가</strong> <code>SELECT</code></li>
</ul>
</li>
<li><code>name=&#39;B&#39;</code> <strong>업데이트</strong> 실행 후 <code>name=&#39;C&#39;</code> <strong>업데이트</strong> 실행<ul>
<li>최초 <strong>더티 체킹 업데이트</strong> 실행</li>
<li><strong>벌크 연산</strong> 추가 실행</li>
</ul>
</li>
<li><code>name=&#39;B&#39;</code> 와 <code>temp_cnt=100</code> <strong>업데이트</strong>가 추가 실행<ul>
<li><code>tempCnt</code> 로 인한 <strong>더티 체킹 업데이트 추가 실행</strong></li>
</ul>
</li>
<li><code>dbMember.getName()</code> 결과 : <strong>&quot;B&quot;</strong></li>
</ul>
<br>

<h3 id="📌-결과-1">📌 결과</h3>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/ddbf89f4-3c8f-4c3b-98f4-4f370c0aafd4/image.png" width="1000" height="1000"/></div>

<ul>
<li><code>TEMP_CNT</code> 컬럼은 <strong>100</strong>이 잘 반영</li>
<li><code>NAME</code> 컬럼은 <strong>&quot;C&quot;</strong> 가 아닌, <strong>&quot;B&quot;</strong>가 최종 반영
<br><br></li>
</ul>
<h2 id="️-dbmembergetname-이-c가-아닌-b로-변경됨">‼️ <code>dbMember.getName()</code> 이 <code>C</code>가 아닌 <code>B</code>로 변경됨</h2>
<ul>
<li><code>@Modifying</code> 쿼리 실행 시점에 <code>NAME</code> 컬럼을 <strong>&quot;C&quot;</strong>로 업데이트</li>
<li><code>findMember.setTempCnt(100)</code> 으로 <strong>기존 영속성 컨텍스트에 존재하는 엔티티</strong>의 <code>tempCnt</code> 속성 변경<ul>
<li>해당 엔티티는 <strong>더티 체킹에 의한 업데이트 대상</strong>으로 변경</li>
<li>이 때 <strong>영속성 컨텍스트</strong>의 <code>name</code> 은 여전히 <strong>&quot;B&quot;</strong></li>
<li>아직 <code>em.clear()</code> 를 하지 않아서 <strong>기존 영속성 컨텍스트</strong>에 엔티티가 <strong>남아있기 때문</strong></li>
</ul>
</li>
<li>원했던 변경된 이름 : <strong>&quot;C&quot;</strong></li>
<li>최종 변경된 이름 : <strong>&quot;B&quot;</strong></li>
</ul>
<blockquote>
<p>_ <strong>영속성 컨텍스트</strong>가 제 때 <strong>초기화되지 않으면</strong> 원하지 않는 결과를 초래할 수 있다!_</p>
</blockquote>
<br>

<hr>
<h1 id="🔨-결론">🔨 결론</h1>
<ul>
<li><code>@Modifying</code> 과 <strong>영속성 컨텍스트 변경</strong> 작업을 <strong>같은 트랜잭션</strong> 내에서 진행하면 <strong>원하지 않은 결과를 초래할 수 있음</strong></li>
<li><strong>추가 비즈니스 로직 작성</strong> 시, <code>clearAutomatically = true</code> 속성을 <strong>반드시 추가</strong>해서 사용</li>
<li>혹시 모를 <strong>Side Effect</strong>가 불안하다면, 애초에 <strong>트랜잭션을 분리</strong>해서 작업을 진행하도록 하자.</li>
</ul>
<blockquote>
<p><em>벌크 연산을 사용하려면 반드시 <strong>영속성 컨텍스트</strong>와 <strong>DB 상태</strong>를 잘 고려하여 사용하자!</em></p>
</blockquote>
<br>

<hr>
<blockquote>
<p>검수)</p>
</blockquote>
<ul>
<li>Google Gemini ( <a href="https://gemini.google.com/app">https://gemini.google.com/app</a> )</li>
<li>ChatGPT ( <a href="https://chatgpt.com/">https://chatgpt.com/</a> )</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[@OnDelete 와 cascade 속성]]></title>
            <link>https://velog.io/@riverpower6_g/OnDelete-%EC%99%80-cascade-%EC%86%8D%EC%84%B1</link>
            <guid>https://velog.io/@riverpower6_g/OnDelete-%EC%99%80-cascade-%EC%86%8D%EC%84%B1</guid>
            <pubDate>Tue, 12 Aug 2025 04:38:44 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; <code>JPA</code> 에 대한 학습을 진행하던 중, <code>@OneToMany</code> 의 <strong>cascade</strong> 속성에 대한 복습을 진행 중이었다.</p>
<p>&nbsp; 보통 <code>@OneToMany</code> 에서 주로 사용되는 속성 연계는 <code>@OneToMany(mappedBy = &quot;fieldName&quot;, cascade = CascadeType.??, orphanRemoval = true)</code> 이다.
데이터 삭제 시 이후에 객체의 양방향 연관관계를 끊어주면, <code>DB</code> 에서 부모와 자식들의 삭제를 <strong>외래키 제약 조건</strong> 위반 없이 제대로 제거할 수 있다.</p>
<p>&nbsp; 근데 여기서 궁금증이 하나 생겼다. 예를들어 <strong>Member</strong>와 <strong>Team</strong> 엔티티가 있을 경우, <strong>Team</strong>을 삭제한다고 <strong>Member</strong>도 삭제해버릴 수는 없을 것이다. <strong>Member</strong>의 <strong>Team</strong> 참조를 <code>NULL</code> 로 변경하던가 하는 식으로 진행해야 한다.
이러한 경우는 <code>cascade</code> 속성으로 처리할 수 없다. <code>cascade</code> 는 사실상 <strong>부모</strong>에 따른 <strong>자식의</strong> <code>CRUD</code> 처리일 뿐, <strong>자식</strong>의 <code>FK</code> 변경에 대한 관여는 없다. 즉, <strong>자식의 생명주기가 부모에 종속될 때</strong>만 사용해야 하는 속성이다.</p>
<p>&nbsp; 그렇다면 어떻게 해야 할까? 가장 간단한 방법은 <code>@OneToMany</code> 에서 별다른 속성 사용 없이 <strong>비즈니스 로직</strong>에서 <code>member.setTeam(null)</code> 과 <strong>Team</strong> 삭제 로직을 혼합해서 사용하는 것이다.</p>
<p>&nbsp; 그런데 방법을 고민하며 찾아보던 중, <code>Hibernate</code> 에 흥미로운 기능이 있었다. <code>@OnDelete</code> 라는 어노테이션이었다. 이 어노테이션을 사용하면 <strong>외래키 제약 조건 기능</strong>을 활용하며, <strong>부모, 자식 삭제 관련</strong> 여러 가지 작업을 아주 편리하게 할 수 있었다.</p>
<p>&nbsp; 처음보는 신기한 기능이기에 공부 내용을 잘 정리해서 남겨놓기로 결정했다.</p>
<hr>
<h1 id="1️⃣-ondelete">1️⃣ <code>@OnDelete</code></h1>
<ul>
<li><code>Hibernate</code> 에서 제공하는 어노테이션으로, 엔티티 삭제 시 <strong>연관된 엔티티</strong>에 어떤 동작을 수행할지 <code>DB</code> <strong>레벨에서 정의</strong><ul>
<li><code>DDL</code> 에서 직접 정의하거나, <code>ddl-auto</code> 옵션을 사용하는 경우 <code>Hibernate</code> 가 자동 설정하며, 이 경우 <strong>1번만 실행</strong></li>
<li><code>JPA</code> 표준이 아닌 <code>Hibernate</code> 구현체의 기술</li>
</ul>
</li>
<li><strong>외래키 제약 조건</strong>을 위반하지 않으며, <code>DB</code> 의 <strong>참조 무결성</strong>을 반드시 유지</li>
<li><code>@ManyToOne</code> 어노테이션과 함께 사용하여, 연관 필드를 <code>FK</code> 를 가진 <strong>주인</strong> 쪽에서 어떻게 처리할지 세팅 가능</li>
<li><code>DB</code> 에서 반영하므로, <strong>영속성 컨텍스트</strong>에는 바로 반영되지 않음<ul>
<li><strong>영속성 컨텍스트</strong>와 <code>DB</code> 상태 <strong>불일치 가능</strong><br>

</li>
</ul>
</li>
</ul>
<h2 id="➕-옵션">➕ 옵션</h2>
<h3 id="1-ondeleteactionno_action-ondeleteactionrestrict">1. OnDeleteAction.NO_ACTION (OnDeleteAction.RESTRICT)</h3>
<ul>
<li><strong>부모</strong> 엔티티 삭제 시, <strong>연관된 자식 엔티티가 존재</strong>하면 <strong>삭제 불가</strong></li>
<li><code>@OnDelete</code> 어노테이션의 <strong>기본값</strong></li>
<li>두 옵션이 완전히 동일한 옵션은 아니지만, <strong>대부분</strong> <code>DB</code> <strong>에서 동일하게 동작</strong><ul>
<li><code>DB</code> 별 미묘한 차이는 존재</li>
</ul>
</li>
<li><strong>DDL Constraint</strong> : <code>ON DELETE NO ACTION</code> 또는 <code>ON DELETE RESTRICT</code><br>

</li>
</ul>
<h3 id="2-ondeleteactioncascade">2. OnDeleteAction.CASCADE</h3>
<ul>
<li><strong>부모</strong> 엔티티 삭제 시, <strong>자동으로 연관된 자식 엔티티 함께 삭제</strong></li>
<li><code>@OneToMany(cascade = CascadeType.REMOVE)</code> 를 지정해주지 않아도, <code>DB</code> 레벨에서 <strong>외래키 제약 조건</strong> 위반 없이 <strong>부모, 자식을 모두 삭제</strong><ul>
<li><code>cascade</code> 속성과는 <strong>별개로 동작</strong></li>
<li><code>CascadeType.REMOVE</code> : <code>JPA</code> 가 <code>DELETE</code> 쿼리를 실행하며, 쓰기 지연 버퍼에는 <strong>자식 -&gt; 부모</strong> 순으로 쿼리 등록</li>
</ul>
</li>
<li><strong>DDL Constraint</strong> : <code>ON DELETE CASCADE</code><br>

</li>
</ul>
<h3 id="3-ondeleteactionset_null">3. OnDeleteAction.SET_NULL</h3>
<ul>
<li><strong>부모</strong> 엔티티 삭제 시, <strong>연관된 자식 엔티티</strong>의 <code>FK</code> 값을 <code>NULL</code> <strong>로 설정</strong></li>
<li><strong>반드시</strong> 해당 컬럼의 <code>nullable</code> 제약 조건이 <code>true</code> 여야 사용 가능</li>
<li><strong>DDL Constraint</strong> : <code>ON DELETE SET NULL</code><br>

</li>
</ul>
<h3 id="4-ondeleteactiondefault">4. OnDeleteAction.DEFAULT</h3>
<ul>
<li><strong>부모</strong> 엔티티 삭제 시, <strong>연관된 자식 엔티티</strong>의 <code>FK</code> 값을 <strong>기본값으로 설정</strong></li>
<li><strong>반드시</strong> 해당 컬럼의 <strong>기본값이 미리 정의</strong>되어 있어야 가능</li>
<li><code>MySQL</code> 에서는 해당 옵션 <strong>사용 불가능</strong></li>
<li><strong>DDL Constraint</strong> : <code>ON DELETE SET DEFAULT</code><br>

</li>
</ul>
<hr>
<h1 id="2️⃣-cascade-와의-쿼리-실행-횟수-비교">2️⃣ cascade 와의 쿼리 실행 횟수 비교</h1>
<ul>
<li><code>@OnDelete</code> 기능은 <code>DB</code> 에서 <strong>직접</strong> 데이터 삭제를 조작</li>
<li><code>cascade</code> 속성 사용과 비교했을 때, <strong>쿼리 횟수 압도적 감소</strong><ul>
<li><strong>네트워크 통신</strong> 비용의 대량 감소</li>
<li><strong>성능 압도적 향상</strong>
<br><br></li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>⚒️ 실습을 통해 알아보자</strong></p>
</blockquote>
<br>

<h2 id="📌-기본-엔티티">📌 기본 엔티티</h2>
<pre><code class="language-java">@Entity(name = &quot;newMember&quot;)
public class Member {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    private Team team;

    public void setTeam(Team team) {
        this.team = team;
    }
}

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = &quot;team&quot;)
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();
}
</code></pre>
<ul>
<li><code>Member</code> 와 <code>Team</code> 은 <strong>N:1(다대일)</strong> 관계</li>
<li><strong>양방향</strong> 참조 가능<br>

</li>
</ul>
<h2 id="📌-실행-메서드">📌 실행 메서드</h2>
<pre><code class="language-java">public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory(&quot;hello&quot;);
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();

    tx.begin();
    try {
        Team team = new Team();
        em.persist(team);
        Member member = new Member();
        member.setTeam(team);
        Member member2 = new Member();
        member2.setTeam(team);
        em.persist(member);
        em.persist(member2);

        tx.commit();
    } catch (Exception e) {
        tx.rollback();
    } finally {
        em.close();
    }

    em = emf.createEntityManager();
    tx = em.getTransaction();
    tx.begin();
    try {
        Team team = em.find(Team.class, 1L);
        em.remove(team);

        tx.commit();
    } catch (Exception e) {
        tx.rollback();
        e.printStackTrace();
    } finally {
        em.close();
    }
    emf.close();
}</code></pre>
<ul>
<li><strong>1번째</strong> 트랜잭션<ul>
<li><code>Team</code> 을 <strong>1개 생성</strong> 후 <code>DB</code> 반영</li>
<li><code>Member</code> 를 <strong>2개 생성</strong> 후 <code>team</code> 과 매핑 후 <code>DB</code> 반영</li>
</ul>
</li>
<li><strong>2번째</strong> 트랜잭션<ul>
<li><strong>1번</strong> <code>Team</code> 데이터 조회</li>
<li><strong>1번</strong> <code>Team</code> 삭제<br>

</li>
</ul>
</li>
</ul>
<h2 id="최초-실행-결과">최초 실행 결과</h2>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/86fffcf8-02e4-4018-9a19-711ae705731d/image.png" width="1000" height="1000"/></div>

<ul>
<li><strong>1번</strong>의 <code>SELECT</code> 와 <strong>1번</strong>의 <code>DELETE</code></li>
<li><code>Team</code> 삭제 중 예외 발생<ul>
<li><code>Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation: &quot;FKA84Y4NUL9OTEXFOY22AFBG081: PUBLIC.NEWMEMBER FOREIGN KEY(TEAM_ID) REFERENCES PUBLIC.TEAM(ID) (CAST(1 AS BIGINT))&quot;; SQL statement:</code></li>
<li>즉, <strong>외래키 제약 조건 위반</strong>으로 인한 데이터 삭제 불가능<br></li>
</ul>
</li>
<li><strong>데이터베이스 데이터 확인</strong><div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/668d0a5e-27a6-4e20-9a75-85150440f045/image.png" width="1000" height="1000"/></div></li>
<li><strong>2개의</strong> <code>Member</code> 와 <strong>1개의</strong> <code>Team</code> 모두 삭제되지 않음<br>

</li>
</ul>
<h2 id="🥇-부모-삭제-시-자식-삭제-제약-조건">🥇 부모 삭제 시 자식 삭제 제약 조건</h2>
<br>

<h3 id="1️⃣-cascade-속성만-사용">1️⃣ cascade 속성만 사용</h3>
<pre><code class="language-java">@Entity
public class Team {
    ...

    // cascade 속성 적용
    @OneToMany(mappedBy = &quot;team&quot;, cascade = CascadeType.REMOVE)
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();
}</code></pre>
<br>

<ul>
<li><p><strong>메서드 실행 결과</strong></p>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/6058d031-f106-49f4-96d6-9c57e189c803/image.png" width="1000" height="1000"/></div>
</li>
<li><p><strong>2번</strong>의 <code>SELECT</code> 와 <strong>3번</strong>의 <code>DELETE</code></p>
<ul>
<li><code>Team</code> 에 해당된 <code>Member</code> 를 찾기 위한 <strong>1번의 추가</strong> <code>SELECT</code> 발생</li>
<li><strong>2명</strong>의 연관된 <code>Member</code> 를 삭제하는 <code>DELETE</code> 쿼리 추가 발생</li>
<li>이 때, <code>Member</code> <strong>(자식)</strong> -&gt; <code>Team</code> <strong>(부모) 순서</strong>로 <code>DELETE</code> 쿼리 발생</li>
</ul>
</li>
<li><p><code>JPA</code> 는 총 <strong>5번의 쿼리 요청</strong></p>
<br></li>
<li><p><strong>데이터베이스 데이터 확인</strong></p>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/884a1532-c46a-4799-9037-297e5d92a112/image.png" width="1000" height="1000"/></div></li>
<li><p>문제 없이 모두 정상 삭제</p>
<br>

</li>
</ul>
<h3 id="2️⃣-ondeleteaction--ondeleteactioncascade-만-사용">2️⃣ <code>@OnDelete(action = OnDeleteAction.CASCADE)</code> 만 사용</h3>
<pre><code class="language-java">@Entity(name = &quot;newMember&quot;)
public class Member {
    ...

    // @OnDelete 와 CASCADE 옵션 사용
    @ManyToOne
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Team team;

    ...
}</code></pre>
<br>

<ul>
<li><p><strong>메서드 실행 결과</strong></p>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/8487bdd5-3de8-4c1c-b034-41b43c4f6444/image.png" width="1000" height="1000"/></div>
</li>
<li><p><strong>1번의</strong> <code>SELECT</code> 와 <strong>1번의</strong> <code>DELETE</code></p>
<ul>
<li>부모인 <code>Team</code> 객체만 조회</li>
<li><code>JPA</code> 는 이 <code>Team</code> 의 <code>DELETE</code> 쿼리 <strong>1개만</strong> <code>DB</code> 로 요청</li>
</ul>
</li>
<li><p><code>JPA</code> 는 총 <strong>2번의 쿼리 요청</strong></p>
<br>
</li>
<li><p><strong>데이터베이스 데이터 확인</strong></p>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/9ad50254-488d-4758-932d-c1b71e97a883/image.png" width="1000" height="1000"/></div>
</li>
<li><p>문제 없이 모두 정상 삭제</p>
<br>

</li>
</ul>
<h2 id="🥈-부모-삭제-시-자식-fk를-null로-변경">🥈 부모 삭제 시 자식 FK를 NULL로 변경</h2>
<br>

<h3 id="1️⃣-비즈니스-로직에서-null-지정">1️⃣ 비즈니스 로직에서 <code>NULL</code> 지정</h3>
<pre><code class="language-java">@Entity
public class Team {
    ...

    // mappedBy 를 제외한 속성 제거
    @OneToMany(mappedBy = &quot;team&quot;)
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();

    // Getter 추가
    public List&lt;Member&gt; getMembers() {
        return this.members;
    }
}</code></pre>
<pre><code class="language-java">...

Team team = em.find(Team.class, 1L);

// Member의 Team을 NULL로 변경
for (Member member : team.getMembers()) {
    member.setTeam(null);
}
em.remove(team);

tx.commit();

...</code></pre>
<ul>
<li><p><code>main()</code> 메서드의 <strong>2번째 트랜잭션</strong>에서 각 <code>Member</code> 의 <code>Team</code> 을 <code>NULL</code> 로 변경</p>
<br>
</li>
<li><p><strong>메서드 실행 결과</strong></p>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/2f796357-64a0-452b-bde7-43261408f6fb/image.png" width="1000" height="1000"/></div>
</li>
<li><p><strong>2번</strong>의 <code>SELECT</code> , <strong>2번</strong>의 <code>UPDATE</code> , <strong>1번</strong>의 <code>DELETE</code></p>
<ul>
<li><code>Team</code> 의 <code>Member</code> 조회를 위한 <strong>1번의</strong> 추가 <code>SELECT</code> 발생</li>
<li><code>Member</code> 의 <strong>FK</strong>를 <code>NULL</code> 로 지정하기 위한 <strong>2번의</strong> 추가 <code>UPDATE</code> 발생</li>
</ul>
</li>
<li><p><code>JPA</code> 는 총 <strong>5번의 쿼리 요청</strong></p>
<br>
</li>
<li><p><strong>데이터베이스 데이터 확인</strong></p>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/d51c0b6b-a261-46f0-90ce-aa88d0c6427a/image.png" width="1000" height="1000"/></div>


</li>
</ul>
<ul>
<li><code>Team</code> 삭제 성공</li>
<li><code>Member</code> 의 <strong>FK</strong>를 <code>NULL</code> 로 변경 성공<br>

</li>
</ul>
<h3 id="2️⃣-ondeleteaction--ondeleteactionset_null--사용">2️⃣ <code>@OnDelete(action = OnDeleteAction.SET_NULL)</code>  사용</h3>
<pre><code class="language-java">@Entity(name = &quot;newMember&quot;)
public class Member {
    ...

    // @OnDelete 와 SET_NULL 옵션 사용
    @ManyToOne
    @OnDelete(action = OnDeleteAction.SET_NULL)
    private Team team;

    ...
}</code></pre>
<pre><code class="language-java">Team team = em.find(Team.class, 1L);
// setNull 제거
em.remove(team);

tx.commit();</code></pre>
<ul>
<li><p>최초 메인 메서드로 롤백</p>
<br>
</li>
<li><p><strong>메서드 실행 결과</strong></p>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/5d42526d-0504-4c15-8921-ef0baae714ff/image.png" width="1000" height="1000"/></div>
</li>
<li><p><strong>1번</strong>의 <code>SELECT</code> 와 <strong>1번</strong>의 <code>DELETE</code></p>
<ul>
<li>부모인 <code>Team</code> 객체만 조회</li>
<li><code>JPA</code> 는 이 <code>Team</code> 의 <code>DELETE</code> 쿼리 <strong>1개만</strong> <code>DB</code> 로 요청</li>
</ul>
</li>
<li><p><code>JPA</code> 는 총 <strong>2번의 쿼리 요청</strong></p>
<br>
</li>
<li><p><strong>데이터베이스 데이터 확인</strong></p>
<div style="text-align:center">
<img src="https://velog.velcdn.com/images/riverpower6_g/post/7f81608b-370e-4f43-aa6d-76a566e56fcc/image.png" width="1000" height="1000"/></div>


</li>
</ul>
<ul>
<li><code>Team</code> 삭제 성공</li>
<li><code>Member</code> 의 <strong>FK</strong>를 <code>NULL</code> 로 변경 성공<br>

</li>
</ul>
<blockquote>
<h3 id="💡-ondelete-와-cascade-속성을-함께-사용한다면">💡 <code>@OnDelete</code> 와 <code>cascade</code> 속성을 함께 사용한다면?</h3>
</blockquote>
<ul>
<li><code>@OneToMany(cascade = CascadeType.REMOVE)</code> 사용했을 때 만큼의 쿼리 실행<ul>
<li><code>@OnDelete</code> 의 이점인 <strong>쿼리 최소화</strong>를 전혀 살릴 수 없음<br>

</li>
</ul>
</li>
</ul>
<h2 id="📋-정리">📋 정리</h2>
<table>
<thead>
<tr>
<th align="left">삭제 방식</th>
<th align="left">CascadeType.REMOVE</th>
<th align="left">@OnDelete</th>
</tr>
</thead>
<tbody><tr>
<td align="left">삭제 주체</td>
<td align="left"><strong>JPA</strong></td>
<td align="left"><strong>DB</strong></td>
</tr>
<tr>
<td align="left">쿼리 발생</td>
<td align="left">__ SELECT (부모) + SELECT (자식) + DELETE (자식 수) + DELETE (부모)__</td>
<td align="left"><strong>SELECT (부모) + DELETE (부모)</strong></td>
</tr>
<tr>
<td align="left">쿼리 발생 횟수</td>
<td align="left">실습에서 총 <strong>5회</strong></td>
<td align="left">실습에서 총 <strong>2회</strong></td>
</tr>
<tr>
<td align="left">성능</td>
<td align="left"><strong>JPA</strong>가 추가적인 쿼리를 요청하여 <strong>네트워크 통신 비용 추가 발생</strong></td>
<td align="left">DB 내부에서 <strong>한 번의 쿼리로 처리</strong></td>
</tr>
<tr>
<td align="left">스펙</td>
<td align="left"><strong>JPA 표준 기능</strong></td>
<td align="left"><strong>Hibernate 확장 기능</strong></td>
</tr>
<tr>
<td align="left">- 실습 예제에서는 <code>em.find()</code> 사용으로 <strong>부모</strong> <code>SELECT</code> <strong>1건</strong> 추가 발생</td>
<td align="left"></td>
<td align="left"></td>
</tr>
<tr>
<td align="left">- <code>em.getReferene()</code> 로 <strong>프록시</strong>를 사용하면 <strong>삭제할</strong> <code>FK ID</code> 를 알고 있으므로, <strong>부모 조회 쿼리 절약 가능</strong></td>
<td align="left"></td>
<td align="left"></td>
</tr>
<tr>
<td align="left">- 이 경우, 예제의 쿼리 횟수는 <strong>4회</strong> <code>VS</code> <strong>1회</strong></td>
<td align="left"></td>
<td align="left"></td>
</tr>
</tbody></table>
<hr>
<h1 id="3️⃣-ondelete-🆚-cascade">3️⃣ <code>@OnDelete</code> 🆚 <code>Cascade</code></h1>
<h2 id="❗-ondelete-의-문제점">❗ <code>@OnDelete</code> 의 문제점</h2>
<p><strong>1. 비즈니스 로직 추가 수행의 어려움</strong></p>
<ul>
<li><code>@OnDelete</code> 는 모든 삭제 로직을 <strong>DB 내부에서 수행하도록 위임</strong><ul>
<li><strong>비즈니스 로직</strong> 내에서 별도의 <strong>연관관계 제거 메서드</strong>를 호출하지 않고도, 단순히 <strong>레포지터리</strong>의 메서드 <code>delete</code> 호출 만으로 간편하게 연관된 모든 데이터 삭제 가능</li>
<li>단 <strong>트랜잭션 종료</strong> 직전에야 <strong>삭제 쿼리 실행이 가능</strong>하므로, <strong>트랜잭션 내에서는</strong> 삭제가 <strong>즉시 반영되지는 않음</strong></li>
</ul>
</li>
<li><code>@OneToMany</code> + <code>cascade</code> 속성 방식은 <strong>연관관계 제거 메서드</strong>가 필수<ul>
<li>이를 통해 <strong>영속성 컨텍스트</strong> 와 <strong>최종</strong> <code>DB</code> 의 <strong>일관성 유지 가능</strong></li>
</ul>
</li>
<li><code>@OnDelete</code> 방식은 <strong>영속성 컨텍스트</strong>가 <strong>제거 상태</strong>를 반영하지 않으므로, 이후 작업에서 <code>DB</code> 와의 일관성에서 <strong>문제 발생 가능</strong></li>
</ul>
<p><strong>2. JPA 표준이 아님</strong></p>
<ul>
<li><code>@OnDelete</code> 는 <code>Hibernate</code> 구현체의 <strong>확장 기능</strong></li>
<li>만약 <code>JPA</code> 구현체를 변경하게 되면, <strong>해당 코드 변경 필요</strong></li>
<li>단, 일반적으로 <code>JPA</code> 를 사용한 <strong>스프링</strong> 개발 시 <code>Hibernate</code> 를 사용하기는 함<br>

</li>
</ul>
<h2 id="💡-비즈니스-로직-추가-수행-방법">💡 비즈니스 로직 추가 수행 방법</h2>
<ul>
<li><code>@OnDelete</code> + <code>EntityManager.flush()</code> 사용<ul>
<li><strong>레포지터리</strong>의 <code>delete</code> 메서드 호출 후, <strong>영속성 컨텍스트</strong>와 <code>DB</code> 의 <strong>일관성 유지</strong>를 위한 <code>em.flush()</code> 호출</li>
<li>이후부터는 <strong>영속성 컨텍스트</strong>와 <code>DB</code> 의 <strong>일관성이 유지</strong></li>
</ul>
</li>
<li>단, 매 삭제 메서드 호출 후에 <strong>반드시</strong> <code>flush()</code> 호출이 필요하다는 단점 존재<ul>
<li><strong>코드의 복잡성</strong> 증가<br>

</li>
</ul>
</li>
</ul>
<h2 id="🔗-ondelete-🆚-cascade">🔗 <code>@OnDelete</code> 🆚 <code>Cascade</code></h2>
<h3 id="1-성능">1. 성능</h3>
<p><strong>1️⃣ @OnDelete</strong></p>
<ul>
<li><strong>애플리케이션</strong> 레벨에서 <strong>추가 로직</strong> 작업 필요 없음<ul>
<li><strong>빠르고 효율적</strong></li>
</ul>
</li>
<li><code>JPA</code> 가 <strong>부모 삭제 쿼리</strong>만 호출<ul>
<li><strong>네트워크</strong> 통신 비용 <strong>절약</strong></li>
</ul>
</li>
</ul>
<p><strong>2️⃣ Cascade</strong></p>
<ul>
<li><strong>애플리케이션</strong> 레벨에서 <strong>추가 로직</strong> 필요<ul>
<li><strong>양방향 연관관계 제거</strong> 메서드 필요</li>
</ul>
</li>
<li><strong>모든</strong> 자식 삭제 쿼리를 <strong>JDBC API</strong>로 요청<ul>
<li><strong>네트워크</strong> 통신 비용 <strong>증가</strong></li>
</ul>
</li>
</ul>
<blockquote>
<p>🎉 <code>OnDelete()</code> 승리</p>
</blockquote>
<ul>
<li>단, <strong>자식 객체의 수</strong>에 따른 <code>DB</code> 부하 고려</li>
<li>네트워크 통신 비용 절약 <strong>VS</strong> <code>DB</code> 부하 감소</li>
<li><strong>자식 객체</strong>가 엄청나게 많은 게 아니라면, <code>OnDelete()</code> 가 더 <strong>좋은 성능 발휘</strong><br>

</li>
</ul>
<h3 id="2-유연성-후속-작업-개별-작업">2. 유연성 (후속 작업, 개별 작업)</h3>
<p><strong>1️⃣ @OnDelete</strong></p>
<ul>
<li>삭제되는 <strong>자식 객체의 개별 작업 불가능</strong><ul>
<li><code>JPA</code> 는 <strong>부모 삭제 쿼리</strong> 만 <strong>JDBC API</strong>에 전달</li>
<li><code>@OnDelete</code> 는 <code>DB</code> 에서 <strong>자식을 한 번에 삭제</strong></li>
<li>삭제되는 <strong>개별 자식 객체</strong> 정보는 <strong>획득 불가능</strong></li>
<li><strong>삭제되는 자식 객체 대상</strong> 추가적인 작업 <strong>불가능</strong></li>
</ul>
</li>
</ul>
<p><strong>2️⃣ Cascade</strong></p>
<ul>
<li>삭제되는 <strong>자식 객체의 개별 작업 가능</strong><ul>
<li><code>JPA</code> 는 <strong>자식 삭제 쿼리</strong>를 모두 전달</li>
<li><code>try-catch</code> 나 <strong>반복문</strong> 등을 활용하여, <strong>삭제되는 객체 대상</strong> 추가적인 작업 <strong>가능</strong></li>
</ul>
</li>
</ul>
<blockquote>
<p>🎉 <code>@OneToMany(cascade)</code> 승리</p>
</blockquote>
<ul>
<li><code>@Transactional</code> 과 함께 사용하면, 양쪽 다 <strong>롤백</strong> 관련 문제는 발생하지 않음</li>
</ul>
<br>

<h3 id="3-유지보수성">3. 유지보수성</h3>
<p><strong>1️⃣ @OnDelete</strong></p>
<ul>
<li><strong>레포지터리</strong>의 <code>delete</code> 호출마다 <code>em.flush()</code> 필수<ul>
<li><strong>영속성 컨텍스트</strong>와 <strong>최종 DB</strong>의 일관성 유지를 위해 반드시 필요</li>
</ul>
</li>
<li>매번 <code>em.flush()</code> 호출이 반드시 필요하므로, 코드의 복잡성 및 실수 확률 증가</li>
</ul>
<p><strong>2️⃣ Cascade</strong></p>
<ul>
<li><strong>양방향 연관관계 제거 메서드</strong> 필수<ul>
<li><strong>영속성 컨텍스트</strong>와 <strong>최종 DB</strong>의 일관성 유지</li>
</ul>
</li>
<li>매번 <strong>연관관계 제거 메서드</strong> 호출이 반드시 필요하지만, <strong>논리적</strong>으로 추가할 수밖에 없음<ul>
<li><code>@OnDelete</code> 방식에 비해 <strong>낮은</strong> 실수 확률</li>
<li>직접적인 <code>EntityManager</code> 호출이 필요 없으므로, 비교적 더 간결한 <strong>복잡성</strong></li>
</ul>
</li>
</ul>
<blockquote>
<p>🎉 <code>@OneToMany(cascade)</code> 승리</p>
</blockquote>
<ul>
<li>실수할 확률, 다른 의존관계 사용 여부 등에서 <code>@OneToMany</code> 가 더 낫다고 생각<br>

</li>
</ul>
<h3 id="📓-결론">📓 결론</h3>
<ul>
<li><strong>개별 작업</strong>이 전혀 <strong>필요 없고</strong>, 엔티티의 삭제만 <strong>최대한의 성능</strong>으로 처리하고 싶다면 <code>@OnDelete</code> 를 사용<ul>
<li>하지만 사실상, 이런 경우는 <strong>거의 없다고 봐도 무방할 것으로 예상됨</strong></li>
</ul>
</li>
<li>그 외의 경우 <code>@OneToMany(cascade = CascadeType.REMOVE)</code> 사용이 <strong>더 유리</strong></li>
</ul>
<blockquote>
<p>‼️ <strong>웬만해서</strong> <code>@OneToMany(cascade = CascadeType.REMOVE)</code> 를 사용하자</p>
</blockquote>
<hr>
<blockquote>
<p>검수)</p>
</blockquote>
<ul>
<li>Google Gemini ( <a href="https://gemini.google.com/app">https://gemini.google.com/app</a> )</li>
<li>ChatGPT ( <a href="https://chatgpt.com/">https://chatgpt.com/</a> )</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TransactionManager와 TransactionSynchronizationManager]]></title>
            <link>https://velog.io/@riverpower6_g/TransactionManager%EC%99%80-TransactionSynchronizationManager</link>
            <guid>https://velog.io/@riverpower6_g/TransactionManager%EC%99%80-TransactionSynchronizationManager</guid>
            <pubDate>Sat, 09 Aug 2025 06:49:47 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp;<strong>트랜잭션</strong>은 데이터를 다루는 개발자에게는 뗄레야 뗄 수 없는 핵심 요소이다.
필자 역시 트랜잭션의 중요성을 알기에, 트랜잭션의 이해와 활용 방식 등에 대해서 시간을 적지 않게 소요했었다.</p>
<p>&nbsp;그렇게 트랜잭션에 대한 이해도가 상당히 올라왔다고 생각하고 이전 프로젝트들을 진행했었다. 도중에 헷갈리는 부분들도 있었지만, 그래도 프로젝트 진행중 추가적으로 공부해나가며 부족한 부분을 잘 메꿀 수 있었다.</p>
<p>&nbsp;최근, 스프링을 더욱 더 깊이 이해하고 기술을 사용하기 위해서 유명한 <strong>김영한</strong>님의 스프링 강의를 들으며 이해하고 있다.
학습 내용 중 <strong>트랜잭션</strong>에 대한 내용이 나왔는데, 기존에 알고 있던 것들이 생각보다 더 얕은 지식들이었다는 걸 느끼게 되었다.</p>
<p>&nbsp;<strong>트랜잭션</strong>의 동작 원리와 흐름, 그 안에서 작동하는 것들에 대해서 좀 더 깊게 공부하였고, 그 부분에 대해서 정리하는 시간을 가져보려고 한다.</p>
<hr>
<h1 id="1️⃣-platformtransactionmanager">1️⃣ PlatformTransactionManager</h1>
<ul>
<li><strong>트랜잭션 매니저 인터페이스</strong></li>
<li>여러 <strong>DB 드라이버</strong>에서 <strong>트랜잭션</strong>과 관련된 기능들을 사용할 수 있도록 설계된 <strong>스프링 인터페이스</strong><ul>
<li><code>JDBC</code> , <code>JPA</code> 등 <strong>특정 기술에 종속되지 않고 일관된 방식으로</strong> 트랜잭션 관리 가능</li>
<li><code>JDBC</code> 만 사용하면 <code>DataSourceTransactionManager</code> 가, <code>JPA</code> 를 사용하면 <code>JpaTransactionManager</code> 가 자동으로 <strong>스프링 빈</strong>에 등록되고 사용</li>
</ul>
</li>
<li><strong>실제 트랜잭션</strong>의 <strong>시작 및 생성, 커밋, 롤백</strong>을 담당하는 그야말로 <strong>핵심 인터페이스</strong></li>
<li><strong>물리 트랜잭션(외부 트랜잭션)</strong>에 대한 관리를 진행<br>
## 💪 역할

</li>
</ul>
<p><strong>1. 트랜잭션 시작</strong></p>
<ul>
<li><code>getTransaction()</code> 메서드를 호출하여 트랜잭션을 새로 생성하고, 해당 커넥션의 <strong>autoCommit(false)</strong>를 호출<ul>
<li><code>getTansaction()</code> 메서드 생성 시, 트랜잭션에 대한 전파, 격리 수준 등의 추가적인 조건을 설정한 트랜잭션을 생성 가능</li>
</ul>
</li>
<li>이 커넥션을 <code>TransactionSynchronizationManager</code> 에 등록</li>
<li>이후 <code>DB</code> 작업 호출이 필요할 땐 등록된 <strong>커넥션</strong>을 사용함으로 <strong>autoCommit(false)</strong> 상태 유지</li>
</ul>
<p><strong>2. 커밋</strong></p>
<ul>
<li><code>commit()</code> 메서드로 <strong>트랜잭션 내의 모든 작업을 <code>DB</code> 에 영구 반영</strong></li>
<li><strong>물리적 DB 커밋</strong></li>
</ul>
<p><strong>3. 롤백</strong></p>
<ul>
<li><code>rollback()</code> 메서드로 <strong>트랜잭션 내의 모든 작업 롤백</strong></li>
<li><strong>물리적 DB 롤백</strong></li>
</ul>
<p><strong>4. 자원 관리</strong></p>
<ul>
<li><strong>커밋, 롤백</strong> 이후, 커넥션의 <code>autoCommit(true)</code> 를 호출</li>
<li><strong>트랜잭션 동기화 매니저</strong>에 등록된 <strong>현재 커넥션</strong>을 <strong>커넥션 풀로 반환</strong><br>

</li>
</ul>
<hr>
<h1 id="2️⃣-transactionsynchronizationmanager">2️⃣ TransactionSynchronizationManager</h1>
<ul>
<li><strong>트랜잭션 동기화 매니저</strong></li>
<li><strong>현재 스레드에서 진행 중인 트랜잭션</strong>에 대한 정보를 관리하고, 해당 커넥션을 <strong>동기화</strong>하는 역할 담당<ul>
<li>해당 트랜잭션 정보들은 <code>ThreadLocal</code> 을 통해 따로 보관</li>
<li><code>@Component</code> 같은 어노테이션으로 <strong>직접 등록하는 일반적인 빈이 아니라,</strong> 스프링 자체에서 <strong>내부적으로 관리하는 유틸리티 클래스에 근접</strong></li>
</ul>
</li>
<li><strong>논리 트랜잭션(내부 트랜잭션)</strong>에 대한 관리를 진행<ul>
<li><strong>트랜잭션 컨텍스트</strong>를 <strong>스레드 단위</strong>로 관리<br>

</li>
</ul>
</li>
</ul>
<h2 id="💪-역할">💪 역할</h2>
<p><strong>1. ThreadLocal 관리</strong></p>
<ul>
<li><strong>현재 스레드의 트랜잭션 관련 정보 바인딩 및 해제</strong><ul>
<li><strong>트랜잭션 객체, 커넥션, rollbackOnly 플래그 ...</strong></li>
</ul>
</li>
</ul>
<p><strong>2. 롤백 전용 플래그 관리</strong></p>
<ul>
<li>메서드에서 <strong>예외 발생 시</strong> <code>setRollbackOnly()</code> 메서드로 현재 트랜잭션을 <strong>롤백 전용 트랜잭션으로 마킹</strong><ul>
<li>이후 <strong>트랜잭션 매니저는 <code>rollbackOnly</code> 플래그 유무를 확인한 후 커밋 또는 롤백 진행</strong></li>
</ul>
</li>
</ul>
<p><strong>3. 트랜잭션 정보 확인</strong></p>
<ul>
<li><code>isActualTransactionActive()</code> , <code>isNewTransaction()</code> 등, <strong>현재 작업 중인 트랜잭션에 대한 정보</strong>를 확인 가능<br>

</li>
</ul>
<h2 id="💾-treadlocal-저장-필드">💾 <code>TreadLocal</code> 저장 필드</h2>
<p><strong>1. TransactionStatus</strong></p>
<ul>
<li><strong>현재 트랜잭션의 상태를 저장</strong>하는 객체</li>
<li>트랜잭션의 <strong>활성 상태, 롤백 전용 표시, 커밋/롤백 여부, 새로운 트랜잭션인지 여부</strong> 등의 정보 포함</li>
</ul>
<p><strong>2. Connection</strong></p>
<ul>
<li><code>DB</code> 와의 실제 연결을 진행하는 <code>Connection</code> 객체<ul>
<li><strong>스프링</strong>은 기본적으로 <code>HikariCP</code> 커넥션 풀을 사용</li>
<li><strong>커넥션 풀</strong>에서 임의의 커넥션을 선택 후 등록</li>
</ul>
</li>
<li><strong>하나의 트랜잭션</strong> 내에서는 반드시, <strong>모든</strong> <code>DB</code> 작업에서 이 <strong>동일한 커넥션</strong> 사용<ul>
<li><strong>데이터 정합성 보장</strong></li>
</ul>
</li>
</ul>
<p><strong>3. EntityManager</strong></p>
<ul>
<li>트랜잭션이 시작될 때, 트랜잭션 동기화 매니저는 <code>EntityManagerFactory</code> 에서 <strong>새로운</strong> <code>Entitymanager</code> 인스턴스 생성<ul>
<li><code>EntityManagerFactory</code> 는 <strong>싱글톤 빈</strong>으로 등록된 객체</li>
<li>여기서 매 요청마다 <strong>새로운</strong> <code>EntityManager</code> 를 생성하므로, <code>EntityManager</code> 는 <strong>싱글톤이 아님</strong></li>
</ul>
</li>
<li>생성된 <code>EntityManager</code> 를 <code>ThreadLocal</code> 에 저장<ul>
<li><code>EntityManager</code> 는 해당 트랜잭션의 <strong>영속성 컨텍스트 담당</strong></li>
<li><code>JPA</code> 에서 <code>EntityManager</code> 는 <strong>영속성 컨텍스트</strong>와 기본적으로 <strong>1:1</strong>로 매칭되기 때문<br>

</li>
</ul>
</li>
</ul>
<blockquote>
<h3 id="💡-전파-수준에-따른-rollbackonly-플래그">💡 전파 수준에 따른 <code>rollbackOnly</code> 플래그</h3>
</blockquote>
<ul>
<li><code>rollbackOnly</code> 플래그는 <strong>&#39;현재 트랜잭션이 새로운 트랜잭션인가&#39;</strong> 를 기준으로 작동<ul>
<li><code>isNewTransaction() == true</code> :<ul>
<li><strong>독립적인 트랜잭션, 즉 하나의 물리적 트랜잭션</strong> 으로 판단</li>
<li>해당 트랜잭션이 <strong>커밋</strong>되면, <code>rollbackOnly</code> 플래그 체크 후 <strong>실제 커밋 진행</strong></li>
<li>해당 트랜잭션이 <strong>롤백</strong>되면, <code>setRollbackOnly()</code> 를 하지 않고 바로 <strong>물리 DB 롤백 진행</strong></li>
</ul>
</li>
<li><code>isNewTransaction() == false</code> :<ul>
<li><strong>기존에 존재하던 트랜잭션</strong>에 합류, <strong>새로운 논리적 트랜잭션</strong></li>
<li>해당 트랜잭션이 <strong>커밋</strong>되면, 바로 다음 시퀀스 진행</li>
<li>해당 트랜잭션이 <strong>롤백</strong>되면, 트랜잭션 동기화 매니저에 <code>rollbackOnly()</code> 플래그를 <code>true</code> 로 변경, 이후 해당 논리 트랜잭션이 <strong>포함</strong>된 <strong>물리 트랜잭션 전체를 롤백</strong><br>

</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h1 id="3️⃣-트랜잭션-동기화-매니저---requires_new">3️⃣ 트랜잭션 동기화 매니저 - <code>REQUIRES_NEW</code></h1>
<ol>
<li><strong>트랜잭션 동기화 매니저</strong>는 기본적으로 <strong>1개의 커넥션만을 관리</strong><br></li>
<li><code>REUIQRES_NEW</code> 로 <strong>새로운 트랜잭션</strong>이 실행되면, <code>ThreadLocal</code> 에 저장된 <strong>기존 트랜잭션 정보</strong>를 <strong>내부 스택에 임시로 저장</strong><ul>
<li>해당 트랜잭션은 <code>Thread</code> 에서의 <strong>블로킹</strong> 처럼, 이후 트랜잭션이 종료되기 전까지 대기</li>
<li>이후 트랜잭션이 종료되지 않으면 <strong>이후 작업 불가능</strong><br></li>
</ul>
</li>
<li><strong>트랜잭션 매니저가 새로운 커넥션을 생성</strong>, 동일하게 <code>autoCommit(false)</code> 로 설정 후 <strong>트랜잭션 동기화 매니저</strong>의 <code>ThreadLocal</code> 에 바인딩<ul>
<li>이 과정에서 <strong>기존 <code>ThreadLocal</code> 트랜잭션 정보를 새로운 커넥션의 트랜잭션 정보로 오버라이딩</strong></li>
<li>이후 진행되는 <code>DB</code> 작업은, 이 <strong>새로 할당된 커넥션</strong>을 통해 진행<br></li>
</ul>
</li>
<li><strong>새로운 트랜잭션</strong>이 <strong>커밋, 롤백</strong>되면 이는 하나의 <strong>독립적인 트랜잭션</strong>이므로 바로 <code>DB</code> 에 반영, 이후 <strong>커넥션 풀로 커넥션 반환</strong><br></li>
<li><strong>트랜잭션 동기화 매니저</strong>는 임시 스택에 저장되어 있던 <strong>기존 트랜잭션 정보</strong>를 <code>ThreadLocal</code> 에 <strong>리바인딩</strong> 후 <strong>대기 중이던 이전 트랜잭션 진행</strong><br>

</li>
</ol>
<hr>
<h1 id="4️⃣-transactiondefinition">4️⃣ TransactionDefinition</h1>
<ul>
<li><strong>트랜잭션 행동 규칙 정의 인터페이스</strong></li>
<li><strong>트랜잭션 매니저</strong>의 <code>getTransaction()</code> 메서드의 <strong>파라미터</strong>에 등록하여, <strong>원하는 트랜잭션 속성</strong>을 적용시킬 수 있음</li>
<li>일반적으로 <strong>기본 구현체</strong>인 <code>DefaultTransactionDefinition</code> 혹은 몇 가지 기능이 추가된 <code>DefaultTransactionAttributes()</code> 구현체를 등록</li>
</ul>
<ul>
<li><p>ex)</p>
<pre><code class="language-java">// 트랜잭션 행동 규칙 객체 (현재 기본 구현체)
DefaultTransactionDefinition definition = new DefaultTransactionAttributes();

// 트랜잭션 매니저는 해당 규칙이 적용된 트랜잭션 생성
PlatformTransactionManager txManager = new JpaTransactionManager(definition);</code></pre>
<br>

</li>
</ul>
<h2 id="🗂️-등록-속성">🗂️ 등록 속성</h2>
<p><strong>1. 전파(Propagation)</strong></p>
<pre><code class="language-java">// 전파 속성 변경 (Default : PROPAGATION_REQUIRED)
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);</code></pre>
<br>

<p><strong>2. 격리 수준(Isolation Level)</strong></p>
<pre><code class="language-java"> // 격리 수준 변경 (DEFAULT :
 //        MySQL : ISOLATION_REPEATABLE_READ    트랜잭션 내에서 같은 데이터를 몇 번 읽어도 동일한 결과 보장
 //        일반 DB : ISOLATION_READ_COMMITED       커밋된 데이터만 읽기 가능
 definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITED)</code></pre>
<br>

<p><strong>3. 타임아웃(Timeout)</strong></p>
<pre><code class="language-java"> // 트랜잭션 완료까지의 최대 허용 시간 변경 (Default : -1, 타임아웃 없음)
 definition.setTimeout(30);        // 트랜잭션이 30초를 촉과하면 롤백</code></pre>
<ul>
<li>트랜잭션이 <strong>지정된 시간을 넘으면 롤백</strong><br>

</li>
</ul>
<p><strong>4. 읽기 전용(Read-Only)</strong></p>
<pre><code class="language-java"> // 읽기 전용 트랜잭션임을 명시 (Default : false)
 // @Transactional(readOnly = true) 과 동일
 definition.setReadOnly(true);</code></pre>
<ul>
<li>읽기 전용 트랜잭션은 <strong>조회에 특화된 트랜잭션</strong></li>
<li><strong>데이터 변경이 불가능</strong>하며, 그만큼 <strong>조회</strong>에서 성능상 유리<br>

</li>
</ul>
<blockquote>
<h3 id="💡-읽기-전용-트랜잭션의-조회-성능-최적화">💡 읽기 전용 트랜잭션의 조회 성능 최적화</h3>
</blockquote>
<p><strong>1. JPA/Hibernate 최적화</strong></p>
<ul>
<li><code>ORM</code> 프레임워크는 <strong>스냅샷</strong>을 유지하고 <strong>더티 체킹</strong>을 수행하는 것이 기본</li>
<li><strong>읽기 전용 트랜잭션</strong>은 <strong>스냅샷을 생성하지 않고</strong> <strong>더티 체킹도 생략</strong><ul>
<li>스냅샷 미생성으로 인한 <strong>메모리 절약</strong></li>
<li>더티 체킹 생략으로 인한 <strong>처리 속도 향상</strong><br>

</li>
</ul>
</li>
</ul>
<p><strong>2. DB 드라이버 최적화</strong></p>
<ul>
<li><strong>Write Lock 생략</strong><ul>
<li><strong>일반적인 트랜잭션</strong>은 <strong>동시성을 고려</strong>하여, 데이터 변경 시 <strong>Write Lock을 획득해야 함</strong></li>
<li><strong>읽기 전용 트랜잭션</strong>은 <strong>Lock</strong> 획득 과정을 <strong>생략 가능</strong></li>
<li><strong>병목 현상 저하</strong> 및 <strong>트랜잭션 처리 속도 향상</strong></li>
</ul>
</li>
<li><strong>캐싱 최적화</strong><ul>
<li><strong>내부 캐싱 메커니즘 최적화 가능</strong></li>
<li><strong>커넥션 풀 리소스</strong>를 더 효율적으로 관리하여 <strong>시스템 성능 향상 가능</strong><br>

</li>
</ul>
</li>
</ul>
<p><strong>3. 복제 환경(Replication) 최적화</strong></p>
<ul>
<li><strong>읽기 부하 분산</strong><ul>
<li><strong>Master-Slave</strong> 구조의 <strong>DB 복제 환경</strong>에서는 일반적으로, <code>Master DB</code> 는 <strong>쓰기 담당</strong>만, <code>Slave DB</code> 는 <strong>읽기 담당</strong>만 하도록 <strong>분산 처리</strong></li>
<li><strong>읽기 전용 트랜잭션</strong>은 요청을 <code>Slave DB</code> 로 <strong>자동 라우팅 가능</strong></li>
</ul>
</li>
<li><code>Master DB</code> <strong>부하 감소</strong><ul>
<li>읽기 요청을 <code>Slave DB</code> 로 분산시킴으로, <code>Master DB</code> 는 <strong>오직 변경 작업에만 집중 가능</strong></li>
<li><code>Master DB</code> 의 부하를 크게 줄임으로써, <code>DB</code> 의 <strong>안정성을 높이고, 쓰기 성능의 향상을 보장 가능</strong></li>
</ul>
</li>
<li><strong>확장성 향상</strong><ul>
<li><strong>조회 요청</strong>이 많아지더라도, <code>Slave DB</code> <strong>서버 추가만으로 서버 안정화 및 성능 확장 가능</strong><br>

</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<p>검수) Google Gemini ( <a href="https://gemini.google.com/app">https://gemini.google.com/app</a> )</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSR, CSR - Next.js 렌더링]]></title>
            <link>https://velog.io/@riverpower6_g/SSR-CSR</link>
            <guid>https://velog.io/@riverpower6_g/SSR-CSR</guid>
            <pubDate>Sat, 02 Aug 2025 14:40:39 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp;<code>Next.js</code> 를 이용해 <strong>프론트엔드</strong> 실습을 진행 중, 헷갈리는 부분이 생겼다. 필자가 알고 있는 <code>SSR</code> 과 <code>CSR</code> 의 개념이 실습 중 혼동을 일으키기 시작했다.</p>
<p>&nbsp;<strong>서버 사이드 렌더링</strong>은 서버 측에서 렌더링을 해서 보여주는 거고, <strong>클라이언트 사이드 렌더링</strong>은 브라우저에서 렌더링을 해서 보여준다 라고만 이해하고 있었다.
그러다보니, &#39; <code>Next.js</code> 에서 <code>JSX</code> 와 <code>JavaScript</code> 를 사용해서 코드를 작성하면 <code>CSR</code> 이다&#39; 라고 잘못 이해하고 있었다.</p>
<p>&nbsp;잘못된 정보에 대한 인식을 개선한 김에, 이후에는 절대 헷갈리지 않도록 관련 개념에 대한 기록을 남겨두기로 했다.</p>
<hr>
<h1 id="1️⃣-ssr">1️⃣ SSR</h1>
<ul>
<li><strong>Server-Side Rendering</strong> - <strong>서버 사이드 렌더링</strong></li>
<li><strong>서버</strong>에서 웹 페이지 렌더링을 담당</li>
<li>클라이언트로부터 페이지를 요청받으면 <strong>완성된 정적 + 동적 HTML 파일</strong>을 <strong>클라이언트</strong>에게 반환<ul>
<li>기본적인 <strong>정적 데이터</strong>인 <code>HTML</code> , <code>JSX</code> 파일은 그대로 반환</li>
<li>서버에 <strong>동적인 데이터를 요청</strong>한 경우, 해당 <code>API</code> 를 <strong>호출한 후의 응답값(동적 데이터)</strong> 또한 <code>HTML</code>, <code>JSX</code> 파일에 담아서 반환</li>
<li>즉, <strong>데이터를 모두 채워 넣은 완전한 <code>HTML</code> 파일 반환</strong></li>
</ul>
</li>
<li><strong>서버 데이터 기반 <code>UI</code> 구성 담당</strong><br>

</li>
</ul>
<h3 id="❓-장점">❓ 장점</h3>
<ol>
<li><strong>빠른 초기 로딩 성능</strong><ul>
<li><code>HTML</code> 파일 자체가 모든 컨텐츠를 이미 포함</li>
<li><strong>클라이언트(브라우저)</strong>는 이 페이지를 바로 렌더링하기만 하면 됨</li>
</ul>
</li>
<li><strong>SEO</strong> 최적화<ul>
<li><strong>검색 엔진 크롤러</strong>가 페이지의 모든 콘텐츠를 쉽게 읽을 수 있음</li>
<li><strong>특정 키워드</strong>에 대한 검색 결과 노출에 유리<br>

</li>
</ul>
</li>
</ol>
<h3 id="❗-단점">❗ 단점</h3>
<ol>
<li><strong>느린 페이지 전환 속도</strong><ul>
<li>페이지 이동 시마다, 서버는 <code>HTML</code> 을 <strong>새로</strong> 렌더링 후 전송</li>
<li>로직 처리에 시간이 다소 소요된다면, <strong>응답받기 전까지 페이지 로딩</strong></li>
<li>이는 <strong>불쾌한 사용자 경험</strong>을 초래</li>
</ul>
</li>
<li><strong>서버 부하의 증가</strong><ul>
<li>매번 서버 <code>API</code> 를 호출하므로, 서버 호출 횟수 증가</li>
<li><strong>트래픽이 많을수록</strong> 서버 자원은 <strong>많이 소모</strong></li>
<li><strong>서버 부하</strong>로 인한 성능 저하, 심하면 <strong>서버 다운</strong>까지 초래</li>
</ul>
</li>
</ol>
<hr>
<h1 id="2️⃣-csr">2️⃣ CSR</h1>
<ul>
<li><strong>Client-Side Rendering</strong> - <strong>클라이언트 사이드 렌더링</strong></li>
<li><strong>클라이언트(브라우저)</strong>에서 페이지 렌더링을 담당</li>
<li><strong>웹 페이지의 모든 구성 요소가 <code>JavaScript</code> 에 의해 동적으로 생성</strong></li>
<li><strong><code>UX</code> 및 사용자 인터렉션 담당</strong><br>

</li>
</ul>
<h3 id="❓-장점-1">❓ 장점</h3>
<ol>
<li><strong>빠른 페이지 전환</strong><ul>
<li><strong>서버 <code>API</code> 호출</strong> 이전에 <strong>페이지를 먼저 렌더링</strong></li>
<li>즉, 서버에 데이터가 저장되기도 <strong>이전에</strong>, 바로 <code>UI</code> 에 먼저 적용</li>
<li><strong>낙관적 업데이트</strong></li>
</ul>
</li>
<li><strong>풍부한 사용자 경험</strong><ul>
<li>사용자에게 <strong>즉각적인 피드백</strong>을 제공</li>
<li>사용자의 <code>UX</code> 극대화<br>

</li>
</ul>
</li>
</ol>
<h3 id="❗-단점-1">❗ 단점</h3>
<ol>
<li><strong>초기 로딩 속도 저하</strong><ul>
<li><code>CSR</code> 은 사용자에게 <strong>해당 컴포넌트의 <code>JavaScript</code> 를 모두 전송</strong><ul>
<li><strong>컴포넌트가 많아질수록</strong> 초기 다운로드 용량 증가</li>
</ul>
</li>
<li>서버로부터 받은 <code>HTML</code> 에 <code>JavaScript</code> 를 연결하여 <strong>이벤트 리스너를 등록</strong> <ul>
<li>이를 <strong>하이드레이션</strong> 이라고 함</li>
<li><strong>하이드레이션에 필요한 작업 증가</strong>로 인한 <strong>인터랙션 소요 시간 증가</strong></li>
</ul>
</li>
</ul>
</li>
<li><strong>SEO</strong> 효율성 감소<ul>
<li><code>JavaScript</code> 기반이므로, 완성된 <code>HTML</code> 파일을 제공하는 <code>SSR</code> 방식에 비해 <strong>낮은 효율</strong><br>

</li>
</ul>
</li>
</ol>
<blockquote>
<h3 id="💡-낙관적-업데이트의-이면">💡 <strong>낙관적 업데이트</strong>의 이면</h3>
</blockquote>
<ul>
<li><strong>가상 시나리오</strong>
1) 사용자가 <strong>좋아요</strong> 버튼 클릭
2) <strong>좋아요 숫자 증가</strong> 및 <strong>버튼 액티브 UI 제공</strong>
3) <strong>서버 <code>API</code> 호출</strong>
4) <code>API</code> 는 <strong>10분</strong> 대기 후 해당 로직 실행
5) <strong>10분 후</strong> 해당 로직 수행 중 <strong>에러 발생</strong>
6) <strong>좋아요 클릭 인터랙트 초기화</strong> 및 <strong>에러 메시지 표시</strong></li>
</ul>
<br>

<ul>
<li>이처럼, <strong>빠른 반응성</strong>을 제공하는 <strong>낙관적 업데이트</strong>이지만 추후 에러 발생 시 <strong>뒤늦은 에러 응답</strong> 발생</li>
<li>이에 따른 <strong>불쾌환 사용자 경험</strong> 제공<br>

</li>
</ul>
<h3 id="⚒️-해결-방안">⚒️ 해결 방안</h3>
<ol>
<li><strong>타임아웃 설정</strong><ul>
<li>서버 응답을 기다리는 <strong>최대 시간 설정</strong></li>
<li><code>UX</code> 저하 방지를 위해, <strong>납득할 정도의 처리 시간 한도 설정</strong></li>
</ul>
</li>
<li><strong>로딩 상태 사용</strong><ul>
<li>서버 응답이 오기 전까지, 페이지에 <strong>로딩 중임을 알리는 상태</strong> 추가</li>
<li><strong>빠른 응답성</strong>이라는 장점을 <strong>로딩 중</strong> 상태 표시에 사용하지만, 그만큼 <strong>안정적인 사용자 경험 제공</strong></li>
</ul>
</li>
<li><strong>백그라운드 재시도</strong><ul>
<li><strong>서버 요청 실패시</strong>, 에러 표시 대신 <strong>백그라운드로 요청 자동 재시도</strong></li>
</ul>
</li>
</ol>
<hr>
<h1 id="3️⃣-spa-single-page-application">3️⃣ SPA (Single-Page Application)</h1>
<ul>
<li><strong>단일 페이지 어플리케이션</strong></li>
<li><strong>페이지 이동 시마다</strong> 서버에 <strong>전체 페이지를 요청</strong>하는 기본 방식과 달리, <strong>최초 페이지 접근 한 번만</strong> 정적 리소스를 다운로드<ul>
<li>이후 상호작용 발생시, <strong>페이지 새로고침을 하지 않고</strong> <code>JavaScript</code> 로 <strong>필요한 데이터만</strong> 서버에서 비동기적으로 호출</li>
<li><strong>브라우저 캐싱</strong>을 통해 <strong>다운로드 리소스들은 재활용</strong></li>
<li><strong>첫 로딩</strong>은 느릴 수 있어도, 이후 페이지 전환은 <strong>매우 빠름</strong></li>
</ul>
</li>
<li>이렇게 <strong>동적 업데이트 렌더링</strong> 수행</li>
<li><code>CSR</code> 방식을 <strong>극대화한</strong> 웹 애플리케이션 형태<br>

</li>
</ul>
<hr>
<h1 id="4️⃣-nextjs-에서의-렌더링">4️⃣ <code>Next.js</code> 에서의 렌더링</h1>
<ul>
<li><code>Next.js</code> 는 <code>SSR</code> 과 <code>CSR</code> 을 함께 사용하는 <strong>하이브리드</strong></li>
<li>&quot;use client&quot; 사용 유무에 따라 <code>SSR</code> 과 <code>CSR</code> 을 구분<ul>
<li>&quot;use client&quot; 사용 : <code>CSR</code> - <strong>클라이언트 컴포넌트 렌더링</strong></li>
<li>&quot;use client&quot; 미사용 : <code>SSR</code> - <strong>서버 컴포넌트 렌더링</strong><br>

</li>
</ul>
</li>
</ul>
<h3 id="📌-use-client">📌 &quot;use client&quot;</h3>
<ul>
<li><strong>클라이언트 컴포넌트</strong>로 정의한다는 의미</li>
<li><strong>브라우저만 사용할 수 있는 기능</strong>을 사용할 때 사용</li>
<li><strong>React Hooks</strong>을 사용하는 경우<ul>
<li>ex) <code>use()</code> , <code>useEffect()</code> , <code>useRouter()</code> ...</li>
</ul>
</li>
<li><code>JavaScript</code> 로 등록된 <strong>이벤트 핸들러</strong>가 존재하는 경우<ul>
<li>ex) <code>onClick()</code> , <code>onSubmit()</code> , <code>onChange()</code> ...</li>
</ul>
</li>
<li><strong>브라우저 <code>API</code></strong>를 사용하는 경우<ul>
<li>ex) <code>window.scrollTo()</code> , <code>document.title</code> ...<br>

</li>
</ul>
</li>
</ul>
<blockquote>
<h3 id="💡-a-🆚-router-🆚-link">💡 <code>a</code> 🆚 <code>router</code> 🆚 <code>Link</code></h3>
</blockquote>
<ul>
<li><code>JavaScript</code> 에서는 <code>Link</code> 나 <code>&lt;a&gt;</code> , <code>router</code> 등으로 <strong>페이지 이동</strong> 지원<br>

</li>
</ul>
<h3 id="🥇-a">🥇 <code>&lt;a&gt;</code></h3>
<ul>
<li><strong>브라우저 기본 링크 동작</strong></li>
<li>클릭 시 <strong>서버에 새로운 페이지 요청</strong></li>
<li>서버는 해당 페이지의 <strong>새로운 HTML</strong> 응답</li>
<li><h3 id="ssr"><code>SSR</code></h3>
</li>
<li><strong>페이지 이동 이후</strong>, 해당 페이지의 &quot;use client&quot; 유무에 따라 <code>CSR</code> 추가</li>
<li>즉, <code>SSR</code> 로 시작하여 필요에 따라 <code>CSR</code> 추가로 <strong>사용자 인터랙션</strong> 제공<br>

</li>
</ul>
<h3 id="🥈-router">🥈 router</h3>
<ul>
<li><strong>JavaScript 이벤트 핸들러 내부</strong>에서 사용</li>
<li>페이지 전체를 <strong>새로고침하는 게 아니라</strong>, <strong>필요한 데이터만</strong> 호출하여 페이지 <strong>업데이트</strong></li>
<li><h3 id="csr"><code>CSR</code></h3>
</li>
<li>즉, <code>CSR</code> 로 시작하여 <strong>사용자 인터랙션</strong>으로 인해 페이지가 이동되고, 이후 <code>SSR</code> 로 <strong>새로운 콘텐츠 응답</strong><br>

</li>
</ul>
<h3 id="🥉-link">🥉 <code>&lt;Link&gt;</code></h3>
<ul>
<li><strong><code>SPA</code> 방식의 페이지 이동 지원</strong></li>
<li>클릭 시 <strong>새로운 페이지 요청이 아닌</strong>, <code>Next.js</code> 가 미리 다운로드해 둔 <strong>필요한 <code>JavaScript</code> 및 데이터만으로</strong> 페이지 <strong>업데이트</strong><ul>
<li>이렇게 다운로드 리소스를 <strong>미리 가져오는 것</strong>을 <strong>프리패칭(Prefetching)</strong> 이라고 함</li>
</ul>
</li>
<li><code>router</code> 와 마찬가지로 <strong>새로고침 없이 페이지 전환</strong></li>
<li><h3 id="ssr--csr-하이브리드"><code>SSR</code> + <code>CSR</code> <strong>(하이브리드)</strong></h3>
</li>
<li><code>SSR</code> 로 초기 페이지 렌더링 이후, <code>&lt;Link&gt;</code> 페이지 이동 시에는 <code>CSR</code> 로 <strong>부드러운 페이지 전환</strong></li>
<li><code>Next.js</code> 의 기본 제공 렌더링 방식으로, <code>SPA</code> 의 장점들을 모두 누릴 수 있음</li>
</ul>
<hr>
<blockquote>
<p>검수) Google Gemini ( <a href="https://gemini.google.com/app">https://gemini.google.com/app</a> )</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[프록시와 AOP, 스프링 빈(@Component와 @Configuration)]]></title>
            <link>https://velog.io/@riverpower6_g/%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-AOP-Component%EC%99%80-Configuration-Bean</link>
            <guid>https://velog.io/@riverpower6_g/%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-AOP-Component%EC%99%80-Configuration-Bean</guid>
            <pubDate>Fri, 01 Aug 2025 13:51:20 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; 필자는 <code>AOP</code> 에 관심이 많다.
공부해오고 프로젝트를 진행해오면서 조금씩 <code>AOP</code> 관련 지식을 쌓아가며 <code>AOP</code> 기술을 점진적으로 발전시키며 활용해왔고, 프로젝트 중 하나에는 <strong>어노테이션</strong>을 이용해서 <code>AOP</code> 처리를 통해 <strong>인가</strong>를 적용한 후 <strong>코드 및 DB 호출</strong>의 <strong>중복</strong>을 방지하기 위해 <strong>ThreadLocal</strong>에 관련 정보를 저장해서 <strong>비즈니스 로직</strong>으로 전달하는 코드까지 구현해보았다.</p>
<p>&nbsp; 어느정도 <code>AOP</code> 에 대한 지식이 있다고 생각하고 있던 찰나, 최근 스프링 관련 강의를 들으며 공부를 하다보니 문득 궁금증이 생겼다.</p>
<blockquote>
<p><em><strong>&#39;내가 구현한 AOP를 처리하는 프록시 객체는 뭘까?&#39;</strong></em>
<em><strong>&#39;어떤 건 프록시고 어떤 건 실제 객체네? 뭐지?&#39;</strong></em></p>
</blockquote>
<p>최근 스프링의 원리를 확실하게 이해하기 위해 강의를 들으며, 궁금한 부분은 바로바로 조사하고 공부하는 습관이 들어있다.
이번에 공부한 내용을 기록으로 남겨두면 여러모로 도움이 많이 될 것 같아서, 관련 내용을 따로 정리해보았다.</p>
<hr>
<h1 id="1️⃣-aop-와-프록시">1️⃣ AOP 와 프록시</h1>
<h2 id="aop">AOP</h2>
<ul>
<li><strong>프로그래밍 패러다임</strong></li>
<li><strong>횡단 관심사</strong>를 중심으로 프로그램을 설계하는 <strong>방법</strong><ul>
<li>여러 객체에 걸쳐 <strong>공통적으로</strong>  적용되는 기능</li>
<li>ex) 로깅, 트랜잭션, 캐싱 ...</li>
</ul>
</li>
<li><code>SOLID</code> 중 <code>OCP</code> 원칙을 준수<ul>
<li><code>AOP</code> 는 기존 기능을 전혀 수정하지 않고, 로깅, 트랜잭션 등 부가기능을 <strong>추가</strong></li>
<li>즉, 일종의 <strong>확장</strong>에 해당</li>
</ul>
</li>
<li><strong>횡단 관심사</strong>를 하나로 모아 관리함으로, 코드의 유지보수성을 높이고 비즈니스 로직과 횡단 관심사를 <strong>분리</strong>하는 곳이 목표<br>

</li>
</ul>
<h2 id="프록시">프록시</h2>
<ul>
<li><strong>디자인 패턴</strong></li>
<li><code>AOP</code> 가 제시한 패러다임을 수행할, <strong>대리자</strong> 역할의 객체를 생성하여 사용</li>
<li>프록시 객체를 통해, <strong>타겟(실제 객체) 전후</strong>에 <strong>횡단 관심사</strong>를 처리 가능</li>
</ul>
<hr>
<h1 id="2️⃣-스프링-빈과-프록시---component-configuration">2️⃣ 스프링 빈과 프록시 - @Component, @Configuration</h1>
<h2 id="component-과-프록시">@Component 과 프록시</h2>
<ul>
<li><code>@Component</code> 로 등록된 스프링 빈은 원칙적으로 <strong>실제 객체</strong><ul>
<li><strong>실제 객체</strong>가 스프링 컨테이너의 빈으로 등록</li>
<li><code>@Component</code> 를 포함하고 있는 <code>@Controller</code> , <code>@Service</code> 등도 마찬가지</li>
</ul>
</li>
<li>만약 컴포넌트 내에서 <code>AOP</code> 를 구현한 로직이 <strong>하나라도</strong> 존재할 경우, 실제 객체가 아닌 <strong>프록시 객체</strong>가 빈으로 등록<ul>
<li><code>@Transactional</code> 또한 내부에서 <code>AOP</code> 를 구현</li>
<li>따라서 <strong>클래스</strong> 또는 <strong>내부 메서드</strong>에 <code>@Transactional</code> 이 하나라도 존재할 경우, 스프링 빈 등록 시 이를 탐지하여 타겟(실제 인스턴스)를 감싸고 있는 <strong>프록시 객체</strong>로 등록</li>
</ul>
</li>
<li><strong>프록시 객쳬</strong> 또한 실제 객체와 마찬가지로 <code>@Component</code> 로 등록되어 있으므로, <strong>싱글톤</strong> 객체로 빈에 등록<br>

</li>
</ul>
<h2 id="configuration-과-프록시">@Configuration 과 프록시</h2>
<ul>
<li><code>@Configuration</code> 으로 등록된 클래스는, <strong>프록시 객체</strong>가 스프링 빈으로 등록<ul>
<li><code>CGLIB</code> 라이브러리를 사용하여 <strong>바이트코드가 조작 된 프록시 객체</strong></li>
</ul>
</li>
<li><code>@ComponentScan</code> 진행 시, <code>@Configuration</code> 내의 <strong>모든</strong> <code>@Bean</code> 을 찾아서 <strong>메서드명을 이름</strong>으로, 스프링 컨테이너에 스프링 빈으로 등록</li>
</ul>
<hr>
<h1 id="3️⃣-프록시-동작-흐름">3️⃣ 프록시 동작 흐름</h1>
<h2 id="component">@Component</h2>
<ul>
<li><code>AOP</code> 를 <strong>한 번이라도</strong> 구현한 경우 <strong>프록시 객체</strong>가 스프링 빈으로 등록</li>
<li>해당 비즈니스 로직이 호출되면 <strong>프록시 객체</strong>가 로직을 처리<br>
### 🌊 FLOW</li>
<li><strong>프록시 객체</strong>로 등록되었을 경우 :<ul>
<li><strong>Case1</strong> -&gt; <code>@Transactional</code> , <code>@Aspect</code> 등 <code>AOP</code> 가 적용된 경우<ul>
<li><strong>프록시 객체</strong>가 해당 로직 실행 전후로 <code>AOP</code> 에 등록된 코드를 수행 후 <code>proceed()</code> 로 <strong>타겟 메서드</strong> 실행</li>
</ul>
</li>
<li><strong>Case2</strong> -&gt; <code>@Transactional</code> , <code>@Aspect</code> 등 <code>AOP</code> 가 적용되지 않은 경우<ul>
<li><strong>프록시 객체</strong>는 <code>AOP</code> 를 적용하지 않고 바로 <code>proceed()</code> 로 <strong>타겟 메서드</strong> 실행</li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<h2 id="configuration">@Configuration</h2>
<ul>
<li>일반적으로 서버가 시작될 때, <strong>스프링 빈</strong> 초기화 -&gt; <strong>스프링 의존관계 주입(DI)</strong> 순서로 진행</li>
<li><code>@Configuration</code> 으로 등록된 <strong><code>CGLIB</code> 프록시 객체의 역할</strong>을 여기서 확인 가능<br>
### 🌊 FLOW</li>
</ul>
<ol>
<li><strong>애플리케이션 시작 시</strong> 스프링 빈을 등록하고 초기화하는 과정에서, <code>@Configuration</code> 의 <code>@Bean</code> 메서드를 모두 호출</li>
<li>해당 메서드가 <strong>다른 객체의 주입을 필요로 할 경우</strong>:<ul>
<li><strong>Case1</strong> -&gt; 해당 객체가 스프링 빈으로 <strong>등록되어 있다면, 해당 싱글톤 객체를 주입</strong></li>
<li><strong>Case2</strong> -&gt; 해당 객체가 아직 스프링 빈으로 <strong>등록되어 있지 않다면, 새로운 싱글톤 객체를 생성하여 스프링 빈으로 등록 후 해당 객체를 주입</strong></li>
</ul>
</li>
<li>과정을 반복하여 모든 <code>@Configuration</code> 의 <code>@Bean</code> 메서드를 <strong>스프링 빈으로 등록</strong><br>
</li>
</ol>
<ul>
<li>즉, <code>@Configuration</code> 의 가장 큰 역할은, <strong>해당 클래스 내 모든 스프링 빈 객체의 싱글톤화</strong></li>
</ul>
<br>

<blockquote>
<p>💡 <code>@Configuration</code>  또는 <code>@Bean</code> 이 없다면?</p>
</blockquote>
<h3 id="1️⃣-bean-만-단독으로-사용">1️⃣ <code>@Bean</code> 만 단독으로 사용</h3>
<ul>
<li><strong>컴포넌트 스캔</strong> 자체가 불가능</li>
<li>따라서 <strong>스프링 빈 등록 과정에서</strong> <code>@Bean</code> 메서드 자체가 호출 불가능</li>
<li><strong>스프링 빈 등록 실패</strong><br>

</li>
</ul>
<h3 id="2️⃣-configuration-만-단독으로-사용">2️⃣ <code>@Configuration</code> 만 단독으로 사용</h3>
<ul>
<li><strong>컴포넌트 스캔</strong>은 가능</li>
<li>따라서 <code>CGLIB</code> 프록시 객체가 <strong>스프링 빈으로 등록</strong></li>
<li>하지만 <code>@Bean</code> 이 없으므로, <strong>스프링 빈 등록 과정에서</strong> 메서드를 호출 불가능</li>
<li><strong>내부의 메서드들은 스프링 빈 등록 실패</strong><br>

</li>
</ul>
<h3 id="3️⃣-component--bean-조합으로-사용">3️⃣ <code>@Component</code> + <code>@Bean</code> 조합으로 사용</h3>
<ul>
<li><code>@Component</code> 로 인해 <strong>실제 객체가 스프링 빈으로 등록</strong></li>
<li><strong>스프링 빈 등록 과정에서</strong> <code>@Bean</code> 메서드 또한 호출 가능</li>
<li>하지만 <code>@Bean</code> 메서드 내부에서 <strong>다른 빈의 주입이 필요할 경우</strong>, 해당 메서드를 매번 호출</li>
<li><code>TypeA</code> 라는 <strong>빈</strong>을 여러 <code>@Bean</code> 메서드에서 <code>DI</code> 할 경우, 매번 다른 <code>TypeA</code> 인스턴스가 생성</li>
<li>따라서 <strong>빈 객체의 싱글톤이 보장되지 않음</strong></li>
</ul>
<hr>
<h1 id="⚒️-jdk-dynamic-proxy">⚒️ JDK Dynamic Proxy</h1>
<ul>
<li><strong>타겟 클래스</strong>가 <strong>인터페이스의 구현체인 경우</strong>, <code>AOP</code> 적용 시 <strong>항상</strong> <code>JDK Dynamic Proxy</code> 를 사용<ul>
<li><strong>인터페이스 메서드</strong>만 가로챌 수 있음</li>
<li><strong>확장된</strong> 기능은 가로챌 수 없음</li>
<li><strong>메인 메서드 실행 클래스</strong>에 <code>@EnableAspectJAutoProxy(proxyTargetClass = true)</code> 설정을 해줌으로, 항상 <code>CGLIB</code> 프록시를 사용하도록 변경할 수 있음</li>
<li><h3 id="스프링부트-20-이상-버전부터는-proxytargetclass--true-가-기본값으로-설정되어-사실상-jdk-동적-프록시는-사용되지-않음">스프링부트 2.0 이상 버전부터는 <code>proxyTargetClass = true</code> 가 기본값으로 설정되어, 사실상 <strong>JDK 동적 프록시</strong>는 사용되지 않음</h3>
<pre><code class="language-java">public interface UserService {
void findById(Long userId);
void findByUsername(String username);
}
</code></pre>
</li>
</ul>
</li>
</ul>
<p>@Service
public class UserServiceImpl implements UserService {</p>
<pre><code>@Override
void findById(Long userId) {
// 구현
}

@Override
void findByUsername(String username) {
// 구현
}

void findByEmail(String email) {
// 구현
}</code></pre><p>}</p>
<p>```</p>
<ul>
<li>현재 시나리오 :<ol>
<li>인터페이스 <code>UserService</code> 가 존재</li>
<li>인터페이스 구현체 <code>UserServiceImpl</code> 이 존재</li>
<li><code>@Service</code> 로 이 클래스를 <strong>스프링 빈으로 등록</strong><br>

</li>
</ol>
</li>
</ul>
<h2 id="➡️-스프링-빈-등록-구성">➡️ 스프링 빈 등록 구성</h2>
<h3 id="1️⃣-aop-미적용-시">1️⃣ AOP 미적용 시</h3>
<ul>
<li>빈이름 : <code>userServiceImpl</code></li>
<li>빈타입 : <code>UserServiceImpl</code></li>
<li>빈실체 : <code>new UserServiceImpl()</code><br></li>
<li><code>AOP</code> 미적용 시, <strong>프록시 객체</strong>가 아닌 <strong>실제 인스턴스</strong>를 <strong>스프링 빈으로 등록</strong></li>
<li>따라서 <code>@Service</code> 가 붙어있는 <code>UserserviceImpl</code> 을 대상으로 <strong>스프링 빈 등록</strong><br>

</li>
</ul>
<h3 id="2️⃣-aop-적용-시">2️⃣ AOP 적용 시</h3>
<ul>
<li>빈이름 : <code>userServiceImpl</code></li>
<li>빈타입 : <code>UserService</code></li>
<li>빈실체 : <code>UserService</code> 구현 프록시 객체<br></li>
<li>현재 <code>UserServiceImpl</code> 의 경우, <strong>인터페이스의 구현체</strong></li>
<li><strong>인터페이스 구현체</strong>에 <code>AOP</code> 를 적용하면, 반드시 <strong>JDK 동적 프록시</strong>에 의해 <strong>프록시 객체 생성</strong></li>
<li>JDK 동적 프록시는 <strong>인터페이스 대상 프록시</strong></li>
<li>따라서 <strong>빈타입</strong>과 <strong>빈실체</strong>는 <strong>인터페이스</strong> 타입으로 등록<br>

</li>
</ul>
<blockquote>
<p>🥷 <code>UserServiceImpl</code> DI <strong>🆚</strong> <code>UserService</code> DI</p>
</blockquote>
<p>1) <code>UserServiceImpl</code> DI :</p>
<ul>
<li><strong>JDK 동적 프록시</strong>는 인터페이스 주입 시에만 사용</li>
<li><code>UserServiceImpl</code> 에 대해 <code>AOP</code> 를 적용했더라도, <code>UserServiceImpl</code> 를 <code>DI</code> 받으면 <strong>실제 <code>UserServiceImpl</code> 인스턴스</strong>가 주입</li>
</ul>
<p>2) <code>UserService</code> DI:</p>
<ul>
<li><code>UserServiceImpl</code> 에 대해 <code>AOP</code> 를 적용하면, <code>UserService</code> 를 <code>DI</code> 받아서 <code>UserService</code> 내의 모든 기능은 <code>AOP</code> 가 적용</li>
<li><code>UserService</code> 인터페이스가 <strong>특정 인터페이스를 상속받고</strong>, 그 인터페이스는 <strong>다른 특정 인터페이스를 상속받는 구조</strong>라도, <code>AOP</code> 가 <code>UserServiceImpl</code> 을 대상으로 적용되면 <code>UserService</code> 내의 모든 기능에서는 <code>AOP</code> 가 적용</li>
<li>확장된 기능은 사용할 수 없음</li>
</ul>
<br>

<blockquote>
<blockquote>
<h3 id="📋-즉-jdk-동적-프록시를-사용하면-확장된-기능에-대한-aop-처리가-불가능하다">📋 즉, <strong>JDK 동적 프록시</strong>를 사용하면 확장된 기능에 대한 <code>AOP</code> 처리가 불가능하다.</h3>
</blockquote>
</blockquote>
<hr>
<h1 id="📌-cglib">📌 CGLIB</h1>
<ul>
<li><strong>클래스 기반 프록시</strong></li>
<li>인터페이스 구현체가 <strong>아닌 클래스</strong>에 <code>AOP</code> 적용 시, 해당 클래스 타입의 <strong>프록시 객체</strong>를 <strong>스프링 빈으로 등록</strong></li>
<li><code>UserService</code> 관련 동일한 시나리오를 가정:<br>

</li>
</ul>
<h2 id="➡️-스프링-빈-등록-구성-1">➡️ 스프링 빈 등록 구성</h2>
<h3 id="1️⃣-aop-미적용-시-1">1️⃣ AOP 미적용 시</h3>
<ul>
<li>빈이름 : <code>userServiceImpl</code></li>
<li>빈타입 : <code>UserServiceImpl</code></li>
<li>빈실체 : <code>new UserServiceImpl()</code><br></li>
<li><code>@Component</code> 클래스이므로, 동일하게 실제 객체 생성<br>

</li>
</ul>
<h3 id="2️⃣-aop-적용-시-1">2️⃣ AOP 적용 시</h3>
<ul>
<li>빈이름 : <code>userServiceImpl</code></li>
<li>빈타입 : <code>UserServiceImpl</code></li>
<li>빈실체 : <code>UserServiceImpl</code> 상속 프록시 객체<br></li>
<li><code>CGLIB</code> 는 <strong>빈 등록 시 모든 것을 클래스 기반으로 등록</strong></li>
<li>따라서 <code>UserServiceImpl</code> 를 <code>DI</code> 받을 수 있으므로, 해당 클래스의 모든 기능에 <code>AOP</code> 를 적용 가능<br>

</li>
</ul>
<hr>
<h1 id="🖇️-jdk-dynamic-proxy-🆚-cglib">🖇️ <code>JDK Dynamic Proxy</code> 🆚 <code>CGLIB</code></h1>
<h3 id="1️⃣-jdk-동적-프록시">1️⃣ JDK 동적 프록시</h3>
<ul>
<li><strong>인터페이스 정의 메서드만</strong> 가로챌 수 있다는 <strong>명확한 한계</strong></li>
<li><code>AOP</code> 를 구현체 대상으로 설정했을 때, <strong>인터페이스 DI</strong>는 <code>AOP</code> 가 작동할 수 있지만 <strong>구현체 DI</strong>는 <strong>프록시 객체가 아닌 실제 구현체 인스턴스</strong>가 등록되므로 <code>AOP</code> 작동 불가능<br>

</li>
</ul>
<h3 id="🔗-solid">🔗 <code>SOLID</code></h3>
<p>1) <strong><code>ISP</code> 준수</strong></p>
<ul>
<li><strong>JDK 동적 프록시</strong> 특성상, <code>AOP</code> 적용을 위해서는 <strong>인터페이스 DI</strong>를 강제</li>
</ul>
<p>2) <strong><code>OCP</code> 위반</strong></p>
<ul>
<li><strong>기능 확장</strong>의 경우, <strong>인터페이스 구현체</strong>에서 추가적으로 구현하는 것이 일반적</li>
<li>하지만 <code>AOP</code> 를 적용하기 위해서는 앞서 말했듯, <strong>인터페이스 DI</strong>가 강제<ul>
<li><strong>인터페이스 구현체</strong>에서 기능을 확장하더라도, 해당 기능에 대한 <strong><code>AOP</code> 적용 불가능</strong></li>
</ul>
</li>
<li><strong>기능 확장</strong>에 <code>AOP</code> 를 적용하기 위해서는, <strong>기존 인터페이스 수정이 필수불가결</strong><br>

</li>
</ul>
<h3 id="2️⃣-cglib">2️⃣ CGLIB</h3>
<ul>
<li><strong>클래스</strong>를 대상으로 <strong>프록시 객체 생성</strong></li>
<li><strong>구현체</strong>를 스프링 빈에 등록할 수 있으므로, <code>AOP</code> 작동에 관해서 자유로움</li>
</ul>
<h3 id="🔗-solid-1">🔗 <code>SOLID</code></h3>
<p>1) <strong><code>ISP</code> , <code>DIP</code> 위반 가능</strong></p>
<ul>
<li><code>CGLIB</code> 는 <strong>인터페이스 DI</strong>와 <strong>인터페이스 구현체 DI</strong>가 모두 가능</li>
<li><strong>인터페이스 DI</strong>의 경우에는 <strong>JDK 동적 프록시</strong>와 마찬가지로, <code>ISP</code> 와 <code>DIP</code> 를 준수</li>
<li>단, 기능 확장 후 <code>AOP</code> 적용을 위해 <strong>구현체 DI</strong>를 할 경우, <code>ISP</code> 와 <code>DIP</code> 를 위반</li>
<li>하지만 <strong>JDK 동적 프록시</strong> 사용 시 <strong>기존 인터페이스 수정 없이는 확장된 기능 <code>AOP</code> 적용 불가능</strong> 이라는 큰 단점을 해소 가능</li>
</ul>
<p>2) <strong><code>OCP</code> 준수</strong></p>
<ul>
<li><strong>기존 인터페이스 코드의 수정이 필요 없으므로</strong> 변경에는 확실히 닫혀있음</li>
<li><code>CGLIB</code> 는 <strong>프록시 객체</strong>가 <strong>구현체를 상속</strong>하기 때문에, <strong>기능 확장 후 <code>AOP</code> 적용이 얼마든지 가능</strong></li>
<li>단 이 경우 <strong>구현체 DI</strong>가 필요할 수 있으므로, <code>ISP</code> 와 <code>DIP</code> 를 위반</li>
</ul>
<blockquote>
<blockquote>
<ul>
<li><code>CGLIB</code> 가 훨씬 유연한 방식을 제공하므로, 현재 스프링 2.0 이상 버전부터는 프록시 객체 사용 시 <code>CGLIB</code> 사용이 기본값으로 변경</li>
</ul>
</blockquote>
</blockquote>
<ul>
<li><code>CGLIB</code> 를 사용함으로 얻을 수 있는 <strong>이점이 더 많다!</strong></li>
</ul>
<hr>
<blockquote>
<p>검수) Google Gemini ( <a href="https://gemini.google.com/app">https://gemini.google.com/app</a> )</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[SOLID]]></title>
            <link>https://velog.io/@riverpower6_g/SOLID</link>
            <guid>https://velog.io/@riverpower6_g/SOLID</guid>
            <pubDate>Fri, 11 Jul 2025 02:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; 스프링의 원리를 공부하던 도중, 익숙한 단어가 나왔다. <code>SOLID</code> , 좋은 객체 지향 설계의 5가지 원칙이다.
기존에도 당연히 알고 있던 개념이지만 사실상 원리를 이해하기 보다는 단순히 외우고만 있었던 개념이었으므로, 확실하게 알고 있다고는 단정할 수 없었다.
<code>SOLID</code> 는 개발자라면 <strong>반드시</strong> 알고 <strong>실무</strong>에서 제대로 사용할 수 있어야 하는 개념이다. 이번 기회에, 보다 좋은 개발자가 될 수 있도록 <code>SOLID</code> 에 대한 확실한 이해를 목표로 학습 및 정리해보았다.</p>
<hr>
<h1 id="📕-solid">📕 SOLID</h1>
<ul>
<li><strong>객체 지향 프로그래밍 및 설계</strong>에서 사용되는 <strong>5가지 기본 원칙</strong></li>
<li>소프트웨어의 <strong>유지보수성, 가독성, 확장성을 향상시키는 것</strong>이 목표
<br><br></li>
</ul>
<h2 id="📌-5가지-원칙">📌 5가지 원칙</h2>
<h3 id="1️⃣-srp-single-responsibility-principle">1️⃣ SRP (Single Responsibility Principle)</h3>
<ul>
<li><p><strong>단일 책임 원칙</strong></p>
</li>
<li><p>하나의 클래스는 <strong>반드시</strong> 하나의 책임만 가져야 함</p>
<ul>
<li><strong>책임</strong>은 <strong>하나의 기능</strong>이라기보다는, 해당 클래스가 변경되어야 할 <strong>단 하나의 이유</strong></li>
<li><strong>여러 이유로 변경될 수 있다면</strong> <code>SRP</code> 위반</li>
<li><strong>한 가지 책임에 집중</strong>할 수 있도록, 클래스를 <strong>적절히 분리해서 설계</strong>해야 한다는 원칙</li>
</ul>
</li>
<li><p><strong>책임의 기준 및 범위</strong>는 개발자마다 다를 수 있음</p>
<ul>
<li><p><code>MemberService</code> 에 <strong>회원 도메인에 대한 로직</strong>을 모아두어도 (하나의 클래스에 하나의 기능만 존재하지 않더라도), <strong>회원</strong>이라는 <strong>일관된 책임 내에서 동작</strong>하므로 <code>SRP</code> 위반이 아님</p>
<pre><code class="language-java">// SRP 위반
public class memberService {
public void join(Member member) {
    // 회원 저장
}

public void sendMail(Member member) {
    // 이메일 발송
}
}</code></pre>
</li>
</ul>
</li>
<li><p><strong>회원 저장</strong>과 <strong>이메일 발송</strong>이라는 서로 다른 두 가지 책임을 가지고 있으므로, <code>SRP</code> 위반</p>
<pre><code class="language-java">// SRP 준수
public class MemberService {
  public void join(Member member) {
      // 회원 저장
  }
}
</code></pre>
</li>
</ul>
<p>public class EmailService {
    public void sendMail(Member member) {
        // 이메일 발송
    }
}</p>
<pre><code>- `MemberService` 는 __회원 도메인__에만, `EmailService` 는 __이메일 도메인__에만 집중하므로, `SRP` 준수
&lt;br&gt;

### 2️⃣ OCP (Open Closed Principle)
- __개방-폐쇄 원칙__
- 소프트웨어는 __확장에는 열려있고 변경에는 닫혀있어야 함__
  - 기능 추가 및 변경 시 __클래스 확장__을 통해 쉽게 구현할 수 있도록 하되, __기존 클래스의 수정은 최소화__할 수 있도록 설계
  - __역할__과 __구현__의 분리
- __다형성__을 활용
  - __인터페이스__를 활용하고, 정책에 따라 다양한 __구현체__를 제공
  - __객체지향__의 장점을 __극대화__할 수 있음
- __스프링__은 `IOC` 및 `DI` 를 통해 __해당 원칙을 준수__할 수 있도록 설계되어있음
```java
// OCP 위반
public class DiscountService {
    public int getNormalDiscountRate() {
        // 할인율 로직
    }
}</code></pre><ul>
<li><code>VIP</code> , <code>GOLD</code> 등 <strong>새로운 정책 추가</strong> 시 <strong>클래스 수정 필요</strong><pre><code class="language-java">// OCP 준수
public interface DiscountPolicy{
  int getDiscountRate();
}
</code></pre>
</li>
</ul>
<p>public class VipDiscountPolicy implements DiscountPolicy {
    public int getDiscountRate() {
        // VIP 할인율 로직
    }
}</p>
<p>public class NormalDiscountPolicy implements DiscountPolicy {
    public int getDiscountRate() {
        // 일반 할인율 로직
    }
}</p>
<p>public class DiscountService {
    private final DiscountPolicy discountPolicy;</p>
<pre><code>public DiscountService(DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
}

public int discount() {
    return discountPolicy.getDiscountRate();
}</code></pre><p>}</p>
<pre><code>- 새 정책 추가 시, `DiscountPolicy` 구현체만 추가하여 주입하면 됨
&lt;br&gt;

### 3️⃣ LSP (Liskov Substitution Principle)
- __리스코프 치환 원칙__
- __하위 타입__은 언제나 __상위 타입__으로 교체할 수 있어야 함
  - __다형성__을 제대로 활용하기 위한 __원칙__
  - __상위 클래스 타입__으로 __하위 클래스 객체__를 받더라도 (업캐스팅), __기능__이 원하는대로 동작해야 한다는 의미
  - __기능적 의미를 해치는, 부적절한 오버라이딩__을 하지 않도록 설계
  - __부모__의 `sleep()` 이라는 __잠자는 의미__의 메서드를, __자식__이 __달리기__라는 의미로 __오버라이딩__하면 `LSP` 원칙을 위반
```java
// LSP 위반
class Parent {
    int[] location = {0, 0};

    void sleep() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {}
    }
}

class Child extends Parent {
    @Override
    void sleep() {
        location[0]++;
    }
}</code></pre><ul>
<li><p>부모는 <strong>쓰레드 대기</strong>의 의미로 <code>sleep()</code> 메서드를 구현했지만, 자식은 <strong>움직이기</strong>의 의미로 <code>sleep()</code> 메서드를 <strong>오버라이딩</strong></p>
<ul>
<li><p><strong>메서드의 의도와 의미</strong>가 전혀 달라지므로 <code>LSP</code> 위반</p>
<pre><code class="language-java">// LSP 준수
class Parent {
int[] location = {0, 0};

void sleep() {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {}
}
}
</code></pre>
</li>
</ul>
</li>
</ul>
<p>class Child extends Parent {
    @Override
    void sleep() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {}
    }
}</p>
<pre><code>- 부모와 자식 모두 __쓰레드 대기__의 의미로 `sleep()` 메서드를 구현
&lt;br&gt;

### 4️⃣ ISP (Interface Segregation Principle)
- __인터페이스 분리 원칙__
- __인터페이스 하나__에 많은 기능을 담기보다는, 각각의 사용처에 맞게 __여러 개로 분리__해야 함
  - `SRP` 는 __클래스 분리__의 원칙이라면, `ISP` 는 __인터페이스 분리__의 원칙
- __특정 클라이언트__를 위해 __적합한 인터페이스__만을 제공하는 것이 목표
- 인터페이스를 한 번 __구성해뒀으면__ 웬만해서는 __변경시키면 안됨__
- __인터페이스가 보다 명확해지고, 대체 가능성의 증가로 유지보수성 향상__
```java
// ISP 위반
interface Machine {
    void print();
    void scan();
    void fax();
}

class PrintMachine implements Machine {
    public void print() {
        // 프린트
    }

    public void scan() {
        throw new RuntimeException(&quot;스캔은 사용할 수 없음&quot;);
    }

    public void fax() {
        throw new RuntimeException(&quot;팩스는 사용할 수 없음&quot;);
    }
}</code></pre><ul>
<li><code>PrintMachine</code> 은 <strong>프린트</strong> 기능만 필요하지만, <code>scan()</code> , <code>fax()</code> 도 반드시 구현해야 하므로 <code>ISP</code> 위반<pre><code class="language-java">// ISP 준수
interface Printer {
  void print();
}
</code></pre>
</li>
</ul>
<p>interface Scanner {
    void scan();
}</p>
<p>class PrintMachine implements Printer {
    public void print() {
        // 프린트
    }
}</p>
<pre><code>- `PrintMachine` 은 __프린트__에 필요한 `Printer` 인터페이스만 구현하면 됨
&lt;br&gt;

### 5️⃣ DIP (Dependency Inversion Principle)
- __의존관계 역전 원칙__
- __구체화__에 의존해선 안되고, __추상화__에 의존해야 함
  - __구현 클래스__에 의존하지 말고, __인터페이스__에 의존할 것
  - __역할__에 의존하도록 설계
  - __변경되기 쉬운 것__보다 __변경되기 어려운 것__에 의존하도록 설계
- __클래스__간의 __결합도__를 낮추는 것이 목표
- __스프링__은 `IOC` 및 `DI` 를 통해 __해당 원칙을 준수__할 수 있도록 설계되어있음
```java
// DIP 위반
public class OrderService {
    private final DiscountPolicy discountPolicy = new VipDiscountPolicy();

    public int order() {
        return discountPolicy.getDiscountRate();
    }
}</code></pre><ul>
<li><p><code>VipDiscountPolicy</code> 에 의존하고 있으므로, 다른 정책으로 변경을 원하면 해당 클라이언트 코드의 <strong>수정이 필요</strong></p>
<pre><code class="language-java">// DIP 준수
public class OrderService {
  private final DiscountPolicy discountPolicy;

  public OrderService(DiscountPolicy discountPolicy) {
      this.discountPolicy = discountPolicy;
  }

  public int order() {
      return discountPolicy.getDiscountRate();
  }
}</code></pre>
</li>
<li><p>외부에서 <code>DiscountPolicy</code> 구현체를 주입하므로, 정책에 따라 자유롭게 교체할 수 있음</p>
</li>
<li><p><strong>스프링</strong>에서는 <code>@Component</code> , <code>@Autowired</code> 등의 기능을 통해 이 구조를 <strong>쉽게 구현 가능</strong></p>
</li>
</ul>
<hr>
<blockquote>
<p>참고) OpenAI. (2024).ChatGPT(4o)[Large language model].<a href="https://chatgpt.com/">https://chatgpt.com/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[서블릿]]></title>
            <link>https://velog.io/@riverpower6_g/%EC%84%9C%EB%B8%94%EB%A6%BF</link>
            <guid>https://velog.io/@riverpower6_g/%EC%84%9C%EB%B8%94%EB%A6%BF</guid>
            <pubDate>Wed, 19 Feb 2025 03:28:25 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp;스프링으로 프로그래밍을 하다보면, 디버깅할 때나 실행할 때나 지긋지긋하게 보이는 것이 있다. 바로 <code>DispatcherServlet</code> 이다.</p>
<p>&nbsp; 이토록 지겹게 마주치는 <strong>서블릿</strong>이지만, 이 서블릿이 하는 일을 설명해보라 했을 때 자세하게 설명할 수 있는 자신은 없다.
&nbsp; 물론 대강 어떤 역할을 하는지는 어렴풋이 눈치채고 있지만, 눈치껏 아는 것과 확실히 개념을 알고 이해하고 있는 것은 하늘과 땅 차이이다.</p>
<p>&nbsp; <strong>서블릿</strong>은 사실상, 웹 개발에서 떼려야 뗄 수 없는 관계이다. 그렇기 때문에 더더욱, <strong>서블릿</strong>의 역할과 하는 일들은 확실하게 이해하고 있어야 한다.
&nbsp; 그런 이유로, 금일은 서블릿에 대해서 학습하고 이를 정리하는 시간을 가져보고자 한다.</p>
<hr>
<h1 id="📑-서블릿-servlet">📑 서블릿 (Servlet)</h1>
<ul>
<li><strong>자바 기반</strong>의 <strong>웹 애플리케이션</strong>을 개발할 때 사용되는 <strong>서버 측 기술</strong></li>
<li><strong>클라이언트</strong>로부터 __ HTTP 요청<strong>을 받아 이를 처리한 후, __응답</strong>을 반환하는 역할을 하는 <strong>자바 기반의 웹 컴포넌트</strong></li>
<li><strong>웹 서버</strong>에서 실행되는 <code>Java</code> 프로그램이며, <code>Java EE</code> (= Jakatra EE)의 <strong>표준 기술</strong>
<br><br></li>
</ul>
<h2 id="🗒️-서블릿의-특징">🗒️ 서블릿의 특징</h2>
<ol>
<li><p><strong><code>Java</code> 기반</strong></p>
<ul>
<li><code>Java</code> 로 작성되므로, <strong>플랫폼 독립성</strong>이 뛰어남<br>
</li>
</ul>
</li>
<li><p><strong><code>HTTP</code> 요청 및 응답 처리</strong></p>
<ul>
<li><code>HttpServlet</code> 을 상속 받아 <code>GET</code> 이나 <code>POST</code> 같은 <code>HTTP</code> 요청을 처리<br>
</li>
</ul>
</li>
<li><p><strong>멀티스레드 환경</strong></p>
<ul>
<li><strong>서블릿 컨테이너</strong>가 <strong>1개</strong>의 서블릿 인스턴스를 여러 요청에 대해 <strong>재사용</strong></li>
<li>각 요청마다 <strong>스레드 풀</strong> 내에서 해당 서블릿의 <strong>스레드</strong>를 생성</li>
<li><code>Tomcat</code> 의 경우, 기본값으로 <strong>200</strong>개의 작업 스레드가 존재<br>
</li>
</ul>
</li>
<li><p><strong><code>MVC</code> 패턴의 컨트롤러 역할</strong></p>
<ul>
<li><code>JSP</code> 나 <strong>템플릿 엔진</strong>과 결합해 <code>View</code> 를 렌더링 후 응답으로 반환
<br><br></li>
</ul>
</li>
</ol>
<h2 id="➡️-서블릿의-동작-방식-및-생명주기">➡️ 서블릿의 동작 방식 및 생명주기</h2>
<ol>
<li><p><strong>서블릿 클래스 로드</strong></p>
<ul>
<li><strong>서블릿 컨테이너</strong>는 <strong>웹 애플리케이션이 시작</strong>될 때나 <strong>서블릿</strong>에 <strong>요청</strong>이 처음 들어왔을 때, <strong>서블릿 클래스</strong>를 메모리에 로드</li>
<li><strong>서블릿 클래스</strong>는 <code>web.xml</code> 혹은 <code>@WebServlet</code> 을 통해 <code>URL</code> 과 매핑되며, 클라이언트의 요청이 해당 서블릿에 전달될 수 있도록 설정됨<br>
</li>
</ul>
</li>
<li><p><strong>서블릿 인스턴스화</strong></p>
<ul>
<li>서블릿 클래스가 <strong>로드</strong>되면, <strong>서블릿 컨테이너</strong>는 해당 클래스로부터 <strong>서블릿 인스턴스 생성</strong></li>
<li><strong>서블릿 생성자</strong>는 <strong>단 한 번만 호출</strong>되며, 이 때 객체가 초기화됨<br>
</li>
</ul>
</li>
<li><p><strong>초기화 메서드 호출 - <code>init()</code></strong></p>
<ul>
<li>서블릿 인스턴스 생성 후, 서블릿 컨테이너는 <code>init()</code> 를 호출하여 <strong>서블릿 초기화</strong></li>
<li><strong>서블릿 초기화</strong> 시 설정 정보를 불러오거나, 자원을 할당하는 작업 등을 수행 가능</li>
<li>마찬가지로 <code>init()</code> 메서드 또한 <strong>단 한 번만 호출</strong>되며, 이 서블릿은 <strong>종료될 때까지 유지됨</strong><br>
</li>
</ul>
</li>
<li><p><strong>요청 처리 메서드 호출 - <code>service()</code></strong></p>
<ul>
<li>클라이언트가 <strong>요청</strong>을 보내면, <strong>서블릿 컨테이너</strong>는 해당 요청을 <strong>서블릿</strong>에 전달한 후 <code>service()</code> 메서드 호출</li>
<li><code>service()</code> 메서드는 클라이언트의 <strong>요청</strong>을 처리하며, <code>HTTP</code> 요청 방식에 따라 <code>doGet()</code> , <code>doPost()</code> 등 적절한 메서드 호출</li>
<li>각 메서드는 클라이언트의 <strong>요청</strong>을 처리한 후 <strong>응답</strong>을 생성<br>
</li>
</ul>
</li>
<li><p><strong>응답 반환</strong></p>
<ul>
<li><strong>서블릿</strong>은 <code>HttpServletResponse</code> <strong>(응답 객체)</strong> 를 사용하여 클라이언트에게 <strong>응답을 반환</strong><br>
</li>
</ul>
</li>
<li><p><strong>서블릿 종료 메서드 호출 - <code>distroy()</code></strong></p>
<ul>
<li><strong>웹 애플리케이션</strong> 또는 <strong>서블릿 컨테이너</strong>가 <strong>종료</strong>되거나 <strong>서블릿</strong>을 제거하려 할 때 <code>distroy()</code> 메서드 호출</li>
<li>서블릿이 <strong>종료되기 전</strong>에 <strong>정리 작업</strong> 진행</li>
<li>ex) <strong>열린 자원 닫기, 데이터 정리 ...</strong></li>
</ul>
</li>
</ol>
<hr>
<h1 id="📖-서블릿-컨테이너-와-서블릿-컨텍스트">📖 서블릿 컨테이너 와 서블릿 컨텍스트</h1>
<h2 id="1️⃣-서블릿-컨테이너-servlet-container">1️⃣ 서블릿 컨테이너 (Servlet Container)</h2>
<ul>
<li>서블릿을 <strong>실행하고 관리하는 환경</strong></li>
<li><code>Apache Tomcat</code> , <code>Jetty</code> , <code>WildFly</code> , <code>GlassFish</code> 등의 <strong>웹 애플리케이션 서버(WAS)</strong> 안에 <strong>포함</strong>되어 있음<ul>
<li><code>WAS</code> 는 <strong>서블릿 컨테이너</strong>의 역할 뿐만 아니라, <strong><code>JSP</code> 실행, 트랜잭션 관리, 데이터 소스 관리</strong> 등의 기능 또한 제공</li>
<li>즉, <strong>서블릿 컨테이너 + α</strong></li>
<li><code>Spring MVC</code> 같은 프레임워크도 실행할 수 있도록 지원</li>
</ul>
</li>
<li>위와 같은 <code>WAS</code> 내에서 서블릿을 실행할 수 있도록 도와주는 역할</li>
<li>단독으로 실행될 수도 있음<br>

</li>
</ul>
<h3 id="🦾-서블릿-컨테이너의-역할">🦾 서블릿 컨테이너의 역할</h3>
<ul>
<li>서블릿의 <strong>생명주기 관리</strong></li>
<li><code>HTTP</code> 요청 및 응답 처리</li>
<li><strong>멀티스레드</strong> 지원</li>
<li><strong>세션 및 쿠키</strong> 관리</li>
<li><code>JSP</code> 컴파일 및 실행</li>
<li><strong>서블릿 컨텍스트</strong> 제공
<br><br></li>
</ul>
<h2 id="2️⃣-서블릿-컨텍스트-servlet-context">2️⃣ 서블릿 컨텍스트 (Servlet Context)</h2>
<ul>
<li><strong>웹 애플리케이션 전체에서 공유</strong>되는 정보와 리소스를 관리하는 <strong>객체</strong></li>
<li><strong>애플리케이션 전역</strong>에서 사용되는 <strong>설정 정보</strong>나 <strong>공유 자원</strong>을 제공하는 역할</li>
<li><code>getServletContext()</code> 로 접근 가능<br>

</li>
</ul>
<h3 id="🦾-서블릿-컨텍스트의-역할">🦾 서블릿 컨텍스트의 역할</h3>
<ul>
<li><strong>애플리케이션 전역</strong>에서 <strong>공유</strong>할 데이터 저장<ul>
<li><code>setAttribute()</code> , <code>getAttribute()</code> ...</li>
</ul>
</li>
<li><strong>초기화 파라미터</strong> 관리<ul>
<li><code>web.xml</code> 에서 설정한 파라미터</li>
</ul>
</li>
<li><strong>웹 애플리케이션</strong> 내의 리소스 접근<ul>
<li><code>getResourceAsStream()</code></li>
</ul>
</li>
<li><strong>로깅</strong> 지원<ul>
<li><code>log()</code></li>
</ul>
</li>
<li><strong>다른 서블릿</strong>과의 <strong>통신</strong><ul>
<li><code>getRequestDispatcher()</code></li>
</ul>
</li>
</ul>
<hr>
<h1 id="⭐-dispatcherservlet">⭐ DispatcherServlet</h1>
<ul>
<li><code>Spring MVC</code> , 즉 <code>Spring</code> 프레임워크에서 사용되는 서블릿</li>
<li><code>Spring Boot</code> 와 함께 가장 많이 사용됨</li>
<li><strong>서블릿 컨테이너</strong>는 모든 <code>HTTP</code> 요청을 <code>DispatcherServlet</code> 으로 전달</li>
<li>하나의 <code>DispatcherServlet</code> 이 <strong>여러 컨트롤러</strong>의 <strong>액션 메서드</strong>를 <strong>라우팅</strong>하여 실행</li>
</ul>
<pre><code class="language-java">@RestController
@RequireMapping(&quot;/api/hotels&quot;)
public class HotelController {
    @GetMapping
    public List&lt;Hotel&gt; getHotels() {
        // ...
    }
}</code></pre>
<ul>
<li><code>/api/hotels</code> 경로로 요청이 오면 <code>DispatcherServlet</code> 이 <code>HandlerMapping</code> 을 통해 해당 컨트롤러의 <code>getHotels()</code> 메서드 호출
<br><br></li>
</ul>
<h2 id="➡️-spring-mvc-에서의-서블릿-실행-흐름">➡️ <code>Spring MVC</code> 에서의 <strong>서블릿 실행 흐름</strong></h2>
<ol>
<li><strong>클라이언트가 <code>HTTP</code> 요청 보냄</strong><br></li>
<li><strong>서블릿 컨테이너가 요청을 받음</strong><ul>
<li><code>web.xml</code> 또는 <code>Spring Boot</code> <strong>설정</strong>을 통해 <code>DispatcherServlet</code> 이 요청을 받도록 설정</li>
<li><code>Spring Boot</code> 는 별도의 설정없이 자동으로 <code>DispatcherServlet</code> 을 등록</li>
<li><strong>서블릿 컨테이너</strong>는 요청을 <code>DispatcherServlet</code> 으로 전달<br></li>
</ul>
</li>
<li><strong><code>DispatcherServlet</code> 이 요청을 분석하고 핸들러(Controller)를 찾음</strong><ul>
<li><code>HandlerMapping</code> 을 사용하여, <strong>어떤 컨트롤러 및 메서드</strong>가 <strong>요청을 처리</strong>할지 결정<br></li>
</ul>
</li>
<li><strong>액션 메서드를 실행하고 결과를 반환</strong><ul>
<li><code>HandlerAdapter</code> 가 <code>HandlerMapping</code> 이 찾아낸 <strong>컨트롤러의 액션 메서드</strong>를 실행한 후 결과를 반환<br></li>
</ul>
</li>
<li><strong><code>DispatcherServlet</code> 이 <code>View</code> 를 선택하거나 <code>JSON</code> 응답을 반환</strong><ul>
<li><code>ViewResolver</code> 를 사용해 적절한 <code>JSP</code> , <code>Thymeleaf</code> 같은 <code>View</code> 를 선택</li>
<li>또는, <code>JSON</code> 응답 반환</li>
</ul>
</li>
</ol>
<blockquote>
<p>💡 <code>HandlerMapping</code> 과 <code>HandlerAdapter</code></p>
</blockquote>
<pre><code>- Spring MVC에서 요청을 적절한 컨트롤러로 연결하고 실행하는 핵심 컴포넌트
- DispatcherServlet이 서블릿 컨테이너 내에서 요청을 처리할 때 사용하는 핵심 인터페이스</code></pre><h3 id="1️⃣-handlermapping">1️⃣ HandlerMapping</h3>
<ul>
<li><strong>요청 URL</strong>을 보고, 해당 요청을 <strong>처리할 컨트롤러(핸들러)</strong>를 찾아서 <strong>라우팅</strong>해주는 역할<br>

</li>
</ul>
<h3 id="2️⃣-handleradapter">2️⃣ HandlerAdapter</h3>
<ul>
<li><code>HandlerMapping</code> 이 찾아낸 <strong>컨트롤러의 액션 메서드</strong>를 실제로 실행하는 역할</li>
<li><strong>컨트롤러의 타입 ( <code>@RequestMapping</code> , <code>@RestController</code> ,  <code>@Controller</code> )</strong>를 확인하고, 적절한 방식으로 호출</li>
</ul>
<blockquote>
<p>🤔 <code>HandlerMapping</code> 과 <code>HandlerAdapter</code> 가 없으면?</p>
</blockquote>
<ul>
<li><code>DispatcherServlet</code> 은 컨트롤러로 연결할 방법을 알 수 없음<ul>
<li><code>HTTP</code> 요청이 오더라도,  이를 어떤 컨트롤러가 처리해야 할지 판단 불가</li>
</ul>
</li>
<li>만약 컨트롤러를 찾더라도, 이를 실행할 방법을 알지못함<br>


</li>
</ul>
<pre><code>- 즉, HandlerMapping 과 HandlerAdapter 는 DispatcherServlet 이 요청을 적절한 컨트롤러의 메서드로 연결하고 실행하는 핵심 역할을 함</code></pre><hr>
<blockquote>
<p>참고) OpenAI. (2024).ChatGPT(4o)[Large language model].<a href="https://chatgpt.com/">https://chatgpt.com/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Filter]]></title>
            <link>https://velog.io/@riverpower6_g/Filter</link>
            <guid>https://velog.io/@riverpower6_g/Filter</guid>
            <pubDate>Tue, 18 Feb 2025 02:29:40 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; 스프링 시큐리티를 복습하던 도중, &#39;추후 학습해야지&#39; 하고 넘어갔던 부분을 발견했다.</p>
<p>&nbsp;스프링 시큐리티에서 <code>CustomFilter</code> 를 생성할 때, <code>OncePerRequestFilter</code> 라는 것을 상속받는다.
&nbsp; 이름만 보고도 요청마다 단 <strong>한 번만</strong> 실행되는 필터라는 것은 알 수 있을 것이다.
&nbsp; 하지만 이 필터가 언제 주로 사용되는 것이고, 정확히 어떤 것인지는 확실히 알지 못한다.</p>
<p>&nbsp; 이번 기회에 이전에 넘겼던 이 <code>OncePerRequestFilter</code> 를 메인으로, 필터에는 어떤 것들이 있고 어떻게 주로 사용되는지 등을 정리해보는 시간을 가져보고자 이 포스팅을 작성하게 되었다.</p>
<hr>
<h1 id="📑-onceperrequestfilter">📑 OncePerRequestFilter</h1>
<ul>
<li><code>Spring</code> 에서 제공하는 <code>Filter</code> 로, <strong>한 요청 당 한 번만 실행되는 것을 보장</strong>하는 필터<ul>
<li>기본적으로 <strong>서블릿 필터</strong>를 직접 구현하면, 하나의 요청이 <strong>여러 번</strong> 필터 체인을 통과할 수도 있음</li>
<li><code>OnceperRequestFilter</code> 의 경우 이러한 것을 미연에 방지하고, <strong>한 요청에 대해 딱 한 번만</strong> 실행되도록 자동으로 처리</li>
</ul>
</li>
<li>내부적으로 <code>isAsyncDispatch()</code> 와 <code>shouldNotFilter()</code> 를 활용하여 <strong>비동기 요청</strong> 이나 <strong>특정 조건에 따른 필터 실행 제외</strong> 도 가능</li>
</ul>
<pre><code class="language-java">public abstract class OncePerRequestFilter implements Filter {
    @Override
    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 비동기 요청은 필터를 실행하지 않도록 설정
        if (isAsyncDispatch(httpRequest)) {
            filterchain.doFilter(request, response);
            return;
        }

        doFilterInternal(httpRequest, httpResponse, filterChain);
    }
}</code></pre>
<ul>
<li><p><strong>인증</strong>과 <strong>인가</strong>의 경우, <strong>요청 당 한 번</strong>만 실행되면 되기 때문에(ex: JWT 토큰 검사), <strong>인증, 인가</strong>에서 주로 사용</p>
<pre><code class="language-java">@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
      String token = request.getHeader(&quot;Authorization&quot;);

      if (token != null &amp;&amp; token.startsWith(&quot;Bearer &quot;)) {
          String jwt = token.substring(7);
      }

      // jwt 활용

      filterChain.doFilter(request, response);
  }
}</code></pre>
</li>
<li><p>모든 요청에 대한 <strong>한 번의 로깅</strong>만 필요한 경우도 사용</p>
<pre><code class="language-java">@Component
@Slf4j
public class LoggingFilter extends OncePerRequestFilter {
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
      log.info(&quot;필터 로그&quot;);
      filterChain.doFilter(request, response);
  }
}</code></pre>
</li>
<li><p>특정 요청에 대해서 <strong>헤더</strong> 변경 시에도 사용</p>
<pre><code class="language-java">@Component
public class CustomHeaderFilter extends OncePerRequestFilter {
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
      response.setHeader(&quot;newHeader&quot;, &quot;header_value&quot;)
      filterChain.doFilter(request, response);
  }
}</code></pre>
</li>
<li><p><strong>CORS (Cross-Origin Resource Sharing) 필터</strong> 로도 사용이 가능</p>
</li>
<li><p>다른 도메인(출처)에서 요청이 올 경우, 이에 따른 <code>CORS</code> 규칙을 적용</p>
</li>
<li><p>이러한 경우에도 모든 요청에 대해서 <strong>한 번만</strong> 적용하면 되므로, <code>OncePerRequestFilter</code> 사용</p>
<pre><code class="language-java">@Component
public class CustomCorsFilter extends OncePerRequestFilter {
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
      response.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;);
      response.setHeader(&quot;Access-Control-Allow-Methods&quot;, &quot;GET&quot;);
      response.setHeader(&quot;Access-Control-Allow-Headers&quot;, &quot;Authorization, Content-Type&quot;);

      filterChain.doFilter(request, response);
  }
}</code></pre>
<p>  모든 <code>GET</code> 요청에 대한 <code>CORS</code> 정책 사용</p>
</li>
</ul>
<hr>
<h1 id="🤔-onceperrequestfilter-를-사용하는-이유">🤔 OncePerRequestFilter 를 사용하는 이유</h1>
<br>

<h3 id="1-중복-실행-방지">1. 중복 실행 방지</h3>
<ul>
<li>앞서 언급했듯, <strong>스프링 시큐리티 필터 체인</strong>은 다양한 필터를 거치며 요청을 처리하는데, 같은 요청이 여러 번 <strong>동일한 필터</strong>를 통과할 수도 있음</li>
<li><code>RequestDispatcher.forward()</code> 나 <code>include()</code> 메서드를 사용하면 같은 요청이 필터를 다시 탈 수 있음</li>
<li>이러한 중복 실행을 방지하고자 <code>OncePerRequestFilter</code> 를 사용</li>
</ul>
<h3 id="2-요청-스레드의-일관성-유지">2. 요청 스레드의 일관성 유지</h3>
<ul>
<li>필터 내부에서 <code>SecurityContext</code> 같은 요청 관련 정보를 설정하고 사용하게 되면, 필터가 여러 번 실행될 경우 정보가 <strong>덮어씌워</strong>질 수도 있음</li>
<li><code>OncePerRequestFilter</code> 를 사용하면 필터가 단 <strong>한 번만</strong> 실행되므로 <strong>정보를 일관되게 유지</strong>할 수 있음</li>
</ul>
<h3 id="3-스프링-시큐리티">3. 스프링 시큐리티</h3>
<ul>
<li>보완 관련 필터를 만들 때 가장 많이 사용</li>
<li><code>UsernamePasswordAuthenticationFilter</code> 같은 기본 필터들이 내부적으로 <code>OncePerRequestFilter</code> 를 상속받아 사용</li>
</ul>
<hr>
<h1 id="⭐-필터에서-자주-쓰이는-기능">⭐ 필터에서 자주 쓰이는 기능</h1>
<h3 id="1-forward">1. forward</h3>
<ul>
<li><strong>현재 요청</strong>을 <strong>새로운 URL</strong>로 이동시킴</li>
<li>단, 클라이언트도 이동 사실을 모르게 실행</li>
<li>요청을 다른 <code>Servlet</code> 및  <code>JSP</code> 로 전달할 때 사용</li>
<li>새로운 요청을 <strong>생성하지 않음</strong> (기존 요청 객체 유지)</li>
<li><strong>클라이언트</strong>는 URL이 바뀌지 않음<pre><code class="language-java">request.getRequestDispatcher(&quot;/&quot;).forward(request, response);</code></pre>
<br>

</li>
</ul>
<h3 id="2-redirect">2. redirect</h3>
<ul>
<li><strong>현재 요청</strong>을 <strong>클라이언트</strong>에게 <strong>새로운 URL</strong>로 이동하라고 응답</li>
<li>클라이언트는 이를 통해 <strong>새로운 요청</strong>을 보냄</li>
<li>따라서 <strong>새로운 요청</strong>이 생성됨</li>
<li><strong>클라이언트</strong>가 <strong>새로운 URL</strong>을 요청하도록 유도 (브라우저의 URL도 변경)</li>
<li>일반적으로 <code>POST</code> 요청 후 <code>redirect</code> 를 통해 <code>GET</code> 요청으로 반환</li>
</ul>
<pre><code class="language-java">response.sendRedirect(&quot;/&quot;);</code></pre>
<br>

<h3 id="3-include">3. include</h3>
<ul>
<li><strong>현재 요청</strong>을 처리하는 동안 <strong>다른 서블릿 또는 JSP의 출력을 포함</strong></li>
<li><strong>공통 헤더, 푸터</strong>를 <strong>여러 페이지</strong>에서 포함할 때 주로 사용</li>
<li><strong>기존 요청</strong>을 유지하면서 <strong>일부 응답 포함</strong></li>
<li><code>forward</code> 와의 차이점은, 기존 페이지 <strong>일부</strong>에 다른 서블릿 내용을 <strong>포함</strong>한다는 점<pre><code class="language-java">RequestDispatcher dispatcher = request.getRequestDispatcher(&quot;/header.jsp&quot;);
dispatcher.include(request, response);</code></pre>
<br>

</li>
</ul>
<h3 id="4-setattribute">4. setAttribute</h3>
<ul>
<li>요청에 <strong>데이터를 저장</strong>하여 다른 서블릿/JSP에서 사용할 수 있게 만듦</li>
<li><code>request.setAttribute()</code> 로 저장한 데이터는 <strong>요청이 유지되는 동안</strong> 사용 가능</li>
<li><code>forward</code> 와 함께 사용하면 유용함<pre><code class="language-java">request.setAttribute(&quot;userRole&quot;, &quot;ADMIN&quot;);</code></pre>
</li>
</ul>
<hr>
<h1 id="📖-그-외-필터의-종류">📖 그 외 필터의 종류</h1>
<h3 id="1-genericfilterbean">1. GenericFilterBean</h3>
<ul>
<li>스프링에서 제공하는 <strong>일반적인</strong> 필터</li>
<li>스프링의 <code>Bean</code> 으로 등록되어 <code>DI</code> 를 쉽게 가능</li>
<li><strong>요청당 한 번 실행을 보장하지 않음</strong></li>
<li>필요하다면 <strong>직접</strong> 실행 횟수 조절이 필요<pre><code class="language-java">@Component
public class CustomFilter extends GenericFilterBean {
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
      filterChain.doFilter(request, response);
  }
}</code></pre>
<br>

</li>
</ul>
<h3 id="2-characterencodingfilter">2. CharacterEncodingFilter</h3>
<ul>
<li><p>요청과 응답의 <strong>문자 인코딩</strong>을 설정하는 필터</p>
</li>
<li><p>일반적으로 <code>UTF-8</code> 인코딩을 적용할 때 사용</p>
</li>
<li><p>웹 애플리케이션이 해당 인코딩을 <strong>강제하도록</strong> 설정 가능</p>
<pre><code class="language-java">@Bean
public FilterRegistrationBean&lt;CharacterEncodingFilter&gt; encodiginFilter() {
  CharacterEncodingFilter filter = new CharacterEncodingFilter();
  filter.setEncoding(&quot;UTF-8&quot;);
  filter.setForceEncoding(true);

  FilterRegistrationBean&lt;CharacterEncodingFilter&gt; registrationBean = new FilterRegistrationBean&lt;&gt;();
  registrationBean.setFilter(filter);
  return registrationBean;
}</code></pre>
<br>

</li>
</ul>
<h3 id="3-corsfilter">3. CorsFilter</h3>
<ul>
<li><p><code>CORS</code> 설정을 담당하는 필터</p>
</li>
<li><p>다른 도메인의 요청을 허용할지 결정</p>
</li>
<li><p><code>@CrossOrigin</code> 과 함께 사용 가능</p>
</li>
<li><p>특정 도메인에서만 요청을 허용하도록 설정도 가능</p>
<pre><code class="language-java">@Bean
public CorsFilter corsFilter() {
  UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  CorsConfiguration config = new CorsConfiguration();

  config.setAllowCredentials(true);
  config.addAllowedOrigin(&quot;*&quot;)    // 모든 도메인에 대한 요청 허용
  config.addAllowedMethod(&quot;*&quot;)    // 모든 HTTP 메서드 요청 허용
  config.addAllowedHeader(&quot;*&quot;)    // 모든 헤더 요청 허용

  source.registerCorsConfiguration(&quot;/**&quot;, config);
  return new CorsFilter(source);
}</code></pre>
<br>

</li>
</ul>
<h3 id="4-hiddenhttpmethodfilter">4. HiddenHttpMethodFilter</h3>
<ul>
<li><code>PUT</code> , <code>DELETE</code> <strong>HTTP 메서드</strong>를 <code>POST</code> 요청에서 사용할 수 있게 해줌</li>
<li><code>&lt;form&gt;</code> 태그는 기본적으로 <code>GET</code> 과 <code>POST</code> 만 지원하므로, 이를 해결할 때 유용</li>
</ul>
<hr>
<h1 id="📚-dofilter와-dofilterinternal">📚 doFilter()와 doFilterInternal()</h1>
<h3 id="1-dofilter">1. doFilter()</h3>
<ul>
<li><code>javax.servlet.Filter</code> 인터페이스에서 제공하는 기본 메서드</li>
<li>모든 <strong>서블릿 필터</strong>는 <code>doFilter()</code> 의 <strong>직접 구현</strong>이 필요</li>
<li>요청 시 <code>doFilter()</code> 가 실행되고, 다음 필터로 전달하기 위해서는 <code>filterChain.doFilter(request, response)</code> 를 호출해야 함</li>
<li>즉, <code>Filter</code> 인터페이스를 <strong>직접 구현</strong>하는 경우에는 <code>doFilter()</code> 메서드를 <strong>반드시</strong> 오버라이드 해야 함</li>
</ul>
<br>

<h3 id="2-dofilterinternal">2. doFilterInternal()</h3>
<ul>
<li><code>OncePerRequestFilter</code> 에서 제공하는 <strong>추상 메서드</strong></li>
<li><code>doFilter()</code> 의 내부에서 <code>doFilterInternal()</code> 이 호출</li>
<li>따라서, 개발자는 <code>doFilter()</code> 대신 <code>doFilterInternal()</code> 만 구현하면 됨</li>
<li><code>doFilter()</code> 는 <code>OncePerRequestFilter</code> 가 자동으로 관리하며, 요청이 <strong>여러 번</strong> 필터 체인을 통과하는 것을 <strong>미연에 방지</strong><br>

</li>
</ul>
<h2 id="✒️-정리">✒️ 정리</h2>
<ul>
<li>일반적인 <code>Filter</code> 인터페이스에서는 <code>doFilter()</code> 를 구현</li>
<li><code>OncePerRequestFilter</code> 를 상속받는다면, 대신 <code>doFilterInternal()</code> 만 구현</li>
</ul>
<hr>
<blockquote>
<p>참고) OpenAI. (2024).ChatGPT(4o)[Large language model].<a href="https://chatgpt.com/">https://chatgpt.com/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[N+1]]></title>
            <link>https://velog.io/@riverpower6_g/N1</link>
            <guid>https://velog.io/@riverpower6_g/N1</guid>
            <pubDate>Fri, 14 Feb 2025 07:17:50 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp;3주간의 프로젝트가 종료되었다. 이번 프로젝트는 스스로 굉장히 얻은 게 많은 프로젝트였다.
이후 프로젝트 회고에서 다룰 예정이지만, 어렵고 힘든 일들도 많았으나 결과적으로 어느정도 괜찮은 퀄리티의 결과물을 뽑아낸 듯하여 조금 마음이 놓인다.</p>
<p>&amp;nbsp 이번 우리 팀 프로젝트 주제는 호텔 예약이다. 이 프로젝트에서 필자는 호텔 및 객실 도메인의 작업을 맡아서 진행하였다.
그러다보니 가장 많이 생각해본 부분이 바로 성능이다. 호텔과 객실이라는 굉장히 많은 양의 데이터들을 어떻게 하면 좀 더 효과적으로 다룰 수 있을까.</p>
<p>&nbsp; 지금 프로젝트에는 <code>default_batch_fetch_size</code> 가 적용되어있다.
처음에는 이거 하나로 <code>N+1</code> 문제를 전부 해결할 수 소리를 듣고 더욱 최적화할 수 있는 쿼리 생성에 집중했지만, 강사님과의 Q&amp;A 이후로 <strong>배치</strong>를 적용하기로 결정했다.</p>
<p> &amp;nbsp 추가로, <code>1:N</code> 의 데이터를 가져오기 위해서 어떤 방법이 가장 좋을까에 대해서도 많은 고민이 있었다.
 <code>JOIN</code> 을 여러 번 사용하는 것과 <code>Batch Fetching</code> 중 어떤 것이 더 좋은 성능을 발휘할 수 있을까.
 <code>DB</code>에서 일부 데이터만 가져오고 내부 메모리에서 변환 과정을 거치는 것이 좋을까, <code>DB</code> 에서 모든 작업을 끝내고 가져오는 것이 더 좋을까.
 지금 이 프로젝트에는 어떤 것이 더 좋을까.</p>
<p> &amp;nbsp 현재는 호텔의 정보를 가져올 때 호텔 이미지의 <strong>썸네일</strong>을 가져오기 위해, <strong>이미지 DB</strong>의 첫번째 값을 가져오기 위해서 조건문에 서브쿼리를 추가하였다.
  지금 당장은 싱글 서버의 소규모 데이터셋을 가지고 진행하는 프로젝트이기 때문에 이러한 방법을 적용했지만, 지금의 이러한 방식은 이후 데이터가 늘어남에 따라 성능저하를 일으킬 수 있는 원인이 되기에 수정해야할 부분이다.</p>
<p>&amp;nbsp이렇듯, 어떻게 보면 간단한 <code>CRUD</code> 작업일지라도 성능에 대한 고려를 하다보니 생각보다 많은 고민이 필요했다.</p>
<p>&amp;nbsp앞으로 수없이 직면할 이러한 문제들을 더욱 더 효과적으로 해결하기 위해, <code>N+1</code> 에 대한 확실한 학습이 필요하다.
프로젝트가 끝난 기념으로 <code>N+1</code> 문제에 대해서 정리해보고자 한다.</p>
<hr>
<h1 id="📖-n1-문제">📖 N+1 문제</h1>
<ul>
<li><code>ORM</code> 환경에서 발생하는 성능 문제로, <strong>1개</strong>의 쿼리를 실행할 때 추가적으로 <strong>N개</strong>의 쿼리가 발생하는 현상</li>
<li>주로 <code>JPA</code> 와 같은 <code>ORM</code> 에서, 연관된 데이터를 <code>Lazy Loading</code> 으로 가져올 때 발생</li>
</ul>
<pre><code class="language-java">@Entity
@Builder
public class Hotel {
    @Id
    @GeneratedValue(strategy = GenerationType.Identity) // 이후 생략
    private Long id;

    @Column
    private String hotelName;

    @OneToMany(mappedBy = &quot;hotel&quot;)
    @Builder.Default
    private List&lt;Room&gt; rooms = new ArrayList&lt;&gt;();
}

@Entity
public class Room {
    // ...

    private String roomName;

    @ManyToOne
    private Hotel hotel;
}</code></pre>
<ul>
<li>알다시피, <code>@OneToMany</code> 의 기본 <code>FetchType</code> 은 <code>Lazy</code></li>
<li><code>hotelRepository.findAll()</code> 로 전체 <code>Hotel</code> 의 정보를 가져올 시<ul>
<li>여기서 <code>SELECT</code> 쿼리가 <strong>1번</strong> 발생</li>
</ul>
</li>
<li><code>Hotel</code> 에는 현재 실제 <code>Room</code> 들의 값이 들어있는 것이 아닌, 해당 <code>Room</code> 들의 주소값을 포함하고 있는 <code>Proxy</code> 객체를 갖고 있음</li>
<li>이후 <code>Room</code> 의 정보를 얻으려고 할 시<ul>
<li><code>Room</code> 에 대한 <code>SELECT</code> 쿼리를 여기서 다시 실행</li>
</ul>
</li>
</ul>
<blockquote>
<p>결과적으로 <code>Hotel</code> 을 가져올 때 1번, <code>Room</code> 의 정보를 가져올 때 <code>N</code> 번, 총 <code>N+1</code> 번의 쿼리가 실행됨</p>
</blockquote>
<ul>
<li><p>쿼리의 총 개수가 적다면 크게 문제가 되지 않을 수 있지만, 큰 규모에서는 이렇게 추가로 생성된 쿼리로 인해 문제가 생길 가능성이 높음</p>
</li>
<li><p>즉, 성능에 큰 영향을 미칠 수 있는 아주 중요한 부분</p>
</li>
</ul>
<hr>
<h1 id="💡-n1-문제-해결-방법">💡 N+1 문제 해결 방법</h1>
<h2 id="1️⃣-fetch-join">1️⃣. Fetch Join</h2>
<ul>
<li><code>1:N</code> 연관관계에서 연관된 엔티티를 <strong>한 번에 조회</strong>할 때 사용</li>
<li>필요할 데이터들을 모두 <strong>한 번에</strong> 가져올 수 있음</li>
<li><code>N+1</code> 을 해결하기 위한 가장 좋은 방법</li>
<li>단, <strong>페이징</strong>이 되지 않는다는 단점이 있음</li>
<li>또한 <code>Repsitory</code> 에서 한 번에 <code>DTO</code> 로 반환 시 사용할 수 없음<ul>
<li>이는 <code>Fetch Join</code> 방식은 <strong>엔티티 그래프</strong>를 한 번에 로딩하는 방식이기 때문</li>
<li><strong>DTO 프로젝션</strong>은 <strong>엔티티</strong>를 가져오는 게 아닌, <strong>특정 필드</strong>만 가져오는 방식</li>
<li><code>Fetch Join</code> 은 <strong>엔티티 전쳬</strong>를 조회해야 동작하므로, <code>DTO</code> 와 함께 사용 불가능<br>
```java
@Query("SELECT h FROM Hotel h JOIN FETCH h.rooms")
List<Hotel> findAllHotels();
```</li>
</ul>
</li>
<li>실제 쿼리가 실행될 때 <code>JOIN Room r ON h.id = r.hotel_id</code> 가 발생
<br><br></li>
</ul>
<h2 id="2️⃣-batch">2️⃣. Batch</h2>
<ol>
<li><p><code>@BatchSize</code> 를 통한 <strong>배치 사이즈</strong> 조정</p>
<pre><code class="language-java">@OneToMany(mappedBy = &quot;hotel&quot;)
@BatchSize(size = 50)
private List&lt;Room&gt; rooms;</code></pre>
<br>
</li>
<li><p><code>Global</code> 설정을 통한 <strong>배치 사이즈</strong> 조정</p>
</li>
</ol>
<pre><code class="language-yaml">spring:
  jpa:
    properties:
      hibernate.default_batch_fetch_size: 50</code></pre>
<ul>
<li>배치 사이즈를 50으로 고정</li>
<li><code>SELECT * FROM  Room WHERE room_id IN (1, 2, 3.... 49, 50)</code> 같이 실행 ( <code>IN</code> 이 사용됨)</li>
<li>한 번 <code>Hotel</code> 의 데이터를 가져올 때 <strong>50개의</strong> <code>Room</code> 데이터를 함께 가져옴</li>
<li><strong>50</strong>개 이후의 데이터를 가져올 때는 또 다시 <code>N+1</code> 문제가 발생
<br><br></li>
</ul>
<h2 id="3️⃣-entitygraph">3️⃣. @EntityGraph</h2>
<ul>
<li><code>Fetch Join</code> 을 사용하지 않는 방법</li>
<li>복잡한 경우 한계가 존재<pre><code class="language-java">@EntityGraph(attributePaths = {&quot;Room&quot;})
@Query(&quot;SELECT h FROM Hotel h JOIN FETCH h.rooms&quot;)
List&lt;Hotel&gt; findAllHotels();</code></pre>
</li>
</ul>
<hr>
<h1 id="📑-여러-개의-onetomany">📑 여러 개의 @OneToMany</h1>
<ul>
<li>하나의 <code>Entity</code> 에 여러개의 <code>@OneToMany</code> 가 달려있는 경우도 있음</li>
<li>이러한 경우에 가장 좋은 방법은 크게 <strong>2가지</strong><br>

</li>
</ul>
<h2 id="1️⃣-batchsize-조정">1️⃣ BatchSize 조정</h2>
<pre><code class="language-java">@OneToMany(mappedBy = &quot;hotel&quot;)
@BatchSize(size = 50)
private List&lt;Room&gt; rooms;

@OneToMany(mappedBy = &quot;hotel&quot;)
@BatchSize(size = 50)
private List&lt;Review&gt; reviews;</code></pre>
<p>혹은</p>
<pre><code class="language-yaml">spring:
  jpa:
    properties:
      hibernate.default_batch_fetch_size: 50</code></pre>
<ul>
<li>마찬가지로 <strong>BatchSize</strong> 조정을 통해 한 번에 여러 개의 데이터를 가져오는 방법<br>

</li>
</ul>
<h2 id="2️⃣-쿼리-분리">2️⃣ 쿼리 분리</h2>
<ul>
<li><code>@OneToMany</code> 관계는 따로 조회하고, 이후에 어플리케이션 레벨에서 조립하는 방식<br></li>
</ul>
<ol>
<li>기존 프로젝트에서 사용한 쿼리</li>
</ol>
<pre><code class="language-java">@Query(&quot;&quot;&quot;
    SELECT new com.ll.hotel.domain.hotel.hotel.dto.HotelWithImageDto(h, i)
    FROM Hotel h
    LEFT JOIN Image i
    ON i.referenceId = h.id
    AND i.imageType = :imageType
    WHERE h.hotelStatus &lt;&gt; &#39;UNAVAILABLE&#39;
    AND (i.createdAt = (
        SELECT MIN(i2.createdAt)
        FROM Image i2
        WHERE i2.referenceId = h.id
        AND i2.imageType = :imageType
        )
    OR i is NULL)
    AND h.streetAddress LIKE %:streetAddress%
    &quot;&quot;&quot;)
Page&lt;HotelWithImageDto&gt; findAllHotels(@Param(&quot;imageType&quot;) ImageType imageType,
                                          @Param(&quot;streetAddress&quot;) String streetAddress, PageRequest pageRequest);</code></pre>
<ul>
<li><code>Image</code> 테이블의 <strong>이미지 타입</strong>이 <strong>호텔</strong>인 이미지들 중 해당하는 첫 번째 이미지(썸네일)만 가져오기 위한 서브쿼리</li>
<li>이 쿼리는 현재는 <strong>데이터셋</strong>이 작기 때문에 큰 문제가 없음</li>
<li>하지만 이후 데이터가 커지면 매 호텔마다 <strong>서브쿼리</strong>가 발생하기 때문에 성능 문제가 생길 수 있음<br>
</li>
</ul>
<ol start="2">
<li>이를 분리한 쿼리<pre><code class="language-java">@Query(&quot;&quot;&quot;
 SELECT h
 FROM Hotel h
 WHERE h.hotelStatus &lt;&gt; &#39;UNAVAILABLE&#39;
 AND h.streetAddress LIKE %:streetAddress%
 &quot;&quot;&quot;)
Page&lt;Hotel&gt; findHotels(@Param(&quot;streetAddress&quot;) String streetAddress, Pageable pageable);
</code></pre>
</li>
</ol>
<p>@Query(&quot;&quot;&quot;
    SELECT i
    FROM Image i
    WHERE i.imageType = :imageType
    AND i.createdAt IN (
        SELECT MIN(i2.createdAt)
        FROM Image i2
        WHERE i2.referenceId IN :hotelIds
        AND i2.imageType = :imageType
        GROUP BY i2.referenceId
        )
    &quot;&quot;&quot;)
List<Image> findFirstImages(@Param(&quot;imageType&quot;) ImageType imageType, @Param(&quot;hotelIds&quot;) List<Long> hotelIds);</p>
<p>```</p>
<ul>
<li>조건에 맞는 호텔들을 먼저 조회</li>
<li>해당하는 호텔의 <code>ID</code> 를 사용하여, 해당 호텔들의 첫 번째 이미지들만 <strong>배치</strong>로 조회</li>
<li>이후 이 두 결과를 <strong>애플리케이션 레벨</strong>에서 매칭하여 <code>HotelWithImageDto</code> 로 변환</li>
<li>이렇게 되면, <strong>서브쿼리</strong>가 호텔마다 중복 실행되지 않으므로 성능의 이점이 있음</li>
</ul>
<blockquote>
<ul>
<li>현재는 <strong>데이터셋</strong>이 많지 않고 실제 서비스가 아닌 프로젝트 레벨이기 때문에, 쿼리를 따로 분리하지 않음</li>
</ul>
</blockquote>
<ul>
<li>데이터셋이 그렇게 크지 않으므로 메모리에서 처리하는 게 더 유리하다고 판단</li>
<li>3차 프로젝트를 진행하면서 더 생각해봐야 할 것 같음</li>
<li>현대 과학의 발전으로 인해 <strong>트레이드 오프</strong>로 <strong>유지보수성</strong>을 챙기는 게 웬만해선 좋을 것으로 판단 중</li>
</ul>
<hr>
<blockquote>
<p>참고) OpenAI. (2024).ChatGPT(4o)[Large language model].<a href="https://chatgpt.com/">https://chatgpt.com/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[식별 관계 - 비식별 관계]]></title>
            <link>https://velog.io/@riverpower6_g/%EC%8B%9D%EB%B3%84-%EA%B4%80%EA%B3%84-%EB%B9%84%EC%8B%9D%EB%B3%84-%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@riverpower6_g/%EC%8B%9D%EB%B3%84-%EA%B4%80%EA%B3%84-%EB%B9%84%EC%8B%9D%EB%B3%84-%EA%B4%80%EA%B3%84</guid>
            <pubDate>Thu, 23 Jan 2025 10:55:33 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; 프로젝트 진행으로 한창 바쁜 요즘이다.</p>
<p>&nbsp; 1차 프로젝트가 끝나자마자 2차 프로젝트에 돌입하였다. 그러다보니 아무래도 학습 기록 일지는 꾸준히 남기고 있지만, 학습한 내용에 대한 정리나 개념에 대한 포스팅을 하기가 쉽지가 않다.</p>
<p>&nbsp;현재는 2차 프로젝트 팀에서 주제를 정한 상태에서 다함께 프로젝트 기획 단계에 돌입하였다.
&nbsp; 그러다보니 <strong>ERD</strong>를 작성하게 되어 처음으로 <strong>ERD Cloud</strong>에서 직접 <strong>ERD</strong> 설계를 진행해보게 되었다.
&nbsp; 그렇게 <strong>ERD</strong>를 그려나가던 중, 갑자기 생소한 문장이 내 숨을 턱 멎게 했다.</p>
<p><strong>&nbsp;&quot;식별 관계로 하시겠습니까? 비식별 관계로 하시겠습니까?&quot;</strong></p>
<p>&nbsp; 이게 정확히 무엇을 의미하는 것일까. 언뜻 살펴보니, 외래키에 대한 내용이었다.
&nbsp; 당연히 <code>@OneToMany</code> 나 <code>@ManyToOne</code> 등으로 외래키를 사용할 건데, 식별 단계 아닌가? 하고 <strong>ERD</strong>를 작성해나가던 도중, 머지않아 이상함을 눈치챘다.</p>
<p>&nbsp; <strong>외래 키</strong>가 <strong>기본 키</strong>에 들어가 있던 것이다. 세상에 이게 무슨 일이람.</p>
<p>&nbsp; 도대체 왜 이런 일이 벌어진 걸까. <strong>ERD</strong>를 제대로 작성하기 위해서 해당 부분을 공부하기로 결정했다.
&nbsp; 이번 포스팅 내용은 짧다. 하지만 스스로 학습한 내용들을 기반으로, 개인적으로 &#39;이렇게하면 가장 쉽게 이해할 수 있겠구나&#39; 싶은 방향으로 정리해보고자 한다.</p>
<hr>
<h2 id="📑-식별-관계와-비식별-관계">📑 식별 관계와 비식별 관계</h2>
<h3 id="1️⃣-식별-관계">1️⃣ 식별 관계</h3>
<ul>
<li><strong>자식 엔티티</strong>가 <strong>부모 엔티티</strong>의 기본 키의 일부를 사용하여 식별되는 관계</li>
<li><strong>기본 키</strong>를 <strong>복합 키</strong>로 가짐<ul>
<li>자식 엔티티의 기본 키 + 부모 엔티티의 외래 키 = 기본 키</li>
</ul>
</li>
<li><strong>부모 엔티티</strong> 없이는 독립적으로 <strong>식별 불가</strong></li>
</ul>
<pre><code class="language-java">@Embeddable
@EqulasAndHashCode
@AllArgsConstructor
@NoArgsConstructor
public class EmplyeeId implements Serializable {
    private long deptId;

    private long empId;
}</code></pre>
<pre><code class="language-java">@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDNETITY)
    private long id;
}</code></pre>
<pre><code class="language-java">@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
    @EmbeddedId
    private EmployeeId employeeId;

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    private Department department;
}</code></pre>
<ul>
<li><code>insertable = false</code> : <code>Employee</code> 테이블에 저장할 때, <code>deptId</code> 값이 자동으로 추가되지 않음</li>
<li><code>updatable = false</code> : <code>Employee</code> 테이블을 업데이트할 때, <code>deptId</code> 값은 변경되지 않음</li>
<li><code>deptId</code> 값은 사실, 자동으로 삽입되거나 업데이트될 일이 없음</li>
<li>위와 같은 설정으로 더 <strong>명시적으로</strong> <code>JPA</code> 에게 <strong>외래 키</strong>를 <strong>자동</strong>으로 관리하지 않고 <strong>수동</strong>으로 설정된 값만 사용하겠다는 의도를 전달하는 용도
<br><br></li>
</ul>
<p>✔️ 이런 방법도 있다고 한다.</p>
<pre><code class="language-java">@Entity
@IdClass(EmployeeId.class)
public class Employee {
    @Id
    private long deptId;

    @Id
    private long empId;
}</code></pre>
<ul>
<li><code>@IdClass</code> 를 따로 지정해주어, 만들어둔 복합키 클래스인 <code>EmployeeId</code> 클래스의 복합키를 지정해줄 수 있음</li>
</ul>
<br>
<br>

<blockquote>
</blockquote>
<p>Q. 복합키 사용시, 객체(Entity)를 생성할 때 수동으로 데이터를 일일히 다 넣어줘야하는데, 좋은 방법이 있을까?
A. <code>@PrePersist</code> 를 사용해 자동으로 <code>empId</code> 를 생성할 수 있다.</p>
<pre><code class="language-java">@Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
    @EmbeddedId
    private EmployeeId employeeId;

    private final EntityManager em;

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    private Department department;

    @PrePersist
    public void prePersist() {
        if (employee.getEmpId() == 0) {
        long nextEmpId = getNextEmpId(department.getId());
        employeeId.setEmpId(nextEmpId);
    }

    private long getNextEmpId(long deptId) {
        Query query = em.createQuery(&quot;SELECT MAX(e.employeeId.empId) FROM Employee e WHERE e.department.id = :deptId&quot;);
        query.setParameter(&quot;deptId&quot;, deptId);
        Long maxEmpId = (Long) query.getSingleResult();

        return (maxEmpId == null) ? 1L : maxEmpId + 1;
    }
}</code></pre>
<ul>
<li><code>@PrePersist</code> 는 <code>Employee</code> 엔티티가 <strong>DB</strong>에 저장되기 전에 실행되는 메서드</li>
<li><code>empId</code> 가 <strong>0</strong>일 때만 실행, 새로운 직원이 추가될 때 부서별 <code>empId</code> 를 자동으로 계산하여 설정</li>
<li><code>getNextEmpId</code> 메서드는 해당 부서에서 가장 큰 <code>empId</code> 값 + <strong>1</strong> 만큼의 값을 새 <code>empId</code> 로 설정
<br><br></li>
</ul>
<h3 id="2️⃣-비식별-관계">2️⃣ 비식별 관계</h3>
<ul>
<li><strong>자식 엔티티</strong>가 <strong>부모 엔티티</strong>의 기본 키 참조 (외래키 보유)</li>
<li><strong>자식 엔티티</strong>의 기본 키에 <strong>부모 엔티티</strong>의 기본 키가 포함되지는 않음</li>
<li><strong>자식 엔티티</strong>는 <strong>부모 엔티티</strong>와 독립적으로 식별 가능</li>
<li><strong>부모 엔티티</strong>의 기본 키는 오직 <strong>자식 엔티티</strong>의 <strong>외래 키</strong>로만 사용</li>
<li>일반적으로 <code>@OneToMany</code> 와 <code>@ManyToOne</code> 을 사용할 때, 비식별 관계로 사용됨</li>
</ul>
<pre><code class="language-java">@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @ManyToOne
    private Department department;
}</code></pre>
<hr>
<blockquote>
<p>참고) OpenAI. (2024).ChatGPT(4o)[Large language model].<a href="https://chatgpt.com/">https://chatgpt.com/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[시간 복잡도]]></title>
            <link>https://velog.io/@riverpower6_g/%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84</link>
            <guid>https://velog.io/@riverpower6_g/%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84</guid>
            <pubDate>Mon, 13 Jan 2025 08:29:59 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; 매일 아침, 일어나서 컴퓨터에 앉자마자 반드시 진행 중인 루틴이 하나 있다. 코딩테스트를 적어도 한 문제 진행하는 것이다.
유명한 코딩테스트 사이트인 백준, 프로그래머스, 리트코드 중 프로그래머스에서 하루에 한 문제 이상씩 풀어보며 코딩테스트에 대한 감을 유지하는 중이다.</p>
<p><img src="https://velog.velcdn.com/images/riverpower6_g/post/7d46a140-9f2c-4534-85f2-67cf4dcdc299/image.png" alt=""></p>
<p>&nbsp; 아직 너무 어려운 코딩테스트 풀이까지는 진입하지 않았다. 전체 문제를 정답률 높은 문제 순으로 필터링하여 차근차근 풀고 있다.
&nbsp; 그러다보니 어느덧, 그렇게 많은 숫자는 아닐지 몰라도 <strong>432</strong> 문제를 풀었다. 매일마다 늘어나는 순위와 해결한 문제를 보며, 작은 성취감과 자존감을 채워나가고 있다.</p>
<p>&nbsp; 코딩테스트를 진행하며, 시간 복잡도 문제로 골머리를 많이 앓고 있다. 내가 생각한 시간 복잡도와 내 풀이의 시간 복잡도가 다를 때도 가끔 있고, 시간 복잡도를 줄이지 못해 <strong>런타임 에러</strong>가 터지는 경우도 왕왕 있다.
&nbsp; 이러한 문제들을 잘 해결하기 위해서는 당연히 시간 복잡도에 대해 잘 알고 있어야 한다. 그렇기에 시간 복잡도에 대해서 간략하게나마 정리해보았다.</p>
<hr>
<h1 id="📕-시간-복잡도">📕 시간 복잡도</h1>
<ul>
<li>알고리즘이 수행되는 데 걸리는 시간을 <strong>데이터의 크기</strong>에 따라 분석하는 방법</li>
<li>실제 실행 시간 측정 대신, <strong>입력 크기 n</strong>이 증가할 때 <strong>연산 횟수</strong>가 어떻게 변하는지에 따라 성능을 평가</li>
<li>일반적으로 <strong>Big-O 표기법</strong>으로 표현되며, 항상 <strong>최악의 경우</strong>의 알고리즘 성능을 나타냄
<br><br></li>
</ul>
<h2 id="✒️-코딩테스트에서의-시간-복잡도">✒️ 코딩테스트에서의 시간 복잡도</h2>
<ul>
<li><strong>코딩테스트</strong>에서는 이 시간 복잡도를 잘 처리하여, <strong>주어진 시간 제한 내에</strong> 알고리즘이 동작하도록 해야 함</li>
<li>일반적으로 <strong>1초에 <code>(10^8)</code> , 즉 1억 번</strong>의 연산을 처리할 수 있다고 가정<br>

</li>
</ul>
<h3 id="1️⃣-o1---상수-시간">1️⃣ O(1) - 상수 시간</h3>
<ul>
<li><code>n</code> 의 크기와 상관없이, 실행 시간은 항상 일정</li>
<li><code>n</code> - 제한 없음</li>
<li>ex) <strong>산술 연산, 조건문, 값 접근</strong> ...<pre><code class="language-java">public int sum(int x, int y) {
  return x + y;
}</code></pre>
</li>
<li><code>x</code> 와 <code>y</code> 의 값에 상관 없이 항상 한 번의 계산으로 실행이 종료됨</li>
<li>따라서 시간 복잡도는 <code>O(1)</code></li>
</ul>
<br>

<h3 id="2️⃣-ologn---로그-시간">2️⃣ O(logn) - 로그 시간</h3>
<ul>
<li><p><code>n</code> 의 크기에 따라 실행 시간이 <strong>로그</strong> 증가</p>
</li>
<li><p><code>n</code> - <code>10^18</code> 이상도 가능, 매우 넉넉한 조건</p>
</li>
<li><p>ex) <strong>이진 탐색, 균형 이진 트리 탐색</strong> ...</p>
<pre><code class="language-java">public int binarySearch(int[] arr, int key) {
  int left = 0, right = arr.length - 1;

  while (left &lt;= right) {
      int mid = left + (right - left) / 2;

      if (arr[mid] == key) {
          return mid;
      } else if (arr[mid] &lt;= key) {
          left = mid + 1;
      } else {
          right = mid - 1;
      }
  }

  return -1;
}</code></pre>
</li>
<li><p><code>while (left &lt;= right)</code> 일 때, 배열을 <strong>반으로 나누는 작업 반복</strong></p>
</li>
<li><p><code>n</code> 을 반으로 나누는 횟수는 <code>logn</code> 에 비례</p>
</li>
<li><p>따라서 시간 복잡도는 <code>O(logn)</code></p>
<br>

</li>
</ul>
<h3 id="3️⃣-on---선형-시간">3️⃣ O(n) - 선형 시간</h3>
<ul>
<li><code>n</code> 의 값과 비례하여 실행 시간 증가</li>
<li><code>n</code> &lt;= <code>100,000,000</code> ~ <code>1,000,000,000</code></li>
<li>ex) <strong>배열, 리스트 등 컬렉션 탐색, 반복문</strong> ...<pre><code class="language-java">public int sumArr(int[] arr) {
  return Arrays.stream(arr).sum();
}</code></pre>
</li>
<li><code>arr</code> 의 크기 <code>n</code> 을 모두 한 번씩 순회하며 값을 더함</li>
<li>따라서 시간 복잡도는 <code>O(n)</code><br>

</li>
</ul>
<h3 id="4️⃣-onlogn---선형-로그-시간">4️⃣ O(nlogn) - 선형 로그 시간</h3>
<ul>
<li><p>( <code>n</code> 값 * 로그 증가) 만큼의 실행 시간</p>
</li>
<li><p><code>n</code> &lt;= <code>1,000,000</code> ~ <code>10,000,000</code></p>
</li>
<li><p>ex) <strong>합병 정렬, 힙 정렬</strong> ...</p>
<pre><code class="language-java">public void mergeSort(int[] arr) {
  if (arr.length &lt; 2) {
      return;
  }

  int mid = arr.length / 2;
  int[] left = Arrays.copyOfRange(arr, 0, mid);
  int[] right = Arrays.copyOfRange(arr, mid, arr.length);

  mergeSort(left);
  mergeSort(right);

  merge(arr, left, right);
}
</code></pre>
</li>
</ul>
<p>public void merge(int[] arr, int left, int right) {
    // 두 배열을 합치는 알고리즘
    // 생략
}</p>
<pre><code>- `mergeSort(left)` 와 `mergeSort(right)` 에서 배열을 반으로 나누므로, `logn` 번 반복
- 나눠진 배열을 __병합__하며 올라오므로 `O(n)`
- 따라서 시간 복잡도는 둘을 곱한 `O(nlogn)`
&lt;br&gt;

### 5️⃣ O(n^2) - 이차 시간
- 값이 `n` 일 때, `n` * `n` 만큼 실행
- `n` &lt;= `1,000` ~ `10,000`
- ex) __이중 반복문, 버블 정렬__ ...
```java
public void bubbleSort(int[] arr) {
    for (int i = 0; i &lt; arr.length; i++) {
        for (int j = 0; j &lt; arr.length - 1; j++) {
            if (arr[j] &gt; arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}</code></pre><ul>
<li>이중 <code>for</code> 루프로 인해 <code>n</code> * <code>n</code> 번 실행</li>
<li>따라서 시간 복잡도는 <code>O(n^2)</code><br>

</li>
</ul>
<h3 id="6️⃣-on3---삼차-시간">6️⃣ O(n^3) - 삼차 시간</h3>
<ul>
<li><p>값이 <code>n</code> 일 때, <code>n</code> * <code>n</code> * <code>n</code>  만큼 실행</p>
</li>
<li><p><code>n</code> &lt;= <code>100</code> ~ <code>300</code></p>
</li>
<li><p>ex) <strong>플로이드-워셜, 삼중 반복문</strong> ...</p>
<pre><code class="language-java">public void flodWarshall(int[][] graph) {
  int n = graph.length;

  for (int k = 0; k &lt; n; k++) {
      for (int i = 0; i &lt; n; i++) {
          for (int j = 0; j &lt; n; j++) {
              graph[i][j] = Math.min(graph[i][j], graph[i][k] + graph[k][j]);
          }
      }
  }
}</code></pre>
</li>
<li><p>삼중 <code>for</code> 루프로 인해 <code>n</code> * <code>n</code> * <code>n</code> 번 실행</p>
</li>
<li><p>따라서 시간 복잡도는 <code>O(n^3)</code></p>
</li>
</ul>
<br>

<h3 id="7️⃣-o2n---지수-시간">7️⃣ O(2^n) - 지수 시간</h3>
<ul>
<li><p>값이 <code>n</code> 일 때, <code>2</code> 의 <code>n</code> <strong>제곱</strong> 만큼 실행</p>
</li>
<li><p><code>n</code> &lt;= 약 <code>20</code></p>
</li>
<li><p>ex) <strong>완전 탐색 DFS, 모든 부분집합 탐색</strong> ...</p>
<pre><code class="language-java">public class Solution {
  List&lt;Integer&gt; list;

  public static void main(String[] args) {
      list = new ArrayList();
      subsetSum(new int[]{1, 2, 3, 4, 5}, 0, 0);
  }

  public void subsetSum(int[] arr, int sum, int idx) {
    if (idx == arr.length) {
      list.add(sum);
      return;
    }

    subsetSum(arr, sum + arr[idx], idx + 1);
    subsetSum(arr, sum, idx + 1);
  }
}</code></pre>
</li>
<li><p><code>arr</code> 의 요소를 <strong>더할 경우와 더하지 않을 경우 총 2가지 실행</strong></p>
</li>
<li><p>각 <code>subsetSum</code> 호출시마다 <strong>2가지 경우의 수</strong>가 존재함</p>
</li>
<li><p>따라서 시간 복잡도는 <code>O(2^n)</code></p>
</li>
</ul>
<br>

<h3 id="8️⃣-on---팩토리얼-시간">8️⃣ O(n!) - 팩토리얼 시간</h3>
<ul>
<li><code>n!</code> 만큼 실행</li>
<li><code>n</code> &lt;= 약 <code>10</code></li>
<li>ex) <strong>순열, Brute Force(완전 탐색)</strong> ...<pre><code class="language-java"></code></pre>
</li>
</ul>
<p>public class Solution {
    int result;
    boolean[] visited;</p>
<pre><code>public int solution(int k, int[][] dungeons) {
    result = Integer.MIN_VALUE;
    visited = new boolean[dungeons.length];
    dfs(k, dungeons, 0, 0);

    return result;
}

public void dfs(int k, int[][] dungeons, int depth, int count) {
    if (depth == dungeons.length) {
        result = Math.max(result, count);
        return;
    }

    for (int i = 0; i &lt; dungeons.length; i++) {
        if (!visited[i]) {
            if (k &gt;= dungeons[i][0]) {
                k -= dungeons[i][1];
                visited[i] = true;
                dfs(k, dungeons, depth + 1, count + 1);
                visited[i] = false;
                k += dungeons[i][1];
            }
        }
    }

    result = Math.max(result, count);
}</code></pre><p>}</p>
<p>```</p>
<ul>
<li>값이 <code>n</code> 일 경우, 재귀 호출 시마다 <code>n - 1</code> 개의 경우의 수 고려</li>
<li>즉, <code>n - 1</code> 씩 줄여가며 반복</li>
<li>따라서 시간 복잡도는 <code>O(n!)</code>
<br><br></li>
</ul>
<h1 id="📚-시간-복잡도-정리">📚 시간 복잡도 정리</h1>
<table>
<thead>
<tr>
<th>시간 복잡도</th>
<th>최대 ( n ) (1초 기준)</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>O(1)</td>
<td>제한 없음</td>
<td>상수 시간 연산은 항상 가능</td>
</tr>
<tr>
<td>O(log n)</td>
<td>매우 큼 ( 10^18 )</td>
<td>로그 시간은 큰 입력에서도 가능</td>
</tr>
<tr>
<td>O(n)</td>
<td>약 ( 10^8 )</td>
<td>선형 시간은 입력이 매우 클 때도 가능</td>
</tr>
<tr>
<td>O(n log n)</td>
<td>약 ( 10^6 ) ~ ( 10^7 )</td>
<td>병합 정렬, 힙 정렬 등에서 자주 사용</td>
</tr>
<tr>
<td>O(n^2)</td>
<td>약 ( 10^3 ) ~ ( 10^4 )</td>
<td>이중 루프 등에서 사용 가능</td>
</tr>
<tr>
<td>O(n^3)</td>
<td>약 ( 100 ) ~ ( 300 )</td>
<td>플로이드-워셜 등</td>
</tr>
<tr>
<td>O(2^n)</td>
<td>약 ( 20 )</td>
<td>부분집합 탐색, 완전 탐색에서 사용</td>
</tr>
<tr>
<td>O(n!)</td>
<td>약 ( 10 )</td>
<td>순열 생성, 최적화 문제에서 사용</td>
</tr>
</tbody></table>
<hr>
<blockquote>
<p>참고) OpenAI. (2024).ChatGPT(4o)[Large language model].<a href="https://chatgpt.com/">https://chatgpt.com/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[MockMvc]]></title>
            <link>https://velog.io/@riverpower6_g/MockMvc</link>
            <guid>https://velog.io/@riverpower6_g/MockMvc</guid>
            <pubDate>Fri, 10 Jan 2025 09:08:26 GMT</pubDate>
            <description><![CDATA[<h1 id="🔎-overview">🔎 <strong>Overview</strong></h1>
<p>&nbsp; 최근, 테스트의 중요성을 여실히 느끼고 있다.
&nbsp; 원래 처음에는 테스트 파일을 따로 생성하여 테스트를 진행하지 않았다. 이유는 간단했다. 잘 사용할 줄 몰랐기 때문에.</p>
<p>&nbsp; 최근 학습을 진행하면서 테스트 파일을 작성하는 법을 체득하고 있다. 처음에는 간단하게 @Autowired로 실제 서비스나 레포지터리를 불러오고 기능이 잘 작동하는지를 테스트하였고, 이후에는 MockMvc를 통한 컨트롤러 테스트 또한 진행하였다.</p>
<p>&nbsp; 테스트 사용법이 조금씩 익숙해지면서, TDD로 기존의 프로젝트를 재구현하는 실습을 진행하며 테스트와 좀 더 친숙해지고 있는 요즘이다.</p>
<p>&nbsp; 개인적으로 처음에 되게 생소하게 느꼈던 것이 바로 컨트롤러 테스트이며, 이를 MockMvc를 이용하여 진행하고 있다.
&amp;nbsp 이 MockMvc가 무엇이며 어떻게 작동하고, 어떻게 응답하는지 정리해보는 시간을 가져보았다.</p>
<hr>
<h1 id="📕-mockmvc">📕 MockMvc</h1>
<ul>
<li><strong>컨트롤러를 테스트</strong>할 때, <code>Servlet Container</code> , 즉 실제 서버를 띄우지 않고도 <strong>컨트롤러 레이어</strong>를 테스트할 수 있도록 도와주는 도구</li>
<li><code>Spring MVC</code> 의 요청-응답 흐름을 시뮬레이션하며, <strong>컨트롤러</strong>가 예상대로 동작하는지 확인하는 과정</li>
<li><strong>HTTP  요청 및 응답</strong>을 <strong>Mocking</strong>해서 테스트 환경을 단순화
<br><br></li>
</ul>
<h2 id="🔎-mockmvc를-사용한-컨트롤러-테스트의-목적">🔎 MockMvc를 사용한 컨트롤러 테스트의 목적</h2>
<p><strong>1. 요청-매핑 검증</strong></p>
<ul>
<li>클라이언트로부터 <code>URI</code> 요청이 올 때, 이것이 올바른 <code>Handler Method</code> 로 매핑되는지 확인</li>
<li>ex) <code>/api/v1/member/login</code> <code>URI</code> 를 요청받았을 때, 이를 처리하는 메서드로 잘 연결되는가<br>

</li>
</ul>
<p><strong>2. 핸들러 메서드 로직 테스트</strong></p>
<ul>
<li><strong>매핑된 컨트롤러 메서드</strong>가 올바른 결과를 반환하는지 확인하기 위함<br>

</li>
</ul>
<p><strong>3. 예외 처리 검증</strong></p>
<ul>
<li><strong>잘못된 요청</strong> 및 <strong>비정상적 입력</strong>에 대한 <strong>예외 처리</strong></li>
<li>정의된 에러 응답을 반환하는지 확인
<br><br></li>
</ul>
<h2 id="🔗-mock과-dispatcherservlet">🔗 Mock과 DispatcherServlet</h2>
<ul>
<li><code>MockMvc</code> 는 실제 <strong>서블릿 컨테이너</strong>는 구동하지 않지만, <code>DispatcherServlet</code> 의 <strong>핵심 로직</strong>을 <code>Mock</code> 환경에서 동작시킴<ul>
<li><code>ServletContainer</code> : <strong>Tomcat</strong>, <strong>Jetty</strong> 등, <strong>실제 HTTP 요청</strong>을 처리하고 서블릿을 실행하는 환경</li>
<li><code>MockMvc</code> 는 이러한 실제 HTTP 요청 대신, <code>MockHttpServletRequest</code> 와 <code>MockHttpServletResponse</code> 를 사용하여 <code>DispatcherServlet</code> 내부 로직 호출</li>
</ul>
</li>
<li><code>DispatcherServlet</code> 은 <strong>서블릿 컨테이너</strong> 없이도 <code>Spring MVC</code> 의 요청-응답 처리 과정을 시뮬레이션 가능
<br><br></li>
</ul>
<h3 id="➡️-동작-방식">➡️ 동작 방식</h3>
<p><strong>1. Mock 요청 생성</strong></p>
<ul>
<li><code>MockMvc</code> 는 실제 HTTP 요청이 아닌, <code>MockHttpServletRequest</code> 를 생성<br>

</li>
</ul>
<p><strong>2. DispatcherServlet 호출</strong></p>
<ul>
<li><code>DispatcherServlet</code> 인스턴스를 <code>Mock</code> 환경에서 초기화</li>
<li><code>service()</code> 메서드 호출<ul>
<li><strong>서블릿 컨테이너</strong> 없이 <code>DispatcherServlet</code> 내부 로직 실행<br>

</li>
</ul>
</li>
</ul>
<p><strong>3. 핸들러 매핑 및 실행</strong></p>
<ul>
<li><code>DispatcherServlet</code> 은 <strong>핸들러 매핑</strong>을 통해 요청 URL에 맞는 컨트롤러를 찾고, 해당 컨트롤러 메서드(핸들러 메서드, 액션 메서드)를 실행<br>

</li>
</ul>
<p><strong>4. Mock 응답 반환</strong></p>
<ul>
<li>컨트롤러 메서드의 <strong>반환값</strong>은 <code>MockHttpServletResponse</code> 에 담겨 <strong>응답 객체</strong>로 사용</li>
</ul>
<hr>
<h1 id="✒️-mockmvc-사용">✒️ MockMvc 사용</h1>
<p><strong>1. MockMvc 생성</strong></p>
<pre><code class="language-java">@SpringBootTest
@AutoConfigureMockMvc
public class MockControllerTest() {
    @Autowired
    private MockMvc mvc;

    @Test
    void t1() {
        ResultActions resultActions = mvc.perform(...);
    }
}</code></pre>
<ul>
<li><code>AutoConfigureMockMvc</code> 를 통해 <code>MockMvc</code> 를 사용할 수 있도록 설정</li>
<li>테스트 환경에서는 <code>AllArgsConstructor</code> , <code>RequiredArgsConstructor</code> 등을 사용할 수 없으므로, <code>@Autowired</code> 를 통해 <code>DI</code> 로 <code>MockMvc</code> 주입</li>
<li><code>perform()</code> 메서드를 통해 <code>URL</code> 요청, 결과를 <code>ResultActions</code> 객체로 받아옴<br>

</li>
</ul>
<p><strong>2. HTTP 요청 메서드</strong></p>
<pre><code class="language-java">resultActions = mvc.perform(
    get(&quot;/api/v1/posts&quot;)
);

resultActions = mvc.perform(
    delete(&quot;/api/v1/posts/1&quot;)
);</code></pre>
<ul>
<li><strong>HTTP 요청 메서드</strong>를 선택</li>
<li><code>get(String url)</code> : <code>Get</code> 요청</li>
<li><code>post(String url)</code> : <code>Post</code> 요청</li>
<li><code>put(String url)</code> : <code>Put</code> 요청</li>
<li><code>delete(String url)</code> : <code>Delete</code> 요청</li>
<li><code>patch(String url)</code> : <code>Patch</code> 요청<br>

</li>
</ul>
<p><strong>3. andExpect()</strong></p>
<pre><code class="language-java">mvc.perform(
    delete(&quot;/api/v1/posts/1&quot;)
)
.andExpect(status().isOk());</code></pre>
<ul>
<li>요청 결과를 <strong>검증</strong>하기 위해 사용</li>
<li><code>status()</code> : 응답 <strong>상태 코드</strong> 검증</li>
<li><code>header()</code> : 응답 <strong>헤더</strong> 검증</li>
<li><code>handler()</code> : 응답 <strong>핸들러</strong> 검증</li>
<li><code>jsonPath()</code> : <strong>JSON 응답</strong>의 특정 필드 검증<br>

</li>
</ul>
<p><strong>4. andDo()</strong></p>
<pre><code class="language-java">mvc.perform(
    get(&quot;/api/v1/posts&quot;)
)
.andDo(print());</code></pre>
<ul>
<li>요청 및 응답 결과를 출력하거나 추가 작업 진행</li>
<li><strong>디버깅 용도</strong>로 주로 사용<br>

</li>
</ul>
<p><strong>5. header()</strong></p>
<pre><code class="language-java">String apiKey = UUID.randomUUID().toString();

mvc.perform(
    post(&quot;/api/v1/posts&quot;)
        .header(&quot;Authorization&quot;, &quot;Bearer &quot; + apiKey)
);</code></pre>
<ul>
<li><strong>HTTP 헤더</strong> 추가</li>
<li><strong>쿠키 확인 및 검증</strong> 등에 주로 사용<br>

</li>
</ul>
<p><strong>6. content(), contentType()</strong></p>
<pre><code class="language-java">mvc.perform(
    post(&quot;/api/v1/posts&quot;)
        .content(&quot;&quot;&quot;
                {
                    &quot;title&quot;: &quot;제목&quot;,
                    &quot;content&quot;: &quot;내용&quot;
                }
                &quot;&quot;&quot;)
        .contentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8))
);</code></pre>
<ul>
<li><code>content()</code> : <strong>요청</strong>에 본문 추가</li>
<li><code>contentType()</code> : <strong>본문 파일 형식</strong> 및 <strong>인코딩 방식</strong> 지정</li>
</ul>
<hr>
<blockquote>
<p>참고) OpenAI. (2024).ChatGPT(4o)[Large language model].<a href="https://chatgpt.com/">https://chatgpt.com/</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>