<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Propagation on kastori</title><link>http://blog.kastori.dev/tags/propagation/</link><description>Recent content in Propagation 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/propagation/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>