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

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

최근 진행하는 프로젝트는 DB 관련 기술로 JPA를 사용하고 있는데, 연동 대상은 레거시 DB이다. 레기서 DB답게 복합키를 갖는 테이블이 다수 존재한다. 아래는 한 예이다.



이 테이블은 특정 업무의 점검 결과를 저장할 때 사용하는데(간결함을 위해 이름을 변경하고 칼럼수도 줄였다), CHECK_H는 점검 결과에 대한 마스터 테이블이고, CHECK_D는 각 세부 점검 항목의 결과를 저장하는 테이블이다. CHECK_H와 CHECK_D는 1:N의 관계를 갖는다.


그림에서 보는 것처럼 CHECK_H의 PK는 네 개의 칼럼을 구성되어 있으며, CHECK_D는 이 네 칼럼을 참조한다.


검사 항목은 20여개 정도 되는데, 각 항목을 그룹으로 나눠서 표현한다. 각 항목이 속한 그룹을 저장하는 칼럼이 GROUP이고 검사 항목을 저장하는 칼럼이 ITEM이며, 그 결과를 저장한 칼럼이 RSLT이다. 화면에 결과를 표시할 때에는 GROUP과 ITEM을 오름차순으로 정렬해서 출력한다.


CHECK_H와 CHECK_D는 개념적으로 하나의 검사 결과를 의미하므로, 이 둘과 매핑되는 모델은 하나의 애그리거트에 포함된다. 매핑할 애그리거트를 다음과 같이 만들었다.



CHECK_D는 별도 라이프사이클이 없고 CHECK_H에 종속되어 있으므로 CHECK_D에 매핑되는 Detail을 밸류로 표현했고, Check를 애그리거트의 루트로 했다. 복합키를 사용하므로 복합키에 해당하는 식별자 클래스인 CheckId도 따로 만들었다.


Detail 클래스와 CHECK_D 테이블의 매핑 설정


Detail은 밸류이므로 CHECK_D의 주요키와 매핑할 필요가 없으므로 밸류가 가져야 할 속성만 정의했다.


@Embeddable

public class Detail {

    private String group;

    private String item;

    private String rslt;


    ...

}


Check 클래스와 CHECK_H 테이블의 매핑 설정


복합키를 위한 CheckId는 다음과 같다.


@Embeddable

public class CheckId implements Serializable {

    

    @Column(name = "JOIN_NUM")

    private String joinNum;


    @Column(name = "PATH_FLAG")

    private String pathFlag;


    @Column(name = "JOIN_YMD")

    private String joinYmd;


    @Column(name = "RSLT_FLAG")

    private String rsltFlag;

}


다음은 Check 클래스 설정이다.


@Entity

@Table(name = "CHECK_H")

public class Check {

    @EmbeddedId

    private CheckId id;


    @Column(name = "FROM_TIME")

    private String fromTime;


    @Column(name = "TO_TIME")

    private String toTime;


    @ElementCollection(fetch = FetchType.EAGER)

    @CollectionTable(name = "CHECK_D", joinColumns = {

            @JoinColumn(name = "JOIN_NUM", referencedColumnName = "JOIN_NUM"),

            @JoinColumn(name = "PATH_FLAG", referencedColumnName = "PATH_FLAG"),

            @JoinColumn(name = "JOIN_YMD", referencedColumnName = "JOIN_YMD"),

            @JoinColumn(name = "RSLT_FLAG", referencedColumnName = "RSLT_FLAG") })

    @org.hibernate.annotations.OrderBy(clause = "GROUP asc, ITEM asc")

    private Set<CheckDetail> details = new LinkedHashSet<>();



@CollectionTable의 joinColumns 속성을 사용해서 CHECK_D에서 CHECK_H를 참조할 때 사용하는 조인 칼럼을 지정했다. Check가 필요한 기능에서 CheckDetail도 함께 사용하기에 @ElementCollection의 fetch 속성을 EAGER로 설정했다. 화면에서 GROUP과 ITEM을 오름차순 기준으로 정렬해서 보여주기 때문에, 하이버네이트의 @OrderBy 애노테이션을 사용해서 값을 정렬했다.



Posted by 최범균 madvirus

댓글을 달아 주세요

발표 자료 첨부합니다.


DDD구현기초@JCOConf13_2p.pdf




Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 마음이 뛰다 2013.02.23 21:20 신고  댓글주소  수정/삭제  댓글쓰기

    오늘 세션 너무 잘 들었습니다~!
    예전에 뭣 모르고 JPA 쓸 때 당최 개념이 그려지지가 않았는데,
    오늘 큰 배움을 얻고 갑니다~ :D

  2. 알려주세요 2013.02.24 07:02 신고  댓글주소  수정/삭제  댓글쓰기

    DDD(Domain Driven Development)관련 자료를 찾고있었는데 자료 감사합니다. Domain Driven Design로 개발하는걸 Domain Driven Development라고 하는건가요? 제가 잘몰라서 ㅜㅜ 알려주세요

  3. devSejong 2013.02.25 16:24 신고  댓글주소  수정/삭제  댓글쓰기

    JCO 재미있게 잘 들었습니다. 세미나 자주자주 해주세요.^^

  4. 일퍼센트 2013.03.07 16:05 신고  댓글주소  수정/삭제  댓글쓰기

    자료 잘 보고 갑니다.~

최근 수행중인 프로젝트에서 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되는 방식의 코드를 사용하게 되었습니다.

ORM!! 오늘 한번 더 ORM을 추종할 수 밖에 없는 일이 벌어졌다. 경험한 내용이 흥분도 되고 ORM의 좋음을 공유하고자 이렇게 글을 남긴다.


최초의 설계: 별도 클래스, 별도 테이블


현재 진행하는 프로젝트에서 컨텐츠에 대한 모델을 설계할 때 컨텐츠 종류마다 별도 테이블과 별도 클래스로 구성하도록 설계를 진행했었다. 각 컨텐츠들이 비슷한 데이터를 갖고 있었지만, 절반은 다른 데이터를 갖고 있었다. 또한, 컨텐츠 타입은 두 개였고 컨텐츠마다 사용되는 영역이 달랐기 때문에, 상위 클래스에 공통 정보를 두지 않고 서로 계층 관계에 묶이지 않는 별도 클래스로 구성하였다. 물론, 테이블도 별도로 구성하였다.




변화의 압박: 서로 다른 컨텐츠 종류에 공통으로 적용되는 기능들


최초 개발하는 동안에는 크게 문제될 것이 없었다. 그런데, PoC 프로젝트를 거의 마무리하는 과정에서 1.0 버전의 기획으로 다음의 기능들이 추가되었다.

  • 모든 컨텐츠에 대해 댓글 달기
  • 모든 컨텐츠에 대해 좋아요 하기
  • 모든 컨텐츠에 대해 즐겨찾기 하기
  • 모든 컨텐츠에 연관 정보 넣기
  • 새로운 종류의 컨텐츠 타입 추가 및 새로운 타입 컨텐츠에도 댓글/좋아요/연관 정보 넣기
고민에 휩싸이기 시작했다. 예를 들어, 댓글 구현은 다음과 같이 두 가지 중 하나로 할 수 있다.
  • 각 컨텐츠 타입마다 댓글을 위한 테이블을 구분해서 만들고, 각 컨텐츠 타입별로 댓글 관련 인터페이스 묶음 구현하기.
  • 댓글 테이블에 컨텐츠 타입 보관 위한 컬럼을 추가하고, 한 묶음의 인터페이스로 각 컨텐츠 타입을 위한 댓글 기능 구현하기.

구현이야 할 수 있겠지만, 둘 다 딱히 마음에 안 들었는다. 첫 번째 방법은 완전히 동일한 데이터 구조, 구현 코드, 테이블이 중복된다는 점이 불만이었다.



두 번째 방법은 하나의 신규 테이블에서 서로 다른 테이블에 대한 참조를 가져야 할 수도 있다는 것이 거슬렸다. 예를 들어, 두 컨텐츠에 동일하게 추가되는 정보인 경우, 두 컨텐츠에 대한 외부키를 각각 갖고 있어야 했다.



또한, 두 방법 모두 2.0 버전에 새로운 컨텐츠 타입이 추가되거나 각 컨텐츠 타입에 공통으로 적용되는 기능이 추가되면 같은 짓을 반복해야 하는 동일한 문제점을 갖고 있었다.


그래서, 뭐가 문제일까 하고 잠시 고민해 본 결과, 모델링을 잘못 했다는 결론에 다다랐다. PoC 시점에서는 그리 잘못 되지 않았지만, 몇 달 후에 신규 기능들이 추가되면서  최초에 만들었던 모델로는 깔끔하게 처리할 수 없게 된 것이다. 이 모든 문제를 깔끔하게 해결하는 방법은 모델을 다시 정리하는 것 뿐이었다.


상속으로 풀기로 결심!


이 모든 문제의 근원은 추상화의 변경에서 비롯되었다. 최초에는 두 개의 컨텐츠를 논리적인 하나의 컨텐츠로 바라보지 않았다. 그런데, 댓글, 연관 정보, 좋아요 기능이 생기면서 개별 컨텐츠 타입이 아닌 '컨텐츠'라는 개념이 도출된 것이다. 따라서, 컨텐츠라는 새로운 상위 개념을 도출했고, 이 상위 개념에 대해서 '좋아요', '댓글' 등의 기능을 적용하기로 하였다. 새로운 모델은 다음과 같이 변경되었다.



컨텐츠라는 단일 개념을 표현하기 위해 Content를 출현시켰고, 댓글과 좋아요 등의 기능은 새롭게 추가한 Content에 대고 구현을 하도록 했다. 각 개별 타입들의 변이는 Content의 하위 타입으로 처리하였다. 그리고, 각 컨텐츠 타입마다 별도의 테이블을 가졌던 것에서 모든 컨텐츠 타입을 한 개의 테이블에 저장하기로 결정했다.


변경의 여파는?


모델을 새롭게 정의했으니, 이제 기존 코드에 변경할 차례이다. 시간은 대략 얼마나 걸렸을까? 주요 기능의 정상적인 동작을 확인하는 데 까지 40분 정도 (밖에 안) 걸렸다. 물론, 개발 과정이기 때문에 기존 데이터의 마이그레이션이나 변경 등의 이슈는 없었고, 단지, 테스트를 위해 마련해 둔 데이터를 변경하는 정도의 데이터 수정 작업이 있었다. 하지만, 그렇다고 하더라도 모델을 변경하고 테이블을 합친 것에 비하면 이는 정말 짧은 시간이다.


그렇다면 어떻게 이것이 가능했을까? 이는 ORM을 사용했기 때문이다. 실제로 모델을 변경하기 위해 한 작업은 다음과 같다.

