1장. Hibernate 개요

1.1. 머리말

이 장은 Hibernate 초심자를 위한 개론적인 튜토리얼이다. 우리는 메모리-내 데이터베이스를 사용하는 간단한 명령 라인 어플리케이션으로 시작하고 단계들을 이해하도록 쉽게 그것을 개발한다.

이 튜토리얼은 Hibernate 신규 사용자들을 의도하고 있지만 Java와 SQL 지식을 필요로 한다. 그것은 Michael Gloegl이 작성한 튜토리얼에 기초하며, 우리가 명명하는 제 3의 라이브러리들은 JDK 1.4와 5.0 버전용이다. 당신은 JDK1.3에 대해 다른 라이브러리들을 필요로 할 수도 있다.

튜토리얼용 소스는 doc/reference/tutorial/ 디렉토리 내에 있는 배포본 내에 포함되어 있다.

1.2. 파트 1 - 첫 번째 Hibernate 어플리케이션

먼저, 우리는 한 개의 간단한 콘솔-기반 Hibernate 어플리케이션을 생성시킬 것이다. 우리는 메모리-내 데이터베이스(HSQL DB)를 사용하므로, 우리는 어떤 데이터베이스 서버를 설치하지 않아도 된다.

우리가 우리가 수반하고자 원하는 이벤트들을 저장할 수 있는 작은 데이터베이스 어플리케이션과 이들 이벤트들의 호스트들에 대한 정보를 필요로 한다고 가정하자.

우리가 행할 첫 번째 것은 우리의 개발 디렉토리를 설정하고, 우리가 필요로 하는 모든 Java 라이브러리들을 그것 속에 집어 넣는 것이다. Hibernate 웹 사이트로부터 Hibernate 배포본을 내려 받아라. 패키지를 추출해내고 /lib 속에서 발견되는 모든 필요한 라이브러리들을 당신의 새로운 개발 작업 디렉토리의 /lib 디렉토리 속에 위치지워라. 그것은 다음과 같을 것이다:

.
+lib
  antlr.jar
  cglib.jar
  asm.jar
  asm-attrs.jars
  commons-collections.jar
  commons-logging.jar
  hibernate3.jar
  jta.jar
  dom4j.jar
  log4j.jar 

이것은 글의 작성 시점에서 Hibernate에 필수적인 최소한의 세트이다(우리는 또한 메인 아카이브인 hibernate3.jar를 복사했음을 노트하라). 당신이 사용 중인 Hibernate 배포본이 더 많거나 보다 적은 라이브러리들을 필요로 할 수도 있다. 필수 라이브러리들과 선택적인 제3의 라이브러리들에 대한 추가 정보는 Hibernate 배포본의 lib/ 디렉토리 내에 있는 README.txt 파일을 보라. (실제로, Log4j는 필수는 아니지만 많은 개발자들에 의해 선호된다.)

다음으로 우리는 우리가 데이터베이스 속에 저장시키고자 원하는 이벤트를 표현하는 한 개의 클래스를 생성시킨다.

1.2.1. 첫 번째 클래스

우리의 첫 번째 영속 클래스는 몇몇 프로퍼티들을 가진 간단한 자바빈즈 클래스이다:

package events;

import java.util.Date;

public class Event {
    private Long id;

    private String title;
    private Date date;

    public Event() {}

    public Long getId() {
        return id;
    }

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

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

당신은 이 클래스가 프로퍼티 getter와 setter 메소드들에 대한 표준 자바빈즈 명명법들 뿐만 아니라 필드들에 대한 private 가시성을 사용하고 있음을 알 수 있다. 이것은 권장되는 설계이지만, 필수적이지는 않다. Hibernate는 또한 필드들에 직접 접근할 수 있으며, accessor 메소드들의 이점은 강건한 리팩토링이다. 아규먼트 없는 생성자는 reflection을 통해 이 클래스의 객체를 초기화 시킬 필요가 있다.

id 프로퍼티는 특별한 이벤트를 위한 유일 식별자를 소유한다. 모든 영속 엔티티 클래스들 (보다 덜 중요한 종속 클래스들도 존재한다)은 우리가 Hibernate의 전체 특징 집합을 사용하고자 원할 경우에 그런 식별자 프로퍼티를 필요로 할 것이다. 사실 대부분의 어플리케이션들(특히 웹 어플리케이션들)은 식별자에 의해 객체들을 구분지을 필요가 있어서, 당신은 이것을 어떤 제약점이라기 보다는 하나의 특징으로 간주할 것이다. 하지만 우리는 대개 객체의 항등(identity)를 처리하지 않으므로, setter 메소드는 private이어야 한다. 객체가 저장될 때, Hibernate는 단지 식별자들을 할당할 것이다. 당신은 Hibernate가 public, private, protected 접근자 메소드들 뿐만 아니라 (public, private, protected) 필드들에도 직접 접근할 수 있음을 알 수 있다. 선택은 당신에게 달려 있으며, 당신은 당신의 어플리케이션 설계에 적합하도록 그것을 부합시킬 수 있다.

아규먼트 없는 생성자는 모든 영속 클래스들에 대한 필요조건이다; Hibernate는 당신을 위해 Java Reflection을 사용하여 객체들을 생성시켜야 한다. 하지만 생성자는 private 일 수 있고, 패키지 가시성은 런타임 프락시 생성과 바이트코드 방편 없는 효율적인 데이터 검색에 필요하다.

이 Java 소스 파일을 개발 폴더 내의 src로 명명된 디렉토리 속에 있는 위치지워라. 이제 그 디렉토리는 다음과 같을 것이다:

.
+lib
  <Hibernate and third-party libraries>
+src
  +events
    Event.java

다음 단계에서, 우리는 Hiberante에게 이 영속 클래스에 대해 알려 준다.

1.2.2. The mapping file

Hibernate는 영속 크래스들에 대한 객체들을 로드시키고 저장시키는 방법을 알 필요가 있다. 이곳은 Hibernate 매핑 파일이 역할을 행하는 곳이다. 매핑 파일은 Hibernate가 접근해야 하는 데이터베이스 내의 테이블이 무엇인지, 그리고 그것이 사용해야 하는 그 테이블 내의 컬럼들이 무엇인지를 Hibernate에게 알려준다.

매핑 파일의 기본 구조는 다음과 같다:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
[...]
</hibernate-mapping>

Hibernate DTD는 매우 정교하다. 당신은 당신의 편집기 또는 IDE 내에서 XML 매핑 요소들과 속성들에 대한 자동 완성 기능을 위해 그것을 사용할 수 있다. 당신은 또한 당신의 텍스트 편집기 내에 DTD 파일을 열 수 있을 것이다 - 그것은 모든 요소들과 속성들에 대한 전체상을 얻고 디폴트들 뿐만 아니라 몇몇 주석들을 보는 가장 손쉬운 방법이다. Hibernate는 웹으로부터 DTD 파일을 로드시키지 않지만, 먼저 어플리케이션의 classpath 경로로부터 그것을 먼저 룩업할 것임을 노트하라. DTD 파일은 hibernate3.jar 속에 포함되어 있을 뿐만 아니라 Hibernate 배포본의 src/ 디렉토리 속에 포함되어 있다.

우리는 코드를 간략화 시키기 위해 장래의 예제에서 DTD 선언을 생략할 것이다. 그것은 물론 옵션이 아니다.

두 개의 hibernate-mapping 태그들 사이에 class 요소를 포함시켜라. 모든 영속 엔티티 클래스들(다시금 종속 클래스들일 수 있고, 그것은 첫번째-급의 엔티티들이 아니다)은 SQL 데이터베이스 내의 테이블에 대한 그런 매핑을 필요로 한다:

<hibernate-mapping>

    <class name="events.Event" table="EVENTS">

