<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>d0ngx2_2.log</title>
        <link>https://velog.io/</link>
        <description>메롱</description>
        <lastBuildDate>Sat, 10 Jan 2026 05:14:14 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>d0ngx2_2.log</title>
            <url>https://velog.velcdn.com/images/d0ngx2_2/profile/51932295-9159-402c-886a-a0cd3cf19f14/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. d0ngx2_2.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/d0ngx2_2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Spring Boot 프로젝트(회고)]]></title>
            <link>https://velog.io/@d0ngx2_2/Spring-Boot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@d0ngx2_2/Spring-Boot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 10 Jan 2026 05:14:14 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-boot-프로젝트에서-aop·인증인가·redis-분산락까지--트러블슈팅과-설계-기록">Spring Boot 프로젝트에서 AOP·인증/인가·Redis 분산락까지 — 트러블슈팅과 설계 기록</h1>
<blockquote>
<p><strong>목표</strong>
단순 기능 구현을 넘어서, <em>왜 이런 구조를 선택했는지</em>, <em>어떤 문제를 겪었고 어떻게 해결했는지</em>를 기록하기 위한 글이다.</p>
</blockquote>
<hr>
<h2 id="1-프로젝트-배경">1. 프로젝트 배경</h2>
<p>이 프로젝트는 공연 예매 서비스로,</p>
<ul>
<li><strong>동시성 이슈가 명확한 좌석 선택 도메인</strong></li>
<li><strong>JWT 기반 인증/인가</strong></li>
<li><strong>Redis를 활용한 캐싱과 분산 락</strong>
을 핵심 기술 포인트로 삼았다.</li>
</ul>
<p>특히 튜터님·코드리뷰 상황에서</p>
<blockquote>
<p>“왜 이렇게 설계했나요?”
라는 질문에 답할 수 있는 구조를 만드는 것을 목표로 했다.</p>
</blockquote>
<hr>
<h2 id="2-인증--인가-설계-jwt">2. 인증 / 인가 설계 (JWT)</h2>
<h3 id="2-1-왜-세션이-아닌-jwt인가">2-1. 왜 세션이 아닌 JWT인가?</h3>
<ul>
<li>서버 확장에 유리한 <strong>Stateless 구조</strong></li>
<li>모바일/프론트엔드와의 연동 용이</li>
<li>인증 정보를 토큰 자체에 포함 가능</li>
</ul>
<p>단점으로는 앞으로 구현할 <strong>로그아웃과 토큰 무효화가 어렵다</strong>는 점이었다.</p>
<hr>
<h3 id="2-2-authenticationmanager를-사용한-이유">2-2. AuthenticationManager를 사용한 이유</h3>
<p><code>AuthenticationManager</code>는 인증의 <strong>진입점</strong> 역할을 한다.</p>
<ul>
<li>Controller → Service가 인증을 직접 처리하지 않게 함</li>
<li>인증 로직을 Spring Security에게 위임</li>
</ul>
<h4 id="인증-책임을-프레임워크에-맡기고-비즈니스-로직과-분리">인증 책임을 프레임워크에 맡기고, 비즈니스 로직과 분리</h4>
<hr>
<h3 id="2-3-jwt-인증-필터를-usernamepasswordauthenticationfilter-앞에-둔-이유">2-3. JWT 인증 필터를 UsernamePasswordAuthenticationFilter 앞에 둔 이유</h3>
<ul>
<li>JWT는 <strong>로그인 이후 요청</strong>을 검증</li>
<li>UsernamePasswordAuthenticationFilter는 <strong>로그인 요청 전용</strong></li>
</ul>
<pre><code class="language-text">[JWT 인증 필터] → [UsernamePasswordAuthenticationFilter]</code></pre>
<h4 id="이미-토큰이-있는-요청은-굳이-로그인-필터를-탈-필요가-없다">이미 토큰이 있는 요청은 굳이 로그인 필터를 탈 필요가 없다..!</h4>
<hr>
<h3 id="2-4-userdetails--userdetailsservice를-분리한-이유">2-4. UserDetails / UserDetailsService를 분리한 이유</h3>
<table>
<thead>
<tr>
<th>구성요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>UserDetails</td>
<td>Spring Security가 이해하는 사용자 모델</td>
</tr>
<tr>
<td>UserDetailsService</td>
<td>사용자 조회 책임</td>
</tr>
</tbody></table>
<h4 id="도메인-user와-보안-user를-분리해-결합도-감소">도메인 User와 보안 User를 분리해 <strong>결합도 감소</strong></h4>
<hr>
<h2 id="3-로그아웃과-blacklist-설계">3. 로그아웃과 Blacklist 설계</h2>
<h3 id="3-1-jwt-로그아웃의-근본적인-문제">3-1. JWT 로그아웃의 근본적인 문제</h3>
<ul>
<li>JWT는 서버에 저장되지 않음(<code>Stateless 특성</code>)</li>
<li>한 번 발급되면 만료 전까지 유효</li>
</ul>
<h4 id="단순히-로그아웃-api-호출로는-토큰을-무효화할-수-없음">단순히 &quot;로그아웃&quot; API 호출로는 토큰을 무효화할 수 없음</h4>
<hr>
<h3 id="3-2-blacklist를-도입한-이유">3-2. Blacklist를 도입한 이유</h3>
<blockquote>
<p>이미 발급된 토큰을 <strong>서버 기준으로 차단</strong>하기 위함</p>
</blockquote>
<ul>
<li>로그아웃 시 토큰을 Blacklist에 저장</li>
<li>이후 요청마다 Blacklist 확인</li>
<li>존재하면 인증 실패</li>
</ul>
<h4 id="stateless-구조를-유지하면서도-즉시-무효화-보장"><code>Stateless</code> 구조를 유지하면서도 <strong>즉시 무효화 보장</strong></h4>
<hr>
<h3 id="3-3-user-테이블과-분리한-이유">3-3. User 테이블과 분리한 이유</h3>
<ul>
<li>인증 상태 ≠ 사용자 정보</li>
<li>인증 관련 데이터는 조회 빈도가 높음</li>
<li>Redis 전환을 고려한 구조</li>
</ul>
<h4 id="책임-분리--확장성-확보">책임 분리 + 확장성 확보</h4>
<hr>
<h3 id="3-4-위-설계로-인한-차후-생각해야될-모델">3-4 위 설계로 인한 차후 생각해야될 모델</h3>
<blockquote>
<p>수많은 유저가 로그아웃을 거치기 때문에 이에따른 병목 현상에 대해 고려</p>
</blockquote>
<ul>
<li>일정 데이터가 쌓이면 삭제하도록 설계</li>
<li>다른 방도가 별도로 필요할 것으로 보임..（；´д｀）ゞ</li>
</ul>
<hr>
<h2 id="4-동시성-문제와-redis-분산-락">4. 동시성 문제와 Redis 분산 락</h2>
<h3 id="4-1-좌석-선택에서-발생하는-문제">4-1. 좌석 선택에서 발생하는 문제</h3>
<ul>
<li>동시에 같은 좌석을 선택하는 요청</li>
<li>DB 트랜잭션만으로는 한계</li>
</ul>
<hr>
<h3 id="4-2-redis-분산-락을-선택한-이유">4-2. Redis 분산 락을 선택한 이유</h3>
<ul>
<li>멀티 인스턴스 환경 대응</li>
<li>DB 락보다 빠른 처리</li>
<li>TTL로 데드락 방지 가능</li>
</ul>
<hr>
<h3 id="4-3-락-구현-방식">4-3. 락 구현 방식</h3>
<pre><code class="language-text">SETNX + TTL
Lua Script로 안전한 해제</code></pre>
<ul>
<li>락 획득 실패 시 즉시 예외</li>
<li>락은 반드시 finally에서 해제</li>
</ul>
<hr>
<h2 id="5-aop로-락-로직-분리">5. AOP로 락 로직 분리</h2>
<h3 id="5-1-왜-aop인가">5-1. 왜 AOP인가?</h3>
<ul>
<li>락 로직은 <strong>횡단 관심사</strong></li>
<li>서비스 코드가 지저분해지는 문제</li>
</ul>
<h4 id="비즈니스-로직에서-락-코드-제거">비즈니스 로직에서 락 코드 제거</h4>
<hr>
<h3 id="5-2-redislock-어노테이션-설계">5-2. @RedisLock 어노테이션 설계</h3>
<ul>
<li>SpEL을 활용한 동적 key 생성 (SpEL에 대해서는 차후 따로 다뤄볼 계획이다!!(⌐■_■))</li>
<li>TTL 정책을 어노테이션으로 명시</li>
</ul>
<pre><code class="language-java">@RedisLock(key = &quot;&#39;seat:&#39; + #seatId&quot;, ttl = 3000)</code></pre>
<hr>
<h3 id="5-3-runtimeexception-문제와-개선-포인트">5-3. RuntimeException 문제와 개선 포인트</h3>
<p>초기에는 AOP 내부에서 <code>RuntimeException</code>으로 감싸며 예외를 던졌다.</p>
<p><code>문제점</code>:</p>
<ul>
<li>서비스 레벨의 CustomException이 그대로 전달되지 않음</li>
</ul>
<p><code>개선 방향</code>:</p>
<ul>
<li>AOP에서는 예외를 감싸지 않고 그대로 throw</li>
<li>또는 공통 인터페이스 기반 예외 처리</li>
</ul>
<hr>
<h2 id="6-scheduler--분산-락">6. Scheduler + 분산 락</h2>
<h3 id="6-1-왜-스케줄러에도-락이-필요한가">6-1. 왜 스케줄러에도 락이 필요한가?</h3>
<ul>
<li>서버가 여러 대일 경우</li>
<li>동일 스케줄이 중복 실행 가능</li>
</ul>
<hr>
<h3 id="6-2-redisschedulerlockaspect-도입">6-2. RedisSchedulerLockAspect 도입</h3>
<ul>
<li><p>@Scheduled 메서드에 어노테이션 적용</p>
</li>
<li><p>이미 실행 중이면 그냥 종료</p>
<p><strong>중복 실행 방지 + 안정성 확보</strong></p>
</li>
</ul>
<hr>
<h2 id="7-트러블슈팅-요약">7. 트러블슈팅 요약</h2>
<table>
<thead>
<tr>
<th>이슈</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td>JWT 로그아웃</td>
<td>Blacklist 도입</td>
</tr>
<tr>
<td>좌석 중복 선택</td>
<td>Redis 분산 락</td>
</tr>
<tr>
<td>락 코드 중복</td>
<td>AOP 분리</td>
</tr>
<tr>
<td>스케줄 중복 실행</td>
<td>Scheduler Lock</td>
</tr>
<tr>
<td>예외 전파 문제</td>
<td>AOP 예외 정책 개선</td>
</tr>
</tbody></table>
<hr>
<h2 id="8-마무리">8. 마무리</h2>
<p>이번 프로젝트를 통해 느낀 점은</p>
<blockquote>
<p><strong>기술 선택의 이유를 설명할 수 있어야 진짜 내 코드가 된다</strong></p>
</blockquote>
<p>라는 것이었다.</p>
<p>단순 구현이 아니라,</p>
<ul>
<li>왜 이 구조인지</li>
<li>어떤 대안이 있었는지</li>
<li>왜 이걸 선택했는지</li>
</ul>
<p>를 계속 고민한 경험이 큰 자산이 되었다.</p>
<hr>
<h3 id="다음으로-정리하고-싶은-주제">다음으로 정리하고 싶은 주제</h3>
<ul>
<li>Redis vs DB Lock 비교 실험</li>
<li>JWT + Refresh Token 고도화</li>
<li>트래픽 테스트와 병목 분석</li>
</ul>
<p>꼭 해보고 싶다!!!!!!!<del>(≧▽≦)/</del> 프로젝트 마무리한 모두 고생했고 좋은 결과만 있기를 (❁´◡`❁)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis Lock + AOP를 이용한 동시성 제어]]></title>
            <link>https://velog.io/@d0ngx2_2/Redis-Lock-AOP%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</link>
            <guid>https://velog.io/@d0ngx2_2/Redis-Lock-AOP%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4</guid>
            <pubDate>Wed, 07 Jan 2026 15:02:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h2 id="왜-redis-lock이-필요한가">왜 Redis Lock이 필요한가?</h2>
</blockquote>
<ul>
<li><p>문제 상황</p>
<ul>
<li>좌석 예매, 쿠폰 발급, 포인트 차감처럼</li>
<li>여러 요청이 동시에 같은 자원을 수정하는 상황 발생</li>
</ul>
</li>
<li><p>단순 조회 후 수정 방식에서는 두 요청이 동시에 isSelected == false 확인</p>
</li>
<li><p>동시에 true로 변경 → 중복 선택 발생</p>
</li>
</ul>
<h4 id="요구사항">요구사항</h4>
<ul>
<li><p>단 하나의 요청만 자원을 수정</p>
</li>
<li><p>나머지는 즉시 실패 또는 대기</p>
</li>
</ul>
<hr>
<blockquote>
<h2 id="redis-lock을-선택한-이유">Redis Lock을 선택한 이유</h2>
</blockquote>
<ul>
<li><p>Redis Lock 특징</p>
<ul>
<li><p>분산 환경에서도 동시성 제어 가능</p>
</li>
<li><p>DB Lock보다 빠르고 부담이 적음</p>
</li>
<li><p>서버가 여러 대여도 하나의 Redis를 통해 락 공유</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="db-lock만으로-부족한-이유">DB Lock만으로 부족한 이유</h2>
</blockquote>
<ul>
<li><p>트랜잭션 범위가 길어질수록 DB 부하 증가</p>
</li>
<li><p>API 서버 확장 시 제어 어려움</p>
</li>
</ul>
<h4 id="분산-락이-필요했기-때문에-redis-선택">“분산 락”이 필요했기 때문에 Redis 선택</h4>
<hr>
<blockquote>
<h2 id="aop로-lock을-분리한-이유">AOP로 Lock을 분리한 이유</h2>
</blockquote>
<ul>
<li>서비스 코드에 락을 직접 쓰면?<pre><code>lock();
try {
  비즈니스 로직
} finally {
  unlock();
}</code></pre></li>
</ul>
<h4 id="모든-메서드에-반복됨">모든 메서드에 반복됨</h4>
<ul>
<li><p>가독성 ↓</p>
</li>
<li><p>실수로 unlock 누락 위험</p>
</li>
<li><p>AOP 적용 후</p>
<pre><code class="language-java">@RedisLock(key = ...)
public void businessLogic() { ... }</code></pre>
</li>
<li><p>락 로직 완전 분리</p>
</li>
<li><p>비즈니스 로직에 동시성 코드 없음</p>
</li>
<li><p>선언적으로 락 적용 가능</p>
</li>
</ul>
<h4 id="락은-횡단-관심사cross-cutting-concern">락은 횡단 관심사(Cross-cutting concern)</h4>
<h4 id="aop로-분리하는-게-가장-적합">AOP로 분리하는 게 가장 적합</h4>
<blockquote>
<h2 id="핵심-키워드-정리">핵심 키워드 정리</h2>
</blockquote>
<ul>
<li><p>AOP 관련</p>
<ul>
<li><p>@Aspect</p>
</li>
<li><p>@Around</p>
</li>
<li><p>ProceedingJoinPoint</p>
</li>
<li><p>횡단 관심사 (Cross-cutting Concern)</p>
</li>
</ul>
</li>
<li><p>Redis Lock 관련</p>
<ul>
<li><p>SETNX (setIfAbsent)</p>
</li>
<li><p>TTL (Time To Live)</p>
</li>
<li><p>분산 락 (Distributed Lock)</p>
</li>
<li><p>Lua Script (원자적 해제)</p>
</li>
</ul>
</li>
<li><p>설계 관련</p>
<ul>
<li><p>SRP (단일 책임 원칙)</p>
</li>
<li><p>DIP (의존성 역전 원칙)</p>
</li>
<li><p>선언적 프로그래밍</p>
</li>
<li><p>관심사 분리 (Separation of Concerns)</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="redislock-어노테이션-설계">RedisLock 어노테이션 설계</h2>
</blockquote>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
    String key();
}</code></pre>
<h4 id="왜-이렇게-설계했는가">왜 이렇게 설계했는가?</h4>
<ul>
<li><p>METHOD 단위 → 락은 로직 실행 범위에 적용</p>
</li>
<li><p>RUNTIME 유지 → AOP 실행 시 접근 필요</p>
</li>
<li><p>key를 SpEL로 받음 → 동적 락 키 생성</p>
</li>
</ul>
<hr>
<blockquote>
<h2 id="spel을-이용한-lock-key-생성">SpEL을 이용한 Lock Key 생성</h2>
</blockquote>
<pre><code class="language-java">@RedisLock(key = &quot;&#39;seat:&#39; + #request.scheduleId + &#39;:&#39; + #request.seatNo&quot;)</code></pre>
<h4 id="spel을-쓴-이유">SpEL을 쓴 이유</h4>
<ul>
<li><p>메서드 파라미터 기반으로 자원 단위 락 구현 가능</p>
</li>
<li><p>예시 결과</p>
<pre><code>lock:seat:3:15</code></pre></li>
</ul>
<h4 id="전체-좌석이-아닌-특정-좌석만-잠그는-최소-범위-락">전체 좌석이 아닌 특정 좌석만 잠그는 최소 범위 락</h4>
<hr>
<blockquote>
<h2 id="redislockaspect의-역할">RedisLockAspect의 역할</h2>
</blockquote>
<ul>
<li><p>역할 요약</p>
<ul>
<li><p>어노테이션 감지</p>
</li>
<li><p>SpEL 파싱</p>
</li>
<li><p>LockService 호출</p>
</li>
<li><p>비즈니스 메서드 실행 제어</p>
</li>
</ul>
</li>
<li><p>핵심 흐름</p>
<ul>
<li><p>@RedisLock 감지</p>
</li>
<li><p>Lock Key 생성</p>
</li>
<li><p>Redis Lock 획득</p>
</li>
<li><p>joinPoint.proceed() 실행</p>
</li>
<li><p>finally에서 락 해제</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="lockservice의-책임-분리">LockService의 책임 분리</h2>
</blockquote>
<pre><code class="language-java">public &lt;T&gt; T executeWithLock(String key, Supplier&lt;T&gt; action)</code></pre>
<ul>
<li><p>책임</p>
<ul>
<li><p>락 획득 / 해제</p>
</li>
<li><p>TTL 설정</p>
</li>
<li><p>락 실패 시 즉시 예외 처리</p>
</li>
</ul>
</li>
<li><p>왜 Supplier?</p>
<ul>
<li><p>비즈니스 로직을 함수로 전달</p>
</li>
<li><p>락 범위를 명확히 제어</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="redislockrepository-구현-포인트">RedisLockRepository 구현 포인트</h2>
</blockquote>
<ul>
<li><p>락 획득</p>
<pre><code>SET key value NX PX ttl</code></pre></li>
<li><p>한 요청만 성공</p>
</li>
<li><p>TTL로 데드락 방지</p>
</li>
<li><p>락 해제 (Lua Script)</p>
<pre><code class="language-java">if redis.call(&#39;get&#39;, key) == value then
  del key
end</code></pre>
</li>
</ul>
<h4 id="락을-건-주체만-해제-가능">락을 건 주체만 해제 가능</h4>
<h4 id="안전한-분산-락-구현">안전한 분산 락 구현</h4>
<hr>
<blockquote>
<h2 id="왜-around를-사용했는가">왜 @Around를 사용했는가?</h2>
</blockquote>
<ul>
<li><p>락은 메서드 실행 전 필요</p>
</li>
<li><p>메서드 실행 후 반드시 해제</p>
</li>
<li><p>try-finally 구조 필요</p>
</li>
</ul>
<h4 id="around만-가능">@Around만 가능</h4>
<hr>
<blockquote>
<h2 id="이-구조의-장점-정리">이 구조의 장점 정리</h2>
</blockquote>
<ul>
<li>비즈니스 코드가 깔끔</li>
<li>락 적용 여부를 어노테이션으로 제어</li>
<li>재사용 가능</li>
<li>테스트/확장 용이</li>
</ul>
<hr>
<blockquote>
<h3 id="요약">요약</h3>
</blockquote>
<h4 id="redis-기반-분산-락을-aop로-분리하여">Redis 기반 분산 락을 AOP로 분리하여</h4>
<h4 id="비즈니스-로직과-동시성-제어를-분리했고">비즈니스 로직과 동시성 제어를 분리했고,</h4>
<h4 id="spel을-활용해-자원-단위의-최소-범위-락을-구현했다">SpEL을 활용해 자원 단위의 최소 범위 락을 구현했다!!</h4>
<p>공부할게 산더미!!!!!!!!!!!!!!!（；´д｀）ゞ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[대용량 데이터 처리 / 검색 성능 개]]></title>
            <link>https://velog.io/@d0ngx2_2/%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC-%EA%B2%80%EC%83%89-%EC%84%B1%EB%8A%A5-%EA%B0%9C</link>
            <guid>https://velog.io/@d0ngx2_2/%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC-%EA%B2%80%EC%83%89-%EC%84%B1%EB%8A%A5-%EA%B0%9C</guid>
            <pubDate>Tue, 30 Dec 2025 13:58:29 GMT</pubDate>
            <description><![CDATA[<h2 id="목표">목표</h2>
<ul>
<li><p>JDBC를 이용해 유저 데이터 500만 건 Bulk Insert</p>
</li>
<li><p>닉네임으로 유저를 검색하는 API 구현</p>
</li>
<li><p>검색 성능 병목을 분석하고 인덱스를 통해 개선</p>
</li>
<li><p>성능 개선을 정량적 근거(EXPLAIN) 로 비교</p>
</li>
</ul>
<hr>
<blockquote>
<h2 id="대용량-데이터-생성-jdbc-bulk-insert">대용량 데이터 생성 (JDBC Bulk Insert)</h2>
</blockquote>
<ul>
<li><p>구현 방식</p>
</li>
<li><p>JPA가 아닌 순수 JDBC + Batch Insert</p>
</li>
<li><p>PreparedStatement + executeBatch()</p>
</li>
<li><p>rewriteBatchedStatements=true 옵션 사용</p>
</li>
<li><p>10,000건 단위 commit</p>
<pre><code class="language-java">String sql = &quot;INSERT INTO users (nick_name) VALUES (?)&quot;;
PreparedStatement ps = con.prepareStatement(sql);
</code></pre>
</li>
</ul>
<p>for (int i = 1; i &lt;= 5_000_000; i++) {
    ps.setString(1, &quot;nick_&quot; + UUID.randomUUID());
    ps.addBatch();</p>
<pre><code>if (i % 10_000 == 0) {
    ps.executeBatch();
    con.commit();
    ps.clearBatch();
}</code></pre><p>}</p>
<pre><code>

&gt;### 깨달은 점
#### JPA로 500만 건 insert는 사실상 불가능
#### JDBC Batch + 옵션이 없으면 속도 &amp; 메모리 문제 발생
#### 테스트 코드로 데이터 생성하는 것은 “한 번 실행용”

---

&gt;## 테스트 환경 트러블 슈팅

- ❌ OutOfMemoryError 발생</code></pre><p>java.lang.OutOfMemoryError: Java heap space</p>
<pre><code>
- 원인

   - batch size 과도

   - MySQL batch rewrite 미적용

   - Test JVM Heap 부족

- 해결

  - batchSize 조절

  - rewriteBatchedStatements=true

  - 테스트는 한 번만 실행

  - ❌ 테스트 데이터가 사라지는 문제

- 현상

  - 테스트에서 500만 건 생성

  - 애플리케이션 실행 시 데이터 사라짐

- 원인</code></pre><p>spring.jpa.hibernate.ddl-auto: create-drop</p>
<pre><code>
#### 애플리케이션 실행 시 테이블 DROP

---

&gt;###정리
#### 테스트 = 데이터 생성
#### 애플리케이션 실행은 불필요
#### DB 확인은 SQL로 직접

---

&gt;## 닉네임 검색 API 구현

```java
@GetMapping(&quot;/users&quot;)
public ResponseEntity&lt;List&lt;UserResponse&gt;&gt; search(@RequestParam String nickName) {
    return ResponseEntity.ok(userService.findByNickName(nickName));
}

List&lt;User&gt; findByNickName(String nickName);</code></pre><ul>
<li><p>정확히 일치 검색 (=)</p>
</li>
<li><p>랜덤 닉네임 → 실제 존재하는 값으로 테스트 필요</p>
</li>
</ul>
<hr>
<blockquote>
<h2 id="성능-병목-분석-인덱스-적용-전">성능 병목 분석 (인덱스 적용 전)</h2>
</blockquote>
<pre><code>EXPLAIN SELECT * FROM users WHERE nick_name = &#39;nick_xxxxx&#39;;</code></pre><ul>
<li>결과</li>
</ul>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>type</td>
<td>ALL</td>
</tr>
<tr>
<td>rows</td>
<td>5,000,000</td>
</tr>
<tr>
<td>key</td>
<td>NULL</td>
</tr>
</tbody></table>
<ul>
<li><p>의미</p>
<ul>
<li><p>Full Table Scan</p>
</li>
<li><p>모든 행을 순차 비교</p>
</li>
<li><p>데이터 증가 시 성능 선형 악화</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="인덱스-적용">인덱스 적용</h3>
</blockquote>
<pre><code>CREATE INDEX idx_users_nick_name ON users (nick_name);</code></pre><ul>
<li>서비스 코드 변경 없이 SQL 인덱스만 추가</li>
</ul>
<hr>
<blockquote>
<h2 id="성능-개선-결과-explain-기준">성능 개선 결과 (EXPLAIN 기준)</h2>
</blockquote>
<table>
<thead>
<tr>
<th>구분</th>
<th>type</th>
<th>rows</th>
</tr>
</thead>
<tbody><tr>
<td>인덱스 전</td>
<td>ALL</td>
<td>5,000,000</td>
</tr>
<tr>
<td>인덱스 후</td>
<td>ref</td>
<td>1</td>
</tr>
</tbody></table>
<ul>
<li><p>핵심 변화</p>
<ul>
<li><p>ALL → ref</p>
</li>
<li><p>Full Scan 제거</p>
</li>
<li><p>B-Tree 인덱스 기반 탐색</p>
</li>
</ul>
</li>
</ul>
<blockquote>
<h2 id="ms보다-explain이-중요한-이유">ms보다 EXPLAIN이 중요한 이유</h2>
</blockquote>
<ul>
<li><p>ms는 캐시·환경 영향 큼</p>
</li>
<li><p>로컬 환경에서는 체감 어려움</p>
</li>
<li><p>EXPLAIN은 DB의 실행 전략 자체</p>
</li>
<li><p>성능 개선의 증거는 “몇 ms 줄었다” 가 아닌 “접근 방식이 ALL → ref로 바뀌었다”</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="핵심-정리">핵심 정리</h3>
</blockquote>
<h4 id="500만-건-데이터-환경에서-닉네임-검색-시">500만 건 데이터 환경에서 닉네임 검색 시</h4>
<h4 id="인덱스-미적용-시-full-table-scan이-발생했으며">인덱스 미적용 시 Full Table Scan이 발생했으며,</h4>
<h4 id="단일-컬럼-인덱스-적용만으로-조회-범위를-500만-→-1건으로-줄일-수-있었다">단일 컬럼 인덱스 적용만으로 조회 범위를 500만 → 1건으로 줄일 수 있었다.</h4>
<blockquote>
<h3 id="느낀-점">느낀 점</h3>
</blockquote>
<h4 id="대용량-데이터에서는-동작함보다-어떻게-접근하는지가-중요">대용량 데이터에서는 “동작함”보다 “어떻게 접근하는지”가 중요</h4>
<h4 id="성능-개선은-코드보다-db-구조-설계가-핵심">성능 개선은 코드보다 DB 구조 설계가 핵심</h4>
<h4 id="인덱스는-단순하지만-가장-강력한-최적화-수단이다">인덱스는 단순하지만 가장 강력한 최적화 수단이다!!</h4>
<p>빠이팅w(ﾟДﾟ)w</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[트러블 슈팅(제출용)]]></title>
            <link>https://velog.io/@d0ngx2_2/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EC%A0%9C%EC%B6%9C%EC%9A%A9</link>
            <guid>https://velog.io/@d0ngx2_2/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EC%A0%9C%EC%B6%9C%EC%9A%A9</guid>
            <pubDate>Tue, 30 Dec 2025 02:19:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h2 id="트러블-슈팅1">트러블 슈팅(1)</h2>
</blockquote>
<hr>
<blockquote>
<h2 id="날짜-검색에서-localdate-vs-localdatetime">날짜 검색에서 LocalDate vs LocalDateTime</h2>
</blockquote>
<h3 id="왜-localdate로-입력을-받았을까">왜 LocalDate로 입력을 받았을까?</h3>
<ul>
<li><p>사용자는 날짜만 신경 쓰지, 시:분:초까지 직접 입력할 필요가 없음</p>
</li>
<li><p>UI/UX 관점에서 날짜 입력이 훨씬 직관적임</p>
</li>
</ul>
<hr>
<h3 id="그런데-응답은-왜-localdatetime일까">그런데 응답은 왜 LocalDateTime일까?</h3>
<ul>
<li><p>DB에는 생성 시점이 LocalDateTime으로 저장됨</p>
</li>
<li><p>응답 DTO는 정확한 생성 시각을 보여주는 게 맞음</p>
</li>
</ul>
<h4 id="입력은-단순하게localdate-출력은-정확하게localdatetime">입력은 단순하게(LocalDate), 출력은 정확하게(LocalDateTime)</h4>
<h4 id="역할에-맞게-타입을-분리하는-것이-설계적으로-더-좋다">역할에 맞게 타입을 분리하는 것이 설계적으로 더 좋다.</h4>
<hr>
<blockquote>
<h2 id="datetimeformat--atstartofday--localtimemax를-쓰는-이유">@DateTimeFormat + atStartOfDay / LocalTime.MAX를 쓰는 이유</h2>
</blockquote>
<ul>
<li>@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)</li>
<li>LocalDate startDate;</li>
</ul>
<h3 id="왜-이런-변환-로직이-필요한가">왜 이런 변환 로직이 필요한가?</h3>
<ul>
<li><p>LocalDate는 시간 정보가 없음</p>
</li>
<li><p>DB 컬럼은 LocalDateTime</p>
<ul>
<li><p>그래서:</p>
</li>
<li><p>시작일 → 00:00:00</p>
</li>
<li><p>종료일 → 23:59:59.999999999</p>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="코드가-지저분한데-다른-방법은">코드가 지저분한데 다른 방법은?</h3>
<ul>
<li><p>이 방식이 의도가 가장 명확하다고 함.</p>
</li>
<li><p>QueryDSL/JPA에서 시간 경계 버그를 가장 확실히 방지</p>
</li>
</ul>
<h4 id="지저분해-보여도-명시적인-코드가-가장-안전하다">“지저분해 보여도, 명시적인 코드가 가장 안전하다”</h4>
<blockquote>
<h2 id="querydsl에서-booleanexpression이-null인데도-동작하는-이유">QueryDSL에서 BooleanExpression이 null인데도 동작하는 이유</h2>
</blockquote>
<pre><code class="language-java">private BooleanExpression titleContains(String keyword) {
    return keyword == null ? null : todo.title.contains(keyword);
}</code></pre>
<h3 id="null-반환인데-왜-검색-조건이-적용될까">null 반환인데 왜 검색 조건이 적용될까?</h3>
<ul>
<li><p>QueryDSL의 where()는 null 조건을 자동으로 무시한다.</p>
</li>
<li><p>조건이 있으면 AND로 결합하고, 없으면 제외한다고 한다.</p>
<pre><code class="language-java">query.where(
  titleContains(keyword),
  createdAtBetween(start, end)
);</code></pre>
</li>
</ul>
<h4 id="null-safe-동적-쿼리를-위한-의도된-설계">null-safe 동적 쿼리를 위한 의도된 설계</h4>
<ul>
<li><p>장점</p>
<ul>
<li><p>if-else 지옥 방지</p>
</li>
<li><p>조건 조합 폭발 방지</p>
</li>
<li><p>가독성, 유지보수성 ↑</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="fetchone-vs-fetch-차이-강의내용-중복">fetchOne() vs fetch() 차이 (강의내용 중복)</h2>
</blockquote>
<h3 id="왜-fetchone을-함부로-쓰면-안-될까">왜 fetchOne을 함부로 쓰면 안 될까?</h3>
<pre><code class="language-java">fetchOne()</code></pre>
<ul>
<li><p>결과가 1개일 거라 확신할 때만</p>
</li>
<li><p>2개 이상이면 예외 발생</p>
<pre><code class="language-java">fetch()</code></pre>
</li>
<li><p>리스트 조회</p>
</li>
<li><p>결과 개수와 무관</p>
</li>
</ul>
<h4 id="단건-조회가-보장될-때만-fetchone">“단건 조회가 보장될 때만 fetchOne”</h4>
<hr>
<blockquote>
<h2 id="optional을-쓰는-이유--orelsethrow-자동완성">Optional을 쓰는 이유 &amp; orElseThrow 자동완성</h2>
</blockquote>
<h3 id="optional을-왜-쓰는가">Optional을 왜 쓰는가?</h3>
<ul>
<li><p>null 반환 → 실수로 NPE 발생</p>
</li>
<li><p>Optional → “없을 수도 있음”을 타입으로 강제</p>
<pre><code class="language-java">userRepository.findById(id)
  .orElseThrow(...)</code></pre>
