코딩/sparta TIL

TIL 39 : 영속성 컨텍스트 - 준영속을 하는 이유 😇

americanoallday 2025. 4. 30. 23:43

em.detach(x)를 하면 준영속 상태가 됨. 

그러니까 영속된 상태와 준영속이 있는데, 영속된 상태는 계속 추적 관리 하는 대상이고, 준영속은 그걸 안한다는 것이라고 함.

Member m = em.find(Member.class, 1L);
em.detach(m);

-> m은 자바 객체로 메모리에 계속 있음

-> m.getName(), m.setName("Sun") 해도 가능

-> 단지, em이 더 이상 m을 감시하거나 추적하거나 flush(DB 반영) 반영을 하지 않음.

-> detach해도 자바 힙에는 살아 있고, 그냥 JPA가 관리를 안하는 것 뿐.

* em : EntityManager(실제 JPA 작업을 수행하는 객체)

 

준영속을 사용하는 이유는

1. 사용자 수정 중 취소(임시 변경)

2. 대량 데이터 조회 후 메모리 절약 시

3. flush 되기 전 강제로 rollback 구조를 설계

 

그리고 필요하면 다시 em.merge(obj)해서 영속 상태로 복구 가능하다고 함.

-> 그래서 그럼 다시 영속으로 복구할 때 다시 DB 조회를 하느냐? YES, 그렇다고 동일 객체는 아님.

Member m1 = em.find(Member.class, 1L); // 영속 상태
em.detach(m1);	// 준영속 상태

m1.setName("변경 이름"); // 변경 감지 X, 업데이트 쿼리 안날라감

Member merged = em.merge(m1); // 다시 영속상태로 복구

1. em.merge(m1) -> m1.getId()로 DB 조회 -> 새로운 영속 객체 m2를 만들고 

2. m2.setName("변경된 이름")으로 m1의 필드를 복사함

3. return m2 -> 이제 m2는 JPA가 다시 영속 상태로 관리함

4. m1은 여전히 준영속 상태

 

그리고 결국 준영속해도 자바 힙 메모리에 존재하는데, 이게 메모리 절약이 맞느냐? 라고 했을 때

자바 힙에서 완전히 사라지진 않지만, detach는 ‘JPA가 쥐고 있는 참조를 끊어서 GC가 더 빨리 수거할 수 있게 해주는 행위 라고 함 ㅡㅡ;

> 자바에서 객체는 참조가 끊기면 GC(가비지 컬렉션) 대상이 됨, 하지만 JPA는 영속성 컨텍스트(1차 캐시)에 해당 객체를 계속 붙들고 있음. 준영속 처리하면 GC가 수거 가능해짐.

 

아니 그럼 또 여기서 궁금한게 아까 준영속 상태에서도 getName, setName 가능하다고 했는데, GC가 수거해 가면 그럼 못하지 않나? 라고 의문이 생겨서 찾아보니... ㅡㅡ;;

> GC가 객체를 수거해가면 불가능하다고 함... ㅎㅎ

 

그래서 직접적으로 메모리를 즉시 줄이진 않지만, JPA가 계속 캐싱하고 추적하는 오버헤비한 관리 상태를 해제해줘서

대량 데이터 처리 시 '현실적인 메모리 절약 효과'과 있다. 라는 것...

상태 1차 캐시에 있음? Dirty Checking 대상? flush 대상? 프록시 초기화 대상?
영속
준영속

 

정리 🧹 (와 이모티콘 입력하니까, 자동으로 인식해서 빗자루 뜸...)

1. em.detach(entity)

- JPA가 이 객체에 대한 관리(1차 캐시, flush, 프록시 초기화)를 끊음

- 코드 어딘가에 객체를 참조하고 있다면, 계속 사용 가능 

 

2. 자바 힙에 아무도 m을 참조하지 않게 되면

- 이 객체는 GC의 수거 대상이 됨

- 이후에는 get/set 안 됨

 

준영속 객체를 null 하거나, gc 호출을 직접 안할 경우 언제 수거해 가는지가 궁금해짐 ㅡㅡ;;

- 자바의 GC는 "알아서 정리하지만, 타이밍은 JVM 마음" 😇 

- JVM은 객체를 주기적으로 스캔해서 더 이상 어디서도 쓰이지 않는 객체를 탐지함 -> 이걸 "Unreachable Object"라고 부름

- 이 객체는 GC 대상이 되고, JVM이 판단했을 때 메모리가 부족하다 싶으면 수거함.

 

GC 발동 조건

  1. 메모리가 부족해졌을 때
  2. CPU가 여유 있을 때
  3. GC의 내부 스케줄러가 실행되었을 때
상황 행동
객체를 명시적으로 버리고 싶을 때 m = null, list.clear() 등
GC가 잘 안 도는 경우 디버깅 시 System.gc() 호출
JPA 캐시 영향만 없애고 싶을 때 em.detach(m) 또는 em.clear()
메모리 민감 시스템(ETL 등) 작업 단위마다 detach + 수동 null 처리 + GC 유도

*em.getTransaction().commit() : flush와 차이점은 commit은 자동으로 내부적으로 flush를 호출한다고 함.
flush는 쿼리 전송만 함, commit이 확정 저장 하는 것. flush 후 commit 안 하면, DB저장 안되고 롤백 됨

 

persist(), merge 차이점

em.persist(m) : 신규 객체 저장

- 영속 상태

- m.setName("변경"); // 바로 변경 감지 -> flush or commit 시 INSERT or Update 발생

 

em.merge(obj) : 준영속 or 신규 객체 병합

 

주의할 점 

Member m1 = new Member("유진");
em.persist(m1);

Member m2 = new Member("유진");
m2.setId(m1.getId());       // 같은 ID지만 다른 객체
em.merge(m2);               // ❗동일성 보장 깨짐 (== false)

→ 해결: 가능하면 영속 객체 그대로 쓰기 + detach 피하기

 

변경 감지 Tip

JPA는 기본적으로 엔티티의 모든 필드를 업데이트 하는 SQL를 생성함

필드가 많은 엔티티의 경우 변경 필드만 업데이트  하도록 설정 가능함

spring.jap.properties.hibernate.jdbc.batch_versioned_data=true