<JPA> 영속성 관리 - 내부 동작 방식

영속성 컨텍스트

  • “Entity”를 영구 저장하는 환경이라는 뜻이다.
  • EntityManager.persist(entity)를 통해서 엔티티를 영속성 컨텍스트에 저장한다.(중요한 포인트는 연동된 DB에 저장하지 않는다는 것이다)
  • EntityManager를 통해서 영속성 컨텍스트에 접근 할 수 있다.
  • 아래 그림과 같이 J2SE환경에서는 서로 1대1관계를 맺고 있다.

엔티티의 생명주기

  • 비영속 : 엔티티를 생성만 한 상태
  • 영속 : em.persist()를 통해 영속성 컨텍스트에 엔티티를 넣은 상태(앞서 언급한 것 처럼 DB에 저장이 되지 않으며, 즉 쿼리가 나가지 않음)
  • 준영속 : em.detach()를 통해 영속성 컨텍스트에서 엔티티를 분리시킨 상태
  • 삭제 : em.remove()를 통해 DB에 저장된 엔티티까지 삭제시킨 상태

실습해보기

  • 아래와 같은 코드를 통해 Member 객체를 생성하고 em.persist()를 통해서 영속성 컨텍스트에 엔티티를 넣었다.
public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {

            Member member = new Member();
            member.setId(100L);
            member.setName("brido");

            //영속
            System.out.println("Before");
            em.persist(member);
            System.out.println("After");
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
  • 하지만 출력된 로그를 통해 보면 Before와 After사이에는 아무런 insert문이 데이터베이스에 날라가지 않았다.
  • 이제부터 이와 관련된 궁금증을 알아보자.
그러면 DB에 entity를 저장하는 쿼리는 언제 발생하는걸까?

영속성 컨텍스트의 기능

1차 캐시

  • 영속성 컨테스트는 아래와 같이 1차 캐시 구조를 가진다.
  • em.find()를 통해서 엔티티를 조회할 경우, 우선적으로 1차 캐시부터 먼저 탐색한다.즉 DB까지 조회하지 않고 엔티티를 조회할 수 있다.
  • 1차 캐시에서 탐색 후, 없는경우에만 직접 DB로 쿼리를 날려 해당되는 엔티티를 가져와서 1차캐시에 저장하고 나서,반환해준다.

영속 엔티티의 동일성 비교(== 비교)

  • 쉽게 말해서 아래와 같은 코드를 실행하면 동일하다고 true가 출력된다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b);
  • 메모리 주소 연산을 하듯이, 동일한 1차캐시에 저장되어 있기에 위와같은 결과가 도출된다.

쓰기 지연

  • 위에서 언급된 질문에 대한 대답을 해줄 수 있는 기능이다.
  • em.persist(memberA), em.persist(memberB)를 한 경우 아래의 그림과 같이 영속성 컨텍스트가 동작한다.
  • 먼저 영속성 컨텍스트에 해당 엔티티를 생성하고 위의 언급된 1차캐시에 저장한다.
  • 이후 Insert 문을 ‘쓰기 지연 SQL 저장소’에 넣어둔다.
  • 해당 저장소에 저장된 쿼리는 어떠한 명령어가 들어오기 전까지 DB로 보내지지 않는다.
  • 모여있는 쿼리들은 transaction.commit()을 통해서 DB로 보내진다.
  • 커밋을 실행하면 아래와 같은 동작방식으로 DB에 쿼리가 날라간다.
  • 아래와 같은 동작방식이란, flush를 통해서 쓰기 지연 저장소에 있는 SQL을 모두 DB로 날려준다.

Flush

  • 영속성 컨텍스트의 변경내용을 데이터베이스에 반영해주는 명령어이다.
  • 즉, 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스로 날려준다.
  • tx.commit()을 할 경우 flush가 자동으로 호출된다. 이후 DCL commit을 실행한다.
  • JPQL 쿼리 실행시 flush가 자동으로 호출된다.(쿼리문이 select인 경우 선택할 데이터들이 DB에 없으면 안되기 때문에 미리 flush를 호출해준다)
  • 마지막으로 em.flush()로 플러시만 따로 호출 할 수 있다.(DB로 날라가는 쿼리를 확인해보고 싶을 경우 사용)

변경 감지

  • 흔히, Ditry-Checking이라고도 불리는 해당 기능은 엔티티에 대한 속성이 변경하고자 할때, 해당 엔티티에 대해서 다시 em.persist()를 하지 않아도 자동으로 DB에 update문을 날려주는 기능이라고 보면 된다.
  • 아래의 코드를 보면 PK값 1L을 가지는 멤버 엔티티를 findMember라는 객체에 저장해 둔다.(em.find의 대상이 되는 memberA는 이미 em.pesist되었다고 가정한다)
  • 이후 해당 엔티티에 대한 데이터를 수정하는 작업을 거치고 tx.commit()으로 인한 flush을 통해 쓰기 지연 SQL 저장소에 있던 쿼리들을 DB로 반영된다.
  • 그러면 아래에서 주석 처리된 em.pesist(memberA)라는 코드를 쓰지 않아도 DB에 업데이트문이 날라가면서 엔티티가 수정된다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // 트랜잭션 시작
// 영속 엔티티 조회
Member findMember = em.find(Member.class, "1L");
// 영속 엔티티 데이터 수정
memberA.setUsername("brido");
memberA.setAge(10);
//em.persist(memberA)
transaction.commit();
  • 아래와 같은 방식으로 엔티티에 대한 변경 감지가 발생한다.

Ref

  • 자바 ORM 표준 JPA 프로그래밍(김영한)