자바 ORM 표준 JPA 프로그래밍 - (3) 영속성 관리
✓︎ 엔티티 매니저 팩토리와 엔티티 매니저
✓︎ 영속성 컨텍스트란?
✓︎ 엔티티의 생명주기
✓︎ 영속성 컨텍스트의 특징
✓︎ 플러시
✓︎ 준영속
1. 엔티티 매니저 팩토리와 엔티티 매니저
- 엔티티 매니저 팩토리는 엔티티를 만드는 공장인데, 공장을 만드는 비용이 상당히 크기 때문에, 한 개만 만들어서 애플리케이션 전체에서 공유하도록 설계되어 있다. 반면에, 엔티티 매니저 팩토리에서 엔티티 매니저를 생성하는 비용은 거의 들지 않는다.
- 엔티티 매니저는 엔티티를 저장하고, 수정하고, 삭제하고, 조회하는 등 엔티티와 관련된 모든 일을 처리한다.
엔티티 매니저는 엔티티를 저장하는 가상의 데이터베이스로 생각하면 된다. - 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드간에는 절대 공유하면 안된다.
- 엔티티 매니저는 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다. 보통 트랜잭션을 시작할 대 커넥션을 획득한다.
-
EntityManagerFactory / EntityManager 생성하기
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook"); EntityManager em = emf.createEntityManager();
Persistence.createEntityManagerFactory("jpabook")
를 호출하면META-INF/persistence.xml 에 있는 정보를 바탕으로 EntityManagerFactory를 생성한다.
2. 영속성 컨텍스트란?
- 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
- 영속성 컨텍스트는 논리적인 개념으로, 엔티티 매니저를 생성할 때 만들어져서 엔티티 매니저를 통해 접근하고 관리할 수 있다.
-
엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장하기
em.persist(member);
3. 엔티티의 생명주기
- 엔티티에는 4가지 상태가 존재한다.
-
비영속
//객체를 생성한 상태(비영속) Member member = new Member(); member.setId("member1"); member.setName("sayho");
-
영속
//객체를 저장한 상태(영속) em.persist(member); //JPQL을 사용해서 조회힌 상태(영속) em.find(Member.class, id);
-
준영속
//회원 엔티티를 영속성 컨텍스트에서 분리(준영속) em.detach(member); //영속성 컨텍스트를 닫거나 초기화(준영속) em.close(); em.clear();
-
삭제
//객체를 삭제한 상태(삭제) em.remove(member);
4. 영속성 컨텍스트의 특징
- 영속 상태는 식별자 값이 반드시 있어야 한다. 식별자 값이 없으면 예외가 발생한다.
- JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영한는데, 이것을 플러시(flush)라고 한다.
영속성 컨텍스트가 엔티티를 관리할 때의 장점
- 1차 캐시
- 영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라고 한다.
- 엔티티를 조회할 때 1차 캐시에서 먼저 찾고, 엔티티가 1차 캐시에 없으면 데이터베이스에서 조회한다.
-
1차 캐시는 key: @Id로 매핑한 식별자, value: Entity 인스턴스인 Map 형태이다.
데이터베이스를 바로 조회하지 않고, 메모리에 있는 1차 캐시를 사용하여 성능상 이점이 생긴다. -
엔티티 등록 :
persist()
,commit()
EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); // 엔티티 매니저는 데이터 쓰기 시 트랜잭션을 시작해야 한다. transaction.begin(); em.persist(memberA); em.persist(memberB); // 여기까지 INSERT SQL을 DB에 보내지 않는다. transaction.commit(); // 커밋하는 순간 DB에 INSERT SQL을 보낸다.
persist()
-
commit()
- 영속 엔티티의 동일성 보장
-
식별자가 같은 엔티티를 조회할 때 1차 캐시에 있는 같은 엔티티 인스턴스를 반환한다.
Member a = em.find(Member.class, "member1"); Member b = em.find(Member.class, "member1"); System.out.println(a == b); //동일성 비교(true)
동일성과 동등성
- 동일성(identity) : 실제 인스턴스가 같다. 따라서 참조 값을 비교하는==
비교의 값이 같다.
- 동등성(equality) : 실제 인스턴스는 다를 수 있지만 인스턴스가 가지고 있는 값이 같다. 자바에서 동등성 비교는equals()
메소드를 구현해야 한다. -
- 트랜잭션을 지원하는 쓰기 지연
- 엔티티 매니저는 트랜잭션을 커밋하기 직전까지 쓰기 지연 SQL 저장소에 모아둔다.
- 트랜잭션을 커밋하면 엔티티 매니저는 영속성 컨텍스트를
flush()
한다.
플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화 하는 작업이다. (쓰기 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 보낸다.) - 이 기능을 잘 활용하면 쓰기 모아둔 쿼리를 데이터베이스에 한번에 전달해서 성능을 최적화 할 수 있다.
- 변경 감지
-
JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 된다.
Member memberA = em.find(Member.class, "memberA"); memberA.setAge(10); // em.update(memberA); // 이런 코드가 없어도 된다. transaction.commit();
-
엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경 감지라고 한다.
- JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 스냅샷을 저장한다. 그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.
- 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다.
- JPA는 변경된 필드만 업데이트 하는 것이 아니라, 엔티티의 모든 필드를 업데이트 한다.
- 모든 필드를 데이터베이스에 보내면 데이터 전송량이 증가하는 단점이 있지만, 수정 쿼리가 동일하여 쿼리를 재사용할 수 있다.
- 엔티티에 필드가 많거나 저장되는 내용이 너무 크면 수정할 필드만 동적으로 UPDATE SQL을 생성하는 전략을 선택할 수 있다.
@DynamicUpdate
: 상황에 따라 다르지만 컬럼이 대략 30개 이상이면 정적 수정 쿼리보다 동적 수정 쿼리가 빠르다고 한다.
-
- 지연 로딩
- 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법이다.
5. 플러시
- 플러시(flush())는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화한다.
- 데이터베이스와 동기화를 최대한 늦추는 것이 가능한 이유는 트랜잭션이라는 작업 단위가 있기 때문이다.
영속성 컨텍스트를 플러시하는 방법
1) em.flush()
를 직접 호출한다.
테스트나 다른 프레임워크와 JPA를 함께 사용할 때를 제외하고 거의 사용하지 않는다.
2) 트랜잭션 커밋 시 플로시가 자동 호출된다.
3) JPQL 쿼리 실행 시 플러시가 자동 호출된다.
플러시 모드 옵션
FlushModeType.AUTO
: 커밋이나 쿼리를 실행할 때 플러시 (기본값)FlushModeType.COMMIT
: 커밋할 때만 플러시 COMMIT 모드는 성능 최적화를 위해 사용할 수 있다.
6. 준영속
- 영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라고 한다.
- 준영속 상태에의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
준영속 상태로 만드는 방법
1) em.detach(entity)
: 특정 엔티티만 준영속 상태로 전환한다.
2) em.clear()
: 영속성 컨텍스트를 완전히 초기화한다.
3) em.close()
: 영속성 컨텍스트를 종료한다.
- 엔티티를 준영속 상태로 전환 :
detach()
- 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거된다.
- 영속성 컨텍스트 초기화 :
clear()
- 영속성 컨텍스트를 초기화해서 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다.
- 영속성 컨텍스트 종료 :
close()
- 해당 영속성 컨텍스트가 관리하던 영속 상태의 엔티티가 모두 준영속 상태가 된다.
준영속 상태의 특징
- 거의 비영속 상태에 가깝다.
- 식별자 값을 가지고 있다.
- 지연 로딩을 할 수 없다.
병합 : merge()
- 준영속 상태의 엔티티를 다시 영속 상태로 변경하려면 병합을 사용하면 된다.
- 병합은 비영속 엔티티도 영속 상태로 만들 수 있다.
- 병합은 파라미터로 넘어온 엔티티의 식별자 값으로 영속성 컨텍스트를 조회하고 찾는 엔티티가 없으면 데이터베이스에서 조회한다. 만약 데이터베이스에서도 발견하지 못하면 새로운 엔티티를 생성해서 병합한다.