<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>junho_99.log</title>
        <link>https://velog.io/</link>
        <description>공부한 내용 정리하고 복습하는 블로그</description>
        <lastBuildDate>Sat, 29 Mar 2025 12:25:45 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>junho_99.log</title>
            <url>https://velog.velcdn.com/images/junho_99/profile/45cb53eb-ca43-404b-bd56-eeaec8eeed28/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. junho_99.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/junho_99" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[JWT 란??]]></title>
            <link>https://velog.io/@junho_99/JWT-%EB%9E%80</link>
            <guid>https://velog.io/@junho_99/JWT-%EB%9E%80</guid>
            <pubDate>Sat, 29 Mar 2025 12:25:45 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/junho_99/post/1994659d-fc4d-47ba-b316-276c47ef883c/image.png" alt=""></p>
<h1 id="jwt-vs-세션-기반-인증-방식-비교-및-실무-대응-전략">JWT vs 세션 기반 인증 방식 비교 및 실무 대응 전략</h1>
<h2 id="✅-인증-방식-개요">✅ 인증 방식 개요</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>JWT (JSON Web Token)</th>
<th>세션 기반 인증</th>
</tr>
</thead>
<tbody><tr>
<td>방식</td>
<td>토큰을 클라이언트가 저장, 매 요청마다 헤더의 Authorization 필드에 담아 전송</td>
<td>세션 ID를 서버에 저장, 클라이언트는 쿠키로 보관</td>
</tr>
<tr>
<td>저장 위치</td>
<td>클라이언트 (로컬스토리지, 쿠키 등)</td>
<td>서버 메모리 또는 세션 저장소 (Redis 등)</td>
</tr>
<tr>
<td>상태성</td>
<td>Stateless (무상태)</td>
<td>Stateful (상태 유지)</td>
</tr>
<tr>
<td>확장성</td>
<td>서버 확장에 유리</td>
<td>서버 확장 시 세션 공유 필요</td>
</tr>
<tr>
<td>보안</td>
<td>탈취 시 위험. 위변조 방지 서명 필요</td>
<td>서버에만 정보가 있어 비교적 안전</td>
</tr>
<tr>
<td>로그아웃 처리</td>
<td>블랙리스트 등 별도 구현 필요</td>
<td>서버에서 세션 삭제 시 즉시 무효화 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="🧩-jwt의-구조와-서명-검증-방식">🧩 JWT의 구조와 서명 검증 방식</h2>
<h3 id="✅-jwt-구성">✅ JWT 구성</h3>
<p>JWT는 세 부분으로 구성되어 있음:</p>
<pre><code>&lt;Header&gt;.&lt;Payload&gt;.&lt;Signature&gt;</code></pre><ul>
<li><strong>Header</strong>: 토큰 타입과 해싱 알고리즘 정보를 담음 (ex. HS256)</li>
<li><strong>Payload</strong>: 유저 정보와 클레임 (exp, sub 등)을 포함</li>
<li><strong>Signature</strong>: Header + Payload를 서버의 비밀 키로 서명한 값</li>
</ul>
<p>예시:</p>
<pre><code class="language-json">// Header
{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}

