주요글: 도커 시작하기
반응형
Hibernate에서 콜렉션 프로퍼티와 many-to-many 관계를 매핑하는 방법을 살펴본다.

콜렉션 매핑 처리

앞선 글들에서 one-to-many의 관계를 설명할 때 set 태그를 사용했던 것을 기억할 것이다. 이 set 태그는 자바의 콜렉션 타입인 java.util.Set을 사용할 때 이용되는데 Hibernate는 Set 말고도 List, Map 그리고 자바 콜렉션에 포함되어 있지는 않은 Bag을 지원하고 있다.

Set, (ID) Bag, List, Map을 어떻게 매핑 처리하는 지 살펴보고, 복합 타입을 콜렉션 타입으로 처리하는 방법도 살펴본다.

Set을 이용한 저장

먼저 Set을 이용하는 방법을 살펴보자. Set을 매핑하는 방법을 살펴보기 전에 예제로 사용될 자바 코드를 살펴보자. 아래 자바 클래스의 images 프로퍼티가 Set 이다.

    public class Content {
        private Set images = new java.util.HashSet();
        
        public Set getImages() {
            return images;
        }
        public void setImages(Set images) {
            this.images = images;
        }
        ...
    }

위 코드에서 Set 타입 프로퍼티인 images는 이미지의 이름을 나타내는 String의 집합을 저장하기 위해서 사용된다. 이 정보를 저장하려면 다음과 같이 두 개의 테이블이 필요할 것이다.


이 관계를 Hibernate의 매핑 설정 파일에서 표현하려면 다음과 같이 set 태그를 사용해주면 된다.

    <class name="Content" table="CONTENT">
        ...
        <set name="images" table="CONTENT_IMAGE" lazy="true">
            <key column="CONTENT_ID" />
            <element type="string" column="FILENAME" not-null="true" />
        </set>
    </class>

위 매핑에서 사용된 태그는 set, key, element이며 각 태그의 의미는 아래와 같다.

  • set - 프로퍼티의 타입이 Set임을 나타낸다. table 속성은 Set에 저장될 요소의 데이터를 저장하고 있는 테이블을 나타낸다.
  • key - set에서 명시한 테이블에서 부모 테이블에 대한 외부키값을 저장하는 컬럼을 명시한다. 예제의 경우 CONTENT_IMAGE 테이블의 CONTENT_ID 컬럼이 CONTENT 테이블을 참조하는 외부키에 해당한다.
  • element - Set에 삽입될 요소에 대한 정보를 지정한다. type 속성은 Set에 저장될 요소의 타입을, column은 Set에 저장될 요소의 값을 읽어올 컬럼명을 나타낸다.
Set에서 중요한 점은 중복된 값을 저장할 수 없다는 점이다. 따라서, Set에 해당하는 데이터를 저장하는 테이블인 CONTENT_IMAGE는 CONTENT_ID, FILENAME 컬럼을 주요키로 사용해야 한다.

Set에 데이터를 저장하거나 Set에서 데이터를 삭제할 때에는 Set 프로퍼티를 직접 변경하면 된다. 예를 들어, Set에 새로운 데이터를 저장할 때에는 다음과 같은 코드를 사용하면 된다.

    Session session = ...;
    Transaction tx = session.beginTransaction();
    
    Content content = new Content();
    Set images = content.getImages();
    images.add("img1.gif");
    images.add("img2.gif");
    
    tx.commit();
    session.close();

Bag을 이용한 저장

Set과 달리 Bag은 중복된 값을 저장할 수 있는 콜렉션이다. 자바의 콜렉션은 Bag을 제공하지 않지만, 프로퍼티 타입을 List로 지정해서 Bag을 구현할 수 있다. java.util.ArrayList와 같은 클래스를 Bag의 구현 클래스로 사용하면 된다. 예를 들면 아래와 같다.

    public class Content {
        ...
        private List images = new ArrayList();
        
        public List getImages() {
            return images;
        }
        public void setImages(List images) {
            this.images = images;
        }
    }

Bag을 사용할 경우 중복된 값을 저장할 수 있기 때문에, 아래 그림과 같이 Bag의 요소를 저장하는 CONTENT_IMAGE 테이블이 외부키만 가질 뿐 주요키는 존재하지 않게 된다.


