3. 영속성 관리 - (3.1 영속성 컨텍스트)

HotFried·2023년 9월 25일
0

  • 요청 시 EntityManagerFactory를 통해 EntityManager를 생성한다.
  • EntityManager는 내부적으로 DB 커넥션을 통해 DB에 접근한다.\

영속성 컨텍스트

  • "Entity를 영구 저장하는 환경"이라는 뜻

  • 영속성 컨텍스트는 논리적인 개념이라 눈에 보이지 않는다.

  • EntityManager를 통해 영속성 컨텍스트에 접근한다.

  • 코드 : EntityManager.persist(entity)


Entity의 생명주기

요약

- 비영속 (new/transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태

- 영속 (managed)
영속성 컨텍스트에 관리되는 상태

- 준영속 (detached)
영속성 컨텍스트에 저장되었다가 분리된 상태

- 삭제 (removed)
삭제된 상태


비영속 (new/transient)

  • 최초에 객체를 생성한 상태
    -> 영속성 컨텍스트와 전혀 관계 없는 새로운 상태

  • 객체 생성 후 EntityManager에 아무 요청도 하지 않은 상태


영속 (managed)

  • 객체 생성 후 em.persist(member) 코드를 통해 영속성 컨텍스트에 객체를 추가한다.
    (비영속 상태의 객체가 영속 상태로 전환된다.)

주의사항

em.persist(member) 코드를 작성한다고 해서 Query가 작성되지 않는다.
tx.commit() 코드가 실행되어야 Query가 작성되어 DB에 객체가 저장된다.


준영속 (detached)

  • 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 영속성 컨텍스트와 아무런 관계도 없다.

삭제 (removed)

  • DB에서 데이터를 삭제

영속성 컨텍스트의 이점

1차 캐시

  • member 객체를 생성 후 Id, Username을 설정만 한 비영속 상태이다.
  • 영속 상태로 전환 시 1차 캐시에 PK와 Entity가 저장된다.

  • 만약 객체가 1차 캐시에 저장이 되어있다면, 조회 시 DB대신 1차 캐시를 먼저 확인하기 때문에 별도의 Query를 생성하지 않고 바로 조회가 가능하다.
public class JpaMain {

    public static void main(String[] args) {
        
        ...

        Member member = new Member();
        member.setId(101L);
        member.setName("HelloJPA");

        System.out.println("=== before ===");
        entityManager.persist(member);
        System.out.println("=== after ===");

        // persist()로 1차 캐시에 이미 있기 때문에 select 쿼리가 나가지 않는다.
        Member findMember = entityManager.find(Member.class, 101L);

        System.out.println("findMember.id: " + findMember.getId());
        System.out.println("findMember.name: " + findMember.getName());

        tx.commit();
    }
}
=== before ===
=== after ===
findMember.id: 101
findMember.name: HelloJPA
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)

em.persist(member) 또는 em.find(Member.class, 101L) 코드 실행 시 별도의 쿼리가 나가지 않는 것을 확인할 수 있다.
tx.commit() 코드 실행 시 insert 쿼리가 나가는 것을 확인할 수 있다.

  1. member2 객체는 1차 캐시에 존재하지 않는다.
  2. 따라서 별도의 Query가 생성되어 DB에서 해당 데이터를 조회
  3. 데이터를 1차 캐시에 저장
  4. 저장된 member2 반환

-> 이후 다시 member2를 조회할 때는, 1차 캐시에서 조회 후 바로 반환

public class JpaMain {

    public static void main(String[] args) {
        
        ...

        Member findMember1 = entityManager.find(Member.class, 101L);
        // 같은 데이터를 조회하면 1차 캐시에서 가져오기 때문에 쿼리가 나가지 않는다.
        Member findMember2 = entityManager.find(Member.class, 101L);

        tx.commit();
    }
}
Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.name as name2_0_0_ 
    from
        Member member0_ 
    where
        member0_.id=?

