public class Person { private Long id; private Integer version; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Integer getVersion() { return version; } public void setVersion(Integer version) { this.version = version; } // person-specific properties and behavior }이 예제에서, 우리는 id 필드와 version 필드를 모두 가지고 있는 가장 좋은 사례를 따랐다. id는 데이터베이스에서 주키로서 사용되는 값을 가지고 있고, version은 0으로 시작되며, 객체가 업데이트 될 때마다 증가하게 된다.(version은 동시 업데이트 문제를 피하는데 도움을 준다.) 더 명확하게 이해하기 위해, 이 객체를 데이터베이스에 저장해 영속성을 부여하기 위한 하이버네이트 매핑 파일을 보자.
하이버네이트 매핑 파일은 Person의 id 필드가 데이터베이스의 ID인 것을 가리키고 있다.(즉, PERSON 테이블에 주키) id 태그 내에 있는 속성인 unsaved-value="null" 은 하이버네이트가 Person 객체가 이전에 저장되었는지 아닌지를 결정하기 위해서 id 필드를 사용한다는 것을 알려준다. ORM 프레임워크는 반드시 SQL INSERT나 UPDATE을 이용해 객체가 저장되었는지를 알기 위해서 구별할 수 있도록 만들어야 한다. 이 경우 하이버네이트는 새로 생성된 객체를 null로 할당해서 시작하고, 처음 저장할 때 id 값이 할당 된다. 또한 generator 태그는 하이버네이트가 처음 저장되는 객체에 할당할 id를 어디서 받아오는지를 의미한다. 이 경우, 하이버네이트는 유일한 ID들의 출처로 데이터베이스 시퀀스를 사용하고 있다. 마지막으로 version 태그는 하이버네이트가 Person 객체의 version 필드를 동시성 운영을 위해서 사용한다는 것을 의미한다. 하이버네이트는 객체의 version 숫자를 데이터베이스의 version 숫자와 체크함으로써 optimistic locking scheme를 강화할 것이다.PERSON_SEQ
public boolean equals(Object o) { if (this == o) return true; if (o == null || !(o instanceof Person)) return false; Person other = (Person)o; if (id == other.getId()) return true; if (id == null) return false; // equivalence by id return id.equals(other.getId()); } public int hashCode() { if (id != null) { return id.hashCode(); } else { return super.hashCode(); } }불행하게도, 이 구현에는 문제가 있다. 처음으로 Person 객체를 생성해 id 필드가 null일 경우, 아직 이 객체가 저장되지 않았다면 어느 두 개의 객체라도 동일하게 취급될 것임을 의미한다. 우리가 Person 객체를 만들고 Set 콜렉션에 이 객체를 넣고, 완벽하게 다른 새로운 Person 객체를 만들고 같은 Set에 다시 넣으면, 두 번째 Person 객체는 Set에 추가될 수 없다. Set은 모든 저장되지 않은 객체를 같은 것으로 결정하기 때문이다.
public boolean equals(Object o) { if (this == o) return true; if (o == null || !(o instanceof Person)) return false; Person other = (Person)o; // unsaved objects are never equal if (id == null || other.getId() == null) return false; return id.equals(other.getId()); }여기서는 문제가 숨겨졌다. 자바 콜렉션 프레임워크는 콜렉션의 생명 주기 동안 불변하는 필드에 기반을 둔 equals()와 hashCode()를 필요로 한다. 다시 말하자면, 객체가 콜렉션 내에 있는 동안에 equals()와 hashCode()의 값이 변할 수 없다는 것이다. 예를 들어, 다음 프로그램을 보면 :
Person p = new Person(); Set set = new HashSet(); set.add(p); System.out.println(set.contains(p)); p.setId(new Long(5)); System.out.println(set.contains(p)); 결과 : true falseset.contains(p)의 두 번째 호출은 Set에 더 이상 p를 찾을 수 없기 때문에 false를 리턴 한다. Set은 우리의 객체를 완전히 잃어버린 것이다! 이것은 객체가 Set 내에 있는 동안에 hashCode()에서 반환되는 값이 변경되었기 때문이다.
public class Person { // assign an id as soon as possible private String id = IdGenerator.createId(); private Integer version; public String getId() { return id; } public void setId(String id) { this.id = id; } public Integer getVersion() { return version; } public void setVersion(Integer version) { this.version = version; } // Person-specific fields and behavior here public boolean equals(Object o) { if (this == o) return true; if (o == null || !(o instanceof Person)) return false; Person other = (Person)o; if (id == null) return false; return id.equals(other.getId()); } public int hashCode() { if (id != null) { return id.hashCode(); } else { return super.hashCode(); } } }이 예는 equals()의 정의와 hashCode()를 얻기 위해서 객체 id를 사용한다. 매우 간단하다. 그렇지만 이것을 만들기 위해서는 우리가 필요한 것이 두 가지 있다. 첫 번째는 모든 객체가 저장되기 전에도 id를 가지고 있다는 것을 보증할 방법이 필요하다. 이 예에서는 id 변수를 선언하자마자 id 값을 할당하고 있다. 두 번째는 이 객체가 새로 생성된 객체인지, 혹은 이전에 저장되었던 객체인지를 결정할 방법이 필요하다. 우리의 최초 예에서는 하이버네이트가 객체가 생성 될지 안 될지 결정하기 위해 id 필드 값의 null 여부를 확인했다. 분명한 것은 이제 우리의 객체 id는 더 이상 null이 될 수가 없다. 우리는 id 필드 보다는 version 필드가 null인지 확인하도록 하이버네이트를 설정함으로서 쉽게 해결할 수 있다. version 필드는 이 객체가 이전에 저장되었었는지를 가리키는데 더 적절하다.
id에 있는 generator 태그는 class="assigend" 속성을 가지고 있어야 함을 기억해라. 이것은 하이버네이트에게 우리가 데이터베이스에 의해 id 값이 할당되게 하는 것이 아니라, 우리 코드 내에서 id 값을 할당 할 것이라 말해준다. 하이버네이트는 저장되지 않은 객체라도 id 값이 있기를 당연히 기대한다. 또한 추가했다. version 태그에 새로운 속성인 unsaved-value="null"을 추가했다. 이것은 하이버네이트에게 객체가 새로 생성 되었는지를 식별하기 위해 null version(null id가 아닌)을 찾으라고 말해준다. Integer값 대신에 version 필드를 위해 int를 사용하는 것이 더 유용하며, 저장되지 않은지를 알기 위한 척도로서 음의 값을 찾으라고 쉽게 말할 수 있다.
public interface PersistentObject { public String getId(); public void setId(String id); public Integer getVersion(); public void setVersion(Integer version); } public abstract class AbstractPersistentObject implements PersistentObject { private String id = IdGenerator.createId(); private Integer version; public String getId() { return id; } public void setId(String id) { this.id = id; } public Integer getVersion() { return version; } public void setVersion(Integer version) { this.version = version; } public boolean equals(Object o) { if (this == o) return true; if (o == null || !(o instanceof PersistentObject)) { return false; } PersistentObject other = (PersistentObject)o; // if the id is missing, return false if (id == null) return false; // equivalence by id return id.equals(other.getId()); } public int hashCode() { if (id != null) { return id.hashCode(); } else { return super.hashCode(); } } public String toString() { return this.getClass().getName() + "[id=" + id + "]"; } }우리는 지금 도메인 객체를 생성하기 위한 단순하고 효과적인 방법을 가지고 있다. AbstractPersistentObject를 상속하면, 처음으로 생성될 때 id 값을 자동으로 주고, equals()와 hashCode()도 적절하게 구현할 수 있다. 또한 선택적인 오버라이드를 사용해서 적당한 toString()의 구현을 얻을 수 있다. 만약 테스트 객체, 혹은 연습을 위한 쿼리에 해당하는 연습용 객체라면, id는 변경되지 않을 수 있고, null로 값이 들어 갈 수 있다. 다른 방법으로는 그것을 변경하지 않게 하는 것이다. 만약 도메인 객체를 만들기 위해서 다른 클래스를 상속해야 한다면, 추상 클래스를 상속하는 것 보다는 PersistentObject 인터페이스를 구현할 수 있다.
이제 우리의 Person 클래스는 매우 단순해졌다 : public class Person extends AbstractPersistentObject { // Person-specific fields and behavior here }하이버네이트 매핑 문서는 이 예에서 변경되지 않는다. 하이버네이트에게 추상 상위 클래스에 대해서 말해 성가시게 하지 않는 대신, 모든 PersistentObject 매핑 파일이 id("assigned" generator 태그를 포함하고 있는)와 unsaved-value="null"로 설정된 version 태그가 포함되어 있는지 확인할 것이다. 눈치 빠른 독자는 id가 영속 객체가 생성될 때마다 할당됨을 알아차렸을 것이다. 이것은 하이버네이트가 데이터베이스에서 이미 존재하는 객체를 읽어서 이 객체의 인스턴스를 메모리에 생성할 때마다 새로운 id를 얻는다는 것을 의미한다. 괜찮다. 하이버네이트는 이어서 객체에 저장되어 있는 id를 새로 할당된 id로 교체하기 위해서 setId()를 호출할 것이다. 이 추가적인 id 생성은 id 생성알고리즘이 비용이 적게 들어가는(즉, 데이터베이스와의 접촉이 필요 없다는 뜻) 동안에는 문제가 되지 않는다.
2cdb8cee-9134-453f-9d7a-14c0ae8184c6이 문자는 숫자의 위치 차이는 ‘-’로 구분해서 바이트를 16진법으로 간단하게 표현한다. 이 포맷은 작업하기 매우 간단하고 쉽지만, 이것의 길이는 36개의 문자열이다. ‘-’는 항상 같은 위치에 위치하며, 32개 문자의 크기를 줄이기 위해서 제거 될 수 있다. 조금 더 정확한 표현을 위해서 byte[16] 배열로 표현되거나, 두 개의 8byte 길이로 표현된 숫자를 갖는 객체를 생성 할 수 있다. 만약 Java 1.5나 그 이후 버전을 사용한다면, 메모리 포맷에 최적화된 방법은 아니지만, UUID 클래스를 직접 사용할 수 있다. 더 많은 정보를 찾길 원하며, 위키피디아 UUID 목록(http://en.wikipedia.org/wiki/UUID)을 보거나 UUID 클래스를 위한 자바 문서 목록(http://java.sun.com/j2se/1.5.0/docs/api/java/util/UUID.html)을 봐라. UUID 생성 알고리즘 구현에는 몇 가지 방법이 있다. 최종적인 UUID는 표준 포맷이기 때문에, 우리만의 IdGenerator 클래스를 사용한다고 문제가 되지는 않는다. 어떤 알고리즘이든지 각 id는 유일한 값이 보장되기 때문에, 우리는 언제나 구현한 것을 변경하거나 조합하거나 다른 구현들과 맞춰볼 수도 있다. 자바 1.5나 그 이후 버전을 사용한다면, 매우 편리한 구현인 java.util.UUID가 있다.
public class IdGenerator { public static String createId() { UUID uuid = java.util.UUID.randomUUID(); return uuid.toString(); } }자바 1.5 이상을 사용하지 않는 경우에, UUID를 구현한, 자바의 이전 버전에 적합한 두 개의 외부 라이브러리인 Apacke Commons ID(http://jakarta.apache.org/commons/sandbox/id/uuid.html) project와 Java UUID Generator(JUG - http://jug.safehaus.org/)가 있다. 둘 다 아파치 라인센스로 사용할 수 있다.
import org.safehaus.uuid.UUIDGenerator; public class IdGenerator { public static final UUIDGenerator uuidGen = UUIDGenerator.getInstance(); public static String createId() { UUID uuid = uuidGen.generateRandomBasedUUID(); return uuid.toString(); } }UUID 생성 알고리즘에 대해서 하이버네이트는 어떤 구조를 구성했을까? 객체 동일성을 위해서 UUID를 얻는 방법이 적당해보이는가? 객체 동일성을 객체의 영속성과 독립시키기를 원치 않는다. 하이버네이트가 당신을 위해 UUID를 생성하는 옵션을 가지고 있다면, 이는 객체가 생성 시기에 ID를 받지 못하고, 저장될 때까지 기다려야 하는 이전과 동일한 문제로 돌아가게 된다. 데이터베이스 주키로 UUID를 사용하는 가장 큰 단점은 인덱스들과 외래키의 증가를 더욱 악화시키는데 있다. 여기에 상충 관계(trade-off)가 있다. 문자열로 표현되는 데이터베이스 주키는 32 혹은 36 바이트다. 이 숫자는 또한 바이트로 바로 저장이 될 수 있고, 반으로 잘린 크기로 저장될 수도 있지만, 이는 데이터베이스에 쿼리를 수행 할 때 이해를 더 어렵게 한다. 이것은 당신의 프로젝트에서 요구사항에 맞추어서 실행하면 된다.
이전 글 : 윈도우즈 비스타에서 꼭 알아야 하는 6가지 팁
다음 글 : 자유소프트웨어와 오픈소스 윈도우 개발도구들의 사례
최신 콘텐츠