4장. 영속 클래스들

영속 클래스들은 비지니스 문제의 엔티티들(예를 들어 E-Commerce 어플리케이션에서 고객이나 주문)을 구현하는 어플리케이션 내의 클래스들이다. 영속 클래스들의 인스턴스들은 영속 상태에 있는 것으로 전혀 간주되지 않는다 - 대신에 하나의 인스턴스는 transient 또는 detached 상태일 수 있다.

Hibernate는 이들 클래스들이 Plain Old Java Object (POJO) 프로그래밍 모형으로서 알려진, 몇몇 간단한 규칙들을 따를 경우에 가장 잘 동작한다. 하지만 이들 규칙들 중 어떤 것도 어려운 사양들이 아니다. 진정 Hibernate3는 당신의 영속 객체들의 특징에 대해 매우 적은 것을 가정한다. 당신은 다른 방법들로 도메인 모형을 표현할 수 있다 : 예를 들어 Map 인스턴스의 트리들을 사용하기.

4.1. 간단한 POJO 예제

대부분의 자바 어플리케이션들은 고양이과들을 표현하는 영속 클래스를 필요로 한다.

package eg;
import java.util.Set;
import java.util.Date;

public class Cat {
    private Long id; // identifier

    private Date birthdate;
    private Color color;
    private char sex;
    private float weight;
    private int litterId;

    private Cat mother;
    private Set kittens = new HashSet();

    private void setId(Long id) {
        this.id=id;
    }
    public Long getId() {
        return id;
    }

    void setBirthdate(Date date) {
        birthdate = date;
    }
    public Date getBirthdate() {
        return birthdate;
    }

    void setWeight(float weight) {
        this.weight = weight;
    }
    public float getWeight() {
        return weight;
    }

    public Color getColor() {
        return color;
    }
    void setColor(Color color) {
        this.color = color;
    }

    void setSex(char sex) {
        this.sex=sex;
    }
    public char getSex() {
        return sex;
    }

    void setLitterId(int id) {
        this.litterId = id;
    }
    public int getLitterId() {
        return litterId;
    }

    void setMother(Cat mother) {
        this.mother = mother;
    }
    public Cat getMother() {
        return mother;
    }
    void setKittens(Set kittens) {
        this.kittens = kittens;
    }
    public Set getKittens() {
        return kittens;
    }
    
    // addKitten not needed by Hibernate
    public void addKitten(Cat kitten) {
    	kitten.setMother(this);
	kitten.setLitterId( kittens.size() ); 
        kittens.add(kitten);
    }
}

준수할 네 개의 주요 규칙들이 다음에 있다:

4.1.1. 아규먼트 없는 생성자를 구현하라

Cat은 아규먼트 없는 생성자를 갖는다. 모든 영속 클래스들은 Hibernate는 Constructor.newInstance()를 사용하여 그것들을 초기화 시킬 수 있도록 디폴트 생성자 (public이 아닐 수 있다)를 가져야 한다. 우리는 Hibernate 내에서 런타임 프락시 생성을 위한 최소한의 패키지 가시성(visibility)를 가진 디폴트 생성자를 가질 것을 강력하게 권장한다.

4.1.2. identifier 프로퍼티를 제공하라(옵션)

Catid로 명명된 하나의 프로퍼티를 갖는다. 이 프로퍼티는 데이터베이스 테이블의 프라이머리 키 컬럼으로 매핑된다. 이 프로퍼티는 어떤 것으로 명명될 수도 있고, 그것의 타입은 임의의 원시 타입, 원시 "wrapper" 타입, java.lang.String 또는 java.util.Date일 수 있다. (만일 당신의 리거시 데이터베이스 테이블이 composite 키들을 갖고 있다면, 당신은 이들 타입들을 가진 사용자 정의 클래스를 사용할 수도 있다 - 나중에 composite 식별자들에 대한 절을 보라)

identifier 프로퍼티는 엄격하게 옵션이다. 당신은 그것을 생략할 수도 있고, Hibernate로 하여금 내부적으로 객체 식별자들을 추적하도록 할 수 있다. 하지만 우리는 이것을 권장하지 않는다.

사실, 어떤 기능은 identifier 프로퍼티를 선언하는 클래스들에 대해서만 이용 가능하다:

