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