  • 상위 Content 클래스를 추가하고 각 하위 타입에 대해 공통인 필드와 관련 메서드를 추가
  • 각 하위 Content 타입 클래스에서 상위 클래스로 옮겨간 필드 및 관련 메서드 제거
  • Content 및 하위 클래스에 대한 ORM 설정
  • 테스트 데이터 구성
  • 기능 테스트 (약 40% 진행)
위에서 가장 시간이 오래 걸린 작업은 테스트 데이터 구성과 기능을 테스트 한 시간이다. Content 및 하위 클래스 구성, 그리고 ORM 관련 설정에는 불과 10분 정도 밖에 소요되지 않았다. 테스트 데이터를 구성하는데 10분 정도가 소요되었고, 기능 테스트에 20분 정도가 소요되었다. 물론, 전체 기능 중에서 조회 위주로만 기능을 테스트 했지만, 놀라운 점은 조회 기능 테스트하는 과정에서 오류가 발생하지 않았다는 점이다. 게다가 위의 작업 내역을 보면 DB 연동을 처리하는 리포지토리는 변경도 하지 않았다. 오!! 놀랍지 않은가?

ORM이 아니였다면?


어떻게 이런 과감한 변경을 하면서도 짧은 시간에 변경이 가능했을까? 정답은 바로 ORM 때문이다.

만약 ORM이 아니였다면 관련된 쿼리들을 쫓아다니면서 테이블을 변경해주고, 쿼리를 변경해주는 등의 쌩노가나를 해야만 했을 것이다. 물론, 이 과정에서 짜증 유발과 함께 오타도 발생했을 거고, 누가 오타를 덜 내면서 쿼리를 변경해 내느냐가 그 사람의 (노가다) 능력으로 인정받았을 것이다.

하지만, 이 프로젝트에서는 ORM을 사용했기에, 매우 적은 스트레스를 받으며, 약간의 노가다 만으로, 그야말로 스마트하게 변경 작업을 진행할 수 있었다. 프로젝트를 진행하다보면, SQL로 했으면 거부감이 확 들었을 이런 종류의 변경에서부터 작게는 사소한 컬럼 추가까지 다양한 변경이 발생하게 되는데, 이만하면 ORM을 도입하기 위한 약간의 학습 비용은 프로젝트 전체로 봤을 때 그리고 향후 유지보수를 생각해 봤을 때 절대로 큰 비용이 아닐 것이다. 아니 오히려 매우 작은 비용일 것이다.

Posted by 최범균 madvirus

댓글을 달아 주세요

Hibernate에서 퍼스시턴트 객체를 이용한 CRUD 처리 및 퍼시스턴트 생명주기, 객체로딩전략, 영속성 전이에 대해서 살펴본다.

퍼시스턴트 객체의 영속성 생명주기

Hibernate의 퍼시스턴트 객체는 영속성과 관련해서 아래 그림과 같은 세 가지 상태를 갖는다.


위 그림에서 모서리가 둥근 건 퍼시스턴트 객체의 상태를 나타내며, 메소드는 Hibernate Session이 제공하는 메소드를 의미한다. 각 상태는 퍼시스턴트가 객체가 데이터베이스 테이블과 연결되었느냐에 따라 달라진다.

  • 비영속 상태 - 퍼시스턴트 객체를 처음 만들었을 때의 상태. 데이터베이스 테이블에 관련 데이터가 없으며, 연관된 Session이 없다.
  • 영속 상태 - 현재 활성화된 Session과 연결된 퍼시스턴트 객체. 이 상태의 퍼시스턴트 객체는 고유성을 가지며, 프로퍼티 값의 변경이 Session을 통해 자동으로 데이터베이스에 반영된다.
  • 준영속 상태 - 영속 상태의 퍼시스턴트 객체가 Session과 연결이 끊기면 준영속 상태가 된다. Hibernate의 관리를 받지는 않지만, 영속 데이터를 갖고 있다.
위 상태를 코드에서 표시하면 아래와 같다.


퍼시스턴트 객체가 고유성이 없는 경우(즉, 데이터베이스에 1대 1로 매핑되는 데이터가 존재하지 않는 경우) 비영속 상태에 해당한다. 예를 들어, 위 코드처럼 퍼시스턴트 객체를 새로 생성한 경우에 비영속 상태가 된다.

비영속 상태의 객체를 Session을 통해 저장하면(save() 또는 saveOrUpdate() 등을 사용) 퍼시스턴트 객체는 영속 상태가 된다. 영속 상태의 퍼시스턴트 객체는 활성화된 Session과 관련을 맺는다. 보통 영속 상태가 된다 하더라도 Session의 트랜잭션이 커밋되지 않으면 데이터베이스에 실제로 저장되지는 않는다.

일단, 퍼시스턴트 객체가 영속 상태가 되면 퍼시스턴트 객체의 값 변화는 자동으로 데이터베이스에서 반영된다. (이를 automatic dirty checking)이라 한다. Hibernate Session은 트랜잭션이 끝날 때 Session과 관련 있는 영속 상태 퍼시스턴트 객체의 프로퍼티가 변경되었는 지 확인한 후, 변경된 내용을 데이터베이스와 동기화한다.

Session이 종료되면 Session과 관련된 모든 영속 상태의 퍼스시턴트 객체는 준영속 상태가 된다. 준영속 상태의 퍼시스턴트 객체는 데이터베이스와 연결되어 있지 않기 때문에 프로퍼티 값을 변경하더라도 그 내역이 데이터베이스에 반영되지는 않는다. 준영속 상태의 퍼시스턴트 객체는 나중에 다시 다른 Session과 연결될 수 있으며, Session과 연결되면 다시 영속 상태로 바뀌게 된다. 다음의 코드는 준영속 상태로 된 객체가 다시 영속 상태로 변경되는 예를 보여주고 있다.

    Session session1 = sessions.openSession();
    ...
    Book book = (Book)session1.get(Book.class, new Integer(1)); // 영속 상태
    ...
    session1.close(); // 이 시점에서 book은 준영속 상태로 변경
    
    ...
    
    Session session2 = sessions.openSession();
    ...
    session2.update(book); // 이 시점에서 다시 영속 상태로 변경
    ...
    session2.close();

준영속 객체의 활용

준영속 객체는 Session과 연결되어 있지는 않지만 테이블의 값을 저장하고 있기 때문에, 데이터를 보여줄 때 유용하게 사용할 수 있다. 보통 MVC 패턴에 기반한 프로젝트인 경우 다음과 같은 방식으로 준영속 객체를 사용한다.

  • 컨트롤러는 Hibernate를 사용하여 데이터베이스로부터 데이터를 읽어온다.
  • 컨트롤러는 읽어온 퍼시스턴트 객체를 준영속 객체로 변환한다. (보통, Session을 종료)
  • 컨트롤러는 준영속 객체를 뷰에 전달한다.
  • 뷰는 준영속 객체를 사용해서 데이터를 출력한다.
뷰에서는 일반 자바빈 객체를 사용해서 데이터를 출력하듯이 준영속 객체를 사용해서 데이터를 출력하면 된다.

또한 준영속 객체는 다시 영속 객체로 전환될 수 있기 때문에, 여러 단계에 걸쳐서 데이터를 수정한 후 DB에 반영하는 방식에서도 잘 적용될 수 있다. 예를 들어, 웹 어플리케이션인 경우 세션에 준영속 객체를 저장한 뒤 여러번에 걸쳐서 객체의 데이터를 수정한다. 그리고 마지막 과정에서 세션에 저장했던 준영속 객체를 읽어와 영속 객체로 전환한 뒤 변경 결과를 반영하면 된다.

Session이 제공하는 CRUD 처리 메소드

Hibernate를 사용하는 가장 큰 이유는 SQL 쿼리를 작성하지 않고도 Hibernate가 제공하는 Session의 메소드를 사용해서 간단한 코드로 CRUD 작업을 처리할 수 있기 때문이다. Session은 CRUD 작업을 처리할 수 있는 메소드를 제공하는데, 이들 메소드는 다음과 같다.

