저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

최근 수행중인 프로젝트에서 JPA의 구현체로 하이버네이트를 사용하고 있다. 매핑 설정에 따라 다르겠지만, 이런 JPA 구현체들은 Lazy 로딩을 구현하기 위해 프록시 객체를 사용하고 있는데, 이와 관련해서 한 가지 주의해야 할 것이 있다. 그것은 바로 this를 리턴하는 메서드와 관련된 것이다.


현재 수행중인 프로젝트에는 상속을 매핑한 코드가 있다. 예를 들면 다음과 같은 구조이다.


public class Content { ... 최상위 }

public class VideoContent extends Content { .... }

public class MovieContent extends VideoContent { ... }

public class BookContent extends Content { ... }


그리고, 특정 클래스과 다음과 같이 Content를 *-to-One의 형식으로 레퍼런스하고 있다.


public class Order {

    private Content content;

    public Content getContent() {

        return content;

    }

}


Order 객체를 구한 뒤에 Content 객체에 접근해야 하는 경우가 있으며, 특히 Content 타입이 아닌 실제 타입에 접근해야 할 때가 있다. 이런 경우에 아주 단순하게 생각하면 다음과 같이 Content에 자기 자신을 리턴하는 코드를 넣는 방법을 생각해 볼 수 있을 것이다.


public Content {

    public Content self() {

        return this; // 자기 자신을 리턴

    }

}


Order o = getOrder(xxx);

Content proxy = o.getContent(); // Content는 프록시

Content real = proxy.self(); // self() 메서드는 자기 자신을 리턴하므로 프록시가 아닌 실제 대상 객체???

if (real instanceof MovieContent) { // 하지만, real은 프록시 객체이므로 항상 false

    ...

}


하지만, 위와 같은 방법은 통하지 않는다. 그 이유는 프록시 객체는 대상 객체가 대상 객체 자신을 리턴하는 경우 프록시 객체를 리턴하도록 만들어지기 때문이다. 

  • proxy.self() 메서드는 대상 객체의 self() 메서드를 호출한다.
  • 대상 객체의 self() 메서드는 자기 자신을 리턴한다.
  • proxy.self() 메서드는 대상 객체가 리턴한 객체가 대상 객체와 동일한지 확인한다.
  • 동일하다면 프록시 객체 자신을 리턴한다.
그렇다면, 왜 프록시 객체가 대상 객체를 리턴하지 않고 자기 자신을 리턴하는 것일까? 그 이유는 간단하다. 대상 객체에 직접 접근하게 될 경우 ORM이 제공하는 Dirty Checking이나 Lazy Loading 등이 적용되지 않기 때문이다.

이런 이유로 실제 대상 객체에 접근해서 뭔가를 하고 싶다면, 대상 객체가 자기 자신을 리턴하도록 하지 말고, Double dispatch를 사용해야 한다. Double dispatch란 다음과 같은 것이다.
  • A 객체의 a() 메서드는 B 인터페이스를 파라미터로 갖는다.
  • B 인터페이스의 b() 메서드는 A를 파라미터로 갖는다.
  • A 객체의 a() 메서드는 파라미터로 전달받은 B 인스턴스의 b() 메서드를 호출할 때 자기 자신을 전달한다.
위 내용을 코드로 다시 살펴보면 다음과 같다.
public interface B {
    public void b(A a);
}

public class A {
    public void a(B b) {
        b.b(this);
    }
}

A aobj = new A();
B bobj = new BImpl();
aobj.a(bobj); // 내부적으로 bobj.b(a)가 호출됨

위 코드를 보면 aobj.a() 메서드를 호출하면, a() 메서드는 내부적으로 다시 파라미터로 전달받은 bobj의 b() 메서드를 호출한다. 이렇게 두 객체간의 호출이 두 번 이루어지기 때문에 이를 Double Dispatch라고 표현한다.

그럼, 요놈을 앞서 Content에 적용해 보자. 다음과 같이 변경해 주면 된다.

public interface Accessor {
    public void access(Content c);
}

public class Content {
    public void access(Accessor access) {
        accessor.access(this);
    }
}

위 코드에서 Accessor의 구현체는 Content의 프록시 객체가 아닌 실제 객체에 접근할 수 있게 된다.

Order o = getOrder(xxx);

Content proxy = o.getContent(); // Content는 프록시

proxy.access(new Accessor() {
    public void access(Content c) {
        // c는 proxy의 대상 객체인 실제 객체
        if (c instanceof MovieContent) {
            ...
        }
    }
});

Content 및 Content의 자식 클래스들은 상속 관계에 있으므로 사용하므로 Visitor 패턴을 사용하면 instanceof 연산자를 사용하지 않고 실제 타입으로 바로 접근할 수도 있어 더 보기 좋은 코드를 만들어 낼 수 있을 것이다.



Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 권남 2012.08.29 23:10 신고  댓글주소  수정/삭제  댓글쓰기

    저도 비슷한 상황에서 Visitor 패턴으로 해결한 적이 있습니다.
    하지만 이런일 자체가 안일어나게 만드는게 제일 좋을 것 같습니다.
    예전에 Visitor 패턴으로 베베꽈가면서 만든 기능을 이번 프로젝트에서는 각 엔티티가 특정 interface를 구현하게 해서 간단히 해결했던게 생각나네요.
    즉, 예제에서 보여주신 Accessor가 할 일을 각 엔티티가 인터페이스를 구현해서 알아서 하게 만든거죠.
    상속구조니까 Accessor가 할일을 인터페이스 메소드로 빼고 @Override를 계속해가면서 어쩌면 쉽게 해결 될수도 있어보입니다. 하지만 프로javascript:;젝트 실제 요구사항이 어떤지를 모르니 속단은 금물이겠죠.

    • 최범균 madvirus 2012.08.30 09:22 신고  댓글주소  수정/삭제

      상속 구조에서 하위 타입이 특정 인터페이스를 상속받는다 하더라도, 최상위 타입에 대한 프록시 객체를 사용하기 때문에, 해당 인터페이스를 사용할 수 없는 상황이었습니다.
      그렇다고 해당 인터페이스를 최상위 타입이 상속받도록 할 수도 없었습니다. 왜냐면, 최상위 타입에는 해당 인터페이스에 대한 역할을 필요가 없었기 때문이다.
      또한, 각 엔티티의 역할이 아니면서 엔티티의 타입에 따라서 다르게 동작해야 하는 것들이 있었습니다.
      그래서 위와 같이 double dispatch되는 방식의 코드를 사용하게 되었습니다.

JPA를 이용해서 구현하는 프로젝트에서 두 개의 프로퍼티를 묶어서 식별자로 사용해야 하는 경우가 생겼다. 그래서, @EmbeddedId를 이용해서 ID 필드를 지정하였다. 이 프로젝트는 Spring Data JPA를 이용해서 Repository를 자동생성하고 있는데, @EmbeddedId 부분에서 문제가 발생했다. 아래 코드는 문제를 발생시킨 리포지토리의 인터페이스이다.


public interface FollowRepository extends Repository<Follow, FollowId> {


    Page<Follow> findAll(Specification<Follow> spec, Pageable pageable);

...

}


Follow의 식별자의 타입은 FollowId 클래스이고, FollowId 클래스는 두 개의 프로퍼티를 갖고 있다. 그런데, 위 코드에서 findAll()을 실행하는 순간에 다음과 같은 쿼리가 실행되면서 문제가 발생했다.


select count((follow0_.FOLLOWING_ID, follow0_.USER_ID)) as col_0_0_ 

from FOLLOW follow0_ 

where ....


DBMS로 오라클을 사용하고 있는데, 위 count() 부분에서 쿼리 오류가 발생한 것이다. 처음엔 Spring Data JPA 문제일까 해서 커스텀 구현을 넣어 보았다.


public class FollowRepositoryImpl implements FollowRepositoryCustom {


    @PersistenceContext

    private EntityManager entityManager;


    @Override

    public Page<Follow> findAll(Specification<Follow> spec, Pageable pageable) {

        List<Follow> result = getResultList(spec, pageable);

        long total = count(spec);

        return new PageImpl<Follow>(result, pageable, total);

    }


    private List<Follow> getResultList(Specification<Follow> spec,

            Pageable pageable) {

        ...

        return query.getResultList();

    }


    private long count(Specification<Follow> spec) {

        CriteriaBuilder cb = entityManager.getCriteriaBuilder();

        CriteriaQuery<Long> c = cb.createQuery(Long.class);

        Root<Follow> root = c.from(Follow.class);

        c.select(cb.count(root));

        Predicate predicate = spec.toPredicate(root, c, cb);

        c.where(predicate);


        TypedQuery<Long> query = entityManager.createQuery(c);

        return query.getSingleResult();

    }


}


하지만, 결과는 동일했다. JPA API를 직접 사용해도 변화가 없는 걸 봐서는 Spring Data JPA의 문제는 아니였다. Spring Data JPA 1.1 버전을 사용하고 있었는데, 이 버전은 하이버네이트 3.6.9를 기준으로 하고 있어서 해당 버전의 하이버네이트를 사용했었다.


하이버네이트의 버전 문제가 아닐까해서 범위를 좀 더 좁혀서 구글링을 해 봤더니 하이버네이트에서 이런 문제가 발생하고 있는 듯 했다. (관련 문의가 포럼에 있었다.) 좀 더 파 볼까 하다가, 하이버네이트 버전을 올려보기로 결심했다. 하이버네이트 버전을 3.6.9에서 4.1.4로 올렸다.


결과는? 야호! 테스트케이스의 녹색바! 에러가 없어졌다. 처음부터 하이버네이트 버전 업부터 할 걸, 괜히 코드 작성했다. 오픈소스에서 뭔가 이상한 문제가 있으면 먼저 버전을 올려보라는 걸 다시 한 번 확인했다.


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 권남 2012.07.12 17:37 신고  댓글주소  수정/삭제  댓글쓰기

    @IdClass를 사용할때도 마찬가지 현상이 발생합니다.
    @IdClass/MySQL

  2. PKH갈휘 2012.07.16 22:22 신고  댓글주소  수정/삭제  댓글쓰기

    버전업후에 말끔해졌네요, 감사합니다.

Hibernate에서 객체 사이의 연관, 클래스 상속 관계, 복합키를 설정하는 방법에 대해서 살펴본다.

클래스 상속과 테이블 매핑

먼저 살펴볼 내용은 클래스 상속 관계를 어떻게 테이블과 매핑할 것인가에 대한 내용이다. 클래스 계층을 ORM으로 처리하는 방식은 크게 다음과 같은 세 가지가 존재한다.

  • 클래스 마다 개별적 테이블 - 가장 단순한 방법으로서, 계층도에 있는 클래스와 완전히 매핑되는 테이블을 사용한다.
  • 클래스 계층 구조에 대해 하나의 테이블 - 계층도에 있는 모든 클래스를 하나의 테이블과 매핑시킨다.
  • 하위 클래스 마다 개별적 테이블 - 클래스 계층도와 매핑되도록 테이블을 설계한다. 테이블의 외부키를 사용해서 클래스의 상속 관계를 표현한다.
이 세가지 방식을 Hibernate에서 어떻게 구현할 수 있는 지 차례대로 살펴보도록 하자.

클래스 마다 개별적 테이블 사용

먼저 클래스 마다 개별적 테이블을 사용하는 방법을 살펴보도록 하자. 이 방식을 그림으로 설명하자면 다음과 같이 표현할 수 있다.


