본문 바로가기
jpa

Java HashMap 동작원리와 Hibernate 영속성 컨텍스트의 엔티티 관리 방식

by 권성호 2023. 4. 17.

사실이 아니라 공부한 내용과 생각을 정리한 글입니다. 언제든 가르침을 주신다면 감사하겠습니다.

JPA를 사용하면서 흔하게 듣는 이야기 중 하나로 엔티티를 작성할 때 equals & hashcode를 id 기반으로 재정의 하라는 말이 있다.

엔티티는 자바 객체이기도 하지만 데이터베이스의 레코드로 보는 것이 좀 더 본질적인 접근일 수 있고 그러한 접근으로 바라봤을 때 id를 기준으로 동등성을 정의하는 게 맞다는 이야기이다. (예를 들어 데이터베이스 입장에서 pk 가 1인 레코드에 대해 자바 객체 인스턴스가 2개가 만들어지면 이 두 인스턴스는 동등하다고 보는 게 자연스럽다.)

위 이야기에 대해 납득을 했고 모든 엔티티에 equals & hashcode 를 재정의 하던 중 한 가지 의문이 들었다.

 

내가 재정의 하고 있는 equals & hashcode 는 java의 hash 기반 collection 동작에 영향을 주는 것 아닌가? 영속성콘텍스트가 java의 hashMap으로 구성이 되어있다면, 내가 엔티티에 재정의하는 equals & hashcode는 영속성콘텍스트에도 영향을 줄 수 있는 거 아닐까? 혹시 내가 정의한 엔티티 클래스의 equals & hashcode를 잘못 정의하게 되면 영속성콘텍스트의 동작도 꼬일 수도 있는 건가?

이 질문에 답을 얻기 위해 우선 영속성콘텍스트를 뜯어보기로 했다.

영속성콘텍스트의 실체를 찾아보자

jpa를 통해 특정 대상을 데이터베이스로부터 조회할 때, 항상 영속성콘텍스트를 먼저 확인하는 것을 이론으로 배웠던 터라 SimpleJpaRepository#findById 메서드를 따라가다 보면 영속성콘텍스트의 실체를 만날 수 있을 거라고 기대하며 코드를 따라가 봤다.

코드를 따라가다 보면 org.hibernate.internal.SessionImpl 클래스를 만나게 되고 그 클래스의 필드로 StatefulPersistenceContext를 찾을 수 있다. StatefulPersistenceContext, 요게 영속성콘텍스트의 실체인 것 같다...!

StatefulPersistenceContext를 따라 들어가면 그 속에 아래와 같은 필드들이 선언된 것을 확인할 수 있다.

// 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;

결국 영속성콘텍스트는 내부적으로 HashMap을 사용해 엔티티를 관리하고 있던 것이다.

그렇다면, HashMap 은 어떻게 동작할까?

Java HashMap 동작원리 와 Equals & Hashcode 

HashMap 은 <Key, Value> 형태(Map 인터페이스의 구현체)로 생겼고 Key를 기준으로 저장 & 삭제 & 수정 & 조회 작업이 이루어진다.

조금 더 구체적으로, 저장 & 삭제 & 수정 & 조회 작업을 위해선 그 작업의 대상이 되는 위치를 찾을 수 있어야 하는데, 이 작업이 Key를 기준으로 이루어진다. 문제를 저장과 조회로 단순화해 보자.

저장 시에는

  1. hashcode 메서드를 통해 Key의 hashcode를 얻어서 버킷의 위치를 찾는다. 
  2. 찾은 버킷에 Key가 없다면 저장대상을 넣고 끝낸다.
  3. 찾은 버킷에 Key가 있다면(그 버킷 내의 원소는 linkedList 구조다) linkedList를 순회하면서 각 원소의 Key 값과 equals 메서드를 통해 동등성 비교를 진행한다.
    1. 동일한 Key가 이미 존재한다면, 그 Key에 대응하는 곳에 저장대상을 덮어쓴다.
    2. 동일한 Key가 없다면, 버킷의 linkedList에 저장대상을 추가한다.

