<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Spring on kastori</title><link>http://blog.kastori.dev/tags/spring/</link><description>Recent content in Spring on kastori</description><generator>Hugo -- gohugo.io</generator><language>ko-kr</language><lastBuildDate>Tue, 19 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="http://blog.kastori.dev/tags/spring/index.xml" rel="self" type="application/rss+xml"/><item><title>[Spring 완전 정복 #1] Spring을 제대로 이해하려면 Servlet부터 — HTTP 요청이 Controller에 닿기까지</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-01-servlet-fundamentals/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-01-servlet-fundamentals/</guid><description>&lt;h2 id="spring을-써도-servlet을-알아야-하는-이유"&gt;&lt;a href="#spring%ec%9d%84-%ec%8d%a8%eb%8f%84-servlet%ec%9d%84-%ec%95%8c%ec%95%84%ec%95%bc-%ed%95%98%eb%8a%94-%ec%9d%b4%ec%9c%a0" class="header-anchor"&gt;&lt;/a&gt;Spring을 써도 Servlet을 알아야 하는 이유
&lt;/h2&gt;&lt;p&gt;&amp;ldquo;Filter와 Interceptor의 차이가 뭔가요?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;면접에서 자주 나오는 질문이다. 많은 개발자들이 &amp;ldquo;Filter는 Servlet 앞, Interceptor는 Controller 앞&amp;quot;이라고 외워서 답한다. 맞는 말이지만, &lt;em&gt;왜&lt;/em&gt; 그런지를 이해하고 있는 사람은 적다.&lt;/p&gt;
&lt;p&gt;그 이유를 이해하려면 HTTP 요청이 Spring Controller에 닿기까지 거치는 전체 레이어를 알아야 한다. 그리고 그 출발점은 &lt;strong&gt;Servlet&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="cgi에서-servlet으로--왜-바뀌었나"&gt;&lt;a href="#cgi%ec%97%90%ec%84%9c-servlet%ec%9c%bc%eb%a1%9c--%ec%99%9c-%eb%b0%94%eb%80%8c%ec%97%88%eb%82%98" class="header-anchor"&gt;&lt;/a&gt;CGI에서 Servlet으로 — 왜 바뀌었나
&lt;/h2&gt;&lt;p&gt;초창기 웹 서버는 정적 파일만 제공했다. 동적 응답(사용자별 맞춤 페이지, DB 조회 결과 등)이 필요해지자 **CGI(Common Gateway Interface)**가 등장했다.&lt;/p&gt;
&lt;p&gt;CGI의 방식은 단순하다: HTTP 요청이 들어올 때마다 새 프로세스를 생성해서 처리하고, 끝나면 종료한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;요청 1 → 프로세스 생성 → 처리 → 프로세스 종료
요청 2 → 프로세스 생성 → 처리 → 프로세스 종료
요청 N → ...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;문제는 프로세스 생성 비용이다. 동시 접속자가 늘어나면 서버가 버티지 못한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java Servlet은 이 문제를 스레드로 해결했다.&lt;/strong&gt; Servlet 인스턴스는 JVM에 딱 1개만 상주하고, 요청마다 스레드를 하나씩 할당해 처리한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Servlet 인스턴스 (1개, JVM에 상주)
 ├─ 요청 1 → 스레드 1
 ├─ 요청 2 → 스레드 2
 └─ 요청 N → 스레드 N
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;인스턴스를 매번 새로 만들지 않으니 메모리와 시간 비용이 크게 줄었다. 이 설계가 오늘날 Spring 애플리케이션의 근간이 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="servlet-생명주기--인스턴스는-한-번만-만들어진다"&gt;&lt;a href="#servlet-%ec%83%9d%eb%aa%85%ec%a3%bc%ea%b8%b0--%ec%9d%b8%ec%8a%a4%ed%84%b4%ec%8a%a4%eb%8a%94-%ed%95%9c-%eb%b2%88%eb%a7%8c-%eb%a7%8c%eb%93%a4%ec%96%b4%ec%a7%84%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;Servlet 생명주기 — 인스턴스는 한 번만 만들어진다
&lt;/h2&gt;&lt;p&gt;Servlet 인스턴스는 **Servlet Container(Tomcat)**가 관리한다. 생명주기는 세 단계다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;최초 요청 또는 서버 시작
 → init() : 인스턴스 생성 + 초기화 (딱 1회)
 → service() : 요청마다 호출 (doGet / doPost 등으로 분기)
 → destroy() : 컨테이너 종료 시 (딱 1회)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;코드로 보면 이렇다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MyServlet&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;extends&lt;/span&gt; HttpServlet {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;init&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// DB 커넥션 풀 초기화 등 1회성 작업&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;doGet&lt;/span&gt;(HttpServletRequest req, HttpServletResponse res)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; IOException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res.&lt;span style="color:#a6e22e"&gt;getWriter&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;write&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Hello&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;destroy&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 리소스 해제&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;직접 이런 코드를 쓸 일은 거의 없다. 하지만 Spring의 &lt;code&gt;DispatcherServlet&lt;/code&gt;이 내부적으로 이 구조를 그대로 따른다는 점이 핵심이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="tomcat의-역할--spring-boot가-jar-하나로-뜨는-이유"&gt;&lt;a href="#tomcat%ec%9d%98-%ec%97%ad%ed%95%a0--spring-boot%ea%b0%80-jar-%ed%95%98%eb%82%98%eb%a1%9c-%eb%9c%a8%eb%8a%94-%ec%9d%b4%ec%9c%a0" class="header-anchor"&gt;&lt;/a&gt;Tomcat의 역할 — Spring Boot가 jar 하나로 뜨는 이유
&lt;/h2&gt;&lt;p&gt;Servlet Container인 Tomcat은 단순히 Servlet을 실행하는 것 이상의 역할을 한다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;역할&lt;/th&gt;
 &lt;th&gt;설명&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Servlet 인스턴스 관리&lt;/td&gt;
 &lt;td&gt;생성, 초기화, 소멸&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;스레드 풀 관리&lt;/td&gt;
 &lt;td&gt;요청마다 스레드 할당&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;HTTP 파싱&lt;/td&gt;
 &lt;td&gt;원시 HTTP 바이트 스트림 → &lt;code&gt;HttpServletRequest&lt;/code&gt; 객체 변환&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;URL 매핑&lt;/td&gt;
 &lt;td&gt;어떤 URL을 어떤 Servlet이 처리할지 결정&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Filter Chain 실행&lt;/td&gt;
 &lt;td&gt;Servlet 앞뒤로 Filter 실행&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Spring Boot가 별도의 WAS 없이 &lt;code&gt;java -jar&lt;/code&gt; 한 줄로 서버를 시작할 수 있는 이유는 **Tomcat을 내장(embedded)**하기 때문이다. 애플리케이션 안에 Tomcat이 들어있으니 따로 설치할 필요가 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="filter--spring보다-바깥-레이어"&gt;&lt;a href="#filter--spring%eb%b3%b4%eb%8b%a4-%eb%b0%94%ea%b9%a5-%eb%a0%88%ec%9d%b4%ec%96%b4" class="header-anchor"&gt;&lt;/a&gt;Filter — Spring보다 바깥 레이어
&lt;/h2&gt;&lt;p&gt;Filter는 Servlet Container 레벨에서 동작한다. Spring Context가 시작되기 전, 즉 DispatcherServlet보다 앞에 위치한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;HTTP 요청 → [Filter1 → Filter2 → Filter3] → Servlet(DispatcherServlet)
HTTP 응답 ← [Filter1 ← Filter2 ← Filter3] ← Servlet
&lt;/code&gt;&lt;/pre&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LoggingFilter&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;implements&lt;/span&gt; Filter {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;doFilter&lt;/span&gt;(ServletRequest request, ServletResponse response,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; FilterChain chain) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; IOException, ServletException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; System.&lt;span style="color:#a6e22e"&gt;out&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;println&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;요청 진입: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; ((HttpServletRequest) request).&lt;span style="color:#a6e22e"&gt;getRequestURI&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; chain.&lt;span style="color:#a6e22e"&gt;doFilter&lt;/span&gt;(request, response); &lt;span style="color:#75715e"&gt;// 다음 Filter 또는 Servlet으로 넘김&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; System.&lt;span style="color:#a6e22e"&gt;out&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;println&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;응답 완료&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Filter가 적합한 작업:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인코딩 설정&lt;/strong&gt;: 모든 요청에 &lt;code&gt;UTF-8&lt;/code&gt; 적용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CORS 처리&lt;/strong&gt;: 응답 헤더 추가&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인증 토큰 1차 검사&lt;/strong&gt;: Authorization 헤더 존재 여부 확인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;요청/응답 로깅&lt;/strong&gt;: URL, 처리 시간 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;한 가지 중요한 제약이 있다. Filter는 Spring Context 밖에 있기 때문에 &lt;strong&gt;Spring의 예외 처리(&lt;code&gt;@ControllerAdvice&lt;/code&gt;)가 적용되지 않는다.&lt;/strong&gt; Filter에서 예외가 발생하면 직접 처리해야 한다.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Spring Security의 필터체인도 Servlet Filter다. 이 때문에 Security 설정이 Spring MVC(Controller, Interceptor 등)보다 먼저 동작한다.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="dispatcherservlet--spring-mvc의-시작점"&gt;&lt;a href="#dispatcherservlet--spring-mvc%ec%9d%98-%ec%8b%9c%ec%9e%91%ec%a0%90" class="header-anchor"&gt;&lt;/a&gt;DispatcherServlet — Spring MVC의 시작점
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;DispatcherServlet&lt;/code&gt;은 Spring MVC의 핵심이지만, 결국 &lt;code&gt;HttpServlet&lt;/code&gt;을 상속한 &lt;strong&gt;Servlet&lt;/strong&gt;이다. Tomcat이 관리하는 수많은 Servlet 인스턴스 중 하나일 뿐이다.&lt;/p&gt;
&lt;p&gt;Spring Boot는 &lt;code&gt;DispatcherServlet&lt;/code&gt;을 자동 등록하고, 모든 URL(&lt;code&gt;/&lt;/code&gt;)을 이 Servlet이 받도록 설정한다. 이후 요청을 어떤 Controller로 보낼지는 &lt;code&gt;DispatcherServlet&lt;/code&gt; 내부의 &lt;code&gt;HandlerMapping&lt;/code&gt;이 결정한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="전체-요청-흐름--한눈에-보기"&gt;&lt;a href="#%ec%a0%84%ec%b2%b4-%ec%9a%94%ec%b2%ad-%ed%9d%90%eb%a6%84--%ed%95%9c%eb%88%88%ec%97%90-%eb%b3%b4%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;전체 요청 흐름 — 한눈에 보기
&lt;/h2&gt;&lt;p&gt;HTTP 요청이 Controller에 닿기까지 거치는 전체 레이어다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;HTTP 요청
 → Tomcat (HTTP 파싱, 스레드 할당)
 → Filter Chain (Servlet Container 레벨)
 → DispatcherServlet (Spring MVC 진입)
 → Interceptor (preHandle)
 → AOP (Before Advice)
 → Controller 메서드 실행
 → AOP (After Advice)
 → Interceptor (postHandle)
 → DispatcherServlet (ViewResolver 등)
 → Filter Chain (역순)
 → HTTP 응답
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 흐름이 Filter / Interceptor / AOP의 차이를 결정한다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;구분&lt;/th&gt;
 &lt;th&gt;위치&lt;/th&gt;
 &lt;th&gt;Spring Bean 접근&lt;/th&gt;
 &lt;th&gt;Spring 예외 처리&lt;/th&gt;
 &lt;th&gt;주요 용도&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Filter&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Servlet Container&lt;/td&gt;
 &lt;td&gt;제한적&lt;/td&gt;
 &lt;td&gt;X&lt;/td&gt;
 &lt;td&gt;인코딩, CORS, 인증 토큰&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Interceptor&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Spring MVC&lt;/td&gt;
 &lt;td&gt;O&lt;/td&gt;
 &lt;td&gt;O&lt;/td&gt;
 &lt;td&gt;로그인 체크, 권한 검사&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;AOP&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Spring Bean&lt;/td&gt;
 &lt;td&gt;O&lt;/td&gt;
 &lt;td&gt;O&lt;/td&gt;
 &lt;td&gt;트랜잭션, 로깅, 성능 측정&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;Servlet은 오래된 기술이지만, Spring MVC의 모든 레이어(Filter, DispatcherServlet, Interceptor, AOP)가 이 위에 쌓여있다. 이 구조를 이해하면 &amp;ldquo;Filter와 Interceptor 중 무엇을 써야 하나?&amp;ldquo;라는 질문에 외운 답이 아닌 구조적 이해로 답할 수 있다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring Core — IoC 컨테이너와 의존성 주입이 어떻게 동작하는지 정리한다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #10] Spring 예외 처리 — @ControllerAdvice와 Custom Exception 설계</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-10-exception-handling/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-10-exception-handling/</guid><description>&lt;h2 id="예외-처리가-흩어져-있으면-생기는-문제"&gt;&lt;a href="#%ec%98%88%ec%99%b8-%ec%b2%98%eb%a6%ac%ea%b0%80-%ed%9d%a9%ec%96%b4%ec%a0%b8-%ec%9e%88%ec%9c%bc%eb%a9%b4-%ec%83%9d%ea%b8%b0%eb%8a%94-%eb%ac%b8%ec%a0%9c" class="header-anchor"&gt;&lt;/a&gt;예외 처리가 흩어져 있으면 생기는 문제
&lt;/h2&gt;&lt;p&gt;Spring 애플리케이션에서 예외 처리를 각 Controller마다 개별로 하면 두 가지 문제가 생긴다. 에러 응답 형식이 제각각이 되고, 같은 예외를 여러 곳에서 중복 처리하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@ControllerAdvice&lt;/code&gt;는 이 문제를 해결한다. &lt;strong&gt;전역 예외 핸들러&lt;/strong&gt;를 하나 만들어 모든 Controller에서 발생하는 예외를 한 곳에서 처리한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="controlleradvice-기본-구조"&gt;&lt;a href="#controlleradvice-%ea%b8%b0%eb%b3%b8-%ea%b5%ac%ec%a1%b0" class="header-anchor"&gt;&lt;/a&gt;@ControllerAdvice 기본 구조
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RestControllerAdvice&lt;/span&gt; &lt;span style="color:#75715e"&gt;// @ControllerAdvice + @ResponseBody&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;GlobalExceptionHandler&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@ExceptionHandler&lt;/span&gt;(UserNotFoundException.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ResponseEntity&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;ErrorResponse&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;handleUserNotFound&lt;/span&gt;(UserNotFoundException e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; ResponseEntity
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(HttpStatus.&lt;span style="color:#a6e22e"&gt;NOT_FOUND&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;(ErrorResponse.&lt;span style="color:#a6e22e"&gt;of&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;USER_NOT_FOUND&amp;#34;&lt;/span&gt;, e.&lt;span style="color:#a6e22e"&gt;getMessage&lt;/span&gt;()));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@ExceptionHandler&lt;/span&gt;(MethodArgumentNotValidException.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ResponseEntity&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;ErrorResponse&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;handleValidation&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; MethodArgumentNotValidException e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;String&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; errors &lt;span style="color:#f92672"&gt;=&lt;/span&gt; e.&lt;span style="color:#a6e22e"&gt;getBindingResult&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;getFieldErrors&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;stream&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;map&lt;/span&gt;(fe &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; fe.&lt;span style="color:#a6e22e"&gt;getField&lt;/span&gt;() &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; fe.&lt;span style="color:#a6e22e"&gt;getDefaultMessage&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;toList&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; ResponseEntity.&lt;span style="color:#a6e22e"&gt;badRequest&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;(ErrorResponse.&lt;span style="color:#a6e22e"&gt;of&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;VALIDATION_FAILED&amp;#34;&lt;/span&gt;, errors.&lt;span style="color:#a6e22e"&gt;toString&lt;/span&gt;()));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@ExceptionHandler&lt;/span&gt;(Exception.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ResponseEntity&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;ErrorResponse&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;handleException&lt;/span&gt;(Exception e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Unhandled exception&amp;#34;&lt;/span&gt;, e);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; ResponseEntity.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(HttpStatus.&lt;span style="color:#a6e22e"&gt;INTERNAL_SERVER_ERROR&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;(ErrorResponse.&lt;span style="color:#a6e22e"&gt;of&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;INTERNAL_ERROR&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;서버 오류가 발생했습니다&amp;#34;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;예외 처리 계층은 다음과 같다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Controller 예외 발생
 → HandlerExceptionResolver (Spring MVC 내부)
 → @ControllerAdvice
 → 일관된 에러 응답 반환
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Filter에서 발생한 예외는 &lt;code&gt;DispatcherServlet&lt;/code&gt; 밖이라 &lt;code&gt;@ControllerAdvice&lt;/code&gt;가 잡지 못한다. Filter 내부에서 직접 응답을 작성해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="에러-응답-dto"&gt;&lt;a href="#%ec%97%90%eb%9f%ac-%ec%9d%91%eb%8b%b5-dto" class="header-anchor"&gt;&lt;/a&gt;에러 응답 DTO
&lt;/h2&gt;&lt;p&gt;클라이언트가 에러 상황을 파악할 수 있도록 일관된 형식으로 응답한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Getter&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ErrorResponse&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; String code;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; String message;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; LocalDateTime timestamp;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ErrorResponse&lt;/span&gt;(String code, String message) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;code&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; code;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; message;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;timestamp&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; LocalDateTime.&lt;span style="color:#a6e22e"&gt;now&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; ErrorResponse &lt;span style="color:#a6e22e"&gt;of&lt;/span&gt;(String code, String message) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; ErrorResponse(code, message);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;code&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;USER_NOT_FOUND&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;message&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;사용자를 찾을 수 없습니다: id=123&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;timestamp&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;2026-05-19T10:30:00&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;code&lt;/code&gt;는 클라이언트가 에러 종류를 분기 처리하기 위한 값이고, &lt;code&gt;message&lt;/code&gt;는 사람이 읽는 설명이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="custom-exception-설계--errorcode-enum-패턴"&gt;&lt;a href="#custom-exception-%ec%84%a4%ea%b3%84--errorcode-enum-%ed%8c%a8%ed%84%b4" class="header-anchor"&gt;&lt;/a&gt;Custom Exception 설계 — ErrorCode Enum 패턴
&lt;/h2&gt;&lt;p&gt;에러 코드가 늘어날수록 관리가 중요해진다. &lt;strong&gt;ErrorCode Enum + BusinessException 기반 클래스&lt;/strong&gt; 패턴이 실무에서 가장 많이 쓰인다.&lt;/p&gt;
&lt;h3 id="1-errorcode-enum"&gt;&lt;a href="#1-errorcode-enum" class="header-anchor"&gt;&lt;/a&gt;1. ErrorCode Enum
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Getter&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;enum&lt;/span&gt; ErrorCode {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 공통&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; INVALID_INPUT(400, &lt;span style="color:#e6db74"&gt;&amp;#34;INVALID_INPUT&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;잘못된 입력입니다&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; UNAUTHORIZED(401, &lt;span style="color:#e6db74"&gt;&amp;#34;UNAUTHORIZED&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;인증이 필요합니다&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; FORBIDDEN(403, &lt;span style="color:#e6db74"&gt;&amp;#34;FORBIDDEN&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;권한이 없습니다&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 사용자&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; USER_NOT_FOUND(404, &lt;span style="color:#e6db74"&gt;&amp;#34;USER_NOT_FOUND&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;사용자를 찾을 수 없습니다&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DUPLICATE_EMAIL(409, &lt;span style="color:#e6db74"&gt;&amp;#34;DUPLICATE_EMAIL&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;이미 사용 중인 이메일입니다&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 주문&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ORDER_NOT_FOUND(404, &lt;span style="color:#e6db74"&gt;&amp;#34;ORDER_NOT_FOUND&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;주문을 찾을 수 없습니다&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; INSUFFICIENT_STOCK(400, &lt;span style="color:#e6db74"&gt;&amp;#34;INSUFFICIENT_STOCK&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;재고가 부족합니다&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 서버&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; INTERNAL_ERROR(500, &lt;span style="color:#e6db74"&gt;&amp;#34;INTERNAL_ERROR&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;서버 오류가 발생했습니다&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; status;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; String code;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; String message;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-businessexception-기반-클래스"&gt;&lt;a href="#2-businessexception-%ea%b8%b0%eb%b0%98-%ed%81%b4%eb%9e%98%ec%8a%a4" class="header-anchor"&gt;&lt;/a&gt;2. BusinessException 기반 클래스
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BusinessException&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;extends&lt;/span&gt; RuntimeException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; ErrorCode errorCode;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BusinessException&lt;/span&gt;(ErrorCode errorCode) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;super&lt;/span&gt;(errorCode.&lt;span style="color:#a6e22e"&gt;getMessage&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;errorCode&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; errorCode;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BusinessException&lt;/span&gt;(ErrorCode errorCode, String detail) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;super&lt;/span&gt;(errorCode.&lt;span style="color:#a6e22e"&gt;getMessage&lt;/span&gt;() &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; detail);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;errorCode&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; errorCode;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getStatus&lt;/span&gt;() { &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; errorCode.&lt;span style="color:#a6e22e"&gt;getStatus&lt;/span&gt;(); }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; String &lt;span style="color:#a6e22e"&gt;getCode&lt;/span&gt;() { &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; errorCode.&lt;span style="color:#a6e22e"&gt;getCode&lt;/span&gt;(); }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-도메인별-구체-예외-클래스"&gt;&lt;a href="#3-%eb%8f%84%eb%a9%94%ec%9d%b8%eb%b3%84-%ea%b5%ac%ec%b2%b4-%ec%98%88%ec%99%b8-%ed%81%b4%eb%9e%98%ec%8a%a4" class="header-anchor"&gt;&lt;/a&gt;3. 도메인별 구체 예외 클래스
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserNotFoundException&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;extends&lt;/span&gt; BusinessException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserNotFoundException&lt;/span&gt;(Long userId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;super&lt;/span&gt;(ErrorCode.&lt;span style="color:#a6e22e"&gt;USER_NOT_FOUND&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;id=&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; userId);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;InsufficientStockException&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;extends&lt;/span&gt; BusinessException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;InsufficientStockException&lt;/span&gt;(Long productId, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; requested, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; available) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;super&lt;/span&gt;(ErrorCode.&lt;span style="color:#a6e22e"&gt;INSUFFICIENT_STOCK&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String.&lt;span style="color:#a6e22e"&gt;format&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;productId=%d, 요청=%d, 재고=%d&amp;#34;&lt;/span&gt;, productId, requested, available));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="4-서비스에서-사용"&gt;&lt;a href="#4-%ec%84%9c%eb%b9%84%ec%8a%a4%ec%97%90%ec%84%9c-%ec%82%ac%ec%9a%a9" class="header-anchor"&gt;&lt;/a&gt;4. 서비스에서 사용
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; User &lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(Long id) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; userRepository.&lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(id)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;orElseThrow&lt;/span&gt;(() &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; UserNotFoundException(id));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="5-핸들러에서-통합-처리"&gt;&lt;a href="#5-%ed%95%b8%eb%93%a4%eb%9f%ac%ec%97%90%ec%84%9c-%ed%86%b5%ed%95%a9-%ec%b2%98%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;5. 핸들러에서 통합 처리
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RestControllerAdvice&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;GlobalExceptionHandler&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// BusinessException 계층 전체를 한 곳에서 처리&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@ExceptionHandler&lt;/span&gt;(BusinessException.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ResponseEntity&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;ErrorResponse&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;handleBusinessException&lt;/span&gt;(BusinessException e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; ResponseEntity
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(e.&lt;span style="color:#a6e22e"&gt;getStatus&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;(ErrorResponse.&lt;span style="color:#a6e22e"&gt;of&lt;/span&gt;(e.&lt;span style="color:#a6e22e"&gt;getCode&lt;/span&gt;(), e.&lt;span style="color:#a6e22e"&gt;getMessage&lt;/span&gt;()));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;새로운 예외가 생겨도 &lt;code&gt;ErrorCode&lt;/code&gt;에 추가하고 &lt;code&gt;BusinessException&lt;/code&gt;을 상속하면 핸들러 수정 없이 자동으로 처리된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="http-상태-코드--자주-쓰는-것"&gt;&lt;a href="#http-%ec%83%81%ed%83%9c-%ec%bd%94%eb%93%9c--%ec%9e%90%ec%a3%bc-%ec%93%b0%eb%8a%94-%ea%b2%83" class="header-anchor"&gt;&lt;/a&gt;HTTP 상태 코드 — 자주 쓰는 것
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;코드&lt;/th&gt;
 &lt;th&gt;의미&lt;/th&gt;
 &lt;th&gt;사용 상황&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;200 OK&lt;/td&gt;
 &lt;td&gt;성공&lt;/td&gt;
 &lt;td&gt;조회, 수정 성공&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;201 Created&lt;/td&gt;
 &lt;td&gt;생성 성공&lt;/td&gt;
 &lt;td&gt;POST로 리소스 생성&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;204 No Content&lt;/td&gt;
 &lt;td&gt;성공, 바디 없음&lt;/td&gt;
 &lt;td&gt;DELETE 성공&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;400 Bad Request&lt;/td&gt;
 &lt;td&gt;잘못된 요청&lt;/td&gt;
 &lt;td&gt;Validation 실패&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;401 Unauthorized&lt;/td&gt;
 &lt;td&gt;미인증&lt;/td&gt;
 &lt;td&gt;로그인 필요&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;403 Forbidden&lt;/td&gt;
 &lt;td&gt;권한 없음&lt;/td&gt;
 &lt;td&gt;인증됐지만 권한 부족&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;404 Not Found&lt;/td&gt;
 &lt;td&gt;리소스 없음&lt;/td&gt;
 &lt;td&gt;존재하지 않는 ID&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;409 Conflict&lt;/td&gt;
 &lt;td&gt;충돌&lt;/td&gt;
 &lt;td&gt;중복 이메일&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;500 Internal Error&lt;/td&gt;
 &lt;td&gt;서버 오류&lt;/td&gt;
 &lt;td&gt;예상치 못한 예외&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;401과 403의 차이를 명확히 해두자. 401은 &amp;ldquo;누구인지 모른다 → 로그인 필요&amp;rdquo;, 403은 &amp;ldquo;누구인지 알지만 권한이 없다&amp;quot;는 뜻이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;Spring 예외 처리의 핵심은 두 가지다. &lt;code&gt;@ControllerAdvice&lt;/code&gt;로 전역 핸들러를 만들어 에러 응답 형식을 통일하고, &lt;code&gt;ErrorCode Enum + BusinessException&lt;/code&gt; 패턴으로 예외를 체계적으로 관리한다. 이 구조를 갖추면 새 예외 추가 시 &lt;code&gt;ErrorCode&lt;/code&gt;에 한 줄 추가하는 것으로 끝난다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring 테스트 — 단위/슬라이스/통합 테스트 전략을 정리한다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #11] Spring 테스트 전략 — 단위·슬라이스·통합 테스트를 언제 어떻게 쓸까</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-11-testing/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-11-testing/</guid><description>&lt;h2 id="테스트를-왜-계층으로-나누는가"&gt;&lt;a href="#%ed%85%8c%ec%8a%a4%ed%8a%b8%eb%a5%bc-%ec%99%9c-%ea%b3%84%ec%b8%b5%ec%9c%bc%eb%a1%9c-%eb%82%98%eb%88%84%eb%8a%94%ea%b0%80" class="header-anchor"&gt;&lt;/a&gt;테스트를 왜 계층으로 나누는가
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;@SpringBootTest&lt;/code&gt;를 쓰면 전체 Spring Context를 로드하므로 모든 것을 테스트할 수 있다. 그런데 왜 단위 테스트, 슬라이스 테스트를 따로 쓸까?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;속도&lt;/strong&gt; 때문이다. &lt;code&gt;@SpringBootTest&lt;/code&gt;는 전체 Context를 띄우므로 느리다. 단순 비즈니스 로직 검증에 매번 10초씩 기다리는 것은 비효율적이다. 테스트 종류를 목적에 맞게 선택하면 빠르고 명확한 테스트를 작성할 수 있다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;단위 테스트 : 빠름 — 비즈니스 로직
@WebMvcTest : 빠름 — API 계약, Validation, HTTP 상태코드
@DataJpaTest : 빠름 — 쿼리 메서드, 커스텀 쿼리
@SpringBootTest : 느림 — 전체 플로우 통합 검증
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2 id="단위-테스트--junit5--mockito"&gt;&lt;a href="#%eb%8b%a8%ec%9c%84-%ed%85%8c%ec%8a%a4%ed%8a%b8--junit5--mockito" class="header-anchor"&gt;&lt;/a&gt;단위 테스트 — JUnit5 + Mockito
&lt;/h2&gt;&lt;p&gt;Spring Context 없이 순수 Java로 테스트한다. 가장 빠르고 의존성이 없다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@ExtendWith&lt;/span&gt;(MockitoExtension.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderServiceTest&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Mock&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; OrderRepository orderRepository;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Mock&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; PaymentService paymentService;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@InjectMocks&lt;/span&gt; &lt;span style="color:#75715e"&gt;// @Mock 객체들을 OrderService에 자동 주입&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; OrderService orderService;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;주문_생성_성공&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// given&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; OrderRequest request &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; OrderRequest(1L, 2, 10000);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Order mockOrder &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Order.&lt;span style="color:#a6e22e"&gt;builder&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;(1L).&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(OrderStatus.&lt;span style="color:#a6e22e"&gt;PENDING&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; given(orderRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(any(Order.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;))).&lt;span style="color:#a6e22e"&gt;willReturn&lt;/span&gt;(mockOrder);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// when&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; OrderDto result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; orderService.&lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;(request);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assertThat(result.&lt;span style="color:#a6e22e"&gt;getId&lt;/span&gt;()).&lt;span style="color:#a6e22e"&gt;isEqualTo&lt;/span&gt;(1L);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; verify(orderRepository, times(1)).&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(any(Order.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;재고_부족_시_예외_발생&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; given(productRepository.&lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(anyLong()))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;willReturn&lt;/span&gt;(Optional.&lt;span style="color:#a6e22e"&gt;of&lt;/span&gt;(Product.&lt;span style="color:#a6e22e"&gt;builder&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;stock&lt;/span&gt;(0).&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;()));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assertThatThrownBy(() &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; orderService.&lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;(request))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;isInstanceOf&lt;/span&gt;(InsufficientStockException.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;ArgumentCaptor&lt;/strong&gt;로 메서드에 전달된 인자를 검증할 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ArgumentCaptor&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; captor &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style="color:#a6e22e"&gt;forClass&lt;/span&gt;(Order.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;verify(orderRepository).&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(captor.&lt;span style="color:#a6e22e"&gt;capture&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;assertThat(captor.&lt;span style="color:#a6e22e"&gt;getValue&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;getStatus&lt;/span&gt;()).&lt;span style="color:#a6e22e"&gt;isEqualTo&lt;/span&gt;(OrderStatus.&lt;span style="color:#a6e22e"&gt;PENDING&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="webmvctest--controller-레이어만-테스트"&gt;&lt;a href="#webmvctest--controller-%eb%a0%88%ec%9d%b4%ec%96%b4%eb%a7%8c-%ed%85%8c%ec%8a%a4%ed%8a%b8" class="header-anchor"&gt;&lt;/a&gt;@WebMvcTest — Controller 레이어만 테스트
&lt;/h2&gt;&lt;p&gt;Controller, Filter, &lt;code&gt;@ControllerAdvice&lt;/code&gt;만 로드한다. Service, Repository는 &lt;code&gt;@MockBean&lt;/code&gt;으로 대체한다. MockMvc로 실제 HTTP 요청처럼 테스트한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@WebMvcTest&lt;/span&gt;(OrderController.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderControllerTest&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Autowired&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; MockMvc mockMvc;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@MockBean&lt;/span&gt; &lt;span style="color:#75715e"&gt;// Spring Context에 Mock Bean 등록&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; OrderService orderService;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Autowired&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; ObjectMapper objectMapper;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;주문_생성_API_테스트&lt;/span&gt;() &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; Exception {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// given&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateOrderRequest request &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; CreateOrderRequest(1L, 2);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; OrderDto response &lt;span style="color:#f92672"&gt;=&lt;/span&gt; OrderDto.&lt;span style="color:#a6e22e"&gt;builder&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;(1L).&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;PENDING&amp;#34;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; given(orderService.&lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;(any())).&lt;span style="color:#a6e22e"&gt;willReturn&lt;/span&gt;(response);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// when &amp;amp; then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mockMvc.&lt;span style="color:#a6e22e"&gt;perform&lt;/span&gt;(post(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/orders&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;contentType&lt;/span&gt;(MediaType.&lt;span style="color:#a6e22e"&gt;APPLICATION_JSON&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;(objectMapper.&lt;span style="color:#a6e22e"&gt;writeValueAsString&lt;/span&gt;(request)))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;andExpect&lt;/span&gt;(status().&lt;span style="color:#a6e22e"&gt;isCreated&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;andExpect&lt;/span&gt;(jsonPath(&lt;span style="color:#e6db74"&gt;&amp;#34;$.id&amp;#34;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;(1L))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;andExpect&lt;/span&gt;(jsonPath(&lt;span style="color:#e6db74"&gt;&amp;#34;$.status&amp;#34;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;PENDING&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;andDo&lt;/span&gt;(print());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Validation_실패_시_400_반환&lt;/span&gt;() &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; Exception {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateOrderRequest invalidRequest &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; CreateOrderRequest(&lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;, 2);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mockMvc.&lt;span style="color:#a6e22e"&gt;perform&lt;/span&gt;(post(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/orders&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;contentType&lt;/span&gt;(MediaType.&lt;span style="color:#a6e22e"&gt;APPLICATION_JSON&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;(objectMapper.&lt;span style="color:#a6e22e"&gt;writeValueAsString&lt;/span&gt;(invalidRequest)))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;andExpect&lt;/span&gt;(status().&lt;span style="color:#a6e22e"&gt;isBadRequest&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Spring Security가 설정된 프로젝트에서는 인증 설정이 필요하다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@WebMvcTest&lt;/span&gt;(OrderController.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@WithMockUser&lt;/span&gt;(roles &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;USER&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;// 인증된 사용자로 테스트&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderControllerTest&lt;/span&gt; { ... }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;@MockBean&lt;/code&gt; vs &lt;code&gt;@Mock&lt;/code&gt;의 차이&lt;/strong&gt;: &lt;code&gt;@MockBean&lt;/code&gt;은 Spring Context에 Mock을 올린다(슬라이스 테스트에서 사용). &lt;code&gt;@Mock&lt;/code&gt;은 Spring 없이 Mockito만으로 사용하는 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="datajpatest--jpa-레이어만-테스트"&gt;&lt;a href="#datajpatest--jpa-%eb%a0%88%ec%9d%b4%ec%96%b4%eb%a7%8c-%ed%85%8c%ec%8a%a4%ed%8a%b8" class="header-anchor"&gt;&lt;/a&gt;@DataJpaTest — JPA 레이어만 테스트
&lt;/h2&gt;&lt;p&gt;JPA 관련 Bean만 로드한다. 기본적으로 인메모리 H2 DB를 사용하고, 각 테스트 후 자동 롤백한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@DataJpaTest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderRepositoryTest&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Autowired&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; OrderRepository orderRepository;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Autowired&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; TestEntityManager em;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;주문_저장_및_조회&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// given&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Member member &lt;span style="color:#f92672"&gt;=&lt;/span&gt; em.&lt;span style="color:#a6e22e"&gt;persist&lt;/span&gt;(Member.&lt;span style="color:#a6e22e"&gt;builder&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;test@test.com&amp;#34;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Order order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Order.&lt;span style="color:#a6e22e"&gt;builder&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;(member).&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(OrderStatus.&lt;span style="color:#a6e22e"&gt;PENDING&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// when&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Order saved &lt;span style="color:#f92672"&gt;=&lt;/span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; em.&lt;span style="color:#a6e22e"&gt;flush&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// 1차 캐시 → DB 즉시 반영&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; em.&lt;span style="color:#a6e22e"&gt;clear&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// 1차 캐시 비우기 → 실제 DB 조회 강제&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Order found &lt;span style="color:#f92672"&gt;=&lt;/span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(saved.&lt;span style="color:#a6e22e"&gt;getId&lt;/span&gt;()).&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assertThat(found.&lt;span style="color:#a6e22e"&gt;getStatus&lt;/span&gt;()).&lt;span style="color:#a6e22e"&gt;isEqualTo&lt;/span&gt;(OrderStatus.&lt;span style="color:#a6e22e"&gt;PENDING&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;em.flush()&lt;/code&gt; + &lt;code&gt;em.clear()&lt;/code&gt; 패턴이 중요하다. flush 없이 findById를 하면 1차 캐시에서 반환되어 DB에 실제로 저장됐는지 확인할 수 없다. flush로 DB에 반영하고 clear로 캐시를 비운 뒤 다시 조회해야 한다.&lt;/p&gt;
&lt;p&gt;실제 DB로 테스트하려면:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@DataJpaTest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@AutoConfigureTestDatabase&lt;/span&gt;(replace &lt;span style="color:#f92672"&gt;=&lt;/span&gt; AutoConfigureTestDatabase.&lt;span style="color:#a6e22e"&gt;Replace&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;NONE&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@ActiveProfiles&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;test&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderRepositoryTest&lt;/span&gt; { ... }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="springboottest--통합-테스트"&gt;&lt;a href="#springboottest--%ed%86%b5%ed%95%a9-%ed%85%8c%ec%8a%a4%ed%8a%b8" class="header-anchor"&gt;&lt;/a&gt;@SpringBootTest — 통합 테스트
&lt;/h2&gt;&lt;p&gt;전체 Spring Context를 로드한다. 실제 DB와 함께 통합 테스트에 사용한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@SpringBootTest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 테스트 후 자동 롤백&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderServiceIntegrationTest&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Autowired&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; OrderService orderService;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Autowired&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; OrderRepository orderRepository;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;주문_생성_통합_테스트&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateOrderRequest request &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; CreateOrderRequest(1L, 2);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; OrderDto result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; orderService.&lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;(request);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Order savedOrder &lt;span style="color:#f92672"&gt;=&lt;/span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(result.&lt;span style="color:#a6e22e"&gt;getId&lt;/span&gt;()).&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assertThat(savedOrder.&lt;span style="color:#a6e22e"&gt;getStatus&lt;/span&gt;()).&lt;span style="color:#a6e22e"&gt;isEqualTo&lt;/span&gt;(OrderStatus.&lt;span style="color:#a6e22e"&gt;PENDING&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MockMvc와 함께 전체 API 플로우를 테스트할 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@SpringBootTest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@AutoConfigureMockMvc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderApiIntegrationTest&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Autowired&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; MockMvc mockMvc;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;전체_주문_플로우_테스트&lt;/span&gt;() &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; Exception {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mockMvc.&lt;span style="color:#a6e22e"&gt;perform&lt;/span&gt;(post(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/orders&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;header&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Authorization&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;Bearer &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; getToken())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;contentType&lt;/span&gt;(MediaType.&lt;span style="color:#a6e22e"&gt;APPLICATION_JSON&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;(requestJson))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;andExpect&lt;/span&gt;(status().&lt;span style="color:#a6e22e"&gt;isCreated&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="4가지-비교"&gt;&lt;a href="#4%ea%b0%80%ec%a7%80-%eb%b9%84%ea%b5%90" class="header-anchor"&gt;&lt;/a&gt;4가지 비교
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;구분&lt;/th&gt;
 &lt;th&gt;속도&lt;/th&gt;
 &lt;th&gt;범위&lt;/th&gt;
 &lt;th&gt;목적&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;JUnit5 + Mockito&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;가장 빠름&lt;/td&gt;
 &lt;td&gt;클래스 단위&lt;/td&gt;
 &lt;td&gt;비즈니스 로직&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;@WebMvcTest&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;빠름&lt;/td&gt;
 &lt;td&gt;Controller 레이어&lt;/td&gt;
 &lt;td&gt;API 계약, Validation, 상태코드&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;@DataJpaTest&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;빠름&lt;/td&gt;
 &lt;td&gt;JPA 레이어&lt;/td&gt;
 &lt;td&gt;쿼리 메서드, 커스텀 쿼리&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;@SpringBootTest&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;느림&lt;/td&gt;
 &lt;td&gt;전체 컨텍스트&lt;/td&gt;
 &lt;td&gt;E2E 플로우, 통합 검증&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="자주-쓰는-assertj"&gt;&lt;a href="#%ec%9e%90%ec%a3%bc-%ec%93%b0%eb%8a%94-assertj" class="header-anchor"&gt;&lt;/a&gt;자주 쓰는 AssertJ
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;assertThat(result).&lt;span style="color:#a6e22e"&gt;isEqualTo&lt;/span&gt;(expected);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;assertThat(list).&lt;span style="color:#a6e22e"&gt;hasSize&lt;/span&gt;(3).&lt;span style="color:#a6e22e"&gt;contains&lt;/span&gt;(element);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;assertThat(list).&lt;span style="color:#a6e22e"&gt;isEmpty&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;assertThatThrownBy(() &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; service.&lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;isInstanceOf&lt;/span&gt;(SomeException.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;hasMessageContaining&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;에러 메시지&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;assertThat(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;extracting&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;containsExactly&lt;/span&gt;(OrderStatus.&lt;span style="color:#a6e22e"&gt;PENDING&lt;/span&gt;, 10000);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;테스트는 단계별로 목적에 맞게 작성하는 것이 효율적이다. 비즈니스 로직은 빠른 단위 테스트로, Controller 계층은 &lt;code&gt;@WebMvcTest&lt;/code&gt;로, JPA 쿼리는 &lt;code&gt;@DataJpaTest&lt;/code&gt;로 검증한다. &lt;code&gt;@SpringBootTest&lt;/code&gt;는 전체 플로우 통합 검증에만 사용한다. 이렇게 하면 테스트 속도를 유지하면서 신뢰성 있는 테스트 스위트를 구성할 수 있다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #12] Spring @Async — 비동기 처리와 CompletableFuture 병렬 실행</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-12-async/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-12-async/</guid><description>&lt;h2 id="이메일-발송을-기다려야-할까"&gt;&lt;a href="#%ec%9d%b4%eb%a9%94%ec%9d%bc-%eb%b0%9c%ec%86%a1%ec%9d%84-%ea%b8%b0%eb%8b%a4%eb%a0%a4%ec%95%bc-%ed%95%a0%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;이메일 발송을 기다려야 할까?
&lt;/h2&gt;&lt;p&gt;주문 완료 후 이메일 알림을 보내는 상황을 생각해보자. 이메일 발송 API가 외부 서비스를 호출하고 2초가 걸린다면, 사용자는 주문 완료 응답을 받기까지 2초를 기다려야 할까?&lt;/p&gt;
&lt;p&gt;주문 저장과 이메일 발송은 독립적인 작업이다. 이메일이 성공했는지 실패했는지를 주문 API 응답에 포함할 필요가 없다면 비동기로 처리하는 것이 맞다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Async&lt;/code&gt;는 메서드를 &lt;strong&gt;별도 스레드 풀에서 비동기 실행&lt;/strong&gt;한다. 호출자는 메서드 완료를 기다리지 않고 즉시 반환받는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="기본-설정"&gt;&lt;a href="#%ea%b8%b0%eb%b3%b8-%ec%84%a4%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;기본 설정
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EnableAsync&lt;/span&gt; &lt;span style="color:#75715e"&gt;// @Async 활성화&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AsyncConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;(name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;mailExecutor&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; TaskExecutor &lt;span style="color:#a6e22e"&gt;mailExecutor&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ThreadPoolTaskExecutor executor &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; ThreadPoolTaskExecutor();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;setCorePoolSize&lt;/span&gt;(5); &lt;span style="color:#75715e"&gt;// 항상 유지할 스레드 수&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;setMaxPoolSize&lt;/span&gt;(20); &lt;span style="color:#75715e"&gt;// 최대 스레드 수&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;setQueueCapacity&lt;/span&gt;(100); &lt;span style="color:#75715e"&gt;// 큐 대기 용량&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;setThreadNamePrefix&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;mail-async-&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;setRejectedExecutionHandler&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; ThreadPoolExecutor.&lt;span style="color:#a6e22e"&gt;CallerRunsPolicy&lt;/span&gt;()); &lt;span style="color:#75715e"&gt;// 큐 꽉 차면 호출 스레드가 직접 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; executor.&lt;span style="color:#a6e22e"&gt;initialize&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; executor;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;스레드 풀 동작 방식:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;1. 스레드 수 &amp;lt; CorePoolSize → 새 스레드 생성
2. 스레드 수 &amp;gt;= CorePoolSize → 큐에 대기
3. 큐가 꽉 참 → MaxPoolSize까지 스레드 추가 생성
4. MaxPoolSize도 꽉 참 → RejectedExecutionHandler 실행
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2 id="async-기본-사용"&gt;&lt;a href="#async-%ea%b8%b0%eb%b3%b8-%ec%82%ac%ec%9a%a9" class="header-anchor"&gt;&lt;/a&gt;@Async 기본 사용
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;NotificationService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Async&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;mailExecutor&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;// 지정한 스레드 풀 사용&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sendWelcomeEmail&lt;/span&gt;(String email) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 이 메서드는 별도 스레드에서 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 호출자는 즉시 반환받음&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; emailClient.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(email, &lt;span style="color:#e6db74"&gt;&amp;#34;환영합니다!&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;registerUser&lt;/span&gt;(UserRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; User user &lt;span style="color:#f92672"&gt;=&lt;/span&gt; userRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; User(request));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; notificationService.&lt;span style="color:#a6e22e"&gt;sendWelcomeEmail&lt;/span&gt;(user.&lt;span style="color:#a6e22e"&gt;getEmail&lt;/span&gt;()); &lt;span style="color:#75715e"&gt;// 기다리지 않음&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// 이메일 발송 완료 안 돼도 여기 도달&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="completablefuture--비동기-결과-조합"&gt;&lt;a href="#completablefuture--%eb%b9%84%eb%8f%99%ea%b8%b0-%ea%b2%b0%ea%b3%bc-%ec%a1%b0%ed%95%a9" class="header-anchor"&gt;&lt;/a&gt;CompletableFuture — 비동기 결과 조합
&lt;/h2&gt;&lt;p&gt;대시보드처럼 여러 데이터를 합쳐서 보여줘야 할 때, 각 조회를 병렬로 실행하면 시간을 줄일 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;DashboardService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Async&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; CompletableFuture&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderStats&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getOrderStats&lt;/span&gt;(Long userId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; CompletableFuture.&lt;span style="color:#a6e22e"&gt;completedFuture&lt;/span&gt;(orderService.&lt;span style="color:#a6e22e"&gt;getStats&lt;/span&gt;(userId));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Async&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; CompletableFuture&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;PaymentStats&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getPaymentStats&lt;/span&gt;(Long userId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; CompletableFuture.&lt;span style="color:#a6e22e"&gt;completedFuture&lt;/span&gt;(paymentService.&lt;span style="color:#a6e22e"&gt;getStats&lt;/span&gt;(userId));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; DashboardDto &lt;span style="color:#a6e22e"&gt;getDashboard&lt;/span&gt;(Long userId) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; Exception {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CompletableFuture&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderStats&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; orderFuture &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getOrderStats(userId);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CompletableFuture&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;PaymentStats&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; paymentFuture &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getPaymentStats(userId);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 둘 다 완료될 때까지 대기&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CompletableFuture.&lt;span style="color:#a6e22e"&gt;allOf&lt;/span&gt;(orderFuture, paymentFuture).&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; DashboardDto.&lt;span style="color:#a6e22e"&gt;of&lt;/span&gt;(orderFuture.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(), paymentFuture.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 두 조회가 각각 1초씩 걸린다면: 순차 2초 → 병렬 ~1초&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="주의사항-3가지"&gt;&lt;a href="#%ec%a3%bc%ec%9d%98%ec%82%ac%ed%95%ad-3%ea%b0%80%ec%a7%80" class="header-anchor"&gt;&lt;/a&gt;주의사항 3가지
&lt;/h2&gt;&lt;h3 id="1-자기-호출--transactional과-같은-함정"&gt;&lt;a href="#1-%ec%9e%90%ea%b8%b0-%ed%98%b8%ec%b6%9c--transactional%ea%b3%bc-%ea%b0%99%ec%9d%80-%ed%95%a8%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;1. 자기 호출 — @Transactional과 같은 함정
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;@Async&lt;/code&gt;도 AOP 프록시로 동작한다. 같은 클래스 안에서 직접 호출하면 프록시를 거치지 않아 동기로 실행된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;(OrderRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sendNotification(request); &lt;span style="color:#75715e"&gt;// ❌ this.sendNotification() → 동기 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Async&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sendNotification&lt;/span&gt;(OrderRequest request) { ... }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;해결책은 &lt;code&gt;sendNotification()&lt;/code&gt;을 별도 Bean(&lt;code&gt;NotificationService&lt;/code&gt;)으로 분리하는 것이다.&lt;/p&gt;
&lt;h3 id="2-예외-처리--호출자에게-전파-안-됨"&gt;&lt;a href="#2-%ec%98%88%ec%99%b8-%ec%b2%98%eb%a6%ac--%ed%98%b8%ec%b6%9c%ec%9e%90%ec%97%90%ea%b2%8c-%ec%a0%84%ed%8c%8c-%ec%95%88-%eb%90%a8" class="header-anchor"&gt;&lt;/a&gt;2. 예외 처리 — 호출자에게 전파 안 됨
&lt;/h3&gt;&lt;p&gt;반환값이 없는 &lt;code&gt;@Async&lt;/code&gt; 메서드에서 예외가 발생해도 호출자에게 전파되지 않는다. &lt;code&gt;AsyncUncaughtExceptionHandler&lt;/code&gt;로 처리해야 한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EnableAsync&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AsyncConfig&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;implements&lt;/span&gt; AsyncConfigurer {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; AsyncUncaughtExceptionHandler &lt;span style="color:#a6e22e"&gt;getAsyncUncaughtExceptionHandler&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; (ex, method, params) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;@Async 예외: method={}&amp;#34;&lt;/span&gt;, method.&lt;span style="color:#a6e22e"&gt;getName&lt;/span&gt;(), ex);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; alertService.&lt;span style="color:#a6e22e"&gt;sendAlert&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;비동기 작업 실패: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; method.&lt;span style="color:#a6e22e"&gt;getName&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-threadlocal-전파-안-됨"&gt;&lt;a href="#3-threadlocal-%ec%a0%84%ed%8c%8c-%ec%95%88-%eb%90%a8" class="header-anchor"&gt;&lt;/a&gt;3. ThreadLocal 전파 안 됨
&lt;/h3&gt;&lt;p&gt;메인 스레드의 &lt;code&gt;SecurityContext&lt;/code&gt;나 &lt;code&gt;ThreadLocal&lt;/code&gt; 값이 &lt;code&gt;@Async&lt;/code&gt; 스레드에 자동으로 전파되지 않는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Async&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;asyncMethod&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SecurityContextHolder.&lt;span style="color:#a6e22e"&gt;getContext&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;getAuthentication&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// null 가능&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;보안 컨텍스트가 필요하다면 스레드 풀에 &lt;code&gt;DelegatingSecurityContextTaskDecorator&lt;/code&gt;를 설정해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="언제-쓸까"&gt;&lt;a href="#%ec%96%b8%ec%a0%9c-%ec%93%b8%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;언제 쓸까
&lt;/h2&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;✅ 적합한 경우
 - 이메일/SMS/푸시 알림 발송
 - 감사 로그 기록
 - 캐시 비동기 갱신
 - 외부 API 호출 (응답 즉시 반환 불필요)

❌ 적합하지 않은 경우
 - 결과를 즉시 응답에 포함해야 할 때
 - 트랜잭션을 호출자와 공유해야 할 때
 - 실패 시 호출자가 반드시 알아야 할 때
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;@Async&lt;/code&gt;와 &lt;code&gt;@Transactional&lt;/code&gt;을 같이 쓰면 비동기 스레드는 호출자의 트랜잭션을 이어받지 않는다. 새로운 독립 트랜잭션으로 실행된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;@Async&lt;/code&gt;는 &amp;ldquo;결과를 기다릴 필요 없는 부가 작업&amp;quot;에 적합하다. 동작 원리는 AOP 프록시이므로 &lt;code&gt;@Transactional&lt;/code&gt;과 같은 자기 호출 함정이 있다. 예외는 &lt;code&gt;AsyncUncaughtExceptionHandler&lt;/code&gt;로 처리하고, ThreadLocal 전파가 필요하면 별도 설정이 필요하다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring Cache — &lt;code&gt;@Cacheable&lt;/code&gt;과 Redis 캐시 전략을 정리한다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #13] Spring Cache — @Cacheable로 DB 부하 줄이기, Redis 캐시 전략</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-13-cache/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-13-cache/</guid><description>&lt;h2 id="같은-데이터를-매번-db에서-가져와야-할까"&gt;&lt;a href="#%ea%b0%99%ec%9d%80-%eb%8d%b0%ec%9d%b4%ed%84%b0%eb%a5%bc-%eb%a7%a4%eb%b2%88-db%ec%97%90%ec%84%9c-%ea%b0%80%ec%a0%b8%ec%99%80%ec%95%bc-%ed%95%a0%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;같은 데이터를 매번 DB에서 가져와야 할까?
&lt;/h2&gt;&lt;p&gt;자주 조회되지만 잘 바뀌지 않는 데이터가 있다. 상품 카탈로그, 카테고리 목록, 설정값 같은 것들이다. 이런 데이터를 요청마다 DB에서 가져오는 것은 비효율적이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;캐시&lt;/strong&gt;는 이 데이터를 메모리에 저장해두고, 다음 요청부터는 DB를 거치지 않고 메모리에서 반환한다. Spring Cache 추상화는 &lt;code&gt;@Cacheable&lt;/code&gt; 어노테이션 하나로 이 동작을 구현한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="cacheable--캐시-조회--없으면-실행--저장"&gt;&lt;a href="#cacheable--%ec%ba%90%ec%8b%9c-%ec%a1%b0%ed%9a%8c--%ec%97%86%ec%9c%bc%eb%a9%b4-%ec%8b%a4%ed%96%89--%ec%a0%80%ec%9e%a5" class="header-anchor"&gt;&lt;/a&gt;@Cacheable — 캐시 조회 → 없으면 실행 → 저장
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ProductService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Cacheable&lt;/span&gt;(value &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;products&amp;#34;&lt;/span&gt;, key &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#productId&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ProductDto &lt;span style="color:#a6e22e"&gt;getProduct&lt;/span&gt;(Long productId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 캐시에 없을 때만 실행 (Cache Miss)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; productRepository.&lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(productId)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;map&lt;/span&gt;(ProductDto::from)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;orElseThrow&lt;/span&gt;(() &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; ProductNotFoundException(productId));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EnableCaching&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 캐시 활성화&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CacheConfig&lt;/span&gt; { }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;동작 흐름:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[첫 번째 호출]
→ 캐시 조회 → Miss → 메서드 실행 → 결과를 캐시에 저장 → 반환

[두 번째 이후]
→ 캐시 조회 → Hit → 메서드 실행 없이 캐시 반환
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;키를 복합적으로 구성할 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Cacheable&lt;/span&gt;(value &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;products&amp;#34;&lt;/span&gt;, key &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#category + &amp;#39;:&amp;#39; + #page&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;ProductDto&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getProductsByCategory&lt;/span&gt;(String category, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; page) { ... }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="cacheevict--데이터-수정-시-캐시-삭제"&gt;&lt;a href="#cacheevict--%eb%8d%b0%ec%9d%b4%ed%84%b0-%ec%88%98%ec%a0%95-%ec%8b%9c-%ec%ba%90%ec%8b%9c-%ec%82%ad%ec%a0%9c" class="header-anchor"&gt;&lt;/a&gt;@CacheEvict — 데이터 수정 시 캐시 삭제
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@CacheEvict&lt;/span&gt;(value &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;products&amp;#34;&lt;/span&gt;, key &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#productId&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;updateProduct&lt;/span&gt;(Long productId, ProductUpdateRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 메서드 실행 후 캐시 삭제 (기본값)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 다음 조회 시 DB에서 새 데이터를 가져와 캐시에 저장&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 전체 삭제&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@CacheEvict&lt;/span&gt;(value &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;products&amp;#34;&lt;/span&gt;, allEntries &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;clearAllCache&lt;/span&gt;() { }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="cacheput--항상-실행--캐시-갱신"&gt;&lt;a href="#cacheput--%ed%95%ad%ec%83%81-%ec%8b%a4%ed%96%89--%ec%ba%90%ec%8b%9c-%ea%b0%b1%ec%8b%a0" class="header-anchor"&gt;&lt;/a&gt;@CachePut — 항상 실행 + 캐시 갱신
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@CachePut&lt;/span&gt;(value &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;products&amp;#34;&lt;/span&gt;, key &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#result.id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ProductDto &lt;span style="color:#a6e22e"&gt;createProduct&lt;/span&gt;(ProductCreateRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Product saved &lt;span style="color:#f92672"&gt;=&lt;/span&gt; productRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Product(request));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; ProductDto.&lt;span style="color:#a6e22e"&gt;from&lt;/span&gt;(saved); &lt;span style="color:#75715e"&gt;// 반환값이 캐시에 저장됨&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;@Cacheable&lt;/code&gt;은 캐시에 있으면 실행을 건너뛰지만, &lt;code&gt;@CachePut&lt;/code&gt;은 항상 실행하고 결과로 캐시를 갱신한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="로컬-캐시에서-redis로-교체"&gt;&lt;a href="#%eb%a1%9c%ec%bb%ac-%ec%ba%90%ec%8b%9c%ec%97%90%ec%84%9c-redis%eb%a1%9c-%ea%b5%90%ec%b2%b4" class="header-anchor"&gt;&lt;/a&gt;로컬 캐시에서 Redis로 교체
&lt;/h2&gt;&lt;p&gt;Spring Cache는 추상화 레이어라 구현체를 쉽게 교체할 수 있다. 코드 변경 없이 &lt;code&gt;CacheManager&lt;/code&gt;만 바꾸면 된다.&lt;/p&gt;
&lt;h3 id="로컬-캐시-기본"&gt;&lt;a href="#%eb%a1%9c%ec%bb%ac-%ec%ba%90%ec%8b%9c-%ea%b8%b0%eb%b3%b8" class="header-anchor"&gt;&lt;/a&gt;로컬 캐시 (기본)
&lt;/h3&gt;&lt;p&gt;기본값은 &lt;code&gt;ConcurrentMapCacheManager&lt;/code&gt;다. 별도 설정 없이 동작하지만 서버가 여러 대면 서버마다 다른 캐시를 갖게 되어 데이터 불일치가 생긴다.&lt;/p&gt;
&lt;h3 id="redis-캐시-다중-서버-환경"&gt;&lt;a href="#redis-%ec%ba%90%ec%8b%9c-%eb%8b%a4%ec%a4%91-%ec%84%9c%eb%b2%84-%ed%99%98%ea%b2%bd" class="header-anchor"&gt;&lt;/a&gt;Redis 캐시 (다중 서버 환경)
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span style="color:#f92672"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-data-redis&lt;span style="color:#f92672"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spring&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;cache&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;type&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;redis&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;redis&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;time-to-live: 3600000 # 기본 TTL&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1시간&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;캐시별로 TTL을 다르게 설정하려면:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; RedisCacheManager &lt;span style="color:#a6e22e"&gt;cacheManager&lt;/span&gt;(RedisConnectionFactory factory) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Map&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;String, RedisCacheConfiguration&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; configs &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; HashMap&lt;span style="color:#f92672"&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; configs.&lt;span style="color:#a6e22e"&gt;put&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;products&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RedisCacheConfiguration.&lt;span style="color:#a6e22e"&gt;defaultCacheConfig&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;entryTtl&lt;/span&gt;(Duration.&lt;span style="color:#a6e22e"&gt;ofMinutes&lt;/span&gt;(30))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;serializeValuesWith&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RedisSerializationContext.&lt;span style="color:#a6e22e"&gt;SerializationPair&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;fromSerializer&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; GenericJackson2JsonRedisSerializer())));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; configs.&lt;span style="color:#a6e22e"&gt;put&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;users&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RedisCacheConfiguration.&lt;span style="color:#a6e22e"&gt;defaultCacheConfig&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;entryTtl&lt;/span&gt;(Duration.&lt;span style="color:#a6e22e"&gt;ofHours&lt;/span&gt;(1)));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; RedisCacheManager.&lt;span style="color:#a6e22e"&gt;builder&lt;/span&gt;(factory)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;withInitialCacheConfigurations&lt;/span&gt;(configs)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="캐시-전략"&gt;&lt;a href="#%ec%ba%90%ec%8b%9c-%ec%a0%84%eb%9e%b5" class="header-anchor"&gt;&lt;/a&gt;캐시 전략
&lt;/h2&gt;&lt;h3 id="cache-aside-lazy-loading--가장-일반적"&gt;&lt;a href="#cache-aside-lazy-loading--%ea%b0%80%ec%9e%a5-%ec%9d%bc%eb%b0%98%ec%a0%81" class="header-anchor"&gt;&lt;/a&gt;Cache-Aside (Lazy Loading) — 가장 일반적
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;읽기: 캐시 → Miss면 DB 조회 → 캐시 저장 → 반환
쓰기: DB 업데이트 → 캐시 삭제 (다음 읽기 때 재적재)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Spring의 &lt;code&gt;@Cacheable&lt;/code&gt; + &lt;code&gt;@CacheEvict&lt;/code&gt; 패턴이 이것이다.&lt;/p&gt;
&lt;h3 id="write-through"&gt;&lt;a href="#write-through" class="header-anchor"&gt;&lt;/a&gt;Write-Through
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;쓰기: DB 업데이트 + 캐시 동시 업데이트
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;@CachePut&lt;/code&gt; 패턴이다. 읽기가 항상 캐시에서 이루어져 빠르지만, 잘 쓰지 않는 데이터도 캐시에 올라가 공간을 낭비할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="주의사항-3가지"&gt;&lt;a href="#%ec%a3%bc%ec%9d%98%ec%82%ac%ed%95%ad-3%ea%b0%80%ec%a7%80" class="header-anchor"&gt;&lt;/a&gt;주의사항 3가지
&lt;/h2&gt;&lt;h3 id="1-캐시-스탬피드"&gt;&lt;a href="#1-%ec%ba%90%ec%8b%9c-%ec%8a%a4%ed%83%ac%ed%94%bc%eb%93%9c" class="header-anchor"&gt;&lt;/a&gt;1. 캐시 스탬피드
&lt;/h3&gt;&lt;p&gt;캐시가 만료되는 순간 대량의 요청이 동시에 DB로 몰리는 현상이다. TTL에 랜덤 jitter를 추가하거나 분산 락으로 하나의 요청만 DB를 조회하도록 처리한다.&lt;/p&gt;
&lt;h3 id="2-캐시-무효화-누락"&gt;&lt;a href="#2-%ec%ba%90%ec%8b%9c-%eb%ac%b4%ed%9a%a8%ed%99%94-%eb%88%84%eb%9d%bd" class="header-anchor"&gt;&lt;/a&gt;2. 캐시 무효화 누락
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@CacheEvict&lt;/span&gt;(value &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;products&amp;#34;&lt;/span&gt;, key &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#id&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;// 잊으면 안 됨&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;updateProduct&lt;/span&gt;(Long id, ProductUpdateRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; productRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(product.&lt;span style="color:#a6e22e"&gt;update&lt;/span&gt;(request));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;데이터 수정 후 &lt;code&gt;@CacheEvict&lt;/code&gt;를 빠트리면 수정 전 데이터가 캐시에 남아 서비스한다.&lt;/p&gt;
&lt;h3 id="3-자기-호출"&gt;&lt;a href="#3-%ec%9e%90%ea%b8%b0-%ed%98%b8%ec%b6%9c" class="header-anchor"&gt;&lt;/a&gt;3. 자기 호출
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;@Cacheable&lt;/code&gt;도 AOP 프록시로 동작한다. &lt;code&gt;@Transactional&lt;/code&gt;, &lt;code&gt;@Async&lt;/code&gt;와 동일하게 같은 클래스 내부에서 직접 호출하면 캐시가 적용되지 않는다.&lt;/p&gt;
&lt;p&gt;Redis에 저장하는 DTO는 직렬화 가능해야 한다. Jackson JSON 직렬화 시 기본 생성자가 필요하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;Spring Cache의 핵심은 &lt;strong&gt;구현체 독립성&lt;/strong&gt;이다. 로컬 캐시에서 Redis로 교체할 때 &lt;code&gt;@Cacheable&lt;/code&gt; 코드는 전혀 바꾸지 않아도 된다. &lt;code&gt;@Cacheable&lt;/code&gt;(조회 캐시), &lt;code&gt;@CacheEvict&lt;/code&gt;(삭제), &lt;code&gt;@CachePut&lt;/code&gt;(갱신) 세 가지 어노테이션을 목적에 맞게 조합하고, 다중 서버 환경에서는 반드시 Redis 같은 분산 캐시를 사용해야 한다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring Scheduling — &lt;code&gt;@Scheduled&lt;/code&gt;와 다중 서버 환경에서의 중복 실행 방지를 정리한다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #14] Spring @Scheduled — cron 표현식과 다중 서버 중복 실행 방지</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-14-scheduling/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-14-scheduling/</guid><description>&lt;h2 id="매일-새벽-2시에-배치를-실행하려면"&gt;&lt;a href="#%eb%a7%a4%ec%9d%bc-%ec%83%88%eb%b2%bd-2%ec%8b%9c%ec%97%90-%eb%b0%b0%ec%b9%98%eb%a5%bc-%ec%8b%a4%ed%96%89%ed%95%98%eb%a0%a4%eb%a9%b4" class="header-anchor"&gt;&lt;/a&gt;매일 새벽 2시에 배치를 실행하려면
&lt;/h2&gt;&lt;p&gt;통계 집계, 만료 데이터 정리, 리포트 생성 같은 작업은 주기적으로 실행해야 한다. Spring &lt;code&gt;@Scheduled&lt;/code&gt;는 메서드에 어노테이션 하나만 붙여서 이런 작업을 등록할 수 있다.&lt;/p&gt;
&lt;p&gt;단 하나의 함정이 있다. &lt;strong&gt;서버가 여러 대라면 모든 서버가 동시에 실행한다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="기본-설정"&gt;&lt;a href="#%ea%b8%b0%eb%b3%b8-%ec%84%a4%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;기본 설정
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EnableScheduling&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 스케줄링 활성화&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;SchedulingConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 기본값은 단일 스레드 — 독립 실행이 필요하면 풀 설정&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; TaskScheduler &lt;span style="color:#a6e22e"&gt;taskScheduler&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ThreadPoolTaskScheduler scheduler &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; ThreadPoolTaskScheduler();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; scheduler.&lt;span style="color:#a6e22e"&gt;setPoolSize&lt;/span&gt;(5);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; scheduler.&lt;span style="color:#a6e22e"&gt;setThreadNamePrefix&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;scheduled-&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; scheduler.&lt;span style="color:#a6e22e"&gt;initialize&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; scheduler;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;기본값이 단일 스레드라는 점을 기억해두자. 스케줄 메서드 A가 10초 걸리면 B는 A가 끝날 때까지 대기한다. 독립적으로 실행돼야 한다면 위처럼 스레드 풀을 설정해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="세-가지-실행-방식"&gt;&lt;a href="#%ec%84%b8-%ea%b0%80%ec%a7%80-%ec%8b%a4%ed%96%89-%eb%b0%a9%ec%8b%9d" class="header-anchor"&gt;&lt;/a&gt;세 가지 실행 방식
&lt;/h2&gt;&lt;h3 id="fixedrate--시작-시점-기준"&gt;&lt;a href="#fixedrate--%ec%8b%9c%ec%9e%91-%ec%8b%9c%ec%a0%90-%ea%b8%b0%ec%a4%80" class="header-anchor"&gt;&lt;/a&gt;fixedRate — 시작 시점 기준
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Scheduled&lt;/span&gt;(fixedRate &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 5000) &lt;span style="color:#75715e"&gt;// 5초마다 (이전 실행 시작으로부터)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fixedRateTask&lt;/span&gt;() { ... }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;실행 시작: 0초
실행 완료: 3초
다음 시작: 5초 (0초 + 5초)
다음 시작: 10초 (5초 + 5초)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;실행 시간이 주기를 초과하면 다음 실행이 겹칠 수 있다(스레드 풀이 있을 때).&lt;/p&gt;
&lt;h3 id="fixeddelay--완료-시점-기준"&gt;&lt;a href="#fixeddelay--%ec%99%84%eb%a3%8c-%ec%8b%9c%ec%a0%90-%ea%b8%b0%ec%a4%80" class="header-anchor"&gt;&lt;/a&gt;fixedDelay — 완료 시점 기준
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Scheduled&lt;/span&gt;(fixedDelay &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 5000) &lt;span style="color:#75715e"&gt;// 이전 실행 완료 후 5초&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fixedDelayTask&lt;/span&gt;() { ... }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;실행 시작: 0초
실행 완료: 3초
다음 시작: 8초 (3초 완료 + 5초)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;실행이 아무리 오래 걸려도 완료 후 5초 뒤에 시작한다. 겹칠 위험이 없다.&lt;/p&gt;
&lt;h3 id="cron--특정-시각에-실행"&gt;&lt;a href="#cron--%ed%8a%b9%ec%a0%95-%ec%8b%9c%ea%b0%81%ec%97%90-%ec%8b%a4%ed%96%89" class="header-anchor"&gt;&lt;/a&gt;cron — 특정 시각에 실행
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Scheduled&lt;/span&gt;(cron &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;0 0 2 * * *&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;// 매일 새벽 2시&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;dailyBatchJob&lt;/span&gt;() { ... }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Scheduled&lt;/span&gt;(cron &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;0 */30 9-18 * * MON-FRI&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;// 평일 9~18시 30분마다&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;businessHoursTask&lt;/span&gt;() { ... }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="크론-표현식-구조"&gt;&lt;a href="#%ed%81%ac%eb%a1%a0-%ed%91%9c%ed%98%84%ec%8b%9d-%ea%b5%ac%ec%a1%b0" class="header-anchor"&gt;&lt;/a&gt;크론 표현식 구조
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;초 분 시 일 월 요일
0 0 2 * * * → 매일 02:00:00
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;자주 쓰는 표현:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;0 0 * * * *&amp;#34; // 매 시간 정각
&amp;#34;0 0 0 * * *&amp;#34; // 매일 자정
&amp;#34;0 0 0 1 * *&amp;#34; // 매월 1일 자정
&amp;#34;0 0 9 * * MON-FRI&amp;#34; // 평일 오전 9시
&amp;#34;0 */5 * * * *&amp;#34; // 5분마다
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="환경별-cron-분리"&gt;&lt;a href="#%ed%99%98%ea%b2%bd%eb%b3%84-cron-%eb%b6%84%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;환경별 cron 분리
&lt;/h3&gt;&lt;p&gt;로컬에서는 빠르게 테스트하고, 운영에서는 실제 스케줄을 적용한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# application.yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;schedule&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;daily-batch&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;cron&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;0 0 2 * * *&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# application-local.yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;schedule&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;daily-batch&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;cron&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;0 */1 * * * *&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# 1분마다 테스트&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Scheduled&lt;/span&gt;(cron &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;${schedule.daily-batch.cron}&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;dailyBatch&lt;/span&gt;() { ... }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="다중-서버-환경의-중복-실행-문제"&gt;&lt;a href="#%eb%8b%a4%ec%a4%91-%ec%84%9c%eb%b2%84-%ed%99%98%ea%b2%bd%ec%9d%98-%ec%a4%91%eb%b3%b5-%ec%8b%a4%ed%96%89-%eb%ac%b8%ec%a0%9c" class="header-anchor"&gt;&lt;/a&gt;다중 서버 환경의 중복 실행 문제
&lt;/h2&gt;&lt;p&gt;서버가 3대면 새벽 2시에 배치가 3번 실행된다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;서버 A ─┐
서버 B ─┼─ 모두 02:00에 dailyBatch() 실행 → 데이터 3번 처리
서버 C ─┘
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="해결-shedlock"&gt;&lt;a href="#%ed%95%b4%ea%b2%b0-shedlock" class="header-anchor"&gt;&lt;/a&gt;해결: ShedLock
&lt;/h3&gt;&lt;p&gt;DB나 Redis를 락 저장소로 사용해 &lt;strong&gt;단 하나의 서버만&lt;/strong&gt; 실행을 보장한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;net.javacrumbs.shedlock&lt;span style="color:#f92672"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;shedlock-spring&lt;span style="color:#f92672"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;net.javacrumbs.shedlock&lt;span style="color:#f92672"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;shedlock-provider-jdbc-template&lt;span style="color:#f92672"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- 락 테이블 (MySQL)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; shedlock (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name VARCHAR(&lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lock_until &lt;span style="color:#66d9ef"&gt;TIMESTAMP&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; locked_at &lt;span style="color:#66d9ef"&gt;TIMESTAMP&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; locked_by VARCHAR(&lt;span style="color:#ae81ff"&gt;255&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;PRIMARY&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;KEY&lt;/span&gt; (name)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EnableScheduling&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EnableSchedulerLock&lt;/span&gt;(defaultLockAtMostFor &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;10m&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;SchedulingConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; LockProvider &lt;span style="color:#a6e22e"&gt;lockProvider&lt;/span&gt;(DataSource dataSource) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; JdbcTemplateLockProvider(dataSource);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BatchJobService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Scheduled&lt;/span&gt;(cron &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;0 0 2 * * *&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@SchedulerLock&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;dailyBatchJob&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 유니크한 락 이름&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lockAtLeastFor &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;5m&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// 빠르게 끝나도 5분간 락 유지 (중복 방지)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lockAtMostFor &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;30m&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 서버가 죽어도 30분 후 자동 해제&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;dailyBatchJob&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 이 서버만 실행 — 다른 서버는 락을 잡지 못해 건너뜀&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; processOrders();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;lockAtMostFor&lt;/code&gt;이 중요하다. 서버가 배치 실행 중에 죽으면 DB의 락 레코드가 남아있게 된다. 이 설정이 있어야 일정 시간 후 락이 자동으로 해제된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="주의사항"&gt;&lt;a href="#%ec%a3%bc%ec%9d%98%ec%82%ac%ed%95%ad" class="header-anchor"&gt;&lt;/a&gt;주의사항
&lt;/h2&gt;&lt;h3 id="예외는-반드시-잡는다"&gt;&lt;a href="#%ec%98%88%ec%99%b8%eb%8a%94-%eb%b0%98%eb%93%9c%ec%8b%9c-%ec%9e%a1%eb%8a%94%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;예외는 반드시 잡는다
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Scheduled&lt;/span&gt;(cron &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;0 0 2 * * *&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;dailyBatch&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; processBatch();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (Exception e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;배치 실패&amp;#34;&lt;/span&gt;, e);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; alertService.&lt;span style="color:#a6e22e"&gt;sendAlert&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;dailyBatch 실패: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; e.&lt;span style="color:#a6e22e"&gt;getMessage&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;예외가 전파되면 다음 실행이 안 될 수 있다. 항상 try-catch로 잡고 알림을 보내는 것이 좋다.&lt;/p&gt;
&lt;h3 id="트랜잭션은-서비스에-위임"&gt;&lt;a href="#%ed%8a%b8%eb%9e%9c%ec%9e%ad%ec%85%98%ec%9d%80-%ec%84%9c%eb%b9%84%ec%8a%a4%ec%97%90-%ec%9c%84%ec%9e%84" class="header-anchor"&gt;&lt;/a&gt;트랜잭션은 서비스에 위임
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Scheduled&lt;/span&gt;(cron &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;0 0 2 * * *&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;dailyBatch&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; batchService.&lt;span style="color:#a6e22e"&gt;processDailyOrders&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// @Transactional이 있는 서비스에 위임&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;@Scheduled&lt;/code&gt;는 트랜잭션을 자동으로 걸지 않는다. 필요하면 &lt;code&gt;@Transactional&lt;/code&gt;을 추가하거나 서비스 레이어에 위임한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;단일 서버라면 &lt;code&gt;@Scheduled&lt;/code&gt;만으로 충분하다. 다중 서버 환경이라면 중복 실행 방지가 필수다. &lt;strong&gt;ShedLock&lt;/strong&gt;이 가장 간단한 해결책이다. DB에 락 레코드를 남기는 방식이라 별도 인프라 없이 사용할 수 있다. 복잡한 잡 관리(의존성, 재시도, 히스토리)가 필요하다면 Quartz를 고려한다.&lt;/p&gt;
&lt;p&gt;Spring 시리즈 마지막 편이다. Servlet 기초부터 스케줄링까지 14편을 통해 Spring의 핵심 구조를 정리했다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #2] Spring Core — IoC, DI, AOP를 코드로 이해하기</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-02-core-ioc-di-aop/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-02-core-ioc-di-aop/</guid><description>&lt;h2 id="service만-붙이면-왜-되는-걸까"&gt;&lt;a href="#service%eb%a7%8c-%eb%b6%99%ec%9d%b4%eb%a9%b4-%ec%99%9c-%eb%90%98%eb%8a%94-%ea%b1%b8%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;&amp;ldquo;@Service만 붙이면 왜 되는 걸까?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;Spring을 처음 쓸 때 누구나 한 번쯤 갖는 의문이다. 클래스에 어노테이션 하나 달았을 뿐인데 의존성이 주입되고, &lt;code&gt;@Transactional&lt;/code&gt;만 붙이면 트랜잭션이 처리된다.&lt;/p&gt;
&lt;p&gt;이게 &amp;ldquo;마법&amp;quot;처럼 느껴지는 건, 내부 동작을 모르기 때문이다. 이 글에서는 Spring의 핵심 메커니즘 세 가지 — &lt;strong&gt;IoC, DI, AOP&lt;/strong&gt; — 를 코드 수준에서 풀어낸다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="어노테이션은-라벨일-뿐이다"&gt;&lt;a href="#%ec%96%b4%eb%85%b8%ed%85%8c%ec%9d%b4%ec%85%98%ec%9d%80-%eb%9d%bc%eb%b2%a8%ec%9d%bc-%eb%bf%90%ec%9d%b4%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;어노테이션은 라벨일 뿐이다
&lt;/h2&gt;&lt;p&gt;먼저 오해를 하나 짚고 시작한다. &lt;code&gt;@Service&lt;/code&gt;, &lt;code&gt;@Autowired&lt;/code&gt; 같은 어노테이션은 &lt;strong&gt;그 자체로 아무 일도 하지 않는다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java 어노테이션은 코드에 붙이는 메타데이터다. Spring Container가 없다면 &lt;code&gt;@Service&lt;/code&gt;를 붙여도 그냥 라벨이 붙은 일반 클래스일 뿐이다.&lt;/p&gt;
&lt;p&gt;Spring이 하는 일은 애플리케이션 시작 시 리플렉션으로 클래스를 스캔해서 어노테이션을 읽고, 그에 따라 동작을 결정하는 것이다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;애플리케이션 시작
 → @ComponentScan이 지정 패키지 하위를 전부 탐색
 → 각 클래스를 리플렉션으로 읽음
 → @Service/@Repository 발견 → &amp;#34;Bean으로 등록&amp;#34;
 → @Autowired 발견 → &amp;#34;등록된 Bean 중 타입 맞는 것을 주입&amp;#34;
 → @Transactional 발견 → &amp;#34;CGLIB 프록시로 감싸기&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Spring이 이 과정을 대신 처리해준다. 개발자가 직접 구현하면 아래처럼 된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Spring 내부에서 실제로 일어나는 일 (단순화)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (Class&lt;span style="color:#f92672"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; clazz : scannedClasses) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (clazz.&lt;span style="color:#a6e22e"&gt;isAnnotationPresent&lt;/span&gt;(Component.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Object instance &lt;span style="color:#f92672"&gt;=&lt;/span&gt; clazz.&lt;span style="color:#a6e22e"&gt;getDeclaredConstructor&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;newInstance&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; beanContainer.&lt;span style="color:#a6e22e"&gt;register&lt;/span&gt;(clazz, instance);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이걸 자동으로 해주는 것이 Spring의 핵심이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="ioc--제어권을-spring에게-넘기다"&gt;&lt;a href="#ioc--%ec%a0%9c%ec%96%b4%ea%b6%8c%ec%9d%84-spring%ec%97%90%ea%b2%8c-%eb%84%98%ea%b8%b0%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;IoC — 제어권을 Spring에게 넘기다
&lt;/h2&gt;&lt;p&gt;전통적인 방식에서는 개발자가 객체를 직접 생성하고 의존성을 연결한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 직접 생성 — 강한 결합&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; PaymentService paymentService &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; PaymentService();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이 코드의 문제는 &lt;code&gt;OrderService&lt;/code&gt;가 &lt;code&gt;PaymentService&lt;/code&gt;의 구체적인 구현에 묶여있다는 점이다. &lt;code&gt;PaymentService&lt;/code&gt; 구현을 &lt;code&gt;KakaopayService&lt;/code&gt;로 바꾸려면 &lt;code&gt;OrderService&lt;/code&gt;도 수정해야 한다.&lt;/p&gt;
&lt;p&gt;**IoC(Inversion of Control, 제어의 역전)**는 이 제어권을 개발자에서 Spring Container로 넘기는 것이다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;전통적: 개발자 → 객체 생성 → 의존성 연결
IoC: Spring Container → 객체 생성 → 의존성 연결 → 개발자에게 제공
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;개발자는 &amp;ldquo;무엇이 필요한지&amp;quot;만 선언하면 된다. &amp;ldquo;어떻게 만들지&amp;quot;는 Spring이 결정한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="di--ioc를-구현하는-방법"&gt;&lt;a href="#di--ioc%eb%a5%bc-%ea%b5%ac%ed%98%84%ed%95%98%eb%8a%94-%eb%b0%a9%eb%b2%95" class="header-anchor"&gt;&lt;/a&gt;DI — IoC를 구현하는 방법
&lt;/h2&gt;&lt;p&gt;DI(Dependency Injection, 의존성 주입)는 IoC를 실현하는 구체적인 방법이다. Spring이 의존성을 주입하는 방식은 세 가지가 있다.&lt;/p&gt;
&lt;h3 id="생성자-주입-권장"&gt;&lt;a href="#%ec%83%9d%ec%84%b1%ec%9e%90-%ec%a3%bc%ec%9e%85-%ea%b6%8c%ec%9e%a5" class="header-anchor"&gt;&lt;/a&gt;생성자 주입 (권장)
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; PaymentService paymentService;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Autowired&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 생성자가 1개면 생략 가능&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderService&lt;/span&gt;(PaymentService paymentService) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;paymentService&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; paymentService;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="필드-주입-비권장"&gt;&lt;a href="#%ed%95%84%eb%93%9c-%ec%a3%bc%ec%9e%85-%eb%b9%84%ea%b6%8c%ec%9e%a5" class="header-anchor"&gt;&lt;/a&gt;필드 주입 (비권장)
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Autowired&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; PaymentService paymentService; &lt;span style="color:#75715e"&gt;// 테스트 어려움&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;생성자 주입을 쓰는 이유가 있다.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;이유&lt;/th&gt;
 &lt;th&gt;설명&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;불변성&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;final&lt;/code&gt; 선언 가능 — 런타임 중 변경 불가&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;필수 의존성 명시&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;컴파일 시점에 의존성 누락 감지&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;테스트 용이&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;new OrderService(mockPaymentService)&lt;/code&gt;로 직접 주입 가능&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;순환 의존성 감지&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;앱 시작 시 즉시 오류 발생&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;필드 주입의 치명적 단점은 테스트다. Spring Container 없이 &lt;code&gt;new OrderService()&lt;/code&gt;를 하면 &lt;code&gt;paymentService&lt;/code&gt;가 null이다. 반면 생성자 주입은 테스트 코드에서도 의존성을 명확히 넘길 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="bean--spring이-관리하는-객체"&gt;&lt;a href="#bean--spring%ec%9d%b4-%ea%b4%80%eb%a6%ac%ed%95%98%eb%8a%94-%ea%b0%9d%ec%b2%b4" class="header-anchor"&gt;&lt;/a&gt;Bean — Spring이 관리하는 객체
&lt;/h2&gt;&lt;p&gt;Bean은 Spring Container가 생성하고 관리하는 객체다. 개발자가 &lt;code&gt;new&lt;/code&gt;로 직접 생성하지 않는다.&lt;/p&gt;
&lt;h3 id="등록-방법"&gt;&lt;a href="#%eb%93%b1%eb%a1%9d-%eb%b0%a9%eb%b2%95" class="header-anchor"&gt;&lt;/a&gt;등록 방법
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;컴포넌트 스캔&lt;/strong&gt; — 클래스에 어노테이션을 붙이면 자동 등록된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Component&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 일반 컴포넌트&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 서비스 레이어&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Repository&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 데이터 접근 레이어 (예외 변환 기능 추가)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Controller&lt;/span&gt; &lt;span style="color:#75715e"&gt;// Spring MVC 컨트롤러&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;자바 설정&lt;/strong&gt; — 외부 라이브러리처럼 소스코드를 수정할 수 없을 때 사용한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AppConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; PaymentService &lt;span style="color:#a6e22e"&gt;paymentService&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; KakaopayService(); &lt;span style="color:#75715e"&gt;// 구현체를 여기서 결정&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="같은-타입-bean이-여러-개일-때"&gt;&lt;a href="#%ea%b0%99%ec%9d%80-%ed%83%80%ec%9e%85-bean%ec%9d%b4-%ec%97%ac%eb%9f%ac-%ea%b0%9c%ec%9d%bc-%eb%95%8c" class="header-anchor"&gt;&lt;/a&gt;같은 타입 Bean이 여러 개일 때
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;PaymentService&lt;/code&gt; 구현체가 &lt;code&gt;KakaopayService&lt;/code&gt;, &lt;code&gt;NaverPayService&lt;/code&gt; 두 개라면 &lt;code&gt;@Autowired&lt;/code&gt;만으로는 어떤 걸 주입할지 알 수 없다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// @Primary — 기본 Bean 지정&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Primary&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;KakaopayService&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;implements&lt;/span&gt; PaymentService { ... }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// @Qualifier — 이름으로 명시적 선택&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Autowired&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Qualifier&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;naverpay&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; PaymentService paymentService;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="singleton-bean의-함정"&gt;&lt;a href="#singleton-bean%ec%9d%98-%ed%95%a8%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;Singleton Bean의 함정
&lt;/h3&gt;&lt;p&gt;Spring Bean의 기본 스코프는 &lt;strong&gt;Singleton&lt;/strong&gt;이다. Container당 인스턴스가 1개이고, 모든 요청에서 이 인스턴스를 공유한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 위험! 여러 스레드가 이 변수를 동시에 읽고 씀&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; orderCount &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 0;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; orderCount&lt;span style="color:#f92672"&gt;++&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// 스레드 안전하지 않음&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Singleton Bean은 **무상태(stateless)**로 설계해야 한다. 인스턴스 변수에 요청별 데이터를 저장하면 안 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="aop--비즈니스-로직에서-공통-코드를-분리하다"&gt;&lt;a href="#aop--%eb%b9%84%ec%a6%88%eb%8b%88%ec%8a%a4-%eb%a1%9c%ec%a7%81%ec%97%90%ec%84%9c-%ea%b3%b5%ed%86%b5-%ec%bd%94%eb%93%9c%eb%a5%bc-%eb%b6%84%eb%a6%ac%ed%95%98%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;AOP — 비즈니스 로직에서 공통 코드를 분리하다
&lt;/h2&gt;&lt;p&gt;로깅, 트랜잭션, 인증 체크 같은 코드는 특성이 있다. 비즈니스 로직과 무관하지만 여러 곳에 반복해서 등장한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// AOP 없이 — 모든 메서드마다 반복&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Order &lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;(...) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;info&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;createOrder 시작&amp;#34;&lt;/span&gt;); &lt;span style="color:#75715e"&gt;// 로깅&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; checkAuth(); &lt;span style="color:#75715e"&gt;// 인증&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; startTransaction(); &lt;span style="color:#75715e"&gt;// 트랜잭션&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Order order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; doCreateOrder(); &lt;span style="color:#75715e"&gt;// 실제 비즈니스 로직&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; commitTransaction();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;info&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;createOrder 완료&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; order;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;AOP는 이 **횡단 관심사(Cross-cutting Concerns)**를 별도 모듈로 분리해서, 비즈니스 코드가 핵심 로직에만 집중하게 한다.&lt;/p&gt;
&lt;h3 id="프록시가-핵심이다"&gt;&lt;a href="#%ed%94%84%eb%a1%9d%ec%8b%9c%ea%b0%80-%ed%95%b5%ec%8b%ac%ec%9d%b4%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;프록시가 핵심이다
&lt;/h3&gt;&lt;p&gt;Spring AOP는 &lt;strong&gt;프록시 패턴&lt;/strong&gt;으로 동작한다. 실제 Bean을 직접 주입하는 대신, Bean을 감싼 프록시 객체를 주입한다. 메서드 호출이 들어오면 프록시가 먼저 받아서 부가 로직을 실행하고, 실제 Bean으로 넘긴다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;호출자 → [프록시] → 실제 Bean
 ↑
 Advice(부가 로직) 실행
&lt;/code&gt;&lt;/pre&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Aspect&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LoggingAspect&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// service 패키지의 모든 메서드 실행 전후&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Around&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;execution(* com.example.service.*.*(..))&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Object &lt;span style="color:#a6e22e"&gt;around&lt;/span&gt;(ProceedingJoinPoint pjp) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; Throwable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;long&lt;/span&gt; start &lt;span style="color:#f92672"&gt;=&lt;/span&gt; System.&lt;span style="color:#a6e22e"&gt;currentTimeMillis&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Object result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pjp.&lt;span style="color:#a6e22e"&gt;proceed&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// 실제 메서드 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log.&lt;span style="color:#a6e22e"&gt;info&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;{} — {}ms&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pjp.&lt;span style="color:#a6e22e"&gt;getSignature&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;getName&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; System.&lt;span style="color:#a6e22e"&gt;currentTimeMillis&lt;/span&gt;() &lt;span style="color:#f92672"&gt;-&lt;/span&gt; start);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; result;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="transactional도-aop다"&gt;&lt;a href="#transactional%eb%8f%84-aop%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;@Transactional도 AOP다
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;@Transactional&lt;/code&gt;이 붙은 Bean에는 &lt;strong&gt;CGLIB 프록시&lt;/strong&gt;가 주입된다. 트랜잭션 시작/커밋/롤백은 프록시가 처리하고, 실제 메서드는 비즈니스 로직만 담는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Order &lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;(...) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 트랜잭션 begin → 프록시가 처리&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Order order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(...);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; order;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 트랜잭션 commit → 프록시가 처리&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;여기서 하나의 함정이 있다. &lt;strong&gt;같은 클래스 내부에서 &lt;code&gt;@Transactional&lt;/code&gt; 메서드를 직접 호출하면 트랜잭션이 적용되지 않는다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; createOrder(); &lt;span style="color:#75715e"&gt;// 프록시를 거치지 않음 → 트랜잭션 미적용!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;() { ... }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;외부에서 호출할 때만 프록시를 거치기 때문이다. 내부 호출은 &lt;code&gt;this.createOrder()&lt;/code&gt;와 같아서 프록시를 건너뛴다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;Spring의 핵심은 결국 세 가지다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IoC&lt;/strong&gt;: 객체 생성·의존성 연결의 제어권을 Spring에게 넘긴다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DI&lt;/strong&gt;: Spring이 필요한 의존성을 주입해준다. 생성자 주입을 쓰자.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AOP&lt;/strong&gt;: 횡단 관심사를 프록시로 분리한다. &lt;code&gt;@Transactional&lt;/code&gt;이 대표적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 구조를 이해하면 &lt;code&gt;@Transactional&lt;/code&gt; 자기 호출 버그나 Singleton Bean 상태 문제 같은 Spring의 흔한 함정을 미리 피할 수 있다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring MVC — DispatcherServlet 내부 구조와 요청 처리 흐름을 정리한다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #3] Spring MVC 내부 구조 — DispatcherServlet이 요청을 처리하는 방법</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-03-mvc-dispatcherservlet/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-03-mvc-dispatcherservlet/</guid><description>&lt;h2 id="모든-요청이-거치는-하나의-관문"&gt;&lt;a href="#%eb%aa%a8%eb%93%a0-%ec%9a%94%ec%b2%ad%ec%9d%b4-%ea%b1%b0%ec%b9%98%eb%8a%94-%ed%95%98%eb%82%98%ec%9d%98-%ea%b4%80%eb%ac%b8" class="header-anchor"&gt;&lt;/a&gt;모든 요청이 거치는 하나의 관문
&lt;/h2&gt;&lt;p&gt;Spring MVC 애플리케이션에서 HTTP 요청은 예외 없이 &lt;code&gt;DispatcherServlet&lt;/code&gt;을 먼저 통과한다. URL이 &lt;code&gt;/users&lt;/code&gt;든 &lt;code&gt;/orders&lt;/code&gt;든 상관없다. 이 &amp;ldquo;하나의 관문&amp;quot;이 무엇이고, 내부에서 무슨 일이 일어나는지 이해하면 Filter, Interceptor, AOP 중 어디서 로직을 처리해야 하는지가 자연스럽게 보인다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="front-controller-패턴--왜-dispatcherservlet이-필요한가"&gt;&lt;a href="#front-controller-%ed%8c%a8%ed%84%b4--%ec%99%9c-dispatcherservlet%ec%9d%b4-%ed%95%84%ec%9a%94%ed%95%9c%ea%b0%80" class="header-anchor"&gt;&lt;/a&gt;Front Controller 패턴 — 왜 DispatcherServlet이 필요한가
&lt;/h2&gt;&lt;p&gt;Spring MVC 이전에는 URL마다 Servlet을 따로 만들었다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/users → UserServlet
/orders → OrderServlet
/items → ItemServlet
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 방식의 문제는 공통 처리다. 인증 체크, 로깅, 인코딩 설정을 Servlet마다 중복해서 넣어야 했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Front Controller 패턴&lt;/strong&gt;은 모든 요청을 하나의 진입점이 받아 적절한 핸들러에 위임한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;모든 요청 (/*) → DispatcherServlet → Controller 위임
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;공통 로직을 &lt;code&gt;DispatcherServlet&lt;/code&gt; 한 곳에서 처리하니 중복이 사라진다. Spring Boot는 이 &lt;code&gt;DispatcherServlet&lt;/code&gt;을 자동으로 등록해 모든 URL(&lt;code&gt;/&lt;/code&gt;)을 받도록 설정한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="dispatcherservlet-내부-구성요소"&gt;&lt;a href="#dispatcherservlet-%eb%82%b4%eb%b6%80-%ea%b5%ac%ec%84%b1%ec%9a%94%ec%86%8c" class="header-anchor"&gt;&lt;/a&gt;DispatcherServlet 내부 구성요소
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;DispatcherServlet&lt;/code&gt;은 모든 일을 혼자 처리하지 않는다. 역할별 전문 컴포넌트에게 위임한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;DispatcherServlet
 ├─ HandlerMapping : &amp;#34;이 URL → 어떤 Controller?&amp;#34;
 ├─ HandlerAdapter : &amp;#34;이 Controller를 어떻게 실행?&amp;#34;
 ├─ HandlerInterceptor : Controller 실행 전·후 부가 처리
 ├─ ViewResolver : &amp;#34;뷰 이름 → 실제 View 파일?&amp;#34;
 └─ HandlerExceptionResolver : &amp;#34;예외 발생 → 어떻게 응답?&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;HandlerMapping&lt;/strong&gt; — &lt;code&gt;@GetMapping(&amp;quot;/users/{id}&amp;quot;)&lt;/code&gt;처럼 선언된 매핑 정보를 읽어 요청 URL에 맞는 Controller 메서드를 찾는다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HandlerAdapter&lt;/strong&gt; — 찾은 Controller 메서드를 실제로 실행한다. &lt;code&gt;@RequestBody&lt;/code&gt;, &lt;code&gt;@PathVariable&lt;/code&gt;, &lt;code&gt;@ModelAttribute&lt;/code&gt; 파라미터 바인딩도 여기서 처리한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ViewResolver&lt;/strong&gt; — Controller가 문자열 뷰 이름을 반환할 때 실제 파일 경로로 변환한다. &lt;code&gt;@RestController&lt;/code&gt;를 쓰면 이 단계를 건너뛰고 &lt;code&gt;HttpMessageConverter&lt;/code&gt;가 JSON으로 직렬화한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HandlerExceptionResolver&lt;/strong&gt; — &lt;code&gt;@ControllerAdvice&lt;/code&gt; + &lt;code&gt;@ExceptionHandler&lt;/code&gt;가 이 메커니즘으로 동작한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RestControllerAdvice&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;GlobalExceptionHandler&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@ExceptionHandler&lt;/span&gt;(UserNotFoundException.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ResponseEntity&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;String&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;handle&lt;/span&gt;(UserNotFoundException e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; ResponseEntity.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(404).&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;(e.&lt;span style="color:#a6e22e"&gt;getMessage&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;단, Filter에서 발생한 예외는 &lt;code&gt;DispatcherServlet&lt;/code&gt; 밖이라 이 핸들러가 잡지 못한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="요청-처리-12단계-흐름"&gt;&lt;a href="#%ec%9a%94%ec%b2%ad-%ec%b2%98%eb%a6%ac-12%eb%8b%a8%ea%b3%84-%ed%9d%90%eb%a6%84" class="header-anchor"&gt;&lt;/a&gt;요청 처리 12단계 흐름
&lt;/h2&gt;&lt;p&gt;HTTP 요청이 응답까지 가는 전체 흐름이다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;① HTTP 요청 → Tomcat
② Filter Chain (전처리)
③ DispatcherServlet 진입
④ HandlerMapping → 처리할 Controller 메서드 조회
⑤ Interceptor.preHandle() — 순서대로 실행
 (false 반환 시 여기서 중단)
⑥ HandlerAdapter → Controller 메서드 실행
 (AOP 프록시를 통해 Before → 메서드 → After)
⑦ ModelAndView 반환
⑧ Interceptor.postHandle() — 역순으로 실행
⑨ ViewResolver → View 렌더링 (REST면 생략)
⑩ Interceptor.afterCompletion() — 역순, 예외 발생해도 항상 실행
⑪ Filter Chain (후처리, 역순)
⑫ HTTP 응답
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;postHandle()&lt;/code&gt;은 Controller에서 예외가 발생하면 실행되지 않는다. &lt;code&gt;afterCompletion()&lt;/code&gt;은 예외 여부와 무관하게 항상 실행된다. 그래서 실행 시간 측정이나 리소스 해제는 &lt;code&gt;afterCompletion()&lt;/code&gt;에 넣어야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="interceptor--spring-내부에서-요청을-제어하다"&gt;&lt;a href="#interceptor--spring-%eb%82%b4%eb%b6%80%ec%97%90%ec%84%9c-%ec%9a%94%ec%b2%ad%ec%9d%84-%ec%a0%9c%ec%96%b4%ed%95%98%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;Interceptor — Spring 내부에서 요청을 제어하다
&lt;/h2&gt;&lt;p&gt;Interceptor는 &lt;code&gt;DispatcherServlet&lt;/code&gt; 내부, Spring Context 안에서 동작한다. Spring Bean을 주입받을 수 있고, &lt;code&gt;@ControllerAdvice&lt;/code&gt; 예외 처리도 적용된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AuthInterceptor&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;implements&lt;/span&gt; HandlerInterceptor {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;boolean&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;preHandle&lt;/span&gt;(HttpServletRequest request,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HttpServletResponse response,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Object handler) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String token &lt;span style="color:#f92672"&gt;=&lt;/span&gt; request.&lt;span style="color:#a6e22e"&gt;getHeader&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Authorization&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (token &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; response.&lt;span style="color:#a6e22e"&gt;setStatus&lt;/span&gt;(401);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// 여기서 중단, Controller까지 가지 않음&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;afterCompletion&lt;/span&gt;(HttpServletRequest request,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HttpServletResponse response,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Object handler, Exception ex) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 요청 처리 시간 로깅 — 예외가 나도 실행됨&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;URL 패턴으로 적용 범위를 지정할 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;WebConfig&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;implements&lt;/span&gt; WebMvcConfigurer {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;addInterceptors&lt;/span&gt;(InterceptorRegistry registry) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; registry.&lt;span style="color:#a6e22e"&gt;addInterceptor&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; AuthInterceptor())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;addPathPatterns&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/**&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;excludePathPatterns&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/login&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/api/signup&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="filter-vs-interceptor-vs-aop--결론은-레이어-위치"&gt;&lt;a href="#filter-vs-interceptor-vs-aop--%ea%b2%b0%eb%a1%a0%ec%9d%80-%eb%a0%88%ec%9d%b4%ec%96%b4-%ec%9c%84%ec%b9%98" class="header-anchor"&gt;&lt;/a&gt;Filter vs Interceptor vs AOP — 결론은 레이어 위치
&lt;/h2&gt;&lt;p&gt;이 세 가지를 구분하는 가장 명확한 기준은 &lt;strong&gt;어느 레이어에 위치하는가&lt;/strong&gt;다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;구분&lt;/th&gt;
 &lt;th&gt;위치&lt;/th&gt;
 &lt;th&gt;Spring Bean 접근&lt;/th&gt;
 &lt;th&gt;예외 처리&lt;/th&gt;
 &lt;th&gt;적합한 용도&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Filter&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Servlet Container (Spring 밖)&lt;/td&gt;
 &lt;td&gt;제한적&lt;/td&gt;
 &lt;td&gt;직접 처리&lt;/td&gt;
 &lt;td&gt;CORS, 인코딩, XSS 방어&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Interceptor&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Spring MVC (DispatcherServlet 내)&lt;/td&gt;
 &lt;td&gt;가능&lt;/td&gt;
 &lt;td&gt;@ControllerAdvice&lt;/td&gt;
 &lt;td&gt;로그인 체크, URL별 접근 제어&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;AOP&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Spring Bean&lt;/td&gt;
 &lt;td&gt;가능&lt;/td&gt;
 &lt;td&gt;@ControllerAdvice&lt;/td&gt;
 &lt;td&gt;트랜잭션, 실행 시간 측정, 메서드 로깅&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Filter는 Spring Context가 시작되기 전에 동작하므로 Spring Bean 주입이 어렵고, 예외가 발생해도 &lt;code&gt;@ControllerAdvice&lt;/code&gt;가 잡지 못한다. 반면 Interceptor와 AOP는 Spring 안에서 동작하므로 Bean 주입과 통합 예외 처리가 모두 가능하다.&lt;/p&gt;
&lt;p&gt;Spring Security의 필터체인이 Filter 레이어에 있는 것도 이 때문이다. 인증되지 않은 요청은 &lt;code&gt;DispatcherServlet&lt;/code&gt;까지 도달하지 못하고 Filter에서 차단된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="입력값-검증--controller-진입-전-자동-검증"&gt;&lt;a href="#%ec%9e%85%eb%a0%a5%ea%b0%92-%ea%b2%80%ec%a6%9d--controller-%ec%a7%84%ec%9e%85-%ec%a0%84-%ec%9e%90%eb%8f%99-%ea%b2%80%ec%a6%9d" class="header-anchor"&gt;&lt;/a&gt;입력값 검증 — Controller 진입 전 자동 검증
&lt;/h2&gt;&lt;p&gt;Spring MVC는 &lt;code&gt;@Valid&lt;/code&gt;로 Controller 파라미터를 자동 검증한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CreateUserRequest&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@NotBlank&lt;/span&gt;(message &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;이름은 필수입니다&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; String name;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Email&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; String email;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Min&lt;/span&gt;(18)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; age;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RestController&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserController&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@PostMapping&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/users&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ResponseEntity&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;UserDto&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;create&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Valid&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;@RequestBody&lt;/span&gt; CreateUserRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 여기까지 오면 request는 유효한 상태&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; ResponseEntity.&lt;span style="color:#a6e22e"&gt;ok&lt;/span&gt;(userService.&lt;span style="color:#a6e22e"&gt;create&lt;/span&gt;(request));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;검증 실패 시 &lt;code&gt;MethodArgumentNotValidException&lt;/code&gt;이 발생한다. &lt;code&gt;@ControllerAdvice&lt;/code&gt;에서 공통 처리하면 된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RestControllerAdvice&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;GlobalExceptionHandler&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@ExceptionHandler&lt;/span&gt;(MethodArgumentNotValidException.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ResponseEntity&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;ErrorResponse&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;handleValidation&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; MethodArgumentNotValidException e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;String&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; errors &lt;span style="color:#f92672"&gt;=&lt;/span&gt; e.&lt;span style="color:#a6e22e"&gt;getBindingResult&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;getFieldErrors&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;stream&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;map&lt;/span&gt;(err &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; err.&lt;span style="color:#a6e22e"&gt;getField&lt;/span&gt;() &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; err.&lt;span style="color:#a6e22e"&gt;getDefaultMessage&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;toList&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; ResponseEntity.&lt;span style="color:#a6e22e"&gt;badRequest&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; ErrorResponse(&lt;span style="color:#e6db74"&gt;&amp;#34;VALIDATION_FAILED&amp;#34;&lt;/span&gt;, errors));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;DispatcherServlet은 Spring MVC의 모든 요청 처리를 조율하는 중앙 컨트롤러다. 이 구조를 이해하면 Filter와 Interceptor의 차이, &lt;code&gt;@ControllerAdvice&lt;/code&gt;가 Filter 예외를 잡지 못하는 이유, Interceptor 세 메서드의 실행 조건 등이 자연스럽게 설명된다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring Boot — 설정 없이 바로 실행되는 원리(자동설정, 스타터, 내장 서버)를 정리한다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #4] Spring Boot — xml 없이 바로 뜨는 이유, 자동설정 원리 완전 분석</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-04-boot-auto-configuration/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-04-boot-auto-configuration/</guid><description>&lt;h2 id="왜-xml-설정-없이-서버가-뜨는-걸까"&gt;&lt;a href="#%ec%99%9c-xml-%ec%84%a4%ec%a0%95-%ec%97%86%ec%9d%b4-%ec%84%9c%eb%b2%84%ea%b0%80-%eb%9c%a8%eb%8a%94-%ea%b1%b8%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;&amp;ldquo;왜 xml 설정 없이 서버가 뜨는 걸까?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;Spring Boot를 처음 접하면 놀랍다. &lt;code&gt;main()&lt;/code&gt; 메서드 하나, &lt;code&gt;@SpringBootApplication&lt;/code&gt; 어노테이션 하나만 있는데 Tomcat이 뜨고, JPA 연결이 설정되고, JSON 변환까지 된다. 아무것도 설정하지 않았는데.&lt;/p&gt;
&lt;p&gt;이 &amp;ldquo;마법&amp;quot;의 이름은 **자동설정(Auto-configuration)**이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="spring-boot-이전--설정-지옥"&gt;&lt;a href="#spring-boot-%ec%9d%b4%ec%a0%84--%ec%84%a4%ec%a0%95-%ec%a7%80%ec%98%a5" class="header-anchor"&gt;&lt;/a&gt;Spring Boot 이전 — 설정 지옥
&lt;/h2&gt;&lt;p&gt;Spring 단독으로 웹 애플리케이션을 만들려면 설정 파일이 여러 개 필요했다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;web.xml → DispatcherServlet 등록
applicationContext.xml → Bean 설정
dispatcher-servlet.xml → MVC 설정
pom.xml → 의존성 + 버전 수동 관리
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;더 큰 문제는 의존성 버전이었다. Spring 버전에 맞는 Hibernate 버전, 그에 맞는 Jackson 버전&amp;hellip; 개발자가 직접 호환성을 맞춰야 했다. 버전 충돌 오류는 흔한 일이었다.&lt;/p&gt;
&lt;p&gt;Spring Boot는 이 &amp;ldquo;설정 지옥&amp;quot;을 없앤다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="springbootapplication-분해하기"&gt;&lt;a href="#springbootapplication-%eb%b6%84%ed%95%b4%ed%95%98%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;@SpringBootApplication 분해하기
&lt;/h2&gt;&lt;p&gt;Spring Boot 진입점에 붙이는 이 어노테이션은 실제로 세 개의 어노테이션을 합친 것이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@SpringBootApplication&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MyApplication&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;main&lt;/span&gt;(String&lt;span style="color:#f92672"&gt;[]&lt;/span&gt; args) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SpringApplication.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(MyApplication.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;, args);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;@SpringBootApplication
 ├─ @SpringBootConfiguration : @Configuration과 동일 — Bean 설정 클래스
 ├─ @EnableAutoConfiguration : 자동설정 활성화 ← 핵심
 └─ @ComponentScan : 현재 패키지 하위 컴포넌트 자동 스캔
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;핵심은 &lt;code&gt;@EnableAutoConfiguration&lt;/code&gt;이다. 이것이 &amp;ldquo;클래스패스를 보고 Bean을 자동으로 등록&amp;quot;하는 자동설정을 켠다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="자동설정-동작-원리"&gt;&lt;a href="#%ec%9e%90%eb%8f%99%ec%84%a4%ec%a0%95-%eb%8f%99%ec%9e%91-%ec%9b%90%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;자동설정 동작 원리
&lt;/h2&gt;&lt;p&gt;Spring Boot는 애플리케이션이 시작될 때 클래스패스에 어떤 라이브러리가 있는지 확인하고, 그에 맞는 Bean을 자동으로 등록한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;spring-boot-starter-web 의존성 추가
 → 클래스패스에 Tomcat, Spring MVC 라이브러리 존재 확인
 → TomcatAutoConfiguration → 내장 Tomcat Bean 자동 등록
 → DispatcherServletAutoConfiguration → DispatcherServlet Bean 자동 등록
 → 개발자가 아무것도 안 해도 웹서버가 뜸
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;자동설정 클래스들은 어떤 조건에서 Bean을 등록할지 &lt;code&gt;@Conditional&lt;/code&gt; 계열 어노테이션으로 선언한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@ConditionalOnClass&lt;/span&gt;(DataSource.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;) &lt;span style="color:#75715e"&gt;// DataSource 클래스가 클래스패스에 있고&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@ConditionalOnMissingBean&lt;/span&gt;(DataSource.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;) &lt;span style="color:#75715e"&gt;// DataSource Bean이 아직 없을 때만&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;DataSourceAutoConfiguration&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; DataSource &lt;span style="color:#a6e22e"&gt;dataSource&lt;/span&gt;() { ... }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;@ConditionalOnMissingBean&lt;/code&gt;이 핵심이다. &lt;strong&gt;개발자가 직접 Bean을 등록하면 자동설정 Bean은 등록되지 않는다.&lt;/strong&gt; 커스텀 설정이 자동설정을 덮어쓸 수 있는 이유다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;어노테이션&lt;/th&gt;
 &lt;th&gt;조건&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;@ConditionalOnClass&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;특정 클래스가 클래스패스에 있을 때&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;@ConditionalOnMissingBean&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;해당 타입 Bean이 없을 때&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;@ConditionalOnProperty&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;특정 프로퍼티 값이 설정되었을 때&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;@ConditionalOnWebApplication&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;웹 애플리케이션일 때&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;어떤 자동설정이 적용됐는지 보려면 &lt;code&gt;--debug&lt;/code&gt; 플래그로 실행하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="스타터--의존성-버전-충돌-해결"&gt;&lt;a href="#%ec%8a%a4%ed%83%80%ed%84%b0--%ec%9d%98%ec%a1%b4%ec%84%b1-%eb%b2%84%ec%a0%84-%ec%b6%a9%eb%8f%8c-%ed%95%b4%ea%b2%b0" class="header-anchor"&gt;&lt;/a&gt;스타터 — 의존성 버전 충돌 해결
&lt;/h2&gt;&lt;p&gt;스타터는 관련 의존성을 하나의 패키지로 묶어 제공한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- 이것 하나로 Spring MVC + Tomcat + Jackson이 한 번에 --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span style="color:#f92672"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-web&lt;span style="color:#f92672"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;스타터&lt;/th&gt;
 &lt;th&gt;포함 내용&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;starter-web&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Spring MVC, Tomcat, Jackson&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;starter-data-jpa&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Spring Data JPA, Hibernate, JDBC&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;starter-security&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Spring Security&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;starter-test&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;JUnit 5, Mockito, AssertJ&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;starter-validation&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Hibernate Validator, Bean Validation&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;버전은 &lt;code&gt;spring-boot-dependencies&lt;/code&gt; BOM(Bill of Materials)이 관리한다. 개발자는 버전을 명시하지 않아도 Spring Boot가 검증된 호환 버전을 자동으로 선택한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- 버전 명시 불필요 --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.fasterxml.jackson.core&lt;span style="color:#f92672"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;jackson-databind&lt;span style="color:#f92672"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="내장-서버--java--jar-한-줄로-실행"&gt;&lt;a href="#%eb%82%b4%ec%9e%a5-%ec%84%9c%eb%b2%84--java--jar-%ed%95%9c-%ec%a4%84%eb%a1%9c-%ec%8b%a4%ed%96%89" class="header-anchor"&gt;&lt;/a&gt;내장 서버 — java -jar 한 줄로 실행
&lt;/h2&gt;&lt;p&gt;Spring Boot는 Tomcat을 jar 안에 내장한다. 별도 WAS 설치 없이 실행된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;./mvnw package
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;java -jar target/myapp-0.0.1-SNAPSHOT.jar
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tomcat 대신 다른 서버를 쓰고 싶다면 교체할 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- Undertow 사용 (논블로킹, 경량) --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span style="color:#f92672"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-web&lt;span style="color:#f92672"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;exclusions&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;exclusion&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span style="color:#f92672"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-tomcat&lt;span style="color:#f92672"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;/exclusion&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;/exclusions&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span style="color:#f92672"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-undertow&lt;span style="color:#f92672"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="프로파일--환경별-설정-분리"&gt;&lt;a href="#%ed%94%84%eb%a1%9c%ed%8c%8c%ec%9d%bc--%ed%99%98%ea%b2%bd%eb%b3%84-%ec%84%a4%ec%a0%95-%eb%b6%84%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;프로파일 — 환경별 설정 분리
&lt;/h2&gt;&lt;p&gt;개발, 스테이징, 운영 환경마다 DB URL, 로그 레벨, 외부 API 키가 다르다. 프로파일로 분리한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;application.yml # 공통 설정
application-dev.yml # 개발 환경
application-prod.yml # 운영 환경
&lt;/code&gt;&lt;/pre&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# application-dev.yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spring&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;datasource&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;url&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jdbc:h2:mem:testdb &lt;/span&gt; &lt;span style="color:#75715e"&gt;# 개발은 인메모리 DB&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;logging&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;level&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;root&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;DEBUG&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# application-prod.yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spring&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;datasource&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;url&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jdbc:postgresql://prod-db:5432/mydb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;logging&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;level&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;root&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;WARN&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;프로파일 활성화는 실행 시 지정한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;java -jar myapp.jar --spring.profiles.active&lt;span style="color:#f92672"&gt;=&lt;/span&gt;prod
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 또는 환경변수로&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;SPRING_PROFILES_ACTIVE&lt;span style="color:#f92672"&gt;=&lt;/span&gt;prod java -jar myapp.jar
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="설정-우선순위-높을수록-우선"&gt;&lt;a href="#%ec%84%a4%ec%a0%95-%ec%9a%b0%ec%84%a0%ec%88%9c%ec%9c%84-%eb%86%92%ec%9d%84%ec%88%98%eb%a1%9d-%ec%9a%b0%ec%84%a0" class="header-anchor"&gt;&lt;/a&gt;설정 우선순위 (높을수록 우선)
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;1. 커맨드라인 인수 --server.port=9090
2. 환경변수 SERVER_PORT=9090
3. application-{profile}.yml
4. application.yml
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;운영 환경에서 민감 정보(DB 패스워드, API 키)는 환경변수나 외부 설정 관리 시스템으로 주입하는 것이 안전하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="configurationproperties--타입-안전한-설정-바인딩"&gt;&lt;a href="#configurationproperties--%ed%83%80%ec%9e%85-%ec%95%88%ec%a0%84%ed%95%9c-%ec%84%a4%ec%a0%95-%eb%b0%94%ec%9d%b8%eb%94%a9" class="header-anchor"&gt;&lt;/a&gt;@ConfigurationProperties — 타입 안전한 설정 바인딩
&lt;/h2&gt;&lt;p&gt;여러 설정값을 하나의 클래스로 묶을 때 &lt;code&gt;@Value&lt;/code&gt;보다 &lt;code&gt;@ConfigurationProperties&lt;/code&gt;가 낫다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;payment&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api-key&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;abc123&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;timeout&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;5000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;retry-count&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@ConfigurationProperties&lt;/span&gt;(prefix &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;payment&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;PaymentProperties&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; String apiKey;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; timeout;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; retryCount;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// getter/setter&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;@Value(&amp;quot;${payment.api-key}&amp;quot;)&lt;/code&gt;로 하나씩 꺼내는 것보다 타입 안전하고, IDE 자동완성도 지원된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="actuator--운영-모니터링"&gt;&lt;a href="#actuator--%ec%9a%b4%ec%98%81-%eb%aa%a8%eb%8b%88%ed%84%b0%eb%a7%81" class="header-anchor"&gt;&lt;/a&gt;Actuator — 운영 모니터링
&lt;/h2&gt;&lt;p&gt;Actuator를 추가하면 애플리케이션 상태를 HTTP 엔드포인트로 조회할 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span style="color:#f92672"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-actuator&lt;span style="color:#f92672"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;엔드포인트&lt;/th&gt;
 &lt;th&gt;설명&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;/actuator/health&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;앱 상태 (UP/DOWN) — 쿠버네티스 헬스체크에 활용&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;/actuator/metrics&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;JVM 메모리, HTTP 요청 수, 응답 시간 등&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;/actuator/env&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;현재 적용된 설정값 (민감 정보 주의)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;운영 환경에서는 &lt;code&gt;health&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;metrics&lt;/code&gt;만 노출하고 나머지는 차단하는 것이 안전하다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;management&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;endpoints&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;web&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;exposure&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;include&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;health,info,metrics&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;Spring Boot가 &amp;ldquo;설정 없이 동작&amp;quot;하는 이유는 세 가지다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;자동설정&lt;/strong&gt;: 클래스패스를 보고 &lt;code&gt;@Conditional&lt;/code&gt; 조건에 따라 Bean을 자동 등록한다. 개발자 Bean이 있으면 자동설정은 물러난다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스타터 + BOM&lt;/strong&gt;: 관련 의존성을 묶어 제공하고, 버전 충돌을 Spring Boot가 관리한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;내장 서버&lt;/strong&gt;: Tomcat이 jar 안에 들어있어 별도 WAS 설치가 필요 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 편에서는 Spring Transaction — &lt;code&gt;@Transactional&lt;/code&gt;의 동작 원리, 전파속성, 격리수준을 정리한다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #5] @Transactional 완전 정복 — 동작 원리, 전파속성, 격리수준</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-05-transaction/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-05-transaction/</guid><description>&lt;h2 id="transactional을-붙이면-어떻게-트랜잭션이-걸리나"&gt;&lt;a href="#transactional%ec%9d%84-%eb%b6%99%ec%9d%b4%eb%a9%b4-%ec%96%b4%eb%96%bb%ea%b2%8c-%ed%8a%b8%eb%9e%9c%ec%9e%ad%ec%85%98%ec%9d%b4-%ea%b1%b8%eb%a6%ac%eb%82%98" class="header-anchor"&gt;&lt;/a&gt;@Transactional을 붙이면 어떻게 트랜잭션이 걸리나
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;(OrderRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Order(request));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; paymentRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Payment(request));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이 어노테이션 하나로 두 save가 하나의 트랜잭션으로 묶인다. 하나라도 실패하면 둘 다 롤백된다. 어떻게?&lt;/p&gt;
&lt;p&gt;답은 &lt;strong&gt;AOP 프록시&lt;/strong&gt;다. &lt;code&gt;@Transactional&lt;/code&gt;이 붙은 Bean에는 실제 Bean 대신 CGLIB 프록시가 주입된다. 프록시가 메서드 호출을 가로채 트랜잭션 시작/커밋/롤백을 처리하고, 실제 메서드는 비즈니스 로직만 담는다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;호출자 → [CGLIB 프록시]
 ↓
 트랜잭션 begin (Connection.setAutoCommit(false))
 ↓
 실제 메서드 실행
 ↓
 성공 → commit / RuntimeException → rollback
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2 id="롤백-기본-규칙--체크-예외-함정"&gt;&lt;a href="#%eb%a1%a4%eb%b0%b1-%ea%b8%b0%eb%b3%b8-%ea%b7%9c%ec%b9%99--%ec%b2%b4%ed%81%ac-%ec%98%88%ec%99%b8-%ed%95%a8%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;롤백 기본 규칙 — 체크 예외 함정
&lt;/h2&gt;&lt;p&gt;롤백 기본 규칙은 직관에서 벗어나는 경우가 있다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;RuntimeException (unchecked) → 자동 롤백 ✅
Error → 자동 롤백 ✅
CheckedException (checked) → 롤백 안 함 ❌ ← 주의
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;IOException&lt;/code&gt;, &lt;code&gt;SQLException&lt;/code&gt; 같은 체크 예외는 기본적으로 롤백되지 않는다. 초기 설계에서 체크 예외를 &amp;ldquo;복구 가능한 예외&amp;quot;로 간주했기 때문이다.&lt;/p&gt;
&lt;p&gt;실무에서는 명시적으로 지정하거나 런타임 예외로 전환하는 경우가 많다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 모든 예외에서 롤백&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;(rollbackFor &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Exception.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 특정 예외는 롤백 제외&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;(noRollbackFor &lt;span style="color:#f92672"&gt;=&lt;/span&gt; IllegalArgumentException.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="readonly--조회-성능을-높이는-간단한-방법"&gt;&lt;a href="#readonly--%ec%a1%b0%ed%9a%8c-%ec%84%b1%eb%8a%a5%ec%9d%84-%eb%86%92%ec%9d%b4%eb%8a%94-%ea%b0%84%eb%8b%a8%ed%95%9c-%eb%b0%a9%eb%b2%95" class="header-anchor"&gt;&lt;/a&gt;readOnly — 조회 성능을 높이는 간단한 방법
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;(readOnly &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getOrders&lt;/span&gt;() { ... }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;읽기 전용 트랜잭션을 선언하면 두 가지 이점이 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JPA Dirty Checking 비활성화&lt;/strong&gt; — 일반 트랜잭션에서 JPA는 조회한 엔티티의 초기 상태를 스냅샷으로 저장하고, 트랜잭션 종료 시 변경 여부를 비교한다. &lt;code&gt;readOnly = true&lt;/code&gt;면 이 스냅샷 저장 단계가 생략된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;읽기 전용 커넥션 라우팅&lt;/strong&gt; — DB 리플리케이션 환경에서 읽기 전용 커넥션을 replica로 라우팅할 수 있다.&lt;/p&gt;
&lt;p&gt;조회 메서드에는 습관적으로 &lt;code&gt;readOnly = true&lt;/code&gt;를 붙이는 것이 좋다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="전파속성--트랜잭션을-어떻게-이어갈까"&gt;&lt;a href="#%ec%a0%84%ed%8c%8c%ec%86%8d%ec%84%b1--%ed%8a%b8%eb%9e%9c%ec%9e%ad%ec%85%98%ec%9d%84-%ec%96%b4%eb%96%bb%ea%b2%8c-%ec%9d%b4%ec%96%b4%ea%b0%88%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;전파속성 — 트랜잭션을 어떻게 이어갈까
&lt;/h2&gt;&lt;p&gt;전파속성은 &lt;code&gt;@Transactional&lt;/code&gt; 메서드가 &lt;strong&gt;이미 진행 중인 트랜잭션을 어떻게 처리할지&lt;/strong&gt; 결정한다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;전파속성&lt;/th&gt;
 &lt;th&gt;기존 트랜잭션 있을 때&lt;/th&gt;
 &lt;th&gt;기존 트랜잭션 없을 때&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;REQUIRED&lt;/strong&gt; (기본)&lt;/td&gt;
 &lt;td&gt;기존 트랜잭션에 합류&lt;/td&gt;
 &lt;td&gt;새 트랜잭션 생성&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;REQUIRES_NEW&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;기존 일시 중단, 새 트랜잭션 생성&lt;/td&gt;
 &lt;td&gt;새 트랜잭션 생성&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;NESTED&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;중첩 트랜잭션 (savepoint)&lt;/td&gt;
 &lt;td&gt;새 트랜잭션 생성&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="required-vs-requires_new--언제-쓸까"&gt;&lt;a href="#required-vs-requires_new--%ec%96%b8%ec%a0%9c-%ec%93%b8%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;REQUIRED vs REQUIRES_NEW — 언제 쓸까
&lt;/h3&gt;&lt;p&gt;가장 중요한 비교다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt; &lt;span style="color:#75715e"&gt;// REQUIRED (기본)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(...);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; notificationService.&lt;span style="color:#a6e22e"&gt;sendEmail&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// 여기서 예외 → order도 롤백&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;NotificationService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;(propagation &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Propagation.&lt;span style="color:#a6e22e"&gt;REQUIRES_NEW&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sendEmail&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 독립 트랜잭션 — 여기서 예외가 나도 createOrder는 영향 없음&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; emailLogRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(...);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;REQUIRED&lt;/strong&gt;: 하나의 트랜잭션으로 묶인다. 어디서 예외가 나든 전체 롤백. &amp;ldquo;주문 저장 + 이메일 전송이 함께 성공해야 할 때&amp;rdquo; 사용.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;REQUIRES_NEW&lt;/strong&gt;: 완전히 독립된 트랜잭션. &amp;ldquo;이메일 전송 실패해도 주문은 저장되어야 할 때&amp;rdquo; 사용.&lt;/p&gt;
&lt;h3 id="nested-vs-requires_new"&gt;&lt;a href="#nested-vs-requires_new" class="header-anchor"&gt;&lt;/a&gt;NESTED vs REQUIRES_NEW
&lt;/h3&gt;&lt;p&gt;둘 다 &amp;ldquo;부분 롤백&amp;quot;처럼 보이지만 차이가 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;REQUIRES_NEW&lt;/strong&gt;: 부모 트랜잭션과 완전히 독립. 부모가 롤백돼도 이미 커밋된 자식은 유지된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NESTED&lt;/strong&gt;: 부모 트랜잭션 안에서 savepoint를 생성. 자식만 롤백 가능하지만, 부모가 롤백되면 자식도 같이 롤백된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="격리수준--동시성-문제를-얼마나-막을까"&gt;&lt;a href="#%ea%b2%a9%eb%a6%ac%ec%88%98%ec%a4%80--%eb%8f%99%ec%8b%9c%ec%84%b1-%eb%ac%b8%ec%a0%9c%eb%a5%bc-%ec%96%bc%eb%a7%88%eb%82%98-%eb%a7%89%ec%9d%84%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;격리수준 — 동시성 문제를 얼마나 막을까
&lt;/h2&gt;&lt;p&gt;여러 트랜잭션이 동시에 실행될 때 생기는 문제가 세 가지 있다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;문제&lt;/th&gt;
 &lt;th&gt;설명&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Dirty Read&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;커밋되지 않은 다른 트랜잭션의 데이터를 읽음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Non-repeatable Read&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;같은 행을 두 번 조회했는데 값이 다름 (중간에 UPDATE 발생)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Phantom Read&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;같은 조건으로 두 번 조회했는데 행 수가 다름 (중간에 INSERT/DELETE 발생)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;격리수준을 높일수록 이 문제를 방지하지만, 잠금이 늘어나 성능이 떨어진다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;격리수준&lt;/th&gt;
 &lt;th&gt;Dirty Read&lt;/th&gt;
 &lt;th&gt;Non-repeatable Read&lt;/th&gt;
 &lt;th&gt;Phantom Read&lt;/th&gt;
 &lt;th&gt;성능&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;READ_UNCOMMITTED&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;발생&lt;/td&gt;
 &lt;td&gt;발생&lt;/td&gt;
 &lt;td&gt;발생&lt;/td&gt;
 &lt;td&gt;가장 빠름&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;READ_COMMITTED&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;방지&lt;/td&gt;
 &lt;td&gt;발생&lt;/td&gt;
 &lt;td&gt;발생&lt;/td&gt;
 &lt;td&gt;빠름&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;REPEATABLE_READ&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;방지&lt;/td&gt;
 &lt;td&gt;방지&lt;/td&gt;
 &lt;td&gt;발생&lt;/td&gt;
 &lt;td&gt;보통&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;SERIALIZABLE&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;방지&lt;/td&gt;
 &lt;td&gt;방지&lt;/td&gt;
 &lt;td&gt;방지&lt;/td&gt;
 &lt;td&gt;가장 느림&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;실무 기본값:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MySQL InnoDB&lt;/strong&gt;: &lt;code&gt;REPEATABLE_READ&lt;/code&gt; (MVCC로 Phantom Read도 어느 정도 방지)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;: &lt;code&gt;READ_COMMITTED&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;(isolation &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Isolation.&lt;span style="color:#a6e22e"&gt;READ_COMMITTED&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="흔한-함정-3가지"&gt;&lt;a href="#%ed%9d%94%ed%95%9c-%ed%95%a8%ec%a0%95-3%ea%b0%80%ec%a7%80" class="header-anchor"&gt;&lt;/a&gt;흔한 함정 3가지
&lt;/h2&gt;&lt;h3 id="1-자기-호출--가장-자주-빠지는-함정"&gt;&lt;a href="#1-%ec%9e%90%ea%b8%b0-%ed%98%b8%ec%b6%9c--%ea%b0%80%ec%9e%a5-%ec%9e%90%ec%a3%bc-%eb%b9%a0%ec%a7%80%eb%8a%94-%ed%95%a8%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;1. 자기 호출 — 가장 자주 빠지는 함정
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; createOrder(); &lt;span style="color:#75715e"&gt;// this.createOrder() — 프록시를 거치지 않음!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createOrder&lt;/span&gt;() { ... }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;process()&lt;/code&gt;에서 &lt;code&gt;createOrder()&lt;/code&gt;를 직접 호출하면 프록시를 건너뛴다. &lt;code&gt;@Transactional&lt;/code&gt;이 무시된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결책&lt;/strong&gt;: &lt;code&gt;createOrder()&lt;/code&gt;를 별도 클래스로 분리하거나, &lt;code&gt;@Transactional&lt;/code&gt;을 &lt;code&gt;process()&lt;/code&gt;에 붙인다.&lt;/p&gt;
&lt;h3 id="2-private-메서드에는-동작-안-함"&gt;&lt;a href="#2-private-%eb%a9%94%ec%84%9c%eb%93%9c%ec%97%90%eb%8a%94-%eb%8f%99%ec%9e%91-%ec%95%88-%ed%95%a8" class="header-anchor"&gt;&lt;/a&gt;2. private 메서드에는 동작 안 함
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;doSomething&lt;/span&gt;() { ... } &lt;span style="color:#75715e"&gt;// 동작 안 함&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;CGLIB 프록시는 클래스를 상속해서 메서드를 오버라이드한다. &lt;code&gt;private&lt;/code&gt; 메서드는 오버라이드할 수 없으니 프록시가 개입할 수 없다.&lt;/p&gt;
&lt;h3 id="3-전파-함정--unexpectedrollbackexception"&gt;&lt;a href="#3-%ec%a0%84%ed%8c%8c-%ed%95%a8%ec%a0%95--unexpectedrollbackexception" class="header-anchor"&gt;&lt;/a&gt;3. 전파 함정 — UnexpectedRollbackException
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt; &lt;span style="color:#75715e"&gt;// REQUIRED — 새 트랜잭션 생성&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;parent&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; child(); &lt;span style="color:#75715e"&gt;// 예외 catch해도 이미 늦음&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (Exception e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 여기서 잡아도 트랜잭션은 rollback-only 상태&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// → UnexpectedRollbackException 발생&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt; &lt;span style="color:#75715e"&gt;// REQUIRED — 부모에 합류&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;child&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;throw&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; RuntimeException(); &lt;span style="color:#75715e"&gt;// 부모 트랜잭션을 rollback-only로 마킹&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;child()&lt;/code&gt;의 예외가 트랜잭션을 rollback-only로 마킹하고 나면, &lt;code&gt;parent()&lt;/code&gt;에서 catch해도 커밋이 불가능하다. &lt;code&gt;child()&lt;/code&gt;를 &lt;code&gt;REQUIRES_NEW&lt;/code&gt;로 분리하거나, 예외 처리 전략을 명확히 해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;@Transactional&lt;/code&gt;을 제대로 쓰려면 세 가지를 이해해야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;동작 원리&lt;/strong&gt;: AOP 프록시. 외부 호출만 가로채므로 자기 호출, private 메서드에는 동작 안 함.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;전파속성&lt;/strong&gt;: REQUIRED는 운명 공동체, REQUIRES_NEW는 완전 독립. 이메일/알림처럼 부가 작업은 REQUIRES_NEW 고려.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;격리수준&lt;/strong&gt;: 높을수록 안전하지만 성능 저하. DB 기본값(MySQL → REPEATABLE_READ, PostgreSQL → READ_COMMITTED)을 먼저 이해하고 필요할 때만 변경.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 편에서는 Spring Data JPA — 영속성 컨텍스트와 N+1 문제를 정리한다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #6] Java 데이터 접근 기술의 진화 — JDBC에서 JPA까지, 왜 바뀌었나</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-06-data-access-evolution/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-06-data-access-evolution/</guid><description>&lt;h2 id="jpa-vs-mybatis-뭐가-더-낫나요"&gt;&lt;a href="#jpa-vs-mybatis-%eb%ad%90%ea%b0%80-%eb%8d%94-%eb%82%ab%eb%82%98%ec%9a%94" class="header-anchor"&gt;&lt;/a&gt;&amp;ldquo;JPA vs MyBatis 뭐가 더 낫나요?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;이 질문의 답을 제대로 하려면 두 기술이 각각 어떤 문제를 해결하기 위해 등장했는지 알아야 한다. 역사를 따라가면 답이 보인다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="1단계-순수-jdbc--모든-것을-직접"&gt;&lt;a href="#1%eb%8b%a8%ea%b3%84-%ec%88%9c%ec%88%98-jdbc--%eb%aa%a8%eb%93%a0-%ea%b2%83%ec%9d%84-%ec%a7%81%ec%a0%91" class="header-anchor"&gt;&lt;/a&gt;1단계: 순수 JDBC — 모든 것을 직접
&lt;/h2&gt;&lt;p&gt;Java에서 DB에 접근하는 가장 원시적인 방법이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Order &lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(Long id) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Connection conn &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PreparedStatement pstmt &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ResultSet rs &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; conn &lt;span style="color:#f92672"&gt;=&lt;/span&gt; DriverManager.&lt;span style="color:#a6e22e"&gt;getConnection&lt;/span&gt;(URL, USER, PASSWORD);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pstmt &lt;span style="color:#f92672"&gt;=&lt;/span&gt; conn.&lt;span style="color:#a6e22e"&gt;prepareStatement&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;SELECT * FROM orders WHERE id = ?&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pstmt.&lt;span style="color:#a6e22e"&gt;setLong&lt;/span&gt;(1, id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rs &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pstmt.&lt;span style="color:#a6e22e"&gt;executeQuery&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (rs.&lt;span style="color:#a6e22e"&gt;next&lt;/span&gt;()) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Order order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Order();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order.&lt;span style="color:#a6e22e"&gt;setId&lt;/span&gt;(rs.&lt;span style="color:#a6e22e"&gt;getLong&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order.&lt;span style="color:#a6e22e"&gt;setStatus&lt;/span&gt;(rs.&lt;span style="color:#a6e22e"&gt;getString&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; order;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (SQLException e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;throw&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; RuntimeException(e);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;finally&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 안 닫으면 커넥션 누수 — 매 메서드마다 이 코드가 반복&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (rs &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; { rs.&lt;span style="color:#a6e22e"&gt;close&lt;/span&gt;(); } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (SQLException e) {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (pstmt &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; { pstmt.&lt;span style="color:#a6e22e"&gt;close&lt;/span&gt;(); } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (SQLException e) {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (conn &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; { conn.&lt;span style="color:#a6e22e"&gt;close&lt;/span&gt;(); } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (SQLException e) {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이 코드의 문제점이 한눈에 보인다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;커넥션 획득 → 쿼리 실행 → 결과 매핑 → 리소스 반납&lt;/strong&gt; 패턴이 모든 메서드에 반복된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;finally&lt;/code&gt;에서 리소스를 수동으로 닫아야 한다. 빠트리면 커넥션 풀 고갈.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SQLException&lt;/code&gt;이 체크 예외라 모든 메서드가 예외를 처리하거나 throws해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="2단계-jdbctemplate--반복-코드-제거"&gt;&lt;a href="#2%eb%8b%a8%ea%b3%84-jdbctemplate--%eb%b0%98%eb%b3%b5-%ec%bd%94%eb%93%9c-%ec%a0%9c%ea%b1%b0" class="header-anchor"&gt;&lt;/a&gt;2단계: JdbcTemplate — 반복 코드 제거
&lt;/h2&gt;&lt;p&gt;Spring이 JDBC의 반복 코드를 추상화한 것이 &lt;code&gt;JdbcTemplate&lt;/code&gt;이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Repository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderRepository&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; JdbcTemplate jdbcTemplate;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Order &lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(Long id) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; jdbcTemplate.&lt;span style="color:#a6e22e"&gt;queryForObject&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;SELECT * FROM orders WHERE id = ?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (rs, rowNum) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Order order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Order();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order.&lt;span style="color:#a6e22e"&gt;setId&lt;/span&gt;(rs.&lt;span style="color:#a6e22e"&gt;getLong&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order.&lt;span style="color:#a6e22e"&gt;setStatus&lt;/span&gt;(rs.&lt;span style="color:#a6e22e"&gt;getString&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; order;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;커넥션 획득/반납, &lt;code&gt;try-finally&lt;/code&gt;, &lt;code&gt;SQLException&lt;/code&gt; 처리가 사라졌다. Spring이 내부적으로 처리해준다. &lt;code&gt;SQLException&lt;/code&gt;은 Spring의 &lt;code&gt;DataAccessException&lt;/code&gt;(런타임 예외)으로 변환된다.&lt;/p&gt;
&lt;p&gt;하지만 SQL은 여전히 직접 작성해야 하고, 결과를 객체에 매핑하는 &lt;code&gt;RowMapper&lt;/code&gt; 코드도 직접 써야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="3단계-mybatis--sql을-코드에서-분리"&gt;&lt;a href="#3%eb%8b%a8%ea%b3%84-mybatis--sql%ec%9d%84-%ec%bd%94%eb%93%9c%ec%97%90%ec%84%9c-%eb%b6%84%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;3단계: MyBatis — SQL을 코드에서 분리
&lt;/h2&gt;&lt;p&gt;MyBatis는 SQL을 XML 파일로 분리하고, 결과 매핑을 자동화한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- OrderMapper.xml --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;mapper&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;namespace=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;com.example.mapper.OrderMapper&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;resultMap&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;orderResultMap&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Order&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;id&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;property=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;id&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;column=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;result&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;property=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;status&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;column=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;result&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;property=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;memberId&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;column=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;member_id&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;/resultMap&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;select&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;findById&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;resultMap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;orderResultMap&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SELECT id, status, member_id FROM orders WHERE id = #{id}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/mapper&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Mapper&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderMapper&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Order &lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(Long id); &lt;span style="color:#75715e"&gt;// 인터페이스만 선언하면 구현체 자동 생성&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MyBatis의 진짜 강점은 &lt;strong&gt;동적 쿼리&lt;/strong&gt;다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;select&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;findByCondition&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;resultMap=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;orderResultMap&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SELECT * FROM orders
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;where&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;status != null&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;AND status = #{status}&lt;span style="color:#f92672"&gt;&amp;lt;/if&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;memberId != null&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;AND member_id = #{memberId}&lt;span style="color:#f92672"&gt;&amp;lt;/if&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;/where&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;조건에 따라 WHERE절을 동적으로 구성하는 것이 직관적이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="4단계-jpa--sql을-아예-작성하지-않는다"&gt;&lt;a href="#4%eb%8b%a8%ea%b3%84-jpa--sql%ec%9d%84-%ec%95%84%ec%98%88-%ec%9e%91%ec%84%b1%ed%95%98%ec%a7%80-%ec%95%8a%eb%8a%94%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;4단계: JPA — SQL을 아예 작성하지 않는다
&lt;/h2&gt;&lt;p&gt;JPA(ORM)는 SQL을 직접 작성하지 않고, &lt;strong&gt;객체 간 관계를 그대로 DB에 매핑&lt;/strong&gt;한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Entity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Order&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Id&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;@GeneratedValue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; Long id;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@ManyToOne&lt;/span&gt;(fetch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; FetchType.&lt;span style="color:#a6e22e"&gt;LAZY&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@JoinColumn&lt;/span&gt;(name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;member_id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; Member member;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// SQL 없이 CRUD&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;orderRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(order); &lt;span style="color:#75715e"&gt;// INSERT 자동&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;orderRepository.&lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(id); &lt;span style="color:#75715e"&gt;// SELECT 자동&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;order.&lt;span style="color:#a6e22e"&gt;setStatus&lt;/span&gt;(COMPLETED); &lt;span style="color:#75715e"&gt;// UPDATE 자동 (Dirty Checking)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;orderRepository.&lt;span style="color:#a6e22e"&gt;delete&lt;/span&gt;(order); &lt;span style="color:#75715e"&gt;// DELETE 자동&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;DB 벤더가 바뀌어도 코드 변경이 없다. 객체 관계를 그대로 코드로 표현할 수 있다.&lt;/p&gt;
&lt;p&gt;단점도 있다. 학습 곡선이 높고(영속성 컨텍스트, 지연 로딩, N+1 등), 복잡한 집계·통계 쿼리는 JPQL이나 QueryDSL이 필요하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="4가지-기술-한눈에-비교"&gt;&lt;a href="#4%ea%b0%80%ec%a7%80-%ea%b8%b0%ec%88%a0-%ed%95%9c%eb%88%88%ec%97%90-%eb%b9%84%ea%b5%90" class="header-anchor"&gt;&lt;/a&gt;4가지 기술 한눈에 비교
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;기준&lt;/th&gt;
 &lt;th&gt;JDBC&lt;/th&gt;
 &lt;th&gt;JdbcTemplate&lt;/th&gt;
 &lt;th&gt;MyBatis&lt;/th&gt;
 &lt;th&gt;JPA&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;SQL 작성&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;직접&lt;/td&gt;
 &lt;td&gt;직접&lt;/td&gt;
 &lt;td&gt;직접 (XML)&lt;/td&gt;
 &lt;td&gt;자동 생성&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;결과 매핑&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;수동&lt;/td&gt;
 &lt;td&gt;RowMapper&lt;/td&gt;
 &lt;td&gt;자동 (resultMap)&lt;/td&gt;
 &lt;td&gt;자동&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;복잡한 쿼리&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;자유롭게&lt;/td&gt;
 &lt;td&gt;자유롭게&lt;/td&gt;
 &lt;td&gt;자유롭게 (동적 쿼리 강점)&lt;/td&gt;
 &lt;td&gt;JPQL/QueryDSL 필요&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;DB 종속성&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;높음&lt;/td&gt;
 &lt;td&gt;높음&lt;/td&gt;
 &lt;td&gt;높음&lt;/td&gt;
 &lt;td&gt;낮음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;학습 난이도&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;낮음&lt;/td&gt;
 &lt;td&gt;낮음&lt;/td&gt;
 &lt;td&gt;중간&lt;/td&gt;
 &lt;td&gt;높음&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;생산성&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;낮음&lt;/td&gt;
 &lt;td&gt;중간&lt;/td&gt;
 &lt;td&gt;중간&lt;/td&gt;
 &lt;td&gt;높음 (단순 CRUD)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="jpa-vs-mybatis--이분법이-아니다"&gt;&lt;a href="#jpa-vs-mybatis--%ec%9d%b4%eb%b6%84%eb%b2%95%ec%9d%b4-%ec%95%84%eb%8b%88%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;JPA vs MyBatis — 이분법이 아니다
&lt;/h2&gt;&lt;p&gt;&amp;ldquo;JPA vs MyBatis&amp;quot;를 선택의 문제로 보는 건 틀린 프레임이다. 실무에서는 함께 쓰는 경우도 많고, 상황에 따라 선택이 다르다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JPA가 유리한 경우&lt;/strong&gt;: 비즈니스 로직이 복잡하고 객체 중심 설계가 중요한 서비스, 단순 CRUD가 많은 경우.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MyBatis가 유리한 경우&lt;/strong&gt;: 복잡한 통계·집계 쿼리가 많은 경우, DBA와 협업하며 SQL을 직접 관리해야 하는 경우, 레거시 DB 스키마에 맞춰야 하는 경우.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실무에서 가장 많이 쓰는 조합&lt;/strong&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;단순 CRUD → JPA Repository
복잡한 조회 → QueryDSL (타입 안전한 JPQL 빌더)
통계·집계 쿼리 → Native Query 또는 MyBatis (별도 모듈)
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2 id="jdbctemplate은-여전히-쓴다"&gt;&lt;a href="#jdbctemplate%ec%9d%80-%ec%97%ac%ec%a0%84%ed%9e%88-%ec%93%b4%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;JdbcTemplate은 여전히 쓴다
&lt;/h2&gt;&lt;p&gt;JPA 프로젝트에서도 JdbcTemplate이 필요한 상황이 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Repository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderBatchRepository&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; JdbcTemplate jdbcTemplate;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 수백만 건 상태 일괄 변경 — JPA Dirty Checking으로 하면 메모리 부족&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bulkUpdateStatus&lt;/span&gt;(String oldStatus, String newStatus) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; jdbcTemplate.&lt;span style="color:#a6e22e"&gt;update&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;UPDATE orders SET status = ? WHERE status = ?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; newStatus, oldStatus
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;JPA &lt;code&gt;@Modifying&lt;/code&gt;으로 안 되는 벌크 업데이트, Spring Batch 처리, 간단한 유틸리티 쿼리 등에서 여전히 활용된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;각 기술의 진화 방향은 &lt;strong&gt;반복 코드 제거 → SQL 관리 편의 → 객체 중심 개발&lt;/strong&gt;이었다. 이 흐름을 이해하면 &amp;ldquo;왜 JPA를 쓰는가&amp;quot;와 &amp;ldquo;언제 MyBatis가 더 나은가&amp;quot;를 맥락 있게 설명할 수 있다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring Data JPA — 영속성 컨텍스트와 N+1 문제를 자세히 정리한다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #7] Spring Data JPA — 영속성 컨텍스트와 N+1 문제 완전 정리</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-07-data-jpa-persistence-context/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-07-data-jpa-persistence-context/</guid><description>&lt;h2 id="jpa-면접--영속성-컨텍스트--n1"&gt;&lt;a href="#jpa-%eb%a9%b4%ec%a0%91--%ec%98%81%ec%86%8d%ec%84%b1-%ec%bb%a8%ed%85%8d%ec%8a%a4%ed%8a%b8--n1" class="header-anchor"&gt;&lt;/a&gt;JPA 면접 = 영속성 컨텍스트 + N+1
&lt;/h2&gt;&lt;p&gt;JPA 관련 면접 질문은 결국 두 가지로 수렴한다. &amp;ldquo;영속성 컨텍스트가 무엇인지 설명해보세요&amp;quot;와 &amp;ldquo;N+1 문제가 무엇이고 어떻게 해결하나요?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;이 두 가지를 이해하면 Dirty Checking, 지연 로딩, LazyInitializationException, N+1이 모두 연결된 하나의 그림으로 보인다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="jpa--hibernate--spring-data-jpa-관계"&gt;&lt;a href="#jpa--hibernate--spring-data-jpa-%ea%b4%80%ea%b3%84" class="header-anchor"&gt;&lt;/a&gt;JPA / Hibernate / Spring Data JPA 관계
&lt;/h2&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;JPA : Java ORM 표준 인터페이스 (명세)
Hibernate : JPA의 대표 구현체 (Spring Boot 기본)
Spring Data JPA : JPA를 더 편리하게 추상화한 Spring 모듈

개발자 코드 → Spring Data JPA → JPA → Hibernate → JDBC → DB
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2 id="영속성-컨텍스트--jpa의-핵심"&gt;&lt;a href="#%ec%98%81%ec%86%8d%ec%84%b1-%ec%bb%a8%ed%85%8d%ec%8a%a4%ed%8a%b8--jpa%ec%9d%98-%ed%95%b5%ec%8b%ac" class="header-anchor"&gt;&lt;/a&gt;영속성 컨텍스트 — JPA의 핵심
&lt;/h2&gt;&lt;p&gt;영속성 컨텍스트는 &lt;strong&gt;Entity 객체를 관리하는 1차 저장소&lt;/strong&gt;다. 트랜잭션당 하나씩 생성된다.&lt;/p&gt;
&lt;h3 id="entity-생명주기"&gt;&lt;a href="#entity-%ec%83%9d%eb%aa%85%ec%a3%bc%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;Entity 생명주기
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;비영속 : new Order() → 컨텍스트와 무관한 일반 객체
영속 : save() 또는 조회 후 → 컨텍스트가 관리, 변경 감지 적용
준영속 : 트랜잭션 종료 → 컨텍스트가 더 이상 관리 안 함
삭제 : delete() → DELETE 예약
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="1차-캐시"&gt;&lt;a href="#1%ec%b0%a8-%ec%ba%90%ec%8b%9c" class="header-anchor"&gt;&lt;/a&gt;1차 캐시
&lt;/h3&gt;&lt;p&gt;같은 트랜잭션 안에서 같은 Entity를 두 번 조회하면 DB를 두 번 치지 않는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Order order1 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(1L); &lt;span style="color:#75715e"&gt;// DB 조회&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Order order2 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(1L); &lt;span style="color:#75715e"&gt;// 1차 캐시에서 반환 — DB 조회 없음&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;order1 &lt;span style="color:#f92672"&gt;==&lt;/span&gt; order2 &lt;span style="color:#75715e"&gt;// true — 동일 객체 보장&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="dirty-checking--save-없이-update"&gt;&lt;a href="#dirty-checking--save-%ec%97%86%ec%9d%b4-update" class="header-anchor"&gt;&lt;/a&gt;Dirty Checking — save() 없이 UPDATE
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;updateOrder&lt;/span&gt;(Long id, String newStatus) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Order order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(id).&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// 영속 상태&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order.&lt;span style="color:#a6e22e"&gt;setStatus&lt;/span&gt;(newStatus); &lt;span style="color:#75715e"&gt;// setter만 호출&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// save() 없어도 트랜잭션 종료 시 UPDATE 자동 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;영속성 컨텍스트는 Entity를 조회할 때 스냅샷을 저장한다. 트랜잭션 종료 시 현재 상태와 스냅샷을 비교해 변경이 있으면 UPDATE를 자동으로 실행한다. 이것이 Dirty Checking이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;readOnly = true&lt;/code&gt; 트랜잭션에서는 스냅샷을 저장하지 않아 성능이 향상된다. 조회 메서드에 &lt;code&gt;@Transactional(readOnly = true)&lt;/code&gt;를 붙이는 이유다.&lt;/p&gt;
&lt;h3 id="지연-쓰기-write-behind"&gt;&lt;a href="#%ec%a7%80%ec%97%b0-%ec%93%b0%ea%b8%b0-write-behind" class="header-anchor"&gt;&lt;/a&gt;지연 쓰기 (Write-behind)
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;saveOrders&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(order1); &lt;span style="color:#75715e"&gt;// INSERT 예약&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(order2); &lt;span style="color:#75715e"&gt;// INSERT 예약&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;(order3); &lt;span style="color:#75715e"&gt;// INSERT 예약&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 트랜잭션 커밋 시 INSERT 3개 한번에 실행&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;쿼리를 모아뒀다가 커밋 직전에 한번에 DB로 보내 네트워크 왕복 횟수를 줄인다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="지연-로딩--필요할-때만-조회"&gt;&lt;a href="#%ec%a7%80%ec%97%b0-%eb%a1%9c%eb%94%a9--%ed%95%84%ec%9a%94%ed%95%a0-%eb%95%8c%eb%a7%8c-%ec%a1%b0%ed%9a%8c" class="header-anchor"&gt;&lt;/a&gt;지연 로딩 — 필요할 때만 조회
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Entity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Order&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@ManyToOne&lt;/span&gt;(fetch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; FetchType.&lt;span style="color:#a6e22e"&gt;LAZY&lt;/span&gt;) &lt;span style="color:#75715e"&gt;// 지연 로딩&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@JoinColumn&lt;/span&gt;(name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;member_id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; Member member;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;FetchType.LAZY&lt;/code&gt;는 Order를 조회해도 Member는 즉시 가져오지 않는다. &lt;code&gt;order.getMember()&lt;/code&gt;를 호출하는 순간 DB 조회가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;기본값 주의&lt;/strong&gt;: &lt;code&gt;@ManyToOne&lt;/code&gt;, &lt;code&gt;@OneToOne&lt;/code&gt;의 기본값은 &lt;code&gt;EAGER&lt;/code&gt;(즉시 로딩)다. 필요한 연관 데이터를 항상 함께 가져와 N+1을 유발할 수 있다. &lt;strong&gt;항상 LAZY로 명시적으로 변경하는 것을 권장한다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="lazyinitializationexception"&gt;&lt;a href="#lazyinitializationexception" class="header-anchor"&gt;&lt;/a&gt;LazyInitializationException
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Transactional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Order &lt;span style="color:#a6e22e"&gt;getOrder&lt;/span&gt;(Long id) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;findById&lt;/span&gt;(id).&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#75715e"&gt;// 트랜잭션 종료 → 영속성 컨텍스트 닫힘&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 컨트롤러에서&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Order order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; orderService.&lt;span style="color:#a6e22e"&gt;getOrder&lt;/span&gt;(1L);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;order.&lt;span style="color:#a6e22e"&gt;getMember&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;getName&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// LazyInitializationException!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;트랜잭션이 끝나면 영속성 컨텍스트가 닫힌다. 이 상태에서 LAZY 로딩을 시도하면 컨텍스트가 없어 예외가 발생한다. 해결책은 트랜잭션 안에서 필요한 데이터를 미리 로딩하거나 Fetch Join을 쓰는 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="n1-문제--jpa의-가장-흔한-함정"&gt;&lt;a href="#n1-%eb%ac%b8%ec%a0%9c--jpa%ec%9d%98-%ea%b0%80%ec%9e%a5-%ed%9d%94%ed%95%9c-%ed%95%a8%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;N+1 문제 — JPA의 가장 흔한 함정
&lt;/h2&gt;&lt;h3 id="원인"&gt;&lt;a href="#%ec%9b%90%ec%9d%b8" class="header-anchor"&gt;&lt;/a&gt;원인
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; orders &lt;span style="color:#f92672"&gt;=&lt;/span&gt; orderRepository.&lt;span style="color:#a6e22e"&gt;findAll&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// 쿼리 1번 (N개 Order 조회)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (Order order : orders) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; System.&lt;span style="color:#a6e22e"&gt;out&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;println&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;getMember&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;getName&lt;/span&gt;()); &lt;span style="color:#75715e"&gt;// 주문마다 Member 조회 N번&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 총 1 + N번 쿼리&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Order가 100개면 쿼리가 101번 실행된다. 이것이 N+1 문제다.&lt;/p&gt;
&lt;h3 id="해결-방법-1-fetch-join"&gt;&lt;a href="#%ed%95%b4%ea%b2%b0-%eb%b0%a9%eb%b2%95-1-fetch-join" class="header-anchor"&gt;&lt;/a&gt;해결 방법 1: Fetch Join
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Query&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;SELECT o FROM Order o JOIN FETCH o.member&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findAllWithMember&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- 실행 쿼리 (1번)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; o.&lt;span style="color:#f92672"&gt;*&lt;/span&gt;, m.&lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; orders o &lt;span style="color:#66d9ef"&gt;INNER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;JOIN&lt;/span&gt; member m &lt;span style="color:#66d9ef"&gt;ON&lt;/span&gt; o.member_id &lt;span style="color:#f92672"&gt;=&lt;/span&gt; m.id
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Order와 Member를 JOIN 한 번으로 가져온다. 단, &lt;code&gt;@OneToMany&lt;/code&gt; 컬렉션 Fetch Join + 페이징은 메모리에서 처리하므로 위험하다.&lt;/p&gt;
&lt;h3 id="해결-방법-2-entitygraph"&gt;&lt;a href="#%ed%95%b4%ea%b2%b0-%eb%b0%a9%eb%b2%95-2-entitygraph" class="header-anchor"&gt;&lt;/a&gt;해결 방법 2: @EntityGraph
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EntityGraph&lt;/span&gt;(attributePaths &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;member&amp;#34;&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Query&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;SELECT o FROM Order o&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findAllWithMember&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Fetch Join과 동일하게 동작하지만 어노테이션으로 간결하게 표현.&lt;/p&gt;
&lt;h3 id="해결-방법-3-batchsize-컬렉션--페이징"&gt;&lt;a href="#%ed%95%b4%ea%b2%b0-%eb%b0%a9%eb%b2%95-3-batchsize-%ec%bb%ac%eb%a0%89%ec%85%98--%ed%8e%98%ec%9d%b4%ec%a7%95" class="header-anchor"&gt;&lt;/a&gt;해결 방법 3: @BatchSize (컬렉션 + 페이징)
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@BatchSize&lt;/span&gt;(size &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 100)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@OneToMany&lt;/span&gt;(mappedBy &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;order&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderItem&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; items;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- N번 쿼리 대신 IN절로 묶어서 실행
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; order_item &lt;span style="color:#66d9ef"&gt;WHERE&lt;/span&gt; order_id &lt;span style="color:#66d9ef"&gt;IN&lt;/span&gt; (&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;, ..., &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;default_batch_fetch_size&lt;/code&gt;를 전역 설정하면 모든 컬렉션에 적용된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spring&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;jpa&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;properties&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hibernate&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;default_batch_fetch_size&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;컬렉션 페이징 + 연관 데이터 조회&lt;/strong&gt;에 가장 실용적인 해결책이다.&lt;/p&gt;
&lt;h3 id="선택-기준"&gt;&lt;a href="#%ec%84%a0%ed%83%9d-%ea%b8%b0%ec%a4%80" class="header-anchor"&gt;&lt;/a&gt;선택 기준
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;ToOne 관계 (ManyToOne, OneToOne) → Fetch Join / @EntityGraph
컬렉션 + 페이징 필요 → @BatchSize (전역 설정 권장)
복잡한 조회 → QueryDSL + Fetch Join
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2 id="연관관계-주인--mappedby-규칙"&gt;&lt;a href="#%ec%97%b0%ea%b4%80%ea%b4%80%ea%b3%84-%ec%a3%bc%ec%9d%b8--mappedby-%ea%b7%9c%ec%b9%99" class="header-anchor"&gt;&lt;/a&gt;연관관계 주인 — mappedBy 규칙
&lt;/h2&gt;&lt;p&gt;양방향 관계에서 외래 키를 관리하는 쪽이 &lt;strong&gt;연관관계의 주인&lt;/strong&gt;이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Order (주인) — @JoinColumn 있음, 외래 키 실제 관리&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@ManyToOne&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@JoinColumn&lt;/span&gt;(name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;member_id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; Member member;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Member (주인 아님) — mappedBy로 읽기 전용 선언&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@OneToMany&lt;/span&gt;(mappedBy &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;member&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; orders &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; ArrayList&lt;span style="color:#f92672"&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;주인에게만 값을 설정해야 DB에 반영된다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;order.&lt;span style="color:#a6e22e"&gt;setMember&lt;/span&gt;(member); &lt;span style="color:#75715e"&gt;// DB에 반영됨 ← 주인&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;member.&lt;span style="color:#a6e22e"&gt;getOrders&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;add&lt;/span&gt;(order); &lt;span style="color:#75715e"&gt;// DB 반영 안 됨 (읽기 전용)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="실무-활용--entity-auditing"&gt;&lt;a href="#%ec%8b%a4%eb%ac%b4-%ed%99%9c%ec%9a%a9--entity-auditing" class="header-anchor"&gt;&lt;/a&gt;실무 활용 — Entity Auditing
&lt;/h2&gt;&lt;p&gt;생성일시·수정일시를 자동 관리한다. 거의 모든 Entity에 적용하는 패턴이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@MappedSuperclass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EntityListeners&lt;/span&gt;(AuditingEntityListener.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;abstract&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BaseEntity&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@CreatedDate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Column&lt;/span&gt;(updatable &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; LocalDateTime createdAt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@LastModifiedDate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; LocalDateTime updatedAt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Entity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Order&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;extends&lt;/span&gt; BaseEntity { ... }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;@SpringBootApplication&lt;/code&gt;에 &lt;code&gt;@EnableJpaAuditing&lt;/code&gt;을 추가하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;영속성 컨텍스트를 이해하면 JPA의 동작 방식이 논리적으로 연결된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dirty Checking&lt;/strong&gt;: 스냅샷 비교 → save() 없이 UPDATE&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1차 캐시&lt;/strong&gt;: 같은 트랜잭션 내 동일 Entity 재조회 최적화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;N+1&lt;/strong&gt;: LAZY 로딩이 반복 호출될 때 발생 → Fetch Join, @BatchSize로 해결&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LazyInitializationException&lt;/strong&gt;: 트랜잭션 밖에서 LAZY 로딩 시도 → 트랜잭션 안에서 미리 로딩&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 편에서는 Spring 쿼리 기술 — JPQL, Criteria API, QueryDSL을 비교한다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #8] QueryDSL이 실무 표준인 이유 — JPQL의 한계와 타입 안전한 동적 쿼리</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-08-querydsl/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-08-querydsl/</guid><description>&lt;h2 id="jpql의-불편함--오타가-런타임에-터진다"&gt;&lt;a href="#jpql%ec%9d%98-%eb%b6%88%ed%8e%b8%ed%95%a8--%ec%98%a4%ed%83%80%ea%b0%80-%eb%9f%b0%ed%83%80%ec%9e%84%ec%97%90-%ed%84%b0%ec%a7%84%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;JPQL의 불편함 — 오타가 런타임에 터진다
&lt;/h2&gt;&lt;p&gt;JPA Repository의 쿼리 메서드로 해결되지 않는 복잡한 조회는 &lt;code&gt;@Query&lt;/code&gt;로 JPQL을 직접 작성한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Query&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;SELECT o FROM Order o WHERE o.member.id = :memberId AND o.status = :status&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findByMemberAndStatus&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;@Param&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;memberId&amp;#34;&lt;/span&gt;) Long memberId,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Param&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;) OrderStatus status);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이 방식의 문제가 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;컴파일 타임에 오류를 잡을 수 없다.&lt;/strong&gt; &lt;code&gt;o.member.id&lt;/code&gt;를 &lt;code&gt;o.member.idx&lt;/code&gt;로 오타를 내도 컴파일은 된다. 런타임에 쿼리가 실행되는 순간에야 오류가 난다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;동적 쿼리가 불편하다.&lt;/strong&gt; 조건이 있을 수도 없을 수도 있는 검색 기능을 JPQL로 만들려면 조건마다 쿼리 문자열을 조합해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;리팩터링에 취약하다.&lt;/strong&gt; 엔티티 필드명을 바꾸면 문자열 안의 JPQL도 직접 찾아서 수정해야 한다.&lt;/p&gt;
&lt;p&gt;QueryDSL은 이 문제를 해결한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="querydsl이란"&gt;&lt;a href="#querydsl%ec%9d%b4%eb%9e%80" class="header-anchor"&gt;&lt;/a&gt;QueryDSL이란
&lt;/h2&gt;&lt;p&gt;QueryDSL은 Java 코드로 SQL처럼 읽히는, 타입 안전한 쿼리를 작성하게 해주는 라이브러리다.&lt;/p&gt;
&lt;p&gt;빌드 시 엔티티 클래스로부터 &lt;strong&gt;Q클래스를 자동 생성&lt;/strong&gt;한다. &lt;code&gt;Order&lt;/code&gt; 엔티티가 있으면 &lt;code&gt;QOrder&lt;/code&gt;가 생성된다. Q클래스의 필드는 엔티티 필드와 1:1 대응한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;QOrder order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; QOrder.&lt;span style="color:#a6e22e"&gt;order&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;QMember member &lt;span style="color:#f92672"&gt;=&lt;/span&gt; QMember.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 자바 코드로 쿼리를 작성 — 컴파일 타임 타입 체크&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;queryFactory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;selectFrom&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;, member).&lt;span style="color:#a6e22e"&gt;fetchJoin&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;eq&lt;/span&gt;(OrderStatus.&lt;span style="color:#a6e22e"&gt;COMPLETED&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;orderBy&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;createdAt&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;desc&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;order.status.eq(OrderStatus.COMPLETED)&lt;/code&gt; — &lt;code&gt;order.status&lt;/code&gt;는 Q클래스의 타입 안전한 필드다. 오타를 내면 컴파일 오류가 발생한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="기본-설정"&gt;&lt;a href="#%ea%b8%b0%eb%b3%b8-%ec%84%a4%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;기본 설정
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.querydsl&lt;span style="color:#f92672"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;querydsl-jpa&lt;span style="color:#f92672"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;classifier&amp;gt;&lt;/span&gt;jakarta&lt;span style="color:#f92672"&gt;&amp;lt;/classifier&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;QueryDslConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; JPAQueryFactory &lt;span style="color:#a6e22e"&gt;jpaQueryFactory&lt;/span&gt;(EntityManager em) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; JPAQueryFactory(em);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="동적-쿼리--querydsl의-핵심-강점"&gt;&lt;a href="#%eb%8f%99%ec%a0%81-%ec%bf%bc%eb%a6%ac--querydsl%ec%9d%98-%ed%95%b5%ec%8b%ac-%ea%b0%95%ec%a0%90" class="header-anchor"&gt;&lt;/a&gt;동적 쿼리 — QueryDSL의 핵심 강점
&lt;/h2&gt;&lt;p&gt;검색 조건이 선택적으로 적용되는 동적 쿼리가 QueryDSL의 진짜 강점이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Repository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderQueryRepository&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; JPAQueryFactory queryFactory;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findByCondition&lt;/span&gt;(OrderSearchCondition cond) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; queryFactory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;selectFrom&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;, member).&lt;span style="color:#a6e22e"&gt;fetchJoin&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; statusEq(cond.&lt;span style="color:#a6e22e"&gt;getStatus&lt;/span&gt;()),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; memberIdEq(cond.&lt;span style="color:#a6e22e"&gt;getMemberId&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// BooleanExpression — null 반환 시 WHERE 조건에서 자동 제외&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; BooleanExpression &lt;span style="color:#a6e22e"&gt;statusEq&lt;/span&gt;(OrderStatus status) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; status &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt; &lt;span style="color:#f92672"&gt;?&lt;/span&gt; order.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;eq&lt;/span&gt;(status) : &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; BooleanExpression &lt;span style="color:#a6e22e"&gt;memberIdEq&lt;/span&gt;(Long memberId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; memberId &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt; &lt;span style="color:#f92672"&gt;?&lt;/span&gt; order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;eq&lt;/span&gt;(memberId) : &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;BooleanExpression&lt;/code&gt;을 반환하는 메서드로 분리하면 두 가지 이점이 있다. null을 반환하면 WHERE 조건에서 자동으로 제외된다. 그리고 이 조건 메서드를 여러 쿼리에서 &lt;strong&gt;재사용&lt;/strong&gt;할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="dto-직접-조회--필요한-필드만"&gt;&lt;a href="#dto-%ec%a7%81%ec%a0%91-%ec%a1%b0%ed%9a%8c--%ed%95%84%ec%9a%94%ed%95%9c-%ed%95%84%eb%93%9c%eb%a7%8c" class="header-anchor"&gt;&lt;/a&gt;DTO 직접 조회 — 필요한 필드만
&lt;/h2&gt;&lt;p&gt;엔티티 전체가 아닌 필요한 컬럼만 DTO로 조회할 때 &lt;code&gt;@QueryProjection&lt;/code&gt;을 사용한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@QueryProjection&lt;/span&gt; &lt;span style="color:#75715e"&gt;// DTO 생성자에 붙이면 QOrderSummary 자동 생성&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderSummary&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; Long orderId;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; String memberName;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; OrderStatus status;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@QueryProjection&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderSummary&lt;/span&gt;(Long orderId, String memberName, OrderStatus status) { ... }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderSummary&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findSummary&lt;/span&gt;(OrderStatus status) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; queryFactory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;select&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; QOrderSummary(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;from&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;, member)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(statusEq(status))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;DTO 생성자 파라미터도 타입 안전하게 체크된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="페이징-최적화"&gt;&lt;a href="#%ed%8e%98%ec%9d%b4%ec%a7%95-%ec%b5%9c%ec%a0%81%ed%99%94" class="header-anchor"&gt;&lt;/a&gt;페이징 최적화
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Page&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderSummary&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findSummaryPage&lt;/span&gt;(OrderStatus status, Pageable pageable) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderSummary&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; content &lt;span style="color:#f92672"&gt;=&lt;/span&gt; queryFactory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;select&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; QOrderSummary(order.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;, order.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;from&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;, member)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(statusEq(status))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;offset&lt;/span&gt;(pageable.&lt;span style="color:#a6e22e"&gt;getOffset&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;limit&lt;/span&gt;(pageable.&lt;span style="color:#a6e22e"&gt;getPageSize&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// count 쿼리 분리 (최적화)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; JPAQuery&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Long&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; countQuery &lt;span style="color:#f92672"&gt;=&lt;/span&gt; queryFactory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;select&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;count&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;from&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(statusEq(status));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; PageableExecutionUtils.&lt;span style="color:#a6e22e"&gt;getPage&lt;/span&gt;(content, pageable, countQuery::fetchOne);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;PageableExecutionUtils.getPage()&lt;/code&gt;는 마지막 페이지이거나 첫 페이지에서 결과가 pageSize보다 적으면 count 쿼리를 실행하지 않는다. 불필요한 COUNT 쿼리를 줄이는 최적화다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="repository-구조-패턴"&gt;&lt;a href="#repository-%ea%b5%ac%ec%a1%b0-%ed%8c%a8%ed%84%b4" class="header-anchor"&gt;&lt;/a&gt;Repository 구조 패턴
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 1. 기본 JPA Repository + 커스텀 인터페이스&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderRepository&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;extends&lt;/span&gt; JpaRepository&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order, Long&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; OrderRepositoryCustom { }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 2. 커스텀 인터페이스 선언&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderRepositoryCustom&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderSummary&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findByCondition&lt;/span&gt;(OrderSearchCondition cond);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 3. QueryDSL 구현체&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Repository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderRepositoryImpl&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;implements&lt;/span&gt; OrderRepositoryCustom {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; JPAQueryFactory queryFactory;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// QueryDSL 쿼리 구현&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;단순 CRUD는 &lt;code&gt;JpaRepository&lt;/code&gt;가, 복잡한 조회는 &lt;code&gt;OrderRepositoryImpl&lt;/code&gt;이 담당하는 역할 분리다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="언제-무엇을-쓸까"&gt;&lt;a href="#%ec%96%b8%ec%a0%9c-%eb%ac%b4%ec%97%87%ec%9d%84-%ec%93%b8%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;언제 무엇을 쓸까
&lt;/h2&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;단순 조회 → JPA 쿼리 메서드 (findByStatus, findByMemberId)
조건 1~2개 → @Query (JPQL)
동적 쿼리 / 복잡한 조인 / DTO 조회 → QueryDSL ← 실무 표준
DB 특화 함수 / 성능 힌트 → Native Query
대용량 통계·배치 → MyBatis (별도 모듈)
&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;기준&lt;/th&gt;
 &lt;th&gt;JPQL&lt;/th&gt;
 &lt;th&gt;QueryDSL&lt;/th&gt;
 &lt;th&gt;Native Query&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;타입 안전성&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;❌ 문자열&lt;/td&gt;
 &lt;td&gt;✅ Q클래스&lt;/td&gt;
 &lt;td&gt;❌ 문자열&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;동적 쿼리&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;불편&lt;/td&gt;
 &lt;td&gt;✅ BooleanExpression&lt;/td&gt;
 &lt;td&gt;불편&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;DB 특화 기능&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;❌&lt;/td&gt;
 &lt;td&gt;❌&lt;/td&gt;
 &lt;td&gt;✅&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;DTO 조회&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;new 키워드 (불편)&lt;/td&gt;
 &lt;td&gt;✅ @QueryProjection&lt;/td&gt;
 &lt;td&gt;수동 매핑&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;QueryDSL이 실무 표준이 된 이유는 명확하다. JPQL의 문자열 기반 한계를 타입 안전한 Java 코드로 대체하고, 동적 쿼리를 &lt;code&gt;BooleanExpression&lt;/code&gt; 패턴으로 깔끔하게 처리한다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring Security — 인증/인가와 JWT 구현을 정리한다.&lt;/p&gt;</description></item><item><title>[Spring 완전 정복 #9] Spring Security + JWT 구현 — 인증 흐름부터 토큰 검증까지</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-09-security-jwt/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-09-security-jwt/</guid><description>&lt;h2 id="인증과-인가--순서가-있다"&gt;&lt;a href="#%ec%9d%b8%ec%a6%9d%ea%b3%bc-%ec%9d%b8%ea%b0%80--%ec%88%9c%ec%84%9c%ea%b0%80-%ec%9e%88%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;인증과 인가 — 순서가 있다
&lt;/h2&gt;&lt;p&gt;Spring Security를 다루기 전에 개념을 먼저 정리한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;인증 (Authentication) : &amp;#34;당신이 누구인지&amp;#34; 확인 → 로그인
인가 (Authorization) : &amp;#34;당신이 무엇을 할 수 있는지&amp;#34; 확인 → 권한 체크
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;순서는 항상 &lt;strong&gt;인증 → 인가&lt;/strong&gt;다. 누구인지 확인도 안 된 상태에서 권한을 체크할 수 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="spring-security-전체-구조"&gt;&lt;a href="#spring-security-%ec%a0%84%ec%b2%b4-%ea%b5%ac%ec%a1%b0" class="header-anchor"&gt;&lt;/a&gt;Spring Security 전체 구조
&lt;/h2&gt;&lt;p&gt;HTTP 요청이 Controller에 닿기 전에 Spring Security의 필터 체인이 먼저 처리한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;HTTP 요청
 ↓
DelegatingFilterProxy ← Servlet Container에 등록된 진입점
 ↓
FilterChainProxy ← Spring Security 필터 체인 관리
 ↓
SecurityFilterChain ← 실제 보안 필터들
 ├── JwtAuthenticationFilter (JWT 검증 — 커스텀)
 ├── UsernamePasswordAuthenticationFilter (폼 로그인)
 ├── ExceptionTranslationFilter (인증/인가 예외 처리)
 └── FilterSecurityInterceptor (URL 기반 인가)
 ↓
DispatcherServlet
 ↓
Controller
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;DelegatingFilterProxy&lt;/code&gt;는 Servlet Container 레벨의 Filter이지만 내부적으로 Spring Bean인 &lt;code&gt;FilterChainProxy&lt;/code&gt;에 처리를 위임한다. Servlet Container와 Spring Context를 연결하는 다리 역할이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="securityfilterchain-설정-spring-boot-3x"&gt;&lt;a href="#securityfilterchain-%ec%84%a4%ec%a0%95-spring-boot-3x" class="header-anchor"&gt;&lt;/a&gt;SecurityFilterChain 설정 (Spring Boot 3.x)
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;WebSecurityConfigurerAdapter&lt;/code&gt;는 deprecated됐다. Spring Boot 3.x에서는 &lt;code&gt;SecurityFilterChain&lt;/code&gt; Bean을 직접 등록한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EnableWebSecurity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EnableMethodSecurity&lt;/span&gt; &lt;span style="color:#75715e"&gt;// @PreAuthorize 활성화&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;SecurityConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; SecurityFilterChain &lt;span style="color:#a6e22e"&gt;filterChain&lt;/span&gt;(HttpSecurity http) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; Exception {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; http
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;csrf&lt;/span&gt;(csrf &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; csrf.&lt;span style="color:#a6e22e"&gt;disable&lt;/span&gt;()) &lt;span style="color:#75715e"&gt;// JWT + Stateless → CSRF 불필요&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;sessionManagement&lt;/span&gt;(session &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; session.&lt;span style="color:#a6e22e"&gt;sessionCreationPolicy&lt;/span&gt;(SessionCreationPolicy.&lt;span style="color:#a6e22e"&gt;STATELESS&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;authorizeHttpRequests&lt;/span&gt;(auth &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; auth
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;requestMatchers&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/auth/**&amp;#34;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;permitAll&lt;/span&gt;() &lt;span style="color:#75715e"&gt;// 로그인, 회원가입&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;requestMatchers&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/admin/**&amp;#34;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;hasRole&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;ADMIN&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;requestMatchers&lt;/span&gt;(HttpMethod.&lt;span style="color:#a6e22e"&gt;GET&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/api/posts/**&amp;#34;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;permitAll&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;anyRequest&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;authenticated&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;addFilterBefore&lt;/span&gt;(jwtAuthenticationFilter,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; UsernamePasswordAuthenticationFilter.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;exceptionHandling&lt;/span&gt;(ex &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; ex
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;authenticationEntryPoint&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; HttpStatusEntryPoint(HttpStatus.&lt;span style="color:#a6e22e"&gt;UNAUTHORIZED&lt;/span&gt;)));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; http.&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; PasswordEncoder &lt;span style="color:#a6e22e"&gt;passwordEncoder&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; BCryptPasswordEncoder();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="폼-로그인-인증-흐름"&gt;&lt;a href="#%ed%8f%bc-%eb%a1%9c%ea%b7%b8%ec%9d%b8-%ec%9d%b8%ec%a6%9d-%ed%9d%90%eb%a6%84" class="header-anchor"&gt;&lt;/a&gt;폼 로그인 인증 흐름
&lt;/h2&gt;&lt;p&gt;로그인 요청이 처리되는 전체 흐름이다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[1] POST /login (username, password)
[2] UsernamePasswordAuthenticationFilter → 미인증 토큰 생성
[3] AuthenticationManager (ProviderManager) → 인증 Provider 목록 순회
[4] DaoAuthenticationProvider → UserDetailsService 호출
[5] UserDetailsService → DB 조회 → UserDetails 반환
[6] PasswordEncoder.matches(입력PW, 저장된해시PW) 검증
[7] 인증 성공 → Authentication 객체 생성 → SecurityContext 저장
&lt;/code&gt;&lt;/pre&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CustomUserDetailsService&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;implements&lt;/span&gt; UserDetailsService {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; MemberRepository memberRepository;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; UserDetails &lt;span style="color:#a6e22e"&gt;loadUserByUsername&lt;/span&gt;(String username) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Member member &lt;span style="color:#f92672"&gt;=&lt;/span&gt; memberRepository.&lt;span style="color:#a6e22e"&gt;findByEmail&lt;/span&gt;(username)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;orElseThrow&lt;/span&gt;(() &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; UsernameNotFoundException(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;사용자를 찾을 수 없습니다: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; username));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; User.&lt;span style="color:#a6e22e"&gt;builder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;username&lt;/span&gt;(member.&lt;span style="color:#a6e22e"&gt;getEmail&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;password&lt;/span&gt;(member.&lt;span style="color:#a6e22e"&gt;getPassword&lt;/span&gt;()) &lt;span style="color:#75715e"&gt;// BCrypt 해시&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;roles&lt;/span&gt;(member.&lt;span style="color:#a6e22e"&gt;getRole&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;()) &lt;span style="color:#75715e"&gt;// &amp;#34;USER&amp;#34; → &amp;#34;ROLE_USER&amp;#34; 자동 변환&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="jwt-인증-구현"&gt;&lt;a href="#jwt-%ec%9d%b8%ec%a6%9d-%ea%b5%ac%ed%98%84" class="header-anchor"&gt;&lt;/a&gt;JWT 인증 구현
&lt;/h2&gt;&lt;h3 id="토큰-생성검증"&gt;&lt;a href="#%ed%86%a0%ed%81%b0-%ec%83%9d%ec%84%b1%ea%b2%80%ec%a6%9d" class="header-anchor"&gt;&lt;/a&gt;토큰 생성/검증
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;JwtTokenProvider&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Value&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;${jwt.secret}&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; String secretKey;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Value&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;${jwt.expiration}&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;long&lt;/span&gt; expiration;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; String &lt;span style="color:#a6e22e"&gt;generateToken&lt;/span&gt;(Authentication authentication) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Jwts.&lt;span style="color:#a6e22e"&gt;builder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;setSubject&lt;/span&gt;(authentication.&lt;span style="color:#a6e22e"&gt;getName&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;setIssuedAt&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Date())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;setExpiration&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Date(System.&lt;span style="color:#a6e22e"&gt;currentTimeMillis&lt;/span&gt;() &lt;span style="color:#f92672"&gt;+&lt;/span&gt; expiration))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;signWith&lt;/span&gt;(getSigningKey(), SignatureAlgorithm.&lt;span style="color:#a6e22e"&gt;HS256&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;compact&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; String &lt;span style="color:#a6e22e"&gt;getUsernameFromToken&lt;/span&gt;(String token) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Jwts.&lt;span style="color:#a6e22e"&gt;parserBuilder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;setSigningKey&lt;/span&gt;(getSigningKey())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;parseClaimsJws&lt;/span&gt;(token)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;getBody&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;getSubject&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;boolean&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;validateToken&lt;/span&gt;(String token) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Jwts.&lt;span style="color:#a6e22e"&gt;parserBuilder&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;setSigningKey&lt;/span&gt;(getSigningKey()).&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;parseClaimsJws&lt;/span&gt;(token);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (ExpiredJwtException e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;throw&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; CustomException(ErrorCode.&lt;span style="color:#a6e22e"&gt;TOKEN_EXPIRED&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (JwtException e) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;throw&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; CustomException(ErrorCode.&lt;span style="color:#a6e22e"&gt;INVALID_TOKEN&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; Key &lt;span style="color:#a6e22e"&gt;getSigningKey&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Keys.&lt;span style="color:#a6e22e"&gt;hmacShaKeyFor&lt;/span&gt;(secretKey.&lt;span style="color:#a6e22e"&gt;getBytes&lt;/span&gt;(StandardCharsets.&lt;span style="color:#a6e22e"&gt;UTF_8&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="jwt-인증-필터"&gt;&lt;a href="#jwt-%ec%9d%b8%ec%a6%9d-%ed%95%84%ed%84%b0" class="header-anchor"&gt;&lt;/a&gt;JWT 인증 필터
&lt;/h3&gt;&lt;p&gt;매 요청마다 토큰을 검증하고 SecurityContext에 인증 정보를 설정한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;JwtAuthenticationFilter&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;extends&lt;/span&gt; OncePerRequestFilter {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; JwtTokenProvider tokenProvider;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; CustomUserDetailsService userDetailsService;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;doFilterInternal&lt;/span&gt;(HttpServletRequest request,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HttpServletResponse response,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; FilterChain filterChain)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; ServletException, IOException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String token &lt;span style="color:#f92672"&gt;=&lt;/span&gt; resolveToken(request);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (StringUtils.&lt;span style="color:#a6e22e"&gt;hasText&lt;/span&gt;(token) &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; tokenProvider.&lt;span style="color:#a6e22e"&gt;validateToken&lt;/span&gt;(token)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String username &lt;span style="color:#f92672"&gt;=&lt;/span&gt; tokenProvider.&lt;span style="color:#a6e22e"&gt;getUsernameFromToken&lt;/span&gt;(token);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; UserDetails userDetails &lt;span style="color:#f92672"&gt;=&lt;/span&gt; userDetailsService.&lt;span style="color:#a6e22e"&gt;loadUserByUsername&lt;/span&gt;(username);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; UsernamePasswordAuthenticationToken authentication &lt;span style="color:#f92672"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; UsernamePasswordAuthenticationToken(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; userDetails, &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;, userDetails.&lt;span style="color:#a6e22e"&gt;getAuthorities&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SecurityContextHolder.&lt;span style="color:#a6e22e"&gt;getContext&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;setAuthentication&lt;/span&gt;(authentication);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; filterChain.&lt;span style="color:#a6e22e"&gt;doFilter&lt;/span&gt;(request, response);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; String &lt;span style="color:#a6e22e"&gt;resolveToken&lt;/span&gt;(HttpServletRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String bearerToken &lt;span style="color:#f92672"&gt;=&lt;/span&gt; request.&lt;span style="color:#a6e22e"&gt;getHeader&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Authorization&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (StringUtils.&lt;span style="color:#a6e22e"&gt;hasText&lt;/span&gt;(bearerToken) &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bearerToken.&lt;span style="color:#a6e22e"&gt;startsWith&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Bearer &amp;#34;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; bearerToken.&lt;span style="color:#a6e22e"&gt;substring&lt;/span&gt;(7);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;OncePerRequestFilter&lt;/code&gt;를 상속하면 같은 요청에서 필터가 한 번만 실행됨을 보장한다.&lt;/p&gt;
&lt;h3 id="로그인-api"&gt;&lt;a href="#%eb%a1%9c%ea%b7%b8%ec%9d%b8-api" class="header-anchor"&gt;&lt;/a&gt;로그인 API
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RestController&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RequestMapping&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/auth&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AuthController&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@PostMapping&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ResponseEntity&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;TokenResponse&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;login&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;@RequestBody&lt;/span&gt; LoginRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Authentication authentication &lt;span style="color:#f92672"&gt;=&lt;/span&gt; authenticationManager.&lt;span style="color:#a6e22e"&gt;authenticate&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; UsernamePasswordAuthenticationToken(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; request.&lt;span style="color:#a6e22e"&gt;getEmail&lt;/span&gt;(), request.&lt;span style="color:#a6e22e"&gt;getPassword&lt;/span&gt;()));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String token &lt;span style="color:#f92672"&gt;=&lt;/span&gt; tokenProvider.&lt;span style="color:#a6e22e"&gt;generateToken&lt;/span&gt;(authentication);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; ResponseEntity.&lt;span style="color:#a6e22e"&gt;ok&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; TokenResponse(token));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="인가--메서드-수준-보안"&gt;&lt;a href="#%ec%9d%b8%ea%b0%80--%eb%a9%94%ec%84%9c%eb%93%9c-%ec%88%98%ec%a4%80-%eb%b3%b4%ec%95%88" class="header-anchor"&gt;&lt;/a&gt;인가 — 메서드 수준 보안
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;@EnableMethodSecurity&lt;/code&gt;를 활성화하면 메서드에 직접 권한 조건을 선언할 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@PreAuthorize&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;hasRole(&amp;#39;ADMIN&amp;#39;)&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@GetMapping&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/admin/users&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;UserDto&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getAllUsers&lt;/span&gt;() { ... }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 본인 또는 ADMIN만 접근&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@PreAuthorize&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;hasRole(&amp;#39;ADMIN&amp;#39;) or #userId == authentication.principal.id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@GetMapping&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/users/{userId}&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; UserDto &lt;span style="color:#a6e22e"&gt;getUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;@PathVariable&lt;/span&gt; Long userId) { ... }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;SpEL(Spring Expression Language)을 지원하므로 &lt;code&gt;@Secured&lt;/code&gt;보다 표현력이 좋다.&lt;/p&gt;
&lt;p&gt;현재 로그인한 사용자 정보는 &lt;code&gt;@AuthenticationPrincipal&lt;/code&gt;로 바로 받을 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@GetMapping&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/me&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; UserDto &lt;span style="color:#a6e22e"&gt;getMyProfile&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;@AuthenticationPrincipal&lt;/span&gt; UserDetails userDetails) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String username &lt;span style="color:#f92672"&gt;=&lt;/span&gt; userDetails.&lt;span style="color:#a6e22e"&gt;getUsername&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="bcrypt--패스워드-암호화"&gt;&lt;a href="#bcrypt--%ed%8c%a8%ec%8a%a4%ec%9b%8c%eb%93%9c-%ec%95%94%ed%98%b8%ed%99%94" class="header-anchor"&gt;&lt;/a&gt;BCrypt — 패스워드 암호화
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; PasswordEncoder &lt;span style="color:#a6e22e"&gt;passwordEncoder&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; BCryptPasswordEncoder(); &lt;span style="color:#75715e"&gt;// 기본 strength: 10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 회원가입 시&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;String encoded &lt;span style="color:#f92672"&gt;=&lt;/span&gt; passwordEncoder.&lt;span style="color:#a6e22e"&gt;encode&lt;/span&gt;(rawPassword);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 로그인 시 검증&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;boolean&lt;/span&gt; matches &lt;span style="color:#f92672"&gt;=&lt;/span&gt; passwordEncoder.&lt;span style="color:#a6e22e"&gt;matches&lt;/span&gt;(rawPassword, encodedPassword);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;BCrypt는 동일한 평문을 암호화해도 매번 다른 해시가 나온다(salt 자동 포함). 역산이 불가능하고, 검증은 반드시 &lt;code&gt;matches()&lt;/code&gt;로 해야 한다. 해시값을 직접 비교하면 항상 false다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="refresh-token-패턴"&gt;&lt;a href="#refresh-token-%ed%8c%a8%ed%84%b4" class="header-anchor"&gt;&lt;/a&gt;Refresh Token 패턴
&lt;/h2&gt;&lt;p&gt;Access Token만 사용하면 만료 시 재로그인이 필요하다. Refresh Token 패턴으로 해결한다.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Access Token : 짧은 만료 (15분~1시간) — 탈취 위험 최소화
Refresh Token : 긴 만료 (7~30일) — DB/Redis에 저장

[1] 로그인 → Access Token + Refresh Token 발급
[2] API 요청 → Access Token 헤더에 담아 전송
[3] Access Token 만료 → Refresh Token으로 재발급 요청
[4] 서버: DB에서 Refresh Token 검증 → 유효하면 새 Access Token 발급
[5] 로그아웃 → DB에서 Refresh Token 삭제
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Access Token은 Stateless라 서버에서 강제 무효화가 불가능하다. Refresh Token을 DB에 저장하면 로그아웃과 강제 만료 처리가 가능해진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;Spring Security는 Filter 레이어에서 동작한다. Controller에 닿기 전에 인증/인가가 처리되는 구조다. JWT 방식의 핵심은 &lt;code&gt;JwtAuthenticationFilter&lt;/code&gt;가 매 요청마다 토큰을 검증하고 &lt;code&gt;SecurityContextHolder&lt;/code&gt;에 인증 정보를 저장하는 것이다. &lt;code&gt;@PreAuthorize&lt;/code&gt;는 AOP로 메서드 단위 인가를 처리한다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring 예외 처리 — &lt;code&gt;@ControllerAdvice&lt;/code&gt;와 커스텀 예외 계층 설계를 정리한다.&lt;/p&gt;</description></item></channel></rss>