1장. Hibernate에 대한 소개

오픈소스 비즈니스 컨설팅
둘러보기로 가기 검색하러 가기

1.1 개요

이 장은 Hibernate에 대해 생소한 사용자를 위한 개요 문서입니다. 내장 메모리 DB를 사용해서 단순한 커맨드 라인 어플리케이션으로 시작하며 단계별로 이해하기 쉽게 구현할 것입니다.

이 문서는 Hibernate에 대해서 생소한 사용자를 위함이지만, 자바와 SQL 에 대한 지식은 필요로 합니다. 이 문서는 Michael Gloegl에 의해 작성된 것을 토대로 하며, JDK1.4 와 5.0을 위한 다른 라이브러리가 필요합니다. 혹은 JDK 1.3을 위한 다른 것들을 필요로 할 수 있습니다.

이 문서의 소스 코드는 배포판의 doc/reference/tutorial/ 디렉토리에 포함되어 있습니다.

1.2 1부 - 첫번째 Hibernate 어플리케이션

먼저 단순한 콘솔 기반 Hibernate 어플리케이션을 작성하려고 합니다. 자바 DB (HSQLDB)를 사용하기 때문에 DB 서버를 설치할 필요가 없습니다.

우리가 참여하고자 하는 이벤트와 이러한 이벤트의 주최측에 대한 정보를 저장할 수 있는 자그마한 DB 어플리케이션이 필요하다고 가정해봅시다.

먼저 우리가 할 일은 개발 디렉토리를 세팅하고 이에 필요한 자바 라이브러리를 모두 넣는 것입니다. Hibernate 웹 사이트에서 배포판을 다운받으세요. 패키지를 풀고 /lib 에 있는 필요한 모든 라이브러리를 새로운 개발 작업 디렉토리의 /lib 디렉토리로 옮기세요. 다음과 같은 모습이 될 겁니다.

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

위의 라이브러리들은 이 문서가 작성되는 시점에 Hibernate에 대한 필요한 최소한의 것들입니다. (메인 디렉토리의 hibernate3.jar 가 복사되었음을 유의하세요.) 여러분들이 사용하고 있는 Hibernate 배포 버전은 몇가지가 더 필요하거나 몇가지가 없을 수도 있습니다. 필수와 선택 라이브러리에 대해서 더 자세한 정보는 Hibernate 배포판의 lib/ 디렉토리에 있는 README.txt 파일을 참조하세요. (실제로 Log4j는 필수는 아니지만 많은 개발자들이 선호합니다.)

다음은 DB에 저장하려는 이벤트를 표현하는 클래스를 생성합니다.


1.2.1 첫번째 클래스

첫번째 저장 클래스는 다음과 같이 몇가지 속성을 지닌 단순한 JavaBean입니다.

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 메소드를 위한 표준 JavaBean 네이밍 규칙 뿐만 아니라 필드를 위한 private 가시성을 사용하고 있음을 볼 수 있습니다. 이는 권고하는 설계입니다. - 하지만 필수는 아닙니다. Hibernate는 필드를 직접적으로 접근이 가능하며, 접근자 메소드에 대한 장점은 리팩토링시 강력하다는 것입니다. 인자가 없는 생성자는 리플렉션을 통해 해당 클래스의 객체를 인스턴스화 하는데 필요합니다.

id 속성은 특정 이벤트에 대한 유일한 식별자를 포함합니다. 모든 저장 개체 클래스들(뿐만 아니라 덜 중요한 연관 클래스들)은 Hibernate의 완전한 기능을 사용하기 원한다면 그러한 식별자 속성을 필요로 합니다. 사실, 대부분의 어플리케이션들 (특히 웹 어플리케이션)은 식별자를 통해 객체를 구별할 필요가 있으며, 따라서 이러한 특징은 제약이라기 보다는 기능으로 생각해야 합니다. 하지만, 보통 객체의 식별자를 수정하지 않기 때문에 setter 메소드는 private 이어야 합니다. Hibernate 만이 객체가 저장될 때 식별자를 할당할 것입니다. Hibernate는 public, private, protected 접근자 메소드 뿐만 아니라, (public, private, protected)필드를 직접적으로 접근하는 것을 볼 수 있습니다. 선택은 여러분이 하며 어플리케이션 설계에 적합하게 선택할 수 있습니다.

인자가 없는 생성자는 모든 저장 클래스에 대해서 필수입니다. Hibernate는 자바 리플렉션을 사용해서 객체를 생성해야 합니다. 하지만, 생성자는 private 일 수 있으며, package 가시성은 바이트코드 해석 없이 실행시 proxy 생성과 효율적인 데이터 조회를 위해 필요합니다.

해당 자바 소스 파일을 개발 폴더의 src 라고 하는 디렉토리와 정확한 패키지 내에 위치시킵니다. 디렉토리는 다음과 같습니다.

