내가 JPA 강의를 보고 작성한 VELOG 시리즈

목표

이전 까지 영속성 컨텍스트를 이해하기 위해 필요한 
PesistenceUnit, EntityManager, EntityMangerFactory 에 대해서 알아봤다.
이번에는 영속성 컨텍스트 내부의 
1차 Cache, 쓰기 지연 큐에 대해서 알아보자

 

쓰기 지연 큐

하이버 네이트의 쓰기지연
사실 JPA 에서는 쓰기지연을 구현하지 않는다.
하지만 JPA 구현체 중 하이버네이트에서 쓰기 지연을 구현하고 있고,
하이버네이트 구현체를 사용하고 있기 때문에 쓰기 지연 기능이 가능한 것이다. 

엔티티가 영속화가 되면 
영속화 된 엔티티의 정보가 쓰기 지연 큐 에 엔티티 쿼리 정보가 등록이 된다. 
이후 특정 이벤트 발생 전까지 엔티티의 상태가 변경되면 변경 내용을 쓰기 지연 큐에 등록하며
이벤트 발생 시 한번에 데이터 베이스에 일괄 처리 된다.

쓰기 지연 큐 장점 

1. 변경 추적 
영속화 된 엔티티의 상태 변경 때마다 변경 내용을 쓰기 지연 큐에 등록한다. 
덕분에 하이버네이트 구현체를 사용하면 update 쿼리 작성 없이 
변경된 부분을 감지하여 수정하는 더티체킹 기능이 가능해진다.

2. 일괄처리 및 그로 인한 성능 향상 
flush 메소드가 작동하기 이전에 쓰기 지연 큐에 쿼리들을 모아두어 
flush 메소드가 작동하면 한번에 데이터베이스로 일괄처리하여 적용 시킨다.
이로 인한 네트워크 및 데이터베이스 부하 감소 및 성능 최적화를 이끌 수 있다.

 

1차 Cache

JPA 의 영속성 컨텍스트 내부에서 @Entity 로 선언된 객체들을 저장하고 관리하는 저장소 
엔티티의 @Id 로 선언된 필드를 식별자로 사용하며 엔티티 인스턴스를 값으로 받는다.
1차 캐시를 사용하여 EntityManager 객체가 Entity 를 관리하며 여러가지 기능들을 제공한다.

persist 메소드로 새로운 객체를 영속화 하거나,
엔티티를 조회하면 1차 캐시에 등록 된다.
// 1차 Cache에 등록 - Persist
entityManager.persist(user);

// 1차 Cache에 등록 - Find @Id 를 식별자로 찾음
entityManager.find(UserEntity.class, user.getId());​

1차 캐시는 EntityManager 의 생명주기와 동일하며
트랜잭션이 롤백되거나 초기화 되면 1차 캐시도 초기화 된다.

- @Id 로 선언된 필드를 식별자로 받고, 엔티티의 인스턴스를 값으로 받는다. 
- 엔티티를 영속화 하거나, 조회 하면 1차 캐시에 등록이 된다.
- 트랜잭션이 초기화 되면 1차 캐시도 초기화 된다.

 

1차 캐시의 기능 | 1. 동일성 보장

동일한 트랜잭션안에 엔티티를 조회 할 때 
조회하려는 엔티티가 1차 캐시에 존재하는 경우,  1차 캐시 내부의 엔티티 값을 반환한다.
이후 여러번 조회 하더라도 캐시된 엔티티를 반환함으로 써 동일성이 보장 된다.
반복적인 조회 시 성능 이점과 동일한 트랜잭션 내에서 일관된 상태를 유지할 수 있다.

UserEntity userEntity  = entityManager.find(UserEntity.class, user.getId());
UserEntity userEntity2 = entityManager.find(UserEntity.class, user.getId());
System.out.println(userEntity2.equals(userEntity) ? "같다" : "다르다");

--------------------------------
같다​


조회하려는 엔티티가 1차 캐시에 존재하지 않는 경우 
즉시 데이터베이스에 해당 엔티티를 조회하여 엔티티를 생성하고 1차 Cache 에 저장한다.
이후 여러번 조회하면 1차 캐시에 있는 엔티티를 반환함으로 동일성 보장이 된다.
            try
            {
                UserEntity user = new UserEntity();
                user.setId(12L);
                user.setUsername("name");
                entityManager.persist(user);

                entityManager.flush();
                System.out.println("1차 캐시 초기화");
                entityManager.clear();

                System.out.println("COMMIT 이전 1차 캐시에 아무것도 없음으로  DB 에서 엔티티 조회");
                UserEntity userEntity  = entityManager.find(UserEntity.class, user.getId());

                System.out.println("이후 1차 캐시에 데이터베이스에서 조회 한 값이 저장됨으로 여러번 호출해도 동일성 보장");
                UserEntity userEntity2 = entityManager.find(UserEntity.class, user.getId());
                System.out.println(userEntity2.equals(userEntity) ? "같다" : "다르다");
                tx.commit();
            }
            
