영속성 컨텍스트에 대해
본 내용은 자바 ORM 표준 JPA 프로그래밍 책을 토대로 정리했습니다.
영속성 컨텍스트란 "엔티티를 영구 저장하는 환경"이라는 뜻이다.
EntityManager.persist(entity)
이것은 entity를 영속성 컨텍스트에 저장한다는 뜻이다.
엔티티의 생명주기
- 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계까 없는 새로운 상태
- 영속(managed) : 영속성 컨텍스트에 관리되는 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) : 삭제된 상태
Member member = new Member();
member.setId("member1");
member.setUsername("회원 1");
위의 상태는 비영속 상태이다. Member 객체만 만들어준 상태이다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(member);
엔티티 매니저를 통해서 persist 하게 되면 영속성 컨텍스트 안에 member가 들어가게 되고, 영속 상태가 된다.
영속 상태는 db에 저장이 되는 상태가 아니다.
em.detach(member)
회원 엔티티를 영속성 컨텍스트에서 분리한다. 준영속 상태가 된다.em.remove(member)
객체를 삭제한 상태다. 삭제 상태.
영속성 컨텍스트의 이점
- 1차 캐시
- 동일성(identity) 보장
- 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
- 변경 감지(Dirty Checking)
- 지연 로딩(Lazy Loading)
1차 캐시
Member member = new Member();
member.setId("member1");
member.setUsername("회원 1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
em.find 를 만나면 db에 바로 접근하는 것이 아니라 1차 캐시에서 먼저 조회를 한다.
이 상황에서 Member findMember2 = em.find(Member.class, "member2");
를 만나게 된다면??
이 때에는 1차캐시에서 member2를 찾고 만약 없다면 db를 조회하고 그 결과를 1차 캐시에 저장을 하고 member2를 반환한다.
이 후에 member2를 재조회하게 된다면 1차 캐시에서 가져다 쓴다.
하지만, 그렇게 큰 도움이 되지는 않는다. 트랜잭션 종료 시에 영속성 컨텍스트도 비워주기 때문에 찰나의 순간에나 도움이 된다.
애플리케이션 전체에서 사용하는 그런 캐시가 아니기 때문에(2차캐시 처럼), 한 트랜잭션 안에서만 이루어지기 때문에 큰 성능의 이점이 되지는 않는다.
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 1L);
다음을 실행할 때에 쿼리문은 1번만 나가게 된다. findMember2는 1차 캐시에서 반환받아 사용하게 된다.
동일성 보장
위의 findMember1와 findMember2를 == 비교를 하게되면 같다는 결과가 나온다.
JPA는 영속 엔티티의 동일성을 보장해준다는 것이다. 이게 가능한 이유는 1차 캐시 덕이라고 할 수가 있다.
트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
em.persist(memberA);
em.persist(memberB);
transaction.commit();
em.persist()를 하는 순간 insert sql를 db에 보내지 않는다. 커밋하는 순간에 보낸다. 그 전까지는 계속 쌓고 있는다.
먼저, em.persist(memberA); 를 하면 1차 캐시에 memberA가 들어가게 된다.
이 순간에, jpa가 이 엔티티를 분석해서 insert 쿼리를 생성해서 쓰기 지연 sql 저장소에 쌓아 둔다.
em.persist(memberB); 를 하면 이 때에도 마찬가지로 1차 캐시에 memberB를 넣고, insert sql를 쓰기 지연 sql 저장소에 쌓아 둔다.
transaction.commit();를 하는 순간 쓰기 지연 sql 저장소에 쌓인 쿼리문 들을 db에 날리게 된다.
변경 감지(Dirty Checking)
Member member = em.find(Member.class , 1L);
member.setName("changedName");
em.persist()를 해주지 않아도 db에 update문을 날리게 된다. JPA는 자바 컬렉션에서 하는 것 처럼 알아서 변경이 된다.
JPA는 commit 하는 시점에 flush()를 호출을 하는데, 엔티티와 스냅샷을 비교한다.
1차 캐시 안에는 pk 값과 엔티티 그리고 스냅샷이라는 것이 있다. 스냅샷은 값을 읽어온 최초 시점의 상태를 가진다.
이제 commit 시점에서 JPA가 이것들을 비교를해서 update sql를 쓰기 지연 sql 저장소에 만들고 update 쿼리를 db에 반영을 하고 commit 한다.
update를 하게되면 변경된 엔티티 값이 스냅샷에 저장되게 된다.
ex) 만약 Member객체를 만들고 set으로 값을 넣어준 후에 persist를 하고 값을 변경한다면 처음 persist 한 지점으로 스냅샷을 따므로 스냅샷을 찍으면서
insert 쿼리문을 쓰기 지연 sql 저장소에 쌓아 둔다. 그 후에 또 다시 set을 통해 값을 변경하고 commit을 하게되면 스냅샷과 값이 다르므로 update 문을
쓰기 지연 sql 저장소에 쌓고 이 저장소들을 db에 반영하게 되므로, Insert문 한 번 Update문 한 번이 실행되게 되는 것이다.
엔티티를 삭제하는 것도 동일하게 돌아간다. commit 시점에서 삭제된다.
flush() 발생하게 되면 변경 감지, 수정된 엔티티 쓰기 지연 sql 저장소에 등록, 쓰기 지연 sql 저장소의 쿼리를 db에 전송하게 된다.
그렇다면 이 flush()는 어떻게 발생시키는지 보자.
- em.flush() - 직접 호출
- 트랜잭션 커밋 - 플러시 자동 호출
- JPQL 쿼리 실행 - 플러시 자동 호출
플러시를 한다고 1차 캐시가 삭제되는 것은 아니다. 그저 변경 감지를 하고 쓰기 지연 sql 저장소의 쿼리들이 db에 반영될 뿐이다.
트랜잭션이라는 작업 단위가 중요하므로 커밋 직전에만 동기화를 하면 된다. 그래서 플러시가 트랜잭션 커밋시에 호출이 되는 것이다.
준영속 상태로 만드는 방법
em.clear()
를 통해 영속성 컨텍스트를 통채로 비우는 방법em.detach(entity)
를 통해 특정 엔티티만 준영속 상태로 전환하는 방법em.close()
를 통해 영속성 컨텍스트를 종료하는 방법
find 시에 1차 캐시에 값이 있다면 그것을 사용하게 되는데, 다른 서버에서 해당 db의 값을 바꿔준다면 이것도 더티 체킹을 해줄까?
이런 상황을 위해 JPA는 db가 제공하는 락 기능을 제공하고 낙관적 락이라는 기능도 제공하게 된다.
'Spring' 카테고리의 다른 글
[JPA] 프록시와 연관관계에 대해 (0) | 2020.10.30 |
---|---|
[JPA] 필드와 컬럼 매핑에 대해 (0) | 2020.10.30 |
[Spring] 의존관계 주입에 대해 (0) | 2020.10.22 |
[Spring] ComponentScan에 대해 (0) | 2020.10.21 |
[Spring] 빈 스코프 (0) | 2020.10.21 |