  • save(Object obj) - 새로운 객체를 (테이블에) 저장한다.
  • save(Object obj, Serializable id) - 새로운 객체를 (테이블에) 저장한다. 이때 식별값으로 두번째 인자로 전달받은 객체를 사용한다.
  • Object get(Class type, Serializable id) - 지정한 타입의 객체를 읽어온다. 두번째 인자는 객체를 읽어올 때 사용할 식별자이다.
  • Object load(Class type, Serializable id) - 지정한 타입의 객체를 읽어온다. 두번째 인자는 객체를 읽어올 때 사용할 식별자이다.
  • load(Object obj, Serializable id) - obj의 타입과 관련된 데이터를 읽어와 obj에 저장한다. 두번째 인자는 객체를 읽어올 때 사용할 식별자이다.
  • update(Object obj) - 객체의 값을 변경한다.
  • update(Object obj, Serializable id) - 지정한 식별자에 해당하는 테이블 행을 객체가 저장한 값으로 변경한다.
  • saveOrUpdate(Object obj) - 새로운 객체인 경우 (테이블에) 저장하고, 이미 존재하는 객체인 경우 (테이블의) 값을 새로운 값으로 변경한다.
  • delete(Object obj) - 객체를 (테이블에서) 삭제한다.
객체를 읽어오는 메소드가 get과 load 두가지가 있는데, 이 두 메소드는 다음과 같은 차이가 있다.

  • get : 읽어올 객체가 없는 경우 null을 리턴하며, 프록시 객체를 리턴하지 않는다.
  • load : 읽어올 객체가 없는 경우 예외를 발생하며, 프록시 객체를 리턴할 수 있다.
load() 메소드는 읽어올 객체가 존재하지 않는 경우 예외를 발생하기 때문에, 존재해야 하는 데이터가 없는 경우를 예외상황으로 처리하고 싶을 때 load() 메소드를 사용하면 된다. 그렇지 않고 단지 객체가 존재하는지의 여부를 판단할 필요만 있는 거라면 get() 메소드를 사용해서 리턴된 값이 null인지의 여부를 검사하는 편이 낫다.

객체를 변경하는 방법은 크게 두가지가 있는데 하나는 Session의 트랜잭션 범위에서 영속 상태의 객체의 프로퍼티를 변경하는 것이며, 두번째 방법은 준영속 상태의 개체를 변경한 뒤 Session과 연관시키는 것이다. 다음은 두 방식의 차이점을 코드로 보여준다.

    Session session1 = sessions.openSession();
    Transaction tx1 = session1.beginTransaction();
    
    Item item = (Item)session1.get(Item.class, new Integer(6));
    item.setTitle(newTitle); // 영속 상태의 객체 프로퍼티 값 변경    
    tx1.commit(); // update() 메소드를 호출하지 않아도 영속 상태 객체의 변경 내용이 적용
    session1.close();
    
    item.setLowPrice(1000); // 준영속 상태의 객체 프로퍼티 값을 변경
    
    Session session2 = sessions.openSession();
    Transaction tx2 = session1.beginTransaction();
    
    session2.update(item); // 준영속 상태를 영속 상태로 변경
    
    tx2.commit();
    session2.close();

객체를 삭제할 때에는 다음과 같이 get()이나 load()로 읽어온 뒤 delete() 메소드를 호출하면 된다.

    Session session = sessions.openSession();
    Transaction tx = session1.beginTransaction();
    
    Item item = (Item)session.get(Item.class, new Integer(6));
    session.delete(item);
    
    tx.commit();
    session.close();

이외에 CRUD를 처리하는 방법으로는 iterate(), update(), delete() 메소드에 쿼리를 전달하는 방식도 있고, Criteria를 사용해서 조건에 따라 조회하는 방법도 있는데 이에 대한 내용은 본 글에서는 다루지 않으며, 시리즈가 진행되는 동안에 살펴볼 것이다.

객체 로딩 전략, 프록시와 외부조인

도메인 영역의 객체는 서로 연관되어 있으며, Hibernate는 이런 연관 관계를 알맞게 처리해준다. 예를 들어, 지난 번 글에서 예제로 사용했었던 Item과 Bid의 관계를 다시 살펴보자.

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

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

위 매핑 설정에서 알 수 있듯이 하나의 Item은 여러개의 Bid를 갖고 있으며 별도의 설정을 하지 않는 이상 Item을 읽어오면 Item과 관련된 모든 Bid 객체를 읽어오게 된다. 하지만, 경우에 따라서는 Item 객체만 필요하고 Item과 관련된 Bid 객체는 필요하지 않은 경우가 있으며, 이 경우 아래와 같은 코드를 실행할 때 Bid는 읽어오지 않길 원할 것이다.

    Session session = ...;
    ..
    Item item = (Item)session.get(Item.class, id); // 관련 Bid는 읽어오지 않길 원할 때가 있다
    ..
    session.close();

반대로 Bid 객체만 필요하고 Bid와 관련된 Item 객체는 필요하지 않은 경우가 있을 수 있다. 이렇게 연관된 객체를 읽어오지 않고 싶은 경우가 있을 수 있고, 반대로 늘 관련 객체를 함께 읽어오길 원하는 경우도 있을 수 있다. 이렇게 상황에 따라 데이터를 조회하는 네 가지 방식을 사용할 수 있는데, 이 네 가지 방식은 다음과 같다.

  • 즉시 읽어오기 방식 - 연관된 객체를 모두 읽어온다. 만약 one-to-one으로 연관되었을 경우, 연관된 객체를 읽어오기 위해 데이터베이스 조회가 발생한다.
  • 실제 사용할 때 읽어오기(lazy) 방식 - 실제로 연관된 객체가 사용될 때, 데이터베이스 조회가 발생한다. 예를 들어, Bid 객체를 읽어왔을 때, Bid.getItem() 메소드가 호출될 때 Item 객체를 읽어오기 위해 데이터베이스 조회가 발생한다.
  • 조인 읽어오기(outer-join) 방식 - 외부 조인을 수행하는 SQL 쿼리를 사용하여 연관된 객체를 모두 읽어온다. 즉시 읽어오기 방식이 연관 객체를 읽어오기 위해 여러번의 데이터베이스 조회가 발생하는 것과 달리 한번의 조회만 발생한다.
Hibernate는 위의 접근 방식을 매핑 파일에 속성을 사용해서 설정할 수 있도록 하고 있는데, 본 글에서는 실제 사용할 때 읽어오는 lazy 방식과 조인해서 읽어오는 outer-join 방식을 살펴보겠다. (참고로, lazy 방식과 outer-join 방식 중 아무것도 사용하지 않으면 즉시 읽어오는 방식을 사용한다.)

lazy 방식(프록시)

실제로 사용할 때 연관 객체를 읽어오는 걸, lazy 방식이라고 한다. Hibernate는 lazy 방식으로 연관 객체를 읽어올 때 다음과 같이 프록시 객체를 사용한다.


lazy 방식으로 지정된 경우, Hibernate는 위 그림에서 볼 수 있듯이 연관된 객체를 곧바로 읽어오지 않는다. 대신 연관된 객체와 연결될 수 있는 프록시 객체를 생성한다. 연관된 객체가 필요할 때 실제 연관될 객체가 생성되서 프록시와 연결된다.

many-to-one 관계에서 lazy 방식을 적용하기 위해서는 다음과 같이 연관될 클래스에 대한 정보를 담고 있는 class 태그의 lazy 속성값을 true로 지정해주어야 한다. (Bid-Item의 관계가 many-to-one)

      <class name="Item" table="ITEM" lazy="true">
            ...
      </class>

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

위와 같이 lazy 속성을 true로 지정하게 되면 Hibernate는 Item 클래스를 위한 프록시 클래스를 자동으로 생성해서 프록시로 사용한다.

one-to-one 관계에서도 마찬가지로 lazy 속성을 사용해서 lazy 방식을 적용할 수 있다. 예를 들어, 아래와 같이 one-to-one 관계를 맺을 때, 양쪽 모두 lazy 방식으로 읽어오도록 설정할 수 있다.

      <class name="Item" table="ITEM" lazy="true">
            ...
            <one-to-one name="detail"
                        class="ItemDetail" 
                        cascade="save-update" />            
            ...
            
      </class>

      <class name="ItemDetail" table="ITEM_DETAIL" lazy="true">
            ...
            <one-to-one name="item"
                        class="Item"
                        constrained="true" />
      </class>

콜렉션을 읽어올 때에도 lazy 방식을 사용할 수 있다. 예를 들어, set 태그에 다음과 같이 lazy 속성을 추가함으로써 lazy 방식으로 콜렉션을 읽어오도록 명시할 수 있다.

    <class name="javacan.hibernate.test.Item" table="ITEM" .. >
        ...

        <set name="bids" inverse="true" cascade="all-delete-orphan" lazy="true">
            <key column="ITEM_ID" />
            <one-to-many class="javacan.hibernate.test.Bid" />
        </set>
    </class>

lazy 방식을 사용할 때 주의할 점은 객체가 영속 상태일 때에만, 즉 Session과 연결되어 있는 경우에만 lazy 방식으로 데이터를 읽어올 수 있다는 것이다. 예를 들어, 아래의 코드처럼 lazy 방식으로 읽어온 프로퍼티를 준영속 상태에서 읽어올 수 없으며, 이런 경우 예외가 발생하게 된다.

      Session session = sessions.openSession();
      Transaction tx = session.beginTransaction();
      
      // Item의 bids 프로퍼티와 관련 객체를 lazy 방식으로 로딩
      Item item = (Item)session.get(Item.class, someId); 
      
      tx.commit();
      session.close();
      
