0. 영속성 컨텍스트란?
Entity를 영구히 저장하는 환경이라고 해석해 볼 수 있습니다.
Entity Manager를 이용해 저장하거나 조회하면 Entity Manager는 영속성 컨텍스트에 해당 Entity를 보관 및 관리합니다.
1. 특징
1-1. 영속성 컨텍스트가 관리하는 Entity는 반드시 식별자가 존재해야 한다.
영속성 컨텍스트는 Entity를 식별자 값(@id로 테이블의 기본키와 맵핑한 값)으로 구분하기 때문에 반드시 존재해야 합니다.
만약 객체에 식별자가 없을 경우 Exception이 발생합니다.
1-2. 1차 캐시
영속성 컨텍스트 내부에는 캐시가 존재합니다. 이를 1차 캐시라고 부르며, 영속 상태의 Entity를 이곳에 저장합니다.
이 1차 캐시는 Key-Value 형태로 저장되며 Key는 해당 Entity의 식별자값입니다.아래는 간단한 예시입니다.
하나의 트랜잭션 안에서 저장을 하고, 조회를 한다면 INSERT 문만 수행되고, SELECT 문은 수행되지 않습니다. (1차 캐시 사용)
Member member = new Member();
member.setId(1);
member.setUserName("회원1");
// 영속성 컨텍스트에 저장
entityManager.persist(member);
// Entity 조회
member = entityManager.find(Member.class, 1);
// 트랜잭션 커밋
transaction.commit();
이렇게 보면 굉장히 좋은 성능을 보여줄 것 같은 특징이지만,
1차 캐시는 하나의 트랜잭션 안에서만 동작하는 짧은 캐시이기 때문에 크게 도움이 안 되는 경우가 많습니다.
1-3. 동일성을 보장한다.
위에서 설명했듯이, 영속성 컨텍스트는 1차 캐시를 지원하고 있기 때문에 동일한 식별자의 Entity를 조회 시 동일한 Entity를 반환하고 있으므로, 당연히 동일한 Entity를 반환합니다.
Member member1 = entityManager.find(Member.class, 1);
Member member2 = entityManager.find(Member.class, 1);
// 이 경우, True를 반환한다.
System.out.println(member1 == member2);
1-4. 쓰기 지연
Entity Manager는 트랜잭션을 커밋하기 직전까지 INSERT 문을 데이터베이스에 질의하지 않고 저장해 둡니다.
그리고 트랜잭션을 커밋할 때 저장해 둔 쿼리를 데이터베이스에 질의하는데 이것을 트랜잭션을 지원하는 쓰기 지연(Transactional write-behind)이라고 합니다.
Member member = new Member();
member.setId(2);
member.setUserName("회원2");
// 트랜잭션 시작
transaction.begin();
// Entity 저장 (데이터베이스엔 아직 저장되지 않는다.)
entityManager.persist(member);
// 이 때 데이터베이스로 INSERT문을 수행한다.
transaction.commit();
이 트랜잭션이 커밋될 때 Entity Manager는 영속성 컨텍스트를 플러시 합니다.
플러시는 간단히 말해 영속성 컨텍스트에서의 변경점(저장, 수정, 삭제)을 데이터베이스와 동기화하는 것을 말합니다.
즉, 위에서 말한 저장해 둔 쿼리를 데이터베이스에 실제 질의를 하는 것이 이때(플러시)입니다.
1-5. 변경 감지 Dirty Checking
MyBatis와 같이 UPDATE SQL 쿼리문을 직접 작성하는 경우 굉장히 많은 수정 쿼리를 직접 작성해야 합니다.
하지만 JPA는 이 변경 감지를 통해 Entity를 수정합니다.
JPA는 Entity를 영속성 컨텍스트에 저장할 때 최초 상태를 복사하여 저장해 두는데, 이를 스냅샷이라고 합니다.
그리고 플러시 될 때 이 스냅샷과 Entity를 비교해 변경을 감지합니다.
이때 변경이 감지되었다면 수정 쿼리문을 생성하여 위의 1-4. 쓰기 지연에서 말했던 지연 저장소에 저장합니다. 이후 트랜잭션이 커밋될 때 수정 쿼리가 수행됩니다.
* 변경 감지는 영속 상태의 Entity에만 적용됩니다.
* JPA는 수정 시 모든 필드를 UPDATE 합니다.
이는 데이터 전송량이 증가할 수 있지만, 미리 수정 쿼리를 생성하고 재사용할 수 있다는 장점도 있습니다.
* 수정되어야 하는 Entity의 필드가 많거나 저장되는 내용이 너무 크다면 수정된 데이터만 동적으로 UPDATE 되는 Hibernate의 DynamicUpdate를 사용할 수 있습니다.
1-6. 지연 로딩 Lazy Loading
어느 Entity를 조회할 때 연관된 Entity가 있다면 실제 연관된 Entity가 아닌 프록시 Entity를 반환합니다.
실제 데이터가 필요할 때 데이터베이스를 조회해 프록시 Entity를 초기화합니다.
이를 통해 성능 상 이점을 가져올 수 있습니다.
물론 항상 좋은 점은 아니고, 상황에 따라 지연 로딩과 즉시 로딩을 적절히 조절해야합니다.
2. 플러시(flush)
플러시는 위에서 설명했듯이 영속성 컨텍스트의 변경 내용을 데이터베이스와 동기화하는 것입니다.
영속성 컨텍스트를 플러시 하려면 아래와 같이 3가지 방법이 존재합니다.
2-1. flush() 메서드를 직접 호출한다.
Entity Manager의 flush 메서드를 직접 호출해서 강제로 플러시 하는 방법입니다.
이 방법은 테스트할 때가 아니면 거의 사용되지 않습니다.
2-2. 트랜잭션 커밋 시 자동 호출한다.
SQL을 데이터베이스에 전달하지 않고 트랜잭션만 커밋된다면 당연히 데이터베이스에 반영되지 않습니다.
이런 상황을 방지하기 위해 JPA는 트랜잭션을 커밋할 때 반드시 플러시를 자동 호출합니다.
2-3. JPQL 쿼리 실행 시 자동 호출한다.
위에서 여태 말했듯이, 하나의 트랜잭션이 커밋되기 전까지 영속성 컨텍스트는 데이터베이스와 동기화하지 않습니다.
하지만 트랜잭션을 커밋하기 전에 JPQL 쿼리를 실행할 경우 영속성 컨텍스트에만 저장되어 쓰기 지연(데이터베이스와 동기화되지 않은)에 걸린 Entity가 문제가 될 수 있습니다.
그리하여 JPA는 이런 상황을 방지하기 위해 JPQL 쿼리가 실행되기 전 플러시를 호출해 동기화를 우선 진행합니다.
'개발 > JPA' 카테고리의 다른 글
Entity의 생명주기 (Entity LifeCycle) (0) | 2024.02.09 |
---|
댓글