.
+lib
   <Hibernate 와 기타 라이브러리들>
+src
  +events
    Event.java

다음 단계에서 이 저장 클래스에 대해 Hibernate에게 알려줄 것입니다.


1.2.2 맵핑 파일

Hibernate는 저장 클래스에 대한 객체를 어떻게 로딩하고 저장하는지를 알 필요가 있습니다. 이것이 Hibernate 맵핑 파일이 제 역할을 지점입니다. 맵핑 파일은 Hibernate에게 접근해야 하는 DB내 테이블이 무엇이며, 사용해야 하는 해당 테이블내 어떤 컬럼인지를 전달해줍니다.

맵핑 파일의 기본적인 구조는 다음과 같습니다.

<?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 파일을 로딩하지 않지만, 먼저 어플리케이션의 클래스패스에서 검색함을 유의하세요. DTD 파일은 hibernate3.jar 뿐만 아니라 Hibernate 배포판의 src/ 디렉토리에 포함되어 있습니다.

코드의 간결성을 위해서 이후 예제에서는 DTD 선언부를 생략할 것입니다. 물론 DTD 선언부는 선택사항이 아닙니다.

두개의 hibernate-mapping 태그 사이에 class 요소를 포함합니다. 모든 저장 개체 클래스들은(첫번째 클래스 개체가 아닌 종속 클래스들이 이후에 나타날 수 있습니다.) SQL 데이터베이스의 테이블로의 다음과 같은 매핑이 필요합니다.

<hibernate-mapping>
   <class name="events.Event" table="EVENTS">
   </class>
</hibernate-mapping>


지금까지 Hibernate에게 각각의 인스턴스가 해당 테이블의 행에 해당하는 EVENTS 테이블에 Event 클래스의 객체를 어떻게 저장하고 로딩하는지를 지정했습니다. 이제 유일한 식별자 속성을 테이블의 PK로 매핑하는 것을 계속하겠습니다. 게다가, 이러한 식별자를 처리하는 것에 신경쓰지 않기 원하기 때문에 PK 컬럼에 대한 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" 는 자바 속성의 이름을 표시합니다. - Hibernate는 속성 접근을 위해 getter/setter 메소드를 사용합니다. column 속성은 Hibernate에게 이 PK를 위해 사용하는 EVENTS 테이블의 어떤 컬럼인지를 지정합니다. 내부에 있는 generator 요소는 식별자 생성 방법을 지정하는 것으로, 위의 경우 native를 사용하고 있는데, 이는 설정된 DB (dialect)에 의존하는 가장 좋은 방법이기도 합니다. Hibernate는 DB에서 생성하거나, 전역적으로 유일한 것 뿐만 아니라 어플리케이션에서 할당하는 식별자를 지원합니다. (혹은 확장하여 작성된 어떠한 방법도 포함)

마지막으로 맵핑 파일에 클래스의 저장 속성에 대한 선언을 포함합니다. 기본적으로 클래스의 어떠한 속성들도 저장되는 것으로 생각하지 않습니다.

<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 속성도 Hibernate에게 어떤 getter/setter를 사용할지를 지정합니다. 따라서 이경우 Hibernate는 getDate()/setDate() 뿐만 아니라 getTitle()/setTitle()을 찾게 됩니다.

date 속성 맵핑이 column 속성을 사용하고, title 을 그렇지 않은 이유가 무엇인가요? column 속성이 없으면 Hibernate는 기본적으로 column 명으로 property 명을 사용합니다. title 이 그 좋은 예입니다. 하지만, date는 대부분의 DB에서 예약어로 등록되어 있기 때문에 이를 다른 이름으로 맵핑한 것입니다.

다음으로 흥미있는 것은 title 맵핑 역시 type 속성이 없다는 것입니다. 맵핑 파일에서 선언하고 사용된 타입은 예상했듯이 자바 데이터 타입이 아닙니다. 또한 SQL DB 타입도 아닙니다. 이러한 타입들을 Hibernate 맵핑 타입이라고 하며, 자바에서 SQL 데이터 타입으로 그리고 그 역으로 변형을 해주는 변환 역할을 해줍니다. Hibernate는 type 속성이 맵핑에 없으면 올바른 변형과 맵핑 타입을 결정하게 됩니다. 어떤 경우에 이러한 자동 탐지(자바 클래스에 대한 리플렉션을 사용한)는 예상하거나 필요한 기본적인 형태를 가지지 않을 수도 있습니다. 이러한 예가 date 속성입니다. Hibernate는 해당 속성이 (java.util.Date 타입) SQL의 date, timestamp, 혹은 time 컬럼으로 맵핑해야 하는지를 알 수 없습니다. timestamp 변환기를 사용해서 속성을 맵핑함으로써 전체 날짜와 시간 정보를 유지합니다.

