JPA 영속성 컨텍스트(Persistence Context)

wujin·2023년 4월 11일
0
post-thumbnail

영속성 컨텍스트(Persistence Context)

Persistence Context는 데이터베이스와의 통신을 담당하는 영속성 유닛(Persistence Unit)의 실행 컨텍스트(Context)로서, JPA에서 엔티티 객체의 생명주기를 관리하고 영속성을 보장한다.

Persistence Context는 엔티티 매니저(Entity Manager)를 통해 생성된다. 엔티티 매니저는 데이터베이스와의 연결을 담당하고, Persistence Context를 생성하여 엔티티 객체를 관리한다. 엔티티 매니저는 Persistence Context에 접근하여 엔티티 객체를 검색, 생성, 수정, 삭제할 수 있다.


영속성 저장 조건

  1. 엔티티 클래스는 @Entity 어노테이션이 적용되어 있어야 한다.
  2. 엔티티 객체의 식별자 값이 있어야 한다. 이는 데이터베이스에서 특정 테이블의 특정 레코드를 식별하기 위한 기준이 되는 값이다. 식별자 값은 엔티티 객체의 필드 중 하나를 @Id 어노테이션으로 지정하여 정의한다.
  3. 엔티티 객체는 데이터베이스의 제약 조건을 준수해야 한다.
  4. 영속성 컨텍스트에서 관리되는 엔티티 객체는 반드시 스레드 안전해야 한다. 즉, 여러 스레드가 동시에 같은 엔티티 객체를 수정하려는 경우에도, 데이터 일관성이 유지되어야 한다.

Entity LifeCycle

JPA에서 관리되는 엔티티 객체가 가질 수 있는 상태를 의미한다. JPA에서 엔티티 객체의 상태는 총 4가지로 정의되어 있으며, 이를 Entity LifeCycle이라고 한다.

1. Transient 상태 (New, 비영속)

Member member = new Member();

엔티티 객체가 생성된 직후의 상태이다. 이때 엔티티 객체는 JPA에서 관리되지 않으며, 데이터베이스와도 관계가 없다. 즉, 아직 영속성 컨텍스트에 등록되지 않은 상태이다. 이러한 엔티티 매니저 에 의해 관리되지 않는 POJO객체를 말한다.

2. Persistent 상태 (Managed, 영속)

//persist()를 통해 영속 컨텍스트에 등록
Member member = new Member();
member.setMemberId(1);
member.setName("홍길동");

entityManager.persist(member);

---

//find()를 통해 DB에서 영속성 컨텍스트로 등록
member = em.find(member.class, 1);

엔티티 객체가 영속성 컨텍스트에 등록된 상태이다. 영속성 컨텍스트에서 관리되는 엔티티 객체는 데이터베이스와 동기화되어 있으며, 엔티티 객체가 수정되면 자동으로 데이터베이스도 변경된다.

엔티티 인스턴스가 persist() 메서드를 통해 엔티티 매니저의 영속성에 등록되거나, find() 메서드 혹은 QueryDSL 메서드 호출로 DB에서 반환된 경우 영속상태로 분류한다. 영속 상태의 엔티티는 1차 캐시에 식별자를 포함하여 저장된다.

3. Detached 상태 (준영속)

엔티티 객체가 영속성 컨텍스트에 존재했다가 분리된 상태이다. 이때 엔티티 객체는 더 이상 영속성 컨텍스트에서 관리되지 않으며, 데이터베이스와의 동기화도 이루어지지 않는다. 따라서, Detached 상태의 엔티티 객체를 수정하더라도 데이터베이스에는 반영되지 않는다.

비영속 상태와 비슷하지만 다음과 같은 차이점이 있다.

  • 한 번 영속상태가 되었기 때문에 식별자가 존재
  • 영속 해제된 상태이므로 persist()를 통해 영속성 컨텍스트 저장 불가능
    • merge() 메서드로 재저장(managed) 가능
    • 이 때 해당 객체가 영속성을 갖는게 아닌 영속성을 갖는 새로운 객체를 반환
  • 지연 로딩 불가능
// member 객체의 영속성 해제
entityManager.detach(member);

// 해당 매니저의 영속성 컨텍스트 내 모든 영속성 해제
entityManager.clear();