설정 파일에 관계를 표시하는 방법은 Set과 완전히 동일하며 차이점이라면 set 태그 대신에 bag 태그를 사용하는 것 뿐이다.

    <class name="javacan.hibernate.test.Content" table="CONTENT">
        ...
        <bag name="images" table="CONTENT_IMAGE" lazy="true">
            <key column="CONTENT_ID" />
            <element type="string" column="FILENAME" not-null="true" />
        </bag>
    </class>

순서가 있는 Bag을 이용한 저장

앞서 Bag은 순서가 없었는데, idbag 태그를 사용하여 순서가 있는 Bag을 사용할 수 있다. 순서가 있는 Bag을 사용하려면 먼저 Bag의 요소를 저장할 테이블에 순서값을 갖는 주요키를 추가해주어야 한다. 예를 들어, CONTENT_IMAGE 테이블에 다음과 같이 주요키 컬럼을 추가할 수 있다.


위와 같이 주요키를 사용하면, 주요키값을 사용해서 순서를 갖는 Bag을 정의할 수 있게 된다. 순서가 있는 Bag은 idbag 태그를 사용해서 정의하면 아래와 같이 collection-id 태그를 사용해서 주요키 컬럼에 대한 정의를 추가하는 것만 제외하면 bag을 정의할 때와 동일한 코드를 사용한다.

    <class name="javacan.hibernate.test.Content" table="CONTENT">
        ...
        <idbag name="images" table="CONTENT_IMAGE" lazy="true">
            <collection-id type="int" column="CONTENT_IMAGE_ID">
                <generator class="increment" />
            </collection-id>
            <key column="CONTENT_ID" />
            <element type="string" column="FILENAME" not-null="true" />
        </idbag>
    </class>

List를 이용한 저장

List는 인덱스라는 개념을 갖고 있다. List는 0번째 인덱스에 저장되는 요소, 5번째 인덱스에 저장되는 요소와 같이 요소의 저장 위치 정보를 필요로 하는 것이다. 따라서 List를 저장하기 위해서는 요소의 위치를 나타내는 인덱스값을 저장할 컬럼을 필요로 한다. 예를 들어, 아래 그림에서 CONTENT_IMAGE 테이블의 POSITION 컬럼과 같이 인덱스를 저장할 컬럼을 필요로 하는 것이다.


위와 같이 List의 인덱스 값을 저장할 컬럼을 지정했다면, 다음과 같이 list 태그를 사용해서 List 타입의 프로퍼티를 설정할 수 있다.

    <class name="javacan.hibernate.test.Content" table="CONTENT">
        ...
        <list name="images" table="CONTENT_IMAGE" lazy="true">
            <key column="CONTENT_ID" />
            <index column="POSITION" />
            <element type="string" column="FILENAME" not-null="true" />
        </list>
    </class>

위 코드에서 중요한 건 index 태그인데, 이 index 태그는 List의 인덱스 값을 저장할 컬럼을 지정해준다.

Map을 이용한 저장

프로퍼티에 Map을 사용해야 하는 경우도 있다. 예를 들어, 회원 가입시 회원이 답한 질문에 대해서만 값을 기록하고 싶다고 해 보자. 이 경우 다음과 같이 테이블이 구성될 것이다. ANSWER 테이블의 MEMBER_ID 컬럼은 외부키이며, QUESTION_NO 컬럼은 Map의 키를 저장한다. Map의 키는 고유해야 하므로 테이블의 주요키는 <외부키(MEMBER_ID), Map의 키(QUESTION_NO)>의 복합키로 구성된다.


이때, MEMBER 테이블과 매핑되는 자바 코드를 아래와 같이 작성할 수 있다.

    public class Member {
        ...
        
        private Map answers = new java.util.HashMap();
        
        public Map getAnswers() {
            return answers;
        }
        public void setAnswers(Map answers) {
            this.answers = answers;
        }
    }

위와 같은 자바 코드가 있을 때, Map 프로퍼티에 대한 매핑 설정은 아래와 같이 지정할 수 있다.

    <class name="javacan.hibernate.test.Member" table="MEMBER">
        ...
        <map name="answers" table="ANSWER" lazy="true">
            <key column="MEMBER_ID" />
            <index column="QUESTION_NO" type="int" />
            <element type="string" column="ANSWER" not-null="true" />
        </map>
    </class>