이러한 맵핑 파일은 Event.hbm.xml에 저장되어야 하며, Event 자바 클래스 소스 파일 다음에 해당 디렉토리에 저장됩니다. 맵핑 파일에 대한 이름 규칙은 임의적일 수 있지만, hbm.xml 이라는 확장명은 Hibernate 개발 진영에서 규약입니다. 디렉토리 구조는 다음과 같습니다.

.
+lib
  <Hibernate 와 기타 라이브러리들>
+src
  +events
    Event.java
    Event.hbm.xml

Hibernate의 주요 설정에 대해서 계속 진행하겠습니다.


1.2.3 Hibernate 설정

이제 저장 클래스와 해당 맵핑 파일이 준비되었습니다. Hibernate를 설정할 시간입니다. 이전에 먼저 DB가 필요합니다. 자바 기반 SQL DBMS인 HSQLDB는 HSQLDB 웹사이트에서 다운받을 수 있습니다. 실제적으로는 다운로드 파일에서 hsqldb.jar 파일만을 필요로 합니다. 이 파일을 개발 폴더의 lib/ 디렉토리에 위치시킵니다.

개발 디렉토리의 루트에 data 라고 하는 디렉토리를 생성합니다. - 이 디렉토리는 HSQLDB 가 데이터 파일을 저장하는 곳입니다. 이제 data 디렉토리에서 java -classpath ../lib/hsqldb.jar org.hsqldb.Server 를 실행해서 DB를 기동시킵니다. DB가 기동되고 TCP/IP 소켓에 바인딩되는 것을 볼 수 있을 것입니다. 이후에 어플리케이션에서 연결될 지점입니다. 만일 이 문서를 진행하는 동안에 새로운 DB로 시작하기를 원한다면 HSQLDB를 셧다운시키고 (윈도우에서 CTRL+C 버튼), data/ 디렉토리에 있는 모든 파일을 삭제 후, HSQLDB를 다시 기동합니다.

Hibernate는 DB와 연결되는 어플리케이션의 영역에 있기 때문에 연결 정보가 필요합니다. 연결은 JDBC 연결 풀을 통해 이루어지며, 이 또한 설정을 해야합니다. Hibernate 배포판은 몇가지 오픈 소스 JDBC 연결 풀링 도구들이 포함되어 있지만, 이 문서에는 Hibernate 내장 연결 풀을 사용할 것입니다. 만일 제품 수준 품질의 다른 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를 설정하는데, 이는 특정 DB를 담당하는 전역 팩토리입니다. 만일 여러 DB를 사용한다면, 여러개의 <session-factory> 설정을 사용하며, 보통은 여러 설정 파일에 위치시킵니다. (쉬운 기동을 위해)

처음 네개의 property 요소들은 JDBC 연결을 위한 필요한 설정을 포함합니다. dialect property 요소는 Hibernate가 생성하는 특별한 SQL 변수를 지정합니다. Hibernate의 저장 컨텍스트를 위한 자동 세션 관리는 곧 보게 될 것이지만 편리합니다. hbm2ddl.auto 설정은 DB로 직접적으로 DB 스키마 자동 생성을 지정합니다. 이러한 설정은 물론 설정하지 않거나(설정에서 제거) SchemaExport Ant task 를 통해서 파일로 지정할 수 있습니다. 마지막으로 설정에 저장 클래스들에 대한 맵핑 파일을 추가했습니다.

위의 파일을 소스 디렉토리에 복사하면, 클래스패스의 루트에 위치하게 됩니다. Hibernate는 자동으로 기동시 클래스패스 루트에서 hibernate.cfg.xml 파일을 검색합니다.


1.2.4 Ant로 작업하기

이제 Ant를 사용해서 이러한 설정을 만들어냅니다. 설치된 Ant 파일을 필요로 합니다. - Ant 다운로드 페이지(http://ant.apache.org/bindownload.cgi)에서 다운로드 받습니다. Ant를 설치하는 방법은 여기서 다루지 않습니다. Ant 매뉴얼(http://ant.apache.org/manual/index.html)을 참고하기 바랍니다. Ant 설치 후에, buildfile 을 생성할 수 있습니다. 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>

위의 설정은 Ant가 컴파일 동안 사용되는 클래스패스에 .jar로 끝다는 lib 디렉토리에 있는 모든 파일을 추가하게 합니다. 또한 설정 파일과 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는 쓰레드에 안전한 전역 객체이며, 한번만 인스턴스화 됩니다.

기동을 담당하고 SessionFactory에 간편하게 접속하는 HibernateUtil helper 클래스를 만듭니다. 다음은 그 구현입니다.

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에 의해 한번만 호출됨) 에 전역 SessionFactory를 생성할 뿐만 아니라, 정적 singleton을 사용한다는 사실을 감추고 있습니다. 또한 어플리케이션 서버의 JNDI로부터 SessionFactory를 찾는 것이 가능합니다.