    </class>

</hibernate-mapping>

지금까지 우리는 그 테이블 내에 있는 한 행에 의해 표현된 각각의 인스턴스인, 클래스의 객체를 영속화 시키고 로드시키는 방법을 Hibernate에게 알려주었다. 이제 우린느 테이블 프라이머리 키에 대한 유일 식별자 프로퍼티 매핑을 계속 행한다. 게다가 우리는 이 식별자를 처리하는 것에 주의를 기울이고자 원하지 않으므로, 우리는 대용 키 프라이머리 키 컬럼에 대한 Hibernate의 식별자 생성 방도를 구성한다:

<hibernate-mapping>

    <class name="events.Event" table="EVENTS">
        <id name="id" column="EVENT_ID">
            <generator class="native"/>
        </id>
    </class>

</hibernate-mapping>

id 요소는 식별자 프로퍼티의 선언이고, name="id"는 Java 프로퍼티의 이름을 선언한다 - Hibernate는 그 프로퍼티에 접근하는데 getter 및 setter 메소드들을 사용할 것이다. column 속성은 우리가 EVENTS 테이블의 어느 컬럼을 이 프라이머리 키로 사용하는지를 Hibernate에게 알려준다. 내포된 generator 요소는 식별자 생성 방도를 지정하며, 이 경우에 우리는 increment를 사용했고, 그것은 대개 테스팅(과 튜토리얼들)에 유용한 매우 간단한 메모리-내 숫자 증가 방법이다. Hibernate는 또한 전역적으로 유일한 데이터베이스에 의해 생성된 식별자 뿐만 아니라 어플리케이션에 의해 할당된 식별자(또는 당신이 확장으로 작성한 어떤 방도)를 지원한다.

마지막으로 우리는 매핑 파일 속에서 클래스의 영속 프로퍼티들에 대한 선언들을 포함한다. 디폴트로, 클래스의 프로퍼티들은 영속적인 것으로 간주되지 않는다:

<hibernate-mapping>

    <class name="events.Event" table="EVENTS">
        <id name="id" column="EVENT_ID">
            <generator class="native"/>
        </id>
        <property name="date" type="timestamp" column="EVENT_DATE"/>
        <property name="title"/>
    </class>

</hibernate-mapping>

id 요소의 경우처럼, property 요소의 name 속성은 사용할 getter 및 setter 메소드들이 어느 것인지를 Hibernate에게 알려준다. 따라서 이 경우에 Hibernate는 getDate()/setDate() 뿐만 아니라 getTitle()/setTitle()을 찾게 될 것이다.

date 프로퍼티 매핑은 column 속성을 포함하는데, 왜 titlecolumn 속성을 포함하지 않는가? column 속성이 없을 경우 Hibernate는 디폴트로 컬럼 이름으로서 프로퍼티 이름을 사용한다. 이것은 에 대해 잘 동작한다. 하지만 date는 대부분의 데이터베이스에서 예약된 키워드이어서, 우리는 그것을 다른 이름으로 더 좋게 매핑 시킨다.

다음 흥미로운 점은 title 매핑 또한 type 속성을 갖지 않는다. 우리가 매핑파일들 속에서 선언하고 사용하는 타입들은 당신이 예상하는 Java 데이터 타입들이 아니다. 그것들은 또한 SQL 데이터베이스 타입들도 아니다. 이들 타입들은 이른바 Hibernate 매핑 타입들, 즉 Java 타입들로부터 SQL 타입들로 변환될 수 있고 반대로 SQL 타입들로부터 Java 타입들로 매핑될 수 있는 컨버터들이다. 다시말해, type 속성이 매핑 속에 존재하지 않을 경우 Hibernate는 정확환 변환 및 매핑 타입 그 자체를 결정하려고 시도할 것이다. 몇몇 경우들에서 (Java 클래스에 대한 Reflection을 사용하는) 이 자동적인 검출은 당신이 예상하거나 필요로 하는 디폴트를 갖지 않을 수도 있다. 이것은 date 프로퍼티를 가진 경우이다. Hibernate는 그 프로퍼티가 SQL date 컬럼, timestamp 컬럼 또는 time 컬럼 중 어느 것으로 매핑되어야 하는지를 알 수가 없다. 우리는 timestamp 컨버터를 가진 프로퍼티를 매핑함으로써 전체 날짜와 시간 정보를 보존하고 싶다고 선언한다.

다음 매핑 파일은 Event Java 클래스 소스 파일과 같은 디렉토리 속에 Event.hbm.xml로서 저장될 것이다. 매핑 파일들에 대한 네이밍은 임의적일 수 있지만, 접미사 hbm.xml은 Hibernate 개발자 공동체 내에서 컨벤션이 되었다. 디렉토리 구조는 이제 다음과 같을 것이다:

.
+lib
  <Hibernate and third-party libraries>
+src
  +events
    Event.java
    Event.hbm.xml

우리는 Hibernate의 메인 구성을 계속 행한다.

1.2.3. Hibernate 구성

우리는 이제 적절한 곳에 한 개의 영속 클래스와 그것의 매핑 파일을 갖고 있다. Hibernate를 구성할 차례이다. 우리가 이것을 행하기 전에, 우리는 데이터베이스를 필요로 할 것이다. 자바 기반의 메모리-내 SQL DBMS인 HSQL DB는 HSQL DB 웹 사이트에서 내려받을 수 있다. 실제로, 당신은 이 다운로드에서 오직 hsqldb.jar 만을 필요로 한다. 개발 폴더의 lib/ 디렉토리 속에 이 파일을 위치지워라.

개발 디렉토리의 루트에 data로 명명된 디렉토리를 생성시켜라 - 이 디렉토리는 HSQL DB가 그것의 데이터 파일들을 저장하게 될 장소이다. 이제 이 데이터 디렉토리에서 java -classpath ../lib/hsqldb.jar org.hsqldb.Server를 실행시켜서 데이터베이스를 시작시켜라. 당신은 그것이 시작되고 이것은 우리의 어플리케이션이 나중에 연결하게 될 장소인, 하나의 TCP/IP 소켓에 바인드 되는 것을 볼 수 있다. 만일 이 튜토리얼 동안에 당신이 새 데이터베이스로 시작하고자 원할 경우, HSQL DB를 셧다운시키고(왼도우에서 CTRL + C를 눌러라), data/ 디렉토리 내에 있는 모든 파일들을 삭제하고 다시 HSQL DB를 시작하라.

Hibernate는 당신의 어플리케이션 내에서 이 데이터베이스에 연결하는 계층이고, 따라서 그것은 커넥션 정보를 필요로 한다. 커넥션들은 마찬가지로 구성되어야 하는 하나의 JDBC 커넥션 풀을 통해 행해진다. Hibernate 배포본은 몇몇 오픈 소스 JDBC 커넥션 풀링 도구들을 포함하고 있지만, 이 튜토리얼에서는 Hibernate에 의해 미리 빌드된 커넥션 풀링을 사용할 것이다. 당신이 필수 라이브러리를 당신의 classpath 속에 복사해야 하고 만일 당신이 제품-특징의 제3의 JDBC 풀링 소프트웨어를 사용하고자 원할 경우에는 다른 커넥션 풀링 설정들을 사용해야 함을 노트하라.

Hibernate의 구성을 위해, 우리는 한 개의 간단한 hibernate.properties 파일, 한 개의 약간 더 세련된 hibernate.cfg.xml 파일, 또는 심지어 완전한 프로그램 상의 설정을 사용할 수 있다. 대부분의 사용자들은 XMl 구성 파일을 선호한다:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>

    <session-factory>

        <!-- Database connection settings -->
        <property name="connection.driver_class">org.hsqldb.jdbcDriver</property>
        <property name="connection.url">jdbc:hsqldb:hsql://localhost</property>
        <property name="connection.username">sa</property>
        <property name="connection.password"></property>

        <!-- JDBC connection pool (use the built-in) -->
        <property name="connection.pool_size">1</property>

        <!-- SQL dialect -->
        <property name="dialect">org.hibernate.dialect.HSQLDialect</property>

