JPA에서 가장 중요한 용어라고 할 수 있다.
영속성 컨텍스트라는 용어의 의미 => 엔티티를 영구 저장하는 환경
엔티티 매니저로 엔티티를 저정하거나 조회하면, 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
em.persist(엔티티)
영속성 콘텍스트는 엔티티 매니저를 생성할 때 만들어진다.
엔티티 매니저를 통하여 영속성 컨택스트에 접근, 관리 할 수 있다.
엔티티에는 4가지 상태가 존재한다.
1. 비영속 : 영속성 컨텍스트와 전혀 관계가 없는 상태
2. 영속 : 영속성 컨텍스트에 저장된 상태
3. 준영속 : 영속성 컨텍스트에 저장되었다가 분리된 상태
4. 삭제 : 삭제된 상태
엔티티 객체를 생성 후 아직 저장하지 않아 순수한 엔티티 자체인 상태이다.
저장을 하지 않았기 때문에 영속성 컨텍스트와 데이터베이스와는 전혀 관련이 없다.
JPA가 관리하지 않는 상태를 말한다.
//엔티티 객체 생성
Member member = new Member();
member.setId("member1");
member.setUsername("deuk");
//영속성 컨텍스트 호출 X
엔티티 매니저를 통해서 영속성 컨텍스트에 저장한 상태
=> 영속성 컨텍스트가 관리하는 엔티티를 영속상태라고 한다.
영속성 컨텍스트에 의해 관리되는 상태를 말한다.
//엔티티 객체 생성
Member member = new Member();
member.setId("member1");
member.setUsername("deuk");
//영속성 컨텍스트 호출
em.persist(member)
여기서 persist를 사용하는 것만 영속성 컨텍스트가 관리하는 것이 아니라
em.find(), JPQL을 사용하여 조회하는 엔티티도 영속성 컨텍스트가 관리하는 영속 상태이다.
영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 더이상 관리하지 않으면 준영속 상태가 된다.
// 준영속 상태로 변환
1. em.detach();
// 영속성 컨텍스트를 닫거나, 초기화를 해도 준영속 상태로 변환
2. em.close();
3. em.clear();
엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다.
em.remove(member);
//엔티티 객체 생성 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("deuk");
//영속성 컨텍스트 호출 (영속)
em.persist(member)
해당 코드를 동작 시키면 1차 캐시에 회원 엔티티를 저장한다.
커밋을 하지 않았기 때문에 DB에 저장하지는 않는 코드이다.
1차 캐시에서 사용하는 키는 식별자 값(@Id)로 관리한다.
해당 식별자 값은 DB의 기본키와 매핑되어있다. 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 DB 기본키 값이다.
//엔티티 조회
Member member = em.find(Member.class, "member1");
//EntityManager.find() 메서드 정의
public <T> T find(Class<T> entityClass, Object primaryKey)
em.find()를 호출하면 1차 캐시에서 엔티티를 우선 찾는다. 만약 찾는 엔티티가 1차 캐시에 존재하지 않을 시, DB에서 조회하는 순서로 이루어진다.
1차 캐시에 데이터가 없을 때 DB에 접근하여 데이터를 가져올 때 바로 사용자에게 전달하는 것이 아니라, 1차 캐시에 등록 절차를 거친 뒤에 엔티티를 반환되는 절차를 거친다. 해당과정을 영속상태로 해주는 과정이라고 생각하면 된다.
Member member2 = em.find(Member.class, "member2");
위와 같이 없는 데이터를 1차 캐시에 저장을 해놓을 때의 장점은 다시 해당 데이터를 조회 할 때 DB에 접근하지 않고 1차 캐시에서 찾으면 됨으로 성능상 이점이 생긴다.
또한 같은 것을 여러번 조회할 때의 동일성을 따지자면, 1차 캐시에 있는 데이터가 조회 되었다면 두개의 반환된 엔티티는 같은 엔티티라고 할 수 있다.
따라서 영속성 컨택스트는 성능상 이점과 엔티티의 동일성을 보장한다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
=> if( a == b ) // true
참고
동일성 (identity): 실제 인스턴스가 같다. => 참조 값을 비교하는 (==) 비교의 값이 같다.
동등성(equality) : 실제 인스턴스는 다를 수 있지만, 인스턴스가 가지고 있는 값이 같다.
자바에서 동등성 비교는 equals() 메서드를 사용
엔티티를 DB에 등록하기 위해서는 트랜잭션 커밋을 수행하여 등록을 할 수 있다.
또한 엔티티 매니저에서 데이터 변경 시 트랜잭션을 시작하지 않은 상태에서 변경을 시도하면, 예외가 발생한다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // 엔티티 매니저 데이터 변경 시 트랜잭선 시작
//엔티티 메니저가 1차 캐시에 등록한다. => 여기까지는 SQL을 DB에 전송 하지 않는다.
em.persist(memberA);
em.persist(memberB);
//커밋하는 순간 DB에 Insert SQL을 전송한다.
transaction.commit();
엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 Insert SQL을 모아둔다.
이후 트랜잭션 커밋을 할 때 모아둔 쿼리를 DB에 보낸다. 이것을 트랜잭션을 지원하는 쓰기 지원이라고 한다.
트랜잭션을 커밋하는 순간 엔티티 매니저는 우선적으로 영속성 컨텍스트를 플러시한다.
영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업, 등록, 수정, 삭제한 엔티티를 DB에 반영하는 작업을 한다. => 쓰기 지연 SQL에 등록되어있는 쿼리를 DB에 보내는 작업을 수행한다.
Flush를 통해 DB와 동기화 작업을 수행 뒤, 실제 DB 트랜잭션을 커밋하여 작업을 완료한다.
해당 기능을 잘 활용하면 DB를 한번에 전달하여 성능을 최적화 할 수 있다.
우리가 일반적으로 DB의 내용을 수정하기 위해서는 UPDATE를 사용하여 테이블의 값을 수정한다.
SQL을 사용하여 데이터를 수정하는 것의 단점은 쿼리를 만들어 둘 때 상황에 따라
다른 쿼리를 만들어 두어야 한다는 것이다.
예를 들어 이름과 전화번호를 수정하는 SQL이 필요하여 만들어 두었는데, 이름만 수정하는 SQL이 추가적으로 필요하면 하나 추가적으로 만들어야한다.
이러한 개발 방식의 문제점은 비슷하게 관리하는 쿼리가 점점 늘어난다는 것이다.
JPA에서는 해당 문제를 해결하는 방법은 모두들 update 메서드가 있을 것이라고 생각을 하지만, 사실 JPA에는 update 메서드가 존재하지 않는다.
JPA에서 값이 변경이 있을 때 자동으로 변경을 해주는 변경 감지 기능을 사용한다.
JPA에서는 영속성 컨텍스트에 보관할 때 최초 상태를 복사하여 저장하는 스냅샷을 사용한다. 이후 트랜젝션 커밋을 수행 할 때 플러시가 동작하는 과정에서 변경되는 엔티티를 찾는 과정을 거친다.
변경감지 기능은 영속성 컨텍스트가 관리하는 영속 상태의 엔티티만 가능하다.
준영속, 비영속 엔티티들은 감지할 수 가 없다.
Member member = new Member();
member.setId(id);
member.setUsername("득회");
member.setAge(21);
em.persist(member);
member.setAge(26);
위와 같은 코드가 있을 때 우리가 생각하는 SQL은 age 필드만 수정이 될 것이라고 생각을 한다. 하지만 JPA 기본 전략은 모든 필드를 업데이트하는 것이 기본 원칙이다.
따라서 결과는 다음과 같다.
Hibernate:
/* update
hellojpa.domain.Member */ update
MEMBER
set
age=?,
NAME=?
where
ID=?
이처럼 모든 필드를 업데이트 하면 DB에 전달하는 데이터의 전송량이 증가하는 단점이 있지만, 장점도 존재한다.
1. 모든 필드를 수정하기 때문에 수정 쿼리가 항상 동일하다. => 수정쿼리를 미리 만들어서 재사용 가능
2. DB에 동일 쿼리 전송시 재사용 가능
필드가 너무 많아서 저장할 내용이 많아 비용이 많이 들어가는 SQL일 것 같다는 판단이 들 때가 있을경우에는 수정할 데이터만 동적으로 SQL을 생성하는 방식을 채택하여 사용하면 된다.
@org.hibernate.annotations.DynamicUpdate
Hibernate:
/* update
hellojpa.domain.Member */ update
MEMBER
set
age=?
where
ID=?
엔티티 클래스 위에 해당 어노테이션을 붙혀 놓으면 동적으로 수정된 데이터만 사용하여 SQL을 생성한다.
또한 Insert를 할 때에 값이 있는 필드만 등록할 경우 어노테이션으로 설정할 수 있다.
@DynamicInsert
기본적으로 필드가 30개 이상이 되면 동적으로 선택적 컬럼을 사용하는 것이 더 성능적으로 좋다고 한다.
em.remove(엔티티)를 사용하여 삭제를 진행한다. 바로 삭제되는 것이 아니라 엔티티 등록과 마찬가지로
쓰기 지연 SQL 저장소에 등록되어 commit을 할때 DB로 SQL을 전송한다.
Member member = em.find(Member.class, "memberA");
em.remove(member);
remove(엔티티)를 호출하는 순간 memberA는 영속성 컨텍스트에서 제거된다.
삭제된 엔티티는 재사용을 하지 않고, 가비지 컬랙션을 통해 제거 되도록 두는 것이 좋다.