조회 시에는

  1. hashcode 메서드를 통해 Key의 hashcode를 얻어서 버킷의 위치를 찾는다.
  2. 찾은 버킷에 Key가 없다면 조회 작업을 종료한다.
  3. 찾은 버킷에 Key가 1개 있다면 해당 원소를 반환한다.
  4. 찾은 버킷에 Key가 여러 개 있다면 likedList를 순회하면서 각 Key에 대해 equals 메서드를 통해 동등성 비교를 진행한다.
    1. 동일한 Key 가 있다면 해당 원소를 반환한다.
    2. 동일한 Key 가 없다면 조회 작업을 종료한다.

즉, Java HashMap 은 Key의 hashcode 메서드를 해시함수로 사용하고 해시 충돌 발생 시 충돌된 원소들을 linkedList로 관리한다. 그리고 linkedList 내의 원소 사이의 동등성 구분은 Key의 equals 메서드를 통해 수행한다. 결국, Key에 들어갈 자료구조의 hashcode와 equals는 HashMap의 동작에 영향을 줄 수 있다.

그렇다면, 우리가 엔티티를 만들 때 정의한 equals & hashcode는 영속성콘텍스트의 동작에 영향을 줄 수 있을까?

Hibernate 영속성 콘텍스트도 HashMap인데 여기서는 어떻게 동작하는지?

StatefulPersistenceContext 내부의 엔티티 관리를 위한 HashMap으로 다시 돌아가보자.

선언된 HashMap을 살펴보면 Key 가 EntityKey 타입인 것을 확인할 수 있다. 즉, 해당 HashMap은 EntityKey에서 정의한 equals & hashcode 기반으로 동작한다. EntityKey 클래스로 들어가서 equals & hashcode를 살펴보자

public EntityKey(Serializable id, EntityPersister persister) {
    this.persister = persister;
    if ( id == null ) {
        throw new AssertionFailure( "null identifier" );
    }
    this.identifier = id;
    this.hashCode = generateHashCode();
}

@Override
public boolean equals(Object other) {
   if ( this == other ) {
      return true;
   }
   if ( other == null || EntityKey.class != other.getClass() ) {
      return false;
   }

   final EntityKey otherKey = (EntityKey) other;
   return samePersistentType( otherKey )
         && sameIdentifier( otherKey );

}

@Override
public int hashCode() {
    return hashCode;
}
private int generateHashCode() {
   int result = 17;
   final String rootEntityName = persister.getRootEntityName();
   result = 37 * result + ( rootEntityName != null ? rootEntityName.hashCode() : 0 );
   result = 37 * result + persister.getIdentifierType().getHashCode( identifier, persister.getFactory() );
   return result;
}

JPA 엔티티에 작성한 equals & hashcode와 별개의 equals & hashcode 가 있는 것을 확인할 수 있다.

즉, 우리가 엔티티에 재정의한 equals & hashcode는 영속성콘텍스트의 관리에 영향을 미치지 않는다는 것이다.

다시 처음으로 돌아가서, 그럼 정말 엔티티에 equals & hashcode 는 필수적으로 재정의해야 하나?

지금까지 살펴본 영속성콘텍스트의 동작을 정리해 보면, 영속성콘텍스트는 우리가 만든 엔티티의 equals & hashcode에 의존하지 않고, 아이디(EntityKey.class)를 기반으로 java의 hashmap을 사용해 엔티티를 관리하고 있는 것을 알 수 있다. 따라서 우리가 equals & hashcode를 어떻게 정의해도 영속성콘텍스트의 동작에는 영향을 주지 않는다.

 

다만 영속성콘텍스트의 동작방식을 보면 jpa가 엔티티를 바라보는 사상을 알 수 있다. 그리고 이를 통해 우리가 엔티티를 어떻게 바라봐야 하는지도 유추할 수 있다.

영속성콘텍스트의 구현 레벨의 동작과 무관하게, 엔티티의 책임&성격상 equals & hashcode는 id 기반으로 정의되어야 한다.

 

참고

댓글