      // 준영속 상태에서 lazy 방식의 프로퍼티에 접근 -> 예외 발생
      Iterator iter = item.getBids().iterator();

따라서, lazy 방식으로 읽어온 프로퍼티를 준영속 상태에서 사용해야 하는 경우에는 사전에 미리 lazy 방식으로 읽어온 프로퍼티를 초기화해야 한다. 이럴 때 사용할 수 있는 메소드가 Hibernate.initialize()이다. 위와 같은 경우 다음과 같이 Session을 종료하기 전에 Hibernate.initialize() 메소드를 호출해주면 된다.

      Session session = sessions.openSession();
      Transaction tx = session.beginTransaction();
      
      // Item의 bids 프로퍼티와 관련 객체를 lazy 방식으로 로딩
      Item item = (Item)session.get(Item.class, someId); 
      Hibernate.initialize(item.getBids());      
      tx.commit();
      session.close();
      
      // 사전에 초기화 되었으므로 예외 발생안함
      Iterator iter = item.getBids().iter();

outer-join 속성을 사용한 연관 객체 로딩

lazy 방식은 연관된 객체가 실제로 사용되기 전까지 최대한 늦게 객체를 로딩하는 반면에 outer-join 방식은 최대한 빨리 연관된 객체를 로딩한다. outer-join 방식은 외부 조인을 사용해서 한번의 SQL 쿼리로 연관 객체와 관련된 모든 데이터를 읽어오므로 데이터베이스 조회 회수를 줄이면서 동시에 연관 객체를 읽어오게 된다.

outer-join 속성값을 사용해서 outer-join 방식을 설정할 수 있는데, outer-join 속성에 사용할 수 있는 값을 다음과 같이 세가지가 존재한다.

  • auto - 기본값으로서 프록시가 가능한 경우 lazy 방식으로 읽어오고, 그렇지 않은 경우 outer-join 방식으로 읽어온다.
  • true - 프록시가 가능한 경우에도, 항상 outer-join 방식으로 읽어온다.
  • false - 프로시를 사용하지 않더라도, 항상 outer-join 방식을 사용하지 않는다.
예를 들어, Bid 객체를 읽어올 때 many-to-one의 관계에 있는 Item을 항상 outer-join 방식으로 읽어오고 싶다면 다음과 같이 하면 된다.

      <many-to-one name="item" class="Item" outer-join="true">

영속성 전이

이제 영속성 전이에 대해서 살펴보자. 영속성 전이는 영속 상태의 객체와 연관된 비영속 및 준영속 상태의 객체에 자동으로 영속성이 전파되는 특징을 말한다. 예를 들어, 다음의 코드를 보자.

      Bid bid = new Bid();
      ... // bid 프로퍼티들의 값 설정
      
      Session session = sessions.openSession();
      Transaction tx = session.beginTransaction();
      
      Item item = (Item)session.get(Item.class, id);
      item.addBid(bid); // 영속 객체(item)에 비영속 객체(bid)를 연관
      
      tx.commit();
      session.close();

위 코드는 영속 객체인 item에 비영속 객체인 bid를 연관시키고 있다. 이때 Hibernate는 자동으로 비영속 상태인 bid 객체를 영속 상태로 전환하게 된다. (영속 상태로 전환된다는 것은 데이터베이스에 저장된다는 것을 의미한다.) 이렇게 영속 상태의 객체와 연관된 비영속/준영속 객체들이 자동으로 영속 상태가 되는 것을 영속성 전이라고 표현한다.

영속성 전이는 저장 뿐만 아니라 삭제인 경우에도 발생할 수 있다. 예를 들어, 하나의 경매 항목이 삭제되면 그와 관련된 입찰 정보도 삭제될 것이다. 이렇게 영속 상태의 객체가 비영속 상태로 변경될 때 연관된 영속 객체들도 함께 비영속 상태로 변경되는 것 역시 영속성 전이에 해당한다.

Hibernate는 영속성 전이를 연관 객체에 대해 어느 정도까지 적용할 지를 설정하기 위해 cascade 속성을 사용하며, cascade 속성은 다음과 같은 값들을 가질 수 있다.

  • none - 연관을 무시한다.
  • save-update - 트랜잭션이 커밋될 때, save()나 update() 메소드가 호출될 때, 새롭게 생성한 비영속 객체를 영속 객체에 연관할 때, 준영속 객체를 영속 상태로 변경할 때, 영속성 전이를 수행한다.
  • delete - 객체가 delete()에 전달될 때 영속성 전이를 수행해서 연관되어 있는 객체를 삭제한다.
  • all - save-update와 delete 옵션을 함께 수행한다. 또한, evict() 메소드나 lock() 메소드가 실행될 때에도 영속성 전이를 수행한다.
  • all-delete-orphan - all과 같은 기능을 수행한다. 더불어, 연관에서 제거된 퍼시스턴트 객체를 삭제한다. 콜렉션과 관련된 영속성 전이에 사용된다.
예를 들기 위해 먼저 다음의 설정을 보자.

      <class name="javacan.hibernate.test.Item" table="ITEM" lazy="true">
            ...
            <set name="bids" inverse="true" cascade="save-update" >
                  <key column="ITEM_ID" />
                  <one-to-many class="javacan.hibernate.test.Bid" />
            </set>
      </class>
      
      <class name="javacan.hibernate.test.Bid" table="BID">
            
            <many-to-one name="item"
                         column="ITEM_ID"
                         class="javacan.hibernate.test.Item"
                         not-null="false"  />
      </class>
      

위 설정 코드는 Item-Bid를 one-to-many 관계로 하고 있고, Item->Bid로의 영속성 전이를 save-update로 설정하였다. save-update는 영속 객체와 연관된 객체에 영속성이 전이되므로 아래 코드와 같이 새로운 Bid 객체를 생성해서 연관한 경우 새롭게 생성한 Bid 객체가 DB에 저장된다.

      Session session = ...;
      Transaction tx = session.beginTransaction();
      
      Bid bid = new Bid();
      Item item = (Item)session.get(Item.class, id);
      item.addBid(bid);
      
      tx.commit();
      session.close();

하지만, 설정 파일의 set 태그에서 cascade 속성을 none으로 하게 되면 영속성 전이가 수행되지 않으므로, 위 코드에서 새롭게 생성한 Bid 객체는 데이터베이스에 저장되지 않는다.

cascade 속성값이 delete인 경우는 연관된 객체만 삭제한다. 예를 들어, 아래의 코드를 살펴보자.

      Session session1 = session.openSession();
      ...
      Item item1 = (Item)session1.get(Item.class, id);
      session1.delete(item1); // item1과 연관된 ItemDetail도 삭제
      ...
      session1.close();
      
      Session session2 = session.openSession();
      ...
      Item item2 = (Item)session2.get(Item.class, id);
      item2.setDetail(null); // item1과 연관됐었던 ItemDetail을 연관시키지 않음
      session2.delete(item2); // item1만 삭제되고, 연관됐던 ItemDetail은 삭제되지 않음
      ...
      session2.close();

cascade 속성값이 delete인 경우에는 첫번째와 같이 연관되어 있는 객체는 함께 삭제되나, 두번째와 같이 연관됐었다가 연관되지 않게된 객체의 경우는 삭제되지 않는다.

delete와 달리 all-delete-orphan은 연관됐었던 객체를 연관에서 해제하더라도 삭제된다. 예를 들어, Item과 Bid의 관계를 아래와 같이 설정했다고 해 보자. 아래 코드는 Item-Bid의 one-to-many 관계에서 cascade 속성을 all-delete-orphan으로 설정하고 있다.

      <class name="javacan.hibernate.test.Item" table="ITEM" lazy="true">
            ...
            <set name="bids" inverse="true" cascade="all-delete-orphan" >
                  <key column="ITEM_ID" />
                  <one-to-many class="javacan.hibernate.test.Bid" />
            </set>
      </class>
      
      <class name="javacan.hibernate.test.Bid" table="BID">
            
            <many-to-one name="item"
                         column="ITEM_ID"
                         class="javacan.hibernate.test.Item"
                         not-null="false"  />
      </class>
      

이 상태에서 다음과 같은 코드를 실행했다고 해 보자.

      Session session = HibernateUtil.currentSession();
      tx = session.beginTransaction();
      
      Item item = (Item)session.get(Item.class, new Integer(2));
      item.getBids().clear(); // Set에 있는 모든 객체를 비움
      
      tx.commit();

위 코드는 Item의 bids 프로퍼티인 Set 객체에 저장되어 있던 객체 자체를 삭제하는 게 아니라 단지 Set 객체를 비운 것에 불과하다. 하지만, cascade 속성 값이 all-delete-orphan 이기 때문에, Set에 저장되어 있는 Bid 객체와 매핑된 테이블 데이터도 함께 제거된다.

다음 글에서는

본 글의 예제 코드에서 set 태그를 사용했었는데, Hibernate는 set 태그 이외에 list, map과 같은 다양한 타입의 콜렉션을 지원하고 있다.다음 글에서는 Hibernate가 제공하는 콜렉션에는 어떤 것들이 있으며 어떻게 사용하는 지에 대해서 살펴볼 것이다. 또한 살펴보지 않았던 many-to-many 관계를 Hibernate에서 어떻게 적용할 수 있는 지에 대해서도 살펴볼 것이다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. darkhorizon 2009.04.14 13:40 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 글이네요.
    좀 담아가도 될까요?

  2. darkhorizon 2009.04.15 15:16 신고  댓글주소  수정/삭제  댓글쓰기