위 그림에서 Work 클래스는 추상 클래스이며, Book 클래스와 Article 클래스는 Work 클래스를 상속받은 하위 클래스이다. 이 클래스 계층 정보를 저장할 테이블을 보면 하위 클래스와 1-1 매핑되어 있는데, 여기서 중요한 건 하위 클래스에 매핑되는 테이블은 하위 클래스의 프로퍼티 뿐만 아니라 상위 클래스의 프로퍼티까지도 매핑한다는 점이다. 예를 들어, BOOK_WORK 테이블은 Book 클래스의 프로퍼티 뿐만 아니라 Work 클래스의 프로퍼티를 저장할 수 있는 컬럼이 정의되어 있으며, ARTICLE_WORK 테이블 역시 Article 클래스와 Work 클래스의 프로퍼티를 저장할 수 있도록 되어 있다.

이 방식은 사실상 다형성을 지원하지 않게 된다. 예를 들어, 위의 매핑을 처리하기 위한 설정 파일을 살펴보자.

      <class name="Book" table="BOOK_WORK">
            ...
      </class>
      
      <class name="Article" table="ARTICLE_WORK">
            ...
      </class>

이 설정 파일의 문제는 Hibernate에서 Work 타입으로 작업을 처리할 수 없다는 점이다. 예를 들어, Work 타입인 데이터를 읽어오려면, Book에 해당하는 데이터 목록을 읽어오고 Article에 해당하는 데이터 목록을 읽어온 뒤 두 목록을 합쳐주어야 한다. 이는 SQL 쿼리를 두번 실행한다는 것을 의미한다.

또, 이 매핑 전략은 개념상의 문제를 야기하는데 서로 다른 테이블이 개념상 같은 컬럼을 공유한다는 점이다. 즉, 위 그림에서 BOOK_WORK 테이블의 BOOK_WORK_ID 컬럼과 ARTICLE_WORK 테이블의 ARTICLE_WORK_ID 컬럼은 모두 Work 객체의 식별자값을 저장하기 위해서 사용된다. 이는 두 테이블의 주요키 컬럼에 들어가는 값이 해당 테이블 내에서 뿐만 아니라 두 테이블에 대해서도 유일해야 한다는 것을 의미하며, 따라서 식별자 값의 생성이 복잡해지게 된다.

클래스 계층 구조에 대해 테이블 사용

두번째 전략은 아래 그림과 같이 테이블 하나에 계층 구조에 있는 모든 클래스의 정보를 저장하는 것이다.


위 그림에서 테이블의 PUBLISHING, PRICE, ISBN 컬럼은 Book 객체에 매핑되며, MAGAZINE 컬럼은 Article 객체에 매핑된다. 어떤 컬럼이 어떤 클래스에 매핑되는 지를 구분하기 위해서는 별도의 컬럼을 필요로 하는데, 위 그림에서 WORK_TYPE 컬럼이 이에 해당한다.

Hibernate에서는 discriminator 태그와 subclass 태그를 사용해서 어떤 컬럼이 어떤 클래스의 어떤 프로퍼티에 매핑되는 지를 표시한다. 예를 들어, 위 그림의 OR 매핑은 다음과 같이 Hibnerate에서 표현할 수 있다.

      <class name="Work"
                table="WORK_ALL"
                discriminator-value="WO">
            
            <id name="id" column="WORK_ID" type="long">
                  <generator class="native" />
            </id>
            
            <discriminator column="WORK_TYPE" type="string" />            
            <property name="author" />
            
            ...
            
            <subclass name="Book" discriminator-value="BO">
                  <property name="publishing" />
                  <property name="price" />
                  <property name="isbn" />
            </subclass>

            <subclass name="Article" discriminator-value="AR">
                  <property name="magazine" />
            </subclass>
            
      </class>

먼저 discriminator 태그에 대해서 살펴보자. discriminator 태그는 테이블에 저장된 레코드가 어떤 클래스의 데이터 인지를 구분하기 위해 사용되는 컬럼을 명시해준다. 위 코드에서는 WORK_TYPE 컬럼을 사용해서 타입을 구분하게 된다.

subclass 태그는 하위 클래스의 프로퍼티 매핑을 처리해준다. subclass 태그는 name 속성을 사용해서 하위 클래스의 이름을 전달받으며, propety 태그를 사용해서 하위 클래스의 프로퍼티가 테이블의 어떤 컬럼과 매핑되는 지를 표시한다.

class 태그와 subclass 태그는 discriminator-value 속성을 지정하고 있는데, 이 속성에서 명시한 값이 discriminator 태그에서 지정한 컬럼에 들어갈 값이 된다. 예를 들어, Book 클래스를 정의하고 있는 subclass 태그의 discriminator-value 속성의 값은 'BO'인데, Book 객체를 생성해서 WORK_ALL 테이블에 데이터를 저장할 경우 WORK_TYPE 컬럼에는 'BO'가 삽입된다. 비슷하게 Article 객체를 저장할 경우에는 'AR'이 WORK_TYPE 컬럼에 저장된다.

subclass 태그는 자식 태그로 subclass를 가질 수 있기 때문에 여러 단계의 계층도 처리할 수 있다.

하위 클래스 마다 개별적 테이블 사용

마지막으로 살펴볼 방법은 테이블의 외부키를 사용한 연관을 이용하는 것이다. 아래 그림은 클래스 계층 구조를 테이블의 외부키 참조 형태로 매핑시키는 방법을 보여주고 있다.


하위 클래스에 해당하는 테이블의 주요키는 상위 클래스에 해당하는 테이블의 주요키를 참조하게 된다. 즉, 하위 클래스에 매핑되는 테이블의 주요키는 동시에 외부키가 된다.

Hibernate는 위 그림과 같은 매핑을 처리할 수 있는 joined-subclass 태그를 제공하고 있다. 앞서 살펴본 subclass 태그와 마찬가지로 joined-subclass 태그도 부모 클래스의 매핑 정보를 담고 있는 class 태그에 포함되며, joined-subclass가 처리할 매핑 정보를 담게 된다. 아래 코드는 위 그림에서 보여준 매핑을 설정한 것이다.

      <class name="Work" table="WORK">
            <id name="id" column="WORK_ID" type="long">
                  <generator class="native" />
            </id>
            
            ...
            
            <joined-subclass name="Book" table="BOOK_WORK">
                  <key column="BOOK_WORK_ID" />                  
                  <property name="publishing" />
                  <property name="price" />
                  <property name="isbn" />
            </joined-subclass>
            
            ...
            
      </class>

joined-subclass 태그는 key 태그를 갖는데, 이 key 태그는 하위 클래스에 매핑되는 테이블의 주요키이면서 외부키인 필드를 정의하게 된다. Hibernate는 이 key 태그에서 졍의한 컬럼과 상위 클래스의 id 태그에 명시한 컬럼을 외부 조인해서 데이터를 읽어오게 된다. 하지만, 외부 조인을 한다는 데서 알 수 있듯이 높은 성능을 보장하지는 못한다.

어떤 방식을 선택할 것인가?

클래스 계층 구조와 테이블 사이의 매핑을 처리하는 세 가지 방법이 있는데 이들은 나름대로의 특징이 있다. 일반적으로는 다음과 같은 규칙을 사용해서 매핑 방식을 선택한다.

  • 클래스 마다 개별적 테이블 사용 - 계층 관계의 다형성 및 다형성을 이용한 쿼리가 필요하지 않은 경우
  • 클래스 계층 구조에 대해 테이블 사용 - 계층 관계의 다형성이 필요하고 하위 클래스가 소수의 프로퍼티만을 가진 경우
  • 하위 클래스 마다 개별적 테이블 사용 - 계층 관계의 다형성이 필요하고 하위 클래스가 다수의 프로퍼티를 가진 경우
클래스 계층 구조에 대해 테이블을 사용하는 것이 다형성을 지원하며 쿼리 성능 또한 좋기 때문에, 기본적으로 두번째 방식을 사용하는 것이 좋다. 하지만, 클래스 계층 구조를 처리한다는 것은 여전히 복잡한 처리를 요구하기 때문에 클래스 상속 방식 대신에 딜리게이션(delegation; 위임) 패턴으로 대체할 수 없는 지에 대해서 고민해볼 필요가 잇다. (딜리게이션 모델의 처리가 덜 복잡하며 더 좋은 성능을 낸다.)

클래스 사이의 연관 처리

객체 지향적으로 비즈니스 도메인 영역을 설계하면 객체들 사이에 다양한 관계가 형성된다. 두 객체가 1 대 1로 연관되는 경우도 있으며, 1 대 n, n 대 1 또는 n 대 m 으로 연관되기도 한다. Hibernate는 이러한 다양한 연관 관계를 처리해줄 수 있는데, 어떻게 이러한 연관을 매핑할 수 있는 지 살펴보도록 하자. (many-to-many 관계에 대해서는 나중에 고급 매핑과 함께 살펴볼 것이다.)

many-to-one과 one-to-many

many-to-one의 관계를 설명하기 위해서 다음과 같은 테이블 구조를 사용할 것이다.


위 그림에서 ITEM 테이블은 경매 제품 항목을, BID 테이블은 입찰 항목을 저장한다고 가정하다. 이 경우, ITEM 항목은 여러 개의 BID 항목을 갖게 될 것이다. (여러 사람이 하나의 제품에 입찰을 여러번 할 수 있다.) 따라서, 하나의 ITEM 항목에 대해 BID가 없을 수도 있고 여러 개 존재할 수도 있다. 즉, BID-ITEM의 관계는 many-to-one의 관계가 되는 것이다.

BID 테이블과 매핑될 Bid 클래스는 다음과 같이 어떤 Item 항목에 입찰했는 지 참조하기 위해 프로퍼티를 가질 수 있을 것이다.

      public class Bid {
            ...
            
            private Item item;
            
            public void setItem(Item item) {
                  this.item = item;
            }
            public Item getItem() {
                  return item;
            }
            ...
      }

BID-ITEM의 관계는 many-to-one 이므로 매핑 설정 파일에서는 다음과 같이 Bid의 매핑 정보를 표시할 수 있다.

      <class name="Bid" table="BID">
            ...
            
            <many-to-one 
                         name="item"
                         column="ITEM_ID"
                         class="Item"
                         not-null="true" />
            ...
      </class>

many-to-one의 관계를 표현하고 싶을 때에는 위 코드에서와 같이 매핑 파일에서 many-to-one 태그를 사용하면 된다. many-to-one 태그에서 사용되는 주요 속성은 위 코드에서 보듯이 세 가지인데, 각 속성은 다음과 같은 의미를 갖는다.

  • name - many-to-one의 관계를 맺는 프로퍼티의 이름(Bid 클래스의 item 프로퍼티)
  • column - many-to-one의 관계에서 one에 해당하는 테이블에 대한 외부키를 저장할 컬럼의 이름. BID 테이블의 경우 ITEM 테이블의 주요키에 대한 외부키가 ITEM_ID 컬럼에 저장된다.
  • class - many-to-one에서 one에 해당하는 클래스의 이름.
위 설정은 단방향 참조만을 처리하고 있다. Bid 객체는 getItem() 메소드를 사용해서 관련 Item 객체에 접근할 수 있는 반면에, Item 객체가 Bid 객체들에 접근하는 것에 대해서는 명시하고 있지 않다.

ITEM-BID의 관계는 BID-ITEM 관계와 정반대이므로 one-to-many의 관계가 된다. 즉, 하나의 ITEM에 대해서 여러개의 BID가 존재할 수 있는 것이다. 이를 자바 코드로 표현하면 다음과 비슷할 것이다.

      public class Item {
            ...
            private Set bids = new HashSet();
            
            public void setBids(Set bids) {
                  this.bids = bids;
            }
            
            public Set getBids() {
                  return bids;
            }
            
            public void addBid(Bid bid) {
                  bid.setItem(this);
                  bids.add(bid);
            }
            ...
      }