만일 설정 파일에서 SessionFactory의 이름을 지정한다면, Hibernate는 SessionFactory가 생성된 후에 JNDI에 이를 바인딩하려고 할 것입니다. 이러한 코드를 완전하게 피하기 위해서 JMX 배포를 사용할 수도 있으며 JMX 가능한 컨테이너에게 JNDI로 HibernateService를 초기화시키거나 바인딩하게끔 할 수 있습니다. 자세한 설정은 Hibernate 참조 문서에 설명되어 있습니다.

HibernateUtil.java 를 다음과 같이 개발 소스 디렉토리의 events 다음의 패키지에 위치시킵니다.

.
+lib
  <Hibernate 와 기타 라이브러리들>
+src
  +events
    Event.java
    Event.hbm.xml
  +util
    HibernateUtil.java
  hibernate.cfg.xml
+data
build.xml

아무런 문제없이 다시 컴파일되어야 합니다. 마지막으로 로깅 시스템을 설정할 필요가 있습니다. - Hibernate는 commons logging을 사용하기 때문에 사용자가 Log4j와 JDK 1.4 logging 중에서 선택할 수 있습니다. 대부분의 개발자들은 Log4j를 선호하기 때문에 Hibernate 배포판에 있는 log4j.properties 파일(etc/ 디렉토리)을 hibernate.cfg.xml 다음의 src 디렉토리에 복사합니다. 예제 설정 파일을 참고하여 더 자세한 출력을 원한다면 세팅을 바꿉니다. 기본적으로 Hibernate 기동 메시지만이 화면에 출력됩니다.

기본적인 내용을 완성되었으며 이제 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을 살펴보고 DB 상에서 INSERT를 실행합니다. 실행 전에 Session과 Transaction 처리 코드를 살펴보도록 하겠습니다.

Session은 단일 작업 단위(unit of work)입니다. 이제부터 모든 것을 단순화시키며 Hibernate Sesison과 DB 트랜잭션 사이에 일대일 관계로 생각합니다. 실질적인 내부의 트랜잭션 시스템으로부터 (이 경우 일반 JDBC이지만, JTA 실행도 가능합니다.) 코드를 보호하기 위해서 Hibernate Session 에서 사용 가능한 Transaction API를 사용합니다.

sessionFactory.getCurrentSession() 은 무슨 일을 하는건가요? 먼저 이것은 일단 SessionFactory를 코드 상으로 가지고 있다면 원하는 만큼 어느 장소에서나 호출이 가능합니다. (HibernateUtil을 통해 쉽게) getCurrentSession() 메소드는 항상 "현재" 작업 단위를 반환합니다. 이러한 메커니즘에 대한 설정을 hibernate.cfg.xml 의 "thread"로 했다는 것을 기억하십니까? 따라서, 현재 작업 단위는 어플리케이션이 실행되는 현재 자바 쓰레드 내에 있습니다. 하지만, 이것은 전체 그림이 아닙니다. 작업 단위가 시작하고 종료했을 때 범위를 고려해야만 합니다.

Session은 처음에 필요시, getCurrentSession()을 처음 호출할 때 시작됩니다. 그 다음에 현재 쓰레드에 대해 Hibernate가 영역을 지정합니다. 커밋이나 롤백을 통해 트랜잭션이 종료했을때 Hibernate는 자동으로 해당 쓰레드로부터 Session을 풀어주며 닫습니다. 만일 다시 getCurrentSession()을 호출하면 새로운 Session 객체를 얻게 되며 새로운 작업 단위를 시작할 수 있습니다. 쓰레드 영역 프로그래밍 모델은 코드에서 유연한 계층을 가능하게 하기 때문에 Hibernate를 사용하는 가장 유용한 방법입니다. (트랜잭션 선언 코드는 데이터 접근 코드와 분리될 수 있는데, 이는 이후 문서에서 설명합니다.)

작업 단위 범위와 관련되어서 Hibernate Session은 하나 혹은 몇몇 DB 오퍼레이션을 실행해야 하는가? 위의 예제는 하나의 오퍼레이션에 대해서 하나의 Session을 사용했습니다. 이는 우연의 일치이며, 예즈는 다른 방법을 보여주기에는 그리 복잡하지 않습니다. Hibernate Session의 범위는 유연하지만 모든 DB 오퍼레이션을 위한 새로운 Hibernate Session을 사용하기 위해서 어플리케이션을 설계해서는 안됩니다. 따라서 다음의 예제들에서 몇번 더 이를 본다고 하더라도 오퍼레이션당 세션은 지양해야할 패턴임을 고려하십시오. 실제 (웹) 어플리켕션은 이 문서 이후에 나타납니다.

트랜잭션 처리와 선언에 대해서 더 자세한 정보는 11장. 트랜잭션과 동시성을 참고하세요. 이전 예제에서 에러 처리와 롤백 역시 건너뜁니다.

첫번째 예제를 실행하기 위해서 Ant 빌드 파일에 다음과 같이 호출 가능한 target를 추가해야 합니다.

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

