본문 바로가기
Homo Faber/Concepts

Hibernate에서 Equals 와 HashCode

by javauser 2008. 2. 21.

자바의 Collection과 관계형 DB (Hibernate)는 통일된 방법으로 객체의 구분을 수행하는데 매우 연관되어 있다. 관계형 DB에서 이는 PK로 하고, 자바에서는 객체의 equals()와 hashCode() 메소드를 사용한다. 다음은 저장(persistent) 클래스에서 equals()와 hashCode()를 구현하는 최적의 방안을 설명한다.

왜 equals()와 hashCode()가 중요한가

보통 대부분의 자바 객체는 객체의 식별자에 근거하여 내장된 equals()와 hashCode()를 제공한다. 따라서 객체에 대해서 new() 를 할 때마다 모든 객체는 서로 다르게 마련이다.

이것인 일반적으로 보통의 자바 프로그램에서 원하는 것이다. 그리고 만일 모든 객체가 메모리에 있다면 이는 좋은 모델이다. 물론, Hibernate의 전반적인 작업은 메모리 영역 외부에 객체를 옮겨 놓는다. 하지만 Hibernate를 사용한다면 이에 대한 고민을 심각하게 해봐야 한다.

Hibernate는 이러한 유일성(uniqueness)를 관리하기 위해서 Hibernate 세션을 사용한다. new()를 사용해서 객체를 생성하고 세션에 저장했을때 Hibernate는 이제 객체에 대해서 쿼리하고 특정 객체를 검색할 때마다 Hibernate가 해당 객체의 해당 인스턴스를 반환해야 한다는 것을 안다. 또한, Hibernate는 바로 그렇게 작동할 것이다.

하지만, 일단 Hibernate 세션을 닫으면 모든 것이 최선의 상태가 아니가. 만일 이제 막 닫은 Hibernate 세션에 생성되거나 로딩된 객체를 계속해서 갖고 있는다면 Hibernate는 그 객체들에 대해서 알 수 있는 방법이 없다. 따라서 또 다른 세션을 열고 "동일한" 객체에 대해서 쿼리를 하면 Hibernate는 새로운 인스턴스를 반환할 것이다. 즉, 세션 사이에서 왔다갔다 하는 객체들의 집합을 계속해서 유지한다면 이상한 행위를 경험하기 시작할 것이다. (주로, collection 내에 중복되는 객체들)

일반적인 원칙은 다음과 같다. 만일 List나 Map, 혹은 Set에 객체를 저장하기 원하면 equals와 hashCode를 구현해서 문서에서 지정된 대로 표준 원칙을 준수하게끔 하는 것이 필요하다.

결국 문제는 무엇인가?

세션에서 세션으로 옮겨다니는 객체, 예를 들어, 몇개의 Hibernate 세션에 걸쳐있는 특정 어플리케이션의 사용자나 다른 범위와 관련된 Set에 있는 객체를 계속해서 유지하기를 원한다고 해보자.

이러한 것을 해결하는 가장 자연스러운 방법은 DB 식별자 (PK 속성) 로 매핑되는 속성을 비교함으로써 equals()와 hashCode()를 구현하는 것이다. 하지만, 이는 Hibernate가 새로운 객체를 저장한 후에 식별자 값을 세팅하기 때문에 새로 생성된 객체에 대해서는 문제를 발생시킬 것이다. 따라서 각각의 새로운 인스턴스는 동일한 식별자인 null (혹은 0)을 갖게 된다. 예를 들어, 다음과 같이 어떤 새로운 객체에 Set을 추가한다고 하면,

// UserManager와 User가 Hibernate로 매핑된 빈이라고 가정
UserManager u = session.load(UserManager.class, id);

// id = null 이거나 id = 0 인 새로운 엔티티를 추가
u.getUserSet().add(new User("newUsername1"));
// 역시 id = null을 갖는데, 마지막으로 추가된 객체를 중복 저장됨.
u.getUserSet().add(new User("newUsername2"));

// u.getUserSet() 은 이제 두번째 User만을 포함함.

보는 바와 같이 저장(persistent) 클래스에 대한 DB 식별자 비교에 의존하는 것은 Hibernate의 자동 생성 id를 사용하고 있다면 문제가 발생될 수 있다. 왜냐하면 식별자 값은 객체가 저장되기 전에 세팅되지 않기 때문이다. 식별자 값은 객체를 저장상태(persistent)로 만들면서 임시(transient) 객체에 session.save()가 호출될 때 세팅된다.