    네. 감사합니다

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에서 객체와 테이블사이의 매핑 설정 파일의 기본 작성법, Hibernate 타입, 키값 생성기에 대해서 살펴본다.

ORM 설정 파일 기본 작성법

본 시리즈의 첫번째 퀵스타트에서 봤듯이, ORM 설정 파일은 XML 파일로 저장된다. ORM 설정 파일에 대해서 살펴보기 전에 먼저 본 글에서 예제로 사용할 자바빈 클래스를 살펴보자. 본 글에서 사용할 자바빈 클래스는 간단한 책 정보를 담고 있는 Book 클래스로서 아래와 같다.

package javacan.hibernate.test;

public class Book {
      private Integer id;
      private String title;
      private String author;
      private String publishingCompany;
      private int price;
      
      public String getAuthor() {
            return author;
      }
      public void setAuthor(String author) {
            this.author = author;
      }
      public Integer getId() {
            return id;
      }
      public void setId(Integer id) {
            this.id = id;
      }
      public int getPrice() {
            return price;
      }
      public void setPrice(int price) {
            this.price = price;
      }
      public String getPublishingCompany() {
            return publishingCompany;
      }
      public void setPublishingCompany(String publishingCompany) {
            this.publishingCompany = publishingCompany;
      }
      public String getTitle() {
            return title;
      }
      public void setTitle(String title) {
            this.title = title;
      }
}

위의 자바빈 객체와 대응되는 테이블은 아래와 같다.

BOOK 테이블
컬럼 타입 NULL 여부
BOOK_ID INTEGER false PK
TITLE VARCHAR(100) false  
AUTHOR VARCHAR(100) false  
PUBLISHING VARCHAR(100) false  
PRICE INTEGER false  

오라클을 사용한다면 테이블 생성 스크립트는 아래와 같을 것이다.

create table BOOK (
         BOOK_ID INTEGER NOT NULL,
         TITLE VARCHAR2(100) NOT NULL,
         AUTHOR VARCHAR2(100) NOT NULL,
         PUBLISHING VARCHAR2(100) NOT NULL,
         PRICE INTEGER NOT NULL,
CONSTRAINT PK_BOOK PRIMARY KEY (BOOK_ID)
)

또한, 본 글에서는 오라클을 기준으로 BOOK_ID 컬럼에 삽입될 값을 오라클 시퀀스로부터 가져오도록 예제를 설정할 것이며, 이때 사용할 시퀀스는 아래와 같은 스크립트를 이용해서 생성해주면 된다.

CREATE SEQUENCE BOOK_ID_SEQ
      START WITH 0
      INCREMENT BY 1
      MINVALUE 0
NOCACHE NOCYCLE NOORDER

앞서 생성한 Book 클래스와 BOOK 테이블 사이의 매핑을 처리해주는 매핑 설정 파일은 아래와 같이 작성할 수 있을 것이다.

<?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.Book" table="BOOK">
            <id name="id" type="integer" unsaved-value="null">
                  <column name="BOOK_ID" sql-type="INTEGER" not-null="true" />
                  <generator class="sequence">
                        <param name="sequence">BOOK_ID_SEQ</param>
                  </generator>
            </id>
            
            <property name="title" type="string">
                  <column name="TITLE" length="100" not-null="true" />
            </property>
            
            <property name="author" />
            
            <property name="publishingCompany" column="PUBLISHING" />
            
            <property name="price" type="integer" />
      </class>
</hibernate-mapping>

위 매핑 설정 파일은 가장 간단한 1대1로 이뤄진 객체-테이블 매핑을 보여주는 가장 간단한 형태의 매핑 설정 파일의 예이다. OR 매핑을 설정하는 가장 기본적인 형태이므로 Hibernate를 사용할 때 반드시 숙지해야 하는 코드이기도 하다.

위 코드에서 사용된 각 태그는 다음과 같은 의미를 지닌다.

  • hibernate-mapping : OR 매핑 정보를 담고 있는 XML의 루트 태그
  • class : OR 매핑에서 사용되는 자바 클래스에 대한 정보를 표시
  • id : 테이블의 PK 컬럼을 저장할 자바빈 프로퍼티에 대한 정보를 표시
  • property : 테이블의 컬럼과 매핑될 자바빈 프로퍼티에 대한 정보를 표시
  • column : 자바빈 프로퍼티와 매핑되는 테이블의 컬럼에 대한 정보를 표시
  • generator : 테이블에 객체를 저장할 때 사용되는 키 값 생성기
위 태그 외에도 다양한 태그가 존재하는데, 이들 태그에 대해서는 이 글의 뒷 부분에서 살펴볼 것이며, 일단 위 예제 코드에서 사용된 태그들에 대해서 차례대로 살펴보도록 하자.

<hibernate-mapping> 태그

hibernate-mapping 태그는 Hibernate가 사용하는 OR 매핑 설정 XML 파일의 루트 태그로서 XML 문서가 Hibernate 매핑 문서임을 나타낸다. 앞서 예제 OR 매핑 파일에서 볼 수 있듯이, hibernate-mapping 태그는 필수 속성을 갖고 있지 않다. hibernate-mapping 태그에서 옵션으로 사용할 수 있는 속성은 아래와 같이 네 개가 존재한다.

속성 설명
schema 데이터베이스 스키마의 이름을 명시한다.
default-cascade 기본적으로 사용할 케스케이딩 전략을 명시한다. 테이블 사이의 연관 관계가 있을 때, 테이블에 자체의 케스케이딩 전략이 없으면 이 속성의 값에서 명시한 전략을 사용한다. 'none'과 'save-update'를 값으로 가질 수 있으며, 기본값은 'none'이다.
auto-import 이 매핑 파일에 있는 클래스 설정 중에서 (패키지 이름을 포함한) 완전한 이름을 명시하지 않은 클래스를QL(Query Language)에서 사용할 수 있는지의 여부를 명시한다. true와 false를 값으로 가지며, 기본값은 true이다.
package 패키지 이름을 포함하지 않은 클래스 이름의 완전한 이름을 유추할 때 사용할 기본 패키지 이름을 명시한다.

schema 속성은 Hibernate가 내부적으로 생성하는 쿼리와 연관되는 데, 예를 들어, schema 속성의 값을 'javacan'이라고 주었다면 아래와 같이 테이블 이름 앞에 데이터베이스 스키마 명이 붙은 쿼리를 내부적으로 사용하게 된다.

   select ... from javacan.tableName ...

<class> 태그

OR 매핑 파일은 다음과 같이 여러 개의 class 태그를 가질 수 있다.

<?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.Book" table="BOOK">
            ...
      </class>

      <class name="javacan.hibernate.test.PublishingCompany" table="PUBLISHING">
            ...
      </class>
      
</hibernate-mapping>

하나의 class 태그는 기본적으로 하나의 클래스와 하나의 테이블에 대한 매핑 정보를 담는데, class 태그를 사용해서 어떤 클래스가 어떤 테이블과 매핑되는 지에 대한 정보 및 기타 CRUD 처리와 관련된 속성 정보를 명시하게 된다. 가장 기본저인 형태는 아래와 같다.

      <class name="javacan.hibernate.test.Book" table="BOOK">
            ... <!-- 프로퍼티와 컬럼에 대한 매핑 정보 설정 -->
      </class>
      

class 태그의 가장 중요한 속성은 name과 table이다. 이 두 속성은 아래와 같다.

  • name - 퍼스시턴트 클래스의 완전한 자바 클래스(또는 인터페이스) 이름
  • table - 데이터베이스 테이블의 이름
즉, 위 코드는 javacan.hibernate.test.Book 클래스와 BOOK 테이블에 매핑된다는 것을 설정하고 있는 것이다.

이 두 속성 외에 다음 표와 같은 속성을 사용할 수 있다.

속성 설명
schema 테이블을 소유한 스키마를 명시한다.
dynamic-update 런타임에, 값이 변경되는 컬럼에 대해서만 SQL UPDATE 쿼리를 동적으로 생성할지의 여부를 명시한다. true 또는 false를 값으로 갖는다. 기본값은 false이다.
dynamic-insert 런타임에, 값이 null이 아닌 컬럼에 대해서만 SQL INSERT 쿼리를 동적으로 생성할지의 여부를 명시한다. true 또는 false를 값으로 갖는다. 기본값은 false이다.
select-before-update 객체가 실제로 변경되는 것이 아닌 경우 SQL UPDATE 쿼리를 수행하지 않을 지의 여부를 명시한다. 기본값은 false이다. 이 값을 true로 지정할 경우 Hibernate는 Session.update() 메소드를 호출할 때, UPDATE 쿼리를 수행할 필요가 있는 지의 여부를 검사하기 위해 SELECT 쿼리를 수행하게 된다. (성능이 좋지 않으므로 트리거와 같이 DBMS가 지원하는 기능으로 대체하는 것이 좋다.)

이 외에 여러 속성들이 존재하는데, 이들 속성들은 본 시리즈 글을 진행하면서 필요할 때에 추가적으로 설명하겠다.

<id> 태그

DB 테이블로부터 읽어온 데이터를 저장하기 위해서 사용되는 객체는 대부분 PK 값을 저장하기 위해 자바빈 스타일의 프로퍼티를 제공한다. (앞서 작성했던 Book 클래스도 id 프로퍼티를 제공하고 있다.) id 태그는 테이블의 PK 컬럼에 매핑되는 프로퍼티를 명시할 때 사용되며, 구문은 다음과 같다.

      <id name = "프로퍼티이름"
            type = "Hibernate타입"
            column = "테이블컬럼이름"
            unsaved-value = "any|none|null|id_value"
            