Map의 키를 저장하는 컬럼을 index 태그로 지정하며, Map의 값을 저장하는 컬럼을 element 태그로 명시한다. Map의 키는 외부키마다 고유하면 되므로, Map은 중복된 값을 가질 수 있다. (즉, ANSWER 컬럼에 중복된 값을 가질 수 있다.)

복합 프로퍼티의 집합 처리

앞서 살펴봤던 Content에서 이미지의 크기 정보까지 저장해야 한다고 해 보자. 이 경우, 다음과 같이 테이블 관계는 다음과 같이 변경될 것이다.


위 관계를 나타내기 위해서는 다음과 같이 Content 클래스 뿐만 아니라 CONTENT_IMAGE 테이블의 정보를 담을 클래스가 필요하다. 즉, 아래와 같이 두 개의 클래스를 필요로 한다.

    public class Content {
        ...
        private List images = new ArrayList();
        
        public List getImages() {
            return images;
        }
        public void setImages(List images) {
            this.images = images;
        }
    }
    
    public class Image {
        private String filename;
        private int width;
        private int height;
        
        public String getFilename() {
            return filename;
        }
        public void setFilename(String filename) {
            this.filename = filename;
        }
        public int getHeight() {
            return height;
        }
        public void setHeight(int height) {
            this.height = height;
        }
        public int getWidth() {
            return width;
        }
        public void setWidth(int width) {
            this.width = width;
        }
    }

Content의 images 프로퍼티에 저장되는 값은 Image가 될 것이다. 이렇게 집합에 들어갈 요소가 여러 프로퍼티로 구성된 경우 element 태그 대신에 composite-element 태그를 사용하면 된다. composite-element 태그는 다음과 같이 사용된다.

    <class name="javacan.hibernate.test.Content" table="CONTENT">
        ...
        <list name="images" table="CONTENT_IMAGE" lazy="true">
            <key column="CONTENT_ID" />
            <index column="POSITION" />
            <composite-element class="javacan.hibernate.test.Image">
                <property name="filename" column="FILENAME" />
                <property name="width" column="WIDTH" />
                <property name="height" column="HEIGHT" />
            </composite-element>
        </list>
    </class>

composite-element 사용시 부모/자식 간에 양방향 연결 매핑

composite-element 태그를 사용할 때, 위와 같이 설정할 경우 Content(부모)에서 Image(자식)로만 접근이 가능하며 반대로 Image(자식)에서 Content(부모)로는 접근할 수가 없다. (예를 들어, Image.getContent()와 같은 메소드가 없다.) 양방향으로 접근할 수 있도록 하려면 Image 클래스에서 다음의 두 메소드를 추가하고,

    public class Image {
        ...
        private Content content;
        
        public void setContent(Content content) {
            this.content = content;
        }
        public Content getContent() {
            return content;
        }
    }

그런 뒤, 매핑 설정 파일에서 다음과 같이 composite-element 태그에 parent 태그를 추가해주면 된다. parent 태그의 name 속성은 부모를 참조하는 프로퍼티의 이름을 나타낸다.

    <class name="javacan.hibernate.test.Content" table="CONTENT">
        ...
        <list name="images" table="CONTENT_IMAGE" lazy="true">
            <key column="CONTENT_ID" />
            <index column="POSITION" />
            <composite-element class="javacan.hibernate.test.Image">
                <parent name="content" />
                <property name="filename" column="FILENAME" />
                <property name="width" column="WIDTH" />
                <property name="height" column="HEIGHT" />
            </composite-element>
        </list>
    </class>

sort와 order-by를 사용한 정렬

Hibernate는 콜렉션의 정렬과 관련해서 두 가지 속성 sort와 order-by를 제공한다. sort는 데이터베이스에서 데이터를 읽어온 뒤 메모리 상에서 정렬을 수행하는 방식이며, order-by는 데이터베이스에서 정렬된 값을 읽어오는 방식이다.

sort 속성을 사용한 Map과 Set의 정렬

먼저 sort 방식부터 살펴보도록 하자. Map과 Set은 sort 속성을 사용해서 데이터를 정렬할 수 있다. 예를 들어, Map에 대한 데이터를 메모리에서 정렬하고 싶다면 다음과 같이 sort 속성을 지정하면 된다.

    <class name="javacan.hibernate.test.Member" table="MEMBER">
        ...
        <map name="answers" table="ANSWER" sort="natural" lazy="true">
            <key column="MEMBER_ID" />
            <index column="QUESTION_NO" type="int" />
            <element type="string" column="ANSWER" not-null="true" />
        </map>
    </class>