action 인자값은 target를 호출할 때 커맨드 행에 다음과 같이 세팅됩니다.

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

컴파일 후에 Hibernate가 기동되고, 설정에 따라서 화면에 로그가 남는 것을 볼 수 있을 것입니다. 마지막에서 다음과 같은 내용을 볼 수 있습니다.

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

이는 Hibernate에 의해 실행되는 INSERT이며, 물음표는 JDBC 바인드 파라미터를 나타냅니다. 인자로 바인딩되는 값을 보거나 로그의 내용을 줄이려면 log4j.properties 를 점검하세요.

이제 저장된 이벤트들을 보기 원하며, 다음과 같은 내용이 메인 메소드에 추가됩니다.

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() 메소드를 다음과 같이 추가합니다.

private List listEvents() {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    List result = session.createQuery("from Event").list();
    session.getTransaction().commit();
    return result;
}

여기에서 하는 것은 DB에 있는 모든 Event 객체를 로딩하기 위해 HQL (Hibernate Query Language) 쿼리를 사용하는 것입니다. Hibernate는 적절한 SQL을 생성하며, DB에 이르 전송하고 해당 데이터를 Event 객체 담습니다. 물론 HQL을 사용해서 좀 더 복잡한 쿼리를 만들 수 있습니다.

이제 이 모든 것들을 실행하고 테스트하기 위해서 다음과 같은 단계를 따릅니다.

DB에 무엇인가 저장하고 물론, hbm2ddl 을 사용하기 전에 DB 스키마를 생성하기 위해서 ant run -Daction=store 을 실행합니다.

hibernate.cfg.xml 파일의 property를 주석처리하여 hbm2ddl 을 비활성화시킵니다. 계속해서 단위 테스팅을 진행할 때 보통은 이 설정을 그대로 두지만, hbm2ddl의 또 다른 실행은 저장되었던 모든 것들이 삭제될 것입니다. - create 설정 세팅은 실제적으로 "SessionFactory가 생성될 때 스키마로부터 모든 테이블을 삭제하고, 그 다음에 모든 테이블을 다시 만든다." 라고 해석합니다.

만일 -Daction=list를 사용해서 Ant를 호출하면, 지금까지 저장된 이벤트들을 볼 수 있습니다. 또한 몇차례 더 store 행위를 호출할 수 있습니다.

주의 : 대부분의 처음 Hibernate 사용자들은 이 부분에서 실패하는데 Table not found 에러 메시지에 대해서 어김없이 질문을 보곤 합니다. 하지만, 위의 단계를 준수한다면 hbm2ddl 이 첫번째 실행에서 DB 스키마를 생성하고 이후 어플리케이션은 이 스키마를 사용하기 때문에 이러한 문제는 부딪히지 않을 것입니다. 맵핑이나 DB 스키마를 변경한다면 다시 한번 hbm2ddl 설정을 해주어야 합니다.


1.3 2부 - 맵핑 관계

저장 개체 클래스를 테이블로 맵핑했습니다. 이를 토대로 몇가지 클래스 관계르 추가하려고 합니다. 먼저 어플리케이션에 사람을 추가하고, 사람들이 참여한 에벤트 리스트를 저장합니다.

1.3.1 Person 클래스 맵핑

Person 클래스의 첫번째 내용은 다음과 같이 단순합니다.

package events;
public class Person {
    private Long id;
    private int age;
    private String firstname;
    private String lastname;
    public Person() {}
    // 모든 속성에 대한 Accessor 메소드와, 'id'에 대한 private setter

}

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), 집합 행위(collection behavior)입니다.

1.3.2 단방향 Set 기반 관계

Person 클래스에 여러 이벤트들을 추가합니다. 이러한 방식으로 aPerson.getEvents() 를 호출함으로써 별도의 질의를 실행하지 않고 특정 사람에 대한 이벤트들을 쉽게 접근할 수 있습니다. 자바 콜렉션인 Set을 사용하는데, 콜렉션은 중복되는 요소를 포함하지 않으며 순서는 별로 중요하지 않기 때문입니다.

단방향성이며, Set으로 구현된 다수값을 갖는 관계가 필요합니다. 자바 클래스에 다음과 같이 이를 작성하고, 맵핑합니다.

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 association)고 합니다. 따라서, 다음과 같이 Hibernate의 다대다 맵핑을 사용합니다.

<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> 가장 보편적입니다. 다대다 관계(혹은 n:m 관계)에 대해서 관계 테이블이 필요합니다. 이 테이블의 각각의 행은 하나의 person과 하나의 event 간의 연결을 나타냅니다. 테이블 명은 set 요소의 table 속성으로 설정됩니다. person에 있는 관계의 식별자 컬럼명은 <key> 요소를 사용해서 정의되며, event의 컬럼명은 <many-to-many>의 column 속성으로 설정됩니다. 또한 Hibernate에 콜렉션에 있는 객체의 클래스를 지정해야 합니다. (수정 : 참조의 콜렉션 반대 편에 있는 클래스)

