Spring/JPA

내부 동작 방식 - 영속성 컨텍스트 2

느리지만 꾸준하게 2022. 4. 12. 13:20

영속성 컨텍스트는 내부에 1차 캐시를 들고 있다.

조회할 때 find해서 동작하게되면 member를 먼저 조회 안하고 1차 캐시에서 조회를 하게 된다.

 

1차 캐시 엔티티에 member가 있게되면 member를 그냥 조회해온다.

 

member2를 이제 찾게 되면 find를 했을 때 1차 캐시에 없으므로 DB에서 찾아서 1차캐시 엔티티에 저장하게 된다.

이후에 find("member2")를 하게되면 DB를 조회하지 않고 바로 1차캐시 엔티티에서 가져오게 된다

 

엔티티 매니저라는 것은 DB 트랜잭션 단위로 보통 만들고

DB 트랜잭션이 끝날 때 종료시켜버린다.

1차 캐시는 DB 트랜잭션 안에서만 효과가 있기 때문에

성능에 이점을 얻을 수 있는 장점은 없다.

이제 저장을 하고 조회를 하기위해 실행을 하면

데이터베이스에 SELECT 쿼리가 안나오게 된다.

persist(member)을 할 때 1차캐시에 저장이 되고

같은 PK값으로 값을 가져왔기 때문에 db에서 가져오는게 아니라 먼저 1차 캐시를 조회하게 된다.

 

 

try {

            // 비영속
            Member member = new Member();
            member.setId (101L);
            member.setUsername("HelloJPA");

            // 영속 상태
            System.out.println("BEFORE");
            em.persist(member);
            System.out.println("AFTER");

            Member findMember = em.find(Member.class, 101L);

            System.out.println("findMember.id = " + findMember.getId());
            System.out.println("findMember.name = " + findMember.getName());
            
            tx.commit();

 

이제 다 삭제하고 member2를 만들어서 실행하게되면 101L은 db에 남아있고 영속성 컨텍스트가 새로 실행이 되게 된다.

그리고 SELECT가 한 번만 나가게 된다.

 

JPA는 엔티티를 조회하면 무조건 영속성 컨텍스트에 다 올리게 된다.

두번째 똑같은 101L를 조회하면 1차캐시를 조회하게 되고 맨처음에 조회를 했기 때문에

영속성 컨텍스트에 있게되고 그걸 반환하게 된다.

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            // 영속 상태
            Member findMember1 = em.find(Member.class, 101L);
            Member findMember2 = em.find(Member.class, 101L);

            tx.commit();

        } catch (Exception e) {
            tx.rollback();
//            e.printStackTrace();
        } finally {
            em.close();
        }


        emf.close();
    }
}

 

 

 

영속 엔티티의 동일성 보장

아래 코드를 보자.

 

== 비교를 하게되면 결과가 true가 나오게 된다. 자바 컬렉션에서 꺼내어서 똑같은 레퍼런스가 있는 객체를 꺼내면 == 비교를 했을 때 똑같은 것 처럼 해준다.

try {

            // 영속 상태
            Member findMember1 = em.find(Member.class, 101L);
            Member findMember2 = em.find(Member.class, 101L);

            System.out.println("result = " + (findMember1 == findMember2));

            tx.commit();

 

 

em.persist(memberA)를 넣었고 em.persist(memberB)해서 memberB를 넣었다.

 

각각 쓰기지연 SQL 저장소에 쌓여있다.(db에는 넣지 않음)

 

그리고 쓰기 지연 SQL 저장소에 있던 것들이 flush를 통해서 DB에 날라가게 된다.

 

아래를 실행하게 되면 === 선을 긋고 나서 쿼리가 나가게 된다.

// Member

@Entity
public class Member {

    @Id
    private Long id;
    private String name;

    
    // 기본 생성자
    public Member() {

    }

    public Member(Long id, String name) {
        this.id = id;
        this.name = name;

    }

    public Long getId() {
        return id; }

}





//JpaMain
try {

            // 영속 상태
            Member member1 = new Member(150L, "A");
            
            em.persist(member1);
            em.persist(member2);


            System.out.println("===================================");

            tx.commit();

        } catch (Exception e) {
            tx.rollback();
//            e.printStackTrace();
        } finally {
            em.close();
        }

 

 

아래 구문을 추가하게 되면 데이터베이스에 한방에 네트워크로 쿼리 두개를 보내고 db를 커밋시킨다. 

// persistence.xml

<property name="hibernate.jdbc.batch_size" value="10"/>

 

 

 

 

 

아래 코드에서는 persist를 넣지 않았는데  JPA에서는 값만 바꾸고 update 쿼리를 나가게 할 수 있다.

        try {
            // 영속
            Member member = em.find(Member.class, 150L);
            Member.setName("ZZZZ");
//            em.persist(member);
            System.out.println("===================================");
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
//            e.printStackTrace();
        } finally {
            em.close();
        }

 

JPA는 dirty checking 변경감지를 통해서 값을 바꿀 수 있다. 

즉 JAVA Collection에서 하는 것처럼 DB에서 그냥 변경이 된다.

내부 원리는 영속성 컨텍스트 안에 있는데,

db를 commit하는 시점에 내부적으로 flush가 호출이 되고

flush가 호출이 되는 시점에 JPA가 엔티티와 스냅샷을 비교한다.

 

그리고 업데이트 쿼리를 쓰기 지연 저장소에 넣어두게 된다.

업데이트 쿼리를 DB에 반영하고 commit을 하게된다.

JPA에서는 아래 if문 없이도 update가 된다.


try {
            // 영속
            Member member = em.find(Member.class, 150L);
            Member.setName("ZZZZ");
            
            
            // JPA에서는 없어도 됨
            if (member.getName().equals("ZZZZZ")) {
                em.update(member);
            }
            
            System.out.println("===================================");
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
//            e.printStackTrace();
        } finally {
            em.close();
        }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

<출처 김영한: 자바 ORM 표준 JPA 프로그래밍 - 기본편 >

https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com