// Payload
{
  &quot;sub&quot;: &quot;1234567890&quot;,
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;exp&quot;: 1710000000
}</code></pre>
<h3 id="✅-서명signature-생성-및-검증">✅ 서명(Signature) 생성 및 검증</h3>
<p><strong>서명 생성 방식 (서버):</strong></p>
<pre><code>HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; + base64UrlEncode(payload),
  secret
)</code></pre><p><strong>검증 시 (서버):</strong></p>
<ol>
<li>클라이언트로부터 받은 JWT를 디코딩</li>
<li>Header와 Payload를 사용하여 서버의 <code>secret</code> 키로 다시 서명 생성</li>
<li>생성된 서명과 클라이언트가 보낸 Signature가 일치하는지 비교<ul>
<li>✅ 일치 → 유효한 토큰</li>
<li>❌ 불일치 → 위변조된 토큰 → 인증 거부</li>
</ul>
</li>
</ol>
<hr>
<h2 id="🔐-jwt-사용-시-예상되는-보안-문제와-해결-방안">🔐 JWT 사용 시 예상되는 보안 문제와 해결 방안</h2>
<h3 id="1-access-token-탈취-시-대응-불가">1. Access Token 탈취 시 대응 불가</h3>
<ul>
<li><strong>문제:</strong> Access Token이 탈취되면 유효기간 내 누구나 사용할 수 있음</li>
<li><strong>해결:</strong><ul>
<li>Access Token 유효기간을 짧게 설정 (ex. 15분)</li>
<li>Refresh Token 도입</li>
<li>Redis 기반 블랙리스트로 탈취 토큰 차단 (토큰 감지 로직 필요)</li>
</ul>
</li>
</ul>
<h3 id="2-refresh-token-탈취-시-무한-재발급">2. Refresh Token 탈취 시 무한 재발급</h3>
<ul>
<li><strong>문제:</strong> Refresh Token이 탈취되면 공격자가 Access Token을 계속 재발급 가능</li>
<li><strong>해결:</strong><ul>
<li>Rotating Refresh Token 전략 적용 (1회 사용 후 폐기)</li>
<li>재사용 감지 시 탈취로 간주하여 차단</li>
<li>IP, User-Agent 바인딩</li>
</ul>
</li>
<li>✅ 예시: 구글, 카카오톡 등에서 &quot;평소와 다른 장소에서 로그인되었습니다&quot; 알림 발생</li>
</ul>
<h3 id="3-로그아웃-불가-stateless-구조의-한계">3. 로그아웃 불가 (Stateless 구조의 한계)</h3>
<ul>
<li><strong>문제:</strong> 서버가 상태를 기억하지 않아 강제 로그아웃 어려움</li>
<li><strong>해결:</strong><ul>
<li>Redis 기반 블랙리스트 구현</li>
<li>로그아웃 시 Access Token을 등록하고 차단</li>
</ul>
</li>
</ul>
<h3 id="4-동시-로그인-제어-불가">4. 동시 로그인 제어 불가</h3>
<ul>
<li><strong>문제:</strong> 하나의 계정으로 여러 기기에서 동시 로그인 가능</li>
<li><strong>해결:</strong><ul>
<li>로그인 시 jti(UUID) 부여 → Redis 저장</li>
<li>요청 시 jti 검증 → 기존 jti 무효화</li>
</ul>
</li>
</ul>
<h3 id="5-토큰-크기-증가로-인한-성능-저하">5. 토큰 크기 증가로 인한 성능 저하</h3>
<ul>
<li><strong>문제:</strong> JWT는 세션 ID보다 길고, 매 요청마다 전송됨 → 트래픽 증가</li>
<li><strong>해결:</strong><ul>
<li>Payload에 최소한의 정보만 포함 (ex. userId, role)</li>
<li>민감 정보는 포함 금지</li>
</ul>
</li>
</ul>
<hr>
<h2 id="✅-결론-jwt-vs-session">✅ 결론: JWT vs Session</h2>
<ul>
<li><strong>JWT 인증</strong>은 서버가 상태를 저장하지 않아 <strong>확장성과 분산 시스템에 유리</strong>하며, 모바일, API 기반 서비스에 적합합니다.</li>
<li>반면 <strong>세션 기반 인증</strong>은 서버에 상태를 저장하므로, <strong>보안과 통제 측면에서 더 강력한 제어가 가능</strong>합니다.</li>
<li>실제 서비스에서는 <strong>두 방식을 적절히 조합</strong>하거나, 서비스 특성에 따라 선택하는 것이 중요합니다.</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis에서 DTO 업데이트는 전체 덮어쓰기? 필드 단위 수정?]]></title>
            <link>https://velog.io/@junho_99/Redis%EC%97%90%EC%84%9C-DTO-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EB%8A%94-%EC%A0%84%EC%B2%B4-%EB%8D%AE%EC%96%B4%EC%93%B0%EA%B8%B0-%ED%95%84%EB%93%9C-%EB%8B%A8%EC%9C%84-%EC%88%98%EC%A0%95</link>
            <guid>https://velog.io/@junho_99/Redis%EC%97%90%EC%84%9C-DTO-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EB%8A%94-%EC%A0%84%EC%B2%B4-%EB%8D%AE%EC%96%B4%EC%93%B0%EA%B8%B0-%ED%95%84%EB%93%9C-%EB%8B%A8%EC%9C%84-%EC%88%98%EC%A0%95</guid>
            <pubDate>Sat, 29 Mar 2025 09:31:08 GMT</pubDate>
            <description><![CDATA[<h1 id="redis에서-dto-업데이트는-전체-덮어쓰기-필드-단위-수정">Redis에서 DTO 업데이트는 전체 덮어쓰기? 필드 단위 수정?</h1>
<p>Redis를 캐시로 사용할 때 가장 많이 하는 고민 중 하나는 다음과 같습니다.</p>
<blockquote>
<p>&quot;데이터를 수정해야 할 때, DTO 전체를 Redis에 다시 저장해야 할까? 아니면 변경된 필드만 수정할 수 있을까?&quot;</p>
</blockquote>
<hr>
<h2 id="✅-redis는-데이터를-어떻게-저장할까">✅ Redis는 데이터를 어떻게 저장할까?</h2>
<p>Redis는 다양한 자료형을 지원합니다. DTO(Data Transfer Object)를 캐시할 때는 주로 두 가지 방식 중 하나를 사용합니다.</p>
<hr>
<h3 id="1-string-타입에-직렬화된-json-형태로-저장">1. <code>String</code> 타입에 직렬화된 JSON 형태로 저장</h3>
<pre><code class="language-java">redisTemplate.opsForValue().set(&quot;post:123&quot;, dto); // JSON 직렬화된 전체 DTO</code></pre>
<p>이 방식은 Redis에 <strong>전체 객체를 문자열로 저장</strong>하는 방식입니다.<br>즉, 필드 하나만 바뀌었더라도 Redis에는 <strong>전체 값을 덮어써야 합니다</strong>.</p>
<h4 id="🔁-업데이트-예시">🔁 업데이트 예시</h4>
<pre><code class="language-java">dto.setTitle(&quot;수정된 제목&quot;);
redisTemplate.opsForValue().set(&quot;post:123&quot;, dto); // 전체 덮어쓰기</code></pre>
<ul>
<li>✅ 장점: 사용이 간단하고 빠르게 구현 가능  </li>
<li>⚠️ 단점: 필드 하나 변경해도 전체 교체 → 네트워크 비용, (de)serialization 비용 증가</li>
</ul>
<hr>
<h3 id="2-hash-타입으로-필드-단위-저장">2. <code>Hash</code> 타입으로 필드 단위 저장</h3>
<pre><code class="language-java">redisTemplate.opsForHash().put(&quot;post:123&quot;, &quot;title&quot;, &quot;제목&quot;);
redisTemplate.opsForHash().put(&quot;post:123&quot;, &quot;content&quot;, &quot;내용&quot;);</code></pre>
<p>이 방식은 DTO의 각 필드를 Redis의 Hash 구조에 <strong>key-value</strong>로 저장합니다.<br>필드 단위로 접근하거나 수정할 수 있어 <strong>필드 하나만 바꿔도 해당 필드만 업데이트</strong>하면 됩니다.</p>
<h4 id="🔁-업데이트-예시-1">🔁 업데이트 예시</h4>
<pre><code class="language-java">redisTemplate.opsForHash().put(&quot;post:123&quot;, &quot;title&quot;, &quot;수정된 제목&quot;);</code></pre>
<ul>
<li>✅ 장점: 필드 단위 업데이트 가능 → 성능 효율적  </li>
<li>⚠️ 단점: 구조 설계 복잡, 객체로 역직렬화 필요</li>
</ul>
<hr>
<h2 id="⚖️-어떤-방식이-더-효율적일까">⚖️ 어떤 방식이 더 효율적일까?</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th><code>String</code>(JSON 직렬화)</th>
<th><code>Hash</code>(필드 단위 저장)</th>
</tr>
</thead>
<tbody><tr>
<td>장점</td>
<td>구현 간단, 통째로 직렬화</td>
<td>필드 단위로 업데이트 가능</td>
</tr>
<tr>
<td>단점</td>
<td>전체 덮어쓰기만 가능</td>
<td>구조 설계 복잡, 역직렬화 필요</td>
</tr>
<tr>
<td>적합한 상황</td>
<td>변경이 적은 데이터</td>
<td>필드 일부가 자주 변경될 때</td>
</tr>
</tbody></table>
<hr>
<h2 id="🧠-팁-상황에-따라-선택하자">🧠 팁: 상황에 따라 선택하자!</h2>
<h3 id="✅-변경이-거의-없는-캐시-예-게시글-본문-작성자-정보">✅ 변경이 거의 없는 캐시 (예: 게시글 본문, 작성자 정보)</h3>
<ul>
<li><code>String</code> 방식으로 간단하게 처리</li>
<li>Redis는 메모리 기반이라 전체 교체도 빠름</li>
</ul>
<h3 id="✅-자주-바뀌는-필드-예-좋아요-수-댓글-수">✅ 자주 바뀌는 필드 (예: 좋아요 수, 댓글 수)</h3>
<ul>
<li><code>Hash</code> 방식 사용</li>
<li>필드만 업데이트해서 네트워크 비용 최소화</li>
</ul>
<hr>
<h2 id="🔚-마무리">🔚 마무리</h2>
<p>Redis는 빠르지만, 데이터 업데이트 전략은 매우 중요합니다.<br>특히 DTO 캐싱에서 전체 덮어쓰기와 필드 단위 수정은 성능과 코드 복잡도에 큰 영향을 미칩니다.</p>
<blockquote>
<p>🚀 변경이 적다면 <code>String</code>으로,<br>🔁 자주 바뀌면 <code>Hash</code>로 관리하자!</p>
</blockquote>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Fetch Join vs IN절 성능 비교 실험기]]></title>
            <link>https://velog.io/@junho_99/Fetch-Join-vs-IN%EC%A0%88-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90-%EC%8B%A4%ED%97%98%EA%B8%B0</link>
            <guid>https://velog.io/@junho_99/Fetch-Join-vs-IN%EC%A0%88-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90-%EC%8B%A4%ED%97%98%EA%B8%B0</guid>
            <pubDate>Thu, 27 Mar 2025 18:39:52 GMT</pubDate>
            <description><![CDATA[<h2 id="🧩-실험-배경">🧩 실험 배경</h2>
<p>지난번 포스트에서 fetch join이 오히려 성능을 떨어트리는 원인을 찾아봤습니다. 이번 포스트에서는 해당 문제점을 해결하여 실질적인 성능향상을 이뤘던 내용을 정리해보려합니다.
페치조인이 성능을 떨어트린 이유는 아래 포스트 참고
<a href="https://velog.io/@junho_99/JPA-Fetch-Join-%EC%BF%BC%EB%A6%AC-%EB%B6%84%EC%84%9D%EC%9C%BC%EB%A1%9C-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90">https://velog.io/@junho_99/JPA-Fetch-Join-%EC%BF%BC%EB%A6%AC-%EB%B6%84%EC%84%9D%EC%9C%BC%EB%A1%9C-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90</a></p>
<p><strong>문제를 요약하면</strong></p>
<ol>
<li>페치조인으로 유저와 해당유저 게시글을 한번에 가져옴</li>
<li>DISTINCT + 정렬 + 페이징 등을 페치조인 쿼리와 함께 사용하면 조인으로 늘어난 row에 대한 계산을 진행해야하므로 성능 저하됨</li>
<li>1:N 관계에서의 페치조인은 row를 N개만큼 중복생성함</li>
</ol>
<hr>
<h2 id="⚙️-초기-실험">⚙️ 초기 실험</h2>
<p><strong>위의 문제들을 해결한뒤에 
페치조인 사용 전 vs 후 성능 비교를 시도했지만, 성능 차이가 거의 없었습니다.</strong></p>
<h3 id="📌-원인">📌 원인</h3>
<ul>
<li>팔로우한 유저 수가 <strong>1명</strong>뿐이었기 때문!</li>
<li>이로 인해 쿼리 횟수가 적고, N+1 문제 체감이 어려웠습니다.</li>
</ul>
<hr>
<h2 id="📈-실험-조건-확장">📈 실험 조건 확장</h2>
<h3 id="✅-실험-설정">✅ 실험 설정</h3>
<ul>
<li>테스트 유저(ID=30000<del>50000)가 각각 **200명(211</del>400)**을 팔로우</li>
<li>팔로우한 유저마다 <strong>게시글 데이터 4개</strong> 삽입</li>
<li>각 게시글마다 <strong>미디어 파일 데이터 2개</strong> 삽입</li>
</ul>
<h3 id="✅-테스트-환경">✅ 테스트 환경</h3>
<ul>
<li>동시 접속 유저: 약 <strong>2만 명</strong></li>
<li>각 유저의 팔로잉 수: <strong>200명</strong></li>
<li>각 유저당 게시글 수: <strong>4개</strong></li>
<li>각 게시글당 미디어파일 수 <strong>2개</strong></li>
</ul>
<hr>
<h2 id="🚀-본격-성능-실험">🚀 본격 성능 실험</h2>
<h3 id="1-user--post-페치조인만-사용">1. User + Post 페치조인만 사용</h3>
<ul>
<li><code>Post ↔ MediaFile</code>은 Lazy 로딩으로 개별 조회 → N+1 발생  </li>
<li>✅ 성능 개선 <strong>일부</strong> 확인</li>
</ul>
<h3 id="2-user--post는-페치조인">2. User + Post는 페치조인</h3>
<ul>
<li>Post + MediaFile은 IN 절 기반 일괄 조회<ul>
<li><code>MediaFile</code> 조회 시 <code>WHERE post_id IN (...)</code> 사용  </li>
<li>중복 row 없이 메모리에서 그룹핑 후 수동 주입  </li>
<li>✅ <strong>가장 큰 성능 향상</strong> 확인!</li>
</ul>
</li>
</ul>
<h3 id="3-모든-관계를-fetch-join으로-처리">3. 모든 관계를 Fetch Join으로 처리</h3>
<ul>
<li><code>Post ↔ MediaFile</code>도 Fetch Join</li>
<li>데이터가 많아질수록 <strong>row 수 폭증</strong> → 정렬 및 페이징 성능 <strong>감소</strong></li>
</ul>
<hr>
<p>아래의 그래프는 왼쪽부터
쿼리최적화X -----------  페치조인+IN절 사용  ------- 캐쉬까지 적용
<img src="https://velog.velcdn.com/images/junho_99/post/bb346a17-b20e-4703-945b-1cb84278d683/image.png" alt=""></p>
<hr>
<p>P50, P95, P99 성능지표
<img src="https://velog.velcdn.com/images/junho_99/post/7b1df202-8c6f-4dd9-8281-2b8d6a657264/image.png" alt=""></p>
<p>페치조인을 사용했을때와 사용하지 않았을때 성능차이가 거의 20~30배가 났습니다.</p>
<hr>
<p>nGrinder 부하테스트 결과
<img src="https://velog.velcdn.com/images/junho_99/post/bf45f93a-3fe0-4c98-ad45-4133377d28c3/image.png" alt=""></p>
<ul>
<li>캐쉬까지 적용한 메서드는 시간이 지날수록 캐쉬에 값들이 저장되고 Cache hit 비율이 증가하면서 TPS가 점점 증가하여 400까지 올라갔습니다.</li>
</ul>
<hr>
<h2 id="🧠-결론">🧠 결론</h2>
<table>
<thead>
<tr>
<th>전략</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>Fetch Join</td>
<td>직관적이고 한 쿼리로 해결</td>
<td>데이터가 많으면 row 폭증</td>
</tr>
<tr>
<td>IN 절</td>
<td>대량 데이터, 정렬/페이징에 유리</td>
<td>코드가 조금 복잡, 쿼리 2번</td>
</tr>
</tbody></table>
<h3 id="✅-최종-전략">✅ 최종 전략</h3>
<ul>
<li><code>User ↔ Post</code>는 Fetch Join</li>
<li><code>Post ↔ MediaFile</code>은 IN 절 기반 조회 + 메모리에서 주입</li>
</ul>
<hr>
<h2 id="💡-학습-포인트">💡 학습 포인트</h2>
<ul>
<li>단순히 &quot;페치조인이 좋다&quot;는 틀렸다!</li>
<li>데이터 양, 정렬, 페이징 여부에 따라 최적 전략은 달라진다.</li>
<li>직접 부하 테스트하고, 실행 계획(Execution Plan)을 분석하며 얻은 실전 지식입니다. 💪</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA Fetch Join,  쿼리 분석으로 성능 비교]]></title>
            <link>https://velog.io/@junho_99/JPA-Fetch-Join-%EC%BF%BC%EB%A6%AC-%EB%B6%84%EC%84%9D%EC%9C%BC%EB%A1%9C-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@junho_99/JPA-Fetch-Join-%EC%BF%BC%EB%A6%AC-%EB%B6%84%EC%84%9D%EC%9C%BC%EB%A1%9C-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Tue, 25 Mar 2025 16:19:47 GMT</pubDate>
            <description><![CDATA[<h1 id="🧠-jpa-fetch-join-과연-뉴스피드에-적합할까">🧠 JPA Fetch Join, 과연 뉴스피드에 적합할까?</h1>
<p>진행 중인 프로젝트에서 뉴스피드를 불러올 때,
팔로우한 유저들의 게시글과 함께 연관 엔티티인 미디어 파일까지 함께 가져오려고 Fetch Join을 적용해봤습니다.</p>
<pre><code>Hibernate: select distinct p1_0.id,p1_0.content,p1_0.created_at,p1_0.likes,mf1_0.post_id,mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.url,p1_0.modified_at,p1_0.title,p1_0.user_id from post p1_0 left join media_file mf1_0 on p1_0.id=mf1_0.post_id where p1_0.user_id in (select f1_0.following_id from follows f1_0 where f1_0.follower_id=?) order by p1_0.created_at desc
Hibernate: select distinct p1_0.id,p1_0.content,p1_0.created_at,p1_0.likes,mf1_0.post_id,mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.url,p1_0.modified_at,p1_0.title,p1_0.user_id,u1_0.id,u1_0.created_at,u1_0.email,u1_0.follower_count,u1_0.following_count,u1_0.is_celeb,u1_0.modified_at,u1_0.password,u1_0.role,u1_0.username from post p1_0 join users u1_0 on u1_0.id=p1_0.user_id left join media_file mf1_0 on p1_0.id=mf1_0.post_id where u1_0.id in (select f1_0.following_id from follows f1_0 where f1_0.follower_id=?) and u1_0.is_celeb=true order by p1_0.created_at desc
Hibernate: select u1_0.id,u1_0.created_at,u1_0.email,u1_0.follower_count,u1_0.following_count,u1_0.is_celeb,u1_0.modified_at,u1_0.password,u1_0.role,u1_0.username from users u1_0 where u1_0.id=?
</code></pre><p>이건 페치조인후 쿼리</p>
<pre><code>**Fetching posts without Fetch Join (N+1 risk)*** for user 23561
Hibernate: select p1_0.id,p1_0.content,p1_0.created_at,p1_0.likes,p1_0.modified_at,p1_0.title,p1_0.user_id from post p1_0 where p1_0.user_id in (select f1_0.following_id from follows f1_0 where f1_0.follower_id=?) order by p1_0.created_at desc fetch first ? rows only
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.post_id,mf1_0.url from media_file mf1_0 where mf1_0.post_id=?
Hibernate: select distinct p1_0.id,p1_0.content,p1_0.created_at,p1_0.likes,mf1_0.post_id,mf1_0.id,mf1_0.created_at,mf1_0.modified_at,mf1_0.url,p1_0.modified_at,p1_0.title,p1_0.user_id,u1_0.id,u1_0.created_at,u1_0.email,u1_0.follower_count,u1_0.following_count,u1_0.is_celeb,u1_0.modified_at,u1_0.password,u1_0.role,u1_0.username from post p1_0 join users u1_0 on u1_0.id=p1_0.user_id left join media_file mf1_0 on p1_0.id=mf1_0.post_id where u1_0.id in (select f1_0.following_id from follows f1_0 where f1_0.follower_id=?) and u1_0.is_celeb=true order by p1_0.created_at desc
Hibernate: select u1_0.id,u1_0.created_at,u1_0.email,u1_0.follower_count,u1_0.following_count,u1_0.is_celeb,u1_0.modified_at,u1_0.password,u1_0.role,u1_0.username from users u1_0 where u1_0.id=?
</code></pre><p>이건 페치조인 안했을때 쿼리</p>
<p>최근 게시물 30개 뽑아와서 각각의 포스트마다 미디어파일을 불러오는 쿼리가 발생했습니다. (N+1문제)</p>
<p>문제는 아래 사진을 보면됩니다.
<img src="https://velog.velcdn.com/images/junho_99/post/48b0a930-e50a-4c9f-b178-340515bc74a9/image.png" alt="">
왼쪽 노란색 그래프: Fetch Join 없이 → N+1 발생</p>
<p>가운데 초록색 그래프: Fetch Join 적용</p>
<p>🙈 예상과 다르게 Fetch Join을 적용했을 때 오히려 더 느려졌습니다.</p>
<p>원인을 파악하고자 PostgreSQL의 EXPLAIN ANALYZE로 직접 쿼리를 뜯어봤습니다.</p>
<h2 id="❗-결론-먼저">❗ 결론 먼저</h2>
<ul>
<li>정렬/페이징 + Fetch Join 조합은 성능상 불리하거나 비정상적인 결과를 유발할 수 있음
→ 특히 1:N 관계에서 JOIN 결과 row 수가 폭증하면서 페이징이 제대로 안 먹히거나, 메모리 낭비 발생</li>
</ul>
<h2 id="🔥-왜-성능이-저하될까">🔥 왜 성능이 저하될까?</h2>
<h3 id="1-fetch-join은-중복-row를-유발">1. <strong>Fetch Join은 중복 row를 유발</strong></h3>
<pre><code class="language-sql">SELECT p FROM Post p
JOIN FETCH p.mediaFiles</code></pre>
<ol>
<li><p>하나의 Post가 MediaFile 3개를 갖고 있다면 → Post row가 3배 중복되어 조회됨</p>
</li>
<li><p>이후 ORDER BY 및 LIMIT 30을 걸면 → 실제 Post는 10개일 수 있음 (중복 포함 30개가 기준이 되기 때문)</p>
</li>
</ol>
<p>📌 페이징 기준은 &quot;Post&quot;인데, MediaFile로 인한 중복 row 때문에 정확한 페이징이 불가능</p>
<h3 id="2-메모리-낭비와-성능-저하">2. <strong>메모리 낭비와 성능 저하</strong></h3>
<p>Fetch Join + ORDER BY 조합 시 → 중복 row가 정렬 대상에 포함됨</p>
<p>→ 정렬 처리량 증가</p>
<p>→ 메모리 소비 증가 및 쿼리 실행 시간 지연</p>
<p>결과적으로:</p>
<p>❌ 불안정한 페이징 결과</p>
<p>❌ 느린 응답 시간</p>
<h2 id="✅-대안-추천-전략">✅ 대안 (추천 전략)</h2>
<table>
<thead>
<tr>
<th>전략</th>
<th>설명</th>
<th>장점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>1. 기본 엔티티만 페이징 후, 서브 엔티티는 IN 절 조회</strong></td>
<td>Post만 페이징 후 MediaFile은 IN으로 따로 조회</td>
<td>✅ 정확한 페이징 ✅ N+1 방지</td>
</tr>
</tbody></table>
<h2 id="💡-참고-코드-예시-spring-data-jpa">💡 참고 코드 예시 (Spring Data JPA)</h2>
<pre><code class="language-java">// Step 1: Post만 페이징
List&lt;Post&gt; posts = postRepository.findByUserIn(userIds, pageable);

// Step 2: Post ID로 MediaFile 일괄 조회
List&lt;MediaFile&gt; mediaFiles = mediaFileRepository.findByPostIdIn(postIds);</code></pre>
<hr>
<h2 id="⚙️-실험-조건">⚙️ 실험 조건</h2>
<ul>
<li>PostgreSQL 17 사용</li>
<li>Hibernate SQL 로그 기반 쿼리 확인</li>
<li>성능 측정 도구: EXPLAIN ANALYZE</li>
</ul>
<hr>
<h2 id="1️⃣-기본-쿼리-post만-조회">1️⃣ 기본 쿼리: post만 조회</h2>
<pre><code class="language-sql">SELECT id, content, created_at, ...
FROM post
WHERE user_id IN (
  SELECT following_id FROM follows WHERE follower_id = 23561
)
ORDER BY created_at DESC
LIMIT 30;</code></pre>
<p>📌 <strong>결과</strong></p>
<ul>
<li>Execution Time: <code>16.7ms</code></li>
<li>Sort Method: <code>top-N heapsort</code> 사용</li>
<li>효율적이고 빠름</li>
</ul>
<hr>
<h2 id="2️⃣-fetch-join-방식">2️⃣ Fetch Join 방식</h2>
<pre><code class="language-sql">SELECT DISTINCT p.id, p.content, m.url
FROM post p
LEFT JOIN media_file m ON p.id = m.post_id
WHERE p.user_id IN (
  SELECT following_id FROM follows WHERE follower_id = 23561
)
ORDER BY p.created_at DESC
LIMIT 30;</code></pre>
<p><img src="https://velog.velcdn.com/images/junho_99/post/f6efd96a-78c5-4d66-9285-939df8ca5d2d/image.png" alt=""></p>
<p>📌 <strong>결과</strong></p>
<ul>
<li>Execution Time: ❌ <code>30.1ms</code></li>
<li>JOIN 결과 row 수 증가 → <code>11,213 rows</code></li>
<li>DISTINCT 처리 비용 발생 (<code>HashAggregate</code>)</li>
<li>성능 저하, 메모리 사용 ↑</li>
</ul>
<hr>
<h2 id="3️⃣-최적화-전략-서브쿼리-방식">3️⃣ 최적화 전략: 서브쿼리 방식</h2>
<pre><code class="language-sql">SELECT * FROM media_file
WHERE post_id IN (
  SELECT id FROM post
  WHERE user_id IN (1,2,3,4,5)
  ORDER BY created_at DESC
  LIMIT 30
);</code></pre>
<p><img src="https://velog.velcdn.com/images/junho_99/post/598834ff-3303-46fe-9db3-94821dcac999/image.png" alt="">
📌 <strong>결과</strong></p>
<ul>
<li>Execution Time: ✅ <code>17.4ms</code></li>
<li>Subquery는 정렬 + LIMIT 정확히 반영</li>
<li>외부 쿼리는 <code>Hash Semi Join</code>으로 최적화</li>
<li><strong>post 30개에만 연결된 media만 조회</strong></li>
</ul>
<hr>
<h2 id="📊-성능-비교-요약">📊 성능 비교 요약</h2>
<table>
<thead>
<tr>
<th>방식</th>
<th>post만 조회</th>
<th>Fetch Join</th>
<th>서브쿼리 방식</th>
</tr>
</thead>
<tbody><tr>
<td>정렬 방식</td>
<td>top-N heapsort</td>
<td>top-N heapsort</td>
<td>top-N heapsort</td>
</tr>
<tr>
<td>조인 row 수</td>
<td>30</td>
<td>11,213</td>
<td>744</td>
</tr>
<tr>
<td>중복 제거</td>
<td>❌ 없음</td>
<td>✅ 필요 (DISTINCT)</td>
<td>❌ 없음</td>
</tr>
<tr>
<td>실행 시간</td>
<td>✅ 16.7ms</td>
<td>❌ 30.1ms</td>
<td>✅ 17.4ms</td>
</tr>
<tr>
<td>구조 안정성</td>
<td>✅ 좋음</td>
<td>❌ 복잡</td>
<td>✅ 매우 좋음</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-최종-결론">✅ 최종 결론</h2>
<p>뉴스피드처럼:</p>
<ul>
<li>정렬 (ORDER BY created_at DESC)</li>
<li>페이징 (LIMIT 30)</li>
<li>연관 데이터 (media_file)</li>
</ul>
<p>이 함께 필요한 상황에서는:</p>
<p>👉 Fetch Join은 오히려 성능을 악화시킬 수 있습니다. 쿼리 구조와 데이터 양, 조건, 정렬 여부 등을 고려해서 필요한 방식으로 유연하게 접근해야 합니다.</p>
<hr>
<p>실제 최적화 이후 성능분석은 아래 링크 참고
<a href="https://velog.io/@junho_99/Fetch-Join-vs-IN%EC%A0%88-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90-%EC%8B%A4%ED%97%98%EA%B8%B0">https://velog.io/@junho_99/Fetch-Join-vs-IN%EC%A0%88-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90-%EC%8B%A4%ED%97%98%EA%B8%B0</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🧩 [데이터베이스] 파티셔닝(Partitioning)과 샤딩(Sharding) 정리]]></title>
            <link>https://velog.io/@junho_99/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9DPartitioning%EA%B3%BC-%EC%83%A4%EB%94%A9Sharding-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@junho_99/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9DPartitioning%EA%B3%BC-%EC%83%A4%EB%94%A9Sharding-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 24 Mar 2025 18:57:45 GMT</pubDate>
            <description><![CDATA[<p>파티셔닝과 샤딩은 데이터를 _<strong>분산 저장</strong>_하여 성능 향상, 확장성 확보, 관리 편의성 등을 제공하는 기법이다.</p>
<p>이번 글에서는 이 둘의 개념과 차이점, 사용 예시까지 정리해보겠습니다.</p>
<hr>
<h2 id="🧱-파티셔닝partitioning이란"><strong>🧱 파티셔닝(Partitioning)이란?</strong></h2>
<blockquote>
<p>파티셔닝은 하나의 테이블을 논리적으로 분할하여 여러 개의 <strong>파티션(Partition)</strong>으로 나누는 방식
DB는 여전히 하나의 인스턴스에서 관리되지만, 테이블이 내부적으로 나뉘어 저장되는 구조</p>
</blockquote>
<p>✔️ 예시
<img src="https://velog.velcdn.com/images/junho_99/post/64c67827-9e90-46d6-9924-e1c8daab75c5/image.png" alt=""></p>
<p>위와같은 테이블이 있다고 했을때 이를 수직 파티셔닝과 수평 파티셔닝 두 가지 방법으로 구현할 수 있다.</p>
<h3 id="수직-파티셔닝">수직 파티셔닝</h3>
<p>수직 파티셔닝은 테이블의 Column을 분할하여 여러 개의 서로 다른 테이블로 나누는 방법이다. 자주 조회하게 되는 칼럼과 잘 조회하지 않는 칼럼을 구분지음으로써 성능을 향상시킬 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/junho_99/post/c04698fd-9c6c-45c2-9cd3-9cde47440cea/image.png" alt=""></p>
<h3 id="수평-파티셔닝">수평 파티셔닝</h3>
<p>데이터베이스에서 테이블의 Row를 분할하여 여러 개의 서로 다른 테이블로 나누는 방법이다.
<img src="https://velog.velcdn.com/images/junho_99/post/3e9cd498-0828-42f9-9563-52384fb6c76e/image.png" alt=""></p>
<p>파티셔닝 범위</p>
<p><strong>1. 범위 분할, Range Partitioning</strong></p>
<p>연속적인 값을 범위를 기준으로 하여 분할
우편 번호, 날짜, 분기 등의 데이터에 적합</p>
<p><strong>2. 목록 분할, List Partitioning</strong></p>
<p>데이터 값이 특정 목록에 포함된 경우 데이터를 분리
나라, 지역 등의 데이터에 적합</p>
<p><strong>3. 해시 분할, Hash Partitioning</strong></p>
<p>Key값 등 특정 Column의 값을 Hashing 하여 분할
균등한 데이터 분할이 가능
범위가 없는 데이터에 적합</p>
<p><strong>4. 합성 분할, Composite Partitioning</strong></p>
<p>위 종류 중 2개 이상을 사용하여 분할</p>
<blockquote>
<p>✅ 장점
쿼리 성능 향상
관리 용이 (파티션 단위 백업/삭제 가능)
단일 인스턴스 운영으로 트랜잭션 관리 쉬움</p>
</blockquote>
<blockquote>
<p>⚠️ 단점
스토리지나 처리 능력은 한 인스턴스에 의존
파티션 설계가 복잡할 수 있음</p>
</blockquote>
<hr>
<h2 id="🪓-샤딩sharding이란">🪓 샤딩(Sharding)이란?</h2>
<blockquote>
<p><strong>샤딩(Sharding)</strong>은 데이터를 <strong>여러 DB 인스턴스(서버)</strong>에 분산 저장하는 방식이다.
즉, 단일 테이블을 나누는 것이 아니라, 동일한 스키마를 가진 데이터베이스를 나눠서 운영한다.
어떻게 보면 샤딩은 수평 파티셔닝과 비슷하지만 차이점은 수평 파티셔닝의 경우 동일한 서버에 저장되어 있고, 샤딩은 서로 다른 서버에 분산하여 저장한다는 점이다. 따라서 쿼리 성능 향상뿐만 아니라 부하가 분산되는 효과까지 얻을 수 있다. 즉, 샤딩은 데이터베이스 차원의 수평 확장(scale-out)이다.</p>
</blockquote>
<p><strong>수평파티셔닝</strong>
<img src="https://velog.velcdn.com/images/junho_99/post/89a6afbb-ae37-4c4d-9a22-4a0da64ba35f/image.png" alt=""></p>
<p><strong>샤딩</strong>
<img src="https://velog.velcdn.com/images/junho_99/post/b4663a90-ed2a-4042-90bc-fae91654637e/image.png" alt="">
위처럼 모든 파티션을 같은 DB 서버에 저장하는 수평 파티셔닝과 다르게 샤딩은 각 파티션들을 서로 다른 DB 서버에 저장함으로서 DB서버의 부하를 분산시키는 목적이 있다. 이때 수평분할된 작은 테이블을 샤드(shard)라고하며 규모가 큰 서비스, 데이터가 많이 쌓이는 테이블, 트래픽이 많이 몰리는 경우에 사용한다.</p>
<blockquote>
<p>✅ 장점
수평적 확장 가능 (서버 추가로 처리량 증가)
트래픽 및 저장소 분산
대규모 시스템에 유리</p>
</blockquote>
<blockquote>
<p>⚠️ 단점
트랜잭션 관리가 어려움 (샤드 간 조인, 정합성 문제)
샤딩 키 설계가 매우 중요 (어느 샤드에만 데이터가 주구장창 모이면 오히려 안좋음)
샤드 추가/병합 시 데이터 재분배 필요</p>
</blockquote>
<p>참고한 블로그</p>
<blockquote>
<p><a href="https://aiday.tistory.com/123">https://aiday.tistory.com/123</a>
<a href="https://velog.io/@yangsijun528/%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9D%EA%B3%BC-%EC%83%A4%EB%94%A9">https://velog.io/@yangsijun528/%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9D%EA%B3%BC-%EC%83%A4%EB%94%A9</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>