이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › JPA
영속성 컨텍스트는 무엇이고, 무엇을 해 주는가
영속성 컨텍스트는 EntityManager(코드의 em)가 엔티티 객체를 보관하면서 지켜보는 곳입니다. 엔티티는 DB의 행 하나에 대응하는 자바 객체이고, 여기서는 Member입니다. em.find로 DB에서 읽어 온 엔티티가 영속성 컨텍스트에 들어갑니다(새로 저장할 때 쓰는 em.persist로 넘긴 엔티티도 들어가지만 이 실습에는 나오지 않습니다). 들어 있는 엔티티는 JPA가 지켜보고 있어서, 값을 바꾸기만 해도 트랜잭션을 커밋할 때 JPA가 UPDATE 문을 만들어 보냅니다. 같은 id를 다시 find하면 DB에 가지 않고 보관하던 그 객체를 줍니다. 반대로 new로 직접 만든 객체나 em.detach로 떼어 낸 객체는 영속성 컨텍스트에 들어 있지 않은 평범한 자바 객체여서, 값을 바꿔도 DB에 반영되지 않습니다. 이 실습은 Member를 얻는 방법을 바꿔 가며 같은 코드(name을 이영희로 바꾸고 커밋)를 실행해, 커밋 뒤 DB의 name과 나간 UPDATE, SELECT 문의 수를 봅니다.
실행으로 대조 · Hibernate 7.4.5와 H2 2.4.240에서 여섯 가지 조합을 실행해 대조, 규칙은 Jakarta Persistence 3.2 명세와 대조
em.getTransaction().begin();
// find: DB에서 읽어 온 엔티티가 영속성 컨텍스트에 들어간다
Member m = em.find(Member.class, 1L);
m.setName("이영희");
em.getTransaction().commit();
Hibernate 7.4.5와 H2 2.4.240(메모리 DB)에서 실행해 대조했습니다. 스프링 없이 SessionFactory를 직접 만들었고, JDK 21입니다. 여섯 가지 조합을 모두, 화면의 코드 줄을 글자 그대로(주석 줄 포함) 컴파일해 실행했습니다. 조합마다 DB와 SessionFactory를 새로 만들고, DB에는 실행 전에 JDBC로 (1, 김철수) 한 행을 미리 넣었습니다. 이 준비의 SQL은 세지 않았습니다. 화면에 보이지 않는 것은 Member 엔티티(@Entity, @Id가 붙은 id와 name, 생성자, setName과 getName)와 EntityManager를 얻고 닫는 코드이고, 트랜잭션은 em에서 직접 시작하고 커밋했습니다. 대조한 것은 커밋 뒤 다른 연결(순수 JDBC)로 조회한 name, Hibernate의 StatementInspector로 센 UPDATE와 SELECT 문의 수(SQL 원문과 나간 줄은 대조 출력에 남겼습니다), 코드가 화면에 찍은 m == m2입니다. 단계 문장이 기대는 사실도 코드 줄마다 바로 아래에 측정 줄을 끼운 두 번째 실행에서 쟀습니다. em.contains(m)(find로 가져온 뒤에는 true, new로 만든 객체와 detach로 뗀 뒤에는 false), UPDATE는 m.setName 줄이 아니라 commit 줄에서 나간다는 것, 같은 id를 다시 find할 때 find 조합에서는 SELECT가 늘지 않고 아직 커밋 전인데도 name이 이영희인 같은 객체를 받는다는 것(new와 detach 조합에서는 SELECT가 늘고 name이 김철수인 새 객체를 받는다는 것), Hibernate가 읽어 올 때의 값을 따로 기억한다는 것(내부 API로 보면 읽은 뒤 [김철수], detach 뒤에는 기억 없음)입니다. 이름을 이영희로 바꿨다가 김철수로 되돌린 뒤 커밋하면 UPDATE가 나가지 않는다는 것도 따로 실행해, "바꾸는 호출을 했는지"가 아니라 처음 값과 비교한다는 근거로 삼았습니다. 또 new로 만든 객체를 em.persist로 넘기면 em.contains가 true가 되고 커밋할 때 INSERT가 나가는 것도 실행해 확인했습니다. 문서가 정한 것은 Jakarta Persistence 3.2 명세의 3.1절(EntityManager마다 영속성 컨텍스트가 있고, 그 안에는 같은 식별자의 엔티티 인스턴스가 하나뿐이라는 것), 3.3절(엔티티 인스턴스의 생명주기: managed는 영속성 컨텍스트에 속한 상태, detached는 속하지 않거나 더는 속하지 않는 상태), 3.3.2절(persist로 managed가 되는 것), 3.3.4절(영속 엔티티의 상태가 트랜잭션 커밋 때 DB와 맞춰지는 것), 3.3.6절과 3.3.7절(영속성 컨텍스트에서 떼어 낸 인스턴스는 그 밖에서 살아간다는 것)입니다. 명세 페이지는 요약으로만 돌아와 이 절들의 원문 전체를 읽지는 못했습니다. 처음 값을 기억해 비교하는 방식과 나가는 SQL의 모양은 명세가 정한 것이 아니라 Hibernate 7.4.5에서 관찰한 동작이므로, 화면에는 SQL 문장 대신 횟수만 적었습니다. 이 실습은 em을 직접 만들어 쓰는 모양이라 스프링의 @Transactional과 함께 쓸 때(영속성 컨텍스트가 트랜잭션과 같이 만들어지고 끝납니다)나, 쓰기 지연, 쿼리를 실행하기 직전의 flush, em.persist로 새로 저장하는 일은 다루지 않으며, 세부는 공부 탭의 "영속성 컨텍스트" 실습에서 다룹니다. MySQL에서의 실행 대조는 하지 않았습니다.
줄을 그은 곳은 처음 코드에서 지운 것이고, 색을 칠한 곳은 새로 쓴 것입니다.