------------------------
Hibernate: 
    /* insert org.example.javamain.Entity.UserEntity
        */ insert 
        into
            USR
            (age, createDate, descriptioin, lastModifiedDate, roleType, name, id) 
        values
            (?, ?, ?, ?, ?, ?, ?)
1차 캐시 초기화
COMMIT 이전 1차 캐시에 아무것도 없음으로  DB 에서 엔티티 조회
Hibernate: 
    select
        userentity0_.id as id1_24_0_,
        userentity0_.age as age2_24_0_,
        userentity0_.createDate as createDa3_24_0_,
        userentity0_.descriptioin as descript4_24_0_,
        userentity0_.lastModifiedDate as lastModi5_24_0_,
        userentity0_.roleType as roleType6_24_0_,
        userentity0_.name as name7_24_0_ 
    from
        USR userentity0_ 
    where
        userentity0_.id=?
이후 1차 캐시에 데이터베이스에서 조회 한 값이 저장됨으로 여러번 호출해도 동일성 보장
같다

1차 Cache 가 초기화 되어 즉시 DB 에서 엔티티를 조회하는 것을 볼 수 있다.
이후 조회된 엔티티가 1차 캐시에 존재하기 때문에 동일성 보장이 되는 것을 볼 수 있다.
이 때문에 만약 1차 캐시에 존재하는 엔티티 정보를 데이터 베이스에서 조회해야할 일이 있다면 
1차 Cache 를 초기화 하고 다시 엔티티를 조회해야한다.

 

1차 캐시의 기능 | 2. 더티 체킹 

1차 캐시는 엔티티가 영속화 될 때 상태를 저장해 둔다. ( SNAP SHOT )
이후 영속화된 엔티티의 상태가 변경 되면 스냅샷과 비교하여 변경된 필드를 감지한다.
( 이때 변경된 필드가 존재하는 엔티티를 "더티" 라고 한다 )
감지된 엔티티들은 쓰기 지연 큐에 등록이 되고,
이후 flush 메소드가 발생하면 변경 감지된 엔티티에 대한 UPDATE 쿼리가 데이터베이스로 전송되어 반영된다.
영속화 된 객체의 필드 값만 변경해도 커밋 시 Update 쿼리가 자동으로 생성 되는 것이다.

트랜잭션이 종료되거나 강제로 쿼리를 발생하지 않는 이상 데이터 베이스에 
변경사항이 적용되지 않음으로 쓰기 지연 효과를 가질 수 있고 
엔티티가 수정 되더라도 수정 쿼리를 따로 작성하지 않아도 되며 이런 장점들로 인한 
성능 향상과 데이터의 일관 성을 유지 할 수 있다.

                UserEntity user = new UserEntity();
                user.setId(12L);
                user.setUsername("name");
                entityManager.persist(user);
                System.out.println("변경 감지");
                user.setUsername("유저 네임");

                System.out.println("commit 시 flush 됨!");
                tx.commit();
-----
변경 감지
commit 시 flush 됨!
Hibernate: 
    /* insert org.example.javamain.Entity.UserEntity
        */ insert 
        into
            USR
            (age, createDate, descriptioin, lastModifiedDate, roleType, name, id) 
        values
            (?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    /* update
        org.example.javamain.Entity.UserEntity */ update
            USR 
        set
            age=?,
            createDate=?,
            descriptioin=?,
            lastModifiedDate=?,
            roleType=?,
            name=? 
        where
            id=?
-----
영속화 된 객체의 변경만으로 Update 쿼리가 발생하는 것을 볼 수 있다

 

 

마무리

1차 캐시와 쓰기 지연 큐에 대해서 알아보며 "더티 채킹" 에서 더티의 의미를 알 수 있었고, 쓰기 지연이 JPA 에서 지원하는 게 아니라 하이버네이트 구현체에서 지원하는 기능 인 것을 알게 되었다. 다음에는 영속화 할 때 사용하는 EntityManager 의 메소드 들에 대해서 알아보고 Bulk 연산과 Merge 와 비교해 보며 영속성 컨텍스트에 대한 내용을 마무리 해야겠다.

 

 

BELATED ARTICLES

more