티스토리 툴바



2007/10/01 20:58

Equals and Hashcode

10.01 update.

[추가] 참조 아티클
- giledeveloper
- blog in artima.com
- Custom Builder
- Effectively using equals()/hashCode()
- Don't Let Hibernate Steal Your Identity (번역본)

원문 : 하이버네이트 위키 페이지[Equals and Hashcode]

Equals and Hashcode

자바의 콜렉션과 관계형 데이터베이스(하이버네이트 또한)는 통합된 방법으로 객체를 구별할 수 있는 방법은 매우 의존적이다. 관계형 데이터베이스의 경우 주키로 구별되며, 자바는 객체에 equals()와 hashCode() 메소드를 가지고 있다. 이번 페이지는 영속성 객체를 위한 equals()와 hashCode()의 구현을 위한 가장 최상의 전략에 대해서 논의할 것이다.

왜 equals()와 hashCode()가 중요한가!
일반적으로, 대부분의 자바 객체는 객체 동일성(identity)의 기반이 되는 equals()와 hashCode() 메소드를 내부적으로 제공한다. 그래서 new()를 통해 생성된 각 객체는 다른 객체들과 차이를 갖게 된다.

이것은 자바 프로그래밍을 원할 경우에는 매우 일반적이다. 그리고 만약 객체가 메모리에 올라간 경우, 이것은 좋은 모델이다. 물론 하이버네이트의 전체 작업에서 객체는 메모리에 올라가게 된다. 그러나 하이버네이트는 이 부분에서 걱정되는 것을 예방하기 최선을 다한다.

하이버네이트는 이 독특성을 관리하기 위해서 하이버네이트 세션을 사용한다. new 키워드를 사용해서 새로운 객체를 만들고, 이 객체를 세션에 저장한 경우, 하이버네이트는 이 객체에 대해서 쿼리를 수행할 경우라면 언제든지 이 특정 객체를 세션에서 찾은 다음, 사용자에게 이 객체의 인스턴스를 리턴 한다. 그리고 하이버네이트는 이대로 수행할 것이다.

그렇지만, 일단 하이버네이트 세션이 닫히면, 이 모든 보증은 없어진다. 하이버네이트 현재 닫친 세션에서 로드했거나 생성한 객체를 계속 유지하고 싶어도, 하이버네이트는 이 객체에 대해서 알 방법이 없다. 그래서 다른 세션을 열고, “같은”객체에 대해 쿼리를 수행한 경우, 하이버네이트는 새로운 인스턴스를 리턴 할 것이다. 그 결과 세션 사이에 객체의 콜렉션을 유지하길 원했을 경우, 당신은 뜻밖의 행동을 경험하게 될 것이다.(콜렉션에 같은 객체가 들어가는 것이 가장 빈번할 것이다.)

일반적으로 List, Map이나 Set 콜렉션에 객체를 저장하길 원하는 경우 (so they obey the standard contract as specified in the documentation) equals()와 hashCode를 구현하는 것이 필요하다.

결국 문제는 무엇인가?
그래서 세션과 세션 사이 동안에 객체를 유지하길 원하는 것에 대해서 말해보자. 예를 들어 몇 개의 짧은 하이버네이트 세션이 있는 특정 어플리케이션 유저와 다른 영역을 포함하는 것에 관련된 Set 콜레션이 있다고 하자.

가장 좋은 생각은 데이터베이스 식별자(즉, 주키 속성)와 매핑된 속성을 비교하는 equals()와 hashCode()를 구현하는 생각이다. 그렇지만 이 구현은 새로운 객체를 생성했을 때 문제의 원인이 될 것이다. 새로 생성한 객체는 데이터베이스에 저장한 후에 식별자 값을 받을 수 있기 때문이다. 그러므로 각 새로 생성된 인스턴스는 null(혹은 <literal> 0 </literal>)이란 같은 식별자를 갖고 있다. 예를 들어, Set에 새로운 객체를 추가한 경우 :

// Suppose UserManager and User are Beans mapped with Hibernate

UserManager u = session.load(UserManager.class, id);

u.getUserSet().add(new User("newUsername1")); // adds a new Entity with id = null or id = 0
u.getUserSet().add(new User("newUsername2")); // has id=null, too, so overwrites last added object.

// u.getUserSet() now contains only the second User