우리는 당신이 영속 클래스들에 대해 일관되게 명명된 identifier 프로퍼티들을 선언할 것을 권장한다. 게다가 우리는 당신이 nullable 타입(예를 들어 non-primitive)을 사용할 것을 권장한다.

4.1.3. final이 아닌 클래스들을 선호하라(옵션)

Hibernate의 중심 특징인, 프락시(proxies)들은 final이 아닌 영속 클래스들 또는 모두 public 메소드들로 선언된 인터페이스의 구현인 영속 클래스들에 의존한다.

당신은 Hibernate로 인터페이스를 구현하지 않은 final 클래스들을 영속화 시킬 수 있지만 당신은 lazy 연관 페칭(lazy association fetching)에 대해 프락시들을 사용할 수 없을 것이다 -그것은 퍼포먼스 튜닝을 위한 당신의 옵션들을 제한시킬 것이다.

당신은 또한 non-final 클래스들 상에 public final 메소드들을 선언하는 것을 피해야 한다. 만일 당신이 public final 메소드를 가진 클래스를 사용하고자 원할 경우, 당신은 lazy="false"를 설정함으로써 명시적으로 프락싱을 사용 불가능하도록 해야 한다.

4.1.4. 영속 필드들을 위한 accessor들과 mutator들을 선언하라(옵션)

Cat은 그것의 모든 영속 필드들에 대해 accessor 메소드들을 선언한다. 많은 다른 ORM 도구들은 인스턴스 변수들을 직접 영속화 시킨다. 우리는 관계형 스키마와 클래스의 내부적인 데이터 구조들 사이에 간접적인 수단을 제공하는 것이 더 좋다고 믿고 있다. 디폴트로 Hibernate는 자바빈즈 스타일 프로퍼티들을 영속화 시키고, getFoo, isFoosetFoo 형식의 메소드 이름들을 인지한다. 당신은 진정으로 특정 프로퍼티에 대한 직접적인 필드 접근으로 전환할 수도 있다.

프로퍼티들은 public으로 선언될 필요가 없다 - Hibernate는 디폴트로 protected get/set 쌍 또는 private get/set 쌍을 가진 프로퍼티를 영속화 시킬 수 있다.

4.2. 상속 구현하기

서브클래스는 또한 첫 번째 규칙들과 두 번째 규칙들을 주시해야 한다. 그것은 슈퍼클래스 Cat으로부터 그것의 identifier 프로퍼티를 상속받는다.

package eg;

public class DomesticCat extends Cat {
        private String name;

        public String getName() {
                return name;
        }
        protected void setName(String name) {
                this.name=name;
        }
}

4.3. equals()hashCode() 구현하기

만일 당신이

  • 하나의 Set 속에 영속 클래스들의 인스턴스들을 집어넣고자 의도하고 (many-valued 연관들에 대해 권장되는 방법) 그리고

  • detached 인스턴스들의 reattachment(재첨부)를 사용하고자 의도하는

경우에 당신은 equals()hashCode() 메소드들을 오버라이드 시켜야 한다.

Hibernate는 특정 session 범위 내에서만 persistent identity(데이터베이스 행)과 Java identity의 같음을 보장한다. 따라서 우리가 다른 세션들에서 검색된 인스턴스들을 혼합시키자마자, 우리가 Set들에 대해 유의미하게 만들고자 원할 경우, 우리는 equals()hashCode()를 구현해야 한다.

가장 명백한 방법은 두 객체들의 identifier 값을 비교함으로써 equals()/hashCode()를 구현하는 것이다. 만일 그 값이 동일하다면, 둘다 동일한 데이터베이스 행이어야 하고, 그러므로 그것들은 같다(둘다 하나의 Set에 추가되는 경우에, 우리는 Set 속에서 하나의 요소만을 갖게 될 것이다). 불행하게도, 우리는 생성되는 식별자들을 갖는 그 접근법을 사용할 수 없다! Hibernate는 오직 식별자 값들을 영속화 되는 객체들에 할당할 것이고, 새로이 생성된 인스턴스는 임의의 identifier 값을 갖지 않을 것이다! 만일 인스턴스가 저장되지 않고 현재 하나의 Set 속에 있을 경우에, 그것을 저장하는것은 하나의 식별자 값을 그 객체에게 할당할 것이다. 만일 equals()hashCode()가 그 식별자 값에 기초할 경우, hash 코드는 Set의 계약을 파기하여 변경될 것이다. 이 문제에 대한 전체 논의에 대해서는 Hibernate 웹 사이트를 보라. 이것은 Hibernate 쟁점이 아닌, 객체 identity와 equality에 관한 통상의 자바 의미론임을 노트하라.

