Persistence Context특징Cache1차 캐쉬2차 캐쉬(= 공유 캐시)EntityManagerFactoryEntity ManagerContainer-Managed EntityManagerApplication-Managed EntityManagerEntity life cycle비영속상태(new, transient)영속상태(managed)준영속상태(detached) — detached 객체를 persist해버리면 에러 발생[참고]삭제(removed)repository.save() — persist 🆚 mergeCustomer Entity를 통한 영속성컨텍스트 이해EntityManager와 EntityManagerFactory 주입받아 이용하기저장Lazy Writerhibernate의 batch size 옵션조회1차 캐시를 이용한 조회DB를 이용한 조회수정(Dirty Checking)삭제
중요한점
- 영속성 컨텍스트는 트랜잭션을 시작할때 생성되고 트랜잭션이 종료될때 없어짐
- 하나의 트랜잭션 안에서 findById로 조회할 때, 해당 값을 보관하고 있음
- findByEmail 등 다른 것으로 조회 시, 조회마다 쿼리 발생하지만 findById로 조회하여 cache처리 되어 있으면 쿼리가 첫번째에만 발생하고 그 후에는 생기지 않음
Persistence Context
- 보통 메모리에 존재하는 데이터는 서비스가 종료되면 사라지는데, 그러한 데이터를 사라지지 않고 지속적으로 처리하는 방법은 파일 혹은 db.
- 영속성 컨텍스트는 JPA 컨테이너 안에서 동작을 하는 entity의 맥락을 관리하는 주체
- 엔티티를 영구 저장하는 환경
- JPA를 이용하는데 가장 중요한 요소임
- 영속성 컨텍스트에 실질적으로 가장 많은 역할을 하는 것이 EntityManager
- JPA를 J2EE나 스프링 프레임워크 같은 컨테이너 위에서 실행하면 트랜잭션을 시작할 때
영속성 컨텍스트를 생성
하고 트랜잭션을 종료할 때영속성 컨텍스트도 종료
함 (OSIV 를 사용하면 요청의 시작부터 끝까지 같은 영속성 컨텍스트를 유지)

- EntityManager가 persist( ) 를 호출하게 되면 entity가 Persistence Context에서 관리되게 됨
특징
- 영속성 컨텍스트와 식별자 값
- 영속성 컨텍스트 안에서 관리되는 엔티티는 식별자 값을 반드시 가져야한다.
- 그 이유는 key-value로 엔티티를 관리하기 때문이다.
- 1차 캐시에서도 보면 @Id와 Entity이 key-value 형태로 entity를 찾게 됨
- 영속성 컨텍스트와 데이터 베이스 저장
- JPA는 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 DB에 반영한다. (FLUSH)
- 플러시(flush)는 영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업인데, 이때 등록, 수정, 삭제한 엔티티를 DB에 반영한다.
- 영속성 컨텍스트가 엔티티를 관리함으로 얻는 이점
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연(Lazy Writing)
- 변경 감지(dirty checking)
- 지연 로딩
Cache

- 하나의 트랜잭션 안에서 findById로 조회시, 해당 값 보관하고 있음
- 1차 캐쉬는 맵의 형태로 만들어짐 (key는 id 값, value 는 해당 entity)
- findByEmail 등 다른 것으로 조회 시, 조회마다 쿼리 발생하지만 findById로 조회하여 cache처리 되어 있으면 쿼리가 첫번째에만 발생하고 그 후에는 생기지 않음
- DB에 반영되는 시점
- flush() 를 호출하는 시점
- Transaction이 끝나서 해당 쿼리가 커밋되는 시점
- 복잡한 조회조건의 JPQL query가 실행되는 시점
- dirty checking : Transaction 안에서 entity의 변경이 일어나면 캐시에 저장되어 있는 snapshot과 비교하여 entity의 변경 내용을 자동으로 데이터베이스에 반영하는 JPA의 특징임
1차 캐쉬
- 영속성 컨텍스트 내부에 엔티티를 보관하는 저장소.
1차 캐시는 트랜잭션이 시작하고 종료할 때까지만 유효함(Persistence context flush 혹은 clear 하기 전까지는 1차 캐시가 비워지지 않음)
- Entity Manager로 조회하거나 변경하는 모든 엔티티는 1차 캐시에 저장됨
- 트랜잭션을 Commit하거나 flush 하게되면 1차 캐시에 있는 entity 변경 사항들 db반영함
2차 캐쉬(= 공유 캐시)
- 2차 캐시는 애플리케이션 범위의 캐시이고 애플리케이션을 종료할 때까지 캐시가 유지됨
- 2차 캐시는 데이터베이스 기본 키를 기준으로 캐시하지만 영속성 컨텍스트가 다르면 객체 동일성(a==b)을 보장하지 않는다.
EntityManagerFactory
- Entity를 관리하는 EntityManager를 생산하는 공장
- Thread Safe함
- 하나만 만들어짐. 꽤나 무거운 객체이기 때문에
Entity Manager