위 코드에서는 여러 Bid 객체를 저장하기 위해 Set 타입을 사용했는데 이는 Hibernate가 many-to-one의 관계를 지원해주는 타입 중의 하나로서, 설정 파일에서는 다음과 같이 매핑 정보를 설정할 수 있다.

      <class name="Item" table="ITEM">
            ...
            <set name="bids" table="BID">
                  <key column="ITEM_ID" />
                  <one-to-many class="Bid" />
            </set>
      </class>

위 코드에서 먼저 set 태그는 프로퍼티 타입이 Set일 때 사용된다. set 태그의 주요 속성은 다음과 같다.

  • name - 프로퍼티의 이름
  • table - 프로퍼티의 값과 관련된 테이블의 이름. 생략할 수 있다. 위의 예에서는 Item의 bids 프로퍼티에 저장되는 값이 BID 테이블과 매핑된 Bid 이므로 table 속성의 값으로 "BID"를 사용하였다.
set 태그는 두 개의 자식 태그를 갖는다. 첫번째로 key 태그는 one-to-many 관계에서 many에 해당되는 테이블이 one을 참조할 때 사용하는 외부키 컬럼을 표시한다. 위 코드에서 BID 테이블의 외부키는 ITEM_ID 이므로 "ITEM_ID"를 key 태그의 column 속성값으로 지정하였다.

두번째 자식 태그는 one-to-many 태그로서 class 속성을 사용해서 many 부분에 해당하는 클래스의 타입을 명시한다.

하지만, 위의 설정만으로는 끝나지 않는다. 지금까지 설정한 것은 단지 두 개의 단방향 연관을 맺은 것에 불과하다. 예를 들어, Item 클래스의 addBid() 메소드 코드를 보자.

      bid.setItem(item);
      bids.add(bid);

위 코드는 Item 객체와 Bid 객체를 연결해주는 클래스인데, 데이터베이스 입장에서 보면 BID 테이블의 ITEM_ID 컬럼 값을 변경해주는 기능에 해당한다. 문제는 Hibernate가 위 코드를 처리할 때 두번의 쿼리를 실행한다는 점이다. 즉, Bid 객체에 대해서 한번의 쿼리를 수행하고 또한 Item의 bids 프로퍼티가 변경된 것에 대해서 한번의 쿼리를 수행하게 된다.

이렇게 두번의 쿼리가 발생하게 되는 이유는 Item과 Bid가 각각 단방향 관계로 설정되어 있기 때문이다. 양방향 관계로 연결시켜줌으로써 이 문제를 처리할 수 있는데, 양방향 연결은 다음과 같이 set 태그에 inverse 속성값을 true로 지정해줌으로써 명시할 수 있다.

      <class name="Item" table="ITEM">
            ...
            <set name="bids" inverse="true">
                  <key column="ITEM_ID" />
                  <one-to-many class="Bid" />
            </set>
      </class>

하지만, 이것만으로는 부족하다. 문제가 되는 부분은 Item을 저장하더라도 Bid가 저장되는 것은 아니라는 것이다. 예를 들어, 다음의 코드를 보자.

      Session session = HibernateUtil.currentSession();
      tx = session.beginTransaction();
      
      Item item = new Item();
      Bid bid = new Bid();
      item.addBid(bid);
      
      session.save(item);
      
      tx.commit();

위 코드를 실행했을 때 원하는 것은 Item이 저장될 때 자동으로 새로 생성한 Bid도 저장되는 것이다. 하지만, Hibernate는 Item 객체만 저장할 뿐 Bid 객체는 저장되지 않는다. Bid 객체도 저장하도록 하려면 다음과 같이 cascade 속성을 set 태그에 명시해주어야 한다.

      <class name="Item" table="ITEM">
            ...
            <set name="bids" inverse="true" cascade="save-update">
                  <key column="ITEM_ID" />
                  <one-to-many class="Bid" />
            </set>
      </class>

위와 같이 cascade 속성의 값을 "save-update"로 지정하면 Item 객체가 저장될 때 관련 객체인 Bid도 함께 저장된다. 저장될 때 뿐만 아니라 기존에 존재하는 Item 객체에 새로운 Bid를 추가한 후, Item 객체를 update() 할 때에도 Bid가 저장된다.

one-to-many 관계에서 one에 해당하는 객체가 삭제될 때 many에 해당하는 객체들도 함께 삭제되어야 하는 경우도 있을 것이다. 예를 들어, 경매 제품 항목(Item)이 삭제된다면 경매와 관련된 입찰 항목(Bid)은 의미가 없어지며, 함께 삭제되는 것이 올바른 구조일 것이다. 이처럼 one-to-many의 관계가 부모-자식 관계인 경우에는 다음과 같이 cascade 속성의 값을 "all-delete-orphan"으로 지정하면 된다.

      <class name="Item" table="ITEM">
            ...
            <set name="bids" inverse="true" cascade="all-delete-orphan">
                  <key column="ITEM_ID" />
                  <one-to-many class="Bid" />
            </set>
      </class>

all-delete-orphan은 save-update도 포함하고 있다. cascade 속성은 퍼시스턴트 객체 사이의 연관 관계에서 중요한 기능을 수행하는데, 이에 대한 자세한 내용은 본 시리즈의 다음 글에서 살펴볼 것이다.

주요키를 이용한 one-to-one 연관

다음과 같이 one-to-one의 관계를 갖는 테이블이 있다고 해 보자.


위 테이블에서 ITEM 테이블과 ITEM_DETAIL은 1대 1의 관계를 갖고 있으며, ITEM_DETAIL의 주요키는 동시에 ITEM의 주요키에 대한 외부키이기도 하다. 이렇게 주요키를 사용하여 1대 1로 연관된 경우에는 양 객체에 대한 매핑에서 one-to-one 태그를 사용해주면 된다.

아마도 ITEM 테이블과 ITEM_DETAIL 테이블에 매핑되는 클래스인 Item과 ItemDetail은 아래와 같은 형태의 코드를 갖게 될 것이다.

      public class Item {
            private Integer id;
            private ItemDetail detail;
            
            ...
            public Integer getId() {
                  return id;
            }
            public void setId(Integer id) {
                  this.id = id;
            }
            public ItemDetail getDetail() {
                  return detail;
            }
            public void setDetail(ItemDetail detail) {
                  this.detail = detail;
            }
            ...
      }
      
      public class ItemDetail {
            private Integer id;
            private Item item;
            ...
            
            public Integer getId() {
                  return id;
            }
            public void setId(Integer id) {
                  this.id = id;
            }
            public Item getItem() {
                  return item;
            }
            public void setItem(Item item) {
                  this.item = item;
            }
            ...
      }

위 코드와 같이 주요키로 연관된 두 객체와 테이블 사이의 매핑은 아래와 같이 one-to-one 태그를 사용해서 처리할 수 있다.

      <class name="Item" table="ITEM">
            <id name="id" type="int" column="ITEM_ID" unsaved-value="null">
                  <generator class="increment" />
            </id>
            
            <one-to-one name="detail"
                        class="ItemDetail" 
                        cascade="save-update" />
            
            ...
            
      </class>

      <class name="ItemDetail" table="ITEM_DETAIL">
            <id name="id" type="int" column="ITEM_ID" unsaved-value="null">
                  <generator class="foreign">
                        <param name="property">item</param>
                  </generator>
            </id>
            
            <one-to-one name="item"
                        class="Item"
                        constrained="true"  />
      </class>

먼저, Item 클래스의 one-to-one 태그를 살펴보자. 이 one-to-one 태그는 detail 프로퍼티의 타입인 ItemDetail 클래스와 one-to-one 관계를 맺는다는 것을 보여주며, Item 객체가 새로 생성되거나 변경될 때 ItemDetail도 저장된다고 설정하였다. (cascade 속성의 값이 save-update)

두번째로, ItemDetail 클래스의 one-to-one 태그를 살펴보자. 이 one-to-one 태그는 item 프로퍼티의 타입이 Item이라는 것을 나타내며, constrained 속성의 값을 true로 지정함으로써 외부키와 연결된 프로퍼티라는 것을 명시한다.

ItemDetail 클래스 매핑의 id 태그를 보면 식별자 생성기로 foreign 을 사용하고 있는데, ItemDetail이 식별자로 사용하는 값을 외부키로부터 가져온다는 것을 의미한다. 이때 외부키를 가져올 프로퍼티는 property 파라미터로 지정한다.

외부키를 이용한 one-to-one 연관

one-to-one 매핑을 처리할 수 있는 또 한가지 방법은 외부키를 사용하는 방법이다. 앞서 살펴봤던 ITEM 테이블과 ITEM_DETAIL 테이블과의 관계를 다음과 같이 변경해보자.


위의 관계에서 ITEM_DETAIL은 자신만의 주요키를 갖고 있으며, ITEM은 외부키 ITEM_DETAIL_ID 컬럼을 사용해서 ITEM_DETAIL과 one-to-one 매핑된다. 이 관계를 객체로 표현하는 두 클래스의 코드는 앞서 살펴봤던 Item 클래스 및 ItemDetail 클래스와 완전히 동일하다. 차이점이라면 ItemDetail의 id 프로퍼티의 값은 Item의 id 프로퍼티의 값과 상관없는 고유의 값이라는 것이다.

외부키를 사용한 one-to-one의 관계에서 외부키를 갖고 있는 부분의 설정방법은 many-to-one의 many 부분의 설정방법과 동일하다. (many-to-one은 참조키를 사용할 때의 관계를 표시한다는 것을 앞서 설명하였다.) 예를 들어, (ITEM_DETAIL 테이블로의 외부키를 갖고 있는) ITEM 테이블과 Item 클래스 사이의 매핑은 다음과 같이 many-to-one 태그를 사용하여 외부키 부분을 설정할 수 있다.

      <class name="javacan.hibernate.test.Item" table="ITEM">
            <id name="id" type="int" column="ITEM_ID" unsaved-value="null">
                  <generator class="increment" />
            </id>

            <many-to-one name="detail"
                         class="javacan.hibernate.test.ItemDetail"
                         column="ITEM_DETAIL_ID"
                         cascade="save-update"
                         unique="true"  />
            ...
      </class>
      

위 코드에서 many-to-one 태그는 Item 클래스의 detail 프로퍼티가 외부키인 ITEM_DETAIL_ID와 매핑된다는 것을 나타낸다. cascade 속성값이 "save-update"인데, 이는 Item이 저장되거나 변경될 때 외부키로 연관된 ItemDetail도 저장되거나 변경된다는 것을 의미한다. 또, 하나의 ITEM은 고유의 ITEM_DETAIL_ID와 연관되므로 unique 속성의 값을 true로 지정하였다.

ItemDetail 입장에서, 하나의 ItemDetail은 하나의 Item과 연관되므로 ItemDetail의 매핑 설정은 다음과 같이 one-to-one 태그를 사용하게 된다.

      <class name="javacan.hibernate.test.ItemDetail" table="ITEM_DETAIL">
            <id name="id" type="int" column="ITEM_DETAIL_ID" unsaved-value="null">
                  <generator class="increment" />
            </id>
            
            <one-to-one name="item"
                        class="javacan.hibernate.test.Item" 
                        property-ref="detail" />
            
      </class>
      

