지나공 : 지식을 나누는 공간

JPA 2 : 영속성 컨텍스트(Persistence Context)란? - (EntityManager, EntityFactory, Flush, transactional write behind) 본문

Tech/JPA

JPA 2 : 영속성 컨텍스트(Persistence Context)란? - (EntityManager, EntityFactory, Flush, transactional write behind)

해리리_ 2020. 9. 10. 23:41

김영한 님의 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 정리했습니다.

 

JPA에서 가장 중요한 것이 '객체와 관계형 데이터베이스 매핑하는 것', 그리고 영속성 컨텍스트입니다.

영속성 컨텍스트를 알아기 전에 먼저 짚어야 할 개념들이 있습니다. 

 

 

EntityManagerFactory, EntityManager ?

 

데이터 요청이 들어올 때마다 EntityManagerFactory는 EntityManager를 생성하고, 이 EntityManager는 데이터베이스커넥션을 활용해서 DB에 접근하여 요청사항을 처리합니다.

 

 

 

EntityManager 사용

EntityManager를 사용해 봅시다. resources 폴더 안에 있는 persistence.xml 파일에서 persistence-unit name은 JPA를 사용하는 메소드에서 EntityManagerFactory를 생성할 때의 파라미터와 일치해야 합니다. 

 

persistence.xml
main함수에서 EntityManagerFactory 생성

 

영속성 컨텍스트(Persistence Context)란?

 

눈에 보이지 않는 논리적인 개념으로, EntityManager를 통해서 영속성 컨텍스트에 접근할 수 있는 환경입니다.

엔티티를 영구 저장하는 환경이라고 직역되고 영속성 컨텍스트를 통해 해당 엔티티를 영속화한다는 의미입니다.

글로는 이해하기 어려우니, 바로 이어질 엔티티의 생명주기까지만 글로 이해하고 그 이후에 코드를 함께 보도록 할게요.

 

 

엔티티의 생명 주기

 

- 비영속(new) : 영속성컨텍스트에 아직 안 들어간, 새로 생성된 상태로 영속성 컨텍스트와 무관한 새로운 상태

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

- 준영속(detached) : 영속성 컨텍스트에 저장되어 있다가 분리된 상태로 영속성 컨텍스트가 제공하는 기능을 쓸 수 없음

- 삭제(removed) : 삭제된 상태

 

영속성 컨텍스트에 관리되는 영역에 엔티티가 들어가냐 마냐와 관련해서 엔티티의 생명주기를 나타낼 수 있습니다.

 

사용하면서 이해하기

 

Member라는 객체(엔티티)가 있고, tx는 EntityManager애서 getTransaction을 통해 만든 트랜잭션입니다.

 

em.persist(member);

 

맨 처음 member객체를 만든 뒤 persist 메소드를 호출하면 DB에 member 엔티티가 저장될 것 같지만!? 저장되지 않고 영속성 컨텍스트로 들어가서 엔티티가 영속상태가 됩니다. 코드를 실행했을 때, 쿼리문이 생성되어 실행되는 시점이 DB에 저장되는 시점이고 위의 코드를 실행하면 쿼리문이 실행되는 건 persist다음이 아니라 commit() 메소드가 실행될 때 입니다.

 

em.detach()는 엔티티를 영속성 컨텍스트에서 분리하여 준영속 상태로 만드는 메소드입니다.

em.remove()는 객체 삭제를 요청합니다.

 

 

영속성 컨텍스트의 이점

 

- 1차 캐시

- 동일성 보장

- 트랜잭션을 지원하는 쓰기 지연(transactional write behind)

- 변경 감지

- 지연 로딩 ( 나중에 설명)

 

1차 캐시

 

영속성 컨텍스트에 들어간 엔티티에 대해서 특정 id에 해당되는 데이터를 찾는다고 해 봅시다.

//em은 EntityManager
Member member = new Member();
member.setId("member1");
member.setName("멤버1");

//영속성 컨텍스트에 의해 관리되도록 영속상태로 만들기
em.persist(member);
Member findMember = em.find(Member.class , "member1");

em.persist(member); 를 하는 순간 member라는 객체는 1차 캐시에 저장되고, 아직 트랜잭션 commit()을 하지 않았으므로 DB에는 저장되지 않은 상태가 됩니다. 하지만 잘 찾아서 출력합니다. 왜?

 

여기서 find를 하면 DB에 가기 전에 1차 캐시로 가서 있는 지 찾아봅니다. 1차 캐시에 해당 내용이 있으면 DB에 안 가고 1차 캐시에서 찾아서 보여줍니다. 

 

 

이번엔 같은 걸 연속으로 두 번 찾아보겠습니다.