        <!-- Enable Hibernate's automatic session context management -->
        <property name="current_session_context_class">thread</property>

        <!-- Disable the second-level cache  -->
        <property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>

        <!-- Echo all executed SQL to stdout -->
        <property name="show_sql">true</property>

        <!-- Drop and re-create the database schema on startup -->
        <property name="hbm2ddl.auto">create</property>

        <mapping resource="events/Event.hbm.xml"/>

    </session-factory>

</hibernate-configuration>

이 XML 구성이 다른 DTD를 사용함을 노트하라. 우리는 Hibernate의 SessionFactory -특정 데이터베이스에 대해 책임이 있는 전역 팩토리-를 구성한다. 만일 당신이 여러 데이터베이스들을 갖고 있다면, (보다 쉬운 시작을 위해) 몇 개의 구성 파일들 속에 여러 개의 <session-factory> 구성들을 사용하라.

처음 네 개의 property 요소들은 JDBC 커넥션을 위한 필수 구성을 포함한다. dialect property 요소는 Hibernate가 발생시키는 특별한 SQL 이형(異形)을 지정한다. hbm2ddl.auto 옵션은 -직접 데이터베이스 속으로- 데이터베이스 스키마의 자동적인 생성을 활성화 시킨다. 물론 이것은 (config 옵션을 제거함으로써) 비활성화 시킬 수 있거나 SchemaExport Ant 태스크의 도움으로 파일로 리다이렉트 될 수 있다. 마지막으로 우리는 영속 클래스들을 위한 매핑 파일(들)을 추가시킨다.

이 파일을 소스 디렉토리 속으로 복사하고, 따라서 그것은 classpath의 루트에서 끝날 것이다. Hibernate는 시작 시에 classpath의 루트에서 hibernate.cfg.xml로 명명된 파일을 자동적으로 찾는다.

1.2.4. Ant로 빌드하기

우리는 이제 Ant로 튜토리얼을 빌드할 것이다. 당신은 Ant를 설치할 필요가 있을 것이다 - Ant 내려받기 페이지에서 Ant를 얻어라. Ant를 설치하는 방법은 여기서 다루지 않을 것이다. Ant 매뉴얼을 참조하길 바란다. 당신이 Ant를 설치한 후에, 우리는 빌드파일 생성을 시작할 수 있다. 그것은 build.xml로 명명되고 개발 디렉토리 속에 직접 위치될 것이다.

기본 빌드 파일은 다음과 같다:

<project name="hibernate-tutorial" default="compile">

    <property name="sourcedir" value="${basedir}/src"/>
    <property name="targetdir" value="${basedir}/bin"/>
    <property name="librarydir" value="${basedir}/lib"/>

    <path id="libraries">
        <fileset dir="${librarydir}">
            <include name="*.jar"/>
        </fileset>
    </path>

    <target name="clean">
        <delete dir="${targetdir}"/>
        <mkdir dir="${targetdir}"/>
    </target>

    <target name="compile" depends="clean, copy-resources">
      <javac srcdir="${sourcedir}"
             destdir="${targetdir}"
             classpathref="libraries"/>
    </target>

    <target name="copy-resources">
        <copy todir="${targetdir}">
            <fileset dir="${sourcedir}">
                <exclude name="**/*.java"/>
            </fileset>
        </copy>
    </target>

</project>

이것은 .jar로 끝나는 lib 디렉토리 내에 있는 모든 파일들을 컴파일에 사용되는 classpath에 추가하도록 Ant에게 알려줄 것이다. 그것은 또한 모든 비-Java 소스 파일들을 대상 디렉토리로 복사할 것이다. 예를 들면, 구성 및 Hibernate 매핑 파일들. 만일 당신이 Ant를 이제 실행할 경우, 당신은 다음 출력을 얻게 될 것이다:

C:\hibernateTutorial\>ant
Buildfile: build.xml

copy-resources:
     [copy] Copying 2 files to C:\hibernateTutorial\bin

compile:
    [javac] Compiling 1 source file to C:\hibernateTutorial\bin

BUILD SUCCESSFUL
Total time: 1 second 

1.2.5. 시작과 helper들

몇몇 Event 객체들을 로드시키고 저장할 차례이지만, 먼저 우리는 어떤 인프라스트럭처 코드로 설정을 완료해야 한다. 우리는 Hibernate를 시작해야 한다. 이 시작은 전역 SessionFactory 객체를 빌드하고 어플리케이션 내에서 용이한 접근을 위해 그것을 어떤 곳에 저장하는 것을 포함한다. SessionFactory는 새로운 Session들을 열 수 있다. Session은 작업의 단일-쓰레드 단위를 표현하며, SessionFactory는 한번 초기화 되는 하나의 thread-safe 전역 객체이다.

우리는 시작을 처리하고 Session 처리를 편리하게 해주는 HibernateUtil helper 클래스를 생성시킬 것이다. 이른바 ThreadLocal Session 패턴이 여기서 유용하며, 우리는 현재의 작업 단위를 현재의 쓰레드와 연관지워 유지한다. 구현을 살펴보자:

package util;

import org.hibernate.*;
import org.hibernate.cfg.*;

public class HibernateUtil {

    private static final SessionFactory sessionFactory;