우리는 Business key equality를 사용하여 equals()hashCode()를 구현할 것 권장한다. Business key equality는 equals() 메소드가 비지니스 키, 즉 실세계에서 우리의 인스턴스를 식별하게 될 키(natural 후보 키)를 형성하는 프로퍼티들만을 비교한다는 점을 의미한다 :

public class Cat {

    ...
    public boolean equals(Object other) {
        if (this == other) return true;
        if ( !(other instanceof Cat) ) return false;

        final Cat cat = (Cat) other;

        if ( !cat.getLitterId().equals( getLitterId() ) ) return false;
        if ( !cat.getMother().equals( getMother() ) ) return false;

        return true;
    }

    public int hashCode() {
        int result;
        result = getMother().hashCode();
        result = 29 * result + getLitterId();
        return result;
    }

}

하나의 비지니스 키는 데이터베이스 프라이머리 키 후보 만큼 견고하지 않아야 한다(11.1.3절. “객체 identity 고려하기”를 보라). 대개 변경할 수 없는 프로퍼티 또는 유일한(unique) 프로퍼티는 대개 비지니스 키에 대한 좋은 후보들이다.

4.4. 동적인 모형들

다음 특징들은 현재 실험적으로 고려되고 있으며 장래에는 변경될 수 있음을 노트하라.

영속 엔티티들은 반드시 실행시에 POJO 클래스들로 또는 자바빈즈 객체들로 표현되어야할 필요는 없다. Hibernate는 또한 (실행 시에 Map들을 가진 Map들을 사용하여) 동적인 모형들을 지원하고 DOM4J 트리들로서 엔티티들에 대한 표현을 지원한다. 이 접근법으로, 당신은 영속 클래스들을 작성하지 않고, 오직 매핑 파일들 만을 작성한다.

디폴트로, Hibernate는 통산의 POJO 모드로 동작한다. 당신은 default_entity_mode 구성 옵션을 사용하여 특별한 SessionFactory에 대해 디폴트 엔티티 표현 모드를 설정할 수 있다 (표 3.3. “Hibernate 구성 프로퍼티들”을 보라).

다음 예제들은Map들을 사용하는 표현을 설명한다. 먼저 매핑 파일에서, entity-name은 클래스 이름 대신에(또는 클래스 이름에 덧붙여) 선언되어야 한다:

<hibernate-mapping>

    <class entity-name="Customer">

        <id name="id"
            type="long"
            column="ID">
            <generator class="sequence"/>
        </id>

        <property name="name"
            column="NAME"
            type="string"/>

        <property name="address"
            column="ADDRESS"
            type="string"/>

        <many-to-one name="organization"
            column="ORGANIZATION_ID"
            class="Organization"/>

        <bag name="orders"
            inverse="true"
            lazy="false"
            cascade="all">
            <key column="CUSTOMER_ID"/>
            <one-to-many class="Order"/>
        </bag>

    </class>
    
</hibernate-mapping>

심지어 비록 연관들이 대상(target) 클래스 이름들을 사용하여 선언될지라도, 연관들의 대상(target) 타입은 또한 POJO가 아닌 동적인 엔티티일 수 있음을 노트하라.

SessionFactory에 대한 디폴트 엔티티 모드를 dynamic-map으로 설정한 후에, 우리는 Map들을 가진 Map들에 대해 실행 시에 작업할 수 있다:

Session s = openSession();
Transaction tx = s.beginTransaction();
Session s = openSession();

// Create a customer
Map david = new HashMap();
david.put("name", "David");

// Create an organization
Map foobar = new HashMap();
foobar.put("name", "Foobar Inc.");

// Link both
david.put("organization", foobar);

// Save both
s.save("Customer", david);
s.save("Organization", foobar);

tx.commit();
s.close();

dynamic 매핑의 장점들은 엔티티 클래스 구현에 대한 필요 없이도 프로토타이핑을 위한 빠른 전환 시간이다. 하지만 당신은 컴파일 시 타입 체킹을 잃고 실행 시에 많은 예외상황들을 다루게 될 것이다. Hibernate 매핑 덕분에, 나중에 고유한 도메인 모형 구현을 상단에 추가하는 것이 허용되어서, 데이터베이스 스키마가 쉽게 정규화 되고 소리가 울려 퍼질 수 있다.

