사실이 아니라 공부한 내용과 생각을 정리한 글입니다. 언제든 가르침을 주신다면 감사하겠습니다.
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를 기준으로 이루어진다. 문제를 저장과 조회로 단순화해 보자.
저장 시에는
- hashcode 메서드를 통해 Key의 hashcode를 얻어서 버킷의 위치를 찾는다.
- 찾은 버킷에 Key가 없다면 저장대상을 넣고 끝낸다.
- 찾은 버킷에 Key가 있다면(그 버킷 내의 원소는 linkedList 구조다) linkedList를 순회하면서 각 원소의 Key 값과 equals 메서드를 통해 동등성 비교를 진행한다.
- 동일한 Key가 이미 존재한다면, 그 Key에 대응하는 곳에 저장대상을 덮어쓴다.
- 동일한 Key가 없다면, 버킷의 linkedList에 저장대상을 추가한다.
조회 시에는
- hashcode 메서드를 통해 Key의 hashcode를 얻어서 버킷의 위치를 찾는다.
- 찾은 버킷에 Key가 없다면 조회 작업을 종료한다.
- 찾은 버킷에 Key가 1개 있다면 해당 원소를 반환한다.
- 찾은 버킷에 Key가 여러 개 있다면 likedList를 순회하면서 각 Key에 대해 equals 메서드를 통해 동등성 비교를 진행한다.
- 동일한 Key 가 있다면 해당 원소를 반환한다.
- 동일한 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 기반으로 정의되어야 한다.
참고
'jpa' 카테고리의 다른 글
Jpa @PrePersist & @PreUpdate 사용해서 Entity 부가정보와 도메인 관심사 분리하기 (0) | 2023.05.01 |
---|---|
jpa hibernate 프록시와 entity equals (0) | 2023.03.25 |
@Transactional 정리가 필요해 (0) | 2021.12.17 |
댓글