<h3 id="optional-안-쓰면-orelsethrow-자동완성이-안-뜬-이유">Optional 안 쓰면 orElseThrow 자동완성이 안 뜬 이유?</h3>
</li>
<li><p>orElseThrow()는 Optional 전용 메서드</p>
</li>
<li><p>엔티티 자체에는 존재하지 않음</p>
</li>
</ul>
<h4 id="optional은-단순-편의가-아니라">Optional은 단순 편의가 아니라</h4>
<h4 id="null-가능성을-코드-레벨에서-드러내는-장치">null 가능성을 코드 레벨에서 드러내는 장치</h4>
<hr>
<blockquote>
<h2 id="n1-문제가-발생한-이유와-해결-방식">N+1 문제가 발생한 이유와 해결 방식</h2>
</blockquote>
<h3 id="왜-n1이-발생했을까">왜 N+1이 발생했을까?</h3>
<ul>
<li><p>연관 엔티티가 LAZY 로딩</p>
</li>
<li><p>반복 접근 시 쿼리가 추가로 실행됨</p>
</li>
</ul>
<h3 id="해결-방법은">해결 방법은?</h3>
<pre><code class="language-java">fetch join</code></pre>
<ul>
<li><p>QueryDSL에서 join + fetch</p>
</li>
<li><p>필요한 필드만 Projections로 조회</p>
</li>
</ul>
<h4 id="조회용-쿼리는-엔티티-조회가-아니라-dto-조회가-더-적합한-경우가-많다">“조회용 쿼리는 엔티티 조회가 아니라 DTO 조회가 더 적합한 경우가 많다”</h4>
<hr>
<blockquote>
<h2 id="securityconfig에-permitall이-있는데-jwt-필터에서-또-체크하는-이유">SecurityConfig에 permitAll이 있는데 JWT 필터에서 또 체크하는 이유</h2>
</blockquote>
<h3 id="securityconfig면-끝-아닌가">SecurityConfig면 끝 아닌가?</h3>
<pre><code class="language-java">.anyRequest().authenticated()</code></pre>
<ul>
<li><p>이건 인가(Authorization) 단계</p>
</li>
<li><p>JwtFilter는 그보다 앞단의 인증(Authentication) 필터</p>
</li>
</ul>
<h3 id="그래서-필터에서도-분기-처리가-필요한-이유">그래서 필터에서도 분기 처리가 필요한 이유</h3>
<ul>
<li><p>필터는 무조건 실행됨</p>
</li>
<li><p>/auth/** 요청에서도 JWT 검사하면 오류 발생</p>
<pre><code class="language-java">@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
  return request.getRequestURI().startsWith(&quot;/auth&quot;);
}</code></pre>
</li>
</ul>
<h4 id="필터-책임-인증-정보-생성">필터 책임: 인증 정보 생성</h4>
<h4 id="securityconfig-책임-접근-허용차단">SecurityConfig 책임: 접근 허용/차단</h4>
<hr>
<blockquote>
<h2 id="usernamepasswordauthenticationfilter-앞에-jwt-필터를-두는-이유">UsernamePasswordAuthenticationFilter 앞에 JWT 필터를 두는 이유</h2>
</blockquote>
<h3 id="이-필터는-뭐-하는-놈인가">이 필터는 뭐 하는 놈인가?</h3>
<ul>
<li><p>폼 로그인 기반</p>
</li>
<li><p>ID/PW 인증 처리</p>
</li>
</ul>
<h3 id="왜-jwt-필터가-앞에-있어야-할까">왜 JWT 필터가 앞에 있어야 할까?</h3>
<ul>
<li><p>JWT는 이미 인증된 사용자</p>
</li>
<li><p>UsernamePasswordAuthenticationFilter까지 갈 필요 없음</p>
</li>
</ul>
<h4 id="jwt-인증이-먼저-끝나야">JWT 인증이 먼저 끝나야</h4>
<h4 id="securitycontext가-채워지고">SecurityContext가 채워지고</h4>
<h4 id="이후-인가-로직이-정상-동작">이후 인가 로직이 정상 동작</h4>
<hr>
<blockquote>
<h2 id="정리">정리</h2>
</blockquote>
<h4 id="spring--jpa--security에서의-설정과-패턴은">“Spring / JPA / Security에서의 설정과 패턴은</h4>
<h4 id="될-때까지-맞추는-코드가-아니라">‘될 때까지 맞추는 코드’가 아니라</h4>
<h4 id="왜-이-레이어에서-이-책임을-가지는지를-이해하는-게-중요하다">왜 이 레이어에서 이 책임을 가지는지를 이해하는 게 중요하다!!!”</h4>
<hr>
<blockquote>
<h2 id="트러블-슈팅2">트러블 슈팅(2)</h2>
</blockquote>
<h1 id="궁금궁금featquerydsl">궁금궁금(feat.QueryDsl)</h1>
<blockquote>
</blockquote>
<pre><code class="language-java">Long total = queryFactory
    .select(todo.id.count())
    .from(todo)
    .where(
        todo.title.contains(keyword),
        todo.createdAt.between(start, end)
    )
    .fetchOne();</code></pre>
<h4 id="페이징-응답을-만들기-위해-전체-데이터-개수를-구하는-쿼리이다">페이징 응답을 만들기 위해 “전체 데이터 개수”를 구하는 쿼리이다.</h4>
<hr>
<blockquote>
<h3 id="왜-이-코드가-추가로-필요할까">왜 이 코드가 추가로 필요할까?</h3>
</blockquote>
<ul>
<li><p>페이징 응답에는 보통 이 정보들이 필요합니다</p>
</li>
<li><p>현재 페이지의 데이터 목록 (content)</p>
</li>
<li><p>전체 데이터 개수 (totalElements)</p>
</li>
<li><p>전체 페이지 수 (totalPages)</p>
</li>
<li><p>현재 페이지 번호</p>
</li>
</ul>
<hr>
<blockquote>
<h2 id="그런데-">그런데 !!!</h2>
</blockquote>
<ul>
<li><p>limit, offset이 들어간 조회 쿼리로는 전체 개수를 알 수 없다.</p>
</li>
<li><p>목록 조회 쿼리만 있으면 생기는 문제</p>
<pre><code class="language-java">List&lt;TodoSearchResponse&gt; content = queryFactory
  .select(...)
  .from(todo)
  .where(...)
  .orderBy(todo.createdAt.desc())
  .offset(pageable.getOffset())
  .limit(pageable.getPageSize())
  .fetch();</code></pre>
</li>
<li><p>“현재 페이지 데이터”만 조회</p>
</li>
<li><p>DB에는 총 몇 건이 있는지 모름!!</p>
</li>
<li><p>프론트 입장에서는</p>
<h4 id="다음-페이지가-있는지">“다음 페이지가 있는지?”,</h4>
<h4 id="총-몇-페이지인지를-알-수-없음">“총 몇 페이지인지?”를 알 수 없음</h4>
</li>
</ul>
<blockquote>
<h4 id="그래서-count-쿼리가-필요함">그래서 count 쿼리가 필요함</h4>
</blockquote>
<h4 id="select-counttodoid-from-todo-">select count(todo.id) from todo ...</h4>
<hr>
<ul>
<li><p>페이징 조건 ❌ (limit/offset 없음)</p>
</li>
<li><p>검색 조건만 동일하게 적용</p>
</li>
<li><p>전체 검색 결과 개수만 조회</p>
<ul>
<li>이 값으로:<pre><code class="language-java">PageImpl&lt;&gt;(content, pageable, total)</code></pre>
</li>
</ul>
</li>
<li><p>프론트에서 정확한 페이지 계산 가능</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="왜-목록-조회랑-쿼리를-분리했을까">왜 목록 조회랑 쿼리를 분리했을까?</h3>
</blockquote>
<h4 id="성능-이유">성능 이유</h4>
<ul>
<li><p>count는 필요한 컬럼이 id 하나뿐</p>
</li>
<li><p>projection / join / orderBy 필요 없음</p>
</li>
</ul>
<h4 id="책임-분리">책임 분리</h4>
<ul>
<li><p>조회 쿼리 → 데이터 가져오기</p>
</li>
<li><p>count 쿼리 → 개수 계산</p>
<ul>
<li>JPA &amp; QueryDSL에서 권장되는 패턴</li>
</ul>
</li>
</ul>
<blockquote>
<h3 id="왜-fetchone을-쓰는가">왜 fetchOne()을 쓰는가?</h3>
</blockquote>
<pre><code class="language-java">.select(todo.id.count())</code></pre>
<ul>
<li><p>count()는 결과가 항상 1행</p>
</li>
<li><p>리스트가 아니라 단일 값</p>
<ul>
<li>그래서:<pre><code class="language-java">fetch() ❌ (List&lt;Long&gt;)
</code></pre>
</li>
</ul>
</li>
</ul>
<p>fetchOne() ✅ (Long)</p>
<pre><code>
---

&gt;### 왜 keyword, start, end 조건을 똑같이 넣어야 할까?

#### “검색 조건이 다르면 total이 의미 없어짐”

- 목록 조회 결과: 5개

- total: 전체 100개 (이러면 안됨)

#### 목록 쿼리와 count 쿼리는 조건이 반드시 동일해야 함!!

---

&gt;### 요약
#### 페이징 처리에서는 현재 페이지의 데이터 조회와
#### 전체 데이터 개수 조회를 분리해야 하며,
#### 이를 위해 QueryDSL에서 count 전용 쿼리를 추가로 작성한다.


---

### 실전 코드 적용

```java
// 실제 목록 조회 쿼리 (페이징 대상)
List&lt;TodoSearchResponse&gt; content = queryFactory
        // TodoSearchResponse DTO로 바로 매핑하기 위해 Projections.constructor 사용
        // 엔티티 전체가 아니라 &quot;필요한 필드만&quot; 조회해서 성능 최적화
        .select(Projections.constructor(
                TodoSearchResponse.class,

                // 일정 제목
                todo.title,

                // 해당 일정의 담당자 수
                // JOIN으로 인해 중복 row가 생길 수 있으므로 countDistinct 사용
                manager.id.countDistinct(),

                // 해당 일정의 댓글 수
                // 댓글도 JOIN으로 중복될 수 있으므로 countDistinct 사용
                comment.id.countDistinct()
        ))
        // 기준 테이블은 todo
        .from(todo)

        // 일정과 담당자 관계 LEFT JOIN
        // 담당자가 없는 일정도 검색 결과에 포함시키기 위해 LEFT JOIN
        .leftJoin(todo.managers, manager)

        // 담당자와 유저 JOIN (닉네임 검색을 위함)
        .leftJoin(manager.user, user)

        // 일정과 댓글 LEFT JOIN
        // 댓글이 없는 일정도 검색 결과에 포함
        .leftJoin(todo.comments, comment)

        // 검색 조건
        .where(
                // 제목 키워드 부분 일치 검색
                todo.title.contains(keyword),

                // 담당자 닉네임 부분 일치 검색
                manager.user.nickName.contains(managerNickname),

                // 일정 생성일 범위 검색
                todo.createdAt.between(start, end)
        )

        // todo 기준으로 집계해야 하므로 groupBy 필수
        // count(), countDistinct()를 사용했기 때문
        .groupBy(todo.id)

        // 최신 일정이 먼저 나오도록 정렬
        .orderBy(todo.createdAt.desc())

        // 한 페이지에 가져올 데이터 개수 제한
        .limit(pageable.getPageSize())

        // 실제 데이터 조회
        .fetch();

// 전체 데이터 개수 조회 쿼리 (count 쿼리)
Long count = queryFactory
        // 전체 검색 결과 개수를 구하기 위한 count 쿼리
        .select(todo.id.count())

        // 기준 테이블은 동일하게 todo
        .from(todo)

        // 목록 조회와 &quot;동일한 검색 조건&quot;을 사용해야 함
        // 그래야 페이지 수(totalPages)가 정확해짐
        .where(
                todo.title.contains(keyword),
                todo.createdAt.between(start, end)
        )

        // count는 항상 단일 결과이므로 fetchOne 사용
        .fetchOne();
</code></pre><hr>
<blockquote>
<h3 id="이-코드에서-꼭-이해해야-할-핵심-포인트">이 코드에서 꼭 이해해야 할 핵심 포인트</h3>
</blockquote>
<ul>
<li><p>목록 조회 쿼리와 count 쿼리를 분리한 이유</p>
<ul>
<li><p>목록 조회: limit, groupBy, join → 데이터 표현용</p>
</li>
<li><p>count 쿼리: 단순한 구조 → 전체 개수 계산용</p>
</li>
</ul>
</li>
</ul>
<h4 id="페이징에서-정확한-total-값을-얻기-위한-필수-구조">페이징에서 정확한 total 값을 얻기 위한 필수 구조</h4>
<hr>
<blockquote>
<h3 id="왜-countdistinct를-쓰는가">왜 countDistinct를 쓰는가?</h3>
</blockquote>
<pre><code class="language-java">manager.id.countDistinct()
comment.id.countDistinct()
JOIN이 들어가면 하나의 todo가 여러 row로 늘어남</code></pre>
<ul>
<li><p>그냥 count() 쓰면 중복 카운트 발생</p>
</li>
<li><p>countDistinct()로 실제 개수만 집계</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="왜-groupbytodoid가-필요한가">왜 groupBy(todo.id)가 필요한가?</h3>
</blockquote>
<ul>
<li><p>count / countDistinct는 집계 함수</p>
</li>
<li><p>집계 대상이 todo 기준이므로 groupBy(todo.id) 필수</p>
</li>
<li><p>없으면 SQL 에러 발생</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="왜-count-쿼리에는-join이-없는가">왜 count 쿼리에는 JOIN이 없는가?</h3>
</blockquote>
<ul>
<li><p>전체 개수만 알면 됨</p>
</li>
<li><p>JOIN은 성능 저하만 유발</p>
</li>
</ul>
<h4 id="의도적으로-단순한-count-쿼리-작성--성능-최적화">의도적으로 단순한 count 쿼리 작성 = 성능 최적화</h4>
<blockquote>
<h3 id="요약">요약</h3>
</blockquote>
<h4 id="querydsl-페이징에서는">QueryDSL 페이징에서는</h4>
<h4 id="목록-조회-쿼리와-전체-개수count-쿼리를-분리하고">목록 조회 쿼리와 전체 개수(count) 쿼리를 분리하고,</h4>
<h4 id="목록-조회는-projection--groupby로-최적화하며">목록 조회는 Projection + groupBy로 최적화하며</h4>
<h4 id="count-쿼리는-조건만-동일하게-유지한-채-최대한-단순하게-작성한다">count 쿼리는 조건만 동일하게 유지한 채 최대한 단순하게 작성한다.</h4>
<hr>
<h2 id="전체-코드">전체 코드</h2>
<pre><code class="language-java">package org.example.expert.domain.todo.searchRepository;

import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.example.expert.domain.todo.dto.response.TodoSearchResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

import java.time.LocalDateTime;
import java.util.List;

import static org.example.expert.domain.comment.entity.QComment.comment;
import static org.example.expert.domain.manager.entity.QManager.manager;
import static org.example.expert.domain.todo.entity.QTodo.todo;
import static org.example.expert.domain.user.entity.QUser.user;

@RequiredArgsConstructor
public class SearchRepositoryImpl implements SearchRepository {

    private final JPAQueryFactory queryFactory;


    @Override
    public Page&lt;TodoSearchResponse&gt; search(
            String keyword,
            String managerNickname,
            LocalDateTime start,
            LocalDateTime end,
            Pageable pageable
    ) {
        List&lt;TodoSearchResponse&gt; content = queryFactory
                .select(Projections.constructor(
                        TodoSearchResponse.class,
                        todo.title,
                        manager.id.countDistinct(),
                        comment.id.countDistinct()
                ))
                .from(todo)
                .leftJoin(todo.managers, manager)
                .leftJoin(manager.user, user)
                .leftJoin(todo.comments, comment)
                .where(
                        todo.title.contains(keyword),
                        manager.user.nickName.contains(managerNickname),
                        todo.createdAt.between(start, end)
                )
                .groupBy(todo.id)
                .orderBy(todo.createdAt.desc())
                .limit(pageable.getPageSize())
                .fetch();

        Long total = queryFactory
                .select(todo.id.count())
                .from(todo)
                .where(
                        todo.title.contains(keyword),
                        todo.createdAt.between(start, end)
                )
                .fetchOne();

        return new PageImpl&lt;&gt;(content, pageable, total == null ? 0 : total);
    }</code></pre>
<hr>
<blockquote>
<h3 id="서비스-→-서비스-di는-별로인가feattransactional의-옵션">“서비스 → 서비스 DI”는 별로인가?(feat.Transactional의 옵션)</h3>
</blockquote>
<ul>
<li><p>보통 사람들이 꺼리는 이유:</p>
<ul>
<li><p>❌ 안 좋은 경우</p>
</li>
<li><p>서비스끼리 순환 의존성</p>
</li>
<li><p>한 서비스가 다른 서비스의 비즈니스 로직을 대신 수행</p>
<pre><code>ServiceA -&gt; ServiceB
ServiceB -&gt; ServiceC
ServiceC -&gt; ServiceA ❌</code></pre></li>
</ul>
</li>
</ul>
<h4 id="이건-설계-붕괴">이건 설계 붕괴</h4>
<hr>
<blockquote>
<h3 id="괜찮은-경우는">괜찮은 경우는?</h3>
</blockquote>
<ul>
<li><p>핵심 차이</p>
</li>
<li><p>ManagerService는 비즈니스 로직 담당</p>
</li>
<li><p>LogService는 기술적 관심사(로깅) 담당</p>
</li>
</ul>
<h4 id="즉-역할이-명확하게-분리되어-있음">즉, 역할이 명확하게 분리되어 있음</h4>
<blockquote>
<h3 id="이-구조가-맞는-이유를-한-줄씩-뜯어보면">이 구조가 “맞는 이유”를 한 줄씩 뜯어보면</h3>
</blockquote>
<h4 id="책임-분리가-명확하다-srp">책임 분리가 명확하다 (SRP)</h4>
<ul>
<li>서비스 : 책임</li>
<li>ManagerService : 매니저 등록 비즈니스 규칙</li>
<li>LogService : 요청 기록을 DB에 안전하게 남김</li>
</ul>
<h4 id="managerservice가-로그-테이블-직접-만지면-책임이-섞임">ManagerService가 로그 테이블 직접 만지면 책임이 섞임</h4>
<hr>
<blockquote>
<h3 id="트랜잭션-경계를-분리하기-위함">트랜잭션 경계를 분리하기 위함</h3>
</blockquote>
<ul>
<li>핵심 목적은 이거 👇</li>
</ul>
<h4 id="매니저-등록이-실패해도-로그는-반드시-저장">“매니저 등록이 실패해도 로그는 반드시 저장”</h4>
<ul>
<li><p>이건</p>
<pre><code class="language-java">@Transactional(REQUIRES_NEW)</code></pre>
</li>
<li><p>메서드 단위 트랜잭션 분리가 필요하고,</p>
</li>
<li><p>클래스 안에서는 불가능</p>
</li>
<li><p>Spring AOP 프록시 특성</p>
</li>
</ul>
<h4 id="그래서-별도-서비스로-분리--di-가-올바른-구조">그래서 별도 서비스로 분리 + DI 가 올바른 구조.</h4>
<h4 id="중요한-포인트-많이-놓친다고함">중요한 포인트 (많이 놓친다고함.)</h4>
<pre><code class="language-java">@Transactional(propagation = REQUIRES_NEW)
public void saveLog() { ... }</code></pre>
<ul>
<li><p>이걸 같은 서비스 클래스에서 호출하면 적용 안 됨</p>
</li>
<li><p>왜냐하면:</p>
<ul>
<li><p>Spring의 @Transactional은 프록시 기반</p>
</li>
<li><p>자기 자신 메서드 호출(self-invocation)은 프록시를 안 탐</p>
</li>
</ul>
</li>
</ul>
<h4 id="그래서-무조건-다른-bean이어야-함">그래서 무조건 다른 Bean이어야 함</h4>
<h4 id="logservice-분리는-필수">LogService 분리는 필수</h4>
<hr>
<blockquote>
<h3 id="생각보다-실무에서-흔한-패턴이라고-한다feat튜터님">생각보다 실무에서 흔한 패턴이라고 한다!(feat.튜터님!)</h3>
</blockquote>
<p>실무 예시들:</p>
<pre><code class="language-java">OrderService → PaymentLogService

UserService → LoginHistoryService

ReservationService → AuditLogService</code></pre>
<h4 id="비즈니스-서비스-→-로그이력-서비스">“비즈니스 서비스 → 로그/이력 서비스”</h4>
<h4 id="매우-정상적인-구조">매우 정상적인 구조</h4>
<hr>
<blockquote>
<h3 id="그럼-언제-서비스-→-서비스-di가-안-좋을까">그럼 언제 서비스 → 서비스 DI가 안 좋을까?</h3>
</blockquote>
<ul>
<li><p>피해야 할 경우 체크리스트</p>
<ul>
<li><p>서로가 서로를 호출한다 (순환 참조)</p>
</li>
<li><p>A 서비스가 B 서비스의 도메인 규칙을 대신 처리</p>
</li>
<li><p>“편해서” 그냥 갖다 쓴 경우</p>
</li>
<li><p>공통 로직이라고 다 서비스로 빼버린 경우</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="요약-1">요약</h3>
</blockquote>
<h4 id="이-구조는-비즈니스-로직과-기술적-관심사를-분리하고">이 구조는 비즈니스 로직과 기술적 관심사를 분리하고,</h4>
<h4 id="트랜잭션-전파-옵션requires_new을-적용하기-위한-의도적인-서비스-분리로">트랜잭션 전파 옵션(REQUIRES_NEW)을 적용하기 위한 의도적인 서비스 분리로</h4>
<h4 id="spring-aop-특성을-고려한-올바른-설계였다">Spring AOP 특성을 고려한 올바른 설계였다!</h4>
<blockquote>
<h3 id="요약2">요약2</h3>
</blockquote>
<h4 id="서비스-간-di는-무조건-나쁜-설계가-아니라">서비스 간 DI는 무조건 나쁜 설계가 아니라,</h4>
<h4 id="책임이-명확히-분리되고-트랜잭션기술적-관심사를-분리하기-위한-경우에는-권장되는-구조다">책임이 명확히 분리되고 트랜잭션/기술적 관심사를 분리하기 위한 경우에는 권장되는 구조다.</h4>
<h4 id="특히-transactionalrequires_new는-자기-자신-호출로는-동작하지-않기-때문에">특히 @Transactional(REQUIRES_NEW)는 자기 자신 호출로는 동작하지 않기 때문에</h4>
<h4 id="별도의-서비스-분리가-필수적이다">별도의 서비스 분리가 필수적이다.</h4>
<hr>
<blockquote>
<h2 id="트러블-슈팅3">트러블 슈팅(3)</h2>
</blockquote>
<blockquote>
<h3 id="spring-boot-서버를-aws-ec2--rds로-배포하고-health-check-api까지-연결하기">Spring Boot 서버를 AWS EC2 + RDS로 배포하고 Health Check API까지 연결하기</h3>
</blockquote>
<h3 id="목표">목표</h3>
<ul>
<li><p>Spring Boot 애플리케이션을 AWS EC2에 배포한다</p>
</li>
<li><p>RDS(MySQL) 와 연동한다</p>
</li>
<li><p>서버가 살아있는지 확인할 수 있는 Health Check API를 만든다</p>
</li>
<li><p>Health API는 누구나 접근 가능해야 한다</p>
</li>
<li><p>배포 과정에서 발생한 문제를 직접 해결하며 AWS 네트워크 / 보안 흐름을 이해한다</p>
</li>
</ul>
<hr>
<h3 id="로컬-환경과-서버-환경의-차이-인식">로컬 환경과 서버 환경의 차이 인식</h3>
<ul>
<li><p>처음에는 IntelliJ에서 실행하면 잘 되는데, EC2에 배포한 JAR를 실행하면 다음과 같은 문제가 발생했다.</p>
<ul>
<li><p>403 Forbidden</p>
</li>
<li><p>JWT 관련 에러</p>
</li>
<li><p>jar 파일 최신화 오류</p>
</li>
<li><p>DB 연결 실패</p>
</li>
<li><p>Hibernate Dialect 오류</p>
</li>
</ul>
</li>
</ul>
<h4 id="이-과정에서-로컬과-서버는-완전히-다른-환경이라는-걸-명확히-인식하게-됐다">이 과정에서 <strong>“로컬과 서버는 완전히 다른 환경”</strong>이라는 걸 명확히 인식하게 됐다.</h4>
<hr>
<h3 id="health-403에러-문제-해결-과정">health 403에러 문제 해결 과정</h3>
<ul>
<li><p>문제 1: EC2연결 후 RDS 연동까지 성공했음에도 health를 연결하였을 때 403 에러가 나는 것을 식별, 코드 수정을 했음에도 불구하고 403에러가 나는 것을 식별</p>
</li>
<li><p>원인</p>
<ul>
<li>어플리케이션 내 코드에 /health 경로를 필터에 통과시키도록 설정해주지 않았음.</li>
<li>코드를 수정한 후에 수정된 jar파일을 EC2에 재빌딩을 해주지 않았음.</li>
</ul>
</li>
<li><p>해결</p>
<ul>
<li>애플리케이션 코드에 JwtFilter와 SecurityConfig에 /health 경로를 예외처리 해주었음.</li>
<li>수정된 코드를 다시 jar파일로 빌드 후 EC2에 다시 재빌딩 해주었더니 정상 작동 되었음.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="jwt-관련-문제-해결-과정">JWT 관련 문제 해결 과정</h3>
<ul>
<li><p>문제 2: EC2에서 서버가 아예 안 뜸</p>
<pre><code>WeakKeyException: key byte array is not secure enough</code></pre></li>
<li><p>원인</p>
<ul>
<li><p>JWT Secret Key 길이가 256bit 미만</p>
</li>
<li><p>Base64 디코딩을 시도했지만, 실제로는 Base64 문자열이 아니었음</p>
</li>
</ul>
</li>
<li><p>해결</p>
<ul>
<li><p>Base64 인코딩된 256bit 이상 키 사용</p>
</li>
<li><p>EC2 환경 변수로 주입</p>
<pre><code>export JWT_KEY=Base64로_인코딩된_충분히_긴_키</code></pre><pre><code>jwt:
secret:
key: ${JWT_KEY}</code></pre></li>
</ul>
</li>
</ul>
<hr>
<h3 id="jpa--rds-연결-문제-해결">JPA / RDS 연결 문제 해결</h3>
<ul>
<li><p>문제 3: Hibernate Dialect 오류</p>
<pre><code>Unable to determine Dialect without JDBC metadata</code></pre></li>
<li><p>원인</p>
<ul>
<li><p>EC2에 DB 환경 변수가 없었음</p>
</li>
<li><p>Spring Boot가 DB 정보를 아예 못 읽음</p>
</li>
</ul>
</li>
<li><p>확인</p>
<pre><code>env | grep DB</code></pre></li>
<li><p>해결</p>
<pre><code>export DB_URL=jdbc:mysql://RDS엔드포인트:3306/mydb
export DB_USERNAME=admin
export DB_PASSWORD=비밀번호</code></pre><pre><code>spring:
datasource:
  url: ${DB_URL}
  username: ${DB_USERNAME}
  password: ${DB_PASSWORD}</code></pre></li>
</ul>
<hr>
<h3 id="ec2-↔-rds-네트워크--보안-그룹-이해">EC2 ↔ RDS 네트워크 &amp; 보안 그룹 이해</h3>
<ul>
<li>핵심 구조<pre><code>[인터넷]
 ↓ 8080
[EC2 - Spring Boot]
 ↓ 3306
[RDS - MySQL]</code></pre></li>
</ul>
<blockquote>
<h4 id="사용한-보안-그룹">사용한 보안 그룹</h4>
</blockquote>
<ul>
<li><p>EC2 보안 그룹 (launch-wizard-1)</p>
<ul>
<li><p>22 (SSH)</p>
</li>
<li><p>8080 (Spring Boot API)</p>
</li>
<li><p>외부에서 서버 접근 허용</p>
</li>
</ul>
</li>
<li><p>RDS 보안 그룹 (rds-ec2-1)</p>
<ul>
<li><p>3306 포트</p>
</li>
<li><p>소스: EC2 보안 그룹</p>
</li>
<li><p>IP가 아닌 보안 그룹 기반 연결</p>
</li>
</ul>
</li>
</ul>
<h4 id="이-설정-덕분에-ec2-→-rds-연결-성공">이 설정 덕분에 EC2 → RDS 연결 성공</h4>
<hr>
<h3 id="jar-재빌드--배포-과정">JAR 재빌드 &amp; 배포 과정</h3>
<ul>
<li><p>로컬에서</p>
<pre><code>./gradlew clean bootJar</code></pre></li>
<li><p>EC2로 전송</p>
<pre><code>scp -i key.pem build/libs/expert-0.0.1-SNAPSHOT.jar ubuntu@EC2_IP:/home/ubuntu</code></pre></li>
<li><p>EC2 실행</p>
<pre><code>java -jar expert-0.0.1-SNAPSHOT.jar</code></pre></li>
<li><p>정상 실행 로그</p>
<pre><code>Tomcat started on port 8080
Started ExpertApplication</code></pre></li>
</ul>
<hr>
<h3 id="health-check-최종-확인">Health Check 최종 확인</h3>
<ul>
<li><p>브라우저에서 접속:</p>
<pre><code>http://EC2_PUBLIC_IP:8080/health</code></pre></li>
<li><p>응답:</p>
<pre><code>OK</code></pre></li>
<li><p>서버 실행 확인</p>
</li>
<li><p>인증 없이 접근 가능</p>
</li>
<li><p>과제 요구사항 충족</p>
</li>
</ul>
<hr>
<h3 id="얻은-것">얻은 것</h3>
<ul>
<li><p>AWS 배포에서 가장 중요한 건 코드보다 환경</p>
</li>
<li><p>보안 그룹은 “열어두는 설정”이 아니라 누구에게 열어두는지가 핵심</p>
</li>
<li><p>JWT / DB / 서버는 모두 환경 변수 기반으로 관리해야 안전하다!!</p>
</li>
<li><p>“된다”보다 “왜 되는지 설명할 수 있는 상태”가 중요함....</p>
</li>
</ul>
<blockquote>
<h3 id="정리-한-줄">정리 한 줄</h3>
</blockquote>
<h4 id="spring-boot-서버를-aws-ec2에-배포하고">Spring Boot 서버를 AWS EC2에 배포하고,</h4>
<h4 id="rds와-보안-그룹-기반으로-연동한-뒤">RDS와 보안 그룹 기반으로 연동한 뒤</h4>
<h4 id="인증-없이-접근-가능한-health-check-api를-통해">인증 없이 접근 가능한 Health Check API를 통해</h4>
<h4 id="서버의-live-상태를-확인할-수-있도록-구성했다">서버의 Live 상태를 확인할 수 있도록 구성했다.</h4>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[과제간 트러블 슈팅(AWS 세팅)]]></title>
            <link>https://velog.io/@d0ngx2_2/%EA%B3%BC%EC%A0%9C%EA%B0%84-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85AWS-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@d0ngx2_2/%EA%B3%BC%EC%A0%9C%EA%B0%84-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85AWS-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Mon, 29 Dec 2025 02:29:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="spring-boot-서버를-aws-ec2--rds로-배포하고-health-check-api까지-연결하기">Spring Boot 서버를 AWS EC2 + RDS로 배포하고 Health Check API까지 연결하기</h3>
</blockquote>
<h3 id="목표">목표</h3>
<ul>
<li><p>Spring Boot 애플리케이션을 AWS EC2에 배포한다</p>
</li>
<li><p>RDS(MySQL) 와 연동한다</p>
</li>
<li><p>서버가 살아있는지 확인할 수 있는 Health Check API를 만든다</p>
</li>
<li><p>Health API는 누구나 접근 가능해야 한다</p>
</li>
<li><p>배포 과정에서 발생한 문제를 직접 해결하며 AWS 네트워크 / 보안 흐름을 이해한다</p>
</li>
</ul>
<hr>
<h3 id="로컬-환경과-서버-환경의-차이-인식">로컬 환경과 서버 환경의 차이 인식</h3>
<ul>
<li><p>처음에는 IntelliJ에서 실행하면 잘 되는데, EC2에 배포한 JAR를 실행하면 다음과 같은 문제가 발생했다.</p>
<ul>
<li><p>403 Forbidden</p>
</li>
<li><p>JWT 관련 에러</p>
</li>
<li><p>DB 연결 실패</p>
</li>
<li><p>Hibernate Dialect 오류</p>
</li>
</ul>
</li>
</ul>
<h4 id="이-과정에서-로컬과-서버는-완전히-다른-환경이라는-걸-명확히-인식하게-됐다">이 과정에서 <strong>“로컬과 서버는 완전히 다른 환경”</strong>이라는 걸 명확히 인식하게 됐다.</h4>
<hr>
<h3 id="health-check-api-설계">Health Check API 설계</h3>
<ul>
<li><p>요구사항</p>
<ul>
<li><p>인증 없이 접근 가능</p>
</li>
<li><p>서버가 살아있다는 것만 확인하면 됨</p>
</li>
</ul>
</li>
<li><p>구현</p>
<pre><code class="language-java">@GetMapping(&quot;/health&quot;)
public ResponseEntity&lt;String&gt; health() {
  return ResponseEntity.ok(&quot;OK&quot;);
}
</code></pre>
</li>
</ul>
<p>Security 설정
.requestMatchers(&quot;/health&quot;).permitAll()</p>
<pre><code>
#### /auth/** 와 /health 는 JWT 없이 접근 가능하도록 설정

---

### JWT 관련 문제 해결 과정
- 문제 1: EC2에서 서버가 아예 안 뜸</code></pre><p>WeakKeyException: key byte array is not secure enough</p>
<pre><code>- 원인

  - JWT Secret Key 길이가 256bit 미만

  - Base64 디코딩을 시도했지만, 실제로는 Base64 문자열이 아니었음

- 해결

  - Base64 인코딩된 256bit 이상 키 사용

  - EC2 환경 변수로 주입</code></pre><p>export JWT_KEY=Base64로_인코딩된_충분히<em>긴</em>키</p>
<pre><code></code></pre><p>jwt:
  secret:
    key: ${JWT_KEY}</p>
<pre><code>---

### JPA / RDS 연결 문제 해결
- 문제 2: Hibernate Dialect 오류</code></pre><p>Unable to determine Dialect without JDBC metadata</p>
<pre><code>- 원인

  - EC2에 DB 환경 변수가 없었음

  - Spring Boot가 DB 정보를 아예 못 읽음

- 확인</code></pre><p>env | grep DB</p>
<pre><code>- 해결</code></pre><p>export DB_URL=jdbc:mysql://RDS엔드포인트:3306/mydb
export DB_USERNAME=admin
export DB_PASSWORD=비밀번호</p>
<pre><code></code></pre><p>spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}</p>
<pre><code>
---

### EC2 ↔ RDS 네트워크 &amp; 보안 그룹 이해
- 핵심 구조</code></pre><p>[인터넷]
   ↓ 8080
[EC2 - Spring Boot]
   ↓ 3306
[RDS - MySQL]</p>
<pre><code>
&gt; #### 사용한 보안 그룹

- EC2 보안 그룹 (launch-wizard-1)

  - 22 (SSH)

  - 8080 (Spring Boot API)

  - 외부에서 서버 접근 허용

- RDS 보안 그룹 (rds-ec2-1)

  - 3306 포트

  - 소스: EC2 보안 그룹

  - IP가 아닌 보안 그룹 기반 연결

#### 이 설정 덕분에 EC2 → RDS 연결 성공

---

### JAR 재빌드 &amp; 배포 과정
- 로컬에서</code></pre><p>./gradlew clean bootJar</p>
<pre><code>- EC2로 전송</code></pre><p>scp -i key.pem build/libs/expert-0.0.1-SNAPSHOT.jar ubuntu@EC2_IP:/home/ubuntu</p>
<pre><code>
- EC2 실행</code></pre><p>java -jar expert-0.0.1-SNAPSHOT.jar</p>
<pre><code>- 정상 실행 로그</code></pre><p>Tomcat started on port 8080
Started ExpertApplication</p>
<pre><code>---

### Health Check 최종 확인

- 브라우저에서 접속:</code></pre><p><a href="http://EC2_PUBLIC_IP:8080/health">http://EC2_PUBLIC_IP:8080/health</a></p>
<pre><code>
- 응답:</code></pre><p>OK</p>
<p>```</p>
<ul>
<li>서버 실행 확인</li>
<li>인증 없이 접근 가능</li>
<li>과제 요구사항 충족</li>
</ul>
<hr>
<h3 id="이번-경험에서-얻은-것">이번 경험에서 얻은 것</h3>
<ul>
<li><p>AWS 배포에서 가장 중요한 건 코드보다 환경</p>
</li>
<li><p>보안 그룹은 “열어두는 설정”이 아니라 누구에게 열어두는지가 핵심</p>
</li>
<li><p>JWT / DB / 서버는 모두 환경 변수 기반으로 관리해야 안전하다!!</p>
</li>
<li><p>“된다”보다 “왜 되는지 설명할 수 있는 상태”가 중요함....</p>
</li>
</ul>
<blockquote>
<h3 id="정리-한-줄">정리 한 줄</h3>
</blockquote>
<h4 id="spring-boot-서버를-aws-ec2에-배포하고">Spring Boot 서버를 AWS EC2에 배포하고,</h4>
<h4 id="rds와-보안-그룹-기반으로-연동한-뒤">RDS와 보안 그룹 기반으로 연동한 뒤</h4>
<h4 id="인증-없이-접근-가능한-health-check-api를-통해">인증 없이 접근 가능한 Health Check API를 통해</h4>
<h4 id="서버의-live-상태를-확인할-수-있도록-구성했다">서버의 Live 상태를 확인할 수 있도록 구성했다.</h4>
<hr>
<p>셋팅하고 이해하는데 오랜 시간이 걸렸지만 얼추 감이 잘 잡혔고, 왜 사용해야하는지를 알 수 있게 되었다 야호 ( •̀ ω •́ )y</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CS (스터디)]]></title>
            <link>https://velog.io/@d0ngx2_2/CS-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@d0ngx2_2/CS-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Wed, 24 Dec 2025 13:06:23 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h2 id="컴퓨터-구조">컴퓨터 구조</h2>
</blockquote>
<ul>
<li><p>컴퓨터가 어떻게 동작하는지, 어떤 부품으로 구성되어 있는지 이해하는 학문/관점.</p>
</li>
<li><p>소프트웨어가 실행되는 기반 구조를 이해하면 문제 해결, 성능/비용 최적화에 강해진다. </p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="컴퓨터가-이해하는-정보">컴퓨터가 이해하는 정보</h3>
</blockquote>
<ul>
<li>컴퓨터는 모두 <strong>이진수(0과 1)</strong>로 정보를 이해합니다.</li>
</ul>
<h4 id="데이터-data">데이터 (Data)</h4>
<ul>
<li><p>숫자, 문자, 이미지, 동영상 등 정적인 정보</p>
</li>
<li><p>컴퓨터가 처리하는 값들의 기본 형태 </p>
</li>
</ul>
<h4 id="명령어-instruction">명령어 (Instruction)</h4>
<ul>
<li><p>컴퓨터를 실제로 움직이게 하는 정보</p>
</li>
<li><p>데이터가 명령어의 재료라면, 명령어는 컴퓨터가 수행할 행동 자체</p>
</li>
</ul>
<hr>
<ul>
<li>예:
1과 2를 더하라
→ 1, 2는 데이터,
→ <em>더하라(add)</em>는 명령어 </li>
</ul>
<hr>
<blockquote>
<h3 id="컴퓨터의-4가지-핵심-부품">컴퓨터의 4가지 핵심 부품</h3>
</blockquote>
<ul>
<li>컴퓨터는 네 가지 주요 부품으로 구성되어 있고, 이들이 함께 작동해 프로그램을 수행합니다. </li>
</ul>
<h4 id="cpu-중앙처리장치">CPU (중앙처리장치)</h4>
<ul>
<li><p>컴퓨터의 두뇌</p>
</li>
<li><p>메모리에서 명령어/데이터를 읽고 해석하고 실행</p>
</li>
<li><p>내부 주요 구성 요소:</p>
</li>
<li><p>ALU (산술논리연산장치): 계산 처리</p>
<ul>
<li><p>레지스터: CPU 내부 임시 저장</p>
</li>
<li><p>제어장치 (CU): 동작 제어 신호 전송 및 명령 해석 </p>
</li>
</ul>
</li>
</ul>
<h4 id="메모리-주기억장치-ram">메모리 (주기억장치, RAM)</h4>
<ul>
<li><p>실행 중인 프로그램의 명령어와 데이터를 저장</p>
</li>
<li><p>프로그램이 실행되기 위해서는 반드시 메모리에 올라가 있어야 함 </p>
</li>
</ul>
<h4 id="보조기억장치">보조기억장치</h4>
<ul>
<li><p>대용량 저장 장치</p>
</li>
<li><p>전원이 꺼져도 데이터 유지</p>
</li>
<li><p>예: SSD, HDD, USB 등 </p>
</li>
</ul>
<h4 id="입출력장치-io">입출력장치 (I/O)</h4>
<ul>
<li><p>외부와 정보 교환 장치</p>
</li>
<li><p>예: 키보드, 마우스, 모니터, 프린터 등 </p>
</li>
</ul>
<h4 id="메인보드--시스템-버스">메인보드 &amp; 시스템 버스</h4>
<ul>
<li>컴퓨터 안의 부품들이 서로 통신하기 위해 꼭 필요한 것들입니다. </li>
</ul>
<h4 id="메인보드">메인보드</h4>
<ul>
<li><p>컴퓨터의 중앙 회로판</p>
</li>
<li><p>CPU, 메모리, 보조기억장치, I/O 장치 등 모든 핵심 부품이 연결됩니다. </p>
</li>
</ul>
<h4 id="시스템-버스">시스템 버스</h4>
<ul>
<li>컴퓨터 부품 간의 통신 통로 역할 </li>
</ul>
<table>
<thead>
<tr>
<th>버스 종류</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>주소 버스</strong></td>
<td>메모리 <em>주소</em> 전달</td>
</tr>
<tr>
<td><strong>데이터 버스</strong></td>
<td><em>데이터/명령</em> 전달</td>
</tr>
<tr>
<td><strong>제어 버스</strong></td>
<td><em>제어 신호</em> 전달</td>
</tr>
</tbody></table>
<h4 id="컴퓨터-동작-흐름-개념">컴퓨터 동작 흐름 개념</h4>
<ul>
<li><p>강의에서 자주 설명되는 컴퓨터 동작의 큰 흐름은 다음과 같습니다:</p>
<ul>
<li><p>프로그램과 데이터는 보조기억장치에 저장</p>
</li>
<li><p>실행을 위해 메모리로 로딩</p>
</li>
<li><p>CPU가 명령어를 하나씩 읽어 해석 → 실행</p>
</li>
<li><p>결과는 메모리에 저장되거나 I/O로 출력</p>
</li>
</ul>
</li>
<li><p>이 과정을 이해하면 컴퓨터가 어떻게 코드를 실제로 해석·실행하는지 큰 그림이 잡힌다.</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="요약">요약</h3>
</blockquote>
<h4 id="컴퓨터-구조의-큰-그림은">컴퓨터 구조의 큰 그림은</h4>
<h4 id="컴퓨터가-어떤-정보데이터명령어를-어떻게-처리하는가와">‘컴퓨터가 어떤 정보(데이터/명령어)를 어떻게 처리하는가’와</h4>
<h4 id="cpu-메모리-보조기억장치-입출력장치가-어떻게-협력하는가의-전체-틀을-이해하는-것입니다">‘CPU, 메모리, 보조기억장치, 입출력장치가 어떻게 협력하는가’의 전체 틀을 이해하는 것입니다.</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[과제 간 트러블 슈팅(2)]]></title>
            <link>https://velog.io/@d0ngx2_2/%EA%B3%BC%EC%A0%9C-%EA%B0%84-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%852</link>
            <guid>https://velog.io/@d0ngx2_2/%EA%B3%BC%EC%A0%9C-%EA%B0%84-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%852</guid>
            <pubDate>Tue, 23 Dec 2025 11:53:36 GMT</pubDate>
            <description><![CDATA[<h1 id="궁금궁금featquerydsl">궁금궁금(feat.QueryDsl)</h1>
<blockquote>
</blockquote>
<pre><code class="language-java">Long total = queryFactory
    .select(todo.id.count())
    .from(todo)
    .where(
        todo.title.contains(keyword),
        todo.createdAt.between(start, end)
    )
    .fetchOne();</code></pre>
<h4 id="페이징-응답을-만들기-위해-전체-데이터-개수를-구하는-쿼리이다">페이징 응답을 만들기 위해 “전체 데이터 개수”를 구하는 쿼리이다.</h4>
<hr>
<blockquote>
<h3 id="왜-이-코드가-추가로-필요할까">왜 이 코드가 추가로 필요할까?</h3>
</blockquote>
<ul>
<li><p>페이징 응답에는 보통 이 정보들이 필요합니다</p>
</li>
<li><p>현재 페이지의 데이터 목록 (content)</p>
</li>
<li><p>전체 데이터 개수 (totalElements)</p>
</li>
<li><p>전체 페이지 수 (totalPages)</p>
</li>
<li><p>현재 페이지 번호</p>
</li>
</ul>
<hr>
<blockquote>
<h2 id="그런데-">그런데 !!!</h2>
</blockquote>
<ul>
<li><p>limit, offset이 들어간 조회 쿼리로는 전체 개수를 알 수 없다.</p>
</li>
<li><p>목록 조회 쿼리만 있으면 생기는 문제</p>
<pre><code class="language-java">List&lt;TodoSearchResponse&gt; content = queryFactory
  .select(...)
  .from(todo)
  .where(...)
  .orderBy(todo.createdAt.desc())
  .offset(pageable.getOffset())
  .limit(pageable.getPageSize())
  .fetch();</code></pre>
</li>
<li><p>“현재 페이지 데이터”만 조회</p>
</li>
<li><p>DB에는 총 몇 건이 있는지 모름!!</p>
</li>
<li><p>프론트 입장에서는</p>
<h4 id="다음-페이지가-있는지">“다음 페이지가 있는지?”,</h4>
<h4 id="총-몇-페이지인지를-알-수-없음">“총 몇 페이지인지?”를 알 수 없음</h4>
</li>
</ul>
<blockquote>
<h4 id="그래서-count-쿼리가-필요함">그래서 count 쿼리가 필요함</h4>
</blockquote>
<h4 id="select-counttodoid-from-todo-">select count(todo.id) from todo ...</h4>
<hr>
<ul>
<li><p>페이징 조건 ❌ (limit/offset 없음)</p>
</li>
<li><p>검색 조건만 동일하게 적용</p>
</li>
<li><p>전체 검색 결과 개수만 조회</p>
<ul>
<li>이 값으로:<pre><code class="language-java">PageImpl&lt;&gt;(content, pageable, total)</code></pre>
</li>
</ul>
</li>
<li><p>프론트에서 정확한 페이지 계산 가능</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="왜-목록-조회랑-쿼리를-분리했을까">왜 목록 조회랑 쿼리를 분리했을까?</h3>
</blockquote>
<h4 id="성능-이유">성능 이유</h4>
<ul>
<li><p>count는 필요한 컬럼이 id 하나뿐</p>
</li>
<li><p>projection / join / orderBy 필요 없음</p>
</li>
</ul>
<h4 id="책임-분리">책임 분리</h4>
<ul>
<li><p>조회 쿼리 → 데이터 가져오기</p>
</li>
<li><p>count 쿼리 → 개수 계산</p>
<ul>
<li>JPA &amp; QueryDSL에서 권장되는 패턴</li>
</ul>
</li>
</ul>
<blockquote>
<h3 id="왜-fetchone을-쓰는가">왜 fetchOne()을 쓰는가?</h3>
</blockquote>
<pre><code class="language-java">.select(todo.id.count())</code></pre>
<ul>
<li><p>count()는 결과가 항상 1행</p>
</li>
<li><p>리스트가 아니라 단일 값</p>
<ul>
<li>그래서:<pre><code class="language-java">fetch() ❌ (List&lt;Long&gt;)
</code></pre>
</li>
</ul>
</li>
</ul>
<p>fetchOne() ✅ (Long)</p>
<pre><code>
---

&gt;### 왜 keyword, start, end 조건을 똑같이 넣어야 할까?

#### “검색 조건이 다르면 total이 의미 없어짐”

- 목록 조회 결과: 5개

- total: 전체 100개 (이러면 안됨)

#### 목록 쿼리와 count 쿼리는 조건이 반드시 동일해야 함!!

---

&gt;### 요약
#### 페이징 처리에서는 현재 페이지의 데이터 조회와
#### 전체 데이터 개수 조회를 분리해야 하며,
#### 이를 위해 QueryDSL에서 count 전용 쿼리를 추가로 작성한다.


---

### 실전 코드 적용

```java
// 실제 목록 조회 쿼리 (페이징 대상)
List&lt;TodoSearchResponse&gt; content = queryFactory
        // TodoSearchResponse DTO로 바로 매핑하기 위해 Projections.constructor 사용
        // 엔티티 전체가 아니라 &quot;필요한 필드만&quot; 조회해서 성능 최적화
        .select(Projections.constructor(
                TodoSearchResponse.class,

                // 일정 제목
                todo.title,

                // 해당 일정의 담당자 수
                // JOIN으로 인해 중복 row가 생길 수 있으므로 countDistinct 사용
                manager.id.countDistinct(),

                // 해당 일정의 댓글 수
                // 댓글도 JOIN으로 중복될 수 있으므로 countDistinct 사용
                comment.id.countDistinct()
        ))
        // 기준 테이블은 todo
        .from(todo)

        // 일정과 담당자 관계 LEFT JOIN
        // 담당자가 없는 일정도 검색 결과에 포함시키기 위해 LEFT JOIN
        .leftJoin(todo.managers, manager)

        // 담당자와 유저 JOIN (닉네임 검색을 위함)
        .leftJoin(manager.user, user)

        // 일정과 댓글 LEFT JOIN
        // 댓글이 없는 일정도 검색 결과에 포함
        .leftJoin(todo.comments, comment)

        // 검색 조건
        .where(
                // 제목 키워드 부분 일치 검색
                todo.title.contains(keyword),

                // 담당자 닉네임 부분 일치 검색
                manager.user.nickName.contains(managerNickname),

                // 일정 생성일 범위 검색
                todo.createdAt.between(start, end)
        )

        // todo 기준으로 집계해야 하므로 groupBy 필수
        // count(), countDistinct()를 사용했기 때문
        .groupBy(todo.id)

        // 최신 일정이 먼저 나오도록 정렬
        .orderBy(todo.createdAt.desc())

        // 한 페이지에 가져올 데이터 개수 제한
        .limit(pageable.getPageSize())

        // 실제 데이터 조회
        .fetch();

// 전체 데이터 개수 조회 쿼리 (count 쿼리)
Long count = queryFactory
        // 전체 검색 결과 개수를 구하기 위한 count 쿼리
        .select(todo.id.count())

        // 기준 테이블은 동일하게 todo
        .from(todo)

        // 목록 조회와 &quot;동일한 검색 조건&quot;을 사용해야 함
        // 그래야 페이지 수(totalPages)가 정확해짐
        .where(
                todo.title.contains(keyword),
                todo.createdAt.between(start, end)
        )

        // count는 항상 단일 결과이므로 fetchOne 사용
        .fetchOne();
</code></pre><hr>
<blockquote>
<h3 id="이-코드에서-꼭-이해해야-할-핵심-포인트">이 코드에서 꼭 이해해야 할 핵심 포인트</h3>
</blockquote>
<ul>
<li><p>목록 조회 쿼리와 count 쿼리를 분리한 이유</p>
<ul>
<li><p>목록 조회: limit, groupBy, join → 데이터 표현용</p>
</li>
<li><p>count 쿼리: 단순한 구조 → 전체 개수 계산용</p>
</li>
</ul>
</li>
</ul>
<h4 id="페이징에서-정확한-total-값을-얻기-위한-필수-구조">페이징에서 정확한 total 값을 얻기 위한 필수 구조</h4>
<hr>
<blockquote>
<h3 id="왜-countdistinct를-쓰는가">왜 countDistinct를 쓰는가?</h3>
</blockquote>
<pre><code class="language-java">manager.id.countDistinct()
comment.id.countDistinct()
JOIN이 들어가면 하나의 todo가 여러 row로 늘어남</code></pre>
<ul>
<li><p>그냥 count() 쓰면 중복 카운트 발생</p>
</li>
<li><p>countDistinct()로 실제 개수만 집계</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="왜-groupbytodoid가-필요한가">왜 groupBy(todo.id)가 필요한가?</h3>
</blockquote>
<ul>
<li><p>count / countDistinct는 집계 함수</p>
</li>
<li><p>집계 대상이 todo 기준이므로 groupBy(todo.id) 필수</p>
</li>
<li><p>없으면 SQL 에러 발생</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="왜-count-쿼리에는-join이-없는가">왜 count 쿼리에는 JOIN이 없는가?</h3>
</blockquote>
<ul>
<li><p>전체 개수만 알면 됨</p>
</li>
<li><p>JOIN은 성능 저하만 유발</p>
</li>
</ul>
<h4 id="의도적으로-단순한-count-쿼리-작성--성능-최적화">의도적으로 단순한 count 쿼리 작성 = 성능 최적화</h4>
<blockquote>
<h3 id="요약">요약</h3>
</blockquote>
<h4 id="querydsl-페이징에서는">QueryDSL 페이징에서는</h4>
<h4 id="목록-조회-쿼리와-전체-개수count-쿼리를-분리하고">목록 조회 쿼리와 전체 개수(count) 쿼리를 분리하고,</h4>
<h4 id="목록-조회는-projection--groupby로-최적화하며">목록 조회는 Projection + groupBy로 최적화하며</h4>
<h4 id="count-쿼리는-조건만-동일하게-유지한-채-최대한-단순하게-작성한다">count 쿼리는 조건만 동일하게 유지한 채 최대한 단순하게 작성한다.</h4>
<hr>
<h2 id="전체-코드">전체 코드</h2>
<pre><code class="language-java">package org.example.expert.domain.todo.searchRepository;

import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.example.expert.domain.todo.dto.response.TodoSearchResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

import java.time.LocalDateTime;
import java.util.List;

import static org.example.expert.domain.comment.entity.QComment.comment;
import static org.example.expert.domain.manager.entity.QManager.manager;
import static org.example.expert.domain.todo.entity.QTodo.todo;
import static org.example.expert.domain.user.entity.QUser.user;

@RequiredArgsConstructor
public class SearchRepositoryImpl implements SearchRepository {

    private final JPAQueryFactory queryFactory;


    @Override
    public Page&lt;TodoSearchResponse&gt; search(
            String keyword,
            String managerNickname,
            LocalDateTime start,
            LocalDateTime end,
            Pageable pageable
    ) {
        List&lt;TodoSearchResponse&gt; content = queryFactory
                .select(Projections.constructor(
                        TodoSearchResponse.class,
                        todo.title,
                        manager.id.countDistinct(),
                        comment.id.countDistinct()
                ))
                .from(todo)
                .leftJoin(todo.managers, manager)
                .leftJoin(manager.user, user)
                .leftJoin(todo.comments, comment)
                .where(
                        todo.title.contains(keyword),
                        manager.user.nickName.contains(managerNickname),
                        todo.createdAt.between(start, end)
                )
                .groupBy(todo.id)
                .orderBy(todo.createdAt.desc())
                .limit(pageable.getPageSize())
                .fetch();

        Long total = queryFactory
                .select(todo.id.count())
                .from(todo)
                .where(
                        todo.title.contains(keyword),
                        todo.createdAt.between(start, end)
                )
                .fetchOne();

        return new PageImpl&lt;&gt;(content, pageable, total == null ? 0 : total);
    }</code></pre>
<hr>
<blockquote>
<h3 id="서비스-→-서비스-di는-별로인가feattransactional의-옵션">“서비스 → 서비스 DI”는 별로인가?(feat.Transactional의 옵션)</h3>
</blockquote>
<ul>
<li><p>보통 사람들이 꺼리는 이유:</p>
<ul>
<li><p>❌ 안 좋은 경우</p>
</li>
<li><p>서비스끼리 순환 의존성</p>
</li>
<li><p>한 서비스가 다른 서비스의 비즈니스 로직을 대신 수행</p>
<pre><code>ServiceA -&gt; ServiceB
ServiceB -&gt; ServiceC
ServiceC -&gt; ServiceA ❌</code></pre></li>
</ul>
</li>
</ul>
<h4 id="이건-설계-붕괴">이건 설계 붕괴</h4>
<hr>
<blockquote>
<h3 id="괜찮은-경우는">괜찮은 경우는?</h3>
</blockquote>
<ul>
<li><p>핵심 차이</p>
</li>
<li><p>ManagerService는 비즈니스 로직 담당</p>
</li>
<li><p>LogService는 기술적 관심사(로깅) 담당</p>
</li>
</ul>
<h4 id="즉-역할이-명확하게-분리되어-있음">즉, 역할이 명확하게 분리되어 있음</h4>
<blockquote>
<h3 id="이-구조가-맞는-이유를-한-줄씩-뜯어보면">이 구조가 “맞는 이유”를 한 줄씩 뜯어보면</h3>
</blockquote>
<h4 id="책임-분리가-명확하다-srp">책임 분리가 명확하다 (SRP)</h4>
<ul>
<li>서비스 : 책임</li>
<li>ManagerService : 매니저 등록 비즈니스 규칙</li>
<li>LogService : 요청 기록을 DB에 안전하게 남김</li>
</ul>
<h4 id="managerservice가-로그-테이블-직접-만지면-책임이-섞임">ManagerService가 로그 테이블 직접 만지면 책임이 섞임</h4>
<hr>
<blockquote>
<h3 id="트랜잭션-경계를-분리하기-위함">트랜잭션 경계를 분리하기 위함</h3>
</blockquote>
<ul>
<li>핵심 목적은 이거 👇</li>
</ul>
<h4 id="매니저-등록이-실패해도-로그는-반드시-저장">“매니저 등록이 실패해도 로그는 반드시 저장”</h4>
<ul>
<li><p>이건</p>
<pre><code class="language-java">@Transactional(REQUIRES_NEW)</code></pre>
</li>
<li><p>메서드 단위 트랜잭션 분리가 필요하고,</p>
</li>
<li><p>클래스 안에서는 불가능</p>
</li>
<li><p>Spring AOP 프록시 특성</p>
</li>
</ul>
<h4 id="그래서-별도-서비스로-분리--di-가-올바른-구조">그래서 별도 서비스로 분리 + DI 가 올바른 구조.</h4>
<h4 id="중요한-포인트-많이-놓친다고함">중요한 포인트 (많이 놓친다고함.)</h4>
<pre><code class="language-java">@Transactional(propagation = REQUIRES_NEW)
public void saveLog() { ... }</code></pre>
<ul>
<li><p>이걸 같은 서비스 클래스에서 호출하면 적용 안 됨</p>
</li>
<li><p>왜냐하면:</p>
<ul>
<li><p>Spring의 @Transactional은 프록시 기반</p>
</li>
<li><p>자기 자신 메서드 호출(self-invocation)은 프록시를 안 탐</p>
</li>
</ul>
</li>
</ul>
<h4 id="그래서-무조건-다른-bean이어야-함">그래서 무조건 다른 Bean이어야 함</h4>
<h4 id="logservice-분리는-필수">LogService 분리는 필수</h4>
<hr>
<blockquote>
<h3 id="생각보다-실무에서-흔한-패턴이라고-한다feat튜터님">생각보다 실무에서 흔한 패턴이라고 한다!(feat.튜터님!)</h3>
</blockquote>
<p>실무 예시들:</p>
<pre><code class="language-java">OrderService → PaymentLogService

UserService → LoginHistoryService

ReservationService → AuditLogService</code></pre>
<h4 id="비즈니스-서비스-→-로그이력-서비스">“비즈니스 서비스 → 로그/이력 서비스”</h4>
<h4 id="매우-정상적인-구조">매우 정상적인 구조</h4>
<hr>
<blockquote>
<h3 id="그럼-언제-서비스-→-서비스-di가-안-좋을까">그럼 언제 서비스 → 서비스 DI가 안 좋을까?</h3>
</blockquote>
<ul>
<li><p>피해야 할 경우 체크리스트</p>
<ul>
<li><p>서로가 서로를 호출한다 (순환 참조)</p>
</li>
<li><p>A 서비스가 B 서비스의 도메인 규칙을 대신 처리</p>
</li>
<li><p>“편해서” 그냥 갖다 쓴 경우</p>
</li>
<li><p>공통 로직이라고 다 서비스로 빼버린 경우</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="요약-1">요약</h3>
</blockquote>
<h4 id="이-구조는-비즈니스-로직과-기술적-관심사를-분리하고">이 구조는 비즈니스 로직과 기술적 관심사를 분리하고,</h4>
<h4 id="트랜잭션-전파-옵션requires_new을-적용하기-위한-의도적인-서비스-분리로">트랜잭션 전파 옵션(REQUIRES_NEW)을 적용하기 위한 의도적인 서비스 분리로</h4>
<h4 id="spring-aop-특성을-고려한-올바른-설계였다">Spring AOP 특성을 고려한 올바른 설계였다!</h4>
<blockquote>
<h3 id="요약2">요약2</h3>
</blockquote>
<h4 id="서비스-간-di는-무조건-나쁜-설계가-아니라">서비스 간 DI는 무조건 나쁜 설계가 아니라,</h4>
<h4 id="책임이-명확히-분리되고-트랜잭션기술적-관심사를-분리하기-위한-경우에는-권장되는-구조다">책임이 명확히 분리되고 트랜잭션/기술적 관심사를 분리하기 위한 경우에는 권장되는 구조다.</h4>
<h4 id="특히-transactionalrequires_new는-자기-자신-호출로는-동작하지-않기-때문에">특히 @Transactional(REQUIRES_NEW)는 자기 자신 호출로는 동작하지 않기 때문에</h4>
<h4 id="별도의-서비스-분리가-필수적이다">별도의 서비스 분리가 필수적이다.</h4>
<hr>
<h4 id="과제를-하면서-딥한-지식들이-점점-늘어난다-재밌으면서도-항상-부족한-느낌을-받아서-아쉽기도-하다（；´д｀）ゞ">과제를 하면서 딥한 지식들이 점점 늘어난다. 재밌으면서도 항상 부족한 느낌을 받아서 아쉽기도 하다..（；´д｀）ゞ</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[과제 간 트러블 슈팅(1)]]></title>
            <link>https://velog.io/@d0ngx2_2/%EA%B3%BC%EC%A0%9C-%EA%B0%84-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%851</link>
            <guid>https://velog.io/@d0ngx2_2/%EA%B3%BC%EC%A0%9C-%EA%B0%84-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%851</guid>
            <pubDate>Mon, 22 Dec 2025 11:52:37 GMT</pubDate>
            <description><![CDATA[<h1 id="왜">“왜?”</h1>
<blockquote>
<h2 id="날짜-검색에서-localdate-vs-localdatetime">날짜 검색에서 LocalDate vs LocalDateTime</h2>
</blockquote>
<h3 id="왜-localdate로-입력을-받았을까">왜 LocalDate로 입력을 받았을까?</h3>
<ul>
<li><p>사용자는 날짜만 신경 쓰지, 시:분:초까지 직접 입력할 필요가 없음</p>
</li>
<li><p>UI/UX 관점에서 날짜 입력이 훨씬 직관적임</p>
</li>
</ul>
<hr>
<h3 id="그런데-응답은-왜-localdatetime일까">그런데 응답은 왜 LocalDateTime일까?</h3>
<ul>
<li><p>DB에는 생성 시점이 LocalDateTime으로 저장됨</p>
</li>
<li><p>응답 DTO는 정확한 생성 시각을 보여주는 게 맞음</p>
</li>
</ul>
<h4 id="입력은-단순하게localdate-출력은-정확하게localdatetime">입력은 단순하게(LocalDate), 출력은 정확하게(LocalDateTime)</h4>
<h4 id="역할에-맞게-타입을-분리하는-것이-설계적으로-더-좋다">역할에 맞게 타입을 분리하는 것이 설계적으로 더 좋다.</h4>
<hr>
<blockquote>
<h2 id="datetimeformat--atstartofday--localtimemax를-쓰는-이유">@DateTimeFormat + atStartOfDay / LocalTime.MAX를 쓰는 이유</h2>
</blockquote>
<ul>
<li>@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)</li>
<li>LocalDate startDate;</li>
</ul>
<h3 id="왜-이런-변환-로직이-필요한가">왜 이런 변환 로직이 필요한가?</h3>
<ul>
<li><p>LocalDate는 시간 정보가 없음</p>
</li>
<li><p>DB 컬럼은 LocalDateTime</p>
<ul>
<li><p>그래서:</p>
</li>
<li><p>시작일 → 00:00:00</p>
</li>
<li><p>종료일 → 23:59:59.999999999</p>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="코드가-지저분한데-다른-방법은">코드가 지저분한데 다른 방법은?</h3>
<ul>
<li><p>이 방식이 의도가 가장 명확하다고 함.</p>
</li>
<li><p>QueryDSL/JPA에서 시간 경계 버그를 가장 확실히 방지</p>
</li>
</ul>
<h4 id="지저분해-보여도-명시적인-코드가-가장-안전하다">“지저분해 보여도, 명시적인 코드가 가장 안전하다”</h4>
<blockquote>
<h2 id="querydsl에서-booleanexpression이-null인데도-동작하는-이유">QueryDSL에서 BooleanExpression이 null인데도 동작하는 이유</h2>
</blockquote>
<pre><code class="language-java">private BooleanExpression titleContains(String keyword) {
    return keyword == null ? null : todo.title.contains(keyword);
}</code></pre>
<h3 id="null-반환인데-왜-검색-조건이-적용될까">null 반환인데 왜 검색 조건이 적용될까?</h3>
<ul>
<li><p>QueryDSL의 where()는 null 조건을 자동으로 무시한다.</p>
</li>
<li><p>조건이 있으면 AND로 결합하고, 없으면 제외한다고 한다.</p>
<pre><code class="language-java">query.where(
  titleContains(keyword),
  createdAtBetween(start, end)
);</code></pre>
</li>
</ul>
<h4 id="null-safe-동적-쿼리를-위한-의도된-설계">null-safe 동적 쿼리를 위한 의도된 설계</h4>
<ul>
<li><p>장점</p>
<ul>
<li><p>if-else 지옥 방지</p>
</li>
<li><p>조건 조합 폭발 방지</p>
</li>
<li><p>가독성, 유지보수성 ↑</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="fetchone-vs-fetch-차이-강의내용-중복">fetchOne() vs fetch() 차이 (강의내용 중복)</h2>
</blockquote>
<h3 id="왜-fetchone을-함부로-쓰면-안-될까">왜 fetchOne을 함부로 쓰면 안 될까?</h3>
<pre><code class="language-java">fetchOne()</code></pre>
<ul>
<li><p>결과가 1개일 거라 확신할 때만</p>
</li>
<li><p>2개 이상이면 예외 발생</p>
<pre><code class="language-java">fetch()</code></pre>
</li>
<li><p>리스트 조회</p>
</li>
<li><p>결과 개수와 무관</p>
</li>
</ul>
<h4 id="단건-조회가-보장될-때만-fetchone">“단건 조회가 보장될 때만 fetchOne”</h4>
<hr>
<blockquote>
<h2 id="optional을-쓰는-이유--orelsethrow-자동완성">Optional을 쓰는 이유 &amp; orElseThrow 자동완성</h2>
</blockquote>
<h3 id="optional을-왜-쓰는가">Optional을 왜 쓰는가?</h3>
<ul>
<li><p>null 반환 → 실수로 NPE 발생</p>
</li>
<li><p>Optional → “없을 수도 있음”을 타입으로 강제</p>
<pre><code class="language-java">userRepository.findById(id)
  .orElseThrow(...)</code></pre>
<h3 id="optional-안-쓰면-orelsethrow-자동완성이-안-뜬-이유">Optional 안 쓰면 orElseThrow 자동완성이 안 뜬 이유?</h3>
</li>
<li><p>orElseThrow()는 Optional 전용 메서드</p>
</li>
<li><p>엔티티 자체에는 존재하지 않음</p>
</li>
</ul>
<h4 id="optional은-단순-편의가-아니라">Optional은 단순 편의가 아니라</h4>
<h4 id="null-가능성을-코드-레벨에서-드러내는-장치">null 가능성을 코드 레벨에서 드러내는 장치</h4>
<hr>
<blockquote>
<h2 id="n1-문제가-발생한-이유와-해결-방식">N+1 문제가 발생한 이유와 해결 방식</h2>
</blockquote>
<h3 id="왜-n1이-발생했을까">왜 N+1이 발생했을까?</h3>
<ul>
<li><p>연관 엔티티가 LAZY 로딩</p>
</li>
<li><p>반복 접근 시 쿼리가 추가로 실행됨</p>
</li>
</ul>
<h3 id="해결-방법은">해결 방법은?</h3>
<pre><code class="language-java">fetch join</code></pre>
<ul>
<li><p>QueryDSL에서 join + fetch</p>
</li>
<li><p>필요한 필드만 Projections로 조회</p>
</li>
</ul>
<h4 id="조회용-쿼리는-엔티티-조회가-아니라-dto-조회가-더-적합한-경우가-많다">“조회용 쿼리는 엔티티 조회가 아니라 DTO 조회가 더 적합한 경우가 많다”</h4>
<hr>
<blockquote>
<h2 id="securityconfig에-permitall이-있는데-jwt-필터에서-또-체크하는-이유">SecurityConfig에 permitAll이 있는데 JWT 필터에서 또 체크하는 이유</h2>
</blockquote>
<h3 id="securityconfig면-끝-아닌가">SecurityConfig면 끝 아닌가?</h3>
<pre><code class="language-java">.anyRequest().authenticated()</code></pre>
<ul>
<li><p>이건 인가(Authorization) 단계</p>
</li>
<li><p>JwtFilter는 그보다 앞단의 인증(Authentication) 필터</p>
</li>
</ul>
<h3 id="그래서-필터에서도-분기-처리가-필요한-이유">그래서 필터에서도 분기 처리가 필요한 이유</h3>
<ul>
<li><p>필터는 무조건 실행됨</p>
</li>
<li><p>/auth/** 요청에서도 JWT 검사하면 오류 발생</p>
<pre><code class="language-java">@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
  return request.getRequestURI().startsWith(&quot;/auth&quot;);
}</code></pre>
</li>
</ul>
<h4 id="필터-책임-인증-정보-생성">필터 책임: 인증 정보 생성</h4>
<h4 id="securityconfig-책임-접근-허용차단">SecurityConfig 책임: 접근 허용/차단</h4>
<hr>
<blockquote>
<h2 id="usernamepasswordauthenticationfilter-앞에-jwt-필터를-두는-이유">UsernamePasswordAuthenticationFilter 앞에 JWT 필터를 두는 이유</h2>
</blockquote>
<h3 id="이-필터는-뭐-하는-놈인가">이 필터는 뭐 하는 놈인가?</h3>
<ul>
<li><p>폼 로그인 기반</p>
</li>
<li><p>ID/PW 인증 처리</p>
</li>
</ul>
<h3 id="왜-jwt-필터가-앞에-있어야-할까">왜 JWT 필터가 앞에 있어야 할까?</h3>
<ul>
<li><p>JWT는 이미 인증된 사용자</p>
</li>
<li><p>UsernamePasswordAuthenticationFilter까지 갈 필요 없음</p>
</li>
</ul>
<h4 id="jwt-인증이-먼저-끝나야">JWT 인증이 먼저 끝나야</h4>
<h4 id="securitycontext가-채워지고">SecurityContext가 채워지고</h4>
<h4 id="이후-인가-로직이-정상-동작">이후 인가 로직이 정상 동작</h4>
<hr>
<blockquote>
<h2 id="오늘의-핵심-정리">오늘의 핵심 정리</h2>
</blockquote>
<h4 id="spring--jpa--security에서의-설정과-패턴은">“Spring / JPA / Security에서의 설정과 패턴은</h4>
<h4 id="될-때까지-맞추는-코드가-아니라">‘될 때까지 맞추는 코드’가 아니라</h4>
<h4 id="왜-이-레이어에서-이-책임을-가지는지를-이해하는-게-중요하다">왜 이 레이어에서 이 책임을 가지는지를 이해하는 게 중요하다!!!”</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 스프링 부트 개념 정리(Index)]]></title>
            <link>https://velog.io/@d0ngx2_2/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACIndex</link>
            <guid>https://velog.io/@d0ngx2_2/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACIndex</guid>
            <pubDate>Fri, 19 Dec 2025 12:55:21 GMT</pubDate>
            <description><![CDATA[<h1 id="인덱스index">인덱스(Index)</h1>
<ul>
<li>인덱스는 데이터 조회 속도를 높이기 위한 ‘검색용 지도’이다.</li>
</ul>
<blockquote>
<h2 id="인덱스-핵심-개념-정리">인덱스 핵심 개념 정리</h2>
</blockquote>
<ul>
<li><p>원하는 데이터를 빠르게 조회하기 위한 자료구조</p>
</li>
<li><p>테이블과는 별도로 저장됨</p>
</li>
<li><p>일반적으로 B-Tree 구조 사용</p>
</li>
<li><p>PRIMARY KEY, UNIQUE 제약 조건 컬럼에는 자동 생성</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="인덱스-적용-전--후-차이">인덱스 적용 전 / 후 차이</h3>
</blockquote>
<table>
<thead>
<tr>
<th>구분</th>
<th>인덱스 없음</th>
<th>인덱스 있음</th>
</tr>
</thead>
<tbody><tr>
<td>검색 방식</td>
<td>전체 테이블 탐색(Full Scan)</td>
<td>인덱스 탐색</td>
</tr>
<tr>
<td>시간 복잡도</td>
<td>O(n)</td>
<td>O(log n)</td>
</tr>
<tr>
<td>I/O 비용</td>
<td>매우 높음</td>
<td>낮음</td>
</tr>
<tr>
<td>추가 저장 공간</td>
<td>없음</td>
<td>필요</td>
</tr>
<tr>
<td>INSERT/UPDATE 성능</td>
<td>빠름</td>
<td>약간 느려짐</td>
</tr>
</tbody></table>
<h4 id="조회-성능은-크게-향상되지만-쓰기-성능과-공간-비용은-증가">조회 성능은 크게 향상되지만, 쓰기 성능과 공간 비용은 증가</h4>
<blockquote>
<h3 id="인덱스-실습">인덱스 실습</h3>
</blockquote>
<ul>
<li><p>사용자 테이블에 대량 데이터(천만 건) 생성</p>
</li>
<li><p>인덱스 미적용 상태에서 조회 시 전체 테이블 스캔 발생</p>
</li>
<li><p>name 컬럼에 인덱스 생성 후
→ 동일 조건 조회가 인덱스 탐색으로 전환되어 속도 개선</p>
<pre><code>CREATE INDEX idx_user_name ON user(name);</code></pre></li>
</ul>
<blockquote>
<h3 id="정리">정리</h3>
</blockquote>
<h4 id="인덱스는-조회-성능을-비약적으로-향상시키지만">인덱스는 조회 성능을 비약적으로 향상시키지만,</h4>
<h4 id="무분별하게-사용하면-쓰기-성능-저하와-공간-낭비를-유발한다">무분별하게 사용하면 쓰기 성능 저하와 공간 낭비를 유발한다.</h4>
<h4 id="자주-조회되는-컬럼에만-전략적으로-적용하는-것이-중요✨">자주 조회되는 컬럼에만 전략적으로 적용하는 것이 중요✨</h4>
<hr>
<blockquote>
<h2 id="인덱스는-단순한-정렬이-아님">인덱스는 단순한 정렬이 아님</h2>
</blockquote>
<h4 id="인덱스는-단순한-가나다순-정렬이-아니라">인덱스는 단순한 가나다순 정렬이 아니라,</h4>
<h4 id="데이터를-빠르게-찾기-위한-b-tree-기반의-탐색-지도이다">데이터를 빠르게 찾기 위한 B-Tree 기반의 탐색 지도이다.</h4>
<ul>
<li><p>단순 정렬: 처음부터 끝까지 순차 탐색</p>
</li>
<li><p>B-Tree 인덱스: 분기(branch)를 따라 필요한 범위만 탐색</p>
</li>
<li><p>탐색 비용은 트리 높이에 비례 → O(log n)</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="b-tree-인덱스-동작-원리">B-Tree 인덱스 동작 원리</h3>
</blockquote>
<ul>
<li><p>루트 → 중간 노드 → 리프 노드 순으로 비교</p>
</li>
<li><p>불필요한 데이터는 건너뛰고 정확한 경로만 탐색</p>
</li>
<li><p>Full Scan 대비 수백~수천 배 빠름</p>
<ul>
<li>WHERE id = 60 같은 조회도<h4 id="몇-단계-비교만으로-실제-레코드-위치-도달">몇 단계 비교만으로 실제 레코드 위치 도달</h4>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="인덱스-스캔-종류-요약">인덱스 스캔 종류 요약</h2>
</blockquote>
<h3 id="unique-scan">Unique Scan</h3>
<ul>
<li><p>PK / UNIQUE 컬럼 조회</p>
</li>
<li><p>정확히 한 건 탐색</p>
</li>
<li><p>가장 빠름</p>
</li>
<li><p>SELECT * FROM users WHERE id = 10;</p>
</li>
</ul>
<h3 id="range-scan">Range Scan</h3>
<ul>
<li><p>범위 조건 조회</p>
</li>
<li><p>시작 위치 탐색 후 연속 읽기</p>
</li>
<li><p>정렬된 인덱스 덕분에 ORDER BY 효율적</p>
<pre><code>WHERE age BETWEEN 20 AND 30
WHERE name LIKE &#39;abc%&#39;</code></pre></li>
</ul>
<h3 id="full-index-scan">Full Index Scan</h3>
<ul>
<li><p>인덱스 전체를 순서대로 탐색</p>
</li>
<li><p>WHERE 조건은 없지만 ORDER BY 최적화 가능</p>
<pre><code>SELECT * FROM users ORDER BY name;</code></pre></li>
</ul>
<h3 id="index-condition-pushdown-icp">Index Condition Pushdown (ICP)</h3>
<ul>
<li><p>조건 일부를 인덱스 단계에서 미리 필터링</p>
</li>
<li><p>테이블 접근 횟수 감소 → 디스크 I/O 절약</p>
<pre><code>WHERE status = &#39;PAID&#39; AND order_date &gt; &#39;2025-11-01&#39;</code></pre></li>
</ul>
<h3 id="table-full-scan">Table Full Scan</h3>
<ul>
<li><p>인덱스를 전혀 사용하지 못하는 경우</p>
</li>
<li><p>모든 행을 직접 비교 → 가장 느림</p>
<pre><code>WHERE city = &#39;Seoul&#39; -- 인덱스 없음</code></pre></li>
</ul>
<hr>
<blockquote>
<h3 id="옵티마이저-역할">옵티마이저 역할</h3>
</blockquote>
<ul>
<li><p>DB가 가장 효율적인 실행 계획을 자동 선택</p>
</li>
<li><p>개발자는 인덱스 설계만 신경 쓰면 됨</p>
</li>
<li><p>실제 사용 여부는 옵티마이저 판단</p>
</li>
<li><p>인덱스의 한계</p>
<ul>
<li>잦은 변경 컬럼 : 삽입/삭제 시 트리 재정렬 비용</li>
<li>과도한 인덱스    쓰기 : 성능 저하</li>
<li>메모리/디스크 사용 : 인덱스도 별도 저장 구조</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="결론">결론</h3>
</blockquote>
<ul>
<li><p>인덱스 유무에 따라 실행 계획(EXPLAIN)이 완전히 달라짐</p>
</li>
<li><p>PK → Unique Scan</p>
</li>
<li><p>범위 조건 → Range Scan</p>
</li>
<li><p>인덱스 없음 → Full Table Scan</p>
</li>
</ul>
<blockquote>
<h3 id="요약">요약</h3>
</blockquote>
<h4 id="인덱스는-단순-정렬이-아닌-b-tree-기반-탐색-구조이며">인덱스는 단순 정렬이 아닌 B-Tree 기반 탐색 구조이며,</h4>
<h4 id="조회-성능을-극적으로-개선하지만-쓰기-비용과-공간을-고려해-설계해야-한다">조회 성능을 극적으로 개선하지만 쓰기 비용과 공간을 고려해 설계해야 한다.</h4>
<hr>
<blockquote>
<h2 id="복합-인덱스composite-index-정의">복합 인덱스(Composite Index) 정의</h2>
</blockquote>
<h4 id="복합-인덱스는-여러-컬럼을-하나의-인덱스로-묶어">복합 인덱스는 여러 컬럼을 하나의 인덱스로 묶어,</h4>
<h4 id="자주-함께-사용되는-조건을-빠르게-탐색하기-위한-인덱스다">자주 함께 사용되는 조건을 빠르게 탐색하기 위한 인덱스다.</h4>
<hr>
<blockquote>
<h3 id="왜-복합-인덱스가-필요한가">왜 복합 인덱스가 필요한가?</h3>
</blockquote>
<ul>
<li><p>단일 인덱스는 하나의 컬럼 기준 정렬만 가능</p>
</li>
<li><p>실무에서는 보통 여러 조건을 함께 조회</p>
</li>
<li><p>예: start_date + end_date</p>
</li>
</ul>
<h4 id="이-경우-단일-인덱스-여러-개보다-복합-인덱스-하나가-훨씬-효율적">이 경우 단일 인덱스 여러 개보다 복합 인덱스 하나가 훨씬 효율적</h4>
<hr>
<blockquote>
<h3 id="복합-인덱스의-핵심-규칙">복합 인덱스의 핵심 규칙</h3>
</blockquote>
<ul>
<li><p>왼쪽 접두어 규칙 (Leftmost Prefix Rule)</p>
</li>
<li><p>복합 인덱스는 왼쪽(선두) 컬럼부터 순서대로만 사용된다.</p>
<pre><code>(start_date, end_date)</code></pre></li>
<li><p>start_date 기준으로 먼저 정렬</p>
</li>
<li><p>같은 start_date 내에서 end_date 정렬</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="인덱스-사용-가능-여부-요약">인덱스 사용 가능 여부 요약</h3>
</blockquote>
<ul>
<li>복합 인덱스 (a, b, c) 기준</li>
</ul>
<table>
<thead>
<tr>
<th>WHERE 조건</th>
<th>인덱스 사용</th>
</tr>
</thead>
<tbody><tr>
<td>a</td>
<td>✅</td>
</tr>
<tr>
<td>a, b</td>
<td>✅</td>
</tr>
<tr>
<td>a, b, c</td>
<td>✅</td>
</tr>
<tr>
<td>b</td>
<td>❌</td>
</tr>
<tr>
<td>b, c</td>
<td>❌</td>
</tr>
</tbody></table>
<h4 id="선두-컬럼a이-빠지면-인덱스-사용-불가">선두 컬럼(a)이 빠지면 인덱스 사용 불가</h4>
<hr>
<blockquote>
<h3 id="포인트">포인트</h3>
</blockquote>
<ul>
<li><p>(start_date, end_date) 인덱스</p>
<ul>
<li><p>start_date + end_date 조건 → ✅ 인덱스 사용</p>
</li>
<li><p>end_date 단독 조건 → ❌ Full Table Scan</p>
</li>
</ul>
</li>
<li><p>인덱스 순서를 바꾸면 사용은 되더라도</p>
</li>
<li><p>쿼리 패턴과 맞지 않으면 비효율적</p>
</li>
</ul>
<h4 id="where-조건과-인덱스-순서는-일치시키는-게-가장-안전">WHERE 조건과 인덱스 순서는 일치시키는 게 가장 안전</h4>
<hr>
<blockquote>
<h3 id="복합-인덱스가-특히-유용한-경우">복합 인덱스가 특히 유용한 경우</h3>
</blockquote>
<ul>
<li><p>항상 함께 조회되는 컬럼들</p>
</li>
<li><p>WHERE + ORDER BY를 동시에 만족해야 할 때</p>
</li>
<li><p>페이징 쿼리 성능이 중요한 경우</p>
</li>
<li><p>대표 예시</p>
<ul>
<li><p>(category_id, price) → 상품 목록 + 정렬</p>
</li>
<li><p>(user_id, order_date) → 사용자별 기간 조회</p>
</li>
<li><p>(board_id, created_at) → 게시판 최신글</p>
</li>
<li><p>(region, district) → 지역 기반 검색</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="단일-인덱스-여러-개-vs-복합-인덱스">단일 인덱스 여러 개 vs 복합 인덱스</h3>
</blockquote>
<ul>
<li><p>단일 인덱스 여러 개: 옵티마이저 조합 필요, 효율 낮음</p>
</li>
<li><p>복합 인덱스 하나: 한 번의 탐색으로 범위 축소</p>
</li>
</ul>
<h4 id="두-컬럼-이상이-항상-함께-쓰이면-복합-인덱스가-정답">두 컬럼 이상이 항상 함께 쓰이면 복합 인덱스가 정답</h4>
<blockquote>
<h3 id="요약-1">요약</h3>
</blockquote>
<h4 id="복합-인덱스는-자주-함께-조회되는-컬럼을-하나의-b-tree로-묶어">복합 인덱스는 자주 함께 조회되는 컬럼을 하나의 B-Tree로 묶어,</h4>
<h4 id="왼쪽-접두어-규칙을-기반으로-조회·정렬-성능을-동시에-최적화한다">왼쪽 접두어 규칙을 기반으로 조회·정렬 성능을 동시에 최적화한다.</h4>
<hr>
<blockquote>
<h3 id="목표">목표</h3>
</blockquote>
<ul>
<li><p>JOIN + GROUP BY 쿼리에서 인덱스 유무에 따른 실행 계획과 성능 차이를 확인한다.</p>
</li>
<li><p>테이블 구조 요약</p>
</li>
<li><p>users</p>
<ul>
<li><p>id (PK)</p>
</li>
<li><p>username (UNIQUE → 자동 인덱스)</p>
</li>
</ul>
</li>
<li><p>posts</p>
<ul>
<li><p>id (PK)</p>
</li>
<li><p>user_id (FK 성격, 인덱스 없음)</p>
</li>
</ul>
</li>
<li><p>comments</p>
<ul>
<li><p>id (PK)</p>
</li>
<li><p>post_id (FK 성격, 인덱스 없음)</p>
</li>
</ul>
</li>
</ul>
<h4 id="join-대상-컬럼postsuser_id-commentspost_id에-인덱스가-없는-상태">JOIN 대상 컬럼(posts.user_id, comments.post_id)에 인덱스가 없는 상태</h4>
<blockquote>
<h3 id="실행-쿼리">실행 쿼리</h3>
</blockquote>
<ul>
<li><p>특정 username의 게시글 목록 조회</p>
</li>
<li><p>게시글별 댓글 수 집계</p>
</li>
<li><p>users → posts → comments LEFT JOIN + GROUP BY post.id</p>
</li>
</ul>
<blockquote>
<h3 id="인덱스-없이-실행했을-때">인덱스 없이 실행했을 때</h3>
</blockquote>
<ul>
<li><p>문제점</p>
<ul>
<li><p>posts, comments 모두 Full Table Scan</p>
</li>
<li><p>JOIN 컬럼에 인덱스가 없어 모든 행 탐색</p>
</li>
<li><p>전체 스캔량: 11 × 8 = 88행</p>
</li>
</ul>
</li>
</ul>
<h4 id="데이터가-커질수록-기하급수적으로-느려짐">데이터가 커질수록 기하급수적으로 느려짐</h4>
<hr>
<blockquote>
<h3 id="explain에서-꼭-볼-3가지">EXPLAIN에서 꼭 볼 3가지</h3>
</blockquote>
<table>
<thead>
<tr>
<th>항목</th>
<th>의미</th>
<th>판단 기준</th>
</tr>
</thead>
<tbody><tr>
<td>type</td>
<td>탐색 방식</td>
<td><code>ALL</code> ❌ / <code>ref</code>, <code>const</code> ✅</td>
</tr>
<tr>
<td>key</td>
<td>사용 인덱스</td>
<td><code>NULL</code> ❌</td>
</tr>
<tr>
<td>rows</td>
<td>예상 읽기 수</td>
<td>작을수록 좋음</td>
</tr>
</tbody></table>
<h4 id="join-컬럼에-인덱스-추가">JOIN 컬럼에 인덱스 추가</h4>
<blockquote>
<h3 id="인덱스-추가-후-실행-계획">인덱스 추가 후 실행 계획</h3>
</blockquote>
<ul>
<li><p>개선 효과</p>
<ul>
<li><p>Full Scan 제거</p>
</li>
<li><p>JOIN 대상만 정확히 탐색</p>
</li>
<li><p>전체 스캔량: 약 3~4행 수준</p>
</li>
<li><p>실행 시간: 100<del>200ms → 1</del>3ms</p>
</li>
</ul>
</li>
</ul>
<blockquote>
<h3 id="요약-2">요약</h3>
</blockquote>
<h4 id="join-성능의-핵심은-where-조건뿐-아니라">JOIN 성능의 핵심은 WHERE 조건뿐 아니라</h4>
<h4 id="join-컬럼user_id-post_id에-인덱스가-있는가이다">JOIN 컬럼(user_id, post_id)에 인덱스가 있는가이다.</h4>
<h4 id="explain에서-all--null--rows-많음이면-→-인덱스-설계부터-의심하자">EXPLAIN에서 ALL + NULL + rows 많음이면 → 인덱스 설계부터 의심하자.</h4>
<hr>
<p>공부하자...이제 쿼리 성능을 개선하는 법도 알게 되었다..! ( •̀ ω •́ )y</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 스프링 부트 개념 정리(Cache)]]></title>
            <link>https://velog.io/@d0ngx2_2/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACCache</link>
            <guid>https://velog.io/@d0ngx2_2/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACCache</guid>
            <pubDate>Thu, 18 Dec 2025 12:59:02 GMT</pubDate>
            <description><![CDATA[<h1 id="캐시cache란">캐시(Cache)란?</h1>
<h4 id="자주-사용하는-데이터를-메모리in-memory에-임시로-저장해">자주 사용하는 데이터를 메모리(In-memory)에 임시로 저장해,</h4>
<h4 id="db나-외부-api-호출-없이-빠르게-응답하기-위한-기술이다">DB나 외부 API 호출 없이 빠르게 응답하기 위한 기술이다.</h4>
<hr>
<blockquote>
<h2 id="캐시의-핵심-목적">캐시의 핵심 목적</h2>
</blockquote>
<ul>
<li><p>응답 속도 향상</p>
</li>
<li><p>DB 부하 감소</p>
</li>
<li><p>트래픽 및 외부 API 비용 절감</p>
</li>
<li><p>캐시 히트(Hit) 시에는 메모리에서 바로 응답하므로 매우 빠름.</p>
</li>
<li><p>캐시 미스(Miss) 시에만 DB를 조회한다.</p>
</li>
</ul>
<hr>
<blockquote>
<h2 id="캐시-기본-구조">캐시 기본 구조</h2>
</blockquote>
<pre><code>요청(Request)
   ↓
[ Cache ] → HIT → 즉시 응답
   ↓
 MISS
   ↓
[ DB ] → 조회 후 캐시에 저장 → 응답</code></pre><h4 id="처음-요청은-느리지만cache-miss">처음 요청은 느리지만(Cache Miss),</h4>
<h4 id="이후-요청은-빠르다cache-hit">이후 요청은 빠르다(Cache Hit)</h4>
<hr>
<blockquote>
<h2 id="db와-캐시의-역할-차이">DB와 캐시의 역할 차이</h2>
</blockquote>
<table>
<thead>
<tr>
<th>구분</th>
<th>DB</th>
<th>Cache</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>
<tr>
<td>데이터</td>
<td>항상 최신</td>
<td>최신 보장 X</td>
</tr>
</tbody></table>
<ul>
<li>DB는 신뢰성 중심,</li>
<li>캐시는 성능 최적화를 위한 보조 수단</li>
</ul>
<h4 id="실무에서는-db--cache를-함께-사용하는-구조가-기본이다">실무에서는 DB + Cache를 함께 사용하는 구조가 기본이다.</h4>
<hr>
<blockquote>
<h3 id="비유로-이해하기">비유로 이해하기</h3>
</blockquote>
<ul>
<li><p>DB = 책상 서랍
→ 안전하지만 꺼내는 데 시간이 걸림</p>
</li>
<li><p>Cache = 책상 위
→ 바로 사용 가능하지만 오래되면 다시 확인 필요</p>
</li>
</ul>
<h4 id="실무-시스템은-서랍에서-꺼내-자주-쓰는-것을-책상-위에-올려두는-구조">실무 시스템은 “서랍에서 꺼내 자주 쓰는 것을 책상 위에 올려두는 구조”</h4>
<hr>
<blockquote>
<h3 id="cache-hit--miss">Cache Hit / Miss</h3>
</blockquote>
<table>
<thead>
<tr>
<th>용어</th>
<th>설명</th>
<th>성능</th>
</tr>
</thead>
<tbody><tr>
<td>Cache Hit</td>
<td>캐시에 데이터 존재</td>
<td>매우 빠름</td>
</tr>
<tr>
<td>Cache Miss</td>
<td>캐시에 없음 → DB 조회</td>
<td>느림 (1회성)</td>
</tr>
</tbody></table>
<ul>
<li><p>예시</p>
<ul>
<li><p>DB 응답: 약 50ms</p>
</li>
<li><p>Cache 응답: 1<del>3ms
➡ 약 15</del>50배 성능 차이</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="캐시가-필요한-이유">캐시가 필요한 이유</h3>
</blockquote>
<ul>
<li><p>캐시가 없을 때</p>
<ul>
<li><p>반복적인 DB 조회</p>
</li>
<li><p>외부 API 호출 지연</p>
</li>
<li><p>트래픽 증가 시 서버 병목 발생</p>
</li>
</ul>
</li>
<li><p>캐시 도입 효과</p>
<ul>
<li><p>응답 속도 대폭 향상</p>
</li>
<li><p>DB 및 외부 API 부하 감소</p>
</li>
<li><p>대량 트래픽 대응 가능</p>
</li>
</ul>
</li>
</ul>
<h4 id="특히-조회read가-많은-api에서-효과가-크다">특히 조회(Read)가 많은 API에서 효과가 크다.</h4>
<hr>
<blockquote>
<h3 id="캐시-유형">캐시 유형</h3>
</blockquote>
<p>로컬 캐시 : 애플리케이션 내부 메모리 [ConcurrentHashMap, Caffeine]
분산 캐시 : 외부 서버에서 공유    [Redis, Memcached]</p>
<ul>
<li><p>로컬 캐시</p>
<ul>
<li><p>매우 빠름</p>
</li>
<li><p>서버 간 데이터 공유 불가</p>
</li>
</ul>
</li>
<li><p>분산 캐시</p>
<ul>
<li><p>네트워크 비용 존재</p>
</li>
<li><p>여러 서버에서 캐시 공유 가능</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="캐시-전략cache-strategy">캐시 전략(Cache Strategy)</h3>
</blockquote>
<ul>
<li>대표 전략</li>
</ul>
<table>
<thead>
<tr>
<th>전략</th>
<th>설명</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>Write-through</td>
<td>DB와 캐시 동시 갱신</td>
<td>일관성 ↑</td>
</tr>
<tr>
<td>Write-back</td>
<td>캐시에 먼저 쓰고 나중에 DB 반영</td>
<td>성능 ↑, 위험 ↑</td>
</tr>
<tr>
<td>Cache-aside</td>
<td>요청 시 캐시 확인 후 DB 조회</td>
<td>가장 일반적</td>
</tr>
<tr>
<td>Cache Invalidation</td>
<td>데이터 변경 시 캐시 삭제</td>
<td>일관성 핵심</td>
</tr>
</tbody></table>
<ul>
<li>Cache-aside 흐름<pre><code>요청 → 캐시 확인
 ↓
없음 → DB 조회
 ↓
캐시에 저장 (TTL 설정)
 ↓
응답 반환</code></pre></li>
</ul>
<h4 id="장점-단순하고-범용적">장점: 단순하고 범용적</h4>
<h4 id="단점-최초-요청은-느림">단점: 최초 요청은 느림</h4>
<hr>
<blockquote>
<h3 id="캐시-일관성-문제">캐시 일관성 문제</h3>
</blockquote>
<ul>
<li><p>문제 상황</p>
<ul>
<li>DB는 최신인데, 캐시는 이전 데이터</li>
</ul>
</li>
<li><p>주요 원인</p>
<ul>
<li><p>TTL이 너무 김</p>
</li>
<li><p>DB 업데이트 시 캐시 무효화 누락</p>
</li>
</ul>
</li>
</ul>
<h4 id="캐시의-가장-큰-기술적-과제는-성능과-정확성-사이의-균형">캐시의 가장 큰 기술적 과제는 “성능과 정확성 사이의 균형”</h4>
<hr>
<blockquote>
<h3 id="ttl--eviction-policy">TTL &amp; Eviction Policy</h3>
</blockquote>
<ul>
<li><p>TTL (Time-To-Live)</p>
<ul>
<li><p>캐시 데이터의 유효 시간</p>
</li>
<li><p>만료 시 자동 삭제</p>
<pre><code>SET key value EX 300   # 300초 유지</code></pre></li>
</ul>
</li>
<li><p>Eviction Policy (삭제 정책)</p>
<ul>
<li><p>LRU: 최근 사용 안 한 데이터 제거</p>
</li>
<li><p>LFU: 사용 빈도 낮은 데이터 제거</p>
</li>
<li><p>FIFO: 먼저 들어온 데이터 제거</p>
</li>
</ul>
</li>
<li><p>실무 캐시 활용 사례</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>서비스</th>
<th>캐시 대상</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>뉴스 메인</td>
<td>인기 기사 목록</td>
<td>짧은 TTL</td>
</tr>
<tr>
<td>상품 상세</td>
<td>상품 정보</td>
<td>DB 접근 최소화</td>
</tr>
<tr>
<td>환율 조회</td>
<td>외부 API 응답</td>
<td>주기적 갱신</td>
</tr>
<tr>
<td>가게 리스트</td>
<td>지역별 리스트</td>
<td>Key 분리</td>
</tr>
</tbody></table>
<ul>
<li><p>캐싱 설계 시 주의점</p>
<ul>
<li><p>캐시는 DB의 보조 수단</p>
</li>
<li><p>TTL은 너무 짧아도, 길어도 문제</p>
</li>
<li><p>변경 잦은 데이터보다 조회 많은 데이터에 적용</p>
</li>
<li><p>캐시 Key 설계 명확히</p>
<pre><code>user:42
post:1001</code></pre></li>
</ul>
</li>
<li><p>캐시 장애 시 DB fallback 처리 필수</p>
</li>
</ul>
<blockquote>
<h3 id="요약">요약</h3>
</blockquote>
<h4 id="캐시는-정확성을-희생하지-않는-선에서-성능을-극대화하기-위한-도구이며">캐시는 “정확성을 희생하지 않는 선에서 성능을 극대화하기 위한 도구”이며,</h4>
<h4 id="읽기-많은-시스템에서-필수적인-인프라이다">읽기 많은 시스템에서 필수적인 인프라이다.</h4>
<hr>
<h1 id="spring-cache-개념과-사용법-정리">Spring Cache 개념과 사용법 정리</h1>
<blockquote>
<h2 id="spring-cache">Spring Cache</h2>
</blockquote>
<h4 id="spring이-제공하는-캐싱-추상화cache-abstraction-기능으로">Spring이 제공하는 캐싱 추상화(Cache Abstraction) 기능으로,</h4>
<h4 id="캐시-로직을-직접-구현하지-않고-어노테이션-기반으로-캐시를-제어할-수-있게-해준다">캐시 로직을 직접 구현하지 않고 어노테이션 기반으로 캐시를 제어할 수 있게 해준다.</h4>
<ul>
<li><p>핵심 포인트</p>
<ul>
<li><p>캐시 구현체와 비즈니스 로직 분리</p>
</li>
<li><p>동일한 코드로 로컬 캐시 ↔ Redis 전환 가능</p>
</li>
<li><p>캐시 접근 로직을 AOP 방식으로 처리</p>
</li>
</ul>
</li>
<li><p>구조 개념</p>
<pre><code>@Cacheable / @CacheEvict / @CachePut
      ↓
[ Spring Cache Abstraction ]
      ↓
CacheManager
      ↓
Cache 구현체 (Caffeine, Redis 등)</code></pre></li>
</ul>
<h4 id="캐시-기술이-바뀌어도-서비스-코드는-그대로-유지">캐시 기술이 바뀌어도 서비스 코드는 그대로 유지</h4>
<blockquote>
<h2 id="기본-설정">기본 설정</h2>
</blockquote>
<ul>
<li><p>의존성 추가 (Caffeine 로컬 캐시 예시)</p>
<pre><code class="language-java">implementation &#39;org.springframework.boot:spring-boot-starter-cache&#39;
implementation &#39;com.github.ben-manes.caffeine:caffeine&#39;</code></pre>
</li>
<li><p>캐시 활성화</p>
<pre><code class="language-java">@SpringBootApplication
@EnableCaching // 캐시 기능 활성화
public class Application {
  public static void main(String[] args) {
      SpringApplication.run(Application.class, args);
  }
}</code></pre>
</li>
<li><p>@EnableCaching 없으면
→ 어노테이션이 있어도 캐시 동작 안 함</p>
</li>
<li><p>캐시 설정 (application.yml)</p>
<pre><code class="language-java">spring:
cache:
  caffeine:
    spec: maximumSize=100,expireAfterWrite=10s</code></pre>
</li>
<li><p>최대 캐시 엔트리: 100개</p>
</li>
<li><p>데이터 생성 후 10초 뒤 만료</p>
</li>
</ul>
<hr>
<blockquote>
<h2 id="ttl-동작-시나리오-이해">TTL 동작 시나리오 이해</h2>
</blockquote>
<table>
<thead>
<tr>
<th>시점</th>
<th>동작</th>
<th>상태</th>
</tr>
</thead>
<tbody><tr>
<td>0초</td>
<td>첫 조회</td>
<td>DB 조회 + 캐시 저장</td>
</tr>
<tr>
<td>5초</td>
<td>재조회</td>
<td>Cache HIT</td>
</tr>
<tr>
<td>10초</td>
<td>TTL 만료 도달</td>
<td>아직 삭제 X</td>
</tr>
<tr>
<td>11초</td>
<td>재조회</td>
<td>캐시 만료 → DB 재조회</td>
</tr>
</tbody></table>
<ul>
<li><p>expireAfterWrite 기준</p>
<ul>
<li><p>조회를 계속해도 TTL은 연장되지 않음</p>
</li>
<li><p>만료 후 접근 시 새 캐시 생성</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="cacheable--조회-결과-캐싱">@Cacheable — 조회 결과 캐싱</h2>
</blockquote>
<pre><code class="language-java">@Cacheable(value = &quot;postCache&quot;, key = &quot;#postId&quot;)
public PostDto getPostById(long postId) {
    log.info(&quot;캐시에 없으니 DB에서 직접 조회&quot;);
    Post post = postRepository.findById(postId)
        .orElseThrow(() -&gt; new IllegalArgumentException(&quot;등록된 포스트가 없습니다.&quot;));
    return PostDto.from(post);
}</code></pre>
<ul>
<li><p>동작 방식</p>
<ul>
<li><p>첫 호출 → DB 조회 후 캐시에 저장</p>
</li>
<li><p>동일 파라미터 재호출 → DB 접근 없이 캐시 반환</p>
</li>
</ul>
</li>
</ul>
<h4 id="두-번째-호출부터는-로그조차-찍히지-않음--캐시-정상-동작">두 번째 호출부터는 로그조차 찍히지 않음 = 캐시 정상 동작</h4>
<hr>
<blockquote>
<h2 id="cacheable의-value--key-구조">@Cacheable의 value &amp; key 구조</h2>
</blockquote>
<ul>
<li><p>value (Cache Name)</p>
<ul>
<li><p>캐시 저장소의 이름</p>
</li>
<li><p>하나의 캐시 그룹(namespace)</p>
<pre><code class="language-java">@Cacheable(value = &quot;postCache&quot;, key = &quot;#postId&quot;)</code></pre>
</li>
</ul>
</li>
</ul>
<h4 id="postcache라는-캐시-영역-생성">&quot;postCache&quot;라는 캐시 영역 생성</h4>
<ul>
<li><p>key (Entry Key)</p>
<ul>
<li><p>캐시 내부에서 데이터를 구분하는 식별자</p>
</li>
<li><p>메서드 파라미터 기반으로 생성</p>
<pre><code class="language-java"></code></pre>
</li>
</ul>
</li>
</ul>
<p>Cache name: postCache
Key: 1
Value: PostDto(...)</p>
<pre><code>
- 전체 캐시 구조 예시</code></pre><p>CacheManager
 ┣━━ userCache
 │     ┗━━ key: 1 → UserDto
 ┣━━ postCache
 │     ┣━━ key: 10 → PostDto
 │     ┗━━ key: 11 → PostDto
 ┗━━ commentCache
       ┗━━ key: 101 → CommentDto</p>
<pre><code>
#### value = 캐비닛 이름, key = 서류 번호

---

&gt;## value &amp; key 설계 기준

- 같은 도메인은 같은 value 사용
```java
@Cacheable(value = &quot;postCache&quot;, key = &quot;#postId&quot;)
@Cacheable(value = &quot;postCache&quot;, key = &quot;#username&quot;)</code></pre><h4 id="같은-캐시-그룹-다른-key">같은 캐시 그룹, 다른 key</h4>
<ul>
<li>key 충돌 주의<pre><code class="language-java"></code></pre>
</li>
</ul>
<p>postCache::1
postCache::&quot;1&quot;</p>
<pre><code>
#### username이 &quot;1&quot;이면 충돌 가능성 발생

- 해결 방법: prefix 사용
```java
@Cacheable(value = &quot;postCache&quot;, key = &quot;&#39;id:&#39; + #postId&quot;)
@Cacheable(value = &quot;postCache&quot;, key = &quot;&#39;username:&#39; + #username&quot;)</code></pre><ul>
<li>결과<pre><code class="language-java">postCache::id:1
postCache::username:kim</code></pre>
</li>
</ul>
<hr>
<blockquote>
<h2 id="캐시-key-네이밍-규칙">캐시 Key 네이밍 규칙</h2>
</blockquote>
<ul>
<li><p>기본 원칙</p>
<ul>
<li><p>고유하고 일관된 형식</p>
</li>
<li><p>도메인 중심 설계</p>
<pre><code class="language-java">user:1
post:1001
search:keyword:page</code></pre>
</li>
</ul>
</li>
<li><p>실무 팁</p>
<ul>
<li><p>: 또는 :: 구분자 사용</p>
</li>
<li><p>Redis 사용 시 prefix 기반 관리</p>
</li>
<li><p>운영 환경에서는 짧지만 의미 명확하게</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="cacheevict--캐시-삭제">@CacheEvict — 캐시 삭제</h2>
</blockquote>
<pre><code class="language-java">@CacheEvict(value = &quot;postCache&quot;, key = &quot;#postId&quot;)
public void deletePost(long postId) {
    // 게시글 삭제 로직
}</code></pre>
<ul>
<li><p>사용 시점</p>
<ul>
<li><p>삭제</p>
</li>
<li><p>상태 변경</p>
</li>
<li><p>더 이상 캐시가 유효하지 않을 때</p>
</li>
</ul>
</li>
</ul>
<blockquote>
<h2 id="cacheput--캐시-갱신">@CachePut — 캐시 갱신</h2>
</blockquote>
<pre><code class="language-java">@CachePut(value = &quot;postCache&quot;, key = &quot;#postId&quot;)
public PostDto updatePost(long postId) {
    // 게시글 수정 로직
}</code></pre>
<ul>
<li><p>특징</p>
<ul>
<li><p>항상 메서드 실행</p>
</li>
<li><p>실행 결과를 캐시에 강제로 갱신</p>
</li>
<li><p>조회용(@Cacheable)과 역할 분리</p>
</li>
</ul>
</li>
</ul>
<blockquote>
<h3 id="요약-1">요약</h3>
</blockquote>
<h4 id="spring-cache는-캐시-구현체와-비즈니스-로직을-분리해">Spring Cache는 캐시 구현체와 비즈니스 로직을 분리해</h4>
<h4 id="어노테이션만으로-일관성-있고-안전한-캐싱을-가능하게-해준다">어노테이션만으로 일관성 있고 안전한 캐싱을 가능하게 해준다.</h4>
<hr>
<h1 id="redis">Redis</h1>
<h4 id="redisremote-dictionary-server-는">Redis(Remote Dictionary Server) 는</h4>
<h4 id="모든-데이터를-메모리ram에-저장하는-초고속-key-value-데이터베이스이다">모든 데이터를 메모리(RAM)에 저장하는 초고속 Key-Value 데이터베이스이다.</h4>
<ul>
<li><p>디스크가 아닌 메모리 기반 → 매우 빠른 읽기/쓰기</p>
</li>
<li><p>주 용도: 캐시, 세션, 실시간 데이터 처리</p>
</li>
</ul>
<h4 id="실무에서는-보통">실무에서는 보통</h4>
<h4 id="db의-보조-저장소cache-server-로-사용된다">DB의 보조 저장소(Cache Server) 로 사용된다.</h4>
<hr>
<blockquote>
<h2 id="rdb-vs-key-value-db-redis">RDB vs Key-Value DB (Redis)</h2>
</blockquote>
<table>
<thead>
<tr>
<th>구분</th>
<th>RDB (MySQL 등)</th>
<th>Redis</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 구조</td>
<td>테이블 (행/열)</td>
<td>Key - Value</td>
</tr>
<tr>
<td>조회 방식</td>
<td>SQL (복잡한 조건 가능)</td>
<td>Key 기반 단일 조회</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>
<h4 id="db--정확성--redis--속도">DB = 정확성 / Redis = 속도</h4>
<hr>
<blockquote>
<h2 id="redis가-빠른-이유">Redis가 빠른 이유</h2>
</blockquote>
<table>
<thead>
<tr>
<th>항목</th>
<th>디스크 기반 DB</th>
<th>Redis</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 위치</td>
<td>디스크</td>
<td>메모리</td>
</tr>
<tr>
<td>접근 방식</td>
<td>파일 I/O</td>
<td>CPU 직접 접근</td>
</tr>
<tr>
<td>응답 속도</td>
<td>ms 단위</td>
<td>μs 단위</td>
</tr>
<tr>
<td>안정성</td>
<td>매우 높음</td>
<td>서버 종료 시 휘발</td>
</tr>
</tbody></table>
<h4 id="redis는디스크-io가-없기-때문에-압도적으로-빠르다">Redis는디스크 I/O가 없기 때문에 압도적으로 빠르다.</h4>
<hr>
<blockquote>
<h3 id="redis의-주요-특징">Redis의 주요 특징</h3>
</blockquote>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>In-memory</td>
<td>모든 데이터를 메모리에 저장</td>
</tr>
<tr>
<td>Key-Value</td>
<td>단순하고 빠른 구조</td>
</tr>
<tr>
<td>자료구조 제공</td>
<td>String, List, Set, Hash, ZSet 등</td>
</tr>
<tr>
<td>TTL 지원</td>
<td>Key 단위 만료 시간 설정 가능</td>
</tr>
</tbody></table>
<h4 id="단순-캐시를-넘어-자료구조-서버-역할까지-수행-가능">단순 캐시를 넘어 자료구조 서버 역할까지 수행 가능</h4>
<hr>
<blockquote>
<h2 id="redis-자료형과-활용-사례">Redis 자료형과 활용 사례</h2>
</blockquote>
<ul>
<li><p>String</p>
<ul>
<li><p>가장 기본적인 타입</p>
</li>
<li><p>문자열, 숫자 모두 저장 가능</p>
</li>
</ul>
</li>
<li><p>활용</p>
<ul>
<li><p>게시글/상품 캐싱</p>
</li>
<li><p>로그인 토큰</p>
</li>
<li><p>조회수, 좋아요 수 카운팅</p>
<pre><code>set viewCount 10
incr viewCount</code></pre></li>
</ul>
</li>
</ul>
<hr>
<ul>
<li><p>List</p>
<ul>
<li><p>순서가 있는 데이터 구조</p>
</li>
<li><p>Queue / Stack 용도로 활용 가능</p>
</li>
</ul>
</li>
<li><p>활용</p>
<ul>
<li><p>실시간 채팅 메시지</p>
</li>
<li><p>최근 본 상품 목록</p>
</li>
<li><p>주문 대기열</p>
<pre><code>lpush recent_posts &quot;post1&quot;
rpush recent_posts &quot;post2&quot;
lrange recent_posts 0 -1</code></pre></li>
</ul>
</li>
</ul>
<hr>
<ul>
<li><p>Set</p>
<ul>
<li>중복 없는 집합</li>
</ul>
</li>
<li><p>활용</p>
<ul>
<li><p>좋아요 누른 사용자 목록</p>
</li>
<li><p>태그 관리</p>
</li>
<li><p>팔로워 / 팔로잉 관계</p>
<pre><code>sadd tags &quot;spring&quot;
sadd tags &quot;redis&quot;
smembers tags</code></pre></li>
</ul>
</li>
</ul>
<hr>
<ul>
<li><p>Hash</p>
<ul>
<li>하나의 Key에 여러 필드-값 저장 (Map 구조)</li>
</ul>
</li>
<li><p>활용</p>
<ul>
<li><p>사용자 정보 캐싱</p>
</li>
<li><p>상품 상세 정보</p>
</li>
<li><p>로그인 세션 데이터</p>
</li>
</ul>
</li>
</ul>
<pre><code>hset user:1 name &quot;Ravi&quot; age 30
hgetall user:1</code></pre><hr>
<h4 id="rdb의-한-row를-redis-hash로-표현하는-경우가-많다">RDB의 한 Row를 Redis Hash로 표현하는 경우가 많다.</h4>
<ul>
<li><p>Sorted Set (ZSet)</p>
<ul>
<li>score 기반 자동 정렬</li>
</ul>
</li>
<li><p>활용</p>
<ul>
<li><p>랭킹 시스템</p>
</li>
<li><p>인기 게시글 순위</p>
</li>
<li><p>실시간 순위 갱신</p>
<pre><code>zadd ranking 100 &quot;ravi&quot;
zrevrange ranking 0 -1 WITHSCORES</code></pre></li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="redis-데이터-영속성-persistence">Redis 데이터 영속성 (Persistence)</h2>
</blockquote>
<h4 id="redis는-기본적으로-휘발성이지만">Redis는 기본적으로 휘발성이지만,</h4>
<h4 id="옵션을-통해-디스크에-저장-가능하다">옵션을 통해 디스크에 저장 가능하다.</h4>
<table>
<thead>
<tr>
<th>방식</th>
<th>설명</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>RDB</td>
<td>주기적으로 전체 스냅샷 저장</td>
<td>빠름, 백업 용도</td>
</tr>
<tr>
<td>AOF</td>
<td>실행 명령어를 순차 기록</td>
<td>안정성 높음</td>
</tr>
<tr>
<td>Hybrid</td>
<td>RDB + AOF 혼합</td>
<td>Redis 7.x 기본</td>
</tr>
</tbody></table>
<h4 id="그래도-rdb만큼의-안정성은-아님">그래도 RDB만큼의 안정성은 아님</h4>
<blockquote>
<h2 id="redis-사용-시-주의사항">Redis 사용 시 주의사항</h2>
</blockquote>
<ul>
<li><p>Redis는 주 저장소가 아니다</p>
</li>
<li><p>메모리 기반 → 장애 시 데이터 유실 가능</p>
</li>
<li><p>반드시 DB와 함께 사용해야 안정적</p>
</li>
<li><p>이상적인 구조</p>
</li>
<li><p>원본 데이터 : MySQL, PostgreSQL</p>
</li>
<li><p>빠른 접근 : Redis</p>
</li>
<li><p>조합 : DB는 진실, Redis는 복사본</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="요약-2">요약</h3>
</blockquote>
<h4 id="redis는-초고속-메모리-기반-key-value-저장소로">Redis는 초고속 메모리 기반 Key-Value 저장소로,</h4>
<h4 id="db의-부하를-줄이고-실시간-처리를-가능하게-해주는-캐시-서버이다">DB의 부하를 줄이고 실시간 처리를 가능하게 해주는 캐시 서버이다.</h4>
<hr>
<h1 id="redistemplate과-cache-aside-패턴-정리">RedisTemplate과 Cache-Aside 패턴 정리</h1>
<blockquote>
<h2 id="redistemplate">RedisTemplate</h2>
</blockquote>
<h4 id="spring이-제공하는-redis-전용-클라이언트로">Spring이 제공하는 Redis 전용 클라이언트로,</h4>
<h4 id="java-코드로-redis-명령어를-실행할-수-있게-해주는-도구이다">Java 코드로 Redis 명령어를 실행할 수 있게 해주는 도구이다.</h4>
<p><strong>즉,</strong></p>
<p><strong>우리는 Java 코드로 set, get을 호출하고</strong></p>
<p><strong>RedisTemplate이 이를 Redis 명령어(SET, GET 등) 로 변환해 서버에 전달한다.</strong></p>
<p><strong>Redis에서 받은 결과는 다시 Java 객체로 변환되어 반환된다.</strong></p>
<hr>
<blockquote>
<h2 id="redistemplate-동작-흐름">RedisTemplate 동작 흐름</h2>
</blockquote>
<pre><code>[ Java 코드 ]
   &quot;user:1 = Ravi&quot; 저장 요청
        ↓
[ RedisTemplate ]
   SET user:1 &quot;Ravi&quot;
        ↓
[ Redis 서버 ]
   메모리에 저장
        ↓
[ 결과 반환 ]</code></pre><h4 id="redis-명령어를-직접-쓰지-않아도-되는-이유">Redis 명령어를 직접 쓰지 않아도 되는 이유</h4>
<hr>
<blockquote>
<h3 id="간단한-예제">간단한 예제</h3>
</blockquote>
<pre><code class="language-java">redisTemplate.opsForValue().set(&quot;user:1&quot;, &quot;Ravi&quot;);
String name = (String) redisTemplate.opsForValue().get(&quot;user:1&quot;);
System.out.println(name); // Ravi</code></pre>
<ul>
<li><p>내부적으로 실행되는 Redis 명령:</p>
<pre><code>SET user:1 &quot;Ravi&quot;
GET user:1</code></pre></li>
<li><p>opsForValue()
→ Redis의 String 타입을 다루는 도우미 객체</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="redistemplate-주요-역할-정리">RedisTemplate 주요 역할 정리</h3>
</blockquote>
<table>
<thead>
<tr>
<th>메서드</th>
<th>Redis 자료형</th>
<th>Redis 명령</th>
</tr>
</thead>
<tbody><tr>
<td>opsForValue()</td>
<td>String</td>
<td>SET / GET</td>
</tr>
<tr>
<td>opsForHash()</td>
<td>Hash</td>
<td>HSET / HGET</td>
</tr>
<tr>
<td>opsForList()</td>
<td>List</td>
<td>LPUSH / LRANGE</td>
</tr>
<tr>
<td>opsForSet()</td>
<td>Set</td>
<td>SADD / SMEMBERS</td>
</tr>
<tr>
<td>opsForZSet()</td>
<td>Sorted Set</td>
<td>ZADD / ZRANGE</td>
</tr>
</tbody></table>
<h4 id="redis의-모든-자료구조를-java-api로-다룰-수-있음">Redis의 모든 자료구조를 Java API로 다룰 수 있음</h4>
<hr>
<blockquote>
<h3 id="ttltime-to-live-설정">TTL(Time To Live) 설정</h3>
</blockquote>
<pre><code class="language-java">redisTemplate.opsForValue()
    .set(&quot;temp:data&quot;, &quot;123&quot;, 10, TimeUnit.MINUTES);</code></pre>
<ul>
<li><p>Redis 내부 명령:</p>
<pre><code>SET temp:data &quot;123&quot; EX 600</code></pre></li>
<li><p>TTL이 지나면 자동 삭제
➡ 캐시 데이터 최신성 유지</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="cache-aside-패턴-읽기-캐싱">Cache-Aside 패턴 (읽기 캐싱)</h3>
</blockquote>
<h4 id="읽을-때는-캐시를-먼저-확인하고">읽을 때는 캐시를 먼저 확인하고,</h4>
<h4 id="없으면-db에서-조회한-뒤-캐시에-저장하는-방식이다">없으면 DB에서 조회한 뒤 캐시에 저장하는 방식이다.</h4>
<ul>
<li><p>기본 원칙</p>
<ul>
<li><p>Read: 캐시 → DB → 캐시 저장</p>
</li>
<li><p>Write: DB 변경 후 캐시 삭제(Evict)</p>
</li>
</ul>
</li>
<li><p>캐시 없는 버전</p>
<pre><code class="language-java">public Post getPost(Long postId) {
  return postRepository.findById(postId)
      .orElseThrow(() -&gt; new RuntimeException(&quot;Post not found&quot;));
}</code></pre>
</li>
<li><p>Redis 적용 버전 (Cache-Aside)</p>
<pre><code class="language-java">public Post getPost(Long postId) {
  String key = PREFIX + postId;

  // 1. 캐시 확인
  Post cached = (Post) redisTemplate.opsForValue().get(key);
  if (cached != null) {
      System.out.println(&quot;Cache Hit&quot;);
      return cached;
  }

  // 2. 캐시 미스 → DB 조회
  Post post = postRepository.findById(postId)
      .orElseThrow(() -&gt; new RuntimeException(&quot;Post not found&quot;));

  // 3. 캐시에 저장
  redisTemplate.opsForValue().set(key, post, 10, TimeUnit.MINUTES);
  return post;
}</code></pre>
</li>
<li><p>핵심 포인트</p>
<ul>
<li><p>캐시에 있으면 DB 접근 ❌</p>
</li>
<li><p>없을 때만 DB 조회</p>
</li>
<li><p>조회 결과를 캐시에 저장</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="redis-캐시-key-네이밍-전략">Redis 캐시 Key 네이밍 전략</h2>
</blockquote>
<h4 id="redis는-모든-데이터가-key-기반이기-때문에">Redis는 모든 데이터가 Key 기반이기 때문에</h4>
<h4 id="key-설계가-곧-운영-안정성이다">Key 설계가 곧 운영 안정성이다.</h4>
<ul>
<li>기본 규칙<pre><code>{도메인}:{리소스}:{식별자}</code></pre></li>
<li>예시</li>
</ul>
<table>
<thead>
<tr>
<th>용도</th>
<th>Key 예시</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>사용자 정보</td>
<td>user:123</td>
<td>userId=123</td>
</tr>
<tr>
<td>게시글</td>
<td>post:987</td>
<td>postId=987</td>
</tr>
<tr>
<td>사용자 피드</td>
<td>feed:user:123</td>
<td>특정 사용자 피드</td>
</tr>
<tr>
<td>댓글 리스트</td>
<td>comment:post:987</td>
<td>게시글 댓글</td>
</tr>
<tr>
<td>랭킹</td>
<td>ranking:game:weekly</td>
<td>주간 랭킹</td>
</tr>
</tbody></table>
<ul>
<li><p>: 로 계층 구조 표현</p>
</li>
<li><p>고정 prefix로 역할 구분</p>
</li>
<li><p>Key만 봐도 의미 파악 가능하게</p>
</li>
<li><p>코드에서는 상수로 관리</p>
<pre><code class="language-java">private static final String USER_CACHE_PREFIX = &quot;user:&quot;;
private static final String POST_CACHE_PREFIX = &quot;post:&quot;;</code></pre>
</li>
</ul>
<hr>
<blockquote>
<h2 id="ttl-설계-전략">TTL 설계 전략</h2>
</blockquote>
<ul>
<li><p>TTL은 캐시의 생명주기다.</p>
</li>
<li><p>예시</p>
<pre><code class="language-java">redisTemplate.opsForValue()
  .set(&quot;post:1&quot;, post, 10, TimeUnit.MINUTES);</code></pre>
</li>
<li><p>10분 후 자동 만료</p>
</li>
<li><p>데이터별 TTL 예시</p>
<table>
<thead>
<tr>
<th>데이터</th>
<th>TTL</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>사용자 프로필</td>
<td>1시간~1일</td>
<td>변경 적음</td>
</tr>
<tr>
<td>게시글 상세</td>
<td>5~30분</td>
<td>수정 가능성</td>
</tr>
<tr>
<td>실시간 인기글</td>
<td>1~5분</td>
<td>빠른 변동</td>
</tr>
<tr>
<td>로그인 세션</td>
<td>10~60분</td>
<td>보안</td>
</tr>
<tr>
<td>통계/대시보드</td>
<td>5분 이내</td>
<td>최신성 중요</td>
</tr>
</tbody></table>
</li>
<li><p>TTL 설계 팁</p>
<ul>
<li><p>변경 주기 ⬆ → TTL 짧게</p>
</li>
<li><p>조회 빈도 ⬆ &amp; 변경 적음 → 캐싱 효율 ↑</p>
</li>
<li><p>TTL 너무 짧음 ❌ → 캐시 효과 감소</p>
</li>
<li><p>TTL 너무 김 ❌ → 데이터 불일치 위험</p>
</li>
</ul>
</li>
</ul>
<blockquote>
<h3 id="요약-3">요약</h3>
</blockquote>
<h4 id="redistemplate은-java-코드로-redis-자료구조와-명령을-다룰-수-있게-해주는-도구이며">RedisTemplate은 Java 코드로 Redis 자료구조와 명령을 다룰 수 있게 해주는 도구이며,</h4>
<h4 id="cache-aside-패턴과-함께-사용하면-읽기-성능을-크게-향상시킬-수-있다">Cache-Aside 패턴과 함께 사용하면 읽기 성능을 크게 향상시킬 수 있다.</h4>
<hr>
<h1 id="spring-boot와-redis-연동-실습-정리">Spring Boot와 Redis 연동 실습 정리</h1>
<blockquote>
<h2 id="spring-boot--redis-연동-목적">Spring Boot + Redis 연동 목적</h2>
</blockquote>
<ul>
<li><p>Redis를 캐시 서버로 사용해 조회 성능 개선</p>
<ul>
<li><p>DB 부하 감소</p>
</li>
<li><p>실무에서 가장 많이 쓰이는 Cache-Aside 패턴 직접 구현</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="의존성-추가">의존성 추가</h2>
</blockquote>
<pre><code class="language-java">// Redis 연동
implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;</code></pre>
<ul>
<li><p>spring-boot-starter-data-redis</p>
<ul>
<li><p>RedisConnectionFactory</p>
</li>
<li><p>RedisTemplate</p>
</li>
<li><p>Lettuce 클라이언트 기본 제공</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="redistemplate-설정-중요-⭐">RedisTemplate 설정 (중요 ⭐)</h2>
</blockquote>
<pre><code class="language-java">@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate&lt;String, Object&gt; redisTemplate(
            RedisConnectionFactory connectionFactory
    ) {
        RedisTemplate&lt;String, Object&gt; template = new RedisTemplate&lt;&gt;();
        template.setConnectionFactory(connectionFactory);

        // Key는 문자열
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        // Value는 JSON (LocalDateTime 대응)
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        template.afterPropertiesSet();
        return template;
    }
}</code></pre>
<ul>
<li><p>설정 이유</p>
</li>
<li><p>StringRedisSerializer : Redis Key를 사람이 읽을 수 있게</p>
</li>
<li><p>GenericJackson2JsonRedisSerializer : 객체(JSON) 직렬화 + LocalDateTime 대응</p>
</li>
<li><p>Object 타입 : 다양한 도메인 객체 캐싱 가능</p>
</li>
<li><p>이 설정 없으면
→ 직렬화 깨짐 / LocalDateTime 에러 자주 발생</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="cache-aside-기본-캐싱">Cache-Aside 기본 캐싱</h3>
</blockquote>
<ul>
<li><p>핵심 개념</p>
<ul>
<li><p>읽기(Read): 캐시 → DB → 캐시 저장</p>
</li>
<li><p>쓰기(Write): DB 수정 후 캐시 삭제</p>
</li>
</ul>
</li>
<li><p>게시글 조회 흐름
```
[Client]
↓
[Controller]
↓
[Service]
↓</p>
</li>
</ul>
<ol>
<li>Redis 캐시 조회
↓</li>
<li>Cache Miss → DB 조회
↓</li>
<li>Redis 캐시에 저장
↓</li>
<li>응답 반환<pre><code></code></pre></li>
</ol>
<ul>
<li>게시글 수정(업데이트) 흐름
```
[Client PUT 요청]
↓</li>
</ul>
<ol>
<li>DB 데이터 수정
↓</li>
<li>Redis 캐시 삭제 (Invalidate)<pre><code></code></pre></li>
</ol>
<h4 id="캐시는-항상-db보다-늦게-믿는다">캐시는 항상 DB보다 늦게 믿는다</h4>
<ul>
<li>실무 포인트</li>
</ul>
<h4 id="수정-시-캐시를-갱신보다-삭제evict-하는-것이-안전">수정 시 캐시를 “갱신”보다 삭제(Evict) 하는 것이 안전</h4>
<h4 id="다음-조회-시-최신-데이터로-재캐싱">다음 조회 시 최신 데이터로 재캐싱</h4>
<hr>
<blockquote>
<h2 id="조회수-기반-인기-게시글-sorted-set">조회수 기반 인기 게시글 (Sorted Set)</h2>
</blockquote>
<ul>
<li><p>왜 ZSet을 쓸까?</p>
<ul>
<li><p>자동 정렬</p>
</li>
<li><p>점수(score) 기반 순위 관리</p>
</li>
<li><p>실시간 랭킹에 최적화</p>
</li>
</ul>
</li>
<li><p>게시글 조회 시 흐름
```
[게시글 조회 요청]
 ↓</p>
</li>
</ul>
<ol>
<li>Redis 캐시 조회
↓</li>
<li>Cache Miss → DB 조회 후 캐시 저장
↓</li>
<li>ZSet 조회수 +1 (ZINCRBY)
↓</li>
<li>게시글 응답<pre><code></code></pre></li>
</ol>
<ul>
<li><p>조회수는 DB에 바로 반영하지 않고
→ Redis에서 실시간 집계</p>
</li>
<li><p>인기 게시글 조회 흐름
```
[인기글 조회 요청]
 ↓</p>
</li>
</ul>
<ol>
<li>ZSet에서 상위 N개 postId 조회 (ZREVRANGE)
↓</li>
<li>postId 기반 캐시 조회
↓</li>
<li>캐시 없으면 DB 조회 후 Redis 저장
↓</li>
<li>조회수 순서대로 PostDto 반환<pre><code></code></pre></li>
</ol>
<ul>
<li><p>이 구조의 장점</p>
<ul>
<li><p>조회수 증가 = Redis 연산 (초고속)</p>
</li>
<li><p>인기글 조회 시 DB 정렬 ❌</p>
</li>
<li><p>대규모 트래픽에서도 성능 유지</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="전체-구조-요약">전체 구조 요약</h3>
</blockquote>
<ul>
<li>역할 분리<ul>
<li>원본 데이터 : RDB (MySQL 등)</li>
<li>빠른 조회 : Redis (Cache)</li>
<li>실시간 랭킹 : Redis Sorted Set</li>
</ul>
</li>
</ul>
<h4 id="db는-진실-redis는-가속기">DB는 진실, Redis는 가속기</h4>
<hr>
<blockquote>
<h3 id="실무-관점에서-배울-점">실무 관점에서 배울 점</h3>
</blockquote>
<ul>
<li><p>RedisTemplate은 저수준 제어에 적합</p>
</li>
<li><p>Spring Cache(@Cacheable)는 선언적 캐싱</p>
</li>
<li><p>랭킹, 카운팅은 Redis 자료구조가 압도적으로 유리</p>
</li>
<li><p>캐시 설계는 “어디까지 Redis를 믿을지” 결정하는 작업</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="요약-4">요약</h3>
</blockquote>
<h4 id="spring-boot에서-redis를-연동해-cache-aside-패턴과-sorted-set을-활용하면">Spring Boot에서 Redis를 연동해 Cache-Aside 패턴과 Sorted Set을 활용하면,</h4>
<h4 id="조회-성능-개선과-실시간-랭킹을-동시에-만족하는-구조를-만들-수-있다">조회 성능 개선과 실시간 랭킹을 동시에 만족하는 구조를 만들 수 있다.</h4>
<hr>
<h4 id="공부할-양이-너무-많다；⌒">공부할 양이.. 너무 많다..(；′⌒`)</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 스프링 부트 개념 정리(Docker)]]></title>
            <link>https://velog.io/@d0ngx2_2/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACDocker</link>
            <guid>https://velog.io/@d0ngx2_2/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACDocker</guid>
            <pubDate>Wed, 17 Dec 2025 12:30:51 GMT</pubDate>
            <description><![CDATA[<h1 id="docker">Docker</h1>
<ul>
<li>Docker는 애플리케이션과 실행 환경을 하나로 묶어</li>
<li>어디서 실행하든 동일하게 동작하도록 만들어주는 컨테이너 기술이다.</li>
</ul>
<h4 id="코드뿐만-아니라-실행에-필요한-os-라이브러리-설정까지-함께-패키징하여">코드뿐만 아니라 실행에 필요한 OS, 라이브러리, 설정까지 함께 패키징하여</h4>
<h4 id="내-컴퓨터에서는-되는데-문제를-해결해준다">“내 컴퓨터에서는 되는데?” 문제를 해결해준다.</h4>
<hr>
<h3 id="요약">요약</h3>
<ul>
<li>Docker = 실행 환경을 통째로 포장하는 기술</li>
</ul>
<hr>
<blockquote>
<h2 id="docker가-필요한-이유">Docker가 필요한 이유</h2>
</blockquote>
<ul>
<li><p>환경이 다르면 결과도 달라지기 때문이다.</p>
</li>
<li><p>개발자 PC: Windows / Mac</p>
</li>
<li><p>서버 환경: Linux</p>
</li>
<li><p>라이브러리 버전, 패키지 충돌, 환경 변수 차이</p>
</li>
<li><p>DB, 포트, 경로 설정 문제</p>
</li>
</ul>
<h4 id="같은-코드인데도-실행-결과가-달라지는-문제가-빈번하게-발생한다">같은 코드인데도 실행 결과가 달라지는 문제가 빈번하게 발생한다.</h4>
<ul>
<li><p><code>Docker</code>는 동일한 환경에서 언제나 같은 결과를 보장하기 위해 존재한다.</p>
</li>
<li><p>기존 방식: 가상 머신(VM)</p>
<ul>
<li><p>예전에는 가상 머신(Virtual Machine) 을 사용했다.</p>
</li>
<li><p>OS 위에 또 다른 OS를 실행</p>
</li>
<li><p>완전히 분리된 환경</p>
</li>
</ul>
</li>
</ul>
<h4 id="하지만-무겁고-느리며-자원-소모가-큼">하지만 무겁고 느리며 자원 소모가 큼</h4>
<ul>
<li><p>문제점</p>
<ul>
<li><p>VM 1개당 OS 1개 필요</p>
</li>
<li><p>부팅 시간 느림</p>
</li>
<li><p>CPU / RAM / 디스크 낭비 큼</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="컨테이너container는-뭐가-다를까">컨테이너(Container)는 뭐가 다를까?</h3>
</blockquote>
<ul>
<li>컨테이너는 운영체제를 새로 띄우지 않는다.</li>
</ul>
<h4 id="호스트-os의-커널을-공유하고">호스트 OS의 커널을 공유하고</h4>
<h4 id="애플리케이션-실행에-필요한-것만-격리해서-실행한다">애플리케이션 실행에 필요한 것만 격리해서 실행한다.</h4>
<ul>
<li>VM vs Container 비교</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>가상 머신(VM)</th>
<th>컨테이너(Container)</th>
</tr>
</thead>
<tbody><tr>
<td>구조</td>
<td>OS 위에 또 다른 OS</td>
<td>OS 커널 공유</td>
</tr>
<tr>
<td>실행 단위</td>
<td>OS 단위</td>
<td>애플리케이션 단위</td>
</tr>
<tr>
<td>부팅 속도</td>
<td>느림 (수 분)</td>
<td>매우 빠름 (수 초)</td>
</tr>
<tr>
<td>용량</td>
<td>수 GB</td>
<td>수백 MB</td>
</tr>
<tr>
<td>자원 사용</td>
<td>많음</td>
<td>효율적</td>
</tr>
</tbody></table>
<ul>
<li><p>VM: 컴퓨터 안에 또 다른 컴퓨터</p>
</li>
<li><p>컨테이너: 하나의 컴퓨터에서 앱만 격리 실행</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="컨테이너가-빠른-이유">컨테이너가 빠른 이유</h3>
</blockquote>
<ul>
<li><p>OS를 새로 부팅하지 않음</p>
</li>
<li><p>호스트 OS의 커널을 그대로 사용</p>
</li>
<li><p>필요한 실행 환경만 로드</p>
</li>
</ul>
<blockquote>
<h3 id="요약-1">요약</h3>
</blockquote>
<h4 id="필요한-환경만-꺼내-쓰기-때문에-빠르고-가볍다">필요한 환경만 꺼내 쓰기 때문에 빠르고 가볍다.</h4>
<hr>
<blockquote>
<h2 id="docker의-핵심-개념">Docker의 핵심 개념</h2>
</blockquote>
<h3 id="image-이미지">Image (이미지)</h3>
<ul>
<li><p>실행 환경을 정의한 설계도</p>
</li>
<li><p>OS, 라이브러리, 애플리케이션, 설정 포함</p>
</li>
</ul>
<h4 id="읽기-전용immutable">읽기 전용(Immutable)</h4>
<ul>
<li><p>특징</p>
<ul>
<li><p>이미지는 실행되지 않음</p>
</li>
<li><p>하나의 이미지로 여러 컨테이너 생성 가능</p>
</li>
</ul>
</li>
<li><p>비유</p>
<ul>
<li>이미지 = 붕어빵 틀</li>
</ul>
</li>
</ul>
<hr>
<h3 id="container-컨테이너">Container (컨테이너)</h3>
<ul>
<li><p>이미지를 실제로 실행한 결과물</p>
</li>
<li><p>독립된 실행 공간</p>
</li>
<li><p>가볍고 빠르게 생성 / 삭제 가능</p>
</li>
<li><p>특징</p>
<ul>
<li><p>서로 간섭하지 않음</p>
</li>
<li><p>필요할 때 만들고 버릴 수 있음</p>
</li>
<li><p>실행 중인 애플리케이션이 여기서 동작</p>
</li>
</ul>
</li>
<li><p>비유</p>
<ul>
<li>컨테이너 = 붕어빵</li>
<li>(같은 틀로 원하는 만큼 생성 가능)</li>
</ul>
</li>
</ul>
<h4 id="컨테이너-하나--포맷된-깨끗한-컴퓨터-1대">컨테이너 하나 = 포맷된 깨끗한 컴퓨터 1대</h4>
<hr>
<h3 id="컨테이너를-사용하는-이유">컨테이너를 사용하는 이유</h3>
<ul>
<li><p>사람마다 다른 개발 환경 문제 해결</p>
</li>
<li><p>OS 상관없이 동일한 실행 보장</p>
</li>
<li><p>환경 설정을 자동화</p>
</li>
<li><p>예시</p>
<ul>
<li><p>Windows / Mac / Linux 상관 없음</p>
</li>
<li><p>환경 변수 차이 없음</p>
</li>
<li><p>Ubuntu 설치 필요 없음</p>
</li>
<li><p>WSL2, VM 설치 없이도 실행 가능</p>
</li>
</ul>
</li>
</ul>
<h4 id="이미지-하나로-모든-설정이-자동-구성">이미지 하나로 모든 설정이 자동 구성</h4>
<hr>
<blockquote>
<h2 id="docker-hub">Docker Hub</h2>
</blockquote>
<ul>
<li><p>Docker Hub는 이미지 저장소이다.</p>
</li>
<li><p>GitHub = 코드 저장소</p>
</li>
<li><p>Docker Hub = 실행 환경(이미지) 저장소</p>
</li>
<li><p>특징</p>
<ul>
<li><p>전 세계 개발자가 만든 이미지 공유</p>
</li>
<li><p>공식 이미지 다수 제공</p>
</li>
</ul>
</li>
<li><p>예시 이미지</p>
<ul>
<li><p>redis</p>
</li>
<li><p>mysql</p>
</li>
<li><p>kafka</p>
</li>
<li><p>elasticsearch</p>
</li>
</ul>
</li>
<li><p>비유</p>
<ul>
<li>Docker Hub = 붕어빵 틀 창고</li>
</ul>
</li>
<li><p>기본 사용 흐름</p>
<ul>
<li>search → pull → run</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="docker-전체-흐름">Docker 전체 흐름</h3>
</blockquote>
<h4 id="docker는-깨끗한-실행-환경을-이미지로-정의하고">Docker는 깨끗한 실행 환경을 이미지로 정의하고,</h4>
<h4 id="그-이미지를-컨테이너로-실행하여">그 이미지를 컨테이너로 실행하여</h4>
<h4 id="어디서든-동일한-결과를-보장하는-기술이다">어디서든 동일한 결과를 보장하는 기술이다.</h4>
<hr>
<blockquote>
<h2 id="docker-hub에서-redis-실행하기-실습-til">Docker Hub에서 Redis 실행하기 (실습 TIL)</h2>
</blockquote>
<ul>
<li><p>Docker Hub에서 Redis 이미지 검색</p>
<pre><code>docker search redis</code></pre></li>
<li><p>Docker Hub는 전 세계 개발자들이 공유하는 도커 이미지 저장소</p>
</li>
<li><p>OFFICIAL 표시가 있는 이미지는 Redis 공식 팀이 배포한 신뢰 가능한 이미지</p>
</li>
<li><p>포인트</p>
<ul>
<li>실무에서는 가급적 OFFICIAL 이미지를 사용한다.</li>
</ul>
</li>
</ul>
<hr>
<ul>
<li><p>Redis 이미지 다운로드 (pull)</p>
<pre><code>docker pull redis:latest</code></pre></li>
<li><p>pull : Docker Hub에서 이미지를 로컬로 다운로드</p>
</li>
<li><p>latest : Redis의 최신 버전 태그</p>
</li>
<li><p>다운로드된 이미지 확인</p>
<pre><code>docker images</code></pre></li>
<li><p>출력 예시:</p>
<pre><code>REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
redis        latest    b5d3b2e6f64e   2 weeks ago    117MB</code></pre></li>
</ul>
<h4 id="redis-실행-환경이-이미지-형태로-내-컴퓨터에-저장됨">Redis 실행 환경이 이미지 형태로 내 컴퓨터에 저장됨</h4>
<hr>
<ul>
<li><p>Redis 컨테이너 실행</p>
<pre><code>docker run -d -p 6379:6379 --name redis-container redis:latest</code></pre></li>
<li><p>옵션 설명
```</p>
</li>
<li><p>d    백그라운드(detached) 실행</p>
</li>
<li><p>p 6379:6379    호스트 ↔ 컨테이너 포트 매핑</p>
</li>
<li><p>-name redis-container    컨테이너 이름 지정
redis:latest    실행할 이미지</p>
<pre><code></code></pre></li>
<li><p>Redis 설치</p>
</li>
<li><p>실행 환경 구성</p>
</li>
<li><p>Redis 서버 실행</p>
<ul>
<li>이 모두가 자동으로 완료됨</li>
</ul>
</li>
</ul>
<hr>
<ul>
<li><p>컨테이너 실행 상태 확인</p>
<pre><code>docker ps</code></pre></li>
<li><p>출력 예시:</p>
<pre><code>CONTAINER ID   IMAGE          STATUS          PORTS
a12b3c4d5e6f   redis:latest   Up 5 seconds    0.0.0.0:6379-&gt;6379/tcp</code></pre></li>
<li><p>확인 포인트</p>
<ul>
<li><p>STATUS 가 Up → 정상 실행 중</p>
</li>
<li><p>6379-&gt;6379 → 외부에서 Redis 접근 가능</p>
</li>
</ul>
</li>
</ul>
<p>-</p>
<blockquote>
<h3 id="5-port포트">5. Port(포트)</h3>
</blockquote>
<ul>
<li><p>포트는 한 컴퓨터 안에서 프로그램을 구분하는 번호</p>
</li>
<li><p>IP 주소: 컴퓨터(서버)의 주소</p>
</li>
<li><p>포트 번호: 그 컴퓨터 안에서 실행 중인 프로그램의 출입구</p>
</li>
<li><p>비유</p>
<ul>
<li>IP = 건물 주소</li>
<li>Port = 건물 안의 문 번호</li>
</ul>
</li>
<li><p>대표적인 포트 예시</p>
<ul>
<li><p>80 : 웹 서버</p>
</li>
<li><p>3306 : MySQL</p>
</li>
<li><p>6379 : Redis</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="docker에서의-포트-매핑">Docker에서의 포트 매핑</h3>
</blockquote>
<h4 id="컨테이너는-외부와-격리된-공간이기-때문에">컨테이너는 외부와 격리된 공간이기 때문에</h4>
<h4 id="외부에서-접근하려면-포트를-연결해야-한다">외부에서 접근하려면 포트를 연결해야 한다.</h4>
<pre><code>-p [호스트 포트]:[컨테이너 포트]</code></pre><ul>
<li><p>예시 1</p>
<pre><code>docker run -d -p 6379:6379 redis:latest</code></pre></li>
<li><p>왼쪽 6379    내 컴퓨터(호스트) 포트</p>
</li>
<li><p>오른쪽 6379    컨테이너 내부 Redis 포트</p>
</li>
</ul>
<h4 id="호스트-6379-→-컨테이너-6379-연결">호스트 6379 → 컨테이너 6379 연결</h4>
<ul>
<li>예시 2<pre><code>docker run -d -p 8000:6379 redis:latest</code></pre></li>
</ul>
<h4 id="호스트-8000-포트로-접근하면-컨테이너-내부의-redis6379로-연결됨">호스트 8000 포트로 접근하면 컨테이너 내부의 Redis(6379)로 연결됨</h4>
<hr>
<blockquote>
<h3 id="실행-중인-redis-컨테이너-접근하기">실행 중인 Redis 컨테이너 접근하기</h3>
</blockquote>
<ul>
<li>컨테이너 ID 확인<pre><code>docker ps
</code></pre></li>
</ul>
<p>a12b3c4d5e6f   redis:latest   Up 30 seconds</p>
<pre><code>
- 컨테이너 내부 접속</code></pre><p>docker exec -it a12b3c4d5e6f bash</p>
<pre><code>
- docker exec : 실행 중인 컨테이너에 명령 실행
- -it : 터미널 입출력 모드
- a12b3c4d5e6f : 컨테이너 ID (앞자리만 가능)
- bash : Bash 쉘 실행

- 접속 성공 시:</code></pre><p>root@a12b3c4d5e6f:/data#</p>
<pre><code>
#### 이제 컨테이너 내부 환경

---

&gt;### Redis CLI 실행
</code></pre><p>redis-cli</p>
<pre><code>
- 정상 접속 시:</code></pre><p>127.0.0.1:6379&gt;</p>
<pre><code>
#### Redis 서버에 직접 명령어 입력 가능

&gt;### 실습 정리 한 줄 요약
#### Docker Hub에서 Redis 이미지를 내려받아
#### 컨테이너로 실행하고,
#### 포트 매핑을 통해 외부에서 Redis에 접근했다.

---

&gt;## Docker로 MySQL 8.0 실행하기

- MySQL 이미지 다운로드</code></pre><p>docker pull mysql:8.0</p>
<pre><code>
- pull : Docker Hub에서 이미지 다운로드

- mysql:8.0 : MySQL 8.0 버전 이미지

- MySQL 8.0은 현재 가장 널리 사용되는 안정적인 LTS 버전

- 이미지 다운로드 확인</code></pre><p>docker images</p>
<pre><code>
- 출력 예시:</code></pre><p>REPOSITORY   TAG   IMAGE ID       CREATED        SIZE
mysql        8.0   9b51f5e3c789   2 weeks ago    595MB</p>
<pre><code>
#### MySQL 실행에 필요한 환경이 이미지로 준비됨

&gt;### MySQL 컨테이너 생성 및 실행

#### 로컬 환경에서 기존 MySQL이 3306 포트를 사용 중이기 때문에 컨테이너는 3307 포트로 매핑하여 실행한다.</code></pre><p>docker run -d <br>  --name mysql-docker <br>  -e MYSQL_ROOT_PASSWORD=1234 <br>  -e MYSQL_DATABASE=nbcam <br>  -p 3307:3306 <br>  mysql:8.0</p>
<pre><code>
- -d : 백그라운드 실행
- --name mysql-docker : 컨테이너 이름 지정
- -e MYSQL_ROOT_PASSWORD=1234 : MySQL root 계정 비밀번호 설정
- -e MYSQL_DATABASE=nbcam : 컨테이너 시작 시 DB 자동 생성
- -p 3307:3306 : 호스트 포트 → 컨테이너 포트 매핑
- mysql:8.0 : 실행할 이미지

- 포트 구조 설명

  - 호스트(내 컴퓨터): 3307

  - 컨테이너(MySQL 내부): 3306

#### 외부에서는 3307로 접속하지만, 컨테이너 내부 MySQL은 기본 포트인 3306을 그대로 사용

---

&gt;### MySQL 컨테이너 실행 상태 확인
</code></pre><p>docker ps</p>
<pre><code>
- 출력 예시:</code></pre><p>CONTAINER ID   IMAGE       STATUS          PORTS
b1c3d5e7f9a1   mysql:8.0   Up 10 seconds   0.0.0.0:3307-&gt;3306/tcp</p>
<pre><code>
- 확인 포인트

  - STATUS : Up → 정상 실행

  - PORTS : 3307-&gt;3306 → 포트 매핑 성공

  - 외부 접근 가능 포트: 3307

---

&gt;### Spring Boot와 Docker MySQL 연결

#### Spring Boot 프로젝트의 application.yml에서
#### Docker MySQL 컨테이너 포트(3307)로 연결 설정을 한다.</code></pre><p>spring:
  datasource:
    url: jdbc:mysql://localhost:3307/nbcam
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver</p>
<pre><code>
- 설정 포인트

  - localhost:3307 → 호스트 포트 기준

  - nbcam → 컨테이너 실행 시 자동 생성된 DB

  - Docker를 사용해도 Spring 설정 방식은 기존과 동일

&gt;### 요약
#### Docker로 MySQL 8.0 이미지를 실행하고,
#### 포트 매핑을 통해 로컬 환경과 충돌 없이
#### Spring Boot에서 안정적으로 DB를 연결했다.

---

&gt;### Redis + MySQL 핵심
#### DB도 설치가 아닌 실행의 개념으로 관리 가능
#### 포트 충돌 문제를 Docker로 깔끔하게 해결
#### 개발 환경을 언제든지 삭제 / 재생성 가능
#### 팀원 간 DB 환경 차이를 제거

---

&gt;## Dockerfile &amp; Docker Compose로 Spring Boot 실행하기

### Dockerfile (Spring Boot 이미지 빌드)</code></pre><p>FROM eclipse-temurin:21-jdk
COPY build/libs/*.jar app.jar
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]
EXPOSE 8080</p>
<pre><code>
- Dockerfile 설명
  - FROM eclipse-temurin:21-jdk : JDK 21 기반 이미지 사용
  - COPY build/libs/*.jar app.jar : 빌드된 jar 파일 복사
  - ENTRYPOINT : 컨테이너 시작 시 실행할 명령
  - EXPOSE 8080 : 애플리케이션 포트 명시

- 포인트

  - Spring Boot 애플리케이션을 컨테이너 이미지로 패키징

  - build/libs/*.jar → Gradle 빌드 결과 자동 매칭

- jar 생성 명령</code></pre><p>./gradlew bootJar</p>
<pre><code>---

&gt;### docker-compose.yml 생성
</code></pre><p>version: &#39;3.8&#39;</p>
<p>services:
  mysql:
    image: mysql:8.0
    container_name: mysql-compose
    environment:
      MYSQL_ROOT_PASSWORD: 1234
      MYSQL_DATABASE: nbcam
    ports:
      - &quot;3307:3306&quot;
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - spring-net</p>
<p>  spring:
    build: .
    container_name: spring-compose
    depends_on:
      - mysql
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3307/nbcam
      SPRING_DATASOURCE_USERNAME: root
      SPRING_DATASOURCE_PASSWORD: 1234
    ports:
      - &quot;8080:8080&quot;
    networks:
      - spring-net</p>
<p>volumes:
  mysql_data:</p>
<p>networks:
  spring-net:</p>
<pre><code>---

&gt;### docker-compose 구성 요소 설명

- 서비스 구성

  - mysql : MySQL 8.0 컨테이너
  - spring : Dockerfile 기반 Spring Boot 애플리케이션
- depends_on</code></pre><p>depends_on:</p>
<ul>
<li>mysql<pre><code></code></pre></li>
</ul>
<h4 id="spring-컨테이너가-mysql-컨테이너-이후에-실행">Spring 컨테이너가 MySQL 컨테이너 이후에 실행</h4>
<h4 id="단-db-준비-완료까지-보장하진-않음-실무에서는-healthcheck-추가">단, DB “준비 완료”까지 보장하진 않음 (실무에서는 healthcheck 추가)</h4>
<hr>
<ul>
<li>Docker 네트워크<pre><code>networks:
- spring-net</code></pre></li>
</ul>
<h4 id="컨테이너는-기본적으로-서로-격리되어-있음">컨테이너는 기본적으로 서로 격리되어 있음</h4>
<h4 id="같은-네트워크에-포함되면-서비스-이름으로-통신-가능">같은 네트워크에 포함되면 서비스 이름으로 통신 가능</h4>
<ul>
<li><p>포인트</p>
<ul>
<li><p>mysql → 컨테이너 이름이자 호스트 이름</p>
</li>
<li><p>Spring에서 localhost ❌</p>
</li>
<li><p>jdbc:mysql://mysql:3306/nbcam (컨테이너 내부 기준)</p>
</li>
</ul>
</li>
<li><p>볼륨(Volume)</p>
<pre><code>volumes:
- mysql_data:/var/lib/mysql</code></pre></li>
</ul>
<h4 id="mysql-데이터-디렉토리와-볼륨-연결">MySQL 데이터 디렉토리와 볼륨 연결</h4>
<h4 id="컨테이너-삭제재시작해도-db-데이터-유지">컨테이너 삭제/재시작해도 DB 데이터 유지</h4>
<ul>
<li><p><code>없다면?</code></p>
<ul>
<li><h4 id="컨테이너-삭제-시-db-데이터-전부-날아감">컨테이너 삭제 시 DB 데이터 전부 날아감</h4>
</li>
</ul>
</li>
<li><p>환경 변수(Environment)</p>
<pre><code>environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3307/nbcam
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: 1234</code></pre></li>
<li><p>Spring Boot 설정을 application.yml 대신 주입</p>
</li>
<li><p>환경별 설정 분리 가능 (local / dev / prod)</p>
</li>
</ul>
<hr>
<h3 id="포트-관련-핵심-정리-중요">포트 관련 핵심 정리 (중요)</h3>
<ul>
<li>MySQL 컨테이너 내부 : 3306</li>
<li>MySQL 컨테이너 간 통신 : 3306</li>
<li>호스트 접근용 : 3307</li>
</ul>
<blockquote>
<h4 id="spring-↔-mysql-컨테이너-통신은">Spring ↔ MySQL 컨테이너 통신은</h4>
</blockquote>
<h4 id="같은-네트워크-내부-통신">“같은 네트워크 내부 통신”</h4>
<h4 id="→-호스트-포트3307가-아니라-컨테이너-포트3306를-사용">→ 호스트 포트(3307)가 아니라 컨테이너 포트(3306)를 사용</h4>
<ul>
<li>실무 기준 권장:<pre><code>SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/nbcam</code></pre></li>
</ul>
<h4 id="3307은-내-pc에서-접근할-때만-필요">(3307은 내 PC에서 접근할 때만 필요)</h4>
<hr>
<blockquote>
<h3 id="docker-compose-실행">docker-compose 실행</h3>
</blockquote>
<pre><code>docker compose up -d</code></pre><ul>
<li>up : compose 파일 기준으로 컨테이너 실행</li>
<li>-d : 백그라운드 실행</li>
</ul>
<blockquote>
<h3 id="실행-확인">실행 확인</h3>
</blockquote>
<pre><code>docker ps</code></pre><ul>
<li><p>mysql-compose, spring-compose 두 컨테이너가 실행 중이면 정상</p>
</li>
<li><p>8080 포트로 Spring Boot 접근 가능</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="요약-2">요약</h3>
</blockquote>
<h4 id="dockerfile로-spring-boot를-이미지화하고">Dockerfile로 Spring Boot를 이미지화하고,</h4>
<h4 id="docker-compose로-mysql과-함께-실행하여">docker-compose로 MySQL과 함께 실행하여</h4>
<h4 id="멀티-컨테이너-환경을-한-번에-구성했다">멀티 컨테이너 환경을 한 번에 구성했다.</h4>
<ul>
<li><p>중요한 이유 (실무 관점)</p>
<ul>
<li><p>로컬 ↔ 서버 환경 차이 제거</p>
</li>
<li><p>DB + 애플리케이션 한 번에 실행</p>
</li>
<li><p>팀원 누구나 docker compose up 한 줄로 환경 구성</p>
</li>
<li><p>운영 환경과 거의 동일한 구조</p>
</li>
</ul>
</li>
</ul>
<hr>
<p>*<em>개념 정리를 하는데 너무 너무 어렵다 반복적으로 시도해보면서 체득하려고 노력해야겠다..（；´д｀）ゞ *</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 스프링 부트 개념 정리(QueryDSL)]]></title>
            <link>https://velog.io/@d0ngx2_2/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACQueryDSL</link>
            <guid>https://velog.io/@d0ngx2_2/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACQueryDSL</guid>
            <pubDate>Tue, 16 Dec 2025 12:04:39 GMT</pubDate>
            <description><![CDATA[<h1 id="jpql의-한계와-querydsl">JPQL의 한계와 QueryDSL</h1>
<blockquote>
<h2 id="jpql이란">JPQL이란?</h2>
</blockquote>
<ul>
<li>JPQL(Java Persistence Query Language) 은 엔티티 객체를 대상으로 쿼리를 작성하는 문자열 기반 쿼리 언어이다.<pre><code class="language-java">String jpql = &quot;SELECT u FROM User u WHERE u.age &gt; 20&quot;;
List&lt;User&gt; result = em.createQuery(jpql, User.class).getResultList();</code></pre>
</li>
</ul>
<hr>
<blockquote>
<h2 id="jpql의-한계">JPQL의 한계</h2>
</blockquote>
<ul>
<li>JPQL은 간단한 쿼리에는 유용하지만, 실무에서 복잡한 조건이나 동적 쿼리를 작성할 때 여러 한계가 드러난다.</li>
</ul>
<h3 id="요약">요약</h3>
<ul>
<li>문자열 기반 : IDE 자동완성, 리팩토링 지원 불가</li>
<li>컴파일 타임 검증 불가 : 오타, 필드명 변경 시 런타임 오류</li>
<li>유지보수 어려움 : 동적 쿼리 작성 시 가독성 급격히 저하</li>
<li>타입 안정성 없음    : 잘못된 타입 비교도 컴파일 통과</li>
</ul>
<h4 id="쿼리가-길어질수록-안정성과-유지보수성이-급격히-떨어짐">쿼리가 길어질수록 안정성과 유지보수성이 급격히 떨어짐</h4>
<hr>
<blockquote>
<h2 id="querydsl의-등장">QueryDSL의 등장</h2>
</blockquote>
<ul>
<li>QueryDSL은 JPQL을 타입 안전(Type-safe) 하게 작성하기 위한 쿼리 빌더 라이브러리이다.</li>
</ul>
<h3 id="핵심-개념">핵심 개념</h3>
<ul>
<li><p>JPQL을 대체하는 것이 아니라 JPQL을 생성하는 DSL</p>
</li>
<li><p>SQL이 아닌 JPA 위에서 동작</p>
</li>
<li><p>Java 코드로 쿼리를 작성</p>
</li>
</ul>
<h4 id="즉-문자열-대신-객체와-메서드로-쿼리를-작성">즉, 문자열 대신 객체와 메서드로 쿼리를 작성</h4>
<h3 id="querydsl-장점-요약">QueryDSL 장점 요약</h3>
<ul>
<li>IDE 자동완성 : 엔티티 필드 자동완성 지원</li>
<li>컴파일 타임 검증 : 잘못된 필드 접근 시 컴파일 에러</li>
<li>동적 쿼리 강력 : BooleanExpression, BooleanBuilder</li>
<li>가독성 : 쿼리 구조가 코드 흐름으로 드러남</li>
<li>유지보수성 : 리팩토링에 안전</li>
</ul>
<hr>
<h3 id="querydsl-설정-spring-boot-3x">QueryDSL 설정 (Spring Boot 3.x)</h3>
<ul>
<li><p>build.gradle 설정</p>
<pre><code>implementation &#39;com.querydsl:querydsl-jpa:5.0.0:jakarta&#39;
annotationProcessor &quot;com.querydsl:querydsl-apt:5.0.0:jakarta&quot;
annotationProcessor &quot;jakarta.annotation:jakarta.annotation-api&quot;
annotationProcessor &quot;jakarta.persistence:jakarta.persistence-api&quot;</code></pre></li>
<li><p>중요</p>
<ul>
<li><p>Spring Boot 3.x → jakarta.persistence 기반</p>
</li>
<li><p>반드시 :jakarta suffix 필요</p>
</li>
</ul>
</li>
<li><p>QuerydslConfig 설정</p>
</li>
</ul>
<pre><code class="language-java">@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(em);
    }
}</code></pre>
<h4 id="jpaqueryfactory를-bean으로-등록해-repository에서-사용">JPAQueryFactory를 Bean으로 등록해 Repository에서 사용</h4>
<hr>
<blockquote>
<h2 id="q-type-이해하기">Q-Type 이해하기</h2>
</blockquote>
<ul>
<li><p>QueryDSL은 엔티티를 기반으로 Q타입 클래스를 자동 생성한다.</p>
</li>
<li><p>엔티티</p>
<pre><code class="language-java">@Entity
public class User {

  @Id @GeneratedValue
  private Long id;

  private String username;
  private int age;
}</code></pre>
</li>
<li><p>생성된 QUser</p>
<pre><code class="language-java">@Generated(&quot;com.querydsl.codegen.EntitySerializer&quot;)
public class QUser extends EntityPathBase&lt;User&gt; {

  public static final QUser user = new QUser(&quot;user&quot;);

  public final StringPath username = createString(&quot;username&quot;);
  public final NumberPath&lt;Integer&gt; age = createNumber(&quot;age&quot;, Integer.class);
}</code></pre>
</li>
</ul>
<h3 id="q클래스-위치">Q클래스 위치</h3>
<p>build → classes → java → main → 엔티티 패키지 → Q클래스</p>
<h4 id="이-q타입을-통해-타입-안전한-쿼리-작성-가능">이 Q타입을 통해 타입 안전한 쿼리 작성 가능</h4>
<hr>
<blockquote>
<h2 id="기본-querydsl-쿼리-예제">기본 QueryDSL 쿼리 예제</h2>
</blockquote>
<ul>
<li><p>Repository 구조</p>
<pre><code class="language-java">public interface PostRepository
  extends JpaRepository&lt;Post, Long&gt;, PostCustomRepository {

  @EntityGraph(attributePaths = {&quot;user&quot;, &quot;comments&quot;})
  List&lt;Post&gt; findByUserUsername(String username);
}
</code></pre>
</li>
</ul>
<p>public interface PostCustomRepository {
    List<PostSummaryDto> findPostSummary(String username);
}</p>
<pre><code>
- Custom Repository 구현
```java
public class PostCustomRepositoryImpl implements PostCustomRepository {

    private final JPAQueryFactory queryFactory;

    public PostCustomRepositoryImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    @Override
    public List&lt;PostSummaryDto&gt; findPostSummary(String username) {
        return queryFactory
            .select(Projections.constructor(
                PostSummaryDto.class,
                post.content,
                comment.countDistinct().intValue()
            ))
            .from(post)
            .leftJoin(post.comments, comment)
            .where(post.user.username.eq(username))
            .groupBy(post.id)
            .fetch();
    }
}</code></pre><hr>
<blockquote>
<h2 id="jpql-→-querydsl-리팩토링-비교">JPQL → QueryDSL 리팩토링 비교</h2>
</blockquote>
<p>❌ JPQL 버전</p>
<pre><code class="language-java">@Query(&quot;&quot;&quot;
    SELECT new org.example.plus.domain.post.model.dto.PostSummaryDto(
        p.content,
        SIZE(p.comments)
    )
    FROM Post p
    WHERE p.user.username = :username
&quot;&quot;&quot;)
List&lt;PostSummaryDto&gt; findAllWithCommentsByUsername(
    @Param(&quot;username&quot;) String username
);</code></pre>
<ul>
<li><p>문제점</p>
<ul>
<li><p>문자열 기반</p>
</li>
<li><p>필드명 변경 시 런타임 오류</p>
</li>
<li><p>IDE 도움 거의 없음</p>
</li>
</ul>
</li>
</ul>
<p>✅ QueryDSL 버전</p>
<pre><code class="language-java">@Override
public List&lt;PostSummaryDto&gt; findAllWithCommentsByUsername(String username) {
    return queryFactory
        .select(Projections.constructor(
            PostSummaryDto.class,
            post.content,
            comment.countDistinct().intValue()
        ))
        .from(post)
        .leftJoin(post.comments, comment)
        .where(post.user.username.eq(username))
        .groupBy(post.id)
        .fetch();
}</code></pre>
<ul>
<li><p>장점</p>
<ul>
<li><p>자동완성 지원</p>
</li>
<li><p>컴파일 타임 검증</p>
</li>
<li><p>쿼리 구조가 코드 흐름 그대로 드러남</p>
</li>
</ul>
</li>
</ul>
<h3 id="요약-1">요약</h3>
<blockquote>
<h4 id="jpql은-문자열-기반이라는-한계가-있으며">JPQL은 문자열 기반이라는 한계가 있으며,</h4>
</blockquote>
<h4 id="querydsl은-이를-타입-안전하고-유지보수-가능한-방식으로-해결한다">QueryDSL은 이를 타입 안전하고 유지보수 가능한 방식으로 해결한다.</h4>
<h4 id="실무에서는-복잡한-조회-로직일수록-querydsl의-가치가-커진다">실무에서는 복잡한 조회 로직일수록 QueryDSL의 가치가 커진다.</h4>
<hr>
<h1 id="querydsl-기본-개념--사용법-정리">QueryDSL 기본 개념 &amp; 사용법 정리</h1>
<blockquote>
<h2 id="querydsl-기본-구조-이해">QueryDSL 기본 구조 이해</h2>
</blockquote>
<ul>
<li><p>QueryDSL의 기본 쿼리 구조는 아래 패턴을 따른다.</p>
<pre><code class="language-java">queryFactory
  .select(조회대상)
  .from(대상엔티티)
  .where(조건)
  .orderBy(정렬)
  .fetch();</code></pre>
</li>
<li><p>SQL과 유사하지만 Java 코드 기반</p>
</li>
<li><p>각 단계가 메서드 체이닝으로 명확하게 구분됨</p>
</li>
<li><p>필요 없는 절은 생략 가능 (where, orderBy 등)</p>
</li>
</ul>
<hr>
<h3 id="q-type타입-활용법">Q-Type(타입) 활용법</h3>
<ul>
<li><p>QueryDSL은 컴파일 시점에 엔티티 기반 Q타입 클래스를 자동 생성한다.</p>
</li>
<li><p>생성된 QUser 예시</p>
<pre><code class="language-java">public class QUser extends EntityPathBase&lt;User&gt; {

  public static final QUser user = new QUser(&quot;user&quot;);

  public final StringPath username = createString(&quot;username&quot;);
  public final StringPath email = createString(&quot;email&quot;);
  public final EnumPath&lt;UserRoleEnum&gt; roleEnum =
      createEnum(&quot;roleEnum&quot;, UserRoleEnum.class);
}</code></pre>
</li>
<li><p>QUser.user 를 통해 엔티티 필드에 접근</p>
</li>
<li><p>문자열이 아닌 객체 + 메서드 기반</p>
</li>
<li><p>IDE 자동완성 지원</p>
</li>
<li><p>필드명 변경 시 컴파일 에러로 즉시 확인 가능</p>
</li>
</ul>
<h4 id="런타임-오류-→-컴파일-타임-오류로-전환">런타임 오류 → 컴파일 타임 오류로 전환</h4>
<hr>
<h3 id="기본-검색-쿼리-예제">기본 검색 쿼리 예제</h3>
<ul>
<li><p>NORMAL 사용자 중 gmail.com 이메일을 가진 사용자 조회</p>
<pre><code class="language-java">List&lt;User&gt; result = queryFactory
  .selectFrom(user)
  .where(
      user.roleEnum.eq(UserRoleEnum.NORMAL),
      user.email.endsWith(&quot;gmail.com&quot;)
  )
  .fetch();</code></pre>
</li>
<li><p>결과</p>
<ul>
<li>앨리스 (<a href="mailto:alice@gmail.com">alice@gmail.com</a>)</li>
<li>찰리 (<a href="mailto:charlie@gmail.com">charlie@gmail.com</a>)</li>
</ul>
</li>
</ul>
<h3 id="자주-사용하는-조건-메서드">자주 사용하는 조건 메서드</h3>
<ul>
<li><p>eq() : 같다 </p>
<ul>
<li>user.roleEnum.eq(ADMIN)</li>
</ul>
</li>
<li><p>ne() : 다르다</p>
<ul>
<li>user.username.ne(&quot;관리자&quot;)</li>
</ul>
</li>
<li><p>gt() : 초과</p>
<ul>
<li>user.id.gt(1)</li>
</ul>
</li>
<li><p>goe() : 이상</p>
<ul>
<li>user.id.goe(3)</li>
</ul>
</li>
<li><p>contains() : 포함</p>
<ul>
<li>user.email.contains(&quot;gmail&quot;)</li>
</ul>
</li>
<li><p>endsWith() : 끝남</p>
<ul>
<li>user.email.endsWith(&quot;.com&quot;)</li>
</ul>
</li>
<li><p>fetch() → 리스트 반환</p>
</li>
<li><p>fetchOne() → 단건 (2개 이상이면 예외)</p>
</li>
<li><p>fetchFirst() → 첫 번째 결과만 반환</p>
</li>
</ul>
<hr>
<h3 id="정렬-order-by">정렬 (Order By)</h3>
<ul>
<li><p>사용자 이름 오름차순 + ID 내림차순으로 정렬 후 3명 조회</p>
<pre><code class="language-java">List&lt;User&gt; result = queryFactory
  .selectFrom(user)
  .orderBy(
      user.username.asc(),
      user.id.desc()
  )
  .limit(3)
  .fetch();</code></pre>
</li>
<li><p>결과</p>
<ul>
<li>관리자</li>
<li>밥</li>
<li>앨리스</li>
</ul>
</li>
<li><p>offset() → 시작 위치</p>
</li>
<li><p>limit() → 조회 개수</p>
</li>
<li><p>Spring Data Pageable과 함께 사용 가능</p>
</li>
</ul>
<hr>
<h3 id="문자열-검색-예제">문자열 검색 예제</h3>
<ul>
<li><p>“여행” 키워드가 포함된 게시글 조회</p>
<pre><code class="language-java">List&lt;Post&gt; result = queryFactory
  .selectFrom(post)
  .where(post.content.contains(&quot;여행&quot;))
  .fetch();</code></pre>
<ul>
<li>결과<ul>
<li>후쿠오카 여행 후기 (작성자: 앨리스)</li>
</ul>
</li>
</ul>
</li>
<li><p>contains() = SQL LIKE &#39;%keyword%&#39;</p>
</li>
</ul>
<hr>
<h3 id="논리-조합-and--or">논리 조합 (AND / OR)</h3>
<ul>
<li>ADMIN 사용자 또는 이름에 “밥”이 포함된 사용자 조회<pre><code class="language-java">List&lt;User&gt; result = queryFactory
  .selectFrom(user)
  .where(
      user.roleEnum.eq(UserRoleEnum.ADMIN)
          .or(user.username.contains(&quot;밥&quot;))
  )
  .fetch();</code></pre>
<ul>
<li>결과<ul>
<li>관리자 (ADMIN)</li>
<li>밥 (NORMAL)</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="조인join-검색">조인(JOIN) 검색</h3>
<ul>
<li><p>“앨리스”가 작성한 게시글 조회</p>
<pre><code class="language-java">List&lt;Post&gt; result = queryFactory
  .selectFrom(post)
  .join(post.user, user)
  .where(user.username.eq(&quot;앨리스&quot;))
  .fetch();</code></pre>
</li>
<li><p>결과</p>
<ul>
<li>후쿠오카 여행 후기</li>
<li>조호바루 맛집 탐방</li>
<li>싱가포르 출퇴근 일상</li>
</ul>
</li>
<li><p>join(post.user, user)
→ SQL의 INNER JOIN과 동일</p>
</li>
</ul>
<h3 id="fetch-join-n1-문제-해결">Fetch Join (N+1 문제 해결)</h3>
<ul>
<li><p>게시글 + 작성자(User)를 한 번에 조회</p>
<pre><code class="language-java">List&lt;Post&gt; result = queryFactory
  .selectFrom(post)
  .join(post.user, user).fetchJoin()
  .fetch();</code></pre>
</li>
<li><p>fetchJoin() 사용 시</p>
<ul>
<li><p>지연 로딩 무시</p>
</li>
<li><p>한 번의 쿼리로 연관 엔티티 조회</p>
</li>
<li><p>N+1 문제 방지</p>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="댓글comment-조회-예제">댓글(Comment) 조회 예제</h3>
<ul>
<li>“리제로 3기 감상평” 게시글의 모든 댓글 조회<pre><code class="language-java">List&lt;Comment&gt; comments = queryFactory
  .selectFrom(comment)
  .join(comment.post, post)
  .where(post.content.eq(&quot;리제로 3기 감상평&quot;))
  .fetch();</code></pre>
</li>
<li>결과<ul>
<li>리제로 명작이죠</li>
</ul>
</li>
</ul>
<h3 id="페이징-처리">페이징 처리</h3>
<ul>
<li>게시글 5개씩 조회 (2페이지: 6~10번)<pre><code class="language-java">List&lt;Post&gt; page2 = queryFactory
  .selectFrom(post)
  .orderBy(post.id.asc())
  .offset(5)
  .limit(5)
  .fetch();</code></pre>
</li>
<li>결과<ul>
<li><ol start="6">
<li>QueryDSL 실무 적용기</li>
</ol>
</li>
<li><ol start="7">
<li>JPA 성능 튜닝 방법</li>
</ol>
</li>
<li><ol start="8">
<li>Docker로 배포 환경 만들기</li>
</ol>
</li>
<li><ol start="9">
<li>리제로 3기 감상평</li>
</ol>
</li>
<li><ol start="10">
<li>롤체 시즌10 덱 분석</li>
</ol>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="요약-2">요약</h3>
</blockquote>
<h4 id="querydsl은-문자열-jpql의-한계를-해결하고">QueryDSL은 문자열 JPQL의 한계를 해결하고,</h4>
<h4 id="타입-안전성·가독성·유지보수성을-동시에-제공하는-jpa쿼리-도구이다">타입 안전성·가독성·유지보수성을 동시에 제공하는 JPA쿼리 도구이다.</h4>
<h4 id="기본-crud를-넘는-조회-로직에서는-사실상-필수에-가깝다">기본 CRUD를 넘는 조회 로직에서는 사실상 필수에 가깝다.</h4>
<hr>
<h2 id="동적-쿼리-dynamic-query">동적 쿼리 (Dynamic Query)</h2>
<ul>
<li><p>사용자가 입력한 조건에 따라 쿼리를 유연하게 생성하는 방식을 말한다.</p>
<ul>
<li>예를 들어 회원 검색 기능에서<pre><code>이름만 입력 → 이름으로 검색
</code></pre></li>
</ul>
</li>
</ul>
<p>이메일만 입력 → 이메일로 검색</p>
<p>이름 + 이메일 → 두 조건 모두로 검색</p>
<pre><code>
- 처럼 입력 조건이 매번 달라지는 경우에 사용한다.

- QueryDSL에서는 주로 다음 두 가지 방식으로 동적 쿼리를 구현한다.

  - BooleanBuilder

  - BooleanExpression

---

### 동적 쿼리가 필요한 이유

- ❌ Spring Data JPA 메서드 이름 방식의 한계
```java
List&lt;User&gt; findByUsernameAndEmailContainsAndRoleEnum(
    String username, String email, UserRoleEnum roleEnum
);</code></pre><ul>
<li>조건이 늘어날수록 메서드가 폭발적으로 증가한다.<pre><code class="language-java">findByUsername
</code></pre>
</li>
</ul>
<p>findByEmailContains</p>
<p>findByUsernameAndRoleEnum</p>
<p>findByUsernameOrEmailContainsAndRoleEnum …</p>
<pre><code>#### 유지보수 불가능

---

- ❌ if문 조합 방식의 한계
```java
if (username != null &amp;&amp; email != null) {
    return userRepository.findByUsernameAndEmail(username, email);
} else if (username != null) {
    return userRepository.findByUsername(username);
} else if (email != null) {
    return userRepository.findByEmail(email);
}</code></pre><ul>
<li><p>조건이 늘어날수록 분기 지옥</p>
</li>
<li><p>코드 중복 증가</p>
</li>
<li><p>검색 조건 수정 시 로직 전체 수정 필요</p>
</li>
</ul>
<hr>
<h3 id="동적-쿼리와-단일-책임-원칙srp">동적 쿼리와 단일 책임 원칙(SRP)</h3>
<ul>
<li>자주 나오는 질문</li>
</ul>
<h4 id="동적-쿼리는-조건을-여러-개-처리하니까-srp에-어긋나는-거-아닌가요">“동적 쿼리는 조건을 여러 개 처리하니까 SRP에 어긋나는 거 아닌가요?”</h4>
<ul>
<li><p>올바른 해석</p>
<ul>
<li>하나의 클래스(또는 메서드)는 하나의 이유로만 변경되어야 한다.</li>
</ul>
</li>
<li><p>동적 쿼리는</p>
<ul>
<li>❌ 여러 비즈니스 로직을 섞는 것이 아니라</li>
<li>✅ “회원 검색”이라는 하나의 책임 안에서</li>
<li>다양한 입력 조건을 처리하는 것</li>
</ul>
</li>
</ul>
<h4 id="srp를-위반하지-않는다">SRP를 위반하지 않는다</h4>
<hr>
<h3 id="booleanbuilder-vs-booleanexpression">BooleanBuilder vs BooleanExpression</h3>
<ul>
<li>QueryDSL에서 동적 쿼리를 작성하는 대표적인 두 방식</li>
</ul>
<blockquote>
<h4 id="booleanbuilder">BooleanBuilder</h4>
</blockquote>
<ul>
<li><p>조건을 하나씩 추가하는 가변(Mutable) 컨테이너</p>
</li>
<li><p>조건을 순차적으로 조립할 때 유용</p>
<pre><code class="language-java">BooleanBuilder builder = new BooleanBuilder();
</code></pre>
</li>
</ul>
<p>if (username != null) {
    builder.and(user.username.contains(username));
}
if (email != null) {
    builder.and(user.email.contains(email));
}
if (role != null) {
    builder.and(user.roleEnum.eq(role));
}</p>
<p>List<User> result = queryFactory
    .selectFrom(user)
    .where(builder)
    .fetch();</p>
<pre><code>- 조건을 자유롭게 추가 / 제거 가능

- if문으로 직접 null 체크 필요

- 코드가 길어질 수 있음

&gt; #### BooleanExpression

- 하나의 조건을 표현하는 불변(Immutable) 표현식

- null을 반환하면 QueryDSL이 자동으로 무시
```java
private BooleanExpression usernameContains(String username) {
    return username != null ? user.username.contains(username) : null;
}

private BooleanExpression emailContains(String email) {
    return email != null ? user.email.contains(email) : null;
}

private BooleanExpression roleEq(UserRoleEnum role) {
    return role != null ? user.roleEnum.eq(role) : null;
}

List&lt;User&gt; result = queryFactory
    .selectFrom(user)
    .where(
        usernameContains(username),
        emailContains(email),
        roleEq(role)
    )
    .fetch();</code></pre><ul>
<li><p>null 자동 무시 → 코드 깔끔</p>
</li>
<li><p>메서드 단위 재사용 가능</p>
</li>
<li><p>구조가 명확함</p>
</li>
</ul>
<hr>
<h3 id="두-방식-비교-요약">두 방식 비교 요약</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>BooleanBuilder</th>
<th>BooleanExpression</th>
</tr>
</thead>
<tbody><tr>
<td>개념</td>
<td>조건 컨테이너</td>
<td>단일 조건 표현식</td>
</tr>
<tr>
<td>객체 성격</td>
<td>가변(Mutable)</td>
<td>불변(Immutable)</td>
</tr>
<tr>
<td>null 처리</td>
<td>직접 if문</td>
<td>자동 무시</td>
</tr>
<tr>
<td>가독성</td>
<td>상대적으로 복잡</td>
<td>깔끔</td>
</tr>
<tr>
<td>재사용성</td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td>추천 상황</td>
<td>조건을 즉석에서 조립해야 할 때</td>
<td>일반적인 검색 조건</td>
</tr>
</tbody></table>
<hr>
<h3 id="실습-예제-querydsl">실습 예제 (QueryDSL)</h3>
<ul>
<li>Repository 구조<pre><code class="language-java">public interface UserRepository 
  extends JpaRepository&lt;User, Long&gt;, UserCustomRepository {
}
</code></pre>
</li>
</ul>
<p>public interface UserCustomRepository {</p>
<pre><code>List&lt;UserSearchResponse&gt; searchUserByMultiCondition(
    UserSearchRequest request, Pageable pageable);

List&lt;UserSearchResponse&gt; searchUserByMultiConditionV2(
    UserSearchRequest request, Pageable pageable);

Page&lt;UserSearchResponse&gt; searchUserByMultiConditionPage(
    UserSearchRequest request, Pageable pageable);</code></pre><p>}</p>
<pre><code>- BooleanExpression 방식
```java
@RequiredArgsConstructor
public class UserCustomRepositoryImpl implements UserCustomRepository {

    private final JPAQueryFactory queryFactory;

    @Override
    public List&lt;UserSearchResponse&gt; searchUserByMultiCondition(
            UserSearchRequest request, Pageable pageable) {

        return queryFactory
            .select(Projections.constructor(UserSearchResponse.class,
                user.username,
                user.email,
                user.roleEnum))
            .from(user)
            .where(
                usernameContains(request.getUsername()),
                emailContains(request.getEmail()),
                roleEq(request.getRole())
            )
            .orderBy(user.username.asc())
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();
    }</code></pre><pre><code class="language-java">    private BooleanExpression usernameContains(String username) {
        return username != null ? user.username.contains(username) : null;
    }

    private BooleanExpression emailContains(String email) {
        return email != null ? user.email.contains(email) : null;
    }

    private BooleanExpression roleEq(UserRoleEnum role) {
        return role != null ? user.roleEnum.eq(role) : null;
    }
}</code></pre>
<ul>
<li><p>BooleanBuilder 방식</p>
<pre><code class="language-java">@Override
public List&lt;UserSearchResponse&gt; searchUserByMultiConditionV2(
      UserSearchRequest request, Pageable pageable) {

  BooleanBuilder builder = new BooleanBuilder();

  if (request.getUsername() != null &amp;&amp; !request.getUsername().isBlank()) {
      builder.and(user.username.contains(request.getUsername()));
  }

  if (request.getEmail() != null &amp;&amp; !request.getEmail().isBlank()) {
      builder.and(user.email.contains(request.getEmail()));
  }

  if (request.getRole() != null) {
      builder.and(user.roleEnum.eq(request.getRole()));
  }

  return queryFactory
      .select(Projections.constructor(UserSearchResponse.class,
          user.username,
          user.email,
          user.roleEnum))
      .from(user)
      .where(builder)
      .orderBy(user.username.asc())
      .offset(pageable.getOffset())
      .limit(pageable.getPageSize())
      .fetch();
}</code></pre>
</li>
<li><p>Page 객체 반환 방식</p>
<pre><code class="language-java">@Override
public Page&lt;UserSearchResponse&gt; searchUserByMultiConditionPage(
      UserSearchRequest request, Pageable pageable) {

  BooleanBuilder builder = new BooleanBuilder();

  if (request.getUsername() != null &amp;&amp; !request.getUsername().isBlank()) {
      builder.and(user.username.contains(request.getUsername()));
  }

  if (request.getEmail() != null &amp;&amp; !request.getEmail().isBlank()) {
      builder.and(user.email.contains(request.getEmail()));
  }

  if (request.getRole() != null) {
      builder.and(user.roleEnum.eq(request.getRole()));
  }

  // 데이터 조회
  List&lt;UserSearchResponse&gt; content = queryFactory
      .select(Projections.constructor(UserSearchResponse.class,
          user.username,
          user.email,
          user.roleEnum))
      .from(user)
      .where(builder)
      .offset(pageable.getOffset())
      .limit(pageable.getPageSize())
      .fetch();

  // 전체 카운트 조회
  Long total = queryFactory
      .select(user.count())
      .from(user)
      .where(builder)
      .fetchOne();

  if (total == null) total = 0L;

  return new PageImpl&lt;&gt;(content, pageable, total);
}</code></pre>
<blockquote>
<h3 id="요약-3">요약</h3>
</blockquote>
<h4 id="동적-쿼리는-검색-조건이-유동적인-상황에서-필수이며">동적 쿼리는 검색 조건이 유동적인 상황에서 필수이며,</h4>
<h4 id="querydsl의-booleanexpression을-활용하면-가독성과-재사용성을-모두-잡을-수-있다">QueryDSL의 BooleanExpression을 활용하면 가독성과 재사용성을 모두 잡을 수 있다.</h4>
</li>
</ul>
<hr>
<h1 id="jpa-연관관계를-사용하는-이유와-실무에서의-변화">JPA 연관관계를 사용하는 이유와 실무에서의 변화</h1>
<blockquote>
<h2 id="jpa에서-연관관계를-사용하는-이유">JPA에서 연관관계를 사용하는 이유</h2>
</blockquote>
<p>JPA는 객체(Object)와 테이블(Table)을 매핑하는 ORM이다.
즉, 단순히 테이블을 다루는 것이 아니라 객체 그래프 탐색을 가능하게 하는 것이 핵심 목적이다.</p>
<p>연관관계 예시
@Entity
public class Post {</p>
<pre><code>@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;user_id&quot;)
private User user;</code></pre><p>}</p>
<ul>
<li><p>이렇게 매핑하면 아래 코드가 가능해진다.</p>
<pre><code class="language-java">post.getUser().getUsername();</code></pre>
</li>
<li><p>개발자가 SQL을 직접 작성하지 않아도</p>
</li>
<li><p>객체 참조만으로 연관 데이터 접근 가능</p>
<p>→ JPA가 내부적으로 쿼리를 생성</p>
</li>
</ul>
<h4 id="연관관계는-객체-그래프-탐색을-위해-존재한다">“연관관계는 객체 그래프 탐색을 위해 존재한다.”</h4>
<h4 id="하지만-이-편리함이-jpa의-가장-큰-단점의-출발점이-된다">하지만 이 편리함이 JPA의 가장 큰 단점의 출발점이 된다.</h4>
<hr>
<h3 id="jpa-연관관계가-만들어내는-문제들">JPA 연관관계가 만들어내는 문제들</h3>
<ul>
<li>대표적인 문제 정리</li>
</ul>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>N+1 문제</td>
<td>연관 엔티티 조회 시 쿼리가 N번 추가 실행</td>
</tr>
<tr>
<td>Lazy / Eager 충돌</td>
<td>서비스마다 로딩 전략이 달라 일관성 붕괴</td>
</tr>
<tr>
<td>순환 참조</td>
<td>양방향 매핑 시 JSON 직렬화 무한 루프</td>
</tr>
<tr>
<td>쿼리 제어 불가</td>
<td>JPA가 어떤 SQL을 날리는지 예측 어려움</td>
</tr>
<tr>
<td>성능 튜닝 한계</td>
<td>fetch join, batch size는 임시 해결책</td>
</tr>
</tbody></table>
<ul>
<li>N+1 문제 예시<pre><code class="language-java">List&lt;Post&gt; posts = postRepository.findAll();
</code></pre>
</li>
</ul>
<p>for (Post post : posts) {
    System.out.println(post.getUser().getUsername());
}</p>
<p>실제 실행 SQL
SELECT * FROM post;
SELECT * FROM user WHERE id = 1;
SELECT * FROM user WHERE id = 2;
SELECT * FROM user WHERE id = 3;
...</p>
<pre><code>
#### 한 번의 조회(1) + 연관 데이터 N개 → N+1 문제 발생

---

### 그럼에도 JPA 연관관계를 써야 했던 이유

#### “이렇게 문제가 많은데, 왜 연관관계를 쓸까?”

- 이유 정리
  - 트랜잭션 관리 : Dirty Checking, Cascade 전파
  - 객체 지향 모델 : 엔티티 간 참조로 도메인 로직 표현
  - 일관된 상태 관리 : 영속성 컨텍스트가 연관 객체 동기화

#### JPA의 핵심 장점은 객체 그래프 관리이고,
#### 이를 위해 연관관계라는 불편함을 감수해야 했다.

---

### QueryDSL의 등장 — 쿼리를 다시 제어하다

- QueryDSL은 타입 안전한 SQL(JPQL) 빌더이다.

- JPA가 “자동으로 쿼리를 만들어주는 방식”이라면
QueryDSL은 “개발자가 쿼리를 직접 조립하는 방식”이다.
```java
List&lt;Post&gt; result = queryFactory
    .selectFrom(post)
    .join(post.user, user)
    .where(user.username.eq(&quot;김동현&quot;))
    .fetch();</code></pre><ul>
<li><p>JPA 내부 동작에 의존 ❌</p>
</li>
<li><p>개발자가 조인 시점과 조건을 명시적으로 제어 ⭕</p>
</li>
<li><p>ID 기반 설계 — 연관관계 없는 엔티티</p>
</li>
<li><p>QueryDSL이 등장하면서 연관관계를 꼭 유지할 필요가 없어졌다.</p>
</li>
<li><p>❌ 기존 연관관계 방식</p>
<pre><code class="language-java">@Entity
public class Post {

  @ManyToOne(fetch = FetchType.LAZY)
  private User user;
}

실무형 ID 기반 방식
@Entity
public class Post {

  private Long userId; // FK만 저장
}</code></pre>
</li>
<li><p>엔티티는 순수 데이터 구조</p>
</li>
<li><p>관계는 쿼리에서만 조립</p>
</li>
</ul>
<hr>
<h3 id="querydsl로-명시적-조인">QueryDSL로 명시적 조인</h3>
<ul>
<li>이제 관계는 엔티티가 아니라 쿼리에서 정의한다.<pre><code class="language-java">List&lt;PostResponse&gt; result = queryFactory
  .select(Projections.constructor(
      PostResponse.class,
      post.content,
      user.username))
  .from(post)
  .leftJoin(user)
  .on(post.userId.eq(user.id))
  .fetch();</code></pre>
</li>
</ul>
<h4 id="관계는-코드manytoone가-아니라-쿼리join로-관리한다">“관계는 코드(@ManyToOne)가 아니라 쿼리(join)로 관리한다.”</h4>
<hr>
<h3 id="설계-변경-후-달라지는-점">설계 변경 후 달라지는 점</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>JPA 연관관계</th>
<th>QueryDSL + ID</th>
</tr>
</thead>
<tbody><tr>
<td>관계 표현</td>
<td>@ManyToOne / @OneToMany</td>
<td>Long userId</td>
</tr>
<tr>
<td>쿼리 생성</td>
<td>JPA 자동</td>
<td>개발자 직접</td>
</tr>
<tr>
<td>성능 예측</td>
<td>어려움</td>
<td>매우 쉬움</td>
</tr>
<tr>
<td>Lazy 로딩</td>
<td>Proxy 필요</td>
<td>불필요</td>
</tr>
<tr>
<td>순환 참조</td>
<td>발생 가능</td>
<td>완전 제거</td>
</tr>
<tr>
<td>유지보수성</td>
<td>낮음</td>
<td>✅ 매우 높음</td>
</tr>
</tbody></table>
<hr>
<h3 id="실무-흐름의-변화-요약">실무 흐름의 변화 요약</h3>
<pre><code>[JPA 초기]
  객체 중심 설계 → 연관관계 사용
        ↓
[N+1, Lazy 문제 발생]
  fetch join, batch size로 임시 해결
        ↓
[QueryDSL 도입]
  쿼리 직접 제어 가능
        ↓
[현재 실무 트렌드]
  연관관계 제거
  ID 기반 설계 + 명시적 Join(QueryDSL)</code></pre><h4 id="연관이-필요-없는-것은-jpa가">“연관이 필요 없는 것은 JPA가,</h4>
<h4 id="조인이-필요한-것은-querydsl이-담당한다">조인이 필요한 것은 QueryDSL이 담당한다.”</h4>
<hr>
<blockquote>
<h3 id="요약-4">요약</h3>
</blockquote>
<h4 id="jpa는-객체-그래프-탐색을-위해-연관관계를-도입했지만">JPA는 객체 그래프 탐색을 위해 연관관계를 도입했지만,</h4>
<h4 id="querydsl의-등장으로-개발자가-직접-join을-제어할-수-있게-되었고">QueryDSL의 등장으로 개발자가 직접 Join을 제어할 수 있게 되었고,</h4>
<h4 id="실무에서는-연관관계를-제거한">실무에서는 연관관계를 제거한</h4>
<h4 id="id-기반-설계--명시적-joinquerydsl-방식이-주류가-되었다">ID 기반 설계 + 명시적 Join(QueryDSL) 방식이 주류가 되었다.</h4>
<hr>
<h3 id="연관관계-끊기-실습-요약">연관관계 끊기 실습 요약</h3>
<pre><code class="language-java">User
// BEFORE
@OneToMany(mappedBy = &quot;user&quot;)
private List&lt;Post&gt; posts = new ArrayList&lt;&gt;();

// AFTER
// 연관관계 제거

Post
// BEFORE
@ManyToOne(fetch = FetchType.LAZY)
private User user;

// AFTER
private long userId;

Comment
// BEFORE
@ManyToOne(fetch = FetchType.LAZY)
private Post post;

// AFTER
private long postId;</code></pre>
<hr>
<blockquote>
<h3 id="요약-5">요약</h3>
</blockquote>
<h4 id="연관관계는-jpa의-철학에서-출발했지만">연관관계는 JPA의 철학에서 출발했지만,</h4>
<h4 id="querydsl은-실무의-통제권을-개발자에게-돌려주었다">QueryDSL은 실무의 통제권을 개발자에게 돌려주었다.</h4>
<h4 id="불편함이-쌓이면-기술은-자연스럽게-진화한다">불편함이 쌓이면, 기술은 자연스럽게 진화한다.</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 스프링 부트 개념 정리(테스트 코드)]]></title>
            <link>https://velog.io/@d0ngx2_2/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C</link>
            <guid>https://velog.io/@d0ngx2_2/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C</guid>
            <pubDate>Mon, 15 Dec 2025 10:57:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h1 id="테스트-코드의-중요성">테스트 코드의 중요성</h1>
</blockquote>
<ul>
<li><p>테스트 코드는 시간 낭비가 아니라, 미래의 나를 지켜주는 보험</p>
</li>
<li><p>테스트 코드가 없으면 기능 수정 시 기존 코드가 깨질까 항상 불안</p>
<ul>
<li><p>배포 후 버그 발견 → 긴급 수정</p>
</li>
<li><p>“어제 되던 기능이 오늘 안 됨” 같은 미스터리 발생</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h2 id="테스트-코드">테스트 코드</h2>
</blockquote>
<ul>
<li><p>기능 수정 후 테스트 한 번으로 안정성 확인</p>
</li>
<li><p>리팩토링을 자신 있게 진행 가능</p>
</li>
<li><p>기존 기능이 깨지지 않았음을 자동으로 증명</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="신입-개발자에게-특히-중요한-이유">신입 개발자에게 특히 중요한 이유</h3>
</blockquote>
<ul>
<li><p>대부분 레거시 프로젝트부터 맡게 됨</p>
</li>
<li><p>코드 히스토리·영향 범위를 알기 어려움</p>
</li>
<li><p>팀원이 매번 코드 전체를 봐줄 수 없음</p>
</li>
</ul>
<blockquote>
<p>이때 테스트 코드가 최후의 안전망</p>
</blockquote>
<ul>
<li><p>배포 전 오류를 자동으로 감지</p>
</li>
<li><p>신입 개발자의 실수를 프로젝트가 대신 막아줌</p>
</li>
</ul>
<hr>
<h3 id="테스트-피라미드-개념">테스트 피라미드 개념</h3>
<pre><code>▲  E2E Test (전체 시나리오)
│  Integration Test (통합)
│  Unit Test (단위)
└──────────────────
   빠르고, 자주, 자동화 쉬움</code></pre><ul>
<li><p>Unit Test: 메서드/클래스 단위 (가장 중요)</p>
</li>
<li><p>Integration Test: 여러 계층 묶어서 검증</p>
</li>
<li><p>E2E Test: 실제 사용자 흐름 검증</p>
</li>
</ul>
<blockquote>
<p>단위 테스트가 많을수록 유지보수 쉬움</p>
</blockquote>
<ul>
<li><p>테스트 코드의 핵심 효과 3가지</p>
<ul>
<li><p>버그 조기 발견</p>
</li>
<li><p>작성 직후 바로 확인 가능</p>
</li>
<li><p>QA 이전에 대부분의 문제 차단</p>
</li>
<li><p>리팩토링 자신감</p>
</li>
<li><p>“기존 기능이 깨지지 않음”을 자동 보장</p>
</li>
</ul>
</li>
<li><p>협업 효율</p>
<ul>
<li><p>테스트 = 코드의 의도 설명서</p>
</li>
<li><p>테스트 통과 = 동작 신뢰</p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="junit-기본-개념">JUnit 기본 개념</h3>
</blockquote>
<ul>
<li><p>Java 대표 단위 테스트 프레임워크</p>
</li>
<li><p>Spring Boot 기본 포함 (spring-boot-starter-test)</p>
</li>
<li><p>현재 표준: JUnit 5 (Jupiter)</p>
</li>
<li><p>기본 실행 흐름
@BeforeEach → @Test → @AfterEach</p>
</li>
</ul>
<ul>
<li><p>각 테스트는 서로 독립적으로 실행</p>
</li>
<li><p>하나 실패해도 다른 테스트에 영향 없음</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="자주-쓰는-junit-어노테이션">자주 쓰는 JUnit 어노테이션</h3>
</blockquote>
<ul>
<li>@Test : 테스트 메서드</li>
<li>@BeforeEach : 테스트 전 초기화</li>
<li>@AfterEach : 테스트 후 정리</li>
<li>@BeforeAll : 전체 테스트 시작 전 1회</li>
<li>@AfterAll : 전체 테스트 종료 후 1회</li>
<li>@DisplayName : 테스트 이름 가독성 향상</li>
<li>@Disabled : 테스트 비활성화</li>
</ul>
<hr>
<blockquote>
<h3 id="assertions-검증-메서드">Assertions (검증 메서드)</h3>
</blockquote>
<ul>
<li>assertEquals : 값 비교</li>
<li>assertTrue / assertFalse : 조건 검증</li>
<li>assertThrows : 예외 발생 검증</li>
<li>assertNotNull    null : 여부</li>
<li>assertAll : 여러 검증 묶기</li>
</ul>
<hr>
<blockquote>
<h2 id="테스트-작성-팁">테스트 작성 팁</h2>
</blockquote>
<ul>
<li>메서드명_상황_기대결과</li>
</ul>
<hr>
<blockquote>
<h3 id="given-when-then-패턴">Given-When-Then 패턴</h3>
</blockquote>
<ul>
<li><p>Given: 조건</p>
</li>
<li><p>When: 실행</p>
</li>
<li><p>Then: 검증</p>
</li>
</ul>
<p><strong>테스트 의도가 명확해짐</strong></p>
<hr>
<blockquote>
<h3 id="단위-테스트-핵심-원칙">단위 테스트 핵심 원칙</h3>
</blockquote>
<ul>
<li><p>가장 작은 단위를 검증</p>
</li>
<li><p>외부 의존성 제거</p>
</li>
<li><p>DB</p>
</li>
<li><p>외부 API</p>
</li>
<li><p>파일 시스템</p>
</li>
</ul>
<p><strong>그래서 필요한 개념이 Mock</strong></p>
<hr>
<blockquote>
<h3 id="mock-객체-mockito">Mock 객체 (Mockito)</h3>
</blockquote>
<ul>
<li><p>실제 객체 대신 사용하는 가짜 객체</p>
</li>
<li><p>테스트 대상 외부 의존성 제거용</p>
</li>
</ul>
<h4 id="사용시기">사용시기</h4>
<ul>
<li><p>Repository, DB, 외부 서비스 의존 시</p>
</li>
<li><p>주요 어노테이션</p>
<ul>
<li>@Mock : 가짜 객체 생성</li>
<li>@InjectMocks : 테스트 대상에 주입</li>
<li>@ExtendWith(MockitoExtension.class) : Mockito 연동</li>
<li>when().thenReturn() : 동작 정의</li>
<li>verify() : 호출 여부/횟수 검증</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<h3 id="mock-사용-여부-판단-기준">Mock 사용 여부 판단 기준</h3>
</blockquote>
<ul>
<li><p>Mock 불필요</p>
<ul>
<li><p>JwtUtil처럼 순수 로직</p>
</li>
<li><p>외부 의존성 없음</p>
</li>
</ul>
</li>
<li><p>Mock 필수</p>
<ul>
<li><p>Service → Repository 의존</p>
</li>
<li><p>DB 연결이 포함된 경우</p>
</li>
</ul>
</li>
</ul>
<h4 id="외부-의존성이-있으면-mock">“외부 의존성이 있으면 Mock”</h4>
<hr>
<blockquote>
<h2 id="테스트-코드는-명세서이자-안전망">테스트 코드는 명세서이자 안전망</h2>
</blockquote>
<ul>
<li><p>테스트 코드는 “이 기능이 어떻게 동작해야 하는지”를 보여주는 살아있는 문서다</p>
</li>
<li><p>코드 설명을 글로 읽지 않아도, 테스트 이름 + 시나리오만 봐도, 시스템의 의도를 이해할 수 있어야 좋은 테스트 코드</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="좋은-테스트-예시">좋은 테스트 예시</h3>
</blockquote>
<pre><code class="language-java">@Test
@DisplayName(&quot;사용자 저장 성공 시, DB에서 동일한 이름으로 조회된다&quot;)
void 사용자_저장_성공_조회_확인() {
    // Given
    User newUser = new User(&quot;ravi&quot;, &quot;1234&quot;);

    // When
    userService.save(newUser);
    User foundUser = userService.findByUsername(&quot;ravi&quot;);

    // Then
    assertNotNull(foundUser);
    assertEquals(&quot;ravi&quot;, foundUser.getUsername());
}</code></pre>
<ul>
<li>테스트 이름만 봐도 행위 + 기대 결과가 명확</li>
<li>테스트 = 코드 기반 명세서</li>
</ul>
<blockquote>
<h3 id="테스트-데이터-중복-문제--fixture">테스트 데이터 중복 문제 &amp; Fixture</h3>
</blockquote>
<ul>
<li><p>테스트가 늘어나면 이런 코드가 반복됨</p>
<pre><code class="language-java">new User(&quot;ravi&quot;, &quot;1234&quot;);</code></pre>
</li>
<li><p>이를 해결하는 방법이 공용 테스트 데이터(Fixture)</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="fixture-관리-방법-static-변수">Fixture 관리 방법 <code>static</code> 변수</h3>
</blockquote>
<pre><code class="language-java">class UserServiceTest {

    private static final String DEFAULT_USERNAME = &quot;ravi&quot;;
    private static final String DEFAULT_PASSWORD = &quot;1234&quot;;

    @Test
    void 회원가입_성공() {
        User user = new User(DEFAULT_USERNAME, DEFAULT_PASSWORD);
        userService.save(user);

        assertEquals(1, userService.count());
    }
}</code></pre>
<ul>
<li><p>한 곳에서 관리</p>
</li>
<li><p>값 변경 시 수정 포인트 1곳</p>
</li>
<li><p>간단한 프로젝트에 적합</p>
</li>
</ul>
<blockquote>
<h3 id="fixture-관리-방법-fixture-전용-클래스-권장">Fixture 관리 방법 <code>Fixture</code> 전용 클래스 (권장)</h3>
</blockquote>
<pre><code class="language-java">public class UserFixture {

    public static User createUser() {
        return new User(&quot;ravi&quot;, &quot;1234&quot;);
    }
}</code></pre>
<ul>
<li><p>테스트에서 사용</p>
<pre><code class="language-java">@Test
void 회원가입_성공() {
  User user = UserFixture.createUser();
  userService.save(user);

  assertEquals(1, userService.count());
}</code></pre>
</li>
<li><p>테스트 가독성 ↑</p>
</li>
<li><p>중복 제거</p>
</li>
<li><p>여러 테스트 클래스에서 재사용 가능</p>
</li>
<li><p>실무에서 가장 많이 쓰는 방식</p>
</li>
</ul>
<blockquote>
<h3 id="beforeeach--fixture-조합"><code>@BeforeEach</code> + <code>Fixture</code> 조합</h3>
</blockquote>
<pre><code class="language-java">@BeforeEach
void setup() {
    userService = new UserService();
    testUser = UserFixture.createUser();
}</code></pre>
<ul>
<li><p>매 테스트마다 완전히 새로운 상태</p>
</li>
<li><p>테스트 간 데이터 간섭(side-effect) 방지</p>
</li>
<li><p>테스트 독립성 확보</p>
</li>
</ul>
<hr>
<blockquote>
<h3 id="좋은-테스트-데이터의-조건">좋은 테스트 데이터의 조건</h3>
</blockquote>
<ul>
<li>명확성 : 어떤 역할의 데이터인지 한눈에 보이기</li>
<li>단순성 : 테스트 목적에 필요한 최소한의 데이터</li>
<li>재사용성 : 여러 테스트에서 활용 가능</li>
<li>독립성 : 다른 테스트 결과에 영향 X</li>
</ul>
<blockquote>
<h3 id="단위-테스트-vs-통합-테스트">단위 테스트 vs 통합 테스트</h3>
</blockquote>
<table>
<thead>
<tr>
<th>구분</th>
<th>단위 테스트</th>
<th>통합 테스트</th>
</tr>
</thead>
<tbody><tr>
<td>검증 범위</td>
<td>클래스/메서드</td>
<td>전체 흐름</td>
</tr>
<tr>
<td>Mock</td>
<td>✅ 사용</td>
<td>❌ 미사용</td>
</tr>
<tr>
<td>속도</td>
<td>빠름</td>
<td>느림</td>
</tr>
<tr>
<td>DB</td>
<td>사용 안 함</td>
<td>실제 DB</td>
</tr>
<tr>
<td>목적</td>
<td>로직 검증</td>
<td>시스템 동작 검증</td>
</tr>
</tbody></table>
<hr>
<blockquote>
<h3 id="통합-테스트란">통합 테스트란</h3>
</blockquote>
<ul>
<li><p>여러 계층을 실제 Bean + 실제 DB로 묶어서 검증하는 테스트</p>
</li>
<li><p>Controller → Service → Repository → DB(H2)</p>
</li>
<li><p>“진짜로 이 기능이 동작하는가?”를 확인</p>
</li>
<li><p>배포 전 최종 안정성 체크 용도</p>
</li>
</ul>
<blockquote>
<h3 id="통합-테스트-핵심-어노테이션">통합 테스트 핵심 어노테이션</h3>
</blockquote>
<ul>
<li>@SpringBootTest : 전체 스프링 컨텍스트 로드</li>
<li>@Transactional : 테스트 후 자동 롤백</li>
<li>@AutoConfigureMockMvc : Controller 테스트용</li>
</ul>
<hr>
<blockquote>
<h3 id="service-통합-테스트-예시">Service 통합 테스트 예시</h3>
</blockquote>
<pre><code class="language-java">@SpringBootTest
@Transactional
class PostServiceIntegrationTest {

    @Autowired
    private PostService postService;

    @Autowired
    private UserRepository userRepository;

    @Test
    void 게시글_생성_통합테스트() {
        // Given
        User user = new User(&quot;ravi&quot;, &quot;1234&quot;);
        userRepository.save(user);

        // When
        PostDto result = postService.createPost(&quot;ravi&quot;, &quot;통합 테스트&quot;);

        // Then
        assertThat(result.getContent()).isEqualTo(&quot;통합 테스트&quot;);
    }
}</code></pre>
<ul>
<li><p>Mock 사용 안할 시</p>
<ul>
<li><p>실제 DB 사용</p>
</li>
<li><p>전체 데이터 흐름 검증</p>
</li>
</ul>
</li>
</ul>
<blockquote>
<h3 id="controller-통합-테스트-mockmvc">Controller 통합 테스트 (MockMvc)</h3>
</blockquote>
<pre><code class="language-java">@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class PostControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void 게시글_생성_API_성공() throws Exception {
        mockMvc.perform(post(&quot;/api/post&quot;)
                .contentType(MediaType.APPLICATION_JSON)
                .content(&quot;{\&quot;content\&quot;:\&quot;테스트\&quot;}&quot;))
            .andExpect(status().isOk());
    }
}</code></pre>
<ul>
<li><p>실제 HTTP 요청처럼 테스트</p>
</li>
<li><p>DispatcherServlet → Controller → Service → DB 전부 실행</p>
</li>
<li><p>브라우저 없이 API 검증 가능</p>
</li>
</ul>
<hr>
<blockquote>
<h4 id="사용시기-1">사용시기</h4>
</blockquote>
<ul>
<li><p>단위 테스트</p>
<ul>
<li><p>로직 검증</p>
</li>
<li><p>빠른 피드백</p>
</li>
<li><p>가장 많이 작성해야 함</p>
</li>
</ul>
</li>
<li><p>통합 테스트</p>
<ul>
<li><p>핵심 기능 위주</p>
</li>
<li><p>전체 흐름 확인</p>
</li>
<li><p>너무 많으면 느려짐</p>
</li>
</ul>
</li>
</ul>
<h4 id="단위-테스트가-기반-통합-테스트는-보조">단위 테스트가 기반, 통합 테스트는 보조</h4>
<hr>
<h3 id="요약">요약</h3>
<ul>
<li><p>테스트 코드는 문서다</p>
</li>
<li><p>테스트 이름 = 기능 설명</p>
</li>
<li><p>단위 테스트로 로직을 지킨다</p>
</li>
<li><p>통합 테스트로 시스템을 검증한다</p>
</li>
</ul>
<blockquote>
<p>공부해야디....ㅠㅠ</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 9095 (스터디)]]></title>
            <link>https://velog.io/@d0ngx2_2/%EB%B0%B1%EC%A4%80-9095-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@d0ngx2_2/%EB%B0%B1%EC%A4%80-9095-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sun, 14 Dec 2025 02:26:28 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>정수 4를 1, 2, 3의 합으로 나타내는 방법은 총 7가지가 있다. 합을 나타낼 때는 수를 1개 이상 사용해야 한다.</p>
<p>1+1+1+1
1+1+2
1+2+1
2+1+1
2+2
1+3
3+1
정수 n이 주어졌을 때, n을 1, 2, 3의 합으로 나타내는 방법의 수를 구하는 프로그램을 작성하시오.</p>
<h2 id="입력">입력</h2>
<p>첫째 줄에 테스트 케이스의 개수 T가 주어진다. 각 테스트 케이스는 한 줄로 이루어져 있고, 정수 n이 주어진다. n은 양수이며 11보다 작다.</p>
<h2 id="출력">출력</h2>
<p>각 테스트 케이스마다, n을 1, 2, 3의 합으로 나타내는 방법의 수를 출력한다.</p>
<hr>
<h3 id="접근법">접근법</h3>
<ul>
<li><p>DP
복잡한 문제를 작은 하위 문제로 나누고, 이전에 계산했던 부분 문제의 답을 저장해두었다가 재활용하여 중복 계산을 피하고 효율적으로 최적의 해답을 찾아내는 알고리즘 설계 기법이자 문제 해결 방식</p>
</li>
<li><p>일단 5까지는 직접 세어 봤다.</p>
</li>
</ul>
<pre><code>1 = 1
2 = 2
3 = 4
4 = 7
5 = 13</code></pre><ul>
<li>뭔가 규칙이 있는지 찾아보니 <ul>
<li>4의 개수는 1,2,3의 개수들의 합</li>
<li>5의 개수는 2,3,4의 개수들의 합</li>
</ul>
</li>
</ul>
<p>이걸 활용해야겠다고 생각하였다..!</p>
<pre><code>테스트 케이스를 받는다
        |
1,2,3의 개수를 디폴트로 받는다        
        |
4부터 미리 계산해둔다        
        |
입력받은 값n에 해당되는 개수를 출력한다.</code></pre><hr>
<h3 id="코드">코드</h3>
<pre><code class="language-java">package Sutdy;

import java.util.Scanner;

public class Beak9095_DP {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int T = sc.nextInt();
        int[] count = new int[12];

        count[1] = 1;
        count[2] = 2;
        count[3] = 4;

        for (int i = 4; i &lt;= 11; i++) {
            count[i] = count[i - 1] + count[i - 2] + count[i - 3];
        }

        for (int i = 0; i &lt; T; i++) {
            int n = sc.nextInt();
            System.out.println(count[n]);
        }
    }
}</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot 개념 정리(JPQL, N+1, )]]></title>
            <link>https://velog.io/@d0ngx2_2/Spring-Boot-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACJPQL-N1</link>
            <guid>https://velog.io/@d0ngx2_2/Spring-Boot-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACJPQL-N1</guid>
            <pubDate>Mon, 08 Dec 2025 13:51:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h2 id="jpql">JPQL</h2>
</blockquote>
<ul>
<li>JPQL (Java Persistence Query Language)은 Entity 객체를 대상으로 쿼리를 작성하는 객체 지향 SQL이다.</li>
</ul>
<hr>
<h3 id="sql-jpql의-차이">SQL, JPQL의 차이</h3>
<p><img src="https://velog.velcdn.com/images/d0ngx2_2/post/a1d35218-de39-4923-9c77-7ad7d7e15dce/image.png" alt=""></p>
<ul>
<li><p>사용하는 이유</p>
<ul>
<li>객체 중심 개발에 자연스럽게 녹아들 수 있음<ul>
<li>JPQL은 SQL이 아닌, 객체(Entity) 를 대상으로 작성함</li>
<li>테이블/컬럼이 아닌 엔티티/필드 기준으로 쿼리를 짜서 객체지향적인 코드를 유지 가능</li>
</ul>
</li>
</ul>
</li>
<li><p>데이터베이스 독립성 확보    </p>
<ul>
<li>JPQL은 JPA 구현체(Hibernate 등)가 SQL로 변환해주므로 DBMS에 종속되지 않음</li>
</ul>
</li>
</ul>
<ul>
<li><p>정적 타입 지원 + 자동 바인딩</p>
<ul>
<li>@Query, 메서드 네이밍, QueryDSL 등과 조합하면 타입 안정성 확보 및 IDE 자동완성 활용 가능</li>
</ul>
</li>
<li><p>복잡한 쿼리도 객체 기준으로 구성 가능</p>
<ul>
<li>복잡한 JOIN, 조건 쿼리도 객체 모델에 맞춰 작성 가능</li>
</ul>
</li>
</ul>
<hr>
<h3 id="jpql-vs-jpa-차이점">JPQL vs JPA 차이점</h3>
<p><img src="https://velog.velcdn.com/images/d0ngx2_2/post/f0aef54c-4cc1-4918-9541-bffb30f3eafb/image.png" alt=""></p>
<h3 id="jpql쓰는-시점">JPQL쓰는 시점</h3>
<p><img src="https://velog.velcdn.com/images/d0ngx2_2/post/0b68b4c5-4668-4864-b5f2-61b865d8f94f/image.png" alt=""></p>
<h3 id="사용-예시">사용 예시</h3>
<ul>
<li>SELECT<pre><code class="language-java">SELECT u FROM User u;      // 전체 유저 객체 조회
</code></pre>
</li>
</ul>
<p>SELECT u.username FROM User u;  // 특정 필드만 조회</p>
<pre><code>
- WHERE
```java
SELECT u FROM User u WHERE u.age &gt; 20;</code></pre><ul>
<li><p>ORDER BY</p>
<pre><code class="language-java">SELECT u FROM User u ORDER BY u.age DESC;</code></pre>
</li>
<li><p>JOIN</p>
<pre><code class="language-java">SELECT * FROM orders o JOIN user u ON o.user_id = u.id WHERE u.username = &#39;KIM&#39;;</code></pre>
</li>
</ul>
<hr>
<blockquote>
<h2 id="eager-vs-lazy-로딩">EAGER vs LAZY 로딩</h2>
</blockquote>
<h3 id="요약본">요약본</h3>
<p><img src="https://velog.velcdn.com/images/d0ngx2_2/post/5e7044c3-a2fb-494d-ac92-66e81336ad11/image.png" alt=""></p>
<h3 id="n1이란">N+1이란?</h3>
<ul>
<li>엔티티 1개를 조회했더니, 추가 쿼리가 N개 더 실행되는 현상</li>
<li>특히 연관된 데이터를 루프를 돌며 접근할 때 자주 발생</li>
</ul>
<hr>
<h3 id="해결방법">해결방법</h3>
<h4 id="fetch-join">Fetch Join</h4>
<ul>
<li>연관된 엔티티를 한 번의 쿼리로 함께 조회하기 위한 JPQL 키워드</li>
</ul>
<pre><code class="language-java">@Query(&quot;SELECT p FROM Post p JOIN FETCH p.comments WHERE p.user.username = :username&quot;)

List&lt;Post&gt; findAllWithCommentsByUsername(@Param(&quot;username&quot;) String username);</code></pre>
<ul>
<li>JOIN FETCH 키워드를 사용하면, 지연 로딩 설정이 되어 있더라도 즉시 로딩됨</li>
<li>연관된 엔티티들을 SQL의 JOIN으로 함께 가져오기 때문에 추가 쿼리 발생이 없다.</li>
</ul>
<h4 id="주의사항">주의사항</h4>
<ul>
<li><p>컬렉션 페이징 불가</p>
<ul>
<li>JOIN FETCH를 사용하면 중복된 결과로 인해 Pageable이 정상 동작하지 않음</li>
</ul>
</li>
<li><p>여러 컬렉션 Fetch Join 금지</p>
<ul>
<li>JPA는 1개 이상의 컬렉션 Fetch Join을 허용하지 않음 </li>
</ul>
</li>
<li><p>중복 제거 필요</p>
<ul>
<li>DISTINCT를 사용하여 중복된 엔티티 제거 필요 </li>
</ul>
</li>
</ul>
<hr>
<h4 id="batchsize">BatchSize</h4>
<ul>
<li><p>Hibernate 설정을 통해 컬렉션 또는 연관된 엔티티들을 배치로 로딩할 수 있게 해주는 기능입니다.</p>
<ul>
<li><p>JPA에서는 LAZY 로딩 전략이 기본이기 때문에 연관된 엔티티들을 개별 쿼리로 가져오며, 
이로 인해 N+1 문제가 발생할 수 있습니다.</p>
</li>
<li><p>BatchSize는 이러한 상황에서 Hibernate가 여러 엔티티를 한 번에 조회할 수 있도록 돕는 설정입니다.</p>
</li>
<li><p>즉, LAZY 전략을 유지하면서도 IN 절을 활용한 일괄 조회가 가능해집니다.</p>
</li>
</ul>
</li>
</ul>
<h4 id="글로벌-설정법">글로벌 설정법</h4>
<ul>
<li><p>글로벌 설정 (application.yml) </p>
<pre><code class="language-java">spring:
 jpa:
     properties:
         hibernate:
             default_batch_fetch_size: 100</code></pre>
</li>
<li><p>개별 설정 (엔티티 또는 컬렉션 필드)</p>
<pre><code class="language-java">  @OneToMany(mappedBy = &quot;user&quot;, fetch = FetchType.LAZY) // ← 지연 로딩!
 @BatchSize(size = 100) 
 private List&lt;Post&gt; posts = new ArrayList&lt;&gt;();</code></pre>
</li>
</ul>
<h4 id="주의-사항">주의 사항</h4>
<ul>
<li><p>BatchSize 크기</p>
<ul>
<li>너무 작으면 효과 미미, 너무 크면 IN 쿼리가 길어져 성능 저하 가능</li>
</ul>
</li>
<li><p>Fetch Join과 병행 사용</p>
<ul>
<li>컬렉션 Fetch Join과 함께 쓰면 안 됨 (예: 페이징 깨짐)</li>
</ul>
</li>
<li><p>LAZY 유지 조건</p>
<ul>
<li>이 설정은 LAZY인 경우에만 유효함</li>
</ul>
</li>
</ul>
<h4 id="entitygraph">EntityGraph</h4>
<ul>
<li>Spring Data JPA에서 지원하는 기능으로, 연관관계를 JPQL 없이 즉시 로딩하도록 지정할 수 있다.</li>
</ul>
<pre><code class="language-java">@EntityGraph(attributePaths = {&quot;posts&quot;})
@Query(&quot;SELECT u FROM User u&quot;)
List&lt;User&gt; findAllWithPosts();</code></pre>
<h4 id="dto-projection">DTO Projection</h4>
<ul>
<li>DB에서 원하는 컬럼만 선택하여 DTO로 직접 매핑하는 방식</li>
</ul>
<pre><code class="language-java">@Query(&quot;SELECT new com.example.dto.UserDto(u.username, u.age) FROM User u&quot;)
List&lt;UserDto&gt; findAllUserDto();</code></pre>
<hr>
<h3 id="마무리">마무리</h3>
<ul>
<li>앞으로 N+1 문제에 대해서도 알아봤고, 해결방안까지 공부를 하였으니 앞으로의 문제에 어떤 방식이 더 효율적인 방식인지 판단할 수 있는 능력을 기를 수 있도록 해야겠다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 16918번 (스터디)]]></title>
            <link>https://velog.io/@d0ngx2_2/%EB%B0%B1%EC%A4%80-16918%EB%B2%88-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@d0ngx2_2/%EB%B0%B1%EC%A4%80-16918%EB%B2%88-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sun, 07 Dec 2025 04:02:52 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>봄버맨은 크기가 R×C인 직사각형 격자판 위에서 살고 있다. 격자의 각 칸은 비어있거나 폭탄이 들어있다.</p>
<p>폭탄이 있는 칸은 3초가 지난 후에 폭발하고, 폭탄이 폭발한 이후에는 폭탄이 있던 칸이 파괴되어 빈 칸이 되며, 인접한 네 칸도 함께 파괴된다. 즉, 폭탄이 있던 칸이 (i, j)인 경우에 (i+1, j), (i-1, j), (i, j+1), (i, j-1)도 함께 파괴된다. 만약, 폭탄이 폭발했을 때, 인접한 칸에 폭탄이 있는 경우에는 인접한 폭탄은 폭발 없이 파괴된다. 따라서, 연쇄 반응은 없다.</p>
<p>봄버맨은 폭탄에 면역력을 가지고 있어서, 격자판의 모든 칸을 자유롭게 이동할 수 있다. 봄버맨은 다음과 같이 행동한다.</p>
<p>가장 처음에 봄버맨은 일부 칸에 폭탄을 설치해 놓는다. 모든 폭탄이 설치된 시간은 같다.
다음 1초 동안 봄버맨은 아무것도 하지 않는다.
다음 1초 동안 폭탄이 설치되어 있지 않은 모든 칸에 폭탄을 설치한다. 즉, 모든 칸은 폭탄을 가지고 있게 된다. 폭탄은 모두 동시에 설치했다고 가정한다.
1초가 지난 후에 3초 전에 설치된 폭탄이 모두 폭발한다.
3과 4를 반복한다.
폭탄을 설치해놓은 초기 상태가 주어졌을 때, N초가 흐른 후의 격자판 상태를 구하려고 한다.</p>
<p>예를 들어, 초기 상태가 아래와 같은 경우를 보자.</p>
<h2 id="입력">입력</h2>
<p>첫째 줄에 R, C, N (1 ≤ R, C, N ≤ 200)이 주어진다. 둘째 줄부터 R개의 줄에 격자판의 초기 상태가 주어진다. 빈 칸은 &#39;.&#39;로, 폭탄은 &#39;O&#39;로 주어진다.</p>
<h2 id="출력">출력</h2>
<p>총 R개의 줄에 N초가 지난 후의 격자판 상태를 출력한다.</p>
<hr>
<h3 id="요약">요약</h3>
<ul>
<li>t = 1</li>
</ul>
<p>→ 입력 그대로 출력.</p>
<ul>
<li>짝수 초(t = 2, 4, 6, …)</li>
</ul>
<p>→ 모든 칸에 폭탄이 설치됨(전부 ‘O’).</p>
<ul>
<li>홀수 초 중 특별한 두 패턴만 반복됨</li>
</ul>
<p>t = 3 → 첫 번째 폭발
t = 5 → 두 번째 폭발
t = 7 → 다시 t=3 상태 반복
t = 9 → t=5 상태 반복</p>
<pre><code class="language-java">if n == 1 → 초기 맵
else if n % 2 == 0 → 전부 폭탄 맵
else if n % 4 == 3 → t=3 결과 맵
else if n % 4 == 1 → t=5 결과 맵</code></pre>
<h3 id="접근법">접근법</h3>
<ul>
<li>초기 맵 저장</li>
<li>전부 폭탄인 맵 생성</li>
<li>초기 맵 기준으로 폭발시킨 t=3 맵 생성</li>
<li>t=3 맵 기준으로 폭발시킨 t=5 맵 생성</li>
<li>시간 n의 규칙에 따라 최종 출력 선택</li>
</ul>
<h3 id="코드">코드</h3>
<pre><code class="language-java">import java.util.*;

public class Main {

    static int R, C, N;      // 행, 열, 시간
    static char[][] initial; // 초기 입력 상태
    static char[][] full;    // 모든 칸이 폭탄인 상태 (짝수 초에서 사용)
    static char[][] boom1;   // 첫 번째 폭발 결과 (t=3)
    static char[][] boom2;   // 두 번째 폭발 결과 (t=5)

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        // 입력 크기 받기
        R = sc.nextInt();
        C = sc.nextInt();
        N = sc.nextInt();
        sc.nextLine(); // 개행 제거

        // 초기 맵 입력 받기
        initial = new char[R][C];
        for (int i = 0; i &lt; R; i++) {
            initial[i] = sc.nextLine().toCharArray(); 
            // 한 줄씩 받고 char[]로 변환
        }

        full = makeFullBoard();    // 전부 &#39;O&#39;로 채운 맵 생성
        boom1 = explode(initial);  // t=3 결과 미리 계산
        boom2 = explode(boom1);    // t=5 결과 미리 계산

        print(selectBoard());      // 시간 N에 해당하는 최종 결과 출력
    }

    // ------------------ 최종 맵 선택 로직 ------------------

    // 시간 N에 따라 어떤 맵을 출력할지 결정하는 메서드
    private static char[][] selectBoard() {

        if (N == 1) return initial; // 1초는 입력 그대로

        if (N % 2 == 0) return full; 
        // 짝수 초는 무조건 모든 칸 폭탄 

        if (N % 4 == 3) return boom1; 
        // 3초, 7초, 11초… → 첫 번째 폭발 패턴 반복

        return boom2; 
        // 5초, 9초, 13초… → 두 번째 폭발 패턴 반복
    }

    // ------------------ 모든 칸 폭탄 보드 생성 ------------------

    private static char[][] makeFullBoard() {
        char[][] map = new char[R][C];  // 새로운 보드 생성
        for (int i = 0; i &lt; R; i++) {
            Arrays.fill(map[i], &#39;O&#39;);  
            // 행 단위로 &#39;O&#39;로 채우기
        }
        return map;
    }

    // ------------------ 폭발 터뜨린 보드 생성 ------------------

    private static char[][] explode(char[][] board) {
        // 폭발 후 남는 영역은 폭탄으로 가득하므로 full보드 기반으로 시작
        char[][] next = makeFullBoard();

        // 폭발 범위 정의 (자기 자신 + 상하좌우)
        int[] dx = {1, -1, 0, 0, 0};
        int[] dy = {0, 0, 1, -1, 0};
        // dx, dy를 배열로 폭발 범위를 처리

        for (int x = 0; x &lt; R; x++) {         // 모든 좌표 탐색
            for (int y = 0; y &lt; C; y++) {

                if (board[x][y] == &#39;O&#39;) {     // 현재 위치에 폭탄이 있으면
                    for (int i = 0; i &lt; 5; i++) { // 자기 + 상하좌우
                        int nx = x + dx[i];
                        int ny = y + dy[i];

                        // 보드 범위를 벗어나지 않는지 체크
                        if (nx &gt;= 0 &amp;&amp; nx &lt; R &amp;&amp; ny &gt;= 0 &amp;&amp; ny &lt; C) {
                            next[nx][ny] = &#39;.&#39;; // 폭발 영향 → 비어있는 칸으로 변경
                        }
                    }
                }
            }
        }

        return next; // 폭발 이후의 새로운 보드 반환
    }

    // ------------------ 출력 담당 ------------------

    private static void print(char[][] board) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i &lt; R; i++) {
            sb.append(board[i]).append(&#39;\n&#39;); 
            // char[] 바로 append하면 문자열로 붙어서 출력
        }

        System.out.print(sb); 
        // 마지막에 한 번만 출력
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 14916, 1092번(스터디)]]></title>
            <link>https://velog.io/@d0ngx2_2/%EB%B0%B1%EC%A4%80-14916-1092%EB%B2%88%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@d0ngx2_2/%EB%B0%B1%EC%A4%80-14916-1092%EB%B2%88%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sat, 06 Dec 2025 18:42:57 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-14016번">문제 14016번</h2>
<p>춘향이는 편의점 카운터에서 일한다.</p>
<p>손님이 2원짜리와 5원짜리로만 거스름돈을 달라고 한다. 2원짜리 동전과 5원짜리 동전은 무한정 많이 가지고 있다. 동전의 개수가 최소가 되도록 거슬러 주어야 한다. 거스름돈이 n인 경우, 최소 동전의 개수가 몇 개인지 알려주는 프로그램을 작성하시오.</p>
<p>예를 들어, 거스름돈이 15원이면 5원짜리 3개를, 거스름돈이 14원이면 5원짜리 2개와 2원짜리 2개로 총 4개를, 거스름돈이 13원이면 5원짜리 1개와 2원짜리 4개로 총 5개를 주어야 동전의 개수가 최소가 된다.</p>
<h2 id="입력">입력</h2>
<p>첫째 줄에 거스름돈 액수 n(1 ≤ n ≤ 100,000)이 주어진다.</p>
<h2 id="출력">출력</h2>
<p>거스름돈 동전의 최소 개수를 출력한다. 만약 거슬러 줄 수 없으면 -1을 출력한다.</p>
<hr>
<h3 id="요약">요약</h3>
<ul>
<li><p>거스름돈을 2원, 5원 동전으로만 만들어야 하는 문제.</p>
</li>
<li><p>목표: 동전 개수를 최소로 사용해야한다.</p>
</li>
<li><p>정확히 나누어 떨어지지 않으면 -1 출력한다.</p>
</li>
</ul>
<hr>
<h3 id="접근법">접근법</h3>
<ul>
<li><p>5원과 2원밖에 없으므로 2원보다 큰 5원을 가장 많이 써야 최소한의 동전을 사용했다는 결론.</p>
</li>
<li><p>따라서 전체 값의 5를 나눴을 때 나머지가 0이나오면 베스트이고 나머지값을 2로 나눴을 시 나누어 떨어지면 그 동전의 개수가 가장 최소한의 개수가 될 것임.</p>
</li>
<li><p>다만 나누어 떨어지지 않을 경우 5원을 빼가면서 다시 계산을 해주어야하고 다 빼서도 안되면 -1을 반환시키면 된다.</p>
</li>
<li><p>n(전체값) = 5a(5원) + b(나머지)</p>
</li>
<li><p>b = n % 5</p>
<pre><code>b = 0 → OK
</code></pre></li>
</ul>
<p>b = 1 → 2원으로 만들 수 없음 → 5원 1개 빼서 조정 필요</p>
<p>b = 2 → 2원 1개로 가능</p>
<p>b = 3 → 2원으로 만들 수 없음 → 5원 1개 빼서 조정 필요</p>
<p>b = 4 → 2원 2개로 가능</p>
<pre><code>
-  b가 1, 3일 경우에만 5원을 하나씩 빼서 재계산을 해야한다.

- 다만!! n자체가 1, 3이 들어올 경우에는 예외처리를 해주어야한다.


### 코드

```java
package Sutdy;

import java.util.Scanner;

public class Beak14916_Greedy {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();

        // n값 자체가 1, 3일경우엔 -1을 반환하도록 예외처리
        if (n == 1 || n == 3) {
            System.out.println(-1);
            return;
        }

        // 5원의 개수
        int fiveNum = n / 5;

        // 나머지 1 또는 3 → 5원 하나를 뺌
        if (n % 5 == 1 || n % 5 == 3) {
            fiveNum -= 1;
        }

        // 나머지 값
        int remainder = n - (fiveNum * 5);

        // 2원의 개수
        int twoNum = remainder / 2;

        //합친 것 (5로 딱 떨어질 경우 2원의 개수는 0으로 채워지기 때문에 이상 없음)
        System.out.println(fiveNum + twoNum);
    }
}</code></pre><hr>
<h2 id="문제-1092번">문제 1092번</h2>
<p>지민이는 항구에서 일한다. 그리고 화물을 배에 실어야 한다. 모든 화물은 박스에 안에 넣어져 있다. 항구에는 크레인이 N대 있고, 1분에 박스를 하나씩 배에 실을 수 있다. 모든 크레인은 동시에 움직인다.</p>
<p>각 크레인은 무게 제한이 있다. 이 무게 제한보다 무거운 박스는 크레인으로 움직일 수 없다. 모든 박스를 배로 옮기는데 드는 시간의 최솟값을 구하는 프로그램을 작성하시오.</p>
<h2 id="입력-1">입력</h2>
<p>첫째 줄에 N이 주어진다. N은 50보다 작거나 같은 자연수이다. 둘째 줄에는 각 크레인의 무게 제한이 주어진다. 이 값은 1,000,000보다 작거나 같다. 셋째 줄에는 박스의 수 M이 주어진다. M은 10,000보다 작거나 같은 자연수이다. 넷째 줄에는 각 박스의 무게가 주어진다. 이 값도 1,000,000보다 작거나 같은 자연수이다.</p>
<h2 id="출력-1">출력</h2>
<p>첫째 줄에 모든 박스를 배로 옮기는데 드는 시간의 최솟값을 출력한다. 만약 모든 박스를 배로 옮길 수 없으면 -1을 출력한다.</p>
<hr>
<h3 id="요약-1">요약</h3>
<ul>
<li><p>여러 개의 크레인 → 각각 들 수 있는 최대 무게가 다름</p>
</li>
<li><p>박스도 여러 개 → 각 박스마다 무게가 있음</p>
</li>
<li><p>1분에 각 크레인이 박스 1개씩만 이동 가능</p>
</li>
<li><p>모든 박스를 옮기는 데 걸리는 최소 시간을 구하는 문제</p>
</li>
</ul>
<hr>
<h3 id="접근법-1">접근법</h3>
<ul>
<li><p>가장 무거운 크레인부터 작업시키기</p>
<ul>
<li>이유 : 무거운 크레인만 들 수 있는 박스가 있기 때문</li>
</ul>
</li>
<li><p>크레인 리스트와 박스 리스트를 내림차순 정렬</p>
<ul>
<li>둘 다 큰 것부터 매칭하면 관리가 훨씬 쉬워짐  </li>
</ul>
</li>
<li><p>반복문 시간 단위로 진행</p>
<ul>
<li><p>한 번의 루프가 1분을 의미</p>
</li>
<li><p>각 크레인이 할 수 있는 박스를 “순서대로” 찾아서 옮김</p>
</li>
<li><p>박스를 옮기면 그 박스는 리스트에서 제거</p>
</li>
<li><p>박스를 못 옮긴 크레인은 그냥 다음 분으로 넘어감  </p>
</li>
</ul>
</li>
</ul>
<h3 id="코드">코드</h3>
<pre><code class="language-java">package Sutdy;

import java.util.*;

public class Beak1092_Greedy {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        int N = sc.nextInt();
        List&lt;Integer&gt; cranes = new ArrayList&lt;&gt;();
        for (int i = 0; i &lt; N; i++) cranes.add(sc.nextInt());

        int M = sc.nextInt();
        List&lt;Integer&gt; boxes = new ArrayList&lt;&gt;();
        for (int i = 0; i &lt; M; i++) boxes.add(sc.nextInt());

        // 크레인, 박스 모두 내림차순 정렬
        cranes.sort(Collections.reverseOrder());
        boxes.sort(Collections.reverseOrder());

        // 무거운 박스를 들 수 있는 크레인이 없으면 불가능
        if (boxes.get(0) &gt; cranes.get(0)) {
            System.out.println(-1);
            return;
        }

        int time = 0;

        // 박스가 모두 없어질 때까지 반복
        while (!boxes.isEmpty()) {
            int boxIdx = 0;    // 현재 보고 있는 박스
            int craneIdx = 0;  // 현재 사용 중인 크레인

            // 1 분 동안 모든 크레인이 박스를 하나씩 옮김
            while (craneIdx &lt; cranes.size()) {
                if (boxIdx &gt;= boxes.size()) break;  // 박스 다 봤으면 종료

                if (cranes.get(craneIdx) &gt;= boxes.get(boxIdx)) {
                    // 들 수 있는 박스 발견 → 바로 제거
                    boxes.remove(boxIdx);
                    craneIdx++;
                } else {
                    // 현재 크레인이 못 들면 더 가벼운 박스 탐색
                    boxIdx++;
                }
            }

            time++;
        }

        System.out.println(time);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[제출용 트러블 슈팅]]></title>
            <link>https://velog.io/@d0ngx2_2/%EC%A0%9C%EC%B6%9C%EC%9A%A9-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@d0ngx2_2/%EC%A0%9C%EC%B6%9C%EC%9A%A9-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Fri, 05 Dec 2025 03:46:50 GMT</pubDate>
            <description><![CDATA[<h3 id="트러블-슈팅1">트러블 슈팅(1)</h3>
<p>ArgumentResolver등록이 안되는 문제 발생</p>
<ul>
<li>코드<pre><code class="language-java">package org.example.expert.config;
</code></pre>
</li>
</ul>
<p>import lombok.RequiredArgsConstructor;
import org.example.expert.domain.common.interceptor.adminCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;</p>
<p>import java.util.List;</p>
<p>@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {</p>
<pre><code>private final adminCheckInterceptor adminCheckInterceptor;

@Override
public void addArgumentResolvers(List&lt;HandlerMethodArgumentResolver&gt; resolvers) {
    resolvers.add(new AuthUserArgumentResolver());
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(adminCheckInterceptor)
            .addPathPatterns(&quot;/admin/users/*&quot;)
            .addPathPatterns(&quot;/admin/comments/*&quot;);
}</code></pre><p>}</p>
<pre><code>
- 오류 코드를 봤을 때 resolver.add(A) A에 들어가는 값이 null 값처리가 된다는 문구를 확인하였고 resolver는 아무 문제가 없었다.

- 한참을 씨름을 하다가 Webconfig 클래스에 @Configuration 어노테이션이 빠져있는 것을 뒤늦게 확인..

- 붙여주니 정상적으로 잘 돌아갔다. 빠진 것이 없는지 위에서부터 순차적으로 보는 것이 디버깅보다 더 첫번째 인 것 같다.

### 트러블 슈팅(2)

- 인터셉터 구현 후 정상 실행이 되는지 테스트를 진행하였고, 로그가 안찍히는 것이 확인이 되었다.

- 튜터님의 세션 덕분에 필터를 거친 후 인터셉터로 넘어가기 때문에 필터에서 중복되는 것이 있기 때문에 인터셉터에서 확인이 불가능한 것인가 의심할 수 있게 되었다.

- jwtFilter에 아니나 다를까 동일한 로직이 실행되고 있었고 그 부분을 주석 처리를 해 다시 확인해 보았다.

- 그래도 나오지 않았다. 코드를 확인해보니 

```java
if(!UserRole.ADMIN.name().equals(role)) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, &quot;권한이 없습니다&quot;);
            return false;
        }</code></pre><ul>
<li>??? 로그 코드에 아예없다. 실수인건지 까먹은건지 로그를 쓰지도 않고 로그를 확인하고 있었다.</li>
</ul>
<pre><code class="language-java">log.info(&quot;접근이 차단되었습니다.&quot;);</code></pre>
<ul>
<li><p>이후 위 코드를 추가 후 정상 작동하는 것을 확인할 수 있었다..</p>
</li>
<li><p>코드는 문제가 없다. 문제는 나에게 있다.ㅎㅎ</p>
</li>
</ul>
<h3 id="트러블-슈팅3">트러블 슈팅(3)</h3>
<ul>
<li>도전과제 5Lv에 AOP를 구현하는 과정 속에 요구사항이 요청/응답의 바디값을 로그에 찍히게 하라는 사항이 있었다.</li>
</ul>
<pre><code class="language-java">package org.example.expert.domain.common.AOP;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.ContentCachingRequestWrapper;

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;

@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class LoggingAop {

    private final ObjectMapper objectMapper;
//    .controller.*AdminController.*

    //전 후로 로그를 찍어준다.
    @Around(&quot;execution(* org.example.expert.domain.*.controller.*AdminController.*(..))&quot;)
    public Object logApi(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        HttpServletResponse response = ((ServletResponseAttributes) ResponseContextHolder.currentResponseAttributes()).getResponse();

        ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;

        Long userId = (Long) request.getAttribute(&quot;userId&quot;);
        LocalDateTime now = LocalDateTime.now();
        String requestURI = request.getRequestURI();
        String resquestBody = new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
        log.info(&quot;userId = {},Time = {}, URL = {}, body = {}&quot;, userId, now, requestURI, resquestBody);

        Object result = joinPoint.proceed();

        log.info(&quot;body = {}&quot;, response);

        return result;
    }
}</code></pre>
<ul>
<li><p>최초 앞서 캐싱 필터를 통해 각 응답/요청을 적용 시키고, 적용 시킨 값을 로그에 찍히게 하려하였지만 응답의 바디 값이 계속 비어져 있는 것을 확인 할 수 있었다.</p>
</li>
<li><p>팀원분과 상의하면서 의논해보니 AOP는 Controller → Service → AOP 순으로 실행되기 때문에 저 시점에는 아직 응답이 읽히지 않을 때이기 때문이란 것을 알게 되었다.</p>
</li>
<li><p>따라서 응답값을 따로 빼주는 것이 아닌</p>
<pre><code class="language-java">package org.example.expert.domain.common.AOP;
</code></pre>
</li>
</ul>
<p>import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.ContentCachingRequestWrapper;</p>
<p>import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;</p>
<p>@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class LoggingAop {</p>
<pre><code>private final ObjectMapper objectMapper;</code></pre><p>//    .controller.<em>AdminController.</em></p>
<pre><code>//전 후로 로그를 찍어준다.
@Around(&quot;execution(* org.example.expert.domain.*.controller.*AdminController.*(..))&quot;)
public Object logApi(ProceedingJoinPoint joinPoint) throws Throwable {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

    ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;

    Long userId = (Long) request.getAttribute(&quot;userId&quot;);
    LocalDateTime now = LocalDateTime.now();
    String requestURI = request.getRequestURI();
    String resquestBody = new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
    log.info(&quot;userId = {},Time = {}, URL = {}, body = {}&quot;, userId, now, requestURI, resquestBody);

    Object result = joinPoint.proceed();

    log.info(&quot;body = {}&quot;, objectMapper.writeValueAsString(result));

    return result;
}</code></pre><p>}</p>
<pre><code>- 오브젝트매퍼를 활용해서 메서드의 결과값을 통해 응답값을 출력할 수 있도록 수정하니 잘 작동되는 것을 확인할 수 있었다.

- 팀원들에게 문제를 공유하고 같이 해결하니 금방할 수 있게 되었다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[6Lv 문제 해결 제출용 ]]></title>
            <link>https://velog.io/@d0ngx2_2/6Lv-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EC%A0%9C%EC%B6%9C%EC%9A%A9</link>
            <guid>https://velog.io/@d0ngx2_2/6Lv-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EC%A0%9C%EC%B6%9C%EC%9A%A9</guid>
            <pubDate>Fri, 05 Dec 2025 03:19:23 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-인식-및-정의">문제 인식 및 정의</h3>
<ul>
<li>Admin API의 응답값이 void라서 로깅 시 Request/Response Body가 모두 null로 찍히는 문제</li>
</ul>
<h4 id="admin-api-중-다음-두-개는-void를-응답으로-사용하고-있었음">Admin API 중 다음 두 개는 void를 응답으로 사용하고 있었음.</h4>
<ul>
<li><p>PATCH /admin/users/{id}</p>
</li>
<li><p>DELETE /admin/comments/{id}</p>
</li>
</ul>
<h4 id="aop에서-요청응답-바디-로깅을-실시하고-있었지만-void-응답인-경우-로그에-responsebody--null만-찍혀서-의미-없는-로그가-생성되게-되었다">AOP에서 “요청/응답 바디 로깅&quot;을 실시하고 있었지만 void 응답인 경우 로그에 “ResponseBody = null”만 찍혀서 의미 없는 로그가 생성되게 되었다.</h4>
<h3 id="해결-방안">해결 방안</h3>
<h4 id="의사결정-과정">의사결정 과정</h4>
<ul>
<li>void 자체가 잘못된 것은 아니지만 운영 환경 로깅을 생각하면 의미 있는 응답을 내려주는 것이 좋다고 판단</li>
<li>UserRole 변경 API는 변경 결과를 내려주는 것이 자연스러움</li>
<li>Comment 삭제도 삭제된 commentId 정도는 내려주면 더 명확해짐</li>
</ul>
<h4 id="해결-과정">해결 과정</h4>
<ul>
<li><p>요청/응답 DTO를 새로 정의</p>
</li>
<li><p>Admin API의 응답을 모두 DTO 기반으로 통일</p>
</li>
<li><p>로깅 AOP도 응답 바디가 null이 아닌 형태로 정상 출력되기 시작    </p>
</li>
</ul>
<h3 id="해결-완료">해결 완료</h3>
<h4 id="회고">회고</h4>
<ul>
<li><p>단순 기능 구현에 집중하다 보면 “운영 환경에서의 사용성”까지는 신경 못 쓰는 경우가 많은 것 같다.</p>
</li>
<li><p>명확한 응답을 내려주는 것은 개발자 경험에도 중요하고, AOP 로깅 퀄리티도 크게 향상시킨다.</p>
</li>
</ul>
<h4 id="전후-데이터-비교">전후 데이터 비교</h4>
<table>
<thead>
<tr>
<th>항목</th>
<th>before</th>
<th>after</th>
</tr>
</thead>
<tbody><tr>
<td>응답</td>
<td>void (null)</td>
<td>의미 있는 DTO</td>
</tr>
<tr>
<td>운영 로그 가독성</td>
<td>매우 낮음</td>
<td>명확한 상태 확인 가능</td>
</tr>
<tr>
<td>향후 확장성</td>
<td>낮음</td>
<td>응답 필드만 추가해 확장 가능</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot 개념 정리(Filter, Interceptor, AOP) + 과제]]></title>
            <link>https://velog.io/@d0ngx2_2/Spring-Boot-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACFilter-Interceptor-AOP-%EA%B3%BC%EC%A0%9C</link>
            <guid>https://velog.io/@d0ngx2_2/Spring-Boot-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%ACFilter-Interceptor-AOP-%EA%B3%BC%EC%A0%9C</guid>
            <pubDate>Thu, 04 Dec 2025 11:35:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h1 id="filter-interceptor-aop">Filter, Interceptor, AOP</h1>
</blockquote>
<h3 id="사용-이유">사용 이유</h3>
<ul>
<li>모든 요청에 대해 로그를 남기고 싶다</li>
<li>로그인된 사용자만 특정 요청을 허용하고 싶다</li>
<li>요청이 들어올 때마다 요청 시간을 측정하고 싶다</li>
<li>특정 서비스 로직 실행 전후로 공통적인 검증이나 처리를 하고 싶다</li>
</ul>
<p><strong>다만 이 로직들을 모든 코드에 작성하게 된다면 가독성도 떨어지고 보수하기 굉장히 불편해진다.</strong></p>
<hr>
<h3 id="특징">특징</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>Filter</th>
<th>Interceptor</th>
<th>AOP</th>
</tr>
</thead>
<tbody><tr>
<td><strong>처리 위치</strong></td>
<td>DispatcherServlet 이전</td>
<td>컨트롤러 실행 전후</td>
<td>Spring Bean 메서드 전후</td>
</tr>
<tr>
<td><strong>대상</strong></td>
<td>서블릿 요청(Request/Response)</td>
<td>핸들러(Controller)</td>
<td>모든 Bean 메서드</td>
</tr>
<tr>
<td><strong>사용 목적</strong></td>
<td>인증, 로깅, CORS 처리 등</td>
<td>세션 체크, 권한 검사</td>
<td>로깅, 트랜잭션, 예외 처리 등</td>
</tr>
<tr>
<td><strong>선언 방법</strong></td>
<td><code>Filter</code> 구현체</td>
<td><code>HandlerInterceptor</code> 구현체</td>
<td><code>@Aspect</code> 클래스</td>
</tr>
</tbody></table>
<hr>
<h3 id="그림으로-이해하기">그림으로 이해하기</h3>
<p><img src="https://velog.velcdn.com/images/d0ngx2_2/post/7a7778d0-0898-4e93-a3d2-b0fc79847a70/image.png" alt=""></p>
<pre><code>[사용자 요청]
   ↓
[Filter] ← 요청 시작 시 로그 출력
   ↓
[Interceptor] ← Controller 실행 전/후 로그 출력
   ↓
[AOP] ← Service 메서드 실행 전/후/예외 시점 로그 출력</code></pre><hr>
<blockquote>
<h2 id="filter--인증-토큰-처리-ex-jwt">Filter – 인증 토큰 처리 (ex. JWT)</h2>
</blockquote>
<ul>
<li>Authorization 헤더에 담긴 JWT 토큰 유효성 검사</li>
<li>모든 요청의 가장 앞단에서 작동해야한다, Spring DispatcherServlet 이전에 실행되어야 한다.</li>
<li>인증 실패 시 컨트롤러까지 가지 않도록 빠르게 차단 해야한다.</li>
</ul>
<hr>
<blockquote>
<h2 id="interceptor--로그인-여부-체크">Interceptor – 로그인 여부 체크</h2>
</blockquote>
<ul>
<li>로그인이 필요한 페이지 요청 시 세션 또는 인증 여부 검증</li>
<li>컨트롤러 실행 직전에 처리 가능하고, 사용자별 세션, 권한 확인 로직 작성에 용이하기 때문.</li>
<li>요청 URL 기반으로 유연하게 경로별 적용 가능하다.<ul>
<li>작성자 본인 확인
ex) 내가 작성한 글만 삭제 가능한 경우</li>
<li>비즈니스 조건<br>ex) 유료 회원만 접근</li>
</ul>
</li>
</ul>
<h3 id="handlerinterceptor">HandlerInterceptor</h3>
<ul>
<li><p>Filter와 마찬가지로 처음부터 다 구현하는 것이 아닌 이미 구현되어 있는 HandlerInterceptor 를 활용한다.</p>
</li>
<li><p>메서드</p>
<ul>
<li><p>preHandle : 컨트롤러 실행 전에 동작 (false 반환 시 흐름 중단 가능)</p>
</li>
<li><p>postHandle : 컨트롤러 실행 후, 뷰 렌더링 전</p>
</li>
<li><p>afterCompletion : 응답 완료 후, 예외 처리 포함</p>
</li>
</ul>
</li>
</ul>
<h3 id="구현-방법">구현 방법</h3>
<pre><code class="language-java">@Slf4j
@Component
@RequiredArgsConstructor
public class UserOwnerCheckInterceptor implements HandlerInterceptor {

    private final UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        // 1. 현재 로그인한 사용자 이름 꺼내기
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String currentUsername = auth.getName();

        // 2. 요청 URI에서 username 추출
        String path = request.getRequestURI();  // /api/user/{username}/email
        String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8);

        String[] parts = decodedPath.split(&quot;/&quot;);
        String username = parts[parts.length - 2];

        // 3. 게시글 작성자와 비교
        if(!currentUsername.equals(username)) {
            log.warn(&quot;작성자 아님. 접근 거부&quot;);
            response.sendError(HttpServletResponse.SC_FORBIDDEN, &quot;작성자만 수정할 수 있습니다.&quot;);
            return false;
        }

        return true;
    }
}</code></pre>
<ul>
<li><p>필요 어노테이션 </p>
<ul>
<li><p>Slf4j : 로깅을 사용할 시</p>
</li>
<li><p>Component : 빈 등록 필수</p>
</li>
<li><p>RequiredArgsConstructor : 속성의 생성자 생성을 위해 사용</p>
</li>
</ul>
</li>
<li><p>HandlerInterceptor을 해당 클래스를 구현한다.</p>
</li>
<li><p>preHandle 메서드의 매개변수는 HttpServlet 요청/응답, Object handler 이고 예외를던 져준다.</p>
</li>
<li><p>해당 리퀘스트를 통해 내부 사용자의 정보를 꺼내 원하는 조건문을 만들어 메서드를 완성 시킨다.</p>
</li>
<li><p>완성된 인터셉터를 WebConfig에 등록을 시켜준다.</p>
<pre><code class="language-java">@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

  private final UserOwnerCheckInterceptor userOwnerCheckInterceptor;

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(userOwnerCheckInterceptor)
          .addPathPatterns(&quot;/api/user/**/email&quot;);
  }
}</code></pre>
<h4 id="저번에-다뤘던-내용처럼-빈으로-등록만-해주는-것이-아닌-인터셉터를-추가를-해주어야만-인식을-하기-때문에-필수-과정이다">저번에 다뤘던 내용처럼 빈으로 등록만 해주는 것이 아닌 인터셉터를 추가를 해주어야만 인식을 하기 때문에 필수 과정이다.</h4>
</li>
</ul>
<hr>
<blockquote>
<h2 id="aop--실행-시간-측정--로깅">AOP – 실행 시간 측정 / 로깅</h2>
</blockquote>
<ul>
<li>Service 계층에서 비즈니스 로직 실행 시간 측정이 필요할 때.</li>
<li>특정 메서드 호출 전후에만 부가 기능을 넣고 싶을 때 유리</li>
<li>핵심 로직은 건드리지 않고 공통 처리만 분리 가능</li>
<li>다양한 메서드에 어노테이션으로 간편하게 확장 가능</li>
</ul>
<h3 id="그림으로-이해하기-1">그림으로 이해하기</h3>
<p><img src="https://velog.velcdn.com/images/d0ngx2_2/post/ffd7925d-c970-49c5-bb66-8809e14f1808/image.png" alt=""></p>
<ul>
<li>다음과 같이 도메인별 서비스 단에 전부 로깅을 추가하기에는 유지보수하기 매우 부적절 하므로 AOP를 통해 일괄적용함에 따라 유지보수성이 굉장히 좋아짐을 확인할 수 있다.</li>
</ul>
<h3 id="용어-정리">용어 정리</h3>
<ul>
<li>Aspect : 공통 관심사를 담는 모듈 (ex. 로깅, 트랜잭션, 권한 체크)</li>
<li>JoinPoint : 메서드 실행 지점</li>
<li>Advice : 실행 시점에 따라 작동하는 AOP 로직 (@Before, @After, @Around)</li>
<li>Pointcut : 어떤 JoinPoint에 Advice를 적용할지 정의 (표현식 기반)</li>
<li>Weaving : Advice를 적용하는 과정 (직접 Weaving을 다룰 일은 없다. 스프링이 해줌) </li>
</ul>
<h4 id="작성-꿀팁">작성 꿀팁</h4>
<ul>
<li>어떤 것을<ul>
<li>PointCut 을 통해서 대상을 지정해준다.</li>
</ul>
</li>
<li>언제 <ul>
<li>Advice를 통해서 실행되는 시점을 지정해준다.</li>
</ul>
</li>
<li>어떻게<ul>
<li>Aspect에 정의한 메서드를 통해서 어떻게 할 것인지 지정해준다.</li>
</ul>
</li>
</ul>
<h3 id="주-어노테이션">주 어노테이션</h3>
<ul>
<li>@Aspect : Aspect 클래스임을 선언</li>
<li>@Around : 메서드 실행 전후 제어 (가장 유연)</li>
<li>@Before : 메서드 실행 전에 수행</li>
<li>@AfterReturning : 정상 반환 후 수행</li>
<li>@AfterThrowing : 예외 발생 시 수행</li>
<li>@After : 정상/예외 관계없이 실행 후 수행</li>
</ul>
<h3 id="활용법">활용법</h3>
<ul>
<li>로깅 : 모든 서비스 실행 시 로그 기록</li>
<li>트랜잭션 : 메서드 단위 트랜잭션 처리</li>
<li>권한 검사 : 관리자 권한 체크</li>
<li>예외 처리 : 공통 에러 메시지 처리</li>
<li>실행 시간 측정 : 성능 모니터링</li>
</ul>
<h3 id="구현-방법-1">구현 방법</h3>
<pre><code class="language-java">package org.example.nbcam_addvanced_1.common.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
public class TimeCheckAop {