만일 수동으로 id를 할당하는 것(예를 들어, "assigned" generator)을 사용한다면, 이러한 문제에 전혀 봉착되지 않으며 객체를 Set에 넣기 전에 식별자 값을 세팅하는 것을 보장하기만 하면 된다. 반면에 이는 대부분의 어플리케이션에서 보장하기가 매우 어렵다.

객체 id와 비즈니스 키 분리

이러한 문제를 피하기 우해서 equals() (와 hashCode())를 구현하는 저장(persistent) 객체의 "semi-유일한 속성을 사용하는 것을 권한다. 기본적으로 DB 식별자는 비즈니스 의미가 전혀 가지고 있지 않다고 생각해야만 한다. (surrogate 식별자 속성과 자동 생성 값은 어찌되었든 추천한다.) DB 식별자 속성은 객체 식별자가 되어야 하며, 기본적으로 Hibernate에서만 사용되어야 한다. 물론, DB 식별자를 읽기 속성(read-only) 처리(예를 들어, 웹 어플리케이션에서 링크를 생성)를 편리하게 하는 것으로 사용할 수도 있다.

동등 비교(equality comparison)으로 DB 식별자를 사용하는 대신에, 개별 객체를 식별하는 equals() 에 대한 속성의 집합을 사용해야 한다. 예를 들어, "Item" 클래스가 있고 "name" String과 "created" Date를 가지고 있다면, equals()를 구현하는데 두가지 모두를 사용할 수 있다. 저장 식별자(persistent identifier)를 사용할 필요가 없으며, 소위 "비즈니스 키"가 훨씬 좋다. 자연적인 key 이지만 이번에는 이를 사용한다고 해서 잘못된 것은 없다!

두개 모두의 필드의 조합은 Item을 포함하는 Set이 살아있는 동안에 충분히 안정적이다. PK 만큰 좋지는 않겠지만, 분명 후보 Key이다. 이를 객체에 대한 "관계형 식별자"를 정의하는 것으로 생각할 수도 있다. - 관계형 모델에서 유일한 필드가 될 수 있는 key 필드이거나 최소한 저장 클래스의 불변의 속성.("created" Date 는 결코 변경되지 않음)

위의 예제에서 아마 "username" 속성을 사용할 수도 있다.

save/flush를 수행하는 대안

만일 정말로 equals()/hashCode() 에 대해서 저장 id 사용을 피할 수 없거나 세션에서 세션으로 옮겨다는 객체를 계속해서 유지해야 한다면(기본 equals()를 사용할 수 없다면), 객체 생성후와 set에 삽입전에 save()/flush()를 강제함으로써 작업을 수행할 수 있다.

// UserManager와 User는 Hibernate를 사용해서 매핑된 빈이라고 가정
UserManager u = session.load(UserManager.class, id);

User newUser = new User("newUsername1");
// u.getUserSet().add(newUser);   // 아직 SET에 추가하지 말라!
session.save(newUser);
session.flush();                         // 이제 id 가 새로운 User 객체에 할당됨.
u.getUserSet().add(newUser);     // 이제 set에 추가가 가능함.
newUser = new User("newUsername2");
session.save(newUser);
session.flush();
u.getUserSet().add(newUser);      // 이제 userSet에는 두개 모두의 user들이 포함됨.

매우 비효율적이며 추천하고 싶지 않음을 유의하라. 또한 thin 클라이언트의 연결이 끊어진 객체 그래프를 사용할 때 깨지기 쉬움을 유의하라.

// 클라이언트에서 UserManager가 비어있다고 가정.
UserManager u = userManagerSessionBean.load(UserManager.class, id);
User newUser = new User("newUsername1");
u.getUserSet().add(newUser);   // 클라이언트는 저장할 수 없으므로 set에 추가해야 함.
userManagerSessionBean.updateUserManager(u);

// 서버
UserManagerSessionBean updateUserManager(UserManager u) {
   // 먼저 user를 꺼냄 (이 예제는 단 하나만 있다고 가정)
   User newUser = (User)u.getUserSet().iterator().next();
   session.saveOrUpdate(u);
   if (!u.getUserSet().contains(newUser)) System.err.println("User set corrupted");
}

