Hibernate Entity Lifecycle

Dev.Hammy·2023년 6월 30일
0

JPA_Hibernate

목록 보기
1/14

모든 Hibernate 엔터티는 프레임워크 내에서 Transient, Managed, Detached, Deleted 상태의 라이프 사이클을 갖는다

Helper Method

  • HibernateLifecycleUtil.getManagedEntities(session) : 세션 내부 저장소에서 모든 Managed 엔터티 가져오기
  • DirtyDataInspector.getDirtyEntities() : 'dirty'로 표시된 모든 엔터티 목록 가져오기
  • HibernateLifecycleUtil.queryCount(query) : 임베디드 데이터베이스에 대해 count(*) 쿼리를 수행하는 편리한 방법
  • static으로 import하여 사용한다

하이버네이트(Hibernate)에서 "더티"란, 영속성 컨텍스트(Persistence Context)에서 관리되는 엔티티(Entity)의 상태가 변경되었음을 나타내는 개념입니다. 더티 상태의 엔티티는 변경 사항이 발생한 후 영속성 컨텍스트에서 추적되며, 이러한 변경 사항을 데이터베이스에 동기화하는 작업을 말합니다.

더티 체킹(Dirty Checking)은 하이버네이트가 엔티티의 상태 변화를 감지하여 자동으로 데이터베이스에 반영하는 메커니즘입니다. 하이버네이트는 트랜잭션이 커밋되거나 flush()되는 시점에 이 변경 사항을 감지하고, 데이터베이스의 해당 레코드를 업데이트합니다. 이렇게 더티 체킹을 통해 개발자는 명시적으로 SQL 쿼리를 작성하지 않고도 엔티티의 변경 사항을 효율적으로 데이터베이스에 반영할 수 있습니다.

"영속성 컨텍스트"는 객체와 데이터베이스 간의 매핑을 관리하는 개념으로, ORM 프레임워크에서 사용되며 객체의 상태 관리와 데이터베이스와의 동기화를 담당합니다.
"데이터베이스 세션"은 관계형 데이터베이스에서 클라이언트와 데이터베이스 간의 연결을 나타내는 개념으로, 데이터베이스 서버와의 상호 작용을 관리합니다.

Persistence Context

  • persistence context란 클라이언트 코드와 데이터 저장소 사이의 스테이징 영역이다.
  • persistence context는 Unit of Work패턴을 구현한 것이다. 모든 데이터의 변경사항을 추적하여 비즈니스 트랜잭션이 끝날 때 데이터베이스에 다시 동기화하는 일을 담당한다.
  • JPA EntityManager 및 Hibernate의 Session으로 persistence context를 구현할 수 있다.

작업단위(Unit of work) : 비즈니스 트랜잭션의 영향을 받는 개체 목록을 유지 관리하고 변경 사항 작성 및 동시성 문제 해결을 조정

개체 모델을 변경할 때마다 데이터베이스를 변경할 수 있지만 이로 인해 매우 작은 데이터베이스 호출이 많이 발생하여 매우 느려질 수 있습니다. 또한 전체 상호 작용에 대해 트랜잭션을 열어야 하는데, 이는 여러 요청에 걸쳐 있는 비즈니스 트랜잭션이 있는 경우 비실용적입니다. 일관되지 않은 읽기를 피하기 위해 읽은 개체를 추적해야 하는 경우 상황은 더욱 악화됩니다.

작업 단위는 데이터베이스에 영향을 줄 수 있는 비즈니스 트랜잭션 중에 수행하는 모든 작업을 추적합니다. 완료되면 작업의 결과로 데이터베이스를 변경하기 위해 수행해야 하는 모든 작업을 파악합니다.

Managed Entity

  • Managed 엔터티는 데이터베이스 테이블의 행을 나타냄. (해당 행이 데이터베이스에 아직 존재하지 않더라도)
  • 현재 실행중인 세션에 의해 모든 변경사항은 추적되고 데이터베이스에 자동으로 전파된다.
  • 세션은 Detatched 엔터티를 다시 연결하거나 엔터티를 데이터베이스에서 불러온다.

예시) 엔터티 변경사항을 데이터 저장소에 반영하는 과정

+-------------------+-------+
| Name              |  ID   |
+-------------------+-------+
| Cristiano Ronaldo | 1     |
| Lionel Messi      | 2     |
| Gianluigi Buffon  | 3     |
+-------------------+-------+

몇가지 샘플 데이터로 데이터 저장소를 위와 같이 초기화 시킨다.
샘플 애플리케이션은 FootballPlayer 클래스를 엔터티로 정의하고 있다. 엔터티에서 Buffon의 이름을 Gigi Buffon대신 Gianluigi Buffon로 변경하고자 한다.

Session session = sessionFactory.openSession();

세션을 가져와서 Unit of Work을 시작한다.
서버 환경에서는 context-aware 프록시를 통해 세션을 코드에 주입할 수도 있다. Unit of Work의 비즈니스 트랜잭션을 캡슐화 하기 위해 세션이 필요한 원칙은 같다.