// member 객체의 속성으로 새로운 영속성 컨텍스트 내 엔티티를 만들어 반환
// 반환 후 원래의 레퍼런스에 저장
member = entityManager.merge(member);

//영속성 컨텍스트 종료(제거)
entityManager.close();

4. Removed 상태 (삭제)

엔티티 객체가 영속성 컨텍스트에서 삭제된 상태이다. 이때 엔티티 객체는 데이터베이스와 동기화되어 삭제된다.

// member 제거
entityManager.remove(member);

JPA에서는 엔티티 객체의 상태 변화를 추적하기 위해 Entity LifeCycle을 관리한다. 이를 통해 영속성 컨텍스트에서 관리되는 엔티티 객체의 데이터 일관성을 유지하고, 데이터베이스와의 동기화를 보장할 수 있다.


1차 캐시

영속성 컨텍스트(Persistence Context) 내부에 존재하는 캐시를 의미한다. 즉, JPA가 데이터베이스에서 조회한 엔티티 객체를 영속성 컨텍스트 내부에 저장해두었다가, 동일한 엔티티 객체가 필요한 경우에는 데이터베이스에 접근하지 않고 캐시에서 먼저 찾아서 반환하는 기능이다. 1차 캐시를 사용하면 조회한 엔티티 객체를 재사용하면서 성능을 최적화할 수 있다.

SELECT 쿼리 중복 수행 제한

JPA는 1차 캐시를 통해 동일한 엔티티 객체를 반복해서 조회하는 경우 SELECT 쿼리를 중복해서 수행하는 것을 방지한다. JPA는 동일한 @Id 값을 가진 엔티티 객체를 1차 캐시에서 먼저 찾아서 반환하므로, 데이터베이스에 접근하지 않아도 되는 것이다. 따라서, 같은 엔티티 객체를 반복해서 조회하는 경우 SELECT 쿼리를 중복해서 수행하는 것을 방지할 수 있다.

동일성 보장

JPA는 1차 캐시를 사용하여 조회한 엔티티 객체에 대해 동일성을 보장한다. 즉, 같은 @Id 값을 가진 엔티티 객체는 1차 캐시에서 하나의 객체만 존재하므로, 같은 엔티티 객체를 참조하는 경우에도 항상 동일한 객체를 참조하게 된다. 이는 JPA에서 엔티티 객체를 수정할 때도 중요한 역할을 한다. JPA는 1차 캐시에 저장된 엔티티 객체를 수정하면 자동으로 데이터베이스에 변경 사항을 반영하므로, 동일한 엔티티 객체에 대해서는 데이터 일관성을 보장할 수 있다.


쓰기 지연(write-behind)

JPA에서는 트랜잭션을 이용하여 쓰기 지연 기능을 제공한다. 쓰기 지연은 JPA에서 엔티티를 수정, 추가, 삭제할 때, 엔티티를 즉시 데이터베이스에 반영하지 않고, 트랜잭션을 flush() 또는 commit()할 때까지 대기하는 기능이다.

예를 들어, 엔티티 매니저를 사용하여 엔티티를 변경하고, 그 변경 내용을 데이터베이스에 반영하는 메소드를 호출하더라도, JPA는 변경 내용을 즉시 데이터베이스에 반영하지 않는다. 대신, JPA는 트랜잭션 내에서 변경된 엔티티를 모아두고, 트랜잭션이 커밋될 때 모아둔 변경 내용을 한 번에 데이터베이스에 반영한다. 이렇게 하면 데이터베이스 쓰기 작업이 한 번만 일어나므로, 성능 향상에 도움이 된다.

쓰기 지연은 또한 엔티티의 상태 변화 추적을 가능하게 하므로, 데이터 일관성 유지에 도움을 준다. 예를 들어, 엔티티를 삭제할 때 즉시 데이터베이스에서 삭제하지 않고, 쓰기 지연 기능을 이용하여 삭제한 후에도 엔티티를 참조하는 다른 엔티티가 있는 경우, 해당 엔티티가 참조하던 엔티티가 삭제된 것을 확인할 수 있다. 이렇게 하면 데이터 일관성을 유지할 수 있다.

Flush()