위의 예제는 실제로 "User set corrupted"를 화면에 찍는다. 왜냐하면 newUser의 hashcode는 saveOrUpdate 호출로 인해 변경될 것이다.

이러한 문제들은 자바의 객체 식별자가 Hibernate에 할당된 DB 식별자에 직접적으로 매핑되는 것처럼 보이지만, 실제적으로는 두개가 서로 다르기 때문에 모두 헛수고가 된다. 심지어 DB 식별자는 객체가 저장되기 전까지 존재하지도 않는다. 객체의 식별자는 객체의 저장 여부와 상관없어야 하지만, equals()와 hashCode() 메소드가 Hibernate 식별자를 사용한다면 객체 id는 저장할 때 바뀐다.

이러한 메소드 작성이 귀찮다. Hibernate는 도움을 줄 수 없는가?

Hibernate가 제공할 수 있는 "도움" 방편은 hbm2java 이다. hbm2java는 위에서 설명한 문제 때문에 id를 기반으로 equals/hashcode를 생성하지 않는다.

적절한 equals/hashcode를 생성하기 위해서 hbm2java를 통해 특정 속성에 <meta attribute="user-in-equals">true</meta>를 표시할 수 있다.

요약

이상 위의 것을 모두 요약하면 equals/hashCode를 처리하는 서로 다른 방법을 사용해서 동작하는 것과 그렇지 않은 것이 다음의 표와 같다.


  eq/hC 사용안함 id 속성으로 eq/hC 사용 비즈니스 키로 eq/hC 사용
복합ID에서 사용 No Yes Yes
set의 여러개의 새로운 인스턴스 Yes No Yes
다른 세션으로부터 동일한 객체에 대한 동일성 No Yes Yes
저장후 collection에 영향받지 않음 Yes No Yes
다양한 문제들은 다음과 같다.

복합 ID에서 사용

복합 ID로 객체를 사용하기 위해서 equals/hasCode를 동일한 방식으로 구현해야 하며, == 식별자는 이 경우 충분치 못함.

set에 여러개의 새로운 인스턴스

다음이 작동하거나 그렇지 않음.
HashSet someSet = new HashSet();
someSet.add(new PersistentClass());
someSet.add(new PersistentClass());
assert(someSet.size() == 2);

다른 세션으로부터 동일한 객체에 대한 동일성

다음이 작동하거나 그렇지 않음.
PersistentClass p1 = sessionOne.load(PersistentClass.class, new Integer(1));
PersistentClass p2 = sessionTwo.load(PersistentClass.class, new Integer(1));
assert(p1.equals(p2));

저장후 collection에 영향받지 않음

다음이 작동하거나 그렇지 않음.
HashSet set = new HashSet();
User u = new User();
set.add(u);
session.save(u);
assert(set.contains(u));

equals와 hashcode에 대한 최상의 기법들
Effective Java Programming Language Guide, equals()와 hashCode() 데 대한 발췌chapter
Java Theory and practice : Hash it out, IBM의 기사
Manish Hatwalne의 정확하게 equals와 hashCode를 구현하는 방법에 대한 기사 : Equals and HashCode
비즈니스 식별자를 저장하지 않고 구현 가능한 토론 : Equals and hashCode: Is there *any* non-broken approach?

java.lang.Object 문서에 따르면 hashCode() 에 대해 항상 0으로 반환하는 것은 완벽하게 괜찮아야 한다. 유일한 객체에 대해 유일한 수로 반환하는 hashCode() 구현의 분명한 효과는 성능을 향상시킬 수 있다는 것이다. 단점은 hashCode()의 행위는 equals()와 반드시 일치해야 한다는 것이다. a와 b 객체에 대해서 만일 a.equals(b)가 참이면, a.hashCode() == b.hashCode()가 참이어야 한다. 하지만, a.equals(b)가 거짓이면 a.hashCode() == b.hashCode()가 여전히 참일 수 있다. '0으로 반환'하는 hashCode()를 구현하는 것은 이러한 조건을 만족하지만 HashSet이나 HashMap과 같은 Hash 기반의 collection에 단적으로 비효율적이다.
반응형