개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › JPA › 영속성 관리

엔티티의 생명주기

같은 setName인데 왜 어떤 때는 DB에 반영되지 않나

엔티티 객체는 비영속, 영속, 준영속, 삭제라는 네 가지 상태 가운데 하나에 있습니다. JPA가 값의 변화를 지켜보고 DB에 반영해 주는 것은 영속 상태일 때뿐입니다.

실행으로 대조 · Hibernate 7.4.5와 H2에서 열네 가지 조합을 실행해 줄마다 나간 SQL까지 대조

이 실습에 나오는 개념

엔티티의 네 가지 상태
엔티티 객체가 영속성 컨텍스트와 어떤 관계에 있는지를 나타내는 구분이며, 엔티티의 생명주기라고도 한다. 비영속은 new로 만들기만 해서 JPA가 모르는 상태이고, 영속은 em.persist로 맡겼거나 조회해 와서 영속성 컨텍스트가 관리하는 상태다. 준영속은 관리되다가 떨어져 나온 상태이고, 삭제는 em.remove로 지울 대상이 된 상태다. 값을 바꾸면 UPDATE가 나가는 변경 감지와 1차 캐시 같은 기능은 영속 상태인 엔티티에만 동작한다. 그래서 값이 DB에 반영되지 않을 때는 그 객체가 지금 어느 상태인지부터 확인한다.
준영속 상태
영속성 컨텍스트가 관리하다가 더는 관리하지 않게 된 엔티티의 상태다. em.detach로 떼어 내거나, em.clear로 영속성 컨텍스트를 비우거나, em.close로 닫으면 그 안에 있던 엔티티가 준영속이 된다. 식별자(id) 값은 가지고 있지만 평범한 자바 객체와 같아서, 값을 바꿔도 UPDATE가 나가지 않고 예외도 나지 않는다. 화면에서 받은 값으로 new 해서 만든 객체도, DB에 이미 있는 행의 id를 가지고 있으면 준영속으로 다룬다. 트랜잭션이 끝난 뒤에 엔티티를 고치는 코드에서 자주 만난다.
영속성 컨텍스트
엔티티를 보관하고 관리하는 JPA의 논리적 공간이다. 조회하거나 저장한 엔티티는 이곳에 보관되며, JPA는 이곳의 엔티티에 일어난 변화를 추적해 트랜잭션이 끝날 때 DB에 반영한다. 보통 트랜잭션 하나와 생명주기를 같이한다(OSIV가 켜진 웹 요청에서는 요청이 끝날 때까지 유지된다). 1차 캐시, 변경 감지, 쓰기 지연 같은 기능이 이 위에서 동작한다.
변경 감지(Dirty Checking)
영속성 컨텍스트가 관리하는 엔티티의 값이 바뀌면, 플러시할 때(보통 커밋 직전) JPA가 처음 읽었던 상태와 비교해 값이 달라진 엔티티에 대한 UPDATE 문을 자동으로 실행하는 기능이다. Hibernate는 기본적으로 바뀐 컬럼만이 아니라 전체 컬럼을 UPDATE 문에 넣으며, 바뀐 컬럼만 넣으려면 @DynamicUpdate를 쓴다. 따라서 조회한 엔티티의 값을 바꾸기만 하면 별도의 저장 메서드를 호출하지 않아도 DB에 반영된다.
쓰기 지연과 flush
쓰기 지연은 INSERT, UPDATE, DELETE 문을 바로 DB에 보내지 않고 모아 두었다가 한꺼번에 보내는 것이다. 단, 기본 키를 IDENTITY 전략(MySQL의 AUTO_INCREMENT 등)으로 생성하는 엔티티는 식별자를 얻기 위해 저장을 요청한 시점에 INSERT가 바로 실행된다. flush는 모아 둔 변경 내용을 DB에 보내는 동작으로, 트랜잭션 커밋 직전이나 JPQL 쿼리 실행 직전에 자동으로 일어난다. flush는 SQL을 DB에 전송할 뿐 트랜잭션을 커밋하는 것은 아니다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. em.find(Member.class, 1L): SELECT로 1번 회원을 읽어 영속성 컨텍스트에 보관합니다. 이때부터 m은 영속 상태입니다. 영속성 컨텍스트가 처음 읽은 값(김철수)을 기억해 두고 m의 변화를 지켜봅니다.
  2. em.detach(m): m을 영속성 컨텍스트에서 떼어 냅니다. m은 영속에서 준영속으로 바뀌었습니다. SQL은 나가지 않습니다.
  3. m.setName("김영수"): 준영속 상태인 객체의 이름을 바꿨습니다. 자바 객체의 값은 바뀌지만, 영속성 컨텍스트는 이 객체를 더는 지켜보지 않습니다. 오류도 나지 않습니다.
  4. em.contains(m): false입니다. 영속성 컨텍스트가 m을 더는 관리하지 않는다는 뜻입니다.
  5. commit: 커밋 직전에 flush가 일어나지만, m은 관리 대상이 아니라서 비교하지 않습니다. UPDATE가 나가지 않고 DB의 이름은 "김철수" 그대로입니다.

실행 결과

코드

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

Member m = em.find(Member.class, 1L);
em.detach(m);
m.setName("김영수");
System.out.println(em.contains(m));
em.getTransaction().commit();

이 모형의 전제

열네 가지 조합(일곱 가지 길 × 이름을 바꾸는 때)을 실제 Hibernate 7.4.5와 H2 데이터베이스에서 실행해, 줄마다 나간 SQL의 종류, em.contains의 결과, DB에 남은 행이 화면과 일치하는 것을 확인했습니다. "이름을 바꾸는 때"는 new만 하는 길, 조회만 하는 길, remove에서는 결과에 영향이 없어 숨겨 두었습니다. 요약에 적은 다음 사실도 따로 실행해 확인했습니다: 값을 바꾼 뒤 em.flush()를 부르고 떼어 내면 반영된다, 떼어 낸 뒤 다시 조회하면 SELECT가 다시 나가고 다른 객체가 온다, remove 한 객체를 커밋 전에 다시 persist하면 DELETE가 나가지 않는다, 지워진 뒤에도 자바 객체의 값은 읽힌다, persist 뒤에 값을 바꾸면 INSERT에는 persist할 때의 값이 들어간다. 회원의 id는 코드에서 직접 넣는 방식으로 두었습니다. DB가 id를 정하는 IDENTITY 방식에서는 persist하는 줄에서 INSERT가 바로 나갑니다. 화면의 SQL 문장은 읽기 쉽게 줄여 적은 것입니다. 스프링에서는 @Transactional 메서드가 시작하고 끝날 때 여기의 begin, commit, close가 대신 일어납니다(OSIV를 켠 웹 요청에서는 em이 요청이 끝날 때 닫힙니다).

앞 실습: 영속성 컨텍스트 · 다음 실습: 준영속과 병합

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요