    static {
        try {
            // Create the SessionFactory from hibernate.cfg.xml
            sessionFactory = new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            // Make sure you log the exception, as it might be swallowed
            System.err.println("Initial SessionFactory creation failed." + ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }

}

이 클래스는 (클래스가 로드될 때 JVM에 의해 한번 호출되는) 그것의 static 초기자 내에 전역 SessionFactory를 산출할 뿐만 아니라 또한 현재 쓰레드에 대한 Session을 소유하는 ThreadLocal 변수를 갖는다. 당신이 HibernateUtil.getCurrentSession()을 호출하는 시점에는 문제가 없으며, 그것은 항상 동일 쓰레드 내에 동일한 Hibernate 작업 단위를 반환할 것이다. HibernateUtil.closeSession()에 대한 호출은 쓰레드와 현재 연관되어 있는 작업 단위를 종료시킨다.

당신이 이 helper를 사용하기 전에 thread-local 변수들에 대한 Java 개념을 확실히 이해하도록 하라. 보다 강력한 HibernateUtil helper는 http://caveatemptor.hibernate.org/에 있는 CaveatEmptor 뿐만 아니라 "Hibernate in Action" 책에서 찾을 수 있다. 당신이 J2EE 어플리케이션 서버 내에 Hibernate를 배치할 경우에 이 클래스는 필수적이지 않다: 하나의 Session은 현재의 JTA 트랜잭션에 자동적으로 바인드 될 것이고 당신은 JNDI를 통해 SessionFactory를 룩업할 수 있다. 만일 당신이 JBoss AS를 사용할 경우, Hibernate는 관리되는 시스템 서비스로서 배치될 수 있고 SessionFactory를 JNDI 이름에 자동적으로 바인드시킬 수 있을 것이다.

개발 소스 디렉토리 속에 HibernateUtil.java 를 위치지우고, 다음으로 Event.java를 위치지워라:

.
+lib
  <Hibernate and third-party libraries>
+src
  +events
    Event.java
    Event.hbm.xml
  +util
    HibernateUtil.java
  hibernate.cfg.xml
+data
build.xml

이것은 문제 없이 다시 컴파일 될 것이다. 우리는 마지막으로 로깅 시스템을 구성할 필요가 있다 - Hibernate는 commons logging를 사용하고 Log4j와 JDK 1.4 사이의 선택은 당신의 몫으로 남겨둔다. 대부분의 개발자들은 Log4j를 선호한다: Hibernate 배포본에 있는 log4j.properties(이것은 디렉토리 etc/ 내에 있다)를 src 디렉토리로 복사하고, 다음으로 hibernate.cfg.xml을 복사하라. 예제 구성을 살펴보고 당신이 더 많은 verbose 출력을 원할 경우에 설정들을 변경하라. 디폴트로 Hibernate 시작 메시지는 stdout 상에 보여진다.

튜토리얼 인프라스트럭처는 완전하다 - 그리고 우리는 Hibernate로 어떤 실제 작업을 행할 준비가 되어 있다.

1.2.6. 객체 로딩과 객체 저장

마지막으로 우리는 객체들을 로드시키고 저장하는데 Hibernate를 사용할 수 있다. 우리는 한 개의 main() 메소드를 가진 한 개의 EventManager 클래스를 작성한다:

package events;
import org.hibernate.Session;

import java.util.Date;

import util.HibernateUtil;

public class EventManager {

    public static void main(String[] args) {
        EventManager mgr = new EventManager();

        if (args[0].equals("store")) {
            mgr.createAndStoreEvent("My Event", new Date());
        }

        HibernateUtil.getSessionFactory().close();
    }

    private void createAndStoreEvent(String title, Date theDate) {

        Session session = HibernateUtil.getSessionFactory().getCurrentSession();

        session.beginTransaction();

        Event theEvent = new Event();
        theEvent.setTitle(title);
        theEvent.setDate(theDate);

        session.save(theEvent);

        session.getTransaction().commit();
    }

}

우리는 한 개의 새로운 Event 객체를 생성시키고, 그것을 Hibernate에게 건네준다. Hibernate는 이제 SQL을 처리하고 데이터베이스 상에서 INSERT들을 실행시킨다. -우리가 이것을 실행하기 전에 코드를 처리하는- SessionTransaction을 살펴보자.

Session은 한 개의 작업 단위이다. 지금부터 우리는 단숨함을 유지할 것이고 Hibernate Session과 데이터베이스 트랜잭션 사이의 일-대-일 과립형(granularity)을 가정할 것이다. 실제 기반 트랜잭션 시스템으로부터 우리의 소스를 은폐시키기 위해(이 경우 통상의 JDBC이지만, 그것은 또한 JTA에도 실행된다) 우리는 Hibernate Session 상에서 이용 가능한 Transaction API를 사용한다.

sessionFactory.getCurrentSession()은 무엇을 행하는가? 먼저 당신은 당신이 (HibernateUtil 덕분에 쉽게) SessionFactory을 당신이 소유하고 있다면, 원하는 만큼 어디서든 여러번 그것을 호출할 수 있다. getCurrentSession() 메소드는 항상 "현재의" 작업 단위를 반환한다. hibernate.cfg.xml에서 우리가 이 메커니즘에 대한 구성 옵션을 "thread"로 전환시켰음을 기억하는가? 그러므로 현재 작업 단위는 우리의 어플리케이션을 실행시키는 현재의 Java 쓰레드에 묶여 있다. 하지만 이것은 전체 그림이 아니며, 작업 단위가 시작되고 그것이 끝낸 때 당신은 또한 영역(scope)을 고려해야 한다.

처름으로 필요할 때, getCurrentSession()에 대한 첫번째 호출이 이루어질 때, Session이 시작된다. 그때 그것은 Hibernate에 의해 현재 쓰레드에 바인드 된다. 트랜잭션이 끝날 때, 비록 커밋되든 또는 롤백되든 어느 경우든 Hibenate는 당신을 위해 쓰레드로부터 Session을 자동적으로 언바인드시키고 그것을 닫는다. 만일 당신이 다시 getCurrentSession()를 호출할 경우, 당신은 새로운 Session을 얻고 새로운 작업 단위를 시작할 수 있다. 이 thread-bound 프로그래밍 모형은 Hibernate를 사용하는 가장 대중적인 방법이다. 왜냐하면 그것은 당신의 코드를 유연하게 배치하는 것을 허용해주기 때문이다(트랜잭션 경계구분 코드는 데이터베이스 접근 코드와 분리될 수 있으며, 우리는 이 튜토리얼의 뒷 부분에서 이것을 다룰 것이다).

작업 영역의 단위와 관련하여, Hibernate Session은 한 개 내지 여러 개의 데이터베이스 오퍼레이션들을 실행시키는데 사용될 수 있는가? 위의 예제는 한 개의 오퍼레이션에 대해 한 개의 Session을 사용하고 있다. 이것은 단순한 일치이며, 예제는 어떤 다른 오퍼레이션을 보여주기에는 충분히 복잡하지 않다. Hibernate Session의 영역은 유연하지만 당신은 모든 데이터페이스 오퍼레이션에 대해 새로운 Hibernate Session을 사용하도록 당신의 어플리케이션을 설계해서는 결코 아니될 것이다. 따라서 만일 당신이 다음 (매우 사소한) 예제들에서 여러번 그것을 보게될지라도, session-per-operation 안티-패턴(anti-pattern)을 고려하라. 실제 (웹) 어플리케이션은 이 튜토리얼의 뒷 부분에 예시되어 있다.

트랜잭션 핸들링과 경계구분에 대한 추가 정보는 11장. 트랜잭션들과 동시성을 살펴보라. 우리는 또한 앞의 예제에서 임의의 오류 처리와 롤백을 생략했다.

이 첫 번째 루틴을 실행하기 위해서 우리는 호출 가능한 대상을 Ant 빌드 파일에 추가해야 한다:

<target name="run" depends="compile">
    <java fork="true" classname="events.EventManager" classpathref="libraries">
        <classpath path="${targetdir}"/>
        <arg value="${action}"/>
    </java>
</target>

action 아규먼트의 값은 대상을 호출할 때 명령 라인 상에서 설정된다:

C:\hibernateTutorial\>ant run -Daction=store

컴파일, 구성에 따른 Hibernate 시작 후에, 당신은 많은 로그 출력을 보게 될 것이다. 끝에서 당신은 다음 라인을 발견할 것이다:

[java] Hibernate: insert into EVENTS (EVENT_DATE, title, EVENT_ID) values (?, ?, ?)

이것은 Hibernate에 의해 실행된 INSERT이고, 물음표 기호는 JDBC 바인드 파라미터들을 나타낸다. 아규먼트로서 바인드 된 값들을 보거나 장황한 로그를 줄이려면 당신의 log4j.properties를 체크하라.

이제 우리는 마찬가지로 저장된 이벤트들을 열거하고자 원하며, 우리는 main 메소드에 한 개의 옵션을 추가한다:

if (args[0].equals("store")) {
    mgr.createAndStoreEvent("My Event", new Date());
}
else if (args[0].equals("list")) {
    List events = mgr.listEvents();
    for (int i = 0; i < events.size(); i++) {
        Event theEvent = (Event) events.get(i);
        System.out.println("Event: " + theEvent.getTitle() +
                           " Time: " + theEvent.getDate());
    }
}

우리는 또한 새로운 listEvents() method 메소드를 추가 시킨다:

private List listEvents() {

    Session session = HibernateUtil.getSessionFactory().getCurrentSession();

    session.beginTransaction();

    List result = session.createQuery("from Event").list();

    session.getTransaction().commit();

    return result;
}

여기서 우리가 행할 것은 데이터베이스로부터 모든 존재하는 Event 객체들을 로드시키기 위해 HQL (Hibernate Query Language) 질의를 사용하는 것이다. Hibernate는 적절한 SQL을 생성시킬 것이고, 그것을 데이터베이스로 전송하고 데이터를 Event 객체들에 거주시킬 것이다. 당신은 물론 HQL로서 보다 복잡한 질의들을 생성시킬 수 있다.

이제 이 모든 것을 실행하고 테스트하기 위해, 다음 단계들을 따르라:

  • 데이터베이스 속으로 어떤 것을 저장하고 물론 앞서 hbm2ddl을 통해 데이터베이스 스키마를 산출시키기 위해 ant run -Daction=store를 실행하라.

  • 이제 당신의 hibernate.cfg.xml 파일 속에서 그 프로퍼티를 주석처리함으로써 hbm2ddl을 사용불가능하게 하라. 대개 당신은 지속되는 단위 테스팅에서는 그것을 사용 가능하게 내버려두어도 되지만, 또 다른 hbm2ddl의 실행은 당신이 저장했던 모든 것을 drop시킬 것이다 - create 구성 설정은 실제로 "스키마로부터 모든 테이블들을 드롭시키고 나서, SessionFactory가 빌드될 때 모든 테이블들을 다시 생성시키는 것"으로 변환된다.

만일 당신이 지금 -Daction=list로 Ant를 호출할 경우, 당신은 당신이 지금까지 저장했던 이벤트들을 보게 될 것이다. 물론 당신은 또한 여러 번 store 액션을 호출할 수 있다.

노트 : 대부분의 Hibernate 사용자들은 이 지점에서 실패하고 우리는 정기적으로 Table not found 오류 메시지들에 관한 질문을 받는다. 하지만 만일 당신이 위에 조명된 단게들을 따를 경우 당신은 이 문제를 겪지 않을 것이고, hbm2ddl이 처음 실행 시에 데이터베이스 스키마를 생성시키므로, 차후의 어플리케이션 재시작은 이 스키마를 사용할 것이다. 만일 당신이 매핑 그리고/또는 데이터베이스 스키마를 변경할 경우에, 당신은 다시 한번 더 hbm2ddl을 이용 가능하도록 해야 한다.

1.3. 파트 2 - 연관들을 매핑하기

우리는 한 개의 영속 엔티티 클래스를 한 개의 테이블로 매핑했다. 이것 위에서 빌드하고 몇몇 클래스 연관들을 추가시키자. 먼저 우리는 우리의 어플리케이션에 사람들을 추가하고 그들이 참여하는 이벤트들의 목록을 저장할 것이다.

1.3.1. Person 클래스 매핑하기

클래스의 첫 번째 장면은 간단하다:

package events;

public class Person {

    private Long id;
    private int age;
    private String firstname;
    private String lastname;

    public Person() {}

    // Accessor methods for all properties, private setter for 'id'

}

Person.hbm.xml로 명명되는 새로운 매핑 파일을 생성시켜라 (맨위에 DTD 참조를 잊지말라):

<hibernate-mapping>

    <class name="events.Person" table="PERSON">
        <id name="id" column="PERSON_ID">
            <generator class="native"/>
        </id>
        <property name="age"/>
        <property name="firstname"/>
        <property name="lastname"/>
    </class>

</hibernate-mapping>

마지막으로 새로운 매핑을 Hibernate의 구성에 추가하라:

<mapping resource="events/Event.hbm.xml"/>
<mapping resource="events/Person.hbm.xml"/>

이제 우리는 이들 두 개의 엔티티들 사이에 한 개의 연관을 생성시킬 것이다. 명백하게, 개인들은 이벤트들에 참여할 수 있고, 이벤트들은 참여자들을 갖는다. 우리가 다루어야 하는 설계 질문들은 다음과 같다 : 방향성(directionality), 다중성(multiplicity), 그리고 콜렉션 특징.

1.3.2. 단방향 Set-기반의 연관

우리는 Person 클래스에 이벤트들을 가진 한 개의 콜렉션을 추가할 것이다. 그 방법으로 우리는 명시적인 질의-aPerson.getEvents()를 호출함으로써-를 실행시키지 않고서 특정 개인에 대한 이벤트들을 쉽게 네비게이트할 수 있다. 우리는 하나의 Java 콜렉션, 하나의 Set를 사용한다. 왜냐하면 그 콜렉션은 중복 요소들을 포함하기 않을 것이고 그 순서가 우리와 관련되어 있지 않기 때문이다.

우리는 하나의 Set으로 구현된, 하나의 단방향, 다중값 연관들을 필요로 한다. Java 클래스들 내에 이를 위한 코드를 작성하고 그런 다음 그것을 매핑시키자:

public class Person {

    private Set events = new HashSet();

    public Set getEvents() {
        return events;
    }

    public void setEvents(Set events) {
        this.events = events;
    }
}

우리가 이 연관을 매핑하기 전에, 다른 측에 대해 생각하라. 명백하게 우리는 이것을 단지 단방향으로 유지시킬 수 있다. 또는 우리가 그것을 양방향으로 네비게이트하는 것-예를 들어 anEvent.getParticipants()-이 가능하도록 원할 경우에, Event측 상에 또 다른 콜렉션을 생성시킬 수 있다. 이것은 당신에게 남겨진 설계 선택이지만, 이 논의에서 명료한 점은 연관의 다중성이다: 양 측 상에서 "다중" 값을 갖는 경우, 우리는 이것을 many-to-many 연관이라고 명명한다. 그러므로 우리는 Hibernate의 many-to-many 매핑을 사용한다:

<class name="events.Person" table="PERSON">
    <id name="id" column="PERSON_ID">
        <generator class="native"/>
    </id>
    <property name="age"/>
    <property name="firstname"/>
    <property name="lastname"/>

    <set name="events" table="PERSON_EVENT">
        <key column="PERSON_ID"/>
        <many-to-many column="EVENT_ID" class="events.Event"/>
    </set>

</class>

Hibernate는 모든 종류의 콜렉션 매핑들, 가장 공통적인 <set>을 지원한다. many-to-many 연관 (또는 n:m 엔티티 관계)의 경우, 한 개의 연관 테이블이 필요하다. 이 테이블 내에 있는 각각의 행은 한 명의 개인과 한 개의 이벤트 사이의 링크를 표현한다. 테이블 이름은 set 요소의 table 속성으로 구성된다. 연관 내의 식별자 컬럼 이름은 개인 측에 대해 <key> 요소로 정의되고 이벤트 측에 대한 컬럼 이름은 <many-to-many>column 속성으로 정의된다. 당신은 또한 당신의 콜렉션 내에 있는 객체들의 클래스(정확하게 : 참조들을 가진 콜렉션의 다른 측 상에 있는 클래스)를 Hibernate에게 알려주어야 한다.

따라서 이 매핑을 위한 데이터베이스 스키마는 다음과 같다:

    _____________        __________________
   |             |      |                  |       _____________
   |   EVENTS    |      |   PERSON_EVENT   |      |             |
   |_____________|      |__________________|      |    PERSON   |
   |             |      |                  |      |_____________|
   | *EVENT_ID   | <--> | *EVENT_ID        |      |             |
   |  EVENT_DATE |      | *PERSON_ID       | <--> | *PERSON_ID  |
   |  TITLE      |      |__________________|      |  AGE        |
   |_____________|                                |  FIRSTNAME  |
                                                  |  LASTNAME   |
                                                  |_____________|
 

1.3.3. 연관들에 작업하기

EventManager 속에 있는 한 개의 새로운 메소드 내에 몇몇 사람들과 이벤트들을 함께 가져오자:

private void addPersonToEvent(Long personId, Long eventId) {

    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();

    Person aPerson = (Person) session.load(Person.class, personId);
    Event anEvent = (Event) session.load(Event.class, eventId);

    aPerson.getEvents().add(anEvent);

    session.getTransaction().commit();
}

PersonEvent를 로드시킨 후에, 정규 콜렉션 메소드들을 사용하여 콜렉션을 간단하게 변경하라. 당신이 알 수 있듯이, update() 또는 save()에 대한 명시적인 호출이 존재하지 않고, 변경되었고 저장할 필요가 있는 콜렉션을 Hibernate가 자동적으로 검출해낸다. 이것은 자동적인 dirty 체킹이라 불려지며, 당신은 또한 당신의 임의의 객체들에 대한 name 또는 date 프로퍼티를 변경함으로써 그것을 시도할 수 있다. 그것들이 영속(persistent) 상태에 있는 동안, 즉 특정 Hibernate Session에 바인드되어 있는 동안(예를 들면. 그것들은 작업 단위 속에 방금 로드되었거나 저장되었다), Hibernate는 임의의 변경들을 모니터링하고 쓰기 이면의 형태로 SQL을 실행시킨다. 메모리 상태를 데이터베이스와 동기화 시키는 과정은 대개 오직 작업 단위의 끝에서이고, flushing이라 명명된다. 우리의 코드에서, 작업 단위는 CurrentSessionContext 클래스에 대한 thread 구성 옵션에 의해 정의된 대로 - 데이터베이스 트랜잭션의 커밋(또는 롤백)으로 끝이난다.

물론 당신은 다른 작업 단위 속에 개인과 이벤트를 로드시킬 수 도 있다. 또는 당신은 하나의 객체그 영속 상태에 있지 않을 때 Session의 외부에서 객체를 변경시킬 수도 있다(만일 객체가 이전에 영속화 되었다면, 우리는 이 상태를 detached라고 부른다). (매우 사실적이지 않은) 코드 내에서 이것은 다음과 같을 수 있다:

private void addPersonToEvent(Long personId, Long eventId) {

    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();

    Person aPerson = (Person) session
            .createQuery("select p from Person p left join fetch p.events where p.id = :pid")
            .setParameter("pid", personId)
            .uniqueResult(); // Eager fetch the collection so we can use it detached

    Event anEvent = (Event) session.load(Event.class, eventId);

    session.getTransaction().commit();

    // End of first unit of work

    aPerson.getEvents().add(anEvent); // aPerson (and its collection) is detached

    // Begin second unit of work

    Session session2 = HibernateUtil.getSessionFactory().getCurrentSession();
    session2.beginTransaction();

    session2.update(aPerson); // Reattachment of aPerson

    session2.getTransaction().commit();
}

update에 대한 호출은 한 개의 detached 객체를 다시 영속화 시키고, 당신은 그것이 새로운 작업 단위에 바인드된다고 말할 수 있고, 따라서 detached 동안에 당신이 그것에 대해 행한 임의의 변경들이 데이터베이스에 저장될 수 있다. 이것은 당신이 그 엔티티 객체의 콜렉션에 대해 행했던 임의의 변경들(추가/삭제)를 포함한다.

물론, 우리의 현재 상황에서 이것은 많이 사용되지 않지만, 그것은 당신이 당신 자신의 어플리케이션 내로 설계할 수 있는 중요한 개념이다. 지금 EventManager의 main 메소드에 한 개의 새로운 액션을 추가하고 명령 라인에서 그것을 호출하여 이 연습을 완료하라. 만일 당신이 한명의 개인과 한 개의 이벤트에 대한 식별자들을 필요로 할 경우 - save() 메소드가 그것을 반환시킨다(당신은 그 식별자를 반환시키는 앞의 메소드들 중 몇몇을 변경시켜야 할 것이다):

else if (args[0].equals("addpersontoevent")) {
    Long eventId = mgr.createAndStoreEvent("My Event", new Date());
    Long personId = mgr.createAndStorePerson("Foo", "Bar");
    mgr.addPersonToEvent(personId, eventId);
    System.out.println("Added person " + personId + " to event " + eventId);
}

이것은 두 개의 동등하게 중요한 클래스들, 두 개의 엔티티들 사이에서 한 개의 연관에 관한 예제였다. 앞서 언급했듯이, 전형적인 모형 내에는 다른 클래스들과 타이들이 존재하는데, 대개 "덜 중요하다". 당신은 이미 int 또는 String과 같은 어떤 것을 이미 보았다. 우리는 이들 클래스들을 값 타입들(value types)이라 명명하고, 그들 인스턴스들은 특정 엔티티에 의존한다(depend). 이들 타입들을 가진 인스턴스들은 그것들 자신의 식별성(identity)를 갖지 않거나, 그것들은 엔티티들 사이에서 공유되지도 않는다(두개의 person들은 심지어 그것들이 같은 첫 번째 이름을 갖는 경우에도 동일한 firstname을 참조하지 않는다 ). 물론 값 타입들은 JDK 내에서 발견될 뿐만 아니라(사실, Hibernate 어플리케이션에서 모든 JDK 클래스들은 값 타입들로 간주된다), 당신은 또한 당신 스스로 종속 클래스들, 예를 들면 Address 또는 MonetaryAmount을 작성할 수 있다.

당신은 또한 값 타입들을 설계할 수 있다. 이것은 다른 엔티티들에 대한 참조들을 가진 콜렉션과는 개념적으로 매우 다르지만, Java에서는 대개 동일한 것으로 보여진다.

1.3.4. 값들을 가진 콜렉션

우리는 값 타입의 객체들을 가진 한 개의 콜렉션을 Person 엔티티에 추가시킨다. 우리는 email 주소를 저장하고자 원하므로, 우리가 사용하는 타입은 String이고, 그 콜렉션은 다시 한 개의 Set이다:

private Set emailAddresses = new HashSet();

public Set getEmailAddresses() {
    return emailAddresses;
}

public void setEmailAddresses(Set emailAddresses) {
    this.emailAddresses = emailAddresses;
}

Set에 대한 매핑은 다음과 같다:

<set name="emailAddresses" table="PERSON_EMAIL_ADDR">
    <key column="PERSON_ID"/>
    <element type="string" column="EMAIL_ADDR"/>
</set>

앞의 매핑과 비교한 차이점은 element 부분인데, 그것은 그 콜렉션이 또 다른 엔티티에 대한 참조들을 포함하지 않을 것이지만 String(소문자 이름은 그것이 Hibernate 매핑 타입/변환자임을 당신에게 말해준다) 타입의 요소들을 가진 한 개의 콜렉션을 포함할 것임을 Hibernate에게 알려준다. 일단 다시 set 요소의 table 속성은 그 콜렉션에 대한 테이블 이름을 결정한다. key 요소는 콜렉션 테이블 내에서 foreign-key 컬럼 이름을 정의한다. element 요소 내에 있는 column 속성은 String 값들이 실제로 저장될 컬럼 이름을 정의한다.

업데이트된 스키마를 살펴보라:

  _____________        __________________
 |             |      |                  |       _____________
 |   EVENTS    |      |   PERSON_EVENT   |      |             |       ___________________
 |_____________|      |__________________|      |    PERSON   |      |                   |
 |             |      |                  |      |_____________|      | PERSON_EMAIL_ADDR |
 | *EVENT_ID   | <--> | *EVENT_ID        |      |             |      |___________________|
 |  EVENT_DATE |      | *PERSON_ID       | <--> | *PERSON_ID  | <--> |  *PERSON_ID       |
 |  TITLE      |      |__________________|      |  AGE        |      |  *EMAIL_ADDR      |
 |_____________|                                |  FIRSTNAME  |      |___________________|
                                                |  LASTNAME   |
                                                |_____________|
 

당신은 콜렉션 테이블의 프라이머리 키가 사실은 두 컬럼들을 사용하는 한 개의 합성 키(composite key)임을 알 수 있다. 이것은 또한 개인에 대해 email 주소가 중복될 수 없음을 의미하며, 그것은 정확하게 우리가 Java에서 set을 필요로 하는 의미론이다.

마치 개인들과 이벤트들을 링크시켜서 이전에 우리가 행했던 것처럼 이제 당신은 요소들을 시도하고 이 콜렉션에 추가할 수 있다. 그것은 Java에서 동일한 코드이다.

private void addEmailToPerson(Long personId, String emailAddress) {

    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();

    Person aPerson = (Person) session.load(Person.class, personId);

    // The getEmailAddresses() might trigger a lazy load of the collection
    aPerson.getEmailAddresses().add(emailAddress);

    session.getTransaction().commit();
}

지금 우리는 콜렉션을 초기화 시키는데 fetch 질의를 사용하지 않았다. 그러므로 콜렉션의 getter 메소드에 대한 호출은 콜렉션을 초기화 시키기 위해 추가적인 select를 트리거 시킬 것이어서, 우리는 그것에 요소를 추가시킬 수 있다. SQL 로그를 관찰하고 이것을 eager fetch로 최적화 시키려고 시도하라.

1.3.5. Bi-directional associations

다음으로 우리는 양방향 연관을 매핑시킬 예정이다-개인과 이벤트 사이에 연관을 만드는 것은 Java에서 양 측들에서 동작한다. 물론 데이터베이스 스키마는 변경되지 않고, 우리는 여전히 many-to-many 다중성을 갖는다. 관계형 데이터베이스는 네트웍 프로그래밍 언어 보다 훨씬 더 유연하여서, 그것은 네비게이션 방향과 같은 어떤 것을 필요로 하지 않는다 - 데이터는 어떤 가능한 바업ㅂ으로 보여질 수 있고 검색될 수 있다.

먼저, 참여자들을 가진 한 개의 콜렉션을 Event Event 클래스에 추가시켜라:

private Set participants = new HashSet();

public Set getParticipants() {
    return participants;
}

public void setParticipants(Set participants) {
    this.participants = participants;
}

이제 Event.hbm.xml 내에 연관의 이 쪽도 매핑하라.

<set name="participants" table="PERSON_EVENT" inverse="true">
    <key column="EVENT_ID"/>
    <many-to-many column="PERSON_ID" class="events.Person"/>
</set>

당신이 볼 수 있듯이, 이것들은 두 매핑 문서들 내에서 정규 set 매핑들이다. keymany-to-many에서 컬럼 이름들은 두 매핑 문서들에서 바뀌어진다. 여기서 가장 중요한 부가물은 Event의 콜렉션 매핑에 관한 set 요소 내에 있는 inverse="true" 속성이다.

이것이 의미하는 바는 Hibernate가 둘 사이의 링크에 대한 정보를 알 필요가 있을 때 다른 측-Person 클래스-를 취할 것이라는 점이다. 일단 당신이 우리의 두 엔티티들 사이에 양방향 링크가 생성되는 방법을 안다면 이것은 이해하기가 훨씬 더 쉬울 것이다.

1.3.6. 양방향 링크들에 작업하기

첫 번째로 Hibernate가 정규 Java 의미론에 영향을 주지 않음을 염두에 두라. 우리는 단방향 예제에서 PersonEvent 사이에 어떻게 한 개의 링크를 생성시켰는가? 우리는 Event 타입의 인스턴스를 Person 타입의 이벤트 참조들을 가진 콜렉션에 추가시켰다. 따라서 명백하게 우리가 이 링크를 양방향으로 동작하도록 만들고자 원한다면, 우리는 다른 측 상에서 -하나의 Person 참조를 하나의 Event 내에 있는 콜렉션에 추가시킴으로써- 동일한 것을 행해야 한다. 이 "양 측 상에 링크 설정하기"는 절대적으로 필수적이고 당신은 그것을 행하는 것을 결코 잊지 말아야 한다.

많은 개발자들은 방비책을 프로그램하고 양 측들을 정확하게 설정하기 위한 하나의 링크 관리 메소드들을 생성시킨다. 예를 들면 Person에서 :

protected Set getEvents() {
    return events;
}

protected void setEvents(Set events) {
    this.events = events;
}

public void addToEvent(Event event) {
    this.getEvents().add(event);
    event.getParticipants().add(this);
}

public void removeFromEvent(Event event) {
    this.getEvents().remove(event);
    event.getParticipants().remove(this);
}

콜렉션에 대한 get 및 set 메소드드은 이제 protected임을 인지하라 - 이것은 동일한 패키지 내에 있는 클래스들과 서브클래스들이 그 메소드들에 접근하는 것을 허용해주지만, 그 밖의 모든 것들이 그 콜렉션들을 (물론, 대개) 직접 만지는 것을 금지시킨다. 당신은 다른 측 상에 있는 콜렉션에 대해 동일한 것을 행할 것이다.

inverse 매핑 속성은 무엇인가? 당신의 경우, 그리고 Java의 경우, 한 개의 양방향 링크는 단순히 양 측들에 대한 참조들을 정확하게 설정하는 문제이다. 하지만 Hibernate는 (컨스트레인트 위배를 피하기 위해서) SQL INSERT 문장과 UPDATE 문장을 정확하게 마련하기에 충분한 정보를 갖고 있지 않으며, 양방향 연관들을 올바르게 처리하기 위해 어떤 도움을 필요로 한다. 연관의 한 측을 inverse로 만드는 것은 기본적으로 그것을 무시하고 그것을 다른 측의 거울(mirror)로 간주하도록 Hibernate에게 알려준다. 그것은 Hibernate가 하나의 방향성 네비게이션 모형을 한 개의 SQL 스키마로 변환시킬 때 모든 쟁점들을 잘 해결하는데 필수적인 모든 것이다. 당신이 염두에 두어야 하는 규칙들은 간단하다 : 모든 양방향 연관들은 한 쪽이 inverse일 필요가 있다. one-to-many 연관에서 그것은 many-측이어야 하고, many-to-many 연관에서 당신은 어느 측이든 선택할 수 있으며 차이점은 없다.

Let's turn this into a small web application.

1.4. 파트 3 - EventManager 웹 어플리케이션

Hibernate 웹 어플리케이션은 대부분의 스탠드얼론 어플리케이션과 같이 SessionTransaction을 사용한다. 하지만 몇몇 공통 패턴들이 유용하다. 우리는 이제 EventManagerServlet를 작성한다. 이 서블릿은 데이터베이스 내에 저장된 모든 이벤트들을 나열할 수 있고, 그것은 새로운 이벤트들을 입력하기 위한 HTML form을 제공한다.

1.4.1. 기본 서블릿 작성하기

다음 장에서 우리는 Hibernate를 Tomcat 및 WebWork와 통합시킨다. EventManager는 우리의 성장하는 어플리케이션을 더이상 감당하지 못한다. 당신의 소스 디렉토리에서 events 패키지 내에 새로운 클래스를 생성시켜라:

package events;

// Imports

public class EventManagerServlet extends HttpServlet {

    // Servlet code
}

서블릿은 HTTP GET 요청들 만을 처리하므로, 우리가 구현하는 메소드는 doGet()이다:

protected void doGet(HttpServletRequest request,
                     HttpServletResponse response)
        throws ServletException, IOException {

    SimpleDateFormat dateFormatter = new SimpleDateFormat("dd.MM.yyyy");

    try {
        // Begin unit of work
        HibernateUtil.getSessionFactory()
                .getCurrentSession().beginTransaction();

        // Process request and render page...

        // End unit of work
        HibernateUtil.getSessionFactory()
                .getCurrentSession().getTransaction().commit();

    } catch (Exception ex) {
        HibernateUtil.getSessionFactory()
                .getCurrentSession().getTransaction().rollback();
        throw new ServletException(ex);
    }

}

우리가 여기서 적용하는 패턴은 session-per-request이다. 하나의 요청이 서블릿에 도달할 때, 하나의 새로운 Hibernate SessionSessionFactory 상의 getCurrentSession()에 대한 첫번째 호출을 통해 열린다. 그때 하나의 데이터베이스 트랜잭션이 시작되고, 모든 데이터 접근이 하나의 트랜잭션 내에서 발생하는 한, 데이터가 읽혀지거나 기록되는데 문제가 없다(우리는 어플리케이션들 내에서 auto-commit 모드를 사용하지 않는다).

모든 데이터베이스 오퍼레이션에 대해 새로운 Hibernate Session을 사용하지 말라. 전체 요청에 대해 영역지워진 한 개의 Hibernate Session을 사용하라. getCurrentSession()을 사용하라. 그것은 현재의 자바 쓰레드에 자동적으로 바인드된다.

다음으로, 요청의 가능한 액션들이 처리되고 응답 HTML이 렌더링된다. 우리는 곧장 그부분으로 갈 것이다.

마지막으로, 프로세싱과 렌더링이 완료될 때 작업 단위가 종료된다. 만일 어떤 문제가 프로세싱과 렌더링 동안에 발생될 경우, 하나의 예외상황이 던져질 것이고 데이터베이스 트랜잭션은 롤백될 것이다. 이것은 session-per-request을 완료시킨다. 모든 서블릿 내에 있는 트랜잭션 구획 코드 대신에 당신은 또한 서블릿 필터를 사용할 수 있다. Open Session in View로 명명되는 이 패턴에 대한 추가 정보는 Hibernate 웹 사이트와 위키를 보라. 당신은 서블릿 내에서가 아닌 JSP 내에 당신의 뷰를 렌더링하는 것을 고려할 때 그것을 필요로 할 것이다.

1.4.2. 프로세싱과 렌더링

요청의 처리와 페이지의 렌더링을 구현하자.

// Write HTML header
PrintWriter out = response.getWriter();
out.println("<html><head><title>Event Manager</title></head><body>");

// Handle actions
if ( "store".equals(request.getParameter("action")) ) {

    String eventTitle = request.getParameter("eventTitle");
    String eventDate = request.getParameter("eventDate");

    if ( "".equals(eventTitle) || "".equals(eventDate) ) {
        out.println("<b><i>Please enter event title and date.</i></b>");
    } else {
        createAndStoreEvent(eventTitle, dateFormatter.parse(eventDate));
        out.println("<b><i>Added event.</i></b>");
    }
}

// Print page
printEventForm(out);
listEvents(out, dateFormatter);

// Write HTML footer
out.println("</body></html>");
out.flush();
out.close();

Java와 HTML이 혼합된 이 코딩이 보다 복잡한 어플리케이션에서 기준이 될 수 없다 할지라도, 우리는 단지 이 튜토리얼 내에서 기본 Hibernate 개념들을 설명하고 있음을 염두에 두라. 코드는 하나의 HTML 헤더와 하나의 footer를 프린트한다. 이 페이지 내에 이벤트 엔트리를 위한 하나의 HTML form과 데이터베이스 내에 있는 모든 이벤트들의 목록이 프린트된다. 첫 번째 메소드는 시행적이고 오직 HTML을 출력한다:

private void printEventForm(PrintWriter out) {
    out.println("<h2>Add new event:</h2>");
    out.println("<form>");
    out.println("Title: <input name='eventTitle' length='50'/><br/>");
    out.println("Date (e.g. 24.12.2009): <input name='eventDate' length='10'/><br/>");
    out.println("<input type='submit' name='action' value='store'/>");
    out.println("</form>");
}

listEvents() 메소드는 하나의 질의를 실행하기 위해서 현재의 쓰레드에 결합된 Hibernate Session을 사용한다:

private void listEvents(PrintWriter out, SimpleDateFormat dateFormatter) {

    List result = HibernateUtil.getSessionFactory()
                    .getCurrentSession().createCriteria(Event.class).list();
    if (result.size() > 0) {
        out.println("<h2>Events in database:</h2>");
        out.println("<table border='1'>");
        out.println("<tr>");
        out.println("<th>Event title</th>");
        out.println("<th>Event date</th>");
        out.println("</tr>");
        for (Iterator it = result.iterator(); it.hasNext();) {
            Event event = (Event) it.next();
            out.println("<tr>");
            out.println("<td>" + event.getTitle() + "</td>");
            out.println("<td>" + dateFormatter.format(event.getDate()) + "</td>");
            out.println("</tr>");
        }
        out.println("</table>");
    }
}

마지막으로, store 액션은 createAndStoreEvent() 메소드로 디스패치된다. 그것은 현재 쓰레드의 Session을 사용한다:

protected void createAndStoreEvent(String title, Date theDate) {
    Event theEvent = new Event();
    theEvent.setTitle(title);
    theEvent.setDate(theDate);

    HibernateUtil.getSessionFactory()
                    .getCurrentSession().save(theEvent);
}

즉 서블릿이 완성된다. 서블릿에 대한 요청은 하나의 단일 SessionTransaction 내에서 처리될 것이다. 이전처럼 스탠드얼론 어플리케이션에서, Hibernate는 이들 객체들을 실행 중인 현재 쓰레드에 자동적으로 바인드시킬 수 있다. 이것은 당신의 코드를 계층화 시키고 당신이 좋아하는 임의의 방법으로 SessionFactory에 접근하는 자유를 당신에게 부여한다. 대개 당신은 보다 세련된 설계를 사용할 것이고 데이터 접근 코드를 데이터 접근 객체들 내로 이동시킬 것이다(DAO 패턴). 추가 예제들은 Hibernate 위키를 보라.

1.4.3. 배치하기 그리고 테스트하기

이 어플리케이션을 배치하기 위해서 당신은 하나의 웹 아카이브, WAR를 생성시켜야 한다. 다음 Ant target을 당신의 build.xml 내에 추가하라:

<target name="war" depends="compile">
    <war destfile="hibernate-tutorial.war" webxml="web.xml">
        <lib dir="${librarydir}">
          <exclude name="jsdk*.jar"/>
        </lib>

        <classes dir="${targetdir}"/>
    </war>
</target>

이 target은 당신의 프로젝트 디렉토리 내에 hibernate-tutorial.war로 명명된 하나의 파일을 생성시킨다. 그것은 당신의 프로젝트의 기본 디렉토리 내에 기대되는 모든 라이브러리들과 web.xml 디스크립터를 패키징한다:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <servlet>
        <servlet-name>Event Manager</servlet-name>
        <servlet-class>events.EventManagerServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>Event Manager</servlet-name>
        <url-pattern>/eventmanager</url-pattern>
    </servlet-mapping>
</web-app>

당신이 웹 어플리케이션을 컴파일하고 배치하기 전에, 하나의 부가적인 라이브러리가 필요함을 노트하라: jsdk.jar. 당신이 이미 이 라이브러리를 갖고 있지 않을 경우, 이것은 Java servlet development kit이며, Sun 웹 사이트로부터 그것을 얻어서 그것을 당신의 라이브러리 디렉토리에 복사하라. 하지만 그것은 오직 컴파일 시에만 사용될 것이고 WAR 패키지에서는 제외된다.

빌드하고 배치하기 위해 당신의 프로젝트 디렉토리 내에서 ant war를 호출하고 hibernate-tutorial.war 파일을 당신의 Tomcat webapp 디렉토리로 복사하라. 만일 당신이 Tomcat을 설치하지 않았다면, 그것을 내려받아 설치 지침들을 따르라. 당신은 이 어플리케이션을 배치하기 위해 임의의 Tomcat 구성을 변경하지 않아야 한다.

일단 배치했고 Tomcat이 실행중이면, http://localhost:8080/hibernate-tutorial/eventmanager로 어플리케이션에 접근하라. 첫 번째 요청이 당신의 서블릿에 도달할 때 Hibernate가 초기화(HibernateUtil 내에 있는 static initializer가 호출된다) 되는 것을 보기 위해 그리고 만일 어떤 예외상황들이 발생할 경우 상세한 출력을 얻기 위해서 Tomcat 로그를 지켜보도록 하라.

1.5. 요약

이 튜토리얼은 간단한 스탠드얼론 Hibernate 어플리케이션과 하나의 작은 웹 어플리케이션을 작성하는 기초를 다루었다.

만일 당신이 이미 Hibernate에 자신이 있다고 느낀다면, 당신이 흥미를 찾는 주제들에 대한 참조 문서 목차를 계속 브라우징하라 - 가장 많이 요청되는 것은 트랜잭션 처리(11장. 트랜잭션들과 동시성), 페치 퍼포먼스(19장. 퍼포먼스 개선하기), 또는 API 사용법(10장. 객체들로 작업하기), 그리고 질의 특징들(10.4절. “질의하기”)이다.

더 많은(특화된) 튜토리얼들에 대해서는 Hibernate 웹 사이트를 체크하는 것을 잊지 말라.