엔티티 표현 모드들은 또한 하나의 단위 Session 기준에 대해 설정될 수 있다:

Session dynamicSession = pojoSession.getSession(EntityMode.MAP);

// Create a customer
Map david = new HashMap();
david.put("name", "David");
dynamicSession.save("Customer", david);
...
dynamicSession.flush();
dynamicSession.close()
...
// Continue on pojoSession

EntityMode를 사용하는 getSession()에 대한 호출은 SessionFactory가 아닌, Session API에 대한 것임을 노트하길 바란다. 그 방법으로, 새로운 Session은 기본 JDBC 커넥션, 트랜잭션, 그리고 다른 컨텍스트 정보를 공유한다. 이것은 당신이 두 번째 Session 상에서 flush()close()를 호출하지 말아야 하고, 또한 트랜잭션 및 커넥션 핸들링을 주된 작업 단위에게 맡긴다는 점을 의미한다.

XML 표현 가용성들에 대한 추가 정보는 18장. XML 매핑에서 찾을 수 있다.

4.5. Tuplizer들

org.hibernate.tuple.Tuplizer, 그리고 그것의 서브-인터페이스들은 데이터의 조각에 대한 특별한 표현의 org.hibernate.EntityMode가 주어지면 그 표현을 관리하는 책임이 있다. 만일 주어진 데이터 조각이 하나의 데이터 구조로 간주될 경우, 그때 하나의 tuplizer는 그런 데이터 구조를 생성시키는 방법과 그런 데이터 구조로부터 값들을 추출시키는 방법 그리고 그런 데이터구조 속으로 값들을 삽입시키는 방법을 알고 있는 것이다. 예를 들어, POJO 엔티티 모드의 경우, 대응하는 tuplizer는 그것의 생성자를 통해 POJO를 생성시키는 방법, 그리고 정의된 프로퍼티 접근자들을 사용하여 POJO 프로퍼티들에 접근하는 방법을 안다. org.hibernate.tuple.EntityTuplizer 인터페이스와 org.hibernate.tuple.ComponentTuplizer 인터페이스에 의해 표현되는 두 가지 고급 유형의 Tuplizer들이 존재한다. EntityTuplizer들은 엔티티들에 관해서는 위에 언급된 계약들을 매핑할 책임이 있는 반면에, ComponentTuplizer들은 컴포넌트들에 대해서도 동일한 것을 행한다.

사용자들은 또한 그들 자신의 tuplizer들을 플러그 시킬 수 있다. 아마 당신은 dynamic-map entity-mode 동안에 사용되는 java.util.HashMap 대신에 하나의 java.util.Map 구현을 필요로 한다; 또는 아마 당신은 디폴트로 사용되는 방도 보다는 하나의 다른 다른 프릭시 산출 방도를 필요로 한다. 둘다 하나의 맞춤형 tuplizer를 정의함으로써 성취될 것이다. Tuplizer들 정의들은 그것들이 관리할 수단인 엔티티 매핑 또는 컴포넌트 매핑에 첨부된다. 우리의 고객 엔티티에 대한 예제로 되돌아가면:

<hibernate-mapping>
    <class entity-name="Customer">
        <!--
            Override the dynamic-map entity-mode
            tuplizer for the customer entity
        -->
        <tuplizer entity-mode="dynamic-map"
                class="CustomMapTuplizerImpl"/>

        <id name="id" type="long" column="ID">
            <generator class="sequence"/>
        </id>

        <!-- other properties -->
        ...
    </class>
</hibernate-mapping>


public class CustomMapTuplizerImpl
        extends org.hibernate.tuple.DynamicMapEntityTuplizer {
    // override the buildInstantiator() method to plug in our custom map...
    protected final Instantiator buildInstantiator(
            org.hibernate.mapping.PersistentClass mappingInfo) {
        return new CustomMapInstantiator( mappingInfo );
    }

    private static final class CustomMapInstantiator
            extends org.hibernate.tuple.DynamicMapInstantitor {
        // override the generateMap() method to return our custom map...
        protected final Map generateMap() {
            return new CustomMap();
        }
    }
}

TODO: property 패키지와 proxy 패키지 내에 user-extension 프레임웍을 문서화 할 것.