Context-aware 프록시"는 프로그래밍과 관련된 개념으로, 코드 실행 컨텍스트에 따라 프록시 객체를 동적으로 변경하거나 추가 동작을 삽입하는 방법을 다루는 것이다. 프록시 객체는 원본 객체를 대신하여 동작하는 객체이다.

"Context-aware 프록시"는 객체 지향 프로그래밍에서 사용되는 개념으로, 주로 AOP (Aspect-Oriented Programming)와 관련이 있습니다. 이는 객체가 어떤 "컨텍스트"에서 실행되는지 인식하고 이를 활용하여 추가적인 동작을 수행하거나 제어하는 데 사용됩니다.

Context-aware 프록시는 변경 사항을 추적하여 데이터베이스에 대한 작업을 미루는 것과는 조금 다릅니다. 이는 주로 객체의 동작을 보강하고 관리하는 목적으로 사용되며, 데이터베이스와의 상호 작용 뿐만 아니라 다양한 상황에서 유용하게 활용될 수 있습니다.

assertThat(getManagedEntities(session)).isEmpty();

List<FootballPlayer> players = s.createQuery("from FootballPlayer").getResultList();

assertThat(getManagedEntities(session)).size().isEqualTo(3);

일반적으로 s라는 변수는 주로 Hibernate 세션(Session) 객체를 가리키는데 사용되는 관례적인 변수명이다.
첫번째 줄의 assert 문에서 볼 수 있듯이 처음 Session을 확보했을 때 persistence context 저장소는 비어있다.
그 다음, 데이터베이스에서 데이터를 가져와, 그 데이터를 표현할 엔터티를 생성하고 반환하는 쿼리를 실행한다.
세션은 내부적으로 persistence context 저장소에 열어둔 모든 엔터티를 추적한다. 위 예시에서는 쿼리 실행 후 3개의 엔터티가 세션의 내부 저장소에 포함될 것이다.

Transaction transaction = session.getTransaction();
transaction.begin();

FootballPlayer gigiBuffon = players.stream()
  .filter(p -> p.getId() == 3)
  .findFirst()
  .get();

gigiBuffon.setName("Gianluigi Buffon");
transaction.commit();

assertThat(getDirtyEntities()).size().isEqualTo(1);
assertThat(getDirtyEntities().get(0).getName()).isEqualTo("Gianluigi Buffon");

트랜잭션이 commit() 또는 flush()를 호출할 때 세션은 dirty 엔터티를 추적리스트에서 찾아내어 데이터베이스의 상태에 동기화한다.
우리가 엔터티를 변경했다는 것을 세션에 알리기 위한 메소드를 호출할 필요가 없다는 점에 주목. Managed 엔터티는 데이터베이스에 자동적으로 변경사항을 전파하기 때문이다.
Managed 엔터티는 항상 영속(persistent) 엔터티이다. 데이터베이스에 행 표현이 생성되지 않았더라도 데이터베이스 식별자를 가지고 있어야 한다. 즉 Unit of Work의 종료를 INSERT문이 보류하고 있는 것이다.

"INSERT문이 보류하고 있는 것"은 새로운 데이터를 데이터베이스에 추가하려는 상황에서 해당 데이터의 영속성 컨텍스트에 대한 변경 사항이 트랜잭션의 커밋 시점까지 보류되고 있다는 의미입니다.
이는 객체의 변경 사항이 데이터베이스에 반영될 때, INSERT문을 실행하여 데이터베이스에 데이터를 추가하려고 할 때 발생할 수 있는 상황을 나타냅니다.

Detached Entity

  • Managed 엔터티와 달리 Detached 엔터티는 persistence context에 의해 추적되지 않는다.
  • Detached 엔터티는 데이터베이스 행에 따라 ID 값이 결정되는 POJO
  • 엔터티를 로드하는데 사용된 세션이 닫히거나 Session.evict(entity) 또는 Session.clear()를 호출할 때 엔터티가 Detached 상태가 된다.
  • Session.merge(entity) 또는 Session.update(entity)로 세션을 다시 연결할 수 있다.

