본문 바로가기
개발/JPA

영속성 컨텍스트 (Persistence Context)

by leedonggeun 2024. 2. 10.

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 되는 HibernateDynamicUpdate를 사용할 수 있습니다.

 

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

댓글