엔티티를 담아 관리하는 작업 공간. JPA의 거의 모든 동작이 여기서 파생된다.
네 가지 기능
- ① 1차 캐시 — 같은 트랜잭션 안에서 같은 id 를 조회하면 DB 를 다시 안 간다
- ② 동일성 보장 — 그 결과가 == 로 같다
- ③ 쓰기 지연 — persist 해도 곧바로 INSERT 하지 않고 모아 뒀다 flush 때 보낸다
- ④ 변경 감지 — 엔티티 필드를 바꾸기만 하면 UPDATE 가 나간다
@Transactional
void demo(Long id) {
Member a = em.find(Member.class, id); // SELECT 실행
Member b = em.find(Member.class, id); // DB 안 감 (1차 캐시)
System.out.println(a == b); // true (동일성 보장)
a.setName("홍길동"); // save() 를 부르지 않는다
} // 커밋 시 UPDATE 가 나간다 (변경 감지)
변경 감지의 내부 — 스냅샷
엔티티를 영속 상태로 만들 때
- 그 시점의 필드 값을 '스냅샷' 으로 함께 보관한다
flush 시점
- 현재 값과 스냅샷을 비교한다
- 다른 필드가 있으면 UPDATE 문을 만든다
그래서 save()를 부를 필요가 없다. 오히려 부르면 불필요한 merge가 일어날 수 있다.
준영속 상태에서는 동작하지 않는다
@Transactional
Member load(Long id) { return em.find(Member.class, id); } // 트랜잭션 종료 → 준영속
void outside() {
Member m = load(1L);
m.setName("변경"); // 아무 일도 일어나지 않는다. 컨텍스트가 없다
}
1차 캐시는 스레드 전용이다
영속성 컨텍스트는 트랜잭션(스레드) 마다 따로 만들어진다
- 다른 요청과 공유되지 않는다
- "캐시" 라는 이름 때문에 성능 캐시로 오해하기 쉽지만,
- 수명이 트랜잭션 하나이므로 캐시 효과는 거의 없다
- 애플리케이션 범위 캐시는 2차 캐시(별도 설정)다
flush가 일어나는 시점
- ① 트랜잭션 커밋 직전
- ② JPQL 쿼리 실행 직전 (쓰기 지연된 내용이 반영돼야 정확한 결과가 나오므로)
- ③ em.flush() 직접 호출
②를 모르면 "아직 커밋 안 했는데 왜 INSERT가 나갔지?" 에서 막힌다.
면접 함정
- ❌ "1차 캐시로 성능이 좋아진다" → 수명이 트랜잭션 하나라 효과가 제한적이다. 주 목적은 동일성 보장과 변경 감지다.
- ❌ "수정하려면 save()를 불러야 한다" → 영속 상태면 필드만 바꾸면 된다.
상태를 직접 확인해 보기
em.contains(entity); // 영속 상태인가
em.detach(entity); // 준영속으로 만든다
em.clear(); // 컨텍스트를 통째로 비운다 (대량 처리에서 필수)
em.flush(); // 쓰기 지연 내용을 지금 SQL 로 보낸다
대량 처리에서 반드시 비운다
@Transactional
void bulkInsert(List<Member> members) {
for (int i = 0; i < members.size(); i++) {
em.persist(members.get(i));
if (i % 1000 == 0) {
em.flush(); // SQL 전송
em.clear(); // 컨텍스트 비우기 ← 없으면 OOM
}
}
}
비우지 않으면 영속성 컨텍스트가 10만 개 엔티티 + 10만 개 스냅샷을 들고 있게 된다
- 메모리도 문제지만, flush 마다 전부 변경 감지를 돌아 급격히 느려진다
JPQL은 flush를 유발한다
em.persist(newMember); // 아직 INSERT 안 나감
List<Member> all = em.createQuery("select m from Member m").getResultList();
// ← 이 순간 flush 가 일어난다 (방금 persist 한 것도 결과에 포함돼야 하므로)
"커밋도 안 했는데 왜 INSERT가 나갔지" 의 답이다. 반대로 em.find()는 1차 캐시를 먼저 보므로 flush를 유발하지 않는다.