    @Around(&quot;execution(* org.example.nbcam_addvanced_1.user.service..*(..))&quot;)
    public Object executionTime(ProceedingJoinPoint joinPoint) throws Throwable {

        long start = System.currentTimeMillis();

        Object result = joinPoint.proceed(); // 실제 메서드 실행 -&gt; Filter에서 doFilter 와 비슷함.

        long end = System.currentTimeMillis();
        log.info(&quot;[AOP] {} 실행됨 in {}ms&quot; , joinPoint.getSignature() , end - start);

        return result;
    }
}</code></pre>
<ul>
<li><p>@Aspect : AOP를 사용할 클래스다! 지정</p>
</li>
<li><p>@Component : 마찬가지로 빈 등록 필수</p>
</li>
<li><p>Slf4j : 로깅을 사용할 경우 필수</p>
</li>
<li><p>@Around(&quot;execution(* org.example.nbcam_addvanced_1.user.service..*(..))&quot;)</p>
<ul>
<li>어노테이션(&quot;&quot;) 안에는 내가 적용시킬 메서드들이 위치한 파일의 경로를 입력해주면 된다.</li>
<li><code>*</code> = 와일드카드 모든~ 이라는 뜻</li>
</ul>
</li>
<li><p>JoinPoint를 매개변수로 받아준다.</p>
</li>
<li><p>System.currentTimeMillis(); = 현재 시간을 가져오는 기능</p>
</li>
</ul>
<hr>
<h3 id="마무리">마무리</h3>
<blockquote>
<h4 id="과제를-진행하게-되면서-테스트코드라는-것을-올바르게-작동하도록-수정하는-작업을-하였는데-틀린-것을-찾고-수정하는-것은-매우-쉬웠지만-저-코드를-테스트할-코드를-작성하는-것이-굉장히-어려울-것이라고-생각이-들어-좀처럼-손이-가지-않았다-튜터님께서-말하신-만큼-중요한-요소라-꼭-도전은-해야겠다만-지금은-조금-버거워-후에-미뤄볼까-한다-절대-귀찮아서가-아니다ㅎㅎ">과제를 진행하게 되면서 테스트코드라는 것을 올바르게 작동하도록 수정하는 작업을 하였는데, 틀린 것을 찾고 수정하는 것은 매우 쉬웠지만, 저 코드를 테스트할 코드를 작성하는 것이 굉장히 어려울 것이라고 생각이 들어 좀처럼 손이 가지 않았다.. 튜터님께서 말하신 만큼 중요한 요소라 꼭 도전은 해야겠다만, 지금은 조금 버거워 후에 미뤄볼까 한다.. 절대 귀찮아서가 아니다ㅎㅎ..</h4>
</blockquote>
<h4 id="또한-배운것을-토대로-인터셉터와-aop를-구현하는데-손쉽게-따라할-수-있었지만-데이터를-뽑아와-새로운-로직을-생성하는-부분이-재밌으면서도-골이-조금-아팠다-더-열심히-해야겠당">또한 배운것을 토대로 인터셉터와 AOP를 구현하는데 손쉽게 따라할 수 있었지만 데이터를 뽑아와 새로운 로직을 생성하는 부분이 재밌으면서도 골이 조금 아팠다. 더 열심히 해야겠당</h4>
]]></description>
        </item>
    </channel>
</rss>