one-to-one 태그는 property-ref 속성을 사용해서 자기 자신을 참조하는 객체가 외부키 연관으로 사용하는 프로퍼티의 이름을 입력받는다. 예를 들어, Item 클래스는 detail 프로퍼티를 사용해서 ItemDetail 클래스로의 외부키 연관을 처리하므로 ItemDetail 클래스 매핑 설정에서 one-to-one 태그의 property-ref 속성의 값을 "detail"로 주었다.

복합키의 처리

대부분의 경우 주요키는 하나의 컬럼을 사용하지만, 다수의 컬럼을 주요키로 사용하는 경우도 있다. 이런 키를 복합키(composite key)라고 부르며, Hibernate는 composite-id 태그를 사용해서 복합키를 표시할 수 있도록 하고 있다. composite-key의 사용방법은 매우 간단한데, 다음과 같이 key-property 태그를 사용해서 복합키와 관련된 프로퍼티와 컬럼을 표시해주면 된다.

      <class name="User" table="USER">
            <composite-id>
                  <key-property name="username" column="USERNAME" />
                  <key-property name="organizationId" column="ORGANIZATION_ID" />
            </composite-id>
            ...
      </class>

복합키를 별도의 클래스 표현할 수도 있을 것이다. 예를 들어, 위에서 설정한 User 클래스의 복합키를 다음과 같은 UserId 클래스로 표현한다고 해보자.

      public class UserId implements Serializable {
            private String username;
            private String organizationId;
            
            // ... get/set 메소드
            
            public boolean equals(Object o) {
                  if (this == o) return true;
                  if (o == null) return false;
                  if (!(o instanceof UserId)) return false;
                  UserId target = (UserId)o;
                  if (!target.getUsername().equals(username)) return false;
                  if (!target.getOrganizationId().equals(organizationId)) return false;
                  return true;
            }
            
            // hashCode() 메소드
      }

위와 같이 복합키를 저장하는 별도의 클래스가 존재할 경우 User 클래스는 다음과 같이 복합키 클래스를 저장할 별도의 프로퍼티를 갖게 될 것이다.

      public class User {
            private UserId id;
            
            public void setId(UserId id) {
                  this.id = id;
            }
            public UserId getId() {
                  return id;
            }
            ...
      }

이 경우 다음가 같이 composite-id 태그를 사용할 수 있다.

      <class name="user" table="USER">
            <composite-id name="id" class="UserId">
                  <key-property name="username" column="USERNAME" />
                  <key-property name="organizationId" column="ORGANIZATION_ID" />
            </composite-id>
      </class>

다음 글에서는

본 글에서는 클래스 사이의 상속 및 one-to-many, many-to-one, one-to-one 연관을 처리하는 방법에 대해서 살펴봤고, 복합키를 설정하는 방법에 대해서도 살펴보았다. 이번 글을 통해 다양한 종류의 매핑을 처리할 수 있게 되었을 것이다. 이렇게 매핑 설정한 뒤 필요한 것은 실제로 퍼시스턴트 객체를 생성하고, 변경하고, 삭제하는 등의 작업을 어플리케이션에서 수행하는 것이다. 다음 글에서는 퍼시스턴트 객체를 어떻게 사용하는 지 그리고 객체의 영속성 상태 및 영속성 전이에 대한 내용을 살펴보기로 하자.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

Hibernate의 OR 매핑에서 사용될 자바 객체에 대해서 살펴본다.

퍼시스턴트 클래스(Persistent Class) 작성 규칙

도메인 영역의 개체(entity)를 표현하는 데 가장 많이 사용되는 자바 클래스는 자바빈의 형태를 취한다. 대부분의 도메인 영역의 개체는 테이터베이스의 테이블에 지속적으로 저장되며 이렇게 지속적으로 저장되는 데이터를 표현하기 위해 사용되는 클래스를 퍼시스턴트 클래스(Persistent Class)라고 부른다. (영속적 클래스, 지속성 클래스 등의 해석한 용어가 어색해서 그냥 퍼시스턴트 클래스라고 부르겠다.)

퍼시스턴트 클래스를 Hibernate에서 사용가능하게 하려면 다음과 같은 규칙을 따라야 한다.

  • 퍼시스턴트 필드에 대한 접근/수정 메소드를 제공해야 한다.
  • 기본 생성자를 제공해야 한다.
  • ID 프로퍼티를 제공해야 한다. (선택사항)
  • non-final 클래스로 작성하는 것이 좋다. (선택사항)
퍼시스턴트 필드에 대한 접근/수정 메소드

퍼시스턴트 클래스는 DB 테이블에서 읽어온 값을 저장하기 위한 멤버 필드가 정의되어 있다. Hibernate는 퍼시스턴트 클래스의 멤버 필드에 직접 접근하기 보다는, 자바빈 스타일의 메소드를 통해서 멤버 필드에 간접적으로 접근한다. 즉, 자바빈 프로퍼티를 사용해서 DB 테이블의 값을 퍼시스턴트 클래스에 저장해준다.

자바빈 프로퍼티의 명명규칙을 따르기 때문에 set/get/is를 메소드 이름에 붙여서 사용하면 된다. 예를 들어, getId, setId, getName, setName 등의 이름을 사용한다.

기본 생성자를 제공해야 한다.

Hibernate는 Constructor.newInstance()를 사용해서 퍼시스턴트 클래스의 인스턴스를 생성하기 때문에, 인자가 없는 기본 생성자를 제공해야 한다. 예를 들어, 다음과 같이 기본 생성자를 제공하지 않는 클래스는 Hibernate에서 사용할 수 없다.

public class InvalidPersistentClass {

    private String name;
    ...
    