"Detached 엔터티는 데이터베이스 행에 따라 ID 값이 결정되는 POJO"라는 내용은 일반적으로 올바르지 않을 수 있습니다. 엔터티의 ID 값은 주로 데이터베이스와 연결된 영역에서 결정됩니다. 일반적으로 JPA(Java Persistence API)와 같은 ORM 프레임워크를 사용할 때, 엔터티의 ID 값은 다음과 같은 방식으로 결정됩니다:

  • Assigned ID (수동 할당): 개발자가 직접 ID 값을 할당하는 경우입니다. 데이터베이스와 무관하게 개발자가 ID 값을 지정합니다.

  • Generated ID (자동 생성): 데이터베이스가 자동으로 ID 값을 생성하는 경우입니다. 대표적으로 AUTO_INCREMENT (MySQL), IDENTITY (MS SQL Server), SEQUENCE (Oracle)와 같은 자동 증가 기능을 사용하여 데이터베이스가 ID 값을 생성합니다.

  • Natural ID (자연 키): 비즈니스 로직에서 사용되는 유일한 속성을 엔터티의 ID로 사용하는 경우입니다. 예를 들어, 이메일 주소나 소셜 보안 번호와 같은 자연스러운 식별자를 사용할 수 있습니다.

  • Composite ID (복합 키): 여러 개의 속성을 결합하여 엔터티의 ID를 생성하는 경우입니다. 엔터티가 다른 엔터티와의 관계를 가지고 있을 때 주로 사용됩니다.

  • Detached 엔터티는 영속성 컨텍스트와의 연결이 끊긴 상태를 나타냅니다. 이때 ID 값은 이전에 영속성 컨텍스트에서 관리되었던 값을 가지며, 데이터베이스 행에 따라 결정되는 것은 아닙니다.

예시) Managed 엔터티가 Session.evict()에 의해 Detached 됨

여기서 1L의 L은 Long 형식을 나타내며, ID 값으로 사용됩니다. 데이터베이스에 저장된 FootballPlayer 엔터티 중에서 ID 값이 1인 엔터티를 조회하고, 조회한 엔터티 정보를 cr7 변수에 할당하는 것을 의미합니다.

FootballPlayer cr7 = session.get(FootballPlayer.class, 1L);

assertThat(getManagedEntities(session)).size().isEqualTo(1);
assertThat(getManagedEntities(session).get(0).getId()).isEqualTo(cr7.getId());

session.evict(cr7);

assertThat(getManagedEntities(session)).size().isEqualTo(0);
cr7.setName("CR7");
transaction.commit();

assertThat(getDirtyEntities()).isEmpty();

예시) Session.update()에 의해 reattach되는 세션

FootballPlayer messi = session.get(FootballPlayer.class, 2L);

session.evict(messi);
messi.setName("Leo Messi");
transaction.commit();

assertThat(getDirtyEntities()).isEmpty();

transaction = startTransaction(session);
session.update(messi);
transaction.commit();

assertThat(getDirtyEntities()).size().isEqualTo(1);
assertThat(getDirtyEntities().get(0).getName()).isEqualTo("Leo Messi");

아래 예시에서 생성자를 통해 엔터티를 인스턴스화 했다.
ID필드의 값을 setId()를 이용해 3으로 설정했다.
이는 Gigi Buffon에 대한 영속(persistent) 데이터의 아이디 값이다. Session.update()를 호출하는 것은 다른 persistence context에서 엔터티를 로드한 것과 같은 효과를 가진다.
세션은 다시 연결된(re-attached) 엔터티가 어디서 기원했는지 구별하지 않는다.
HTML form 값으로부터 분리된(detached) 엔터티를 생성하는 것은 웹 애플리케이션에서 흔한 시나리오다.
세션의 관점에서, 분리된 엔티티(detached entity)는 식별 값(identity value)이 영속 데이터(persistent data)와 일치하는 단순한 엔티티일 뿐이다.

Transient Entity

  • Transient 엔터티는 영속(persistent) 저장소에 표현되지 않고, 세션에 의해 관리되지 않는 단순한 엔터티이다.
  • 생성자로 새 엔터티를 초기화하는 경우가 Transient 엔터티의 대표적 예시다.
  • Transient 엔터티를 persistent로 만들기 위해서는 Session.save(entity) 또는 Session.saveOrUpdate(entity)를 호출해야 한다.
FootballPlayer neymar = new FootballPlayer();
neymar.setName("Neymar");
session.save(neymar);

assertThat(getManagedEntities(session)).size().isEqualTo(1);
assertThat(neymar.getId()).isNotNull();

int count = queryCount("select count(*) from Football_Player where name='Neymar'");

assertThat(count).isEqualTo(0);

transaction.commit();
count = queryCount("select count(*) from Football_Player where name='Neymar'");

assertThat(count).isEqualTo(1);

Session.save(entity)를 실행하면 해당 엔터티에 ID값이 부여되고 세션에 의해 관리(managed)된다.
Unit of Work이 종료될 때까지 INSERT 실행이 지연되기 때문에 데이터베이스에서는 아직 사용할 수 없을 수도 있다.

Deleted Entity

  • Session.delete(entity)가 호출되고 세션이 엔터티를 삭제하도록 표시한 경우 엔터티는 삭제(제거) 상태가 된다.
  • Unit of Work의 끝에 DELETE 명령이 시작된다.
  • 그러나 엔터티는 Unit of Work이 끝날 때까지 Persistence Context 저장소에 남아있다.
session.delete(neymar);

assertThat(getManagedEntities(session).get(0).getStatus()).isEqualTo(Status.DELETED);

0개의 댓글