본문 바로가기

Back-End/JPA

영속성 관리 ( persistence )

JPA에서 제공해주는 기능은 크게 2가지이다.

  1. @Entity와 Table을 매핑하는 설계 부분
  2. 매핑한 @Entity를 실제로 사용하는 부분 ( CRUD )

Spring Boot 프로젝트를 할 때 @Entity 어노테이션을 명시만 해봤는데, 이번 포스팅에서 EntityManager가 매핑한 Entity를 어떻게 사용하는지 살펴보자.


Entity Manager와 Entity Manager Factory

Entity Manager Factory

데이터베이스를 하나만 사용하는 애플리케이션에서는 하나의 Entity Manager Factory를 생성한다. 설정 정보는 META-INF/persistence.xml에서 읽어 온다.
EntityManagerFactory emf 
= Persistence.createEntityManagerFactory("tedkim");

<persistence-unit name = "tedkim">
<properties>
<property name="javax.persistence.jdbc.driver"
value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.url"
value="jdbc:h2:tcp://localhost/~/test"
</properties>
</persistence-unit>



Entity Manager Factory는 말그래도 팩토리이기 때문에 만드는 비용이 크다. 그래서 한개만 만들어서 애플리케이션 전체에서 공유하도록 설계되어 있다. 그에 반해 Entity Manager는 생성 비용이 적다.

Entity Manager는 여러 스레드 간 공유하면 안된다.

아래 그림은 위의 설명을 나타내주는 그림이다. 
  • EntityManager가 생성된다고 connection을 얻지 않는다. ( EntityManager 1 ) 즉, 데이터베이스 연결이 꼭 필요한 시점에 connection을 얻는다.
  • JPA 구현체들은 EntityManagerFactory가 생성될 때 connection pool을 만든다. 스프링 프레임워크에서 사용하면 해당 컨테이너가 제공하는 DataSource를 사용한다.

entity manager factory에 대한 이미지 검색결과



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

우리가 회원 엔티티를 저장한다고 표현하는 것을 좀 더 정확하게 표현하면, '회원 엔티티를 엔티티 매니저가 영속성 컨텍스트에 저장한다.' 라고 할 수 있다. 즉, 영속성 컨텍스트는 엔티티를 영구 저장하는 환경이라 이해하면 된다. 예제를 보면서, 영속성 컨텍스트의 특징과 동작과정을 살펴보자.

Entity 조회

EntityManager em = emf.createEntityManager();

// Entity를 생성한 상태 ( 비영속 / new )
Collector collector = new Collector();
collector.setUserId("tedkim");

em.persist(collector);

위의 코드는 "tedkim"이라는 id를 가진 Collector 객체를 엔티티 메니저를 통해 영속성 컨텍스트에 저장한 것이다. 영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 '1차 캐시'라 한다. 


1차 캐시의 키는 데이터베이스의 기본키와 매핑되어 있다. 즉, 아래 그림에서는 userid가 캐시의 키가 되는 것이다.  엔티티를 조회해보자.


Collector find = em.find(Collector.class, "tedkim");

위의 함수를 호출하면 먼저 1차 캐시에서 엔티티를 찾고 만약 찾는 값이 1차 캐시에 없으면 데이터베이스에서 조회한다. 메모리에 있는 값을 조회하기 때문에 성능상 이점을 취할 수 있다. 


Entity 등록

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// Entity Manager는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin();

em.persist(collector1);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

transaction.commit();
// 커밋하는 순간에 INSERT SQL을 보낸다.

위의 예제는 collector를 데이터베이스에 저장하는 코드다. 주석을 살펴보면 Entity Manager에 entity를 등록해도 sql문을 날리지 않는다고 되어있다. 엔티티 매니저는 트랜잭션이 커밋되기 전까지 내부 쿼리 저장소에 SQL문을 모아둔다. 그리고 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보내는데 이를 트랜잭션을 지원하는 쓰기지연 ( transaction write-behind ) 이라 한다.


  1. Collector1을 영속화한다.
  2. 1차 캐시에 collector1 엔티티를 저장하면서 동시에 등록 쿼리를 만든다.
  3. 만들어진 쿼리를 쓰기 지연 SQL 저장소에 등록한다.

위 과정은 트랜잭션 커밋하기 전까지의 흐름이다.


트랜잭션을 커밋하는 순간 쓰기 지연 SQL 저장소에 있던 SQL문들을 DB에 보낸다. 



Entity 수정

transaction.begin();

Collector find = em.find(Collector.class, "tedkim");
find.setEmail("new@google.com");

// em.update(find); 수정된 걸 알려줘야 하지 않을까?

transaction.commit();

위 코드는 JPA에서 엔티티를 수정한 경우이다. 그런데 업데이트를 알려주지 않고 단순히 Entity를 조회해서 데이터만 변경하면 된다. 변경 감지 (dirty checking) 기능이 있기 때문에 이것이 가능하다.  아래 그림을 살펴보자.



JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초상태를 복사해서 저장해두는데 이것을 Snapshot이라 한다. 그리고 Flush 시점에 Entity와 Snapshot을 비교하여 변경된 Entity를 찾는다. 위의 그림을 보면,

  1. 트랜잭션 커밋을 호출하면 Entity Manager 내부에서 Flush()가 호출된다.
  2. Entity와 Snapshot을 비교하여 변경된 Entity를 찾는다.
  3. 변경된 Entity를 update해줄 SQL문을 생성하여 쓰기 지연 SQL 저장소에 넣어둔다.
  4. 데이터베이스 트랜잭션을 커밋한다.
변경 감지로 생성된 SQL문을 살펴보면, 아래와 같이 수정된 항목만 반영할 것이라 생각되지만 그렇지 않다.
UPDATE COLLECTOR
SET
EMAIL=?
WHERE
USERID=?

아래와 같이 Entity의 모든 필드를 수정한다.

UPDATE COLLECTOR
SET
EMAIL=?,
AGE=?
...
WHERE
USERID=?

왜 비효율적으로 전체 필드를 수정하냐고 생각할 수도 있다. 다음과 같은 장점때문에 이렇게 사용한다. ( 참고로 컬럼이 30개 이상되면 비효율적이라 취급할 수 있다. 하지만 이는 애초에 테이블을 잘못 설계했다고 볼 수 있다. )

  • 모든 필드를 사용하기 때문에 수정 쿼리가 항상 같다. 그래서 애플리케이션 로딩 시점에 쿼리를 미리 생성해두고 재사용할 수 있다.
  • 데이터베이서에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다.