    /* 기본 생성자가 없음!! 아래와 같은 기본 생성자를 추가해주어야 Hibernate에서 사용가능 public InvalidPersistentClass() { } /*    
    public InvalidPersistentClass(String name) {
        ...
    }
    
    public String getName() {
        return name;
    }
    ...
}

자바에서는 어떤 생성자도 제공하지 않으면 기본 생성자를 자동으로 추가해준다. 따라서, 위 코드와 같이 생성자를 포함한 클래스는 기본 생성자를 추가해주어야 Hibernate에 적용할 수 있지만, 아래와 같이 어떤 생성자도 포함하지 않으면 기본 생성자가 자동으로 추가되므로 Hibernate에서 사용할 수 있다.

public class Product {
    private String id;
    private String name;
    
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    ...
}

식별자 프로퍼티를 제공해야 한다. (선택사항)

퍼시스턴트 클래스는 DB 테이블의 Primary Key 필드와 매핑되는 프로퍼티를 제공하는 것이 좋다. 이 프로퍼티의 타입은 기본 데이터 타입일 수도 있고, Integer나 Long과 같은 래퍼 타입일 수도 있고, String이나 Date 같은 일반 자바 객체일 수도 있다. (필드가 여러개로 구성된 복합 키이거나 사용자 정의 타입을 사용할 수도 있는데 이에 대한 대한 내용은 본 시리즈의 4회 글인 OR 매핑 부분에서 살펴볼 것이다.)

식별자 프로퍼티는 선택사항이다. 식별자 프로퍼티를 정의하지 않고, Hibernate가 내부적으로 객체의 ID를 추적하도록 할 수 있다. 하지만, 가급적이면 식별자 프로퍼티를 제공하는 것이 퍼시스턴트 클래스의 좋은 설계 방식이다.

식별자 프로퍼티의 경우 기본 데이터 타입보다는 클래스 타입을 사용하는 것이 좋다. 예를 들어, PK가 정수타입인 경우 다음과 같이 두 가지 형태로 식별자 프로퍼티를 정의할 수 있을 것이다.

public class Some {
    private int id;
    
    public int getId() {
        return id;
    }
    ...
}

public class Any {
    private Integer id;
    
    public Integer getId() {
        return id;
    }
    ...
}

여기서 Some은 값을 DB 테이블에 삽입하기 위해 새로운 객체를 생성하면 자동으로 id 프로퍼티에 0이라는 값이 할당된다. (int 타입의 기본값은 0이다.) 따라서, id 프로퍼티의 값을 올바르게 할당하지 않으면 0 값이 DB 테이블에 삽입될 수도 있다. 하지만, Any의 경우는 새로운 객체를 생성하더라도 id 프로퍼티의 값은 null이다. PK 필드에는 null값이 들어갈 수 없으므로, 실수로 id 프로퍼티의 값을 할당하지 않을 경우 잘못된 값이 DB 테이블에 삽입되는 것이 아니라 에러가 발생하게 된다. 따라서, 식별자 프로퍼티의 경우는 오류 방지를 위해 null 값을 가질 수 있는 타입으로 지정하는 것이 좋다.

non-final 클래스로 작성하는 것이 좋다. (선택사항)

Hibernate는 프록시 기능을 제공하는데, 이 기능을 사용하려면 퍼시스턴트 클래스가 final 클래스가 아니거나, public 메소드를 선언한 인터페이스를 구현해야 한다. final 클래스를 Hibernate에 적용할 수는 있지만, public 메소드를 선언한 인터페이스를 구현하지 않은 final 클래스의 경우 프록시 기능을 사용할 수 없게 된다.

equals()/hashCode() 메소드 오버라이딩 & 라이프사이클 콜백

equals() 메소드와 hashCode() 메소드의 구현

만약 퍼시스턴트 클래스들을 객체들이 섞인다면 equals() 메소드와 hashCode() 메소드를 알맞게 오버라이딩 해 주는 것이 좋다. (예를 들어, 퍼시스턴트 클래스 객체를 Set이나 Map 같은 곳에 저장할 경우)

테이블의 같은 레코드에 매핑된 두 객체가 서로 다른 Session에서 로딩된 경우를 생각해보자. 비록 두 객체가 갖는 값은 같더라도, 기본 equals() 메소드는 == 연산자를 사용해서 두 객체를 비교하기 때문에 두 객체는 서로 다른 객체로 판단된다. 따라서, 값이 같을 때 equals() 메소드가 true를 리턴하도록 하려면 equals() 메소드를 알맞게 구현해주어야 한다.

equals() 메소드를 구현하는 가장 확실한 방법은 객체의 각각의 프로퍼티가 동일한지 비교하는 것이다. 하지만, DB 테이블이 Primary Key 필드(또는 Unique 필드)로 각각의 레코드를 구분하므로, Primary Key 필드(또는 Unique 필드)에 해당하는 프로퍼티만 비교하면 효율적으로 비교를 할 수 있다.

라이프사이클 콜백: Lifecycle 인터페이스

새로운 객체를 DB 테이블에 저장할 때, DB 테이블로부터 데이터를 읽어와 객체에 저장할 때, 값을 변경할 때, DB 테이블에서 데이터를 삭제할 때 별도의 처리가 필요할 때가 있을 것이다. (예를 들어, 객체를 삭제하기 전에 객체에 의존하고 있는 또 다른 객체가 존재할 경우 객체 삭제가 안 되도록 하고 싶은 경우가 있다.)

이렇게 CRUD 작업이 처리될 때 값을 검증한다거나 별도의 처리를 하고 싶을 경우, 퍼시스턴트 클래스는 net.sf.hibernate.Lifecycle 인터페이스를 구현해주어야 한다. Lifecycle 인터페이스는 아래와 같이 네 개의 메소드를 선언하고 있다.

public interface Lifycycle {
    public boolean onSave(Session s) throws CallbackException;
    public boolean onUpdate(Session s) throws CallbackException;
    public boolean onDelete(Session s) throws CallbackException;
    public void onLoad(Session s, Serializable id) throws CallbackException;
}

각 메소드는 아래와 같은 기능을 제공한다.

  • onSave - 객체가 저장되기(삽입되기) 전에 호출된다.
  • onUpdate - 객체의 수정 결과가 테이블에 반영되기 전에(즉, Session.update() 메소드에 객체가 전달될 때) 호출된다.
  • onDelete - 객체가 삭제되기 전에 호출된다.
  • onLoad - 객체를 로딩한 이후에 호출된다.
onSave, onUpdate, onDelete 메소드는 boolean 타입을 리턴하는데, 리턴값에 따라서 다음과 같이 처리된다.

  • true - 작업을 거부한다. 예를 들어, onSave() 메소드가 true를 리턴하면 값이 저장되지 않는다.
  • false - 작업을 수행한다. 예를 들어, onSave() 메소드가 true를 리턴하면 값이 저장된다.
예를 들어, 아래와 같이 퍼시스턴트 클래스를 작성할 수 있을 것이다.

public class Product implements Lifecycle {
    ...
    private int stock; // 재고 개수
    
    ...
    
    public boolean onSave(Session s) throws CallbackException {
        if (stock < 0) return true;
        return false;
    }
    public boolean onUpdate(Session s) throws CallbackException {
        return false;
    }
    public boolean onDelete(Session s) throws CallbackException {
        // 재고가 남아 있는 경우 삭제 불가.
        if (stock > 0) throw CallbackException("stock > 0:" + stock);
    }
    public void onLoad(Session s, Serializable id) throws CallbackException {
    }
}

onSave, onUpdate, onDelete 메소드가 true를 리턴할 경우, 작업을 거부하기 때문에 DB 테이블에 객체값이 반영되지 않는다. 하지만, Hibernate를 사용하는 어플리케이션 코드에서는 DB 테이블에 반영되지 않은 사실을 알지 못한다.

    // Cat이 Lifecycle 인터페이스를 구현했다고 가정
    Product product = ...;
    
    tx = hbSession.beginTransaction();
    hbSession.save(product); // Product.onSave() 메소드가 true를 리턴하더라도 예외는 발생하지 않음
    tx.commit();

만약 Lifecycle 메소드 처리시에 CRUD 작업이 처리되지 않았다는 사실을 어플리케이션 코드가 알아야 한다면 true를 리턴하지 말고 CallbackException을 발생시켜야 한다. 앞서 예로 보여준 Product 클래스의 onDelete() 메소드는 stock 필드 값이 0보다 큰 경우 CallbackException 예외를 발생시키며, 이 경우 아래와 같이 어플리케이션에서 try-catch 블럭을 사용해서 CRUD 작업이 수행되지 않았음을 확인할 수 있게 된다.

    Product product = ...;
    tx = hbSession.beginTransaction();
    try {
        hbSession.delete(product); 
    } catch(CallbackException ex) {
        // onDelete()에서 예외 발생
    }
    tx.commit();

다음 글에서는

본 장에서는 퍼시스턴트 클래스를 작성하는 방법을 살펴봤는데, 퍼시스턴트 클래스의 목적은 DB 테이블과 매핑되어 테이블의 각 레코드를 메모리상에서 표현하는 것이다. 따라서, 퍼시스턴트 클래스와 DB 테이블이 어떻게 서로 연결되는 지에 대한 정보를 표시할 방법이 필요하며 이 방법에 대해서는 다음 글에서 살펴보도록 하겠다.

Posted by 최범균 madvirus

댓글을 달아 주세요

Hibernate API에서 사용될 세션 및 트랜잭션 프로퍼티를 설정하는 방법에 대해서 살펴본다.

Hibernate의 세션 설정 방법 및 프로퍼티 설정 방법

Hibernate API를 사용하는 기본 코드는 아래와 같은 형태를 띄고 있다.

    // 1. 초기화 메소드에서 SessionFactory를 초기화한다.
    Configuration cfg = new Configuration();
    cfg.configure();
    SessionFactory sessionFactory = cfg.buildSessionFactory();
    
    // 2. 실제 메소드에서는 sessionFactory에서 Session을 생성해서 사용한다.
    Session s = sessionFactory.openSession();
    ... // Session을 사용해서 CRUD 실행
    s.close();

코드는 크게

  1. OR 매핑 정보 및 커넥션 프로퍼티 정보를 담고 있는 Configuration을 생성하고, Configuration으로부터 SessionFactory를 생성하는 부분과
  2. SessionFactory로부터 Session을 구해 CRUD 작업을 처리하는 부분
이렇게 두 부분을 구성된다. 첫번째 부분은 Hibernate Session에 대한 어플리케이션의 초기화 과정에서 실행될 것이고, 두번째 부분은 어플리케이션의 로직을 구현하는 과정에서 사용될 것이다. 본 글에서는 첫번째 부분, 즉 OR 매핑 정보 및 커넥션 프로퍼티 정보를 담고 있는 Configuration을 생성하는 방법에 대해서 살펴볼 것이다.

Hibernate Session 생성 방법

Session은 데이터베이스 세션을 의미하는 것으로서, Session이 사용할 데이터베이스 커넥션을 구하는 방법은 다음과 같이 3가지가 존재한다.

  • JNDI로부터 커넥션 DataSource를 받아서 Session에 전달하기
  • Hibernate가 제공하는 커넥션 풀을 사용하기
  • 직접 생성한 커넥션을 Session에 전달하기
JNDI로부터 DataSource를 받거나 Hibernate가 제공하는 커넥션 풀을 사용하는 것이 일반적인 방법이다. 경우에 따라서 직접 커넥션을 생성해서 Hibernate Session에 제공해주어야 하는 경우가 있지만 비추천, 어쩔 수 없는 경우가 아니라면 직접 커넥션을 생성해서 제공하지 않기 바란다.

프로퍼티 정보 명시하기

커넥션 정보는 프로퍼티를 통해서 명시할 수 있는데, 프로퍼티는 4가지 방법으로 지정할 수 있다. Hibernate는 다음의 순서대로 프로퍼티 정보를 읽어온다.

  1. Configuration.setProperties() 메소드에 전달된 java.util.Properties에 저장된 프로퍼티 정보
  2. 클래스패스 루트에 위치한 hibernate.properties 파일에 저장된 프로퍼티 정보
  3. java -Dproperty=value를 사용해서 전달된 시스템 프로퍼티
  4. hibernate.cfg.xml 파일에 <property> 태그로 명시한 프로퍼티 정보
본 시리즈의 첫번째 글인 'Hibernate를 이용한 ORM 1 - 퀵 스타트'에서 사용한 방법은 hibernate.cfg.xml 파일을 사용한 네번째 방법이었다. 필자의 경우는 두번째와 네번째 방법을 선호하는데, 자신만의 설정 시스템에 Hibernate 설정 정보를 담고 싶다면 첫번째 방법을 사용하면 된다.

위의 네 가지 방법 중에서 두번째와 네번째는 파일을 클래스패스 루트에 위치시키면 다음의 코드를 통해서 Hibernate가 설정 파일로부터 프로퍼티를 읽어온다.

    Configuration cfg = new Configuration().configure();
    // 또는 아래와 같이 두줄로 실행
    // Configuration cfg = new Configuration();
    // cfg.configure();

하지만, 첫번째 방법을 사용하고 싶다면 아래와 같이 직접 코드에서 Properties 객체를 생성한 뒤 Configuration 객체에 전달해주어야 한다.

    Properties prop = new Properties();
    prop.setProperty(...);
    ...
    Configuration cfg = new Configuration();
    cfg.setProperties(prop);

네 가지 방법 중 어떤 방법으로 사용하더라도 Hibernate가 사용하는 프로퍼티의 이름과 값의 의미는 동일하므로, 본 글에서는 XML 파일 포맷인 hibernate.cfg.xml 파일에 프로퍼티 정보를 명시하는 방법을 사용해서 커넥션 정보를 명시하는 형태로 예제를 보여줄 것이다.

Sessin 설정 방법: JNDI 설정/커넥션 풀/직접 커넥션 제공

앞서 말했듯이 Hibernate Session이 사용할 데이터베이스 커넥션을 JNDI나 Hibernate의 커넥션 풀로부터 얻어오거나 또는 사용자가 직접 제공한 커넥션을 사용할 수도 있다.

JNDI 설정

Hibernate가 JNDI에 바인딩된 DataSource로부터 데이터베이스 커네션을 구하도록 설정하려면 다음과 같은 프로퍼티를 사용하면 된다.

프로퍼티 이름
hibernate.connection.datasource DataSource의 JNDI 이름
hibernate.jndi.url JNDI 제공자의 URL (옵션)
hibernate.jndi.class JNDI의 InitialContextFactory의 클래스 (옵션)
hibernate.connection.username 데이터베이스 사용자 (옵션)
hibernate.connection.password 데이터베이스 사용자 암호 (옵션)

예를 들어, JNDI에 등록된 DataSource의 이름이 "hibernate" 라면 다음과 같이 connection.datasource 프로퍼티 값을 설정해주면 된다. (위 표에 있는 나머지 프로퍼티들은 대부분의 WAS에서 설정하지 않아도 문제가 되지 않는다.)

    <?xml version="1.0" encoding="euc-kr" ?>
    
    <!DOCTYPE hibernate-configuration
        PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
               "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">
    
    <hibernate-configuration>
        <session-factory>
            <property name="connection.datasource">hibernate</property>
            <property name="hibernate.transaction.factory_class">
                net.sf.hibernate.transaction.JTATransactionFactory
            </property>
            <property name="hibernate.trasaction.manager_lookup_class">
                net.sf.hibernate.transaction.WeblogicTransactionManagerLookup
            </property>
            <property name="dialect">net.sf.hibernate.dialect.Oracle9Dialect</property>
            
            <mapping resource="Cat.hbm.xml" />
        </session-factory>
    </hibernate-configuration>

위 프로퍼티에서 DataSource외에 나머지 프로퍼티에 대해서 'SQL Dialect와 트랜잭션 전략 설정' 부분에서 설명할 것이므로 아직까진 신경쓰지 말기 바란다.

간단하게 톰캣의 예를 들어, 데이터 소스 설정 예제를 살펴보도록 하겠다. 참고로, 여기서 보여주는 예제는 지난 1회 글 'Hibernate를 이용한 ORM 1 - 퀵 스타트'에서 사용한 예제에서 설정 파일부분만 변경한 것이다.

먼저 톰캣에 웹 어플리케이션에서 사용할 DataSource에 대한 JNDI 설정을 추가해야 한다. 이 설정은 [톰캣]/conf/server.xml 파일에 추가하면 되며, 아래와 같은 형태의 코드를 사용하면 된다. (톰캣에서 DataSource를 추가하는 방법에 대한 자세한 내용은 톰캣 사이트인 http://jakarta.apache.org/tomcat/을 참고하기 바란다.)

파일 [톰캣]/config/server.xml에 추가할 내용
    <Host ...>
        ...
        <Context path="/hibernate" docBase="hibernate">
            <Resource name="jdbc/hibernate" auth="Container" 
                         scope="Shareable" type="javax.sql.DataSource" />
            <ResourceParams name="jdbc/hibernate">
                <parameter>
                    <name>factory</name>
                    <value>org.apache.commons.dbcp.BasicDataSourceFactory</value>
                </parameter>
                <parameter>
                    <name>url</name>
                    <value>jdbc:oracle:thin:@localhost:1521:ORA</value>
                </parameter>
                <parameter>
                    <name>driverClassName</name>
                    <value>oracle.jdbc.driver.OracleDriver</value>
                </parameter>
                <parameter>
                    <name>username</name>
                    <value>scott</value>
                </parameter>
                <parameter>
                    <name>password</name>
                    <value>tiger</value>
                </parameter>
                <parameter>
                    <name>maxWait</name>
                    <value>3000</value>
                </parameter>
                <parameter>
                    <name>maxIdle</name>
                    <value>100</value>
                </parameter>
                <parameter>
                    <name>maxActive</name>
                    <value>10</value>
                </parameter>
            </ResourceParams>
        </Context>
        
        ...
    </Host>

위 코드에서는 JNDI에 등록될 DataSource의 이름을 "jdbc/hibernate"로 지정하였는데, 이는 실제로 "java:comp/env/" 콘텍스트에 추가되므로 설정한 DataSource의 완전한 JNDI 이름은 "java:comp/env/jdbc/hibernate"가 된다. 따라서, Hibernate의 설정 파일에 아래와 같이 hibernate.connection.datasource 프로퍼티의 값을 주면 된다.

파일경로: [톰캣]/webapps/hibernate/WEB-INF/classes/hibernate.cfg.xml
<?xml version="1.0" encoding="euc-kr" ?>

<!DOCTYPE hibernate-configuration
    PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
           "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">

<hibernate-configuration>
    <session-factory>
        <property name="connection.datasource">java:comp/env/jdbc/hibernate</property>
        <property name="dialect">net.sf.hibernate.dialect.Oracle9Dialect</property>
        
        <mapping resource="Cat.hbm.xml" />
    </session-factory>
</hibernate-configuration>

어플리케이션 서버(톰캣)에 DataSource를 JNDI에 등록하고, JNDI를 사용하도록 Hibernate의 프로퍼티를 설정하면 모든 게 완료된다. 소스 코드는 전혀 변경할 필요가 없이 이 두가지만 변경해주면 1회에 작성했던 예제를 실행할 수 있을 것이다.

커넥션 풀 설정

Hibernate는 자체적으로 커넥션 풀 기능을 제공하고 있는데, Hibernate Session이 커넥션 풀로부터 필요한 데이터베이스 커넥션을 구하도록 설정할 수 있다. 기본적인 설정 방법은 다음과 같다.

<?xml version="1.0" encoding="euc-kr" ?>

<!DOCTYPE hibernate-configuration
    PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
           "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">

<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">JDBC 드라이버 클래스</property>
        <property name="hibernate.connection.url">JDBC URL</property>
        <property name="hibernate.connection.username">DB User</property>
        <property name="hibernate.connection.password">DB User Password;/property>
        <property name="hibernate.connection.pool_size">DB User Password;/property>
        
        ...
        
    </session-factory>
</hibernate-configuration>

위의 설정 내용을 보면 알겠지만, Hibernate Session이 Hibernate가 제공하는 커넥션 풀을 사용하도록 설정하려면 다음의 네 가지 기본 프로퍼티를 요구한다.

  • hibernate.connection.driver_class - DB 연결시 사용할 JDBC 드라이버 클래스
  • hibernate.connection.url - JDBC URL
  • hibernate.connection.username - DB 사용자 이름
  • hibernate.connection.password - DB 사용자 암호
  • hibernate.connection.pool_size - 커넥션 풀의 최대 크기 명시. (선택)
위의 프로퍼티를 설정하면 Hibernate는 자체적으로 제공하는 커넥션 풀을 사용하게 된다. 하지만, Hibernate에서 제공하는 커넥션 풀은 그다지 좋지 못하며, Hibernate 레퍼런스 문서를 보면 Hibernate의 배포판과 함께 제공하는 C3P0, DBCP 또는 Proxool 라이브러리를 사용하도록 권고하고 있다. 위의 다섯가지 속성 중에서 hibernate.connection.pool_size 대신에 DBCP, C3P0 또는 Proxool 관련 프로퍼티를 설정하면 Hibernate는 자동으로 이들 라이브러리를 커넥션 풀로 사용한다. 본 글에서는 DBCP에 대한 내용만 살펴보며, 나머지 C3P0와 Proxool에 대해서는 살펴보지 않겠다. C3P0와 Proxool를 설정하기 위한 프로퍼티는 Hibernate 홈페이지 및 각 라이브러리의 홈페이지를 참고하기 바란다.

DBCP 관련 프로퍼티

  • DBCP를 실행하는 데 필요한 jar 파일을 복사한다. 이 파일들은 hibernate 배포판의 lib/ 폴더에 포함되어 있다. 웹 어플리케이션이라면 DBCP 관련 jar 파일들을 WEB-INF/lib 폴더에 복사해주면된다.
    commons-dbcp-1.2.1.jar, commons-collections-2.1.1.jar, commons-pool-1.2.jar
  • DBCP 관련 프로퍼티를 설정한다.
DBCP 관련 프로퍼티에는 다음과 같은 것들이 있다.

프로퍼티 이름
hibernate.dbcp.maxActive 한번에 풀에서 갖다 쓸 수 있는 최대 커넥션 개수
hibernate.dbcp.whenExhaustedAction 풀에서 더이상 가져올 수 있는 커넥션이 없을 때 어떻게 처리할지를 명시한다. 값이 1이면, 풀에서 가져올 커넥션이 생길 때 까지 블럭킹된다. 값이 2이면, 풀은 새로운 커넥션을 생성해서 리턴한다. 값이 0이면, 에러를 발생시킨다. 기본값은 1이다.
hibernate.dbcp.maxWait hibernate.dbcp.whenExhaustedAction 프로퍼티의 값이 1인 경우, 블럭킹될 시간을 1/1000초 단위로 지정한다. 0보다 작으면 무한히 대기한다.
hibernate.dbcp.maxIdle 사용되지 않고 풀에 저장될 수 있는 최대 커넥션 개수
hibernate.dbcp.validationQuery 커넥션을 가져올 때 커넥션의 유효성 여부를 검사할 쿼리를 입력한다. 예를 들어, 오라클인 경우 select count(*) from dual과 같은 쿼리를 사용할 수 있다.
hibernate.dbcp.testOnBorrow true이면 풀에서 커넥션을 가져올 때 커넥션이 유효한지 검사한다.
hibernate.dbcp.testOnReturn true이면 커넥션을 풀에 반환할 때 커넥션이 유효한지 검사한다.

위의 모든 프로퍼티를 지정할 필요는 없으며, hibernate.dbcp. 로 시작하는 프로퍼티가 존재하면 Hibernate는 DBCP를 커넥션 풀 모듈로 사용하게 된다.

커넥션 Session에 직접 전달

앞서 두 가지 방법, 즉 JNDI를 사용하는 방법과 커넥션 풀을 사용하는 방법은 Hibernate의 프로퍼티로 설정할 수 있었다. 그런데 부득이한 이유로 JNDI나 커넥션 풀을 사용할 수 없는 경우도 있을 것이다. 예를 들어, 기존 커넥션 관련 모듈이 있어서 Hibernate도 이 모듈에서 제공하는 커넥션을 사용해야 하는 경우가 발생할 수 있다.

이런 경우에는 커넥션 관련된 Hibernate 프로퍼티에서 설정할 값이 없으며, 다음과 같이 SessionFactory.openSession(Connection conn) 메소드를 호출해서 Session 객체를 생성하면 된다.

    SessionFactory sessionFactory = ....;
    
    Connectoin conn = ...;
    
    Session session = sessionFactory.openSession(conn);
    ...
    session.close();
    conn.close();

직접 커넥션을 Session에 전달할 때 주의할 점은, 하나의 JDBC 커넥션에 대해서 동시에 두 개의 열린 Session을 생성해서는 안 된다는 점이다. 즉, 다음과 같은 코드를 작성하지 않도록 주의해야 한다.

    Connectoin conn = ...;
    
    Session session1 = sessionFactory.openSession(conn);
    Session session2 = sessionFactory.openSession(conn); // 같은 커넥션으로 Session 생성
    ...
    session1.close();
    session2.close();
    ...
    
    conn.close();

SQL Dialect와 트랜잭션 속성/처리 클래스 설정

Hibernate는 DBMS에 따라서 최적화된 기능을 제공할 수 있으며, 또한 사용할 어플리케이션 서버에 알맞은 트랜잭션 기능을 제공할 수 있다. DBMS에 최적화된 기능을 제공하기 위해 사용되는 것이 SQL Dialect이며, hibernate.trasaction.manager_lookup_class 프로퍼티를 통해서 트랜잭션 처리 클래스를 설정할 수 있다.

SQL Dialect

Hibernate는 특정 DBMS가 제공하는 기능을 사용할 수 있는 기능을 제공하며, Dialect 프로퍼티를 사용해서 이 기능을 사용할 수 있다. 예를 들어, Hibernate는 오라클를 사용할 때는 오라클에 최적화되도록 기능을 수행하며 MySQL을 사용하는 경우에는 MySQL에 최적화되도록 기능을 수행할 수 있다. 예를 들어, 오라클에 알맞은 Dialect를 설정하면, Hibernate는 시퀀스와 같이 DBMS에 특징적인 기능을 개발자가 직접 코딩하지 않고도 사용할 수 있게 된다.

hibernate.dialect 프로퍼티 값을 명시하면 SQL Dialect를 설정할 수 있는데, 각 DBMS에 따라 다음 표와 같이 프로퍼티값을 명시하면 된다.

DBMS 프로퍼티값
DB2 net.sf.hibernate.dialect.DB2Dialect
DB2 AS/400 net.sf.hibernate.dialect.DB2400Dialect
DB2 OS390 net.sf.hibernate.dialect.DB239Dialect
PostgreSQL net.sf.hibernate.dialect.PostgreSQLDialect
MySQL net.sf.hibernate.dialect.MySQLDialect
Oracle (모든 버전) net.sf.hibernate.dialect.OracleDialect
Oracle 9/10g net.sf.hibernate.dialect.Oracle9Dialect
Sybase net.sf.hibernate.dialect.SybaseDialect
Sybase Anywhere net.sf.hibernate.dialect.SybaseAnywhereDialect
Microsoft SQL Server net.sf.hibernate.dialect.SQLServerDialect
SAP DB net.sf.hibernate.dialect.SAPDBDialect
Informix net.sf.hibernate.dialect.InformixDialect
HypersonicSQL net.sf.hibernate.dialect.HSQLDialect
Ingres net.sf.hibernate.dialect.IngresDialect
Progress net.sf.hibernate.dialect.ProgressDialect
Mckoi SQL net.sf.hibernate.dialect.MckoiDialect
Interbase net.sf.hibernate.dialect.InterbaseDialect
Pointbase net.sf.hibernate.dialect.PointbaseDialect
FrontBase net.sf.hibernate.dialect.FrontbaseDialect
Firebird net.sf.hibernate.dialect.FirebirdDialect

트랜잭션 속성 및 처리 클래스 설정

Hibernate의 Transaction 객체를 사용하기 위해서는 hibernate.transaction.factory_class 프로퍼티 속성을 설정해야 한다. 이 속성을 통해서 사용할 Transaction 구현체를 명시하게 된다. Hibernate는 기본적으로 두 가지 Transaction 구현체를 제공하고 있으며, 이 두 구현체는 다음과 같다.

  • net.sf.hibernate.transaction.JDBCTransactionFactory
    JDBC 트랜잭션에 트랜잭션 처리를 위임한다.
  • net.sf.hibernate.transaction.JTATransactionFactory
    JTA에 처리를 위임한다. (기존의 트랜잭션이 존재하면 그 트랜잭션을 따르고, 존재하지 않을 경우 Session은 새로운 트랜잭션을 수행한다.
JTATransactionFactory 구현체를 사용할 때, JTATransactionFactory가 어플리케이션 서버로부터 JTA UserTransaction을 구하기 위해 사용할 JNDI 이름은 jta.UserTransaction 프로퍼티를 사용해서 명시할 수 있다.

JTA 환경에서 변경가능한 데이터를 JVM 레벨에서 캐싱하기 위해서는, JTA TransactionManager를 구하기 위한 전력을 명시해야 한다. JTA TransactionManager를 구할 클래스를 명시할 때에는 hibernate.transaction.manager_lookup_class 프로퍼티의 값을 WAS에 따라 알맞게 명시하면 된다. 아래표는 WAS에 따라 사용할 프로퍼티 값 목록이다.

WAS 프로퍼티값
JBoss net.sf.hibernate.transaction.JBossTransactionManagerLookup
Weblogic net.sf.hibernate.transaction.WeblogicTransactionManagerLookup
WebSphere net.sf.hibernate.transaction.WebSphereTransactionManagerLookup
Orion net.sf.hibernate.transaction.OrionTransactionManagerLookup
Resin net.sf.hibernate.transaction.ResinTransactionManagerLookup
JOTM net.sf.hibernate.transaction.JOTMTransactionManagerLookup
JOnAS net.sf.hibernate.transaction.JOnASTransactionManagerLookup
JRun4 net.sf.hibernate.transaction.JRun4TransactionManagerLookup
Borland ES net.sf.hibernate.transaction.BESTransactionManagerLookup

SessionFactory JNDI 등록

hibernate.session_factory_name 프로퍼티를 사용해서 JNDI 이름공간에 SessionFactory를 등록할 수 있다. hibernate.session_factory_name 프로퍼티의 값은 SessionFactory를 JNDI에 등록할 때 사용될 이름이 된다. 예를 들어, java:comp/env/hibernate/SessionFactory와 같은 이름을 사용하게 된다.

hibernate.session_factory_name 프로퍼티를 생략하면 SessionFactory를 JNDI에 등록하지 않는다. Tomcat과 같이 읽기전용 JNDI 구현체를 사용하는 서버에서는 hibernate.session_factory_name 프로퍼티 값을 설정하지 않는 것이 좋다.

hibernate.jndi.url 프로퍼티와 hibernate.jndi.class 프로터리를 사용해서 SessionFactory를 JNDI에 등록할 때 사용할 InitialContext를 명시할 수 있다. 이 속성값을 명시하지 않으면 기본 InitialContext가 사용된다.

다음 글에서는

이번 2회에서는 Hibernate가 사용할 데이터베이스 세션을 설정하는 세 가지 방법, 즉 JNDI를 이용하는 방법, 커넥션 풀을 사용하는 방법, 그리고 직접 코드에서 커넥션을 제공하는 방법에 대해서 살펴보았다. 그리고 DBMS의 기능을 활용할 수 있는 SQL Dialect 설정 방법과 WAS 서버에 따라 달라지는 JNDI 트랜잭션 속성 및 처리 클래스의 설정 방법에 대해서도 알아보았다.

다음 3회 글에서는 OR 매핑의 한 부분인 Object, 즉, Hibernate가 데이터를 메모리 상에서 표현할 자바 퍼시스턴트 클래스의 작성 방법에 대해서 살펴볼 것이다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 전재형 2015.09.09 10:39 신고  댓글주소  수정/삭제  댓글쓰기

    하이버네이트 세션처리을 찾는 중. 들렀습니다.

    너무 잘 정리된 내용이라. 블러그에 두고 보고 싶어요^^

Hibernate API를 사용하여 OR 매핑을 처리하는 예제를 작성해봄으로써 일단 Hibernate에 발을 담군다.

OR 매핑과 Hibernate API 사용 준비

OR 매핑은 객체와 관계형 DB 테이블 사이의 매핑을 뜻하는 것으로서, 객체의 데이터를 DB 테이블의 레코드로 또는 DB 테이블의 레코드를 객체의 데이터로 이동시켜주는 기법을 의미한다. 웹 어플리케이션 개발시 작성하는 코드의 핵심 부분은 거의 95% 이상이 DB 테이블과의 CRUD 작업과 연관되어 있기 때문에, OR 매핑 도구를 알맞게 사용하면 개발 효율성을 높일 수 있게 된다.(예를 들어, select 쿼리를 작성하고 테스트 하는 소비되는 시간이 없어지고, 대신 도메인 로직을 처리하는 코드를 작성하는 데 더 많은 시간을 투자할 수 있다.)

현재 Sun의 JDO, 자카르타의 OJB, Hibernate 등 다양한 OR 매핑 API가 존재한다.이 중에서, Hibernate API는 기존에 작성한 도메인 영역의 엔티티 객체를 그대로 사용할 수 있기 때문에 기존 코드를 작성하기가 쉬우며, OR 매핑 정보를 설정 파일로 관리하기 때문에 OR 매핑 설정이 쉽다. 또한, SELECT 쿼리 수행시 다양한 검색 조건을 제공하며, DBMS에 특징적인 기능도 지원하고 있다. 또한, 현재 많은 개발자들이 애용하고 있는 API이기도 한다. 이런 이유로 필자는 Hibernate API의 사용하는 방법에 대해서 7~8회에 걸쳐서 살펴보도록 하겠다.

Hibernate API를 사용하기 위한 준비

본 글에서는 Hibernate API를 사용하는 웹 어플리케이션을 작성해볼 것이다. 톰캣 5.0.xx 버전과 JDK 1.4.2 버전을 사용해서 웹 어플리케이션을 실행하였다. 웹 어플리케이션을 위한 폴더를 아래와 같이 생성하였다. (WEB-INF 폴더에 들어갈 web.xml 파일은 알맞게 작성하기 바란다.) 하단에 있는 관련 자료에서 예제 코드를 직접 다운로드 받아서 사용할 수 있다.

  - C:jakarta-tomcat-5.0.28
      - webapps
          - hibernate
              - WEB-INF
                  - lib
                  - classes

위와 같이 웹 어플리케이션 폴더를 작성했다면, 아래의 순서에 따라 Hibernate API를 사용하면 된다.

  1. Hibernate API를 다운로드 한다.
  2. 필요한 jar 파일을 복사한다.
  3. 매핑될 자바 클래스 파일을 작성한다.
  4. 객체의 데이터를 저장할 테이블을 생성한다.
  5. 자바 객체와 테이블 사이의 매핑 정보 파일을 작성한다.
  6. 자바 코드에서 Hibernate Session을 이용하여 OR 매핑을 처리한다.
먼저, Hibernate는 Hibernate 홈페이지인 http://www.hibernate.org 에서 다운로드 받을 수 있다. 현재 production 버전은 2.1.7c이며, development 상태의 버전은 3.0beta1이다. 본 글에서는 2.1.7c 버전을 사용할 것이다.

다운로드 받은 hibernate-2.1.7c.zip 파일의 압축을 풀면 여러 가지 jar 파일이 존재하는데, 이들 jar 파일 중에서 다음의 파일들을 WEB-INFlib 폴더에 복사한다.

  • hibernate2.jar
  • lib/cglib-full-2.0.2.jar
  • lib/commons-collections-2.1.1.jar
  • lib/commons-logging-1.0.4.jar
  • lib/dom4j-1.4.jar
  • lib/ehcache-0.9.jar
  • lib/jta.jar
  • lib/odmg-3.0.jar
  • JDBC 드라이버
JDBC 드라이버는 독자가 테스트를 위해 사용할 DBMS에 알맞은 걸 사용하면 된다. (본 글에서는 오라클9i에 해당하는 classes12.jar 파일을 사용해서 테스트할 것이다.)

필요한 jar 파일을 복사했다면 Hibernate API를 사용하기 위한 준비가 끝난 것이다.

매핑될 자바 클래스 작성과 테이블 생성

OR 매핑의 핵심 개체는 메모리 상에 존재할 자바 객체와 물리적으로 존재할 DB 테이블이다. Hibernate에 발을 담구는 단계이므로 간단한 객체와 테이블을 사용해볼 것이다. (참고로 여기서 사용하는 자바 클래스와 테이블은 Hibernate 레퍼런스의 퀵스타트에서 사용한 것들이다.)

매핑될 자바 클래스

테이블과 매핑될 자바 클래스는 다음과 같다.

package javacan.hibernate.test;

/**
 * Cat 클래스
 * 
 * @author 최범균
 */
public class Cat {
    private String id;
    private String name;
    private char sex;
    private float weight;
    