flush()는 JPA의 영속성 컨텍스트를 DB와 동기화하는 작업을 말한다. EntityManager.flush()가 수행되어야 쿼리 저장소에 있는 DB를 보내는 것이다. 이 때 Dirty Checking를 통해 영속상태의 엔티티 중 속성이 변경된 엔티티를 Update한다.

  • flush 동작 순서
    1. 변경 감지로 수정된 엔티티 UPDATE
    2. 쓰기 지연 쿼리 동기화

Commit

트랜잭션을 begin() 으로 수행한 뒤 commit() 하여 트랜잭션을 종료해야한다. commit()을 수행하게 되면 내부적으로 EntityManager의 flush() 메서드를 호출한 후 트랜잭션을 닫는다.

Commit vs Flush

flush()는 쿼리를 전송하는 역할이고 commit()은 내부적으로 flush()를 수행한 뒤 트랜잭션을 끝내는 역할이다.

flush()로 전송된 쿼리는 rollback()이 가능하지만 commit()은 트랜잭션을 끝내므로 rollback()이 불가능하다.


Dirty Checking(변경 감지)

JPA는 EntityManager가 엔티티를 저장/조회/수정/삭제를 진행한다. 그런데 엔티티 매니저의 메서드를 찾아보면, persist(저장), find(조회), delete(삭제)로 update에 해당되는 메서드가 존재하지 않는다. 대신 JPA는 update에 해당하는 더티 체킹(Dirty Checking)을 지원한다.

더티 체킹은 Transaction 안에서 엔티티의 변경이 일어나면, 변경된 내용을 자동으로 데이터베이스에 반영하는 JPA 특징이다.

  • 데이터베이스에 변경 데이터를 저장하는 시점
    1. Transaction commit() 시점
    2. EntityManager flush() 시점
    3. JPQL 사용 시점

또한, 영속성 컨택스트(Persistence Context)안에 있는 엔티티를 대상으로 더티 체킹이 일어난다. 여기서 Dirty란 "엔티티 데이터의 변경된 부분"으로 해석하면 된다. 즉, 변경된 부분을 체크해서 DB에 반영한다는 뜻으로 해석한다.

더티 체킹이 일어나는 환경은 아래 두 가지 조건이 충족되어야 한다.

  1. 영속 상태(Managed) 안에 있는 엔티티인 경우
  2. Transaction 안에서 엔티티를 변경하는 경우

Transaction은 두 가지 방식으로 사용할 수 있다.

  1. Service Layer에서 @Transactional을 사용하는 경우
  2. EntityTransaction을 이용해서 트랜잭션을 범위를 지정하고 사용하는 경우

사용 예시

0. 엔티티 객체 정의

@Entity
@Getter @Setter
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;
}

1. 객체 생성 및 값 할당

User user = new User();
user.setName("John");
user.setEmail("john@example.com");

new 키워드를 사용하여 user 객체를 생성한다. 현재는 비영속 상태이며 아직 데이터베이스에 반영되지 않는다.

2. 객체를 영속성에 등록

EntityManager em = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
entityManager.persist(user);

EntityManager가 트랜잭션을 시작하고, user 객체를 persist() 메서드를 사용하여 영속화 시킨다.

영속성 컨텍스트 내에는 1차 캐시가 존재하여 managed(영속)가 된 entity를 캐시에 저장한다. 저장된 엔티티는 각각의 고유한 식별자를 갖고 있다.

3. 엔티티 객체 속성 변경

user.setName("David");

user 객체의 name 속성을 변경한다면, JPA는 1차 캐시에 저장된 user 객체를 통해 객체의 상태가 변경되었음을 알게 되고, 이후에 이 객체가 데이터베이스에 저장될 때(flush() 또는 commit()) 이 변경사항도 함께 반영(update query)된다. 이런 과정을 Dirty Checking이라 한다.

따라서, Dirty Checking은 JPA가 엔티티의 상태를 감지하여 데이터베이스에 업데이트할 필요가 있는지 여부를 결정하는 기능이다. 이를 통해 JPA는 개발자가 직접 데이터베이스에 업데이트하는 코드를 작성하지 않아도 되므로 개발 생산성을 높일 수 있다.

0개의 댓글