Hibernate는 map 태그의 sort 속성을 "natural"로 지정하면 SortedMap을 사용하여 관련 값을 저장한다. 정렬 대상은 Map의 키에 해당하는 필드이며, Object.compareTo() 메소드를 사용하여 키를 비교하게 된다.

만약 composite-element와 같이 복합 요소를 값으로 사용하는 경우에는 다음과 같이 sort 속성의 값에 Comparator 클래스를 명시해주면 된다.

    <class name="javacan.hibernate.test.Member" table="MEMBER">
        ...
        <map name="answers" table="ANSWER" lazy="true"
                sort="javacan.hibernate.test.ImageComparator" >
            <key column="MEMBER_ID" />
            <index column="QUESTION_NO" type="int" />
            <composite-element class="javacan.hibernate.test.Image">
                ...
            </composite-element>
        </map>
    </class>

sort 속성에 명시한 Comparator는 composite-element에서 명시한 클래스의 알맞은 속성값을 사용하여 값을 비교하면 된다.

set 태그에 sort 속성을 추가하면 TreeSet과 같은 방식으로 동작하며, map의 경우와 마찬가지로 "natural"을 값으로 갖거나 또는 Comparator를 직접 명시할 수도 있다.

Bag과 List는 sort를 사용하여 정렬할 수 없다. (Bag의 경우는 마땅한 구현체가 존재하지 않으며, List는 인덱스값을 사용하여 정렬하기 때문이다.)

order-by 속성을 사용한 Map, Set, Bag의 정렬

order-by 속성은 SQL의 ORDER BY 절을 통해 데이터베이스의 정렬 기능을 사용하여 목록을 읽어오는 방식이다. 이 속성을 통해 테이블의 임의의 컬럼을 기준으로 데이터를 읽어올 수 있게 된다. Map과 Set 그리고 Bag에 대해서 이 속성을 사용할 수 있으며, 다음과 같이 SQL 쿼리의 ORDER BY 부분에 들어갈 쿼리를 입력해주면 된다.

    <class name="javacan.hibernate.test.Member" table="MEMBER">
        ...
        <map name="answers" table="ANSWER" lazy="true"
                  order-by="QUESTION_NO asc">
            <key column="MEMBER_ID" />
            <index column="QUESTION_NO" type="int" />
            <element type="string" column="ANSWER" not-null="true" />
        </map>
    </class>

order-by 속성을 사용할 때 주의할 점은 Set과 Map의 경우 JDK 1.4 또는 그 이상의 버전에서만 지원하는 LinkedHashSet과 LinkedHashMap을 사용해서 매핑을 처리한다는 점이다. 따라서, JDK 1.3 을 사용하는 경우에는 Map과 Set에서 order-by 속성을 사용할 수 없다.

many-to-many 관계의 매핑 처리

때에 따라 many-to-many 관계를 표시해주어야 하는 경우도 있다. 보통 many-to-many 관계는 카테고리와 항목의 관계를 표시할 때 사용된다. 예를 들어, 쇼핑몰의 카테고리와 제품의 관계를 생각해보자. 카테고리에는 여러 제품이 포함될 수 있으며, 반대로 제품은 여러 카테고리에 포함될 수 있다.

이런 many-to-many의 관계를 표현하기 위해서는 다음과 같이 중간에 다리 역할을 하는 테이블을 필요로 한다.


테이블의 관계에서는 위와 같이 중간에 다리 역할을 하는 테이블이 존재하지만, 자바에서는 중간 객체를 필요로 하기 보다는 아래와 같이 콜렉션을 통해서 many-to-many 관계를 표시하게 될 것이다.

    public class Category {
        private Integer id;
        private Set items = new HashSet(); // Item에 대한 매핑 저장
        
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }
        public Set getItems() {
            return items;
        }
        public void setItems(Set items) {
            this.items = items;
        }
        ...
    }

    public class Item {
        private Integer id;
        private Set categories = new HashSet(); // Category에 대한 매핑 저장
        
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }
        public Set getCategories() {
            return categories;
        }
        public void setCategories(Set categories) {
            this.categories = categories;
        }
    }