- EntityManager는 Entity를 저장하고 수정하고 삭제하고 조회하는 (CRUD) 등 Entity와 관련된 모든 일을 처리함
- Thread Safe하지 않음. 여러 Thread에서 동시에 접근할 경우 동시성 이슈가 발생함
- EntityManager Factory가 각각의 request별 Thread안에 각각 만들어주게 됨. 그리고 db와의 작업을 위해서 connection을 획득함
- JPA의 구현체들은 EntityManagerFactory를 생성할 때 커넥션풀을 만듦
- Repository를 이때까지 만들어서 사용했지만 SimpleJpaRepository안에 EntityManager를 통하여 쿼리를 만들고 데이터를 불러옴. custom하게 하고 싶으면 EntityManager를 가지고 변형해야함
- Hibernate에서는 EntityManager를 session이라고 함
- 기본적으로 EntityManager에는 두가지 종류가 있다
- Container Managed EntityManager
- Application Managed EntityManager
Container-Managed EntityManager
- Spring에서는 EntityManager를 Proxy를 통해 감싸고 EntityManager를 사용할 때 Proxy를 통해 EntityManager를 생성함(Autowire로 EntityManager를 받으면 SharedEntityManagerCreator가 proxy를 통해 생성을 해줌 → 여러 스레드에서 동시 접근해도 Thread Safety를 보장해줌)
- 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용함
- 트랜잭션이 시작할 때(tx.begin( )) 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때(tx.commit( )) 영속성 컨텍스트를 끝낸다.(by 컨테이너)
- 즉 @Transactional이 붙은 하나의 메서드를 시작하면서 영속성 컨텍스트 생성하고, 그 메서드가 끝나면 영속성 컨텍스트가 끝나는 것임!. 그 해당 메서드에 여러 request가 동시에 들어온다고 하더라도 별개의 트랜잭션이 생성되는 것이니 엔티티 매니저도 별개, 영속성 컨텍스트도 별개인 것!
- 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용함(여러 EntityManager를 사용해도 한 트랜잭션으로 묶이면 영속성 컨텍스트를 공유함)
- 하나의 트랜잭션으로 묶인 메서드에 여러 request가 들어온다고 했을 때 하나의 트랜잭션으로 묶이니 영속성 컨텍스트는 공유한다는 것임
- 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용함
- 같은 EntityManager를 사용해도 트랜젝션에 따라 접근하는 영속성 컨텍스트가 다르다
- 따라서 같은 EntityManager를 호출해도 접근하는 영속성 컨텍스트가 다르므로 멀티스레드에 안전함!
Application-Managed EntityManager
EntityManagerFactory emf = Persistence.createEntityManagerFactory("com.baeldung.movie_catalog"); EntityManager em = emf.createEntityManager(); // 직접 생성.
- 우리가 직접 생성했으니, 닫는 것도 우리의 책임임
Entity life cycle