1차 캐시에 저장되어있지 않은 객체 조회 시 "select from where" 쿼리가 생성되어 DB에서 해당 데이터를 조회하는 것을 확인할 수 있다.

이후 같은 객체를 다시 조회할 경우, 해당 객체가 1차 캐시에 저장되어있기 때문에 별도의 쿼리가 생성되지 않는 것을 확인할 수 있다.

특징

  • 성능상 이점은 크게 없다.
  • EntityManager는 트랜잭션 단위로 만들고 트랜잭션이 종료되면 삭제된다.
    -> 이때 1차 캐시가 함께 삭제된다.
  • 많은 고객에게 요청이 와도 트랜잭션을 각자 가지고 있어 찰나의 순간에만 사용된다.

동일성 보장

자바 컬렉션에서 꺼낸 데이터는 레퍼런스가 같은 특성을 가진다.
JPA도 이와 같은 동일성을 보장해준다.

1차 캐시는 Map으로 Entity 인스턴스를 캐싱하고있기 때문에, 같은 식별자(@Id 값)에 대해 매번 같은 인스턴스에 접근하게 되므로 동일성이 보장이 된다.


트랜잭션을 지원하는 쓰기 지연

em.persist() 객체를 영속상태로 전환할 때 Query를 보내지 않는다.
commit()하는 순간 DB에 Insert Query를 보낸다.

객체를 persist()를 통해 영속 상태로 만들면 쓰기 지연 SQL저장소에 insert 쿼리를 쌓아둔다.

이후 트랜잭션을 commit()하는 시점에서 쓰기 지연 SQL저장소에 저장된 쿼리가 flush()되어 DB에 날아간다.

Member.java

@Entity
public class Member {

    @Id
    private Long id;
    private String name;

    // JPA는 내부적으로 reflection을 이용해 동적으로 객체를 생성해내기 때문에 기본 생성자가 필요하다. 
    // JPA 기본 스펙
    public Member() {

    }

    // 객체 생성을 쉽게 하기 위해 새로운 생성자를 만든다.
    public Member(Long id, String name) {
        this.id = id;
        this.name = name;
    }

  ...
}

JpaMain.java

public class JpaMain {

    public static void main(String[] args) {
        
        ...

        Member member1 = new Member(150L, "A");
        Member member2 = new Member(160L, "B");

        // 쓰기 지연 SQL 저장소에 Query가 저장된다.
        entityManager.persist(member1);
        entityManager.persist(member2);
        System.out.println("------------");

        // 쿼리가 날아간다.
        tx.commit();
    }
}
------------
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)

member1, member2를 저장하는 쿼리가 commit()시점에 한번에 날아가는 것을 확인할 수 있다.


참고 : batch_size 만큼 Query를 저장해 DB에 날릴 수 있는 프로퍼티
<property name="hibernate.jdbc.batch_size" value="10"/>

버퍼와 유사하게 여러 개의 Query를 모아서 보낸다면, 성능 최적화가 가능하다.


변경 감지 (더티 체킹)

  • JPA는 자바 컬렉션에 넣은 것처럼 값을 다루는 게 목적이다.
    -> 컬렉션에서 꺼낸 값을 변경했을 때 다시 컬렉션에 넣는 코드를 작성하지 않는다.

  • 마찬가지로 JPA는 데이터 변경 후에 persist()를 하지 않아도 된다.

1차 캐시에는 @id와 @entity 외에도 스냅샷이 있다.
(최초로 영속성 컨텍스트에 들어온 상태를 저장하는 것)

commit() 시점에 Query가 flush() 되면서 Entity와 스냅샷을 비교한다.
만약 변경사항이 있으면 update Query 생성 후, DB에 반영한다.


엔티티 삭제

삭제는 remove()만 해주면 된다.
쓰기 지연 저장소에 쿼리를 모았다가 커밋 시점에 실행한다.


참고 :

김영한. 『자바 ORM 표준 JPA 프로그래밍』. 에이콘, 2015.

자바 ORM 표준 JPA 프로그래밍 - 기본편

profile
꾸준하게

0개의 댓글