About JPA (2)

EntityManager and PersistenceContext


JPA를 잘이해하려면 엔티티매니저에 대한 이해가 선행되어야한다. JPA의 핵심인 엔티티매니저와 영속성컨텍스트에 대해 알아보자.

1. 엔티티매니저 팩토리


엔티니매니저를 생성하는 주체, JPA 설정(Persistence.xml 등)에 따라 생성된다.

1.1. 특징

  • 엔티티매니저를 생성하는 엔티티매니저 공장으로 엔티티매니저를 관리한다.
  • 엔티티매니저 팩토리의 생성비용은 상당히 커서 한 개만 만들고 어플리케이션 전체에서 공유하도록 설계되어있다.
  • 엔티티매니저를 생성하는 것은 많은 비용이 들지 않는다.
  • 스레드간 공유해도 문제가 없다.
  • 만약 어플리케이션 내에서 사용되는 데이터베이스가 여러개라면 엔티티매니저 팩토리도 여러개 생성한다.

2. 엔티티매니저


영속성컨텍스트를 관리한다.

2.1 특징

  • 엔티티매니저 팩토리에 의해 관리된다.
  • 스레드간 공유하면 동시성 문제가 발생할 수 있으므로 공유해선 안된다.
  • 데이터베이스 커넥션풀과 연결되어있는데, 꼭 필요한 시점(트랜잭션 시작)까지 커넥션 획득을 미룬다.
  • 보통 하나의 요청에 하나의 엔티티매니저가 생성되어 사용된다.

3. 영속성컨텍스트


3.1. 특징

  • 엔티티매니저를 생성할 때 하나 만들어진다.
  • 엔티티매니저를 통해 접근하고 관리할 수 있다.
  • 1차 캐시를 통해 데이터베이스가 아닌 애플리케이션 차원에서 반복가능한 읽기(REPEATABLE READ)를 제공한다.

3.2. 영속성컨텍스트 내 엔티티의 생명주기

3.2.1. 비영속

엔티티 객체를 생성만 한 상태. 엔티티매니저에 아직 저장되지 않았으므로 영속성 컨텍스트나 데이터베이스와 전혀관련이 없다.

// 객체를 생성한 상태(비영속)
User user = new User();
user.setId(1L);
user.setName("홍길동");
3.2.2. 영속

엔티티매니저를 통해 영속성컨텍스트에 저장되어 영속성 컨텍스트가 관리하는 상태

// 객체를 저장한 상태(영속)
em.persist(user);
3.2.3. 준영속

영속성컨텍스트가 관리하던 영속 상태의 엔티티를 더이상 관리하지 않는 상태. 비영속 상태에 가깝고 지연 로딩을 할 수 없지만, 식별자 값은 존재한다. 다시 영속상태로 돌아가려면 merge를 사용한다.

// 영속성 컨텍스트로부터 분리
em.detach(user);
// 준영속 객체를 영속 상태로 변경
em.merge(user);

// 영속성 컨텍스트 내의 모든 엔티티를 지움
em.clear();
// 영속석 컨텍스트를 종료
em.close();
3.2.4. 삭제

엔티티를 영속성컨텍스트와 데이터베이스에서 삭제한다. 이렇게 삭제된 엔티티는 재사용하지 말고 가비지 컬렉션의 대상이 되도록 두는 것이 좋다.

// 객체를 삭제한 상태(삭제)
em.remove(member);

// 다시 영속상태로 돌아갈 수 있다.
em.remove(member);
// 객체를 영속상태로 변경, 커밋이 일어나도 삭제되지 않는다.
em.persist(member);

3.3. 1차 캐시

영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라고하며, 영속 상태의 엔티티는 모두 이곳에 저장된다. 1차 캐시는 쉽게말해 영속성컨텍스트 내부에 있는 Map이라고 보면되는데, 이 1차 캐시는 엔티티 고유의 식별자를 key 값으로 엔티티 인스턴스를 저장한다.

엔티티매니저의 find메소드가 호출되면 먼저 영속성컨텍스트의 1차 캐시에서 값을 찾고 만약 찾는 엔티티가 1차 캐시에 없다면 데이터베이스에서 조회한다. 이는 같은 엔티티를 조회할 때 성능상 이점을 누릴 수 있음을 의미한다.

3.4. 영속 엔티티의 동일성 보장

동일성이란 실제 참조하는 인스턴스가 같다는 의미로, 영속성컨텍스트 내의 식별자가 같은 엔티티를 조회할 경우 동일성이 보장된다. 아래 예제를 참고해보자.

// User 엔티티의 식별자가 1인 객체를 조회하라.
User user1 = em.find(User.class, 1L);
User user2 = em.find(User.class, 1L);

System.out.println(user1 == user2);
// user1과 user2는 같은 식별자로 엔티티를 조회했으므로 user1과 user2는 동일하다.

3.5. 쓰기 지연

새로운 엔티티가 생성(persist)될 경우 엔티티매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 쓰기 지연 SQL 저장소에 SQL을 보관한다. 트랜잭션 내 모든 작업이 끝나고 커밋이 일어나면 그때서야 저장소 내의 SQL을 데이터베이스로 전달하는데, 이를 쓰기지연이라고 한다. 영속 상태의 엔티티가 변경될 경우도 마찬가지로 쓰기 지연 SQL 저장소에 UPDATE SQL을 보관했다가 커밋이 일어나면 한번에 반영한다.

3.6. 변경감지

엔티티를 수정할 때는 엔티티를 조회해서 데이터만 변경해주면 된다. 영속성컨텍스트가 수정된 데이터를 찾아서 데이터베이스에게 알려주기 때문이다. 이러한 일이 가능한 이유는 영속성컨텍스트 내 1차 캐시에는 엔티티의 현재 값과 더불어 최초값인 스냅샷도 저장되어 있는데, 커밋이 일어나면 현재 값과 스냅샷을 비교하여 변경된 내역을 감지하기 때문이다.

엔티티의 데이터가 여러번 변경되어도 최초 값과 마지막 값만 비교하기 때문에 마지막으로 변경된 엔티티의 상태를 기준으로 UPDATE SQL이 생성된다.

// user 조회, 최초 name 값은 홍길동
User user = em.find(User.class, 1L);

// user의 name 값을 여러번 변경
user.setName("홍일동");
user.setName("홍이동");
user.setName("홍삼동");
user.setName("홍사동");
user.setName("홍길동");
// 마지막으로 변경된 홍길동 이전인 홍일동부터 홍사동까지는 무시된다.
// 커밋이 일어나면 영속성컨텍스트는 최초 값 "홍길동"과 마지막 값 "홍길동"을 비교하지만 변경되지 않았으므로 UPDATE SQL을 생성하지 않는다.

변경감지는 영속 상태의 엔티티에만 적용되므로, 비영속상태이거나 준영속상태인 엔티티는 값이 변경되어도 데이터베이스에 반영되지 않는다.

3.7. flush

영속성컨텍스트의 변경 내용을 데이터베이스에 반영하는 것을 의미한다. 순서는 아래와 같다.

1. 트랜잭션 커밋 발생
2. 변경감지가 작동되어 영속성컨텍스트 내의 모든 엔티티를 스냅샷과 비교한다.
3. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
4. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.
5. 데이터베이스는 쿼리를 반영하고 커밋한다.

참고자료

자바 ORM 표준 JPA 프로그래밍 (김영한)

Written on January 17, 2017