//em은 EntityManager
Member member = new Member();
member.setId("member1");
member.setName("멤버1");

//영속성 컨텍스트에 의해 관리되도록 영속상태로 만들기
em.persist(member);
Member findMember1 = em.find(Member.class , 101L);
Mmeber findMember2 = em.find(Member.class, 101L);

 

 

지금 조건에 맞는 객체를 찾으려는데 만약 이게 처음으로 찾는 거라면 DB에만 객체가 있고 1차 캐시에는 없는 상태일 것입니다. 따라서 처음엔 DB로 가서 select문으로 검색을 해야 하니까 쿼리문이 실행됩니다. 그리고 이 때 DB에서 찾아온 데이터를 1차 캐시에 올립니다. 그러면 같은 조건으로 한 번 더 find를 하게 된다면 1차 캐시를 먼저 찾아봤을 때 찾을 수 있으므로 쿼리문이 실행되지 않습니다. 즉, 똑같은걸 두번 찾게 되면 쿼리문은 한 번 실행되는 겁니다.

 

 

동일성보장

 

바로 위에서 본 코드에서처럼, 같은 것을 두 번 꺼내어 저장한 두 객체를 비교해보면 같다는 if문에 대해 true가 반환됩니다. 마치 자바 컬렉션에서 같은 것을 꺼내면 똑같은 레퍼런스에 있는 애를 꺼내므로 둘을 비교했을 때 같은 것 처럼 말입니다. jpa에서 이러한 동일성 보장이 되는 것은 1차 캐시가 있기 때문에 가능합니다. 1차 캐시는 같은 엔티티가 있으면 해당 엔티티를 그대로 반환하기 때문입니다.

 

트랜잭션을 지원하는 쓰기 지연(transactional write behind)

 

commit메소드 실행 전까지는 쿼리를 실행하지 않다가 commit을 하면 이전의 나온 것들에 대한 sql문을 생성합니다.

처음 commit전에 여러 개의 sql문이 요청되면 sql문들은 전부 쓰기 지연 sql 저장소로 쌓이고 엔티티는 1차 캐시에 저장됩니다. 그러다가 트랜잭션을 commit하는 시점에 쓰기 지연 sql 저장소에 있던 sql문들이 flush됩니다. (DB로 들어가서 실행됨)

 

transactional write behind를 활용하는 방안으로는 JDBC 배치가 있는데, hibernate.jdbc.batch_size의 value를 설정하면 모았다가 DB에 한번에 넣을 수 있습니다.

 

변경감지

 

id를 통해 데이터를 find한 다음에 변경을 한다고 생각하면, 다시 persist메소드를 통해 변경된 내용을 1차캐시(영속성컨텍스트)로 넣어야 할 것 같지만, 그럴 필요 없습니다. jpa의 목적은 자바 컬렉션처럼 쓰고 싶은 거예요. 따라서 컬렉션 사용하는 것처럼 그냥 변경하려는 값으로 set을 하면 DB에 update 쿼리가 실행됩니다.

 

스냅샨은 맨 처음 최초로 1차 캐시에 들어온 상태를 가지고 있는데요. 그러다가 어떤 데이터를 변경하면 commit하는 시점에 jpa가 스냅샷과 현재 엔티티를 일일이 비교합니다. 그러다 둘이 다르면 update 쿼리를 쓰기 전에 쓰기 지연 sql 저장소에 update 쿼리를 저장해 놓고 commit할 때 이 쿼리를 실행합니다.

 

 

 

플러시(Flush)

 

영속성 컨텍스트의 변경된 내용을 데이터베이스에 반영하는 것입니다.

 

과정은,

변경 감지 -> 수정된 엔티티에 대해 쓰기 지연 sql 저장소에 update 쿼리문을 등록 -> commit 시점에 쓰기 지연 sql문 저장소에 있는 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제의 쿼리) 하는 순입니다.

 

 

플러시를 하는 방법

 

- em.flush(); 엔티티 매니저를 통해 직접 flush를 호출하는 방식

 : 쿼리가 어떻게 생겼는 지 보고 싶을 때 강제호출 가능하고, DB에 반영됩니다.

- 트랜잭션 커밋 : 플러시가 자동 호출 됩니다.

- JPQL 쿼리 실행 : 플러시가 자동호출 됩니다.

 

em.flush() 를 하고 나서 em.clear()를 하면,

flush를 호출할 때 DB에서 조회해서 영속성 컨텍스트에 데이터를 올려 놓고,

clear를 호출하면서 그 영속성 컨텍스트에 올려진 데이터가 사라집니다. 이렇게 되면 같은 것을 호출했을 때 또다시 쿼리문이 날아가니까 우리가 직접 쿼리문을 확인할 수 있습니다.

728x90
Comments