따라서, 위의 맵핑에 대한 DB 스키마는 다음과 같습니다.

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

1.3.3 관계로 작업하기

EventManager에 새로운 메소드로 people과 event를 같이 엮어보겠습니다.

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();
}

하나의 Person과 하나의 Event를 로딩한 뒤에 일반적인 collection 메소드를 사용해서 collection을 수정합니다. 보는 바와 같이 update()나 save() 를 명시적으로 호출하지 않습니다. Hibernate는 자동으로 해당 collection이 수정되었고 수정할 필요가 있는지를 탐지합니다. 이것을 automatic dirty checking이라고 하며, 해당 객체의 이름이나 날짜 속성을 변경하는 것도 가능합니다. 객체들이 저장(persistent) 상태에 있는 한, 즉, 특정 Hibernate Session 영역에 있으면 (다시 말하면, 객체들이 작업 단위에서 방금 로딩되거나 저장되었을때), Hibernate는 어떠한 변화도 모니터링하고 작성되지 않은 형태의(write-behind fashion) SQL을 실행합니다. DB와 메모리 상태를 맞추는 과정을 통상 단위 작업의 끝에 발생되며 flushing 이라고 합니다. 코드에서 단위 작업은 DB 트랜잭션의 커밋(혹은 롤백)과 같이 종료됩니다. - CurrentSessionContext 클래스에 대한 thread 설정값에 의해 정의됨.

물론 서로 다른 단위 작업에 person과 event를 로딩할 수도 있습니다. 혹은 저장 상태가 아닐때 어느 한 Session의 외부의 객체를 수정할 수도 있습니다. (이전에 저장 상태이었다면 이 상태를 detached 라고 합니다.) 객체가 detached 되었을때 다음과 같이 collection을 수정하는 것조차 가능합니다.

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 상태에서 이루어지는 모든 수정은 DB에 저장될 수 있습니다. 이는 해당 엔티티 객체의 collection에 대해서 모든 변경 (추가/삭제)을 포함합니다.

이러한 개념은 현재 상태에서 많이 사용되지 않지만, 어플리케이션으로 설계할 수 있는 중요한 개념입니다. 지금까지 EventManager의 main 메소드에 새로운 행위를 추가함으로서 이러한 실습을 완료했으며 커맨드 행에서 호출합니다. 만일 person 과 event의 식별자를 필요로 한다면 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 type 이라고 부르며, 인스턴스들은 특정 개체에 의존합니다. 이러한 타입의 인스턴스들은 자신만의 식별자를 가지고 있지 않거나, 엔티티 간에 공유되지 않습니다. (두개의 person은 동일한 이름을 가진다고 하더라도 동일한 firstname 객체를 참조하지 않습니다.) 물론, value type은 JDK에 있는 것 뿐만 아니라(사실, Hibernate 어플리케이션에서 모든 JDK 클래스들은 value type으로 간주합니다.), Address나 MonetaryAmount와 같이 직접 의존성이 있는 클래스를 작성할 수도 있습니다.

또한, value type에 대한 collection을 설계할 수 있습니다. 이는 개념적으로 다른 개체에 대한 참조의 collection과는 매우 다르지만, 자바에서 거의 동일하게 보입니다.


1.3.4 값에 대한 collection

Person 개체에 value type의 객체에 대한 collection을 추가합니다. 이메일 주소를 저장하기 원하며, 따라서 사용할 타입은 String이고, collection은 다시 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 부분이며, 이는 Hibernate에게 collection이 다른 개체에 대한 참조를 포함하지는 않지만, String 타입의 element의 collection 임을 말해주고 있습니다. (소문자는 Hiberante 맵핑 타입/변형자임을 가르킵니다.) 다시 한번 set 요소의 table 속성은 collection의 테이블명을 결정합니다. key 요소는 collection 테이블의 FK 컬럼명을 정의합니다. 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   |
                                               |_____________|

사실 collection 테이블의 PK가 양쪽의 컬럼을 사용한 복합키임을 확인할 수 있습니다. 또한 사람마다 이메일이 중복될 수 없음을 의미하며, 이는 자바에서 set에 필요한 의미와 정확하게 일치합니다.

이제 이 collection에 이전에 person과 event를 연결했던 것과 같이 요소들을 추가할 수 있습니다. 자바에서의 코드는 다음과 같습니다.

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();
}

이번에는 collection을 초기화하는 fetch 쿼리를 사용하지 않았습니다. 따라서, getter 메소드에 대한 호출은 이를 초기화기 위한 부수적인 select를 유발시키며, 따라서 요소를 추가할 수 있습니다. SQL 로그를 보고 eager fatchf를 사용하여 이를 최적화해보세요.


1.3.5 양방향 관계

