앞선 포스팅에서 JPA 가 동작하는 과정에서 EntityMangerFactory 와 EntityManager 가 생성되는 과정을 살펴보았다.
해당 과정을 이미지화해서 보자면 다음과 같다.
그리고 DB에 영속화하기 위한 코드를 상기해보면 다음과 같다.
EntityManger.persist(entity);
그렇다면 persist 를 한다는 것은 DB에 영속화를 시킨다는 것일까?
정답은 '아니다' 이다.
이때 persist() 를 하면, DB에 저장되는 것이 아니라 '영속성컨텍스트' 라는 논리적 계층에 영속화하게 된다.
즉, EntityManger 는 DB를 관리하는 주체가 아니고, 영속성컨텍스트를 관리하는 주체인 것이다.
그리고 commit 이 일어날 때 영속성컨텍스트에 영속화된 Entity 들을 실제 DB에 영속화 하게 되는 것이다.
그렇다면 왜 DB에 바로 영속화 하지 않고 이렇게 논리적인 계층을 만들어 사용하는 것일까?
앞선 시간에 우리가 JPA 를 사용하는 이유 중 하나는 성능최적화가 있었다.
그리고 성능최적화가 가능한 이유가 서로 다른 두 세계를(ex : 웹어플리케이션과 RDB) 이어주는 계층에서 버퍼와 캐싱이 가능하기 때문이라고 설명하였다.
바로 영속성컨텍스트라는 이 논리적 공간이 두 세계를 이어주는 과정에서 버퍼의 역할을 하기도 하고 캐시의 역할을 하기도 하는 공간이 되는 것이다.
자, 그럼 이제 JPA의 정확한 구조를 다시 한번 이미지로 살펴보도록 하자.
JPA에서 Entity 는 다양한 상태로 존재할 수 있다. 어떠한 상태들로 존재하는지 확인해보자
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()
영속성컨텍스트 에서는 내부에서 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 를 활용해 가능한 일이다.
스냅샷은 엔티티를 영속성컨텍스트에 최초로 영속화 했을때의 엔티티를 의미한다.
앤티티매니저가 commit() 을 호출해 진행되는 과정을 살펴보며 변경감지에 대해 이해해보자.
Flush 는 영속성컨텍스트의 변경내용과 DB를 동기화 하는 작업을 의미한다.
영속성컨텍스트에 flush 가 일어나는 경우는 3가지 정도로 볼 수 있는데
1. 엔티티매니저를 통해 직접 flush() 호출
2. 앤티티매니저가 얻은 트랜잭션을 commit() 할 때
3. JQPL 쿼리실행전 자동호출 (Optional 하게 바꿀 수 있지만 default 는 변경이다)
Flush 는 다음과 같은 흐름으로 DB를 동기화 하게된다.
해당 포스팅은 김영한님의 '자바 ORM 표준 JPA 프로그래밍' 강의를 참고하여 작성된 내용입니다.