            generator class="generatorClass" />
      </id>

각 속성은 다음과 같은 의미를 지닌다.

속성 설명
name 식별자로 사용될 프로퍼티의 이름 (옵션)
type Hibernate 타입 (옵션)
column PK 컬럼의 이름. 입력하지 않은 경우 프로퍼티의 이름을 사용한다. (옵션)
unsaved-value 새로 생성한 객체를 아직 저장하지 않았음을 나타낼 때 사용할 식별자 프로퍼티의 값. Hibernate는 식별자 프로퍼티의 값이 이 속성에 명시한 값과 가을 경우, 객체가 DB 테이블에 저장되어 있지 않은 새롭게 생성된 객체라고 가정한다. 기본값은 null이다. (옵션)

만약 name 속성을 입력하지 않으면 클래스가 식별자 프로퍼티를 갖고 있지 않다고 가정한다. 이 경우 Hibernate는 읽어온 객체를 변경하거나 삭제할 때 사용할 키값을 별도로 관리하게 된다.

<property> 태그와 <column> 태그

property 태그는 PK 컬럼 이외의 컬럼과 매핑되는 프로퍼티에 대한 정보를 명시할 때 사용된다. property 태그의 일반적인 형태는 아래와 같다.

      <class name=".." table="..">
            <id .... > .. </id>
            
            <property name="title" type="string">
                  <column name="TITLE" length="100" not-null="true" />
            </property>            
            <property name="author" column="AUTHOR" />            
            ...
      </class>

첫번째 property 태그는 매핑될 컬럼 정보를 자식 태그인 column 태그에 명시하는 방법을 보여주고 있고, 두번째 property 태그는 컬럼 정보를 속성을 사용해서 명시하는 방법을 보여주고 있다. property 태그의 필수 속성은 name 뿐이며 나머지 속성은 hibernate가 기본값을 적용하여 알맞게 처리해준다. property 태그에서 사용가능한 속성을 다음 표와 같다.

속성 설명
name 프로퍼티의 이름. 첫번째 글자는 소문자이다. (옵션)
column 프로퍼티와 매핑될 테이블의 컬럼 이름. 기본값은 프로퍼티 이름이다. (옵션)
type 타입명을 입력한다. (옵션)
unique UNIQUE 컬럼과 매핑될 경우 true를 입력한다. 기본값은 false. (옵션)
update 매핑될 컬럼이 SQL의 UPDATE 쿼리에 포함될지의 여부를 나타낸다. 기본값은 true. (옵션)
insert 매핑될 컬럼이 SQL의 INSERT 쿼리에 포함될지의 여부를 나타낸다. 기본값은 true. (옵션)
formula 프로퍼티가 계산에 의해서 구해지는 경우, 프로퍼티의 값을 구할 때 사용되는 SQL 수식을 입력. 계산된 값을 갖는 프로퍼티는 매핑되는 컬럼을 갖지 못한다. (옵션)

type 속성은 타입명을 입력하는데, 타입명에는 다음과 같은 값이 올 수 있다.

  1. Hibernate 기본 타입 (예, integer, string, date, binary 등)
  2. Hibernate 기본 타입과 관련된 자바 클래스 이름 (예, int, java.lang.String, java.util.Date, java.sql.Clob)
  3. PersistentEnum의 하위 클래스 이름
  4. 직렬화 가능한 자바 클래스 이름
  5. 커스텀 타입의 클래스 이름
보통은 첫번째와 두번째로 대부분의 경우의 수를 처리할 수 있으며, 필요에 따라 세번째 값이 사용된다. 만약 type 속성의 값을 명시하지 않으면, Hibernate는 리플레션을 사용해서 프로퍼티의 타입을 알아낸 뒤, 그 타입에 따라 알맞은 type 속성의 값을 지정하게 된다.

<component> 태그를 이용한 값 저장

회원 정보는 다음과 같이 자택 주소와 직장 주소를 포함하는 것이 일반적이다.



위 코드에서 주소 자택 주소에 해당하는 필드와 직장 주소에 해당하는 필드는 동일한 구조를 갖고 있다. 따라서 자바에서는 다음과 같이 Address 객체를 사용해서 주소 정보를 담을 수가 있다.

public class Address {
      private String zipCode;
      private String address1;
      private String address2;
      
      public void setZipCode(String zipCode) {
            this.zipCode = zipCode;
      }
      public String getZipCode() {
            return zipCode;
      }
      
      .... // 관련 get/set 메소드
      
}

주소를 Address 클래스를 사용하여 표현할 경우 MEMBER 테이블과 매핑될 Member 클래스는 다음과 같은 코드 형태를 취하게 될 것이다.

public class Member {
      private String id;
      private String name;
      private String homeAddress;
      private String jobAddress;
      
      ....
      
      public void setHomeAddress(Address address) {
            homeAddress = address;
      }
      public Address getHomeAddress() {
            return homeAddress;
      }
      public void setJobAddress(Address address) {
            jobAddress = address;
      }
      public Address getJobAddress() {
            return jobAddress;
      }
}

위 Member 클래스가 의미하는 것은 MEMBER 테이블의 집주소에 해당하는 세 개의 컬럼, 즉 HOME_ZIPCODE, HOME_ADDRESS1, HOME_ADDRESS2가 homeAddress 프로퍼티(즉, Address 객체에) 매핑된다는 것을 의미한다. 이렇게 테이블의 여러 컬럼이 하나의 프로퍼티에 매핑될 때 사용되는 태그가 component 태그이다. MEMBER 테이블과 Member 클래스 사이의 매핑에서 component 태그는 다음과 같이 사용된다.

      <class name="Member" table="MEMBER" >
            <id name="id" column="MEMBER_ID">
                  <generator class="assigned" />
            </id>
            
            <property name="name" />
            
            <component name="homeAddress" class="Address">
                  <property name="zipCode" column="HOME_ZIPCODE" />
                  <property name="address1" column="HOME_ADDRESS1" />
                  <property name="address2" column="HOME_ADDRESS2" />
            </component>
            <component name="jobAddress" class="Address">
                  <property name="zipCode" column="JOB_ZIPCODE" />
                  <property name="address1" column="JOB_ADDRESS1" />
                  <property name="address2" column="JOB_ADDRESS2" />
            </component>
            ...
      </class>

component 태그의 name 속성과 class 속성은 다음과 같다.

  • name - component가 속한 클래스의 프로퍼티 이름.
  • class - 프로퍼티의 자바 클래스 이름
위의 코드에서 첫번째 component 태그는 Member 클래스의 homeAddress 프로퍼티를 나타내므로, name 속성의 값을 "homeAddress"로 주었으며, homeAddress 프로퍼티의 타입이 Addressd 클래스이므로 class 속성의 값을 "Address"로 주었다.

component 태그가 나타내는 자바 클래스와 테이블 사이의 매핑 정보를 처리하기 위해서는 component 태그에 property 태그를 포함시키면 된다. component 태그에 중첩된 property 태그는 class 태그가 나타내는 클래스가 아닌 component 태그가 나타내는 클래스의 프로퍼티와 테이블 사이의 매핑을 표현하게 된다.

Hibernate의 타입

자바 객체의 프로퍼티 타입과 테이블의 컬럼 타입은 정확하게 일치하지 않는다. 예를 들어, 자바의 java.lang.String 타입의 프로퍼티가 SQL의 VARCHAR로 매핑될 수도 있고, CHAR로 매핑될 수도 있다. Hibernate는 이러한 타입의 불일치 문제를 해결하기 위해서 Hibernate 자체적으로 타입을 제공하고 있으며, 이 타입을 사용해서 자바의 타입과 SQL 타입 사이의 매핑 문제를 해결하고 있다.

Hibernate가 제공하는 매핑 타입은 자바의 기본 데이터 타입, 날짜 및 시간 관련 타입, 대량 데이터 관련 타입 등이 존재한다. 이들 타입은 property 태그나 id 태그 등 프로퍼티와 컬럼 사이의 매핑 정보를 표시하는 부분에서 사용되며, 이 타입 정보를 통해 Hibernate는 OR 매핑을 올바르게 처리할 수 있게 된다.

자바 기본 데이터 타입 매핑

자바의 기본 데이터 타입의 매핑을 처리해주는 Hibernate 타입은 다음과 같다.

매핑 타입 자바 타입 SQL 타입
integer int 또는 java.lang.Integer INTEGER
long long 또는 java.lang.Long BIGINT
short short 또는 java.lang.Short SMALLINT
float float 또는 java.lang.Float FLOAT
double double 또는 java.lang.Double DOUBLE
big_decimal java.math.BigDecimal NUMERIC
character java.lang.String CHAR(1)
string java.lang.String VARCHAR
byte byte 또는 java.lang.Byte TINYINT
boolean boolean 또는 java.lang.Boolean BIT
yes_no boolean 또는 java.lang.Boolean CHAR(1) ('Y' 또는 'N')
true_false boolean 또는 java.lang.Boolean CHAR(1) ('T' 또는 'F')

날짜 및 시간 타입 매핑

자바의 날짜 및 시간 타입의 매핑을 처리해주는 Hibernate 타입은 다음과 같다.

매핑 타입 자바 타입 SQL 타입
date java.util.Date 또는 java.sql.Date DATE
time java.util.Time 또는 java.sql.Time TIME
timestamp java.util.Timestamp 또는 java.sql.Timestamp TIMESTAMP
calendar java.util.Calendar TIMESTAMP
calendar_date java.util.Calendar DATE

대량 데이터 관련 매팅 타입

바이너리 데이터와 대량 데이터를 처리할 때 사용되는 Hibernate 타입은 다음과 같다.

매핑 타입 자바 타입 SQL 타입
binary byte[] VARBINARY (또는 BLOB)
text java.lang.String CLOB
serializable java.io.Serializable를 구현한 모든 자바 클래스 VARBINARY (또는 BLOB)
clob java.sql.Clob CLOB
blob java.sql.Blob BLOB

JDBC 드라이버중에 clob이나 blob의 경우는 지원하지 않은 경우도 있으므로, clob과 blob을 사용할 때에는 먼저 JDBC 드라이버가 해당 타입을 지원하는지의 여부부터 확인해야 한다.

키 값 생성기

앞서 예로 들었던 OR 매핑 설정 파일의 일부를 다시 한번 살펴보자.

<hibernate-mapping>
      <class name="javacan.hibernate.test.Book" table="BOOK">
            <id name="id" type="integer" unsaved-value="null">
                  <column name="BOOK_ID" sql-type="INTEGER" not-null="true" />
                  <generator class="sequence">
                        <param name="sequence">BOOK_ID_SEQ</param>
                  </generator>
            </id>
            