다음은 양방향 관계를 맵핑하려고 합니다. - person과 event 간의 관계를 자바에서 양쪽으로 관계를 맺습니다. 물론, DB 스키마는 변경되지 않으며, 여전히 다대다 다수성을 가집니다. 관계형 DB는 네트워크 프로그래밍 언어보다도 더 유연해서 방향성과 같은 것이 필요하지 않습니다. - 데이터는 어느 방향으로든 조회됩니다.

처음에, Event 클래스에 참석자의 collection을 다음과 같이 추가합니다.

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 맵핑이 있습니다. key와 many-to-many 에 있는 컬럼명이 양쪽의 맵핑 문서에서 바뀌져있음을 유의하세요. 여기서 가장 중요하게 추가된 내용은 Event의 collection 맵핑의 set 요소에 있는 inverse="true" 속성입니다.

이것이 의미하는 바는 Hibernate는 둘 간의 연결에 대한 정보를 검색할 필요가 있는 경우 반대편 - Person 클래스 - 을 취해야 한다는 것입니다. 이는 두개의 개체 간의 양방향 연결이 어떻게 생성되는지를 한번 보면 이해가 훨씬 쉬울 것입니다.


1.3.6 양방향 연결로 작업하기

먼저 Hibernate는 일반적인 자바 문법에 영향을 주지 않는다는 것을 유념하세요. 단방향 예제에서 Person과 Event 간의 연결을 어떻게 생성했나요? Event의 인스턴스를 event 참조인 Person 인스턴스의 collection에 추가했습니다. 따라서, 분명히 이러한 연결이 양방향으로 작동하기 원한다면, 다른 편에도 동일한 행위를 해주어야 합니다. - 즉, Event 에 있는 collection에 Person 참조를 추가합니다. 이러한 "양쪽에 연결을 세팅"하는 것은 절대적으로 필요하며 이를 결코 잊어버리면 안됩니다.

많은 개발자들은 방어적으로 프로그래밍하며 양쪽에 정확하게 세팅하기 위해서 예를 들어 다음의 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);
}

collection에 대한 get 과 set 메소드는 protected 임을 유의하세요. 이는 동일한 패키지의 클래스들과 하위 클래스들이 여전히 메소드를 접근할 수 있지만, 해당 collection을 직접적으로 망치는 것으로부터 보호해줍니다. 다른 편의 collection에도 동일하게 작업할 수도 있습니다.

inverse 맵핑 속성은 무엇을 의미하는가? 자바에서는 양방향 연결은 단순하게 양쪽에 정확하게 참조를 세팅하는 문제입니다. 하지만 Hibernate는 SQL INSERT와 UPDATE 구문을 정확하게 생성하는 충분한 정보(constraint violation을 피하기 위해)를 가지고 있지 못하며, 양방향 관계를 적절하게 처리하는데 어떤 도움을 필요로 합니다. 관계의 한방향성을 만들기 위해 inverse는 Hibernate에게 기본적으로 이를 무시하고, 다른 편의 거울과 같이 여기라고 전달해주는 역할을 합니다. 이것이 Hibernate가 방향성 모델에서 SQL DB 스키마로 변경이 발생되는 경우의 모든 이슈를 해결하는데 필요한 것입니다. 기억해야만 하는 규칙은 다음과 같이 단순합니다. 모든 양방향 관계는 inverse와 같이 한쪽 방향을 필요로 합니다. 일대다 관계에서는 다인 편에 위치해야 하며, 다대다 관계에서 양쪽 중에 선택할 수 있으며 차이는 없습니다.

이제 자그마한 웹 어플리케이션을 통해 이를 적용해봅시다.


1.4 3부 - EventManager 웹 어플리케이션

Hibernate 웹 어플리케이션은 독립적인 어플리케이션과 거의 유사하게 Session과 Transaction을 사용합니다. 하지만, 몇가지 공통된 패턴이 사용됩니다. 이제 EventManagerServlet 을 작성합니다. 이 서블릿은 DB에 저장된 모든 이벤트들을 조회하고, 새로운 이벤트를 입력하기 위해 HTML 형식을 제공합니다.


1.4.1 기본적인 서블릿 작성

소스 디렉토리의 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 Session이 SessionFactory 상의 getCurrentSession() 을 처음 호출함으로써 열립니다. 그 다음에 DB 트랜잭션이 시작됩니다. - 데이터를 읽든 쓰든 상관없이 트랜잭션 내부에서 발생되기 위해 모든 데이터 접근이 발생됩니다. (어플리케이션 자동 커밋 모드를 사용하지 않습니다.)

모든 DB 오퍼레이션을 위해 새로이 Hibernate Session을 사용하지 마십시오. 전체 요청을 담당하는 하나의 Hibernate Session을 사용하십시오. 현재 자바 쓰레드에 자동으로 포함되도록 getCurrentSession()을 사용하십시오.