비영속상태(new, transient)
- 영속성 context가 해당 entity 객체를 관리하지 않는 상태
- @Transient 를 필드에 붙이면 database에 만들지 않음
- 그냥 단순한 자바객체. 영속화되지 않고 garbage collector에 의해 사라짐
Customer customer = new Customer(); customer.setId(1L); customer.setFirstName("honggu"); customer.setLastName("kang";
영속상태(managed)
entityManager.persist(user);
로 객체를 persistence context에서 관리되도록 했을 때, 영속상태로 전환
- 영속상태인 경우
- 변경 감지(dirty check)
- 조회에 대한 1차 캐쉬(id로)
- 쓰기 지연
Customer customer = new Customer(); customer.setId(1L); customer.setFirstName("honggu"); customer.setLastName("kang"; em.persist(customer);
준영속상태(detached) — detached 객체를 persist해버리면 에러 발생[참고]
@Transactional public void put(){ User user = new User(); user.setName("newUser"); user.setEmail("newUser@gmail.com"); entityManager.persist(user); entityManager.detach(user); // 일반적으로는 거의 안씀. persistence context에서 관리 안함 user.setName("newUserAfterPersist"); // -> detach로 인하여 update 쿼리 발생안함 // 관리되고 있다면 Transactional 덕분에 dirty checing이 일어나 update 쿼리 발생 entityManager.merge(user); // merge the state of the given entity into the current // persistence context. // merge를 함으로써 update 쿼리 발생 // 보통 clear 쓰려면 그 전에 flush()를 사용하여 db에 반영하고 사용함 entityManager.clear(); // /* 영속상태의모든 객체를 영속성컨텍스트에서 분리함 이때까지의 변경사항들 다 없애고 준영속상태. */ em.close() // 영속성컨텍스트를 종료 }
삭제(removed)
- delete 쿼리임
em.remove(customer)
— 영속성 컨텍스트에서 분리하고, DB에서도 삭제함
repository.save() — persist 🆚 merge
@Test void persist() { Customer customer = new Customer(); customer.setFirstName("honggu"); customer.setLastName("kang"); Customer ret = repository.save(customer); Assertions.assertThat(ret == customer).isTrue(); } @Test void merge() { Customer customer = new Customer(); customer.setId(1L); customer.setFirstName("honggu"); customer.setLastName("kang"); Customer ret = repository.save(customer); Assertions.assertThat(ret != customer).isTrue(); } public boolean isNew(T entity) { ID id = this.getId(entity); Class<ID> idType = this.getIdType(); if (!idType.isPrimitive()) { return id == null; } else if (id instanceof Number) { return ((Number)id).longValue() == 0L; } else { throw new IllegalArgumentException(String.format("Unsupported primitive id type %s!", idType)); } } @Transactional public <S extends T> S save(S entity) { Assert.notNull(entity, "Entity must not be null."); if (this.entityInformation.isNew(entity)) { this.em.persist(entity); return entity; } else { return this.em.merge(entity); } }
- save를 호출했을 시, isNew를 통해 persist와 merge로 분기를 하게 됨
- isNew는 primitiveType이면 0인지 확인하고, referenceType이면 null인지 확인함
- setId를 호출 시
- id값이 있으니 merge를 호출 → parameter와 return instance가 같지않음
- id값이 없으면 persist를 호출 → parameter instance와 return instance가 같음
- merge호출 전에 detach 하고 나서 하면 반환되는 값 다름
- Entity 새거 = em.merge(옛날거)
- 옛날거 = 준영속 여전히
- 그냥 반환된 객체를 이용하기! 헷갈리니까
Customer Entity를 통한 영속성컨텍스트 이해
- 데이터를 조작하기 위해서는 Transaction begin, commit 안에서 하기. CUD
EntityManager와 EntityManagerFactory 주입받아 이용하기
[ StackOverflow ] PersistenceUnit vs PersistenceContext
@PersistenceContext
: EntityManager를 주입해줌
@PersistenceUnit
: EntityManagerFactory를 주입
class SupportRepositoryTest( @PersistenceContext private val entityManager: EntityManager ) : BaseTest({ describe("test") { val now = now(Timezone.KOR) context("test") { val event1 = SupportEventEntity( 0, PolicyTarget.COMMON, FileData(null), FileData(null), FakeValue.email(), 1, true, "메인 이벤트", "테스트 컨텐츠", false ) it("test2") { val transaction = entityManager.transaction transaction.begin() entityManager.persist(event1) transaction.commit() } } } })
@PersistenceContext
를 이용하여 위처럼 사용하면 예외 발생Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead
대신
@PersistenceUnit
을 통해 EntityManagerFactory를 주입받고 걔를 이용해 entityManager를 생성하여 사용하면 됨저장
EntityManager em = emf.createEntityManager(); // 1)엔티티 매니저 생성 EntityTransaction transaction = em.getTransaction(); // 2)트랜잭션 획득 transaction.begin(); // 3)트랙잰셕 begin Customer customer = new Customer(); // 4-1)비영속 customer.setId(1L); customer.setFirstName("honggu"); customer.setLastName("kang"); em.persist(customer); // 4-2)영속화 transaction.commit(); // 5)트랜잭션 commit // 트랜잭션이 커밋이 되는 순간 쿼리가 수행된다. flush DB와 동기화가 된다.

- Entity를 Persistence Context에서 관리하기 위해서 항상 Transaction을 시작하고 Transaction을 끝내야 함
- 그리고 Entity가 Persistence Context에서 관리되기 시작하면
- 쓰기 지연 저장소에 쿼리 저장(Insert into ... 쿼리)
- 1차 캐시에 key-value형태로 해당 Entity저장
- Transaction commit할 시,
- em.flush()
- 쓰기 지연 저장소에 있던 INSERT 쿼리를 DB에 적용
Lazy Writer
- query가 생성될 때마다 바로 DB에 보내지 않고, 어느정도 query를 쌓은 뒤 한번에 보내는 것을 쓰기 지연이라 함
- 한번에 많은 query를 보내 DB connection 시간을 줄일 수 있고 한 transaction이 table에 접근하는 시간을 줄일 수 있는 장점이 있다.
- CUD 쿼리를 쌓아뒀다가 한번에 보냄. DB에 데이터를 업데이트 하는 쿼리들
hibernate의 batch size 옵션
<property name="hibernate.jdbc.batch_size" value="10"/>
위 옵션을 추가하면 value 만큼 쿼리를 쌓은 다음에 DB로 한번에 query들을 보내 commit 한다.
조회
1차 캐시를 이용한 조회

@Test void 조회_1차캐시_이용() { EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); transaction.begin(); Customer customer = new Customer(); customer.setId(1L); customer.setFirstName("honggu"); customer.setLastName("kang"); em.persist(customer); transaction.commit(); Customer entity = em.find(Customer.class, 1L); /* 1차 캐시에서 조회한다. == 쿼리가 날라가지 않음 */ log.info("{} {}", entity.getFirstName(), entity.getLastName()); }
DB를 이용한 조회

@Test void 조회() { EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); transaction.begin(); Customer customer = new Customer(); customer.setId(2L); customer.setFirstName("guppy"); customer.setLastName("hong"); em.persist(customer); transaction.commit(); em.clear(); //영속성 컨텍스트를 초기화 한다. Customer entity = em.find(Customer.class, 2L); // DB 에서 조회한다. SELECT ... log.info("{} {}", entity.getFirstName(), entity.getLastName()); em.find(Customer.class, 2L); // SELECT Query 수행되지 않는다. 1차캐시 사용 }
수정(Dirty Checking)

- 1차 캐쉬에서 스냅샷을 관리함
- flush( ) 됐을 때, 1차 캐시의 스냅샷과 비교하여 변경사항이 있는지 확인함
- 변경사항이 있으면 update Query를 수행
- 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용이 됨
- em.clear() 로 영속성 컨텍스트에서 지워버리면 dirty checking 당연히 수행안됨. 비교할 대상(영속성 컨텍스트에 존재하는 snapshot)이 없으니까. 그리고 entity 또한도 detach 되어버리고
@Test void 수정() { EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); transaction.begin(); Customer customer = new Customer(); customer.setId(1L); customer.setFirstName("honggu"); customer.setLastName("kang"); em.persist(customer); transaction.commit(); // 엔티티를 영속화한후, 커밋을해서 flush()를 통해 DB에 저장. transaction.begin(); Customer entity = em.find(Customer.class, 1L); entity.setFirstName("guppy"); entity.setLastName("hong"); // em.update(entity) ??!! transaction.commit(); // flush -> UPDATE ... }
삭제
@Test void 삭제() { EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); transaction.begin(); Customer customer = new Customer(); customer.setId(1L); customer.setFirstName("honggu"); customer.setLastName("kang"); em.persist(customer); transaction.commit(); transaction.begin(); Customer entity = em.find(Customer.class, 1L); em.remove(entity); transaction.commit(); // flush -> DELETE .. }