            <property name="title" type="string">
                  <column name="TITLE" length="100" not-null="true" />
            </property>
            ...
      </class>
</hibernate-mapping>

위 코드에서 id 태그는 테이블의 PK 컬럼과 매핑을 처리할 때 사용된다고 했으며, 새롭게 생성한 자바 객체를 테이블에 저장할 때 사용할 키값은 generator 태그에서 명시한 클래스를 통해서 생성된다. generator 태그는 키값을 생성할 클래스와 클래스가 사용할 파라미터 값을 전달하는데 사용되며, 보통 다음과 같은 형태를 갖는다.

      <generator class="클래스이름">
            <param name="파라미터이름">파라미터값</param>
            <param name="파라미터이름">파라미터값</param>
      </generator>

클래스 이름에는 net.sf.hibernate.id.IdentifierGenerator 인터페이스를 구현한 클래스의 완전한 이름을 적어주면 된다. Hibernate는 몇가지 형태의 키값 생성기를 기본적으로 제공하고 있으며, 이들 생성기를 짧은 이름을 통해서 사용할 수 있도록 하고 있다. 다음은 Hibernate가 제공하는 기본 키값 생성기의 이름이다.

  • increment : 정수 타입의 키 값을 사용할 경우, 키 값을 1씩 증가시켜 생성한다.
  • identity : 테이블의 식별 컬럼을 지원한다. (예, MySQL의 auto_increment 컬럼)
  • sequence : 시퀀로부터 키 값을 가져온다. (예, 오라클의 시퀀스)
  • hilo / seqhilo : hi/lo 알고리즘을 사용하여 정수값 키값을 생성한다.
  • uuid.string / uuid.hex : UUID 알고리즘을 사용하여 키값을 생성한다.
  • native : DBMS 및 테이블에 알맞은 키값 생성기를 자동 선택한다.
  • assigned : 어플리케이션에 직접 키값을 생성한다.
  • foreign : 연관 객체의 식별자를 키값으로 사용한다. <one-to-one> PK 연관에서 주로 사용된다.
increment 생성기

increment는 DBMS에 상관없이 사용할 수 있는 키값 생성기로서 long, short, int 타입의 식별값을 생성한다. increment 생성기는 파라미터 값을 필요로 하지 않으며, 다음과 같이 사용할 수 있다.

      <id name="id" type="integer" column="BOOK_ID" unsaved-value="null">
            <generator class="increment" />
      </id>

클러스터(cluster)에서는 사용하면 안 된다.

identity 생성기

DB2, MySQL, MS SQL 서버, Sybase 그리고 HypersonicSQL의 식별 컬럼을 지원한다. (예를 들어, MySQL의 auto_increment 컬럼) 리턴된 식별값의 타입은 long, short, int 중 하나이다. increment 생성기와 마찬가지로 파라미터를 필요로 하지 않으며, 다음과 같이 생성기의 이름만 지정해주면 된다.

      <id name="id" type="integer" column="BOOK_ID" unsaved-value="null">
            <generator class="identity" />
      </id>

sequence 생성기

DB2, 오라클, PostgreSQL, SAP DB, McKoi의 시퀀스나 Interbase의 generator를 사용해서 키값을 생성한다. 리턴된 식별자의 타입은 long, short, int 중 하나이다. sequence 생성기는 아래와 같이 sequence 파라미터로부터 사용할 시퀀스의 이름을 전달받는다.

      <id name="id" type="integer" column="BOOK_ID" unsaved-value="null">
            <generator class="sequence">
                  <param name="sequence">BOOK_ID_SEQ</param>
            </generator>
      </id>

hilo / seqhilo 생성기

hilo 생성기는 hi/lo 알고리즘을 사용한다. 알고리즘에서 hi 값을 읽어올 때 사용할 테이블과 컬럼의 이름을 table 파라미터와 column 파라미터로부터 읽어온다. (테이블과 컬럼의 이름을 명시하지 않을 경우 기본값은 테이블 이름은 'hi_value'이고 컬럼 이름은 'next_value'이다.) 또한 hi/lo 알고리즘에서 사용될 값을 max_lo 파라미터로부터 읽어온다. hilo 생성기는 long, short, int 타입의 식별값을 생성한다. 다음은 hilo 생성기의 사용예이다.

      <id name="id" type="integer" column="BOOK_ID" unsaved-value="null">
            <generator class="hilo">
                  <param name="table">HILO_VALUE</param>
                  <param name="column">NEXT_VAL</param>
                  <param name="max_lo">5</param>
            </generator>
      </id>

hilo 생성기를 사용할 때 주의할 점은 테이블이 미리 생성되어 있어야 하며, 테이블이 한개의 레코드가 존재해야 한다는 점이다. 또 다른 주의점은, 데이터베이스 Connection을 세션에 직접 제공하거나 또는 JTA를 사용하기 위해서 어플리케이션 서버가 제공하는 DataSource를 사용하는 경우에는 hilo 생성기를 사용할 수 없다는 점이다.

seqhilo 생성기는 hilo 생성기와 비슷하나 알고리즘에서 사용할 값을 테이블이 아닌 시퀀스로부터 읽어온다는 차이점이 있다. 시퀀스의 이름은 sequence 파라미터로부터 전달받는다. 다음은 seqhilo 생성기의 사용예이다.

      <id name="id" type="integer" column="BOOK_ID" unsaved-value="null">
            <generator class="seqhilo">
                  <param name="sequence">BOOK_ID_SEQ</param>
                  <param name="max_lo">5</param>
            </generator>
      </id>

uuid.string / uuid.hex 생성기

uuid.string 생성기와 uuid.hex 생성기는 128비트 UUID 알고리즘을 사용하여 string 타입의 값을 생성한다. 차이점은 다음과 같다.

  • uuid.string - 16 개의 ASCII 글자로 구성된 식별값을 생성한다. PostgreSQL에서는 사용해서는 안 된다
  • uuid.hex - 32 개의 16진수 숫자로 구성된 식별값을 생성한다.
별도의 파라미터는 없으며 다음과 같이 사용된다.

      <id name="id" type="string" column="CAT_ID" unsaved-value="null">
            <generator class="uuid.string" />
      </id>

native 생성기

native 생성기는 DBMS가 제공하는 기능에 따라 identity, sequence, hilo 생성기를 선택적으로 사용한다.

assigned 생성기

assigned 생성기는 어플리케이션에서 직접 식별값을 입력한다는 것을 의미한다. assigned 생성기를 사용하면 객체를 저장하기 전에(즉, Session.save() 메소드를 호출하기 전에) 식별자에 키값을 입력해주어야 한다.

다음 글에서는

이번 글에서는 테이블과 객체의 매핑에 대해서만 살펴봤는데, 실제로는 객체 사이의 상속 관계와 테이블, 객체 사이의 연관과 테이블 사이의 연관 등 처리해줘야 할 것이 많다. 또한, 여러 컬럼이 PK가 되는 복합키에 대한 처리도 필요하다. 다음 글에서는 이처럼 좀더 복잡한 OR 매핑을 처리하는 방법에 대해서 살펴보도록 하자.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

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

댓글을 달아 주세요

객체와 데이터베이스 사이의 그리고 매퍼 자체의 독립성을 유지하는 매퍼 레이어에 대해서 살펴본다.

메타 데이터 매퍼

본 글에서 소개하는 패턴은 '엔터프라이즈 어플리케이션 아키텍처 패턴' (마틴 파울러 저)에 소개된 패턴으로서, 필자가 파울러가 소개한 패턴을 보다 쉽게 이해할 수 있도록 재구성한 것이다.

'데이터 매퍼(Data Mapper)'는 이 글의 시리즈 글인 '데이터 소스 아키텍처 패턴 1'에서 살펴봤던 테이블 데이터 게이트웨이와 상당히 유사하다. 데이터 매퍼는 테이블 데이터 게이트웨이와 마찬가지로 도메인 모델 객체와 데이터베이스 테이블 사이에 존재하는 레이어로서 도메인 모델 객체와 데이터베이스 테이블 사이의 데이터 이동을 처리해준다.

테이블 데이터 게이트웨이는 테이블과 관련된 CRUD 작업을 처리하는 데 초점을 맞추고 있다면, 데이터 매퍼는 도메인 모델 객체와 테이블 사이의 매핑을 처리하는 데에 초점을 맞추고 있다. 이런 관점에서 테이블 데이터 게이트웨이는 테이블과 관련된 값을 RecordSet과 같은 범용타입을 통해서 처리해도 무방하지만, 데이터 매퍼의 경우는 정확한 타입의 객체와 테이블 사이의 매핑을 처리해주어야 한다.

마틴 파울러의 책에서 보여주는 데이터 매퍼의 구현 방법은 사실상 테이블 데이터 게이트웨이의 구현 코드에 '이미 로딩한 객체의 캐싱' 및 '추상 클래스를 통한 공통 코드 처리'를 추가 구현한 정도에 지나지 않으며, 같은 책의 뒤에서 소개하고 있는 메타 데이터 매퍼가 실제로 쓰임새 있는 패턴이라고 볼 수 있다. 메타 데이터 매퍼 패턴은 데이터 매퍼 개념을 범용적으로 확장한 것이므로 본 글에서는 메타 데이터 매퍼에 대해서 살펴볼 것이다.

메타 데이터 매퍼 패턴은 아래 그림과 같이 구성된다.


위 그림에서 DataMapper는 모델 객체가 테이블가 어떻게 연결되는지를 정의한 매핑 정보를 저장하고 있다. 예를 들어, DataMapper는 Team 객체의 id 프로퍼티와 데이터베이스의 TEAM 테이블의 TEAM_ID 필드가 매핑된다는 정보를 갖고 있다. 이 매핑 정보를 이용해서 DataMapper는 TEAM 테이블로부터 데이터를 읽어와 Team 객체를 생성하게 되며, Team 객체에 저장되어 있는 정보를 TEAM 테이블에 반영하게 된다.

DataMapper는 <객체, 테이블> 매핑 정보만 갖고 있으면 모든 CRUD 작업을 처리할 수 있게 된다. 설명에서 느꼈겠지만, 마틴 파울러가 설명한 데이터 매퍼 및 메타 데이터 매퍼 패턴은 객체(Object)-관계 데이터베이스(Relation Database) 매핑, 즉, OR 매핑에 대한 패턴이다.

구현 방법

메타 데이터 매퍼 패턴, 즉, OR 매핑을 구현하기 위해서는 다음의 정보가 필요하다.