    public Cat() {
    }
    
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public char getSex() {
        return sex;
    }
    public void setSex(char sex) {
        this.sex = sex;
    }
    public float getWeight() {
        return weight;
    }
    public void setWeight(float weight) {
        this.weight = weight;
    }
}

위 코드를 보면 알겠지만 OR 매핑에서 테이블과 매핑될 객체를 위한 클래스는 Hibernate API에 대해서 전혀 연관되어 있지 않다.

테이블 생성

앞서 작성한 Cat 객체의 데이터를 저장할 테이블의 스키마는 아래와 같다.

컬럼명 컬럼타입 NOT NULL 인덱스
CAT_ID CHAR(32) NOT NULL PK
NAME VARCHAR(16) NOT NULL  
SEX CHAR(1)    
WEIGHT REAL    

빠른 이해를 위해 Cat 클래스의 프로퍼티와 CAT 테이블의 필드가 1대 1로 매핑되도록 하였다.

매핑 파일 및 Hibernate 세션 설정 파일 작성

매핑될 자바 객체와 DB 테이블이 준비되었다면, 그 다음으로 해야 할 작업은 매핑 설정 파일을 작성하는 것이다. 앞서 작성한 Cat 클래스와 CAT 테이블에 대한 매핑 정보를 담고 있는 매핑 설정 파일은 아래와 같다.

파일경로:/WEB-INF/classes/Cat.hbm.xml<?xml version="1.0" encoding="euc-kr" ?>

<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

<hibernate-mapping>
    <class name="javacan.hibernate.test.Cat" table="CAT">
        <id name="id" type="string" unsaved-value="null">
            <column name="CAT_ID" sql-type="char(32)" not-null="true" />
            <generator class="uuid.hex" />
        </id>
        