영속 객체 비교는 데이터베이스 식별자에 의존적이기 때문에, 객체가 저장되기 전에는 식별자 값을 받을 수 없는 하이버네이트에서 생성하는 id값을 사용한 경우 문제가 발생 할 것이다. 식별자 값은 transient 객체에 대해 session.save()를 호출한 경우 할당되며, 이때 이 객체는 영속 객체가 된다.

만약 사용자 임의로 id값을 할당(예를 들어, “assigned" generator) 한다면, 위의 모든 것은 문제가 되지 않고, Set 콜렉션에 객체를 추가하기 전에 객체에 저장되어 있는 식별자를 확인하는 것만 확실히 하면 된다. 다시 말하면, 이것은 대부분의 어플리케이션에서 보장하기 매우 어렵다.

객체 ID와 비즈니스 키 분리해내기
이 문제를 피하기 위해서 우리는 equals()와 hashCode()의 구현을 위해서 영속 객체 클래스의 “준”-유일 속성을 사용할 것을 추천한다. 기본적으로 비즈니스 로직을 담고 있지 않는 데이터베이스 식별자에 대해서 생각해봐야 한다.(어쨌든 대표 식별자 속성과 자동적으로 생성되는 값을 추천한다는 것을 기억해라) 데이터베이스 식별자 속성은 객체 식별이 되는 유일한 것이며, 기본적으로 하이버네이트에서 사용되는 유일한 방법이다. 물론, 웹 어플리케이션에서 링크를 구축하기 위한 예처럼, 편리하게 읽기 전용으로 다루기 위해 데이버베이스 식별자를 사용하기도 한다.

동치성(equality) 비교를 위해 데이터베이스 식별자를 사용하는 것 대신에, 객체 내에서 정의한 equals()에서 속성들의 집합을 사용할 수도 있다. 예를 들어, “Item" 클래스에 ”name" 문자열, “created" Date 속성을 가지고 있다면, 좋은 equals() 메소드를 구현하기 위해 두 속성 모두 사용하는 것은 좋은 방법이 될 수 있다. 영속 객체 식별자를 사용할 필요도 없어, 이것을 ”비즈니스 키“라 부르며 더 좋은 해결책이 될 것이다. 이것은 natural key이지만, 이것을 사용한다고 잘못되는 것은 아무것도 없다!

두 개의 필드 조합은 Items 클래스를 포함하는 Set 콜렉션의 생명 주기 동안에 사용하기 충분히 안정적이다. 주키를 사용하는 것만큼 좋지는 않지만, 확실한 대안적인 키(방법)가 될 수 있다. 객체-관계형 모델에서 UNIQUE 속성을 갖게 될 키 필드나 적어도 영속 객체에서 불변성을 갖는 필드(“created"-를 위해서 “관계형 동일성”을 정의하는 것으로 해석할 수도 있을 것이다.

위의 예에서 아마도 “username" 속성을 사용하게 될 것이다.

앞으로 나올 것은 대부분의 경우에서 equals()/hashCode()에 대해서 알아야 하는 것들의 모든 것이다. 만약 단지 읽기만 한다면, 완벽히 작동하지 않는 해결책이거나 당신을 도와주기에 충분치 않은 제안을 찾을 수도 있다. 당신 자신의 위험에 따라서 다음 내용들을 적절하게 사용해라.

save/flush를 통한 극복
equals()/hashCode()로 영속성 id를 사용해 극복할 수 없거나, 세션과 세션 간(그러므로 기본적으로 제공되는 equals()/hashCode()를 사용할 수 없다.)에 객체를 유지해야 한다면, 객체를 만든 후에 그리고 객체를 콜렉션에 할당하기 전에 save()/flush()를 강제해서 해결할 수 있다.

// Suppose UserManager and User are Beans mapped with Hibernate

UserManager u = session.load(UserManager.class, id);

User newUser = new User("newUsername1");
// u.getUserSet().add(newUser); // DO NOT ADD TO SET YET!
session.save(newUser);
session.flush();             // The id is now assigned to the new User object
u.getUserSet().add(newUser); // Now OK to add to set.
newUser = new User("newUsername2");
session.save(newUser);
session.flush();
u.getUserSet().add(newUser); // Now userSet contains both users.


이 방법은 매우 비효율적이어서 추천하지 않는다. 또한 씬 클라이언트에서 접속이 끊긴 객체를 사용하기에는 너무 빈약한 방법이다.

// on client, let's assume the UserManager is empty:
UserManager u = userManagerSessionBean.load(UserManager.class, id);
User newUser = new User("newUsername1");
u.getUserSet().add(newUser); // have to add it to set now since client cannot save it
userManagerSessionBean.updateUserManager(u);

// on server:
UserManagerSessionBean updateUserManager (UserManager u) {
  // get the first user (this example assumes there's only one)
  User newUser = (User)u.getUserSet().iterator().next();
  session.saveOrUpdate(u);
  if (!u.getUserSet().contains(newUser)) System.err.println("User set corrupted.");
}

newUser의 해시 코드 값은 saveOrUpdate를 호출할 때 변경될 것이기 때문에, 실제로 “User Set corrupted"를 출력할 것이다.

자바 객체 동일성은 하이버네이트에서 할당된 데이터베이스 동일성과 직접적으로 매핑되는 것처럼 생각되기 때문에 모든 것들을 방해한다. 그렇지만 실제로는 이 두 개의 동일성은 차이가 있는데, 데이터베이스 동일성은 객체가 저장될 때까지는 존재하지도 않는다는 것이다. 객체의 동일성은 그 객체가 저장이 되었는지 안 되었는지에 의존하면 안 된다. 그러나 만약 equals()와 hashCode()가 하이버네이트 동일성을 사용한다면, 객체는 저장이 될 때 변하게 된다.

이 메소드들을 쓰는 것은 성가신데, 하이버네이트가 도와줄 수 없니?
하이버네이트 “도움”의 손은 hbm2java가 제공할 것이다.
hbm2java는 이번 장에서 논의하는 주제에서 벗어나지만 더 이상 id기반의 equals/hashcode를 생성하지 않는다. 적절한 equals/hashcode를 생성하기 해야 한다는 것을  hbm2java에게 알려주기 위해 <meta attribute="use-in-equals">true</meta>로 특정 속성을 표시해 줄 수 있다.

정리
위 내용들을 모두 합쳐서 equals/hashCode를 다루기 위한 여러 방법들이 어떤 것을 할 수 있고, 어떤 것은 못하는지에 대한 리스트를 작성했다.

사용자 삽입 이미지

여러 문제가 나타나고 있는 곳은 다음과 같다.

복합키 사용
복합키를 객체에서 사용하기 위해서는, 어떤 방법으로든 equals/hashCode 메소드를 구현해야 한며, 이 경우에 ==를 사용한 동일성은 충분하지 못 하다.
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));