  • 객체와 매핑할 테이블은 무엇인가?
  • 객체의 프로퍼티를 테이블의 어떤 필드에 매핑할 것인가?
보통은 위의 매핑 정보를 설정 파일로 처리한다. 예를 들어, 아래와 같은 설정 파일을 사용할 수 있다.

   <?xml version="1.0" ?>
   
   <mapping>
       <object class="madvirus.javacan.Employ" table="EMPLOY">
           <property name="id" field="EMPLOY_ID" />
           <property name="name" field="NAME" />
           <property name="address" field="ADDRESS" />
           
           <primary-key>
               <property>id</property>
           <primary-key>
       </object>
   </mapping>

위의 설정 파일을 보면 Employ 객체와 매핑되는 테이블이 EMPLOY 테이블이며, 객체의 id 프로퍼티, name 프로퍼티, address 프로퍼티는 각각 EMPLOY 테이블의 EMPLOY_ID 필드, NAME 필드, ADDRESS 필드와 매핑된다는 것을 나타내고 있음을 쉽게 알 수 있다.

DataMapper는 위와 같은 형태의 설정 파일로부터 매핑 정보를 읽어와 객체와 테이블 사이의 매핑을 알맞게 처리하게 된다. 위의 매핑 정보는 다음과 같은 형태의 클래스를 사용해서 저장될 것이다.

    public class MetaData {
        private Class object;
        private String tableName;
        private List<FieldMapping> fieldMappingList = new ArrayList<FieldMapping>();
        private List<FieldMapping> pkFieldList = new ArrayList<FieldMapping>();
        
        public List<FieldMapping> getFieldMappingList() {
            return fieldMappingList
        }
        public List<FieldMapping> getPKFieldList() {
            return pkFieldList;
        }
        ...
    }
    
    public class FieldMapping {
        private String property; // <property> 태그의 name 속성값
        private String field; // <property> 태그의 field 속성값
        
        ...
    }

DataMapper는 위의 메타 정보를 사용해서 알맞은 쿼리를 생성한다. 예를 들어, 특정 테이블로부터 데이터를 읽어오는 select() 메소드는 다음과 같은 형태가 될 것이다.

    public class DataMapper {
        Map<MetaData> map = new java.util.HashMap<MetaData>(); // <Object타입, MetaData> 매핑 저장
        
        public Object select(Class objectType, Map keyValue) {
            MetaData metaData = map.get(objectType);
            List<FieldMapping> fieldList = metaData.getFieldMappingList();
            List<FieldMapping> pkList = metaData.getFieldMappingList();
            
            StringBuffer query = new StringBuffer();
            query.append("select ");
            for (int i = 0 ; i < fieldList.size() ; i++) {
                query.append(fieldList.get(i).getField());
                if (i < fieldList.size() - 1) query.append(", ");
            }
            query.append(" from ").append(metaData.getTableName()).append(" where ");
            
            for (int i = 0 ; i < pkList.size() ; i++) {
                query.append(pkList.get(i).getField());
                query.append(" = ? ");
                if (i < pkList.size() - 1) query.append(" and ");
            }
            
            String sql = query.toString();
            
            PreparedStatement pstmt = null;
            ResultSet rs = null;
            
            try {
                pstmt = DB.prepareStatement(sql);
                for (int i = 0 ; i < pkList.size() ; i++) {
                    Object val = keyValue.get(pkList.get(i).getProperty());
                    
                    // setParameter 메소드는 PreparedStatement의 IN 파라미터값을
                    // val 객체의 타입에 알맞게 처리해준다고 가정하자.
                    setParameter(pstmt, i+1, val); 
                }
                rs = pstmt.executeQuery();
                if (rs.next()) {
                    Object obj = objectType.newInstance();
                    ... // rs의 값을 obj에 복사.
                    return object;
                }
            } finally {
                ...
            }
        }
    }

위 코드에서 볼 수 있듯이 객체와 테이블 사이의 관계를 DataMapper가 모두 처리해주기 때문에, 객체는 테이블에 대해서 전혀 알 필요가 없으며, 심지어 DataMapper의 존재에 대해서도 전혀 알 필요가 없다. 도데인 영역의 객체가 테이블과 DataMapper에 의존적이지 않기 때문에 데이터베이스를 변경하거나 테이블이 변경될 경우 모델에 끼치는 영향을 최소화할 수 있다.

데이터 매퍼의 장점

웹 어플리케이션 프로그래밍의 90% 이상이 데이터베이스와 연동하는 코드를 포함하고 있다. CRUD 코드를 일일이 작성하는 것은 굉장히 지루하고 실수를 유발하기 쉬울 뿐만 아니라 코딩 시간의 많은 부분을 차지하는 작업인데, 데이터 매퍼를 사용함으로써 개발 시간을 상당량 줄일 수 있게 되며 잘못된 코딩에 따른 버그도 줄일 수 있게 된다.

CRUD 코드를 작성하는 데 소모되는 시간을 줄일 수 있다는 것은 비즈니스 로직 부분의 코드를 작성하는데 더 많은 시간을 투자할 수 있다는 것을 의미한다. 로직 부분에 집중할 수 있는 시간이 늘어남으로써 빠른 시간에 원하는 어플리케이션을 구현할 수 있게 된다.

또한, 데이터 매퍼를 사용하게 되면, 테이블이 변경될 때 객체의 변경을 최소화시킬 수 있다. 예를 들어, 테이블 스키마에서 필드 하나를 제거했다고 해 보자. 이 경우 객체는 전혀 변경할 필요가 없이 매핑 정보만 변경하면 된다. 필드가 삭제되었다고 해서 변경해야 할 쿼리는 하나도 없는 것이다.

반대로, 테이블에 저장해야 하는 객체의 프로퍼티가 새롭게 추가되었다고 해 보자. 이때 해야 할 작업은 테이블에 관련 필드를 추가하고 매핑 정보를 변경하는 것이다. 앞의 경우와 마찬가지로 변경해야 할 쿼리는 전혀 없다.

마지막으로 객체가 데이터 매퍼에 의존적이지 않기 때문에, 데이터 매퍼를 독립적으로 유지할 수 있다. 즉, 객체에 상관없이 데이터 매퍼를 수정할 수 있으며, 심지어 데이터 매퍼 구현 클래스를 변경할 수도 있다.

데이터 매퍼를 직접 구현할 필요는 없다!

의욕이 높거나 한번 해보고 싶은 마음에 데이터 매퍼를 직접 구현해보는 것은 좋으나, 도메인 모델에 맞는 데이터 매퍼를 직접 구현하는 것은 쉬운 일이 아니다. 또한, 데이터 매퍼를 구현하는 시간이 짧지도 않다. 따라서, 미리 구현해둔 데이터 매퍼가 없거나 또는 기존에 구현한 데이터 매퍼가 프로젝트에서 사용하는 도메인 모델에 잘 맞지 않을 경우에는, 데이터 매퍼를 직접 구현하는 것 보다는 이미 존재하는 데이터 매퍼 라이브러리를 사용하는 것이 가장 좋다. 예를 들어, Hibernate와 같이 널리 사용되는 OR 매핑 도구를 사용하는 것이 직접 구현하는 것보다 프로젝트 개발 시간을 단축시키는 데 도움이 될 것이다.

관련링크:

 

Posted by 최범균 madvirus

댓글을 달아 주세요