다음은 요청에 대해 가능한 행위를 진행시키고 응답 HTML으로 보냅니다. 이제 곧 그 부분을 다룰 것입니다.

마지막으로 단위 작업은 프로세싱과 렌더링이 완료되면 마치게 됩니다. 만일 프로세싱이나 렌더링 동안 문제가 발생되면 예외가 발생되며 DB 트랜잭션은 롤백됩니다. 여기까지가 요청당 세션 패턴입니다. 모든 서블릿에 트랜잭션 선언 코드를 작성하는 대신에 서블릿 필터를 작성할 수도 있습니다. Open Session in View 라고 하는 이러한 패턴에 대한 더 자세한 정보는 Hibernate 웹사이트와 Wiki를 참고하세요. - 서블릿이 아니라 JSP에서 페이지를 렌더링하는데 고려할 때 이 패턴이 필요할 것입니다.


1.4.2 프로세싱과 렌더링


요청에 대한 처리와 페이지 렌더링 구현은 다음과 같습니다.

// Write HTML header
PrintWriter out = response.getWriter();
out.println("Event Manager");
 
 // 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("Please enter event title and date.");
     } else {
         createAndStoreEvent(eventTitle, dateFormatter.parse(eventDate));
         out.println("Added event.");
     }
 }
 
 // Print page
 printEventForm(out);
 listEvents(out, dateFormatter);
 
 // Write HTML footer
 out.println("");
out.flush();
out.close();

자바 코드와 HTML이 같이 나타나는 위와 같은 코딩 스타일은 더 복잡한 어플리케이션에서 확장되기 힘들 것입니다. - 이 문서에서는 기본적인 Hibernate 개념을 설명하고 있다는 것을 염두하세요. 코드는 HTML 헤더와 풋터를 출력합니다. 이 페이지 내에서 이벤트 입력을 위한 HTML 형식과 DB의 모든 이벤트 리스트를 출력합니다. 첫번째 메소드는 단순하고 HTML을 출력만 합니다.

private void printEventForm(PrintWriter out) {
   out.println("<h2>Add new event:</h2>");
   out.println("<form>");
   out.println("Title: <input name='eventTitle' length='50'/>
"); out.println("Date (e.g. 24.12.2009): <input name='eventDate' length='10'/>
"); 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);
}

이제 서블릿이 완성되었습니다. 서블릿으로의 요청은 단일 Session과 Transaction에서 수행됩니다. 이전의 독립적인 어플리케이션에서와 같이 Hibernate는 자동으로 해당 객체들을 실행되는 현재 쓰레드에 바인딩할 수 있습니다. 이러한 방법은 코드를 레이어시키고 원하는 방식으로 SessionFactory를 접근하는 자유를 부여합니다. 통상 더 복잡한 설계를 사용하며 데이터 접근 코드는 데이터 접근 객체로 이동시킵니다. (DAO 패턴) 더 많은 예제들은 Hibernate Wiki를 참고하세요.


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가 필요함을 유의하세요. 이는 자바 서블릿 개발 킷으로 이 라이브러리를 가지고 있지 않다면 Sun 웹 사이트에서 다운받아서 라이브러리 디렉토리에 복사하세요. 하지만, 컴파일 시에만 사용될 뿐이며, WAR 패키지에서는 제외됩니다.

컴파일과 배포를 위해 프로젝트 디렉토리에서 ant war를 호출하고 hibernate-tutorial.war 파일을 Tomcat의 webapp 디렉토리에 복사하세요. 만일 Tomcat이 설치되어 있지 않다면, 다운로드 받고 설치 가이드를 참고하세요. 하지만 위의 어플리케이션을 배포하기 위해 Tomcat 설정을 바꿀 필요는 없습니다.

일단 배포되고 Tomcat이 실행하면, http://localhost:8080/hibernate-tutorial/eventmanager로 어플리케이션을 접속하세요. 처음의 요청이 서블릿을 호출할 때 Hibernate가 기동하는지를(HibernateUtil의 정적 초기자가 호출됨) 살펴보려면 Tomcat 로그를 확인하고 예외가 발생시 세부적인 내용이 출력됩니다.

1.5 요약

이 문서는 단순한 독립 Hibernate 어플리케이션과 조그만 웹 어플리케이션을 작성하는 기초적인 내용을 담고 있습니다.

만일 Hibernate에 대해서 이미 확신감을 느꼈다면, 흥미로운 것을 찾는 주제에 대해서 참조 문서의 차례를 통해 살펴보세요. - 대부분이 질문하는 내용은 트랜잭션 처리 (11장. 트랜잭션과 동시성)와, fetch 성능 (19장. 성능 향상), 혹은 API 사용법 (10장. 객체로 작업하기) 과 쿼리 특징 (10.4 쿼리) 입니다.

더 (세부적인) 많은 문서에 대해서는 Hibernate 웹 사이트를 참고하세요.