저장 후 콜렉션이 변하지 않는지
다음 작업이 수행이 되는지 안 되는지 확인 :
HashSet set = new HashSet();
User u = new User();
set.add(u);
session.save(u);
assert(set.contains(u));


equals와 hashCode를 위한 가장 좋은 실례는?

참조문서에 있는 링크와 API에 있는 문서를 읽어봐라. 이 문서들은 더 상세한 내용을 제공한다.
더 나아가 앞으로 나서서 자신들의 “패턴”을 사용한 equals와 hashcode 구현에 대해서 더 좋은 정보와 팁을 모든 사람들과 나누기를 장려한다. 나는 그 정보들을 취합해서 hbm2java에 넣음으로, 조금 더 도움이 되도록 만드는 것을 시도할 수도 있다.

참조 문서
Effective Java Programming Language Guide, sample chapter about equals() and hashCode()
Java theory and practice: Hashing it out, Article from IBM
Sam Pullara (BEA) comments on object identity: Blog comment
Article about how to implement equals and hashCode correctly by Manish Hatwalne: Equals and HashCode
비즈니스 동일성 정의 없이 구현이 가능한지에 대한 토론 쓰레드 : Equals and hashCode

java.lang.Object 문서에 따르면 hashCode()를 0을 리턴하면 항상 ok를 받을 수 있도록 완벽히 구현할 수 있다고 한다. 유일한 객체를 위해서 유일한 숫자를 리턴하는 hashCode()를 리턴하는 이유는 성능 향상에 있다. 단점은 hashCode()의 행동이 equals()와 동일해야만 한다는 것이다. a, b 객체에서, a.equals(b)가 참이면, a.hashCode() == b.hashCode()도 참이 될 것이다. 하지만 a.equals(b)가 거짓일 때도, a.hashCode() == b.hashCode()는 여전히 참이다. '0 반환‘으로 hashCode()를 구현하는 것은 이 기준을 따른 것이지만, 이것은 HashSet이나 HashMap같은 해시 기반의 콜렉션에서 극단적으로 비효율적이다.
크리에이티브 커먼즈 라이선스
Creative Commons License
Trackback 2 Comment 2