이 관계를 표시하는 방법은 콜렉션과 one-to-many를 사용하던 것과 동일한데, one-to-many 대신에 다음과 같이 many-to-many 태그를 사용하여 표현하면 된다.

    <class name="javacan.hibernate.test.Category" table="CATEGORY" lazy="true">
        <id name="id" type="int" column="CATEGORY_CODE" unsaved-value="null">
            <generator class="increment" />
        </id>
        
        <set name="items" table="CATEGORY_ITEM" lazy="true" cascade="save-update">
            <key column="CATEGORY_CODE" />
            <many-to-many class="javacan.hibernate.test.Item" column="ITEM_ID" />
        </set>
    </class>
    
    <class name="javacan.hibernate.test.Item" table="ITEM" lazy="true">
        <id name="id" type="int" column="ITEM_ID" unsaved-value="null">
            <generator class="increment" />
        </id>

        <set name="categories" table="CATEGORY_ITEM" lazy="true" cascade="save-update"
             inverse="true">
            <key column="ITEM_ID" />
            <many-to-many class="javacan.hibernate.test.Category" column="CATEGORY_CODE" />
        </set>
    </class>

위 코드를 보면 many-to-many 관계를 표현하기 위해 콜렉션 태그인 set 태그에 many-to-many 태그를 중첩한 것을 알 수 있는데, 설정 정보에서 사용된 요소를 설명하면 아래와 같다.

  • set 태그의 table 속성 - 두 테이블을 연결해서 many-to-many 관계를 만들어주는 연관 테이블을 명시한다.
  • key 태그의 column 속성 - 연관 테이블에서 현재 클래스에 대한 외부키 저장 필드를 명시한다. 예를 들어, Category 클래스 설정 부분에서 key 태그의 column 속성은 CATEGORY_ITEM(연관 테이블)가 CATEGORY 테이블을 참조하는 컬럼인 CATEGORY_ID를 값으로 갖는다.
  • many-to-many 태그의 class 속성 - Set의 요소로 저장될 클래스. Category의 Set에는 Item이 저장되므로, Category에서는 이 속성의 값에 "Item"을 명시한다.
  • many-to-many 태그의 column 속성 - 연관 테이블에서 Set에 저장할 요소를 참조할 때 사용될 컬럼. 예를 들어, Category 클래스의 Set에는 Item이 포함되는데, CATEGORY_ITEM(연관 테이블)이 ITEM 테이블을 참조하는 컬럼은 ITEM_ID 이므로, Category에서는 이 속성의 값에 "ITEM_ID"를 명시한다.
Item 클래스와 관련된 set 태그를 보면 inverse 속성의 값이 true인 것을 알 수 있다. 양방향으로 연결하고 싶을 때에 inverse 속성의 값을 true로 지정한다고 했었는데, 실제로 이 inverse 속성의 값이 의미하는 바는, '연관의 반대편에서만 연관의 변경작업을 할 수 있다'는 의미이다. 예를 들어, Category-Item 관계에서 Item에 inverse 속성값이 true인데, 이는 Item에서 연관된 카테고리를 수정하더라도 그 변경 내역이 적용되지 않는다는 것을 의미한다. 다음의 코드를 보자.

    Transaction tx = session.beginTransaction();
    Item item = (Item)session.get(Item.class, id);
    Set categories = item.getCategories();
    categories.clear();
    tx.commit();

위 코드를 실행하면 예상은 Item이 관련 카테고리에 제거되는 것이겠지만, inverse 속성이 true이기 때문에 Item에서 변경한 Category와의 연관은 적용되지 않는다. Item-Category 관계에서 연관을 변경하고 싶다면, 다음과 같이 Category에서 처리해야 한다.

    Transaction tx = session.beginTransaction();
    Item item = (Item)session.get(Item.class, id);
    Iterator iter = item.getCategories().iterator();
    while (iter.hasNext()) {
        Category category = (Category)iter.next();
        category.getItems().remove(item);
    }
    tx.commit();

inverse 속성을 true로 지정할 관계를 어느 객체에서 처리해주어야 하는 지 기억해두기 바란다. 그래야 버그없는 어플리케이션을 개발할 수 있을 것이다. 관련링크:

+ Recent posts