        <property name="name">
            <column name="NAME" length="16" not-null="true" />
        </property>
        
        <property name="sex" />
        
        <property name="weight" />
    </class>
</hibernate-mapping>

OR 매핑 파일을 작성하는 방법에 대해서는 본 시리즈의 4회 글에서 살펴볼 것이며, 본 글에서는 위 설정 파일의 각 태그에 대해서 자세히 설명하지는 않겠다. 간단하게 각 태그 및 속성들은 간단히 설명하면 아래와 같다.

  1. <class> 태그 - OR 매핑에서 사용될 클래스와 테이블을 명시한다. name 속성에는 매핑될 클래스의 완전한 이름을, table 속성에는 매핑딜 DB 테이블의 이름을 명시한다.
  2. <id> 태그 - DB 테이블의 주요키와 매핑되는 객체의 프로퍼티를 설정한다. name 속성은 프로퍼티 이름을, type은 프로퍼티의 타입을 나타낸다.
  3. <property> 태그 - 객체에서 DB 테이블과 매핑될 프로퍼티의 이름을 명시한다.
  4. <column> 태그 - <id> 태그나 <property> 태그로 지정한 객체의 프로퍼티와 매핑되는 DB 테이블의 필드에 대한 정보를 지정한다. name 속성은 필드명을, sql-type은 SQL 타입을 not-null은 필드가 null 값을 허용하는지의 여부를 명시한다.
  5. <generator> 태그 - 키값 생성기를 나타낸다. 객체를 생성하면 <id> 태그에 해당하는 DB 테이블의 필드인 PK 필드에 키값을 삽입해야 하는데, 이때 <generator> 태그가 생성한 값을 키값으로 사용한다. 위 예제코드에서는 32자리의 HEX 코드를 만들어내는 생성기를 사용한다.
<property> 태그가 <column> 태그를 자식으로 갖지 않을 경우에는 프로퍼티 이름과 동일한 이름의 테이블 필드를 참조하게 된다. 예를 들어, weight 프로퍼티의 값을 저장하는 DB 테이블의 필드는 weight가 되는 것이다.

매핑 설정 파일을 작성했다면 그 다음으로 해야 할 일은 Hibernate의 세션 설정 파일을 작성하는 것이다. 세션 설정 파일에서는 다음과 같은 것들을 명시하게 된다.

