영속성 컨텍스트

김종하·2021년 3월 14일
0

JPA

목록 보기
4/10

영속성컨텍스트(Persistence Context)

앞선 포스팅에서 JPA 가 동작하는 과정에서 EntityMangerFactory 와 EntityManager 가 생성되는 과정을 살펴보았다.
해당 과정을 이미지화해서 보자면 다음과 같다.

그리고 DB에 영속화하기 위한 코드를 상기해보면 다음과 같다.
EntityManger.persist(entity);
그렇다면 persist 를 한다는 것은 DB에 영속화를 시킨다는 것일까?
정답은 '아니다' 이다.
이때 persist() 를 하면, DB에 저장되는 것이 아니라 '영속성컨텍스트' 라는 논리적 계층에 영속화하게 된다.
즉, EntityManger 는 DB를 관리하는 주체가 아니고, 영속성컨텍스트를 관리하는 주체인 것이다.
그리고 commit 이 일어날 때 영속성컨텍스트에 영속화된 Entity 들을 실제 DB에 영속화 하게 되는 것이다.

그렇다면 왜 DB에 바로 영속화 하지 않고 이렇게 논리적인 계층을 만들어 사용하는 것일까?
앞선 시간에 우리가 JPA 를 사용하는 이유 중 하나는 성능최적화가 있었다.
그리고 성능최적화가 가능한 이유가 서로 다른 두 세계를(ex : 웹어플리케이션과 RDB) 이어주는 계층에서 버퍼와 캐싱이 가능하기 때문이라고 설명하였다.
바로 영속성컨텍스트라는 이 논리적 공간이 두 세계를 이어주는 과정에서 버퍼의 역할을 하기도 하고 캐시의 역할을 하기도 하는 공간이 되는 것이다.

자, 그럼 이제 JPA의 정확한 구조를 다시 한번 이미지로 살펴보도록 하자.

JPA 세상의 Entity 상태

JPA에서 Entity 는 다양한 상태로 존재할 수 있다. 어떠한 상태들로 존재하는지 확인해보자

  1. 비영속(new/transient) 상태
    영속성 컨텍스트와 전혀 관계가 없고, 새로 생성된 상태
  2. 영속(managed) 상태
    영속성 컨텍스트에 관리되는 상태
  3. 준영속(detached) 상태
    영속성 컨텍스트에 저장되었다가 분리된 상태
  4. 삭제(removed) 상태
    실제 DB에서 삭제될 상태

Managed(영속) / Detached(준영속) 상태

Entity 가 영속성컨텍스트에서 관리되는 (1차캐시에 있는) 경우는
entityManger.persist(entity) 를 했거나 entityManger.find(entity)를 한 경우가 있을 것이다.

이 경우를 1차캐시에 Entity 가 올라가고 Managed(관리중인/영속)상태가 되고 있다고 표현한다.

반대로 detached 상태가 된다는 것은 영속성컨텍스트가 관리하지 않는 (1차캐시에 없는) 상태가 되는 것을 의미한다.

entity 를 detached 상태로 만드는 경우를 살펴보자면 다음과 같다.
1. entityManger.detached(entity)
2. entityManger.clear()
3. entityManger.close()

영속성 컨텍스트 사용의 장점

1차 캐시

영속성컨텍스트 에서는 내부에서 Map 자료구조 (key-value 형식의 자료구조) 를 사용해 1차캐시를 생성해 사용한다

            Member member1 = new Member();
            member1.setId(1L);
            member1.setName("jaden");
            
            Member member2 = new Member();
            member2.setId(2L);
            member2.setName("son");

            em.persist(member1);
            em.persist(member2);

위와 같은 코드의 결과로 영속성 컨텍스트의 1차캐시는 다음과 같이 생겼을 것이다.

