TIL 39 : 영속성 컨텍스트 - 준영속을 하는 이유 😇
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 발동 조건
- 메모리가 부족해졌을 때
- CPU가 여유 있을 때
- 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