JPA에서 제공해주는 기능은 크게 2가지이다.
- @Entity와 Table을 매핑하는 설계 부분
- 매핑한 @Entity를 실제로 사용하는 부분 ( CRUD )
Spring Boot 프로젝트를 할 때 @Entity 어노테이션을 명시만 해봤는데, 이번 포스팅에서 EntityManager가 매핑한 Entity를 어떻게 사용하는지 살펴보자.
Entity Manager와 Entity Manager Factory
Entity Manager Factory
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>
- EntityManager가 생성된다고 connection을 얻지 않는다. ( EntityManager 1 ) 즉, 데이터베이스 연결이 꼭 필요한 시점에 connection을 얻는다.
- JPA 구현체들은 EntityManagerFactory가 생성될 때 connection pool을 만든다. 스프링 프레임워크에서 사용하면 해당 컨테이너가 제공하는 DataSource를 사용한다.
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 ) 이라 한다.
트랜잭션을 지원하는 쓰기 지연이 가능한 경우
- Collector1을 영속화한다.
- 1차 캐시에 collector1 엔티티를 저장하면서 동시에 등록 쿼리를 만든다.
- 만들어진 쿼리를 쓰기 지연 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를 찾는다. 위의 그림을 보면,
- 트랜잭션 커밋을 호출하면 Entity Manager 내부에서 Flush()가 호출된다.
- Entity와 Snapshot을 비교하여 변경된 Entity를 찾는다.
- 변경된 Entity를 update해줄 SQL문을 생성하여 쓰기 지연 SQL 저장소에 넣어둔다.
- 데이터베이스 트랜잭션을 커밋한다.
UPDATE COLLECTOR
SET
EMAIL=?
WHERE
USERID=?
아래와 같이 Entity의 모든 필드를 수정한다.
UPDATE COLLECTOR
SET
EMAIL=?,
AGE=?
...
WHERE
USERID=?
왜 비효율적으로 전체 필드를 수정하냐고 생각할 수도 있다. 다음과 같은 장점때문에 이렇게 사용한다. ( 참고로 컬럼이 30개 이상되면 비효율적이라 취급할 수 있다. 하지만 이는 애초에 테이블을 잘못 설계했다고 볼 수 있다. )
- 모든 필드를 사용하기 때문에 수정 쿼리가 항상 같다. 그래서 애플리케이션 로딩 시점에 쿼리를 미리 생성해두고 재사용할 수 있다.
- 데이터베이서에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다.
플러시( flush )
- JPQL 실행 시 자동 호출 - 이전에 persist()들이 DB에 반영되어야 하기 때문에
- 트랜잭션 커밋 시 자동 호출 - 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영해야 되기 때문에 자동 호출되도록 JPA에서 구현
- 직접 호출 ( 거의 사용할 일 없음 )
준영속 ( detached )
'Back-End > JPA' 카테고리의 다른 글
ObjectOptimisticLockingFailureException (0) | 2019.11.02 |
---|---|
N+1 문제 해결하기 ( fetch join, @EntityGraph ) (0) | 2019.08.14 |
JPA 1차 캐시 - Database와 동기화가 되지 않은 데이터를 읽는 문제 (0) | 2019.04.23 |
13장. 웹애플리케이션과 영속성 관리 (0) | 2019.04.17 |
Persistence Framework에 관하여 ( 장,단점 ) (0) | 2019.02.25 |