2025. 11. 10. 08:02ㆍDev./Spring
영속성 컨텍스트
데이터베이스와 애플리케이션 사이에 JPA가 만든 임시 저장소(캐시)
영속성 컨텍스트의 역할
| 역할 | 설명 | 이해를 위한 비유 |
| ① 1차 캐시 | 같은 엔티티를 두 번 조회해도 DB에 다시 안 감 | 한 번 불러온 책은 책상 위에 둔다 |
| ② 변경 감지 (Dirty Checking) | 엔티티의 필드가 바뀌면 자동으로 UPDATE 감지 | 책을 수정하면 포스트잇으로 표시해둔다 |
| ③ 쓰기 지연 (Flush 시점에 DB 반영) | INSERT, UPDATE, DELETE가 트랜잭션 끝날 때 실행됨 | 숙제는 한 번에 제출한다 |
- DB 접근을 최소화한 ORM이 효율적인 ORM
생명주기에 따른 4가지 상태
| 상태 | 설명 | 예시 |
| 비영속 (new) | 아직 JPA에 저장 안 됨 | new Todo("공부") |
| 영속 (managed) | EntityManager에 저장되어 관리 중 | em.persist(todo) or repository.save(todo) |
| 준영속 (detached) | 한때 관리됐지만 지금은 연결 끊김 | 트랜잭션 끝나거나 em.detach() |
| 삭제 (removed) | 삭제 예약됨 | em.remove(todo) 후 flush 시 DB 반영 |
생명주기 흐름
- 트랜잭션이 시작되면, 영속성 컨텍스트가 새로 만들어짐
- find()나 save()로 엔티티를 등록하면, 컨텍스트에 저장
- 같은 엔티티를 또 조회해도, DB 대신 캐시(1차 캐시)에서 반환
- 엔티티의 필드가 바뀌면 JPA가 변경을 감지하고 기록
- 커밋 시점(flush)에 변경사항을 모아 SQL로 한 번에 DB에 반영
왜 스프링 프로젝트의 대부분의 트러블 슈팅의 원인이 영속성 컨텍스트일까?
- 영속성 컨텍스트는 JPA의 모든 자동화가 일어나는 중심축이기 때문 , 데이터 변경·조회·삭제, 캐싱, 트랜잭션, flush 등 굉장히 많은 기능을 다루는 만큼 문제도 가장 많이 생김
- JPA의 장점은 SQL을 몰라도 개발자가 CRUD가 가능하다는 것 → 문제는 눈에보이지 않기 때문에 영속성 컨텍스트 동작 타이밍과 연관이 깊음
- 쿼리 타이밍 : 분명 save()가 없는데 왜 저장될까?, update안했는데 왜 값이 바뀌어? → 다 영속성 컨텍스트 동작 타이밍문제
- 1차 캐시로 데이터 불일치 : DB 바꿨는데 반영이 안되네 → 영속성 컨텍스트의 특징 캐시 때문
- N+1 : 하나 조회하는데 쿼리가 두번이 나가네 → 언제 DB를 접근할지 모르기 때문, 컨텍스트 fetch 시점 문제
영속성 컨텍스트와 트랜잭션의 관계
⭐️ 영속성 컨텍스트는 트랜잭션 단위로 만들고 사라짐
- 트랜잭션 시작 → 영속성 컨텍스트 생성 ↓ CRUD (엔티티 상태 관리) ↓ flush → commit → close()
JPA 내부의 흐름
- @Transactional 트랜잭션 시작
- 새로운 영속성 컨텍스트를 생성한다. ( 생성의 단위가 트랜잭션)
- 엔티티 조회 or 등록
- DB에서 데이터를 가져와 영속성 컨텍스트 내부 1차 캐시에 저장
- 엔티티 변경
- DB 쿼리 안날림. 영속성 컨텍스트 내부에 Dirty로 구분
- 커밋 시점
- JPA가 Dirty 들을 찾아 flush() → SQL 생성 및 실행 → 커밋
- 트랜잭션 종료
- 영속성 컨텍스트 즉 임시저장소가 닫히며 캐시 삭제, 엔티티도 준 영속 상태가 됨
그래서 왜 트랜잭션이 중요한걸까?
- JPA는 단독으로 존재 할 수 없음. 항상 트랜잭션 안에서 생성되고 소멸
- ⭐️⭐️⭐️트랜잭션이 영속성 컨텍스트의 수명임⭐️⭐️⭐️
- JPA 내부 흐름이 생명주기 흐름과 같은 이유
| 상황 | 영속성 컨텍스트 상태 |
| 트랜잭션 안 | 활성 (1차 캐시 유지, 변경 감지 작동) |
| 트랜잭션 끝남 | 소멸 (준영속 상태 전환, 캐시 초기화) |
요약
트랜잭션 동안만 임시저장소에 있다가 커밋할때 한번에 DB 반영
- 트랜잭션 = 영속성 컨텍스트의 수명
- flush = DB로 실제 반영
- commit = flush + 트랜잭션 종료
@Transactional
public void example() {
Schedule schedule = new Schedule("공부");
scheduleRepository.save(schedule); // INSERT 안 나감
schedule.setTitle("복습"); // UPDATE 안 나감
} // ✅ 트랜잭션 끝날 때 INSERT + UPDATE 실행됨 (flush + commit)
그럼 영속성 컨텍스트가 왜 그렇게 중요한걸까?
영속성 컨텍스트를 이해하지 못하면 JPA 사용할때 raw SQL 쓰는 것만 못하다는 것과 훨씬 복잡하고 어려울거라는 이야기를 들었다.
영속성 컨텍스트 이해 못하고 JPA 사용 == 나 너가 통제 불가능한 버그 발생할께! 나 찾아바! 야근 좋아!
1. 데이터 일관성의 중심
DB단과 앱단 사이의 중간이라 영속성 컨텍스트가 없으면 JPA는 그저 SQL 실행 도구
영속성 컨텍스트의 흐름(생명주기 흐름) 덕분에 단순 ORM이 아니라 데이터 일관성 시스템으로서 사용할 수 있음
2. SQL로는 불가능한 객체 단위 개발 가능
이전에는 개발자가 raw SQL 직접 날려야했었음, 객체 변환도 직접 작성
ResultSet rs = stmt.executeQuery("SELECT * FROM schedule");
Schedule schedule = new Schedule(rs.getString("title"));
JPA는
Schedule schedule = scheduleRepository.findById(1L).get();
schedule.setTitle("할일 완료");
이게 가능한 이유가 영속성 컨텍스트가 Schedule 객체의 생명주기를 추적해 커밋시점에 알아서 update 쿼리를 날려주기 때문
즉, 개발자는 객체만 다룰 수 있게 됨 DB 상태를 알아서 맞춰줌
하지만 알아서 맞춰주기때문에 영속성 컨텍스트를 모르면 SQL 실행 타이밍을 예측 할 수 없음. 우리가 설계한 대로 코드가 실행되지 않을 수 있음 → 그래서 JPA를 모르면 명확하게 SQL날리는게 더 쉬울거같다는 이야기가 나온거같음
3. 성능과 안정성의 핵심 최적화
DB접근을 최소화해야 효율적임
4. 캐시,트랜잭션,성능 최적화등 전략이 꼬임
JPA의 대부분 성능 최적화 == 영속성 컨텍스트의 캐시를 얼마나 잘 활용할 수 있는가
예시
- 1차 캐시 → 중복 조회 방지
- Dirty Checking → 자동 변경 감지
- Flush 타이밍 → SQL 배치 최적화
- Fetch 전략 → N+1 방지
이전 N+1문제가 발생했던것도 영속성 컨텍스트 개념을 제대로 잡지 못했기 때문
영속성 컨텍스트 실습
package com.todolist;
import com.todolist.entity.Todo;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class JpaConsoleTest implements CommandLineRunner {
@PersistenceContext
private EntityManager em;
@Override
@Transactional
public void run(String... args) throws Exception {
System.out.println("\n================= JPA 영속성 컨텍스트 실험 =================");
// 1.비영속 상태
Todo todo = new Todo("영속성 컨텍스트 테스트", "persist 전 상태", "diy0ung", "1234");
System.out.println("[비영속] em.contains(todo)? → " + em.contains(todo));
// 2.영속 상태로 전환
em.persist(todo);
System.out.println("[영속] em.contains(todo)? → " + em.contains(todo));
System.out.println("[영속] 아직 INSERT SQL 안 나감 (flush 전)");
// 3.flush 실행 (쓰기 지연 SQL 실행)
em.flush();
System.out.println("[flush 이후] INSERT SQL 실행됨");
//4.1차 캐시 확인 (같은 ID로 조회 시, DB 접근 안 함)
Todo find1 = em.find(Todo.class, todo.getId());
Todo find2 = em.find(Todo.class, todo.getId());
System.out.println("[1차 캐시] find1 == find2 ? → " + (find1 == find2));
// 5.변경 감지 (Dirty Checking)
find1.updateTitle("제목 변경됨");
System.out.println("[Dirty Checking] 엔티티 수정했지만 flush 전이라 아직 UPDATE 없음");
em.flush(); // UPDATE SQL 실행
System.out.println("[Dirty Checking] flush 시점에 UPDATE SQL 나감");
// 6.clear로 영속성 컨텍스트 초기화
em.clear();
System.out.println("[clear 이후] em.contains(find1)? → " + em.contains(find1));
// 7.다시 조회 → DB에서 SELECT 발생 (1차 캐시 날아갔으므로)
Todo find3 = em.find(Todo.class, todo.getId());
System.out.println("[DB 조회] find1 == find3 ? → " + (find1 == find3));
System.out.println("================= 실험 종료 =================\n");
}
}
예상
- 비영속 상태이니 false
- 영속 상태에선 true
- clear 이후에는 캐시가 없으니 false
- 다시 조회 해도 1차 캐시가 없으니 false
콘솔
================= JPA 영속성 컨텍스트 실험 =================
[비영속] em.contains(todo)? → false
[영속] em.contains(todo)? → true
[영속] 아직 INSERT SQL 안 나감 (flush 전)
Hibernate:
insert into todos (...) values (...)
[flush 이후] INSERT SQL 실행됨
[1차 캐시] find1 == find2 ? → true
[Dirty Checking] 엔티티 수정했지만 flush 전이라 아직 UPDATE 없음
Hibernate:
update todos set title=? where id=?
[Dirty Checking] flush 시점에 UPDATE SQL 나감
[clear 이후] em.contains(find1)? → false
[DB 조회] find1 == find3 ? → false
================= 실험 종료 =================
실습 포인트 정리
| 영속성 컨텍스트 개념 | 핵심 키워드 | |
| em.persist() | 엔티티를 영속 상태로 등록 | 1차 캐시 등록 |
| em.flush() | SQL 실행 시점 강제 | 쓰기 지연 저장소 |
| em.find() | 1차 캐시 조회 | DB 접근 최소화 |
| 엔티티 수정 + flush() | 변경 감지 (Dirty Checking) | 자동 UPDATE |
| em.clear() | 캐시 초기화 | detached 상태 |
| 다시 find() | DB 접근 발생 | 1차 캐시와 DB 동기화 |
개념 정리 후 실습을 통해 영속성 컨텍스트가 눈에 보이지 않는 캐시구나 라고 체감이 되었다.
'Dev. > Spring' 카테고리의 다른 글
| [Spring] JPA N+1 문제 (0) | 2025.11.07 |
|---|---|
| [Spring] JPA 관계 설정(1:N/N:1) (0) | 2025.11.05 |
| [Spring] 수정 사항 발생시 자동 리빌드 Auto Reload (0) | 2025.11.04 |
| [내배캠] 한달 회고 (0) | 2025.10.31 |