그렇다면 1차캐시를 통해서 얻을 수 있는 장점은 무엇일까?
조회 과정을 생각해보자.
만약, 1차캐시라는 개념이 존재하지 않으면 엔티티를 조회할때 DB에 SELECT 쿼리를 날려 조회하는 수 밖에 없다. 하지만 1차캐시가 있음으로써 우선, 1차캐시에 조회하려는 엔티티가 있는지 확인하고 없는 경우에만 DB에 쿼리를 날려 조회해오면 되는 것이다. 결과적으로 비용이 많이드는 DB와의 통신작업을 줄일 수 있게 되는 것이다.
(참고로 1차캐시에 없어 DB에서 조회해온 경우 해당 엔티티는 1차캐시에 저장된 후 1차캐시에서 반환된다)

영속 엔티티의 동일성 보장

객체 세상에서 동일성은 참조하는 인스턴스가 같은 인스턴스인가에 따라 결정된다.
그에 반해 RDB 세상에서 동일성은 같은 PK 인가에 따라 결정된다.
패러다임의 불일치가 벌어졌으니 이를 해결해야할텐데 어떻게 해결할 수 있을까?
영속성컨텍스트는 내부적으로 1차캐시를 사용해 key 값에 따라 동일성이 결정된다. 그리고 그 key 값은 pk 값과 동일하다.
즉, Map 이라는 자료구조를 활용하여 패러다임의 불일치를 해결할 수 있게 된 것이다.

쓰기지연

앞서서 말했듯, em.persist(entity)를 한다고 해서 DB로 바로 쿼리가 날라가지 않는다는 것을 기억하며 살펴보자.

영속성컨텍스트는 '쓰기지연 SQL 저장소' 를 가지고 있다. 해당 공간에 실제 DB로 날릴 쿼리들이 차곡차곡 쌓이게 된다.

4개의 entity를 영속화 하려한다고 가정하자

em.persist(entity1)
em.persist(entity2)
em.persist(entity3)
em.persist(entity4)

이 경우, 총 4개의 INSERT 쿼리가 쓰기지연 SQL 저장소에 쌓이게 된다.

그리고 commit() 을 날리는 순간 DB에 모아둔 쿼리가 날라가(flush 라고한다) DB에 영속화 된다.

참고로 한꺼번에 쿼리를 날리수 있는 이유는 JDBC batch 를 활용해 가능한 일이다.

변경감지 (Dirty Checking)

스냅샷은 엔티티를 영속성컨텍스트에 최초로 영속화 했을때의 엔티티를 의미한다.

앤티티매니저가 commit() 을 호출해 진행되는 과정을 살펴보며 변경감지에 대해 이해해보자.

  1. commit() 을 하게되면 엔티티 매니저 내부에서 먼저 플러시flush()가 호출된다.
  2. 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
  3. 변경된 엔티티가 있으면 UPDATE 쿼리를 생성해서 쓰기지연 SQL 저장소에 보낸다. ( UPDATE 쿼리를 생성하면, 변경된 엔티티가 스냅샷이 된다 )
  4. 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
  5. 데이터베이스 트랜잭션을 커밋한다

영속성컨텍스트 flush

Flush 는 영속성컨텍스트의 변경내용과 DB를 동기화 하는 작업을 의미한다.

영속성컨텍스트에 flush 가 일어나는 경우는 3가지 정도로 볼 수 있는데
1. 엔티티매니저를 통해 직접 flush() 호출
2. 앤티티매니저가 얻은 트랜잭션을 commit() 할 때
3. JQPL 쿼리실행전 자동호출 (Optional 하게 바꿀 수 있지만 default 는 변경이다)

Flush 는 다음과 같은 흐름으로 DB를 동기화 하게된다.

  1. 변경감지 (Dirty Checking)이 수행되어 스냡샷과 다른 엔티티가 있다면 UPDATE 쿼리를 쓰기지연 SQL 저장소에 추가한다.
  2. 쓰기지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.
    (단, flush 를 한다고 해서 commit 된다는 뜻은 아니다. 실제 commit 을 해줘야 DB에 영속화된다.

해당 포스팅은 김영한님의 '자바 ORM 표준 JPA 프로그래밍' 강의를 참고하여 작성된 내용입니다.

0개의 댓글