  • DB 커넥션과 관련된 프로퍼티 지정
  • 세션이 사용할 OR 매핑 설정 파일 정보
설정 파일의 형태는 간단한데, 본 글에서 예젤 사용한 세션 설정 파일은 다음과 같다.

파일경로: /WEB-INF/classes/hibernate.cfg.xml
<?xml version="1.0" encoding="euc-kr" ?>

<!DOCTYPE hibernate-configuration
    PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
           "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">

<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">oracle.jdbc.driver.OracleDriver</property>
        <property name="hibernate.connection.url">jdbc:oracle:thin:@localhost:1521:ORA</property>
        <property name="hibernate.connection.username">scott</property>
        <property name="hibernate.connection.password">tiger</property>
        
        <property name="dialect">net.sf.hibernate.dialect.Oracle9Dialect</property>
        
        <mapping resource="Cat.hbm.xml" />
    </session-factory>
</hibernate-configuration>

위 파일에서 <property> 태그는 DB 세션에 대한 설정을, <mapping> 태그는 이 세션과 관련된 매핑 설정 파일을 명시한다.

위 예제 파일은 Hibernate가 제공하는 커넥션 풀을 사용하도록 설정하고 있는데, 이 외에도 CP30이나 DBCP와 같은 다른 커넥션풀을 사용할 수 있으며, 또한 어플리케이션 서버(WAS)가 제공하는 JNDI로부터 커넥션을 구할 수도 있다. 심지어 프로그램에서 직접 세션에 커넥션을 전달할 수도 있는데 이에 대해서는 다음 글에서 살펴보도록 하겠다.

Hibernate Session을 사용한 OR 매핑 처리 코드 작성

Hibernate 세션 설정 파일 및 OR 매핑 설정 파일을 작성했다면 이제 남은 일은 Hibernate가 제공하는 OR 매핑 기능을 사용해서 어플리케이션을 개발하는 것이다.

세션을 구하기 위한 유틸리티 클래스 작성

Hibernate가 제공하는 세션을 구하는 코드는 아래와 같은 형태를 지닌다.

SessionFactory sessionFactory = null;
sessionFactory = new Configuration().configure().buildSessionFactory();

Session s = sessionFactory.openSession(); // Session을 통해서 CRUD 작업을 수행하게 된다.

...

s.close();

위 코드에서 Configuration.configure() 메소드를 호출하는 부분이 있는데, 이 코드가 실행되면서 설정 파일로부터 세션 정보를 읽어오게 된다.

SessionFactory는 초기화를 위해서 한번만 생성하면 되므로 보통은 아래와 같은 형태의 유틸리티 클래스를 작성해서 Hibernate 세션을 사용한다.

package javacan.hibernate.test;

import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.SessionFactory;
import net.sf.hibernate.cfg.Configuration;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Hibernate의 Session을 open/close 해주는 유틸리티 클래스.
 * 
 * @author 최범균
 */
public class HibernateUtil {
    private static Log log = LogFactory.getLog(HibernateUtil.class);
    
    private static final SessionFactory sessionFactory;
    
    static {
        try {
            sessionFactory = new Configuration().configure().buildSessionFactory();
        } catch(Throwable ex) {
            log.error("Initial SessionFactory creation failed.", ex);
            throw new ExceptionInInitializerError(ex);
        }
    }
    
    public static final ThreadLocal local = new ThreadLocal();
    
    public static Session currentSession() throws HibernateException {
        Session s = (Session) local.get();
        if (s == null) {
            s = sessionFactory.openSession();
            local.set(s);
        }
        return s;
    }
    public static void closeSession() throws HibernateException {
        Session s = (Session) local.get();
        local.set(null);
        if (s != null) {
            s.close();
        }
    }
}

위와 같이 유틸리티 클래스를 만들면 Hibernate 세션을 구하는 코드는 아래와 같이 간단해진다.

Session s = HibernateUtil.currentSession();

...

HibernateUtil.closeSession();

Hibernate를 이용한 CRUD 처리

Hibernate 세션을 구해주는 유틸리티 클래스도 작성했으므로, Hibernate를 이용해서 CRUD 처리를 어떻게 처리하는 지 대략적으로 살펴보도록 하자. 먼저 Hibernate를 사용하는 전형적인 코드는 아래와 같다.

    Session hbSession = null;
    Transaction tx = null;
    
    try {
        hbSession = HibernateUtil.currentSession();
        tx = hbSession.beginTransaction();
        
        // Hibernate 세션을 이용한 CRUD 처리 코드
        
        tx.commit();
    } catch(Exception ex) {
        if (tx != null) tx.rollback();
    } finally {
        HibernateUtil.closeSession();
    }

Transaction은 트랜잭션을 처리해주며, tx.commit()을 호출하면 트랜잭션이 완료되고, tx.rollback()을 호출하면 트랜잭션이 롤백된다. 트랜잭션은 Session.beginTransaction() 메소드를 호출하면 시작된다.

hbSession은 트랜잭션 처리를 위한 save(), update(), delete() 등의 메소드를 제공한다. 예를 들어, 앞서 작성했던 Cat 클래스의 객체를 생성한 뒤 프로퍼티 값을 알맞게 지정하고 DB 테이블에 저장하는 코드는 아래와 같이 save() 메소드를 사용해서 작성할 수 있다.

관련코드: /makeCat.jsp    
Session hbSession = null;
Transaction tx = null;

try {
    hbSession = HibernateUtil.currentSession();
    tx = hbSession.beginTransaction();
    
    Cat cat = new Cat();
    cat.setName("나비");
    cat.setSex('F');
    cat.setWeight(2.0f);
    
    hbSession.save(cat);    
    tx.commit();
} catch(Exception ex) {
    if (tx != null) tx.rollback();
} finally {
    HibernateUtil.closeSession();
}

만약 위의 코드를 JDBC API 만으로 작성한다면 아래와 같은 형태를 취할 것이다.

    Cat cat = new Cat();
    cat.setId(생성한ID값);
    ...

    Connection conn = null;
    PreparedStatement pstmt = null;
    
    try {
        conn = DButil.getConnection();
        pstmt = conn.prepareStatement("insert into cat values (?, ?, ?, ?)");
        pstmt.setString(1, cat.setId());
        ...
        pstmt.setFloat(4, cat.getWeight());
        pstmt.executeUpdate();
        
    } finally {
        if (pstmt != null) try { pstmt.close(); } catch(SQLException ex) {}
        if (conn != null) try { conn.close(); } catch(SQLException ex) {}
    }

앞서 Hibernate를 사용한 코드와 위의 JDBC를 사용한 코드를 비교해보면 Hibernate를 사용한 코드가 훨씬 이해가 빠르며, 코드도 짧은 것을 알수가 있다. 처리해야 할 필드의 개수가 많으면 많아질 수록 코딩량의 차이는 더욱 늘어날 것이다.

테이블에서 데이터를 읽어오는 부분은 가장 기본적인 형태는 아래와 같다.

관련코드: /listCat.jsp
    Session hbSession = null;
    Transaction tx = null;
    
    try {
        hbSession = HibernateUtil.currentSession();
        tx = hbSession.beginTransaction();
        
        Query query = hbSession.createQuery("select c from Cat as c where c.sex = :sex");        query.setCharacter("sex", request.getParameter("sex").charAt(0));
        for (Iterator iter = query.iterate() ; iter.hasNext() ; ) {
            Cat c = (Cat) iter.next();
        }
        tx.commit();
    } catch(Exception ex) {
        if (tx != null) tx.rollback();
    } finally {
        HibernateUtil.closeSession();
    }

Query 객체는 SQL 쿼리와 비슷한 HQL 정보를 담고 있는 객체로서, 위 코드에서는 select 쿼리를 위한 정보를 담고 있다. 위 코드에서 사용된 select 쿼리는 일반적인 SQL 쿼리와 비슷하지만 다른 형태를 취하고 있는 것을 알 수 있는데, 이 쿼리는 Hibernate가 자체적으로 제공하는 HQL이다. HQL은 SQL과 비슷하기 때문에 쉽게 배울 수 있는데, 이에 대해서는 본 시리즈의 6번째 글에서 살펴볼 것이다.

예제 실행 방법

글 하단에 위치한 관련 링크에서 본 글에서 사용한 예제 코드를 다운로드 받을 수 있다. 예제 파일은 hibernate.war인데, 이 파일을 톰캣이나 레신과 같은 웹 콘테이너에 알맞게 복사한다. 톰캣의 경우 [톰캣]/webapps 폴더에 복사한다. 알맞게 복사한 뒤 실행을 하면 hibernate.war 파일이 자동으로 배치될 것이다.

압축이 풀리면 /WEB-INF/classes/hibernate.cfg.xml 파일을 열어서 아래의 부분을 테스트하려는 DBMS에 알맞게 수정한다.

    <property name="hibernate.connection.driver_class">oracle.jdbc.driver.OracleDriver</property>
    <property name="hibernate.connection.url">jdbc:oracle:thin:@localhost:1521:ORA</property>
    <property name="hibernate.connection.username">scott</property>
    <property name="hibernate.connection.password">tiger</property>

윗 부분을 알맞게 수정하고, DBMS에 앞서 보여줬던 CAT 테이블을 생성하고, DBMS에 알맞은 JDBC 드라이버를 /WEB-INF/lib 폴더에 복사한 뒤, WAS를 다시 시작하면 예제를 실행할 수 있다.

예제는 다음과 같은 3개의 JSP 파일이 있다.

  • makeForm.jsp - 고양이 정보를 입력하는 폼.
  • makeCat.jsp - makeForm.jsp로부터 전달받은 고양이 정보를 Cat 객체에 저장한 뒤, Hibernate를 사용해서 CAT 테이블에 저장
  • listCat.jsp - Hibernate를 사용해서 CAT 테이블에 저장된 정보를 읽어와 Cat 객체에 저장한 뒤 출력
makeForm.jsp를 실행해서 고양이 정보를 입력한 뒤, listCat.jsp를 통해서 DB 테이블에 저장되는 지의 여부를 확인할 수 있을 것이다.

다음 글에서는

이번 1회에서는 Hibernate가 무엇인지 감을 잡을 수 있도록 전체적인 사용 순서를 정리해보았다. 이번 글을 통해서 Hibernate가 무엇이며 어떤식으로 이용되는 지를 대략적으로 이해했을 것이라 생각한다. 다음 글에서는 Hibernate가 DB와 통신할 때 사용되는 Session에 대한 설정 방법에 대해서 살펴볼 것이다.

관련링크:

 

Posted by 최범균 madvirus

댓글을 달아 주세요