반응형
Hibernate에서 객체 사이의 연관, 클래스 상속 관계, 복합키를 설정하는 방법에 대해서 살펴본다.
클래스 상속과 테이블 매핑
먼저 살펴볼 내용은 클래스 상속 관계를 어떻게 테이블과 매핑할 것인가에 대한 내용이다. 클래스 계층을 ORM으로 처리하는 방식은 크게 다음과 같은 세 가지가 존재한다.
클래스 마다 개별적 테이블 사용
먼저 클래스 마다 개별적 테이블을 사용하는 방법을 살펴보도록 하자. 이 방식을 그림으로 설명하자면 다음과 같이 표현할 수 있다.
위 그림에서 Work 클래스는 추상 클래스이며, Book 클래스와 Article 클래스는 Work 클래스를 상속받은 하위 클래스이다. 이 클래스 계층 정보를 저장할 테이블을 보면 하위 클래스와 1-1 매핑되어 있는데, 여기서 중요한 건 하위 클래스에 매핑되는 테이블은 하위 클래스의 프로퍼티 뿐만 아니라 상위 클래스의 프로퍼티까지도 매핑한다는 점이다. 예를 들어, BOOK_WORK 테이블은 Book 클래스의 프로퍼티 뿐만 아니라 Work 클래스의 프로퍼티를 저장할 수 있는 컬럼이 정의되어 있으며, ARTICLE_WORK 테이블 역시 Article 클래스와 Work 클래스의 프로퍼티를 저장할 수 있도록 되어 있다.
이 방식은 사실상 다형성을 지원하지 않게 된다. 예를 들어, 위의 매핑을 처리하기 위한 설정 파일을 살펴보자.
이 설정 파일의 문제는 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에서 표현할 수 있다.
먼저 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가 처리할 매핑 정보를 담게 된다. 아래 코드는 위 그림에서 보여준 매핑을 설정한 것이다.
joined-subclass 태그는 key 태그를 갖는데, 이 key 태그는 하위 클래스에 매핑되는 테이블의 주요키이면서 외부키인 필드를 정의하게 된다. Hibernate는 이 key 태그에서 졍의한 컬럼과 상위 클래스의 id 태그에 명시한 컬럼을 외부 조인해서 데이터를 읽어오게 된다. 하지만, 외부 조인을 한다는 데서 알 수 있듯이 높은 성능을 보장하지는 못한다.
어떤 방식을 선택할 것인가?
클래스 계층 구조와 테이블 사이의 매핑을 처리하는 세 가지 방법이 있는데 이들은 나름대로의 특징이 있다. 일반적으로는 다음과 같은 규칙을 사용해서 매핑 방식을 선택한다.
클래스 사이의 연관 처리
객체 지향적으로 비즈니스 도메인 영역을 설계하면 객체들 사이에 다양한 관계가 형성된다. 두 객체가 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 항목에 입찰했는 지 참조하기 위해 프로퍼티를 가질 수 있을 것이다.
BID-ITEM의 관계는 many-to-one 이므로 매핑 설정 파일에서는 다음과 같이 Bid의 매핑 정보를 표시할 수 있다.
many-to-one의 관계를 표현하고 싶을 때에는 위 코드에서와 같이 매핑 파일에서 many-to-one 태그를 사용하면 된다. many-to-one 태그에서 사용되는 주요 속성은 위 코드에서 보듯이 세 가지인데, 각 속성은 다음과 같은 의미를 갖는다.
ITEM-BID의 관계는 BID-ITEM 관계와 정반대이므로 one-to-many의 관계가 된다. 즉, 하나의 ITEM에 대해서 여러개의 BID가 존재할 수 있는 것이다. 이를 자바 코드로 표현하면 다음과 비슷할 것이다.
위 코드에서는 여러 Bid 객체를 저장하기 위해 Set 타입을 사용했는데 이는 Hibernate가 many-to-one의 관계를 지원해주는 타입 중의 하나로서, 설정 파일에서는 다음과 같이 매핑 정보를 설정할 수 있다.
위 코드에서 먼저 set 태그는 프로퍼티 타입이 Set일 때 사용된다. set 태그의 주요 속성은 다음과 같다.
두번째 자식 태그는 one-to-many 태그로서 class 속성을 사용해서 many 부분에 해당하는 클래스의 타입을 명시한다.
하지만, 위의 설정만으로는 끝나지 않는다. 지금까지 설정한 것은 단지 두 개의 단방향 연관을 맺은 것에 불과하다. 예를 들어, Item 클래스의 addBid() 메소드 코드를 보자.
위 코드는 Item 객체와 Bid 객체를 연결해주는 클래스인데, 데이터베이스 입장에서 보면 BID 테이블의 ITEM_ID 컬럼 값을 변경해주는 기능에 해당한다. 문제는 Hibernate가 위 코드를 처리할 때 두번의 쿼리를 실행한다는 점이다. 즉, Bid 객체에 대해서 한번의 쿼리를 수행하고 또한 Item의 bids 프로퍼티가 변경된 것에 대해서 한번의 쿼리를 수행하게 된다.
이렇게 두번의 쿼리가 발생하게 되는 이유는 Item과 Bid가 각각 단방향 관계로 설정되어 있기 때문이다. 양방향 관계로 연결시켜줌으로써 이 문제를 처리할 수 있는데, 양방향 연결은 다음과 같이 set 태그에 inverse 속성값을 true로 지정해줌으로써 명시할 수 있다.
하지만, 이것만으로는 부족하다. 문제가 되는 부분은 Item을 저장하더라도 Bid가 저장되는 것은 아니라는 것이다. 예를 들어, 다음의 코드를 보자.
위 코드를 실행했을 때 원하는 것은 Item이 저장될 때 자동으로 새로 생성한 Bid도 저장되는 것이다. 하지만, Hibernate는 Item 객체만 저장할 뿐 Bid 객체는 저장되지 않는다. Bid 객체도 저장하도록 하려면 다음과 같이 cascade 속성을 set 태그에 명시해주어야 한다.
위와 같이 cascade 속성의 값을 "save-update"로 지정하면 Item 객체가 저장될 때 관련 객체인 Bid도 함께 저장된다. 저장될 때 뿐만 아니라 기존에 존재하는 Item 객체에 새로운 Bid를 추가한 후, Item 객체를 update() 할 때에도 Bid가 저장된다.
one-to-many 관계에서 one에 해당하는 객체가 삭제될 때 many에 해당하는 객체들도 함께 삭제되어야 하는 경우도 있을 것이다. 예를 들어, 경매 제품 항목(Item)이 삭제된다면 경매와 관련된 입찰 항목(Bid)은 의미가 없어지며, 함께 삭제되는 것이 올바른 구조일 것이다. 이처럼 one-to-many의 관계가 부모-자식 관계인 경우에는 다음과 같이 cascade 속성의 값을 "all-delete-orphan"으로 지정하면 된다.
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은 아래와 같은 형태의 코드를 갖게 될 것이다.
위 코드와 같이 주요키로 연관된 두 객체와 테이블 사이의 매핑은 아래와 같이 one-to-one 태그를 사용해서 처리할 수 있다.
먼저, 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 태그를 사용하여 외부키 부분을 설정할 수 있다.
위 코드에서 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 태그를 사용하게 된다.
one-to-one 태그는 property-ref 속성을 사용해서 자기 자신을 참조하는 객체가 외부키 연관으로 사용하는 프로퍼티의 이름을 입력받는다. 예를 들어, Item 클래스는 detail 프로퍼티를 사용해서 ItemDetail 클래스로의 외부키 연관을 처리하므로 ItemDetail 클래스 매핑 설정에서 one-to-one 태그의 property-ref 속성의 값을 "detail"로 주었다.
복합키의 처리
대부분의 경우 주요키는 하나의 컬럼을 사용하지만, 다수의 컬럼을 주요키로 사용하는 경우도 있다. 이런 키를 복합키(composite key)라고 부르며, Hibernate는 composite-id 태그를 사용해서 복합키를 표시할 수 있도록 하고 있다. composite-key의 사용방법은 매우 간단한데, 다음과 같이 key-property 태그를 사용해서 복합키와 관련된 프로퍼티와 컬럼을 표시해주면 된다.
복합키를 별도의 클래스 표현할 수도 있을 것이다. 예를 들어, 위에서 설정한 User 클래스의 복합키를 다음과 같은 UserId 클래스로 표현한다고 해보자.
위와 같이 복합키를 저장하는 별도의 클래스가 존재할 경우 User 클래스는 다음과 같이 복합키 클래스를 저장할 별도의 프로퍼티를 갖게 될 것이다.
이 경우 다음가 같이 composite-id 태그를 사용할 수 있다.
다음 글에서는
본 글에서는 클래스 사이의 상속 및 one-to-many, many-to-one, one-to-one 연관을 처리하는 방법에 대해서 살펴봤고, 복합키를 설정하는 방법에 대해서도 살펴보았다. 이번 글을 통해 다양한 종류의 매핑을 처리할 수 있게 되었을 것이다. 이렇게 매핑 설정한 뒤 필요한 것은 실제로 퍼시스턴트 객체를 생성하고, 변경하고, 삭제하는 등의 작업을 어플리케이션에서 수행하는 것이다. 다음 글에서는 퍼시스턴트 객체를 어떻게 사용하는 지 그리고 객체의 영속성 상태 및 영속성 전이에 대한 내용을 살펴보기로 하자.
관련링크:
클래스 상속과 테이블 매핑
먼저 살펴볼 내용은 클래스 상속 관계를 어떻게 테이블과 매핑할 것인가에 대한 내용이다. 클래스 계층을 ORM으로 처리하는 방식은 크게 다음과 같은 세 가지가 존재한다.
- 클래스 마다 개별적 테이블 - 가장 단순한 방법으로서, 계층도에 있는 클래스와 완전히 매핑되는 테이블을 사용한다.
- 클래스 계층 구조에 대해 하나의 테이블 - 계층도에 있는 모든 클래스를 하나의 테이블과 매핑시킨다.
- 하위 클래스 마다 개별적 테이블 - 클래스 계층도와 매핑되도록 테이블을 설계한다. 테이블의 외부키를 사용해서 클래스의 상속 관계를 표현한다.
클래스 마다 개별적 테이블 사용
먼저 클래스 마다 개별적 테이블을 사용하는 방법을 살펴보도록 하자. 이 방식을 그림으로 설명하자면 다음과 같이 표현할 수 있다.
위 그림에서 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>
...
</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>
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>
<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 태그에 명시한 컬럼을 외부 조인해서 데이터를 읽어오게 된다. 하지만, 외부 조인을 한다는 데서 알 수 있듯이 높은 성능을 보장하지는 못한다.
어떤 방식을 선택할 것인가?
클래스 계층 구조와 테이블 사이의 매핑을 처리하는 세 가지 방법이 있는데 이들은 나름대로의 특징이 있다. 일반적으로는 다음과 같은 규칙을 사용해서 매핑 방식을 선택한다.
- 클래스 마다 개별적 테이블 사용 - 계층 관계의 다형성 및 다형성을 이용한 쿼리가 필요하지 않은 경우
- 클래스 계층 구조에 대해 테이블 사용 - 계층 관계의 다형성이 필요하고 하위 클래스가 소수의 프로퍼티만을 가진 경우
- 하위 클래스 마다 개별적 테이블 사용 - 계층 관계의 다형성이 필요하고 하위 클래스가 다수의 프로퍼티를 가진 경우
클래스 사이의 연관 처리
객체 지향적으로 비즈니스 도메인 영역을 설계하면 객체들 사이에 다양한 관계가 형성된다. 두 객체가 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;
}
...
}
...
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
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에 해당하는 클래스의 이름.
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);
}
...
}
...
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 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"를 사용하였다.
두번째 자식 태그는 one-to-many 태그로서 class 속성을 사용해서 many 부분에 해당하는 클래스의 타입을 명시한다.
하지만, 위의 설정만으로는 끝나지 않는다. 지금까지 설정한 것은 단지 두 개의 단방향 연관을 맺은 것에 불과하다. 예를 들어, Item 클래스의 addBid() 메소드 코드를 보자.
bid.setItem(item);
bids.add(bid);
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>
...
<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();
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>
...
<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>
...
<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;
}
...
}
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>
<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>
<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>
<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>
<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() 메소드
}
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;
}
...
}
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>
<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 연관을 처리하는 방법에 대해서 살펴봤고, 복합키를 설정하는 방법에 대해서도 살펴보았다. 이번 글을 통해 다양한 종류의 매핑을 처리할 수 있게 되었을 것이다. 이렇게 매핑 설정한 뒤 필요한 것은 실제로 퍼시스턴트 객체를 생성하고, 변경하고, 삭제하는 등의 작업을 어플리케이션에서 수행하는 것이다. 다음 글에서는 퍼시스턴트 객체를 어떻게 사용하는 지 그리고 객체의 영속성 상태 및 영속성 전이에 대한 내용을 살펴보기로 하자.
관련링크: