@Transient 어노테이션과 영속성 컨텍스트 (2) - 분석편

조갱·2023년 11월 13일
0

저번 포스팅에서 영속성 컨텍스트의 이론에 대해서 살펴봤다.
이번 포스팅에서는 직접 디버깅을 하며 동작 과정에 대해 알아보자.

Hibernate의 EntityManager 구현체 - SessionImpl

class 경로 : org.hibernate.internal.SessionImpl
org.hibernate.Session 클래스를 상속받는다.

영속성 컨텍스트

SessionImpl 클래스 (Hibernate의 EntityManager)를 보면, 영속성 컨텍스트를 가지고 있는 것을 확인할 수 있다.

public class SessionImpl
		extends ...
		implements ... {

	...
	private transient StatefulPersistenceContext persistenceContext;

StatefulPersistenceContext가 Hibernate가 구현하는 영속성 컨텍스트이다. (StatefulPersistenceContext는 PersistenceContext 를 상속받는다.)

1차 캐싱

Hibernate의 영속성 컨텍스트 (StatefulPersistenceContext)를 보면, 1차 캐싱 및 더티 체킹을 위한 객체를 가지고 있음을 볼 수 있다.

보통 캐싱된 데이터는 PersistenceContext 객체 내에 있는 여러 Map 형태의 변수에 저장된다. 그 중, 몇 가지 중요한 변수들을 뽑아보자면

  • entityEntries
    Entity 객체를 식별자(PK)와 매핑하는 데 사용되는 Map이다.
    여기에 Entity 객체가 캐싱된다.
  • collections
    (관련 Entity의) 컬렉션을 캐싱하기 위한 Map이다.
  • proxies
    프록시 객체를 캐싱하기 위한 Map이다.

이러한 Map들은 영속성 컨텍스트의 특정 기능(예: Entity 조회)을 수행할 때 갱신된다.

public class StatefulPersistenceContext implements PersistenceContext {
	...
    
	// Loaded entity instances, by EntityKey
	private HashMap<EntityKey, Object> entitiesByKey;

	// Loaded entity instances, by EntityUniqueKey
	private HashMap<EntityUniqueKey, Object> entitiesByUniqueKey;

	// Entity proxies, by EntityKey
	private ConcurrentReferenceHashMap<EntityKey, Object> proxiesByKey;

	// Snapshots of current database state for entities
	// that have *not* been loaded
	private HashMap<EntityKey, Object> entitySnapshotsByKey;

	// Identity map of array holder ArrayHolder instances, by the array instance
	private IdentityHashMap<Object, PersistentCollection> arrayHolders;

	// Identity map of CollectionEntry instances, by the collection wrapper
	private IdentityMap<PersistentCollection, CollectionEntry> collectionEntries;

	// Collection wrappers, by the CollectionKey
	private HashMap<CollectionKey, PersistentCollection> collectionsByKey;

	// Set of EntityKeys of deleted objects
	private HashSet<EntityKey> nullifiableEntityKeys;

	// properties that we have tried to load, and not found in the database
	private HashSet<AssociationKey> nullAssociations;

	// A list of collection wrappers that were instantiating during result set
	// processing, that we will need to initialize at the end of the query
	private ArrayList<PersistentCollection> nonlazyCollections;

	// A container for collections we load up when the owning entity is not
	// yet loaded ... for now, this is purely transient!
	private HashMap<CollectionKey,PersistentCollection> unownedCollections;

	// Parent entities cache by their child for cascading
	// May be empty or not contains all relation
	private IdentityHashMap<Object,Object> parentsByChild;
    ...
    
    @Override
	public Entry<Object,EntityEntry>[] reentrantSafeEntityEntries() {
		return entityEntryContext.reentrantSafeEntityEntries();
	}

여기서 EnriryEntries는 나중에 더티체킹을 위한 1차캐싱으로 사용된다.

flush와 더티 체킹

org.hibernate.event.internal.DefaultFlushEntityEventListener 클래스에서, Entity에 flush가 발생하면 실행되는 이벤트 리스너를 확인할 수 있다.

/**
 * An event that occurs for each entity instance at flush time
 *
 * @author Gavin King
 */
public class DefaultFlushEntityEventListener implements FlushEntityEventListener, CallbackRegistryConsumer {
	

	/**
	 * Flushes a single entity's state to the database, by scheduling
	 * an update action, if necessary
	 */
	public void onFlushEntity(FlushEntityEvent event) throws HibernateException {
		... // 중략

		if ( isUpdateNecessary( event, mightBeDirty ) ) {
			substitute = scheduleUpdate( event ) || substitute;
		}
	}
private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mightBeDirty) {
	final Status status = event.getEntityEntry().getStatus();
	if ( mightBeDirty || status == Status.DELETED ) {
		// compare to cached state (ignoring collections unless versioned)
		dirtyCheck( event );
		... // 중략
protected void dirtyCheck(final FlushEntityEvent event) throws HibernateException {
	... // 중략
	final EntityEntry entry = event.getEntityEntry();
	final Object[] loadedState = entry.getLoadedState();

	int[] dirtyProperties = session.getInterceptor().findDirty(...);
    if ( dirtyProperties == null ) {
        if ( entity instanceof SelfDirtinessTracker ) {
            if ( ( (SelfDirtinessTracker) entity ).$$_hibernate_hasDirtyAttributes() || persister.hasMutableProperties() ) {
					dirtyProperties = persister.resolveDirtyAttributeIndexes(...);
					... // 중략

더티체크는 위와 같은 방식으로 진행된다.
loadedState는 값이 변경되었는지 확인하기 위한 스냅샷이다.
getLoadedState() 를 타고 올라가보면, StatefulPersistenceContext.reentrantSafeEntityEntries() 로부터 얻어옴을 확인할 수 있을것이다.

findDirty(...) 메소드와
persister.resolvedDirtyAttributeIndexes(...) 의 구현체인 org.hibernate.persister.entity.AbstractEntityPersister.resolvedDirtyAttributeIndexes(...) 를 살펴보면, loadedState가 previousState로써 사용되고, 더티체킹을 위한 로직에 사용됨을 확인할 수 있다.

final boolean dirty = currentState[i] != LazyPropertyInitializer.UNFETCHED_PROPERTY &&
	// Consider mutable properties as dirty if we don't have a previous state
	(previousState == null ||
      previousState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ||
    	(propertyCheckability[i] && propertyTypes[i].isDirty(previousState[i],currentState[i], propertyColumnUpdateable[i], session))
    );

Transaction과 Commit, Rollback

트랜잭션이 커밋될 때, AbstractPlatformTransactionManager 클래스의 commit() 메소드가 실행된다.

@Override
public final void commit(TransactionStatus status) throws TransactionException {
	... // 중략
	processCommit(defStatus);
}

클래스의 앞에 붙은 Abstract 에서도 알 수 있듯, 실제 커밋하는 역할은 각 구현체에서 수행하는데, Jpa를 사용한다면 JpaTransactionManager 클래스의 doCommit() 이 실행된다.

@Override
protected void doCommit(DefaultTransactionStatus status) {
	JpaTransactionObject txObject = (JpaTransactionObject) status.getTransaction();
	... // 중략
	try {
		EntityTransaction tx = txObject.getEntityManagerHolder().getEntityManager().getTransaction();
		tx.commit();
	}
	... // 중략
}

그리고 이 commit() 이 수행되면, Hibernate의 구현체인 SessionImpl 클래스의 beforeTransactionCompletion 메소드에서 Transaction이 commit되기 전에 flush()가 수행됨을 알 수 있다.

@Override
public void beforeTransactionCompletion() {
	... // 중략
	flushBeforeTransactionCompletion();
    ... // 중략
}

그리고 영속성 컨텍스트의 타입에 따라 Commit 이후 Entity의 영속 상태가 달라질 수 있음에 유의하자.

JSR338 을 참조하면, 아래와 같은 구문을 볼 수 있다.

3.3 Persistence Context Lifetime and Synchronization Type
... (중략)
An EntityManager with an extended persistence context maintains its references to the entity objects
after a transaction has committed. Those objects remain managed by the EntityManager, and they can
be updated as managed objects between transactions.
... (중략)
3.3.2 Transaction Commit
The managed entities of a transaction-scoped persistence context become detached when the transaction
commits; the managed entities of an extended persistence context remain managed.
3.3.3 Transaction Rollback
For both transaction-scoped persistence contexts and for extended persistence contexts that are joined to
the current transaction, transaction rollback causes all pre-existing managed instances and removed
instances to become detached.
... (중략)

어떤 말이냐면,
3.3.2 항목에서

  • Transaction-scoped Persistence
    commit 이후에 Entity가 EntityManager 에서 detach 될 수 있음
  • Extended Persistence
    commit 이후에도 Entity가 EntityManager에 남아있음

그리고, 3.3.3 항목에서는
Transaction-scoped Persistence 와 Extended Persistence 모두 Rollback 이후에는 Entity가 EntityManager에서 detach 된다는 의미이다.

그러면, Transaction-scoped Persistence와 Extended Persistence의 차이는 무엇일까?
EJB 3.0의 스펙인 JSR220 을 참조하면 그 뜻을 알 수 있는데
아래와 같이 소개되고 있다.

5.6 Container-managed Persistence Contexts
(...)

A container-managed persistence context may be defined to have either a lifetime that is scoped to a single transaction
or an extended lifetime that spans multiple transactions, depending on the PersistenceContextType that is specified when its EntityManager is created.
This specification refers to such persistence contexts as transaction-scoped persistence contexts and extended persistence contexts respectively.

(...)

5.6.1 Container-managed Transaction-scoped Persistence Context
The application may obtain a container-managed entity manager with transaction-scoped persistence context
bound to the JTA transaction by injection or direct lookup in the JNDI namespace.
The persistence context type for the entity manager is defaulted or defined as PersistenceContextType.TRANSACTION.

A new persistence context begins when the container-managed entity manager is invoked in the scope of an active JTA transaction,
and there is no current persistence context already associated with the JTA transaction.
The persistence context is created and then associated with the JTA transaction.

The persistence context ends when the associated JTA transaction commits or rolls back,
and all entities that were managed by the EntityManager become detached.

If the entity manager is invoked outside the scope of a transaction,
any entities loaded from the database will immediately become detached at the end of the method call.

5.6.2 Container-managed Extended Persistence Context
A container-managed extended persistence context can only be initiated within the scope of a stateful session bean.
It exists from the point at which the stateful session bean that declares a dependency on an entity manager of type PersistenceContextType.
EXTENDED is created, and is said to be bound to the stateful session bean.
The dependency on the extended persistence context is declared by means of the PersistenceContext annotation or persistence-context-ref deployment descriptor element.

The persistence context is closed by the container when the @Remove method of the stateful session bean completes (or the stateful session bean instance is otherwise destroyed).

(...)

org.springframework.orm.jpa.JpaTransactionManager 클래스의 doCleanupAfterCompletion 메소드를 보면, 트랜잭션 이후에 Entity 매니저가 닫히면서 영속 대상에서 제외됨을 확인할 수 있다.

관련 스택 오버플로우

@Override
protected void doCleanupAfterCompletion(Object transaction) {
	JpaTransactionObject txObject = (JpaTransactionObject) transaction;

	... // 중략
	// Remove the entity manager holder from the thread.
	if (txObject.isNewEntityManagerHolder()) {
		EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
		if (logger.isDebugEnabled()) {
			logger.debug("Closing JPA EntityManager [" + em + "] after transaction");
		}
		
        	EntityManagerFactoryUtils.closeEntityManager(em);
	} else {
		logger.debug("Not closing pre-bound JPA EntityManager after transaction");
    }
}

실제로, Persistence Context의 type을 TRANSACTION 으로 설정하면 아래와 같이 entityManager가 닫히는 부분에 브레이크 포인트가 걸린다.

profile
A fast learner.

0개의 댓글