행복한 아빠

[Grails1.0 사용자 가이드] 5. Object Relational Mapping (GORM) - 3 본문

Grails

[Grails1.0 사용자 가이드] 5. Object Relational Mapping (GORM) - 3

행복한아빠 2008. 3. 3. 12:35


5. Object Relational Mapping (GORM) - 3

  • 고급 GROM 기능들
  • 프로그램 방식의 트랜잭션
  • GORM과 제약조건


5.5 고급 GORM 기능들
다음의 장들에서는 캐싱, custom 매핑 그리고 이벤트를 포함한 GORM의 고급 사용방법을 다룬다.

5.5.1 이벤트와 자동 시간기록(Timestamping)
GORM은 삭제, 추가, 업데이트 같은 특정 이벤트가 발생할 때 수행하는 이벤트를 클로우저(closure)로 등록하도록 지원한다. 이벤트를 등록하려면 간단히 도메인 클래스에 관련 closure를 등록한다. 이벤트 종류는 다음과 같다.

beforeInsert 이벤트
객체가 DB에 저장되기 전에 실행됨

class Person {
   Date dateCreated
   def beforeInsert = {
       dateCreated = new Date()
   }
}


beforeUpdate 이벤트
이미 존재하는 객체가 업데이트되기 전에 실행됨

class Person {
   Date dateCreated
   Date lastUpdated
   def beforeInsert = {
       dateCreated = new Date()
   }
   def beforeUpdate = {
       lastUpdated = new Date()
   }
}


beforeDelete 이벤트
객체가 삭제되기 전에 실행됨

class Person {
   String name
   Date dateCreated
   Date lastUpdated
   def beforeDelete = {
      new ActivityTrace(eventName:"Person Deleted",data:name).save()
   }
}


onLoad 이벤트
객체가 DB에서 로드될 때 실행됨

class Person {
   String name
   Date dateCreated
   Date lastUpdated
   def onLoad = {
      name = "I'm loaded"
   }
}


자동 시간기록(timestamping)
위에서 객체의 갱신 자취(tack)를 유지할 용도로 lastUpdated와 dateCreated를 갱신하기 위해 이벤트를 사용한 예제가 있다. 하지만 이것은 실제로 필요없다. 단지 lastUpdated와 dateCreated 속성만 정의하면 이러한 것들은 GORM이 자동으로 갱신해 준다.

이러한 동작이 원하는 게 아니면 다음과 같이 이 기능을 못쓰게 할 수 있다.

class Person {
   Date dateCreated
   Date lastUpdated
   static mapping = {
      autoTimestamp false
   }
}


5.5.2 사용자 정의 ORM Mapping
Grails는 Object Relational Mapping Domain Specify Language(ORM DSL)을 통해 도메인 클래스를 많은 레가시 시스템 스키마에 매핑할 수 있다. 다음 장들은 ORM DSL로 가능한 것들을 다룬다.

사용자 삽입 이미지
GORM이 약속에 의해 정의하는 테이블, 컬럼 등의 이름을 사용하는데 만족한다면 이 기능은 필요없다. 만일 GORM이 레가시 스키마에 매핑하는 방법을 어떤 식으로든 조정할 필요가 있거나 캐싱을 수행하기 위해서만 이 기능이 필요하다.

사용자 정의 매핑은 도메인 클래스안에 static mapping 블럭으로 정의한다.

class Person {
  ..
  static mapping = {  }
}


5.5.2.1 테이블과 컬럼 이름

테이블 이름
클래스와 매핑되는 데이터베이스 테이블을 조정하기 위해서는 table을 사용한다.

class Person {
  ..
  static mapping = {
      table 'people'
  }
}

이 경우 클래스를 기본 이름인 person으로 매핑하지 않고 people이라는 테이블로 매핑한다.

컬럼 이름
각 데이터베이스의 각 컬럼으로 매핑하는 것도 역시 조정이 가능하다. 예를 들면 다음과 같이 변경할 수 있다.

class Person {
  String firstName
  static mapping = {
      table 'people'
      firstName column:'First_Name'
  }
}

이 경우 각 프로퍼티 이름과 매치되는 메소드 호출을 정의한다(이 경우 firstName). 그런 후 column이라는 named 파라메터로 매핑될 컬럼 이름을 지정한다.


컬럼 타입
GORM은 type 속성을 이용하여 DSL을 통한 Hibernate 타입 설정을 지원한다. 이것은 org.hibernate.types.UserType 클래스를 상속한 사용자 정의 타입도 해당된다. 이것은 특정 타입이 어떻게 저장되는지에 대한 복잡한 커스터마이징이 가능하게 한다. 예제로 PostCodeType 클래스를 만들었다면 다음과 같이  타입으로 사용할 수 있다.

class Address {
   String number
   String postCode
   static mapping = {
      postCode type:PostCodeType
   }
}

다른 방법으로는 Grails가 선택한 디폴트 타입 대신 Hibernate의 기본 타입들 중 하나로 매핑하고 싶으면 다음과 같이 사용한다.

class Address {
   String number
   String postCode
   static mapping = {
      postCode type:'text'
   }
}

이것은 사용하는 데이터베이스에 따라 postCode 컬럼을 SQL TEXT나 CLOB 타입으로 매핑한다.


일-대-일 매핑
관계를 사용하는 경우 관계를 매핑하기 위해 사용하는 foreign 키를 변경하는 것도 가능하다. 일-대-일 관계의 경우 이것은 어느 보통의 컬럼과 마찬가지로 정확하게 동일한다. 아래의 예를 살펴보자

class Person {
  String firstName
  Address address
  static mapping = {
      table 'people'
      firstName column:'First_Name'
      address column:'Person_Adress_Id'
  }
}

기본으로는 address 관계는 address_id라는 foreign 키 컬럼으로 매핑될 것이다. 위의 매핑을 사용함으로써 foreign 키 이름을 Peron_Adress_Id로 변경한다.


일-대-多 매핑
양방향 일-대-다에서는 이전 장의 일-대-일 관계의 예처럼 관계의 多쪽에서 컬럼 이름을 변경하는 것으로 간단히 foreign 키를 변경할 수 있다. 그러나 단 방향 관계에서는 관계 자체에 foreign 키를 지정해야 한다. 예를 들면 주어진 Person과 Address 사이의 단방향 일-대-다 관계에서 아래 코드는 address 테이블의 foreign 키를 바꿀 것이다.

class Person {
    String firstName
    static hasMany = [addresses:Address]
    static mapping = {
        table 'people'
        firstName column:'First_Name'
        addresses column:'Person_Address_Id'
    }
}

address 테이블의 컬럼 대신 어떤 중간의 조인 테이블을 쓰고 싶으면 joinTable 파라메터를 사용할 수 있다.

class Person {
    String firstName
    static hasMany = [addresses:Address]
    static mapping = {
        table 'people'
        firstName column:'First_Name'
        addresses joinTable:[name:'Person_Addresses', key:'Person_Id', column:'Address_Id']
    }
}



多-대-多 매핑
Grails는 기본적으로 조인 테이블을 이용하여 다-대-다 관계를 매핑한다. 예를 들어 아래의 다-대-다 관계를 생각해보자

class Group {
    …
    static hasMany = [people:Person]
}
class Person {
    …
    static belongsTo = Group
    static hasMany = [groups:Group]
}

이 경우 person_idgroup_id foreign 키를 포함하는 group_person 이라는 조인 테이블을 생성한다. 각 foreign 키는 person과 group 테이블을 참조한다. 컬럼이름을 바꾸고 싶을 경우 각 클래스의 mapping에서 컬럼을 지정할 수 있다.

class Group {
    …
    static mapping = {
        people column:'Group_Person_Id'
    }
}
class Person {
    …
    static mapping = {
        groups column:'Group_Group_Id'
    }
}

사용할 조인 테이블의 이름도 지정할 수 있다.

class Group {
    …
    static mapping = {
        people column:'Group_Person_Id',joinTable:'PERSON_GROUP_ASSOCIATIONS'
    }
}
class Person {
    …
    static mapping = {
        groups column:'Group_Group_Id',joinTable:'PERSON_GROUP_ASSOCIATIONS'
    }
}


5.5.2.2 캐싱 전략

캐싱 설정
Hibernate에는 사용자가 바꿀 수 있는 캐싱 공급자(provider)로 두번째 수준의 캐싱 기능이 있다. 이 기능은 다음과 같이 grails-app/conf/DataSource.groovy 파일에 설정한다.

hibernate {
    cache.use_second_level_cache=true
    cache.use_query_cache=true
    cache.provider_class='org.hibernate.cache.EhCacheProvider'
}

물론 이 설정은 원하는 데로 변경할 수 있는데 분산 캐싱 메커니즘을 사용할 경우가 그 예이다.

사용자 삽입 이미지
캐싱 그리고 특히 Hibernate의 두번째 수준 캐싱에 대해 더 알고 싶으며 Hiberante 문서의 관련주제를 참조한다.


인스턴스 캐싱하기
기본 설정으로 캐싱하기 위해서는 매핑 블럭에서 cache 메소드를 호출한다.

class Person {
    ..
    static mapping = {
        table 'people'
        cache true
    }
}

이것은 lazy와 non-lazy 속성 모두 '읽기-쓰기' 캐싱로 설정한다. 이것을 조정하기 위해서는 다음과 같이 할 수 있다.

class Person {
    ..
    static mapping = {
        table 'people'
        cache usage:'read-only', include:'non-lazy'
    }
}


관계(associations) 캐싱하기
인스턴스를 캐싱하기 위해 Hibernate의 두번째 수준 캐시를 사용하는 것처럼 객체의 collections(associations)도 역시 캐싱할 수 있다. 예를 들면

class Person {
    String firstName
    static hasMany = [addresses:Address]
    static mapping = {
        table 'people'
        version false
        addresses column:'Address', cache:true
    }
}
class Address {
    String number
    String postCode
}

이것은 address들의 집합에 대해 'read-write' 캐싱을 하게 한다. 다음과 같이 사용할 수도 있다.

cache:'read-write' // 또는 'read-only' 또는 'transactional'

더 상세히 조정하려면 아래 캐시 사용법을 보라


캐시 사용법
아해는 다른 캐시 설정과 그 사용법을 설명한 것이다.

  • read-only:  애플리케이션이 저장 클래스의 인스턴스를 절대 수정하지 않고 읽기만 필요할 경우 read-only 캐시를 사용할 수 있다.
  • read-write: 애플리케이션이 데이터를 수정할 필요가 있다면 read-write 캐시가 알맞을 것이다.
  • nonstrict-read-write: 애플리케이션이 가끔 데이터를 수정하고(즉 두 개의 트랜잭션이 동시에 동일한 아이템을 수정하려는 경우가 거의 일어나지 않는 경우) 엄격한 트랜잭션 격리(isolation)가 필요없다면 nonstrict-read-write 캐시가 적당할 것이다.
  • transactional: 이 캐시 전략은 JBoss TreeCache와 같이 완전한 트랜잭션 기반 캐시 공급자를 위한 것이다. 이런 캐시는 JTA 환경에서만 사용할 수 있고 grails-app/conf/DataSource.groovy 파일 hibernate config의 hibernate.transaction.manager_lookup_class를 설정하여야 한다.


5.5.2.3 상속 전략
기본으로 GORM은 계층당 하나의 테이블(table-per-hierarchy)로 매핑하는 상속 매핑 전략을 이용한다. 이 전략은 DB 수준에서 컬럼에 NOT-NULL 제약을 줄 수 없는 약점을 가진다. 서브클래스 당 하나의 테이블(table-per-subclass) 상속 전략을 사용하고 싶을 때는 다음과 같이 할 수 있다.

class Payment {
    Long id
    Long version
    Integer amount    static mapping = {
        tablePerHierarchy false
    }
}
class CreditCardPayment extends Payment  {
    String cardNumber
}

최상위 Payment 클래스에 저렇게 설정하면 모든 자식 클래스들은 계층당 테이블(table-per-hierarchy) 매핑을 사용하지 않을 것이다.


5.5.2.4 사용자 정의 Database 식별자
DSL을 이용하여 GORM이 데이터베이스 식별자를 생성하는 방법을 변경할 수 있다. 기본으로 GROM은 아이디 생성에 데이터베이스 고유의 메커니즘을 이용한다. 이것은 분명 최상의 접근방법이지만 여전히 식별을 위한 다른 접근방법을 을 가진 스키마가 많다.

이것을 다루기 위해서 Hibernate의 아이디 생성기(id generator) 개념을 정의한다. 아이디 생성기와 컬럼을 매핑하기 위해서는 아래와 같이 한다.

class Person {
    ..
    static mapping = {
        table 'people'
        version false
        id generator:'hilo', params:[table:'hi_value',column:'next_value',max_lo:100]
    }
}

이 경우 Hibernate에 내장된 'hilo' 생성기를 사용하는데 이는 아이디를 생성하기 위해 분리된 테이블을 사용한다.

사용자 삽입 이미지
또 다른 Hibernate 생성기에 대한  자세한 정보는 Hibernate 참조 문서를 본다.


단지 id가 저장될 컬럼만 지정하기 위해서는 다음과 같이 한다.

class Person {
    ..
    static mapping = {
        table 'people'
        version false
        id column:'person_id'
    }
}


5.5.2.5 합성 Primary Key
GORM은 식별자가 2개 이상의 속성으로 구성된 합성 식별자 개념을 지원한다. 이것은 권장하는 접근방법은 아니지만 필요할 때 사용할 수 있다.

class Person {
    String firstName
    String lastName  static mapping = {
        id composite:['firstName', 'lastName']
    }
}

위 예제는 Person 클래스의 firstName과 lastName 석송으로 합성 아이디를 생성한다. 후에 아이디로 인스턴스를 읽기 위해서는 객체 자신의 견본(prototype)을 사용해야 한다.

def p = Person.get(new Person(firstName:"Fred", lastName:"Flintstone"))
println p.firstName


5.5.2.6 Database Indices
DB 질의에서 최적의 성능을 얻기을 얻기 위해서 종종 테이블 인덱스 정의를 조정할 필요가 있다. 어떻게 조정하는가의 문제는 영역마다 다르고 당신의 질의가 어떤 패턴으로 사용하는지 모니터링 결과에 따라 다른다. GORM의 DSL을 이용하여 어떤 컬럼이 어떤 인덱스가 필요한지 지정할 수 있다.

class Person {
    String firstName
    String address
    static mapping = {
        table 'people'
        version false
        id column:'person_id'
        firstName column:'First_Name', index:'Name_Idx'
        address column:'Address', index:'Name_Idx, Address_Index'
    }
}


5.5.2.7 낙관적인(Optimistic) 잠금과 버전기록
낙관적인 잠금과 비관적인 잠금 장에서 논의했듯이 GORM은 기본으로 낙관적인 잠금을 사용하고 데이터베이스 수준의 version 컬럼과 매핑하기 위해 모든 클래스에 version 속성을 자동으로 끼워넣는다(inject).
레가시 시스템과 매핑할 경우 이것은 문제를 일으키므로 아래와 같이 이 기능을 사용하지 않을 수 있다.

class Person {
  ..
  static mapping = {
      table 'people'
      version false
  }
}

사용자 삽입 이미지
낙관적인 잠금을 사용하지 않으면 본질적으로 동시 수정을 고려하여야하고 비관적인 잠금을 사용하지 않는한 덮어쓸 수 있기 때문에 사용자 데이터를 손실할 수 있는 위험을 감수해야 한다.


5.5.2.8 Eager 패칭과 Lazy 패칭

Lazy Collections
Eager and Lazy fetching 장에서 논의했듯이 기본으로 GORM collection은 lazy 패칭을 사용하고 fetchMode 설정을 통해 변경한다. 어쨌든 매핑 블럭안에 모든 매핑 정보를 모으기를 원하면 패칭을 설정하기 위해 ORM DSL을 사용할 수도 있다.

class Person {
    String firstName
    static hasMany = [addresses:Address]
    static mapping = {
        addresses lazy:false
    }
}
class Address {
    String street
    String postCode
}


하나를 가지는 관계(Single-Ended Associations)의 lazy 패칭
GORM에서 일-대-일과 多-대-일 관계는 기본으로 non-lazy(한 번에 다 읽음)이다. 다른 쪽 엔티티와 관계를 가진 많은 엔티티를 로드할 때 새로운 SELECT 문장을 로드되는 매 엔티티마다 실행하는 문제가 있을 수 있다. Lazy collections와 같은 기술을 이용하여 일-대-일과 多-대-일 관계에서 lazy를 사용할 수 있다.

class Person {
    String firstName
    static belongsTo = [address:Address]
    static mapping = {
        address lazy:true // address를 필요할 때 패칭
    }
}
class Address {
    String street
    String postCode
}

여기에서 Person 클래스의 address 속성을 필요할 때(lazily) 로드하도록 설정했다.


5.6 프로그램 방식의(Programmatic) 트랜잭션
Grails는 Spring을 기반으로 하기 때문에 프로그램 방식의 트랜잭션을 다루기 위해 Spring의 트랜잭션 추상을 이용한다. 그러나 GORM는 클래스를 보강하여 이것을 단순하게 만들기 위해 withTransaction 메소드를 통해 이루어진다. 이것은 블럭이 첫번째 아규먼트로 스프링의 TransactionStatus 객체를 받도록 한다.
전형적인 사용 시나리오는 다음과 같다.

def transferFunds = {
    Account.withTransaction { status ->
        def source = Account.get(params.from)
        def dest = Account.get(params.to)
        def amount = params.amount.toInteger()
        if(source.active) {
            source.balance -= amount
            if(dest.active) {
                dest.amount += amount
            }
            else {
                status.setRollbackOnly()
            }
        }  
    }
}

이 예제에서 목표 계좌가 active가 아닐 경우 롤백을 하고, 처리 중 어떠한 예외가 발생할 경우 자동으로 롤백을 한다.

전체 트랜잭션이 롤백되지 않고 특정 시점의 트랜잭션만 롤백하기 위해 "save points"를 사용할 수도 있다. 이것은 Sprint의 SavePointManager 인터페이스를 이용하여 이루어진다.

withTransaction 메소드는 블럭의 주어진 범위에 해당하는 로직의 begin/commit/rollback 처리를 한다.


5.7 GORM과 제약조건(Constraint)
유효성검사(Validation) 장에서 제약조건(constraints)을 다루기는 하지만 어떤 제약조건은 데이터베이스 스키말 생성에 영향을 줄 수 있기에 여기에서 언급하는 것이 중요하다.
그럴듯 하게도 도메인 클래스 속성에 대응하는 데이터베이스 컬럼 생성에 영향을 주기 위해 Grails는 도메인 클래스의 제약조건을 이용한다.

다음 예제를 살펴보자. 다음 속성을 갖는 도메인 클래스를 가정해 보자.

String name
String description

기본으로 MySQL의 경우 Grails는 이 컬럼을 다음과 같이 정의한다.

컬럼 이름    | 데이터 타입
description  | varchar(255)

그러나 아마 이 도메인 클래스 상태의 업무 규칙상 description은 문자 길이가 1000자를 넘을 수 있을 것이다. 이러한 경우 SQL 스크립트로 테이블을 생성한다면 아래와 같이 컬럼을 정의할 것이다.

컬럼 이름    | 데이터 타입
description  | TEXT

어떠한 레코드를 저장하기 전이라도 1000자 한계를 넘지 않도록 확인하는 기회는 애플리케이션 기반 유효성 검증에서도 갖기를 원할 것이다. Grails에서 제약조건(constraints)들을 통해 이 유효성 검증을 할 수 있다. 도메인 클래스에 다음의 제약조건 선언을 추가했다.

static constraints = {
    description(maxSize:1000)
}

이 제약조건은 우리가 원하는 애플리케이션 기반의 유효성 검증을 제공하기도 하고 위에 본 것처럼 스키마를 생성하도록 하기도 한다. 아래에는 스키마 생성에 영향을 주는 다른 제약조건들을 설명한 것이다.

문자열 속성에 영향을 주는 제약조건들

maxSizesize 제약조건을 정의할 경우 Grails는 제약조건 값으로 최대 컬럼 길이를 설정한다.

일반적으로 동일한 도메인 클래스 속성에 이 두 개의 제약조건을 동시에 사용하는 것은 권장하지 않는다. 어쨌거나 maxSize 제약조건과 size 제약조건을 정의했을 경우 Grails는 maxSize와 size 제약조건의 상한 중 최소값으로 컬럼 길이를 설정한다. (Grails는 두 값의 최소값을 사용한다. 왜냐하면 그 최소값을 넘는 값은 유효성 검사 에러를 발생하기 때문이다.)

maxSize와 size제약조건 없이 inList 제약조건을 정의한다면 Grails는 유효한 값 목록 중 가장 긴 문자열의 길이로 컬럼 최대 길이를 설정한다. 예를 들어 "java", "Groovy", "C++"을 포함한 목록이 주어졌을 때 Grails는 컬럼 길이를 8(즉 "Groovy"의 문자길이)로 설정할 것이다.

숫자 속성에 영향을 주는 제약조건들


만일 max 제약조건이나 min 제약조건 또는 range 제약조건을 정의하면 Grails는 제약조건 값을 기반으로 컬럼 정밀도를 설정한다. (이런 영향을 주는 시도의 성공여부는 Hibernate가 DBMS와 어떻게 연동하는지에 크게 의존한다.)

일반적으로 min/max 쌍과 range 제약조건을 동일한 도메인 클래스 속성에 결합하는 것은 권장하지 않는다. 어쨌거나 이 두 개의 제약조건들을 정의하면 Grails는 제약조건 중 최소 정밀도 값을 사용한다. (최소 정밀도를 넘는 어떠한 길이도 유효성 검사 에러를 발생하므로 Grails는 두 개중 최소값을 사용한다.)

scale 제약조건을 정의하면 Grails는 제약조건 값을 기반으로 컬럼 스케일을 설정한다. 이 규칙은 오직 부동 소수점에만 적용된다. (즉 java.lang.Float, java.Lang.Double, java.lang.BigDecimal, 또는 java.lang.BigDecimal의 서블 클래스들) (이런 영향을 주는 시도의 성공여부는 Hibernate가 DBMS와 어떻게 연동하는지에 크게 의존한다.)

이 제약조건은 최소/최대 숫자 값을 정의한다. 그리고 Grails는 정밀도를 사용하여 수의 최대값을 유도한다. min/max 제약조건 중 오직 하나를 설정하는 것은 스키마 생성에 영향을 주지 않는 것을 기억한다.(예를 들어 max:100으로 설정된 속성 값에는 매우 큰 음수가 있을 수 있기 때문이다.) 설정된 제약조건 값이 일정 이상의 자리수를 필요로 하지 않는 한 기본 Hibernate 컬럼 정밀도는 현재 19이다. 예를 들면

someFloatValue(max:1000000, scale:3)

위 결과는 아래와 같다.

someFloatValue DECIMAL(19, 3) // 기본 정밀도를 사용

그러나

someFloatValue(max:12345678901234567890, scale:5)

위 결과는 아래와 같다.

someFloatValue DECIMAL(25, 5) // 정밀도 = 최대자리수(20) + 스케일(5)

그리고

someFloatValue(max:100, min:-100000)

위 결과는 아래와 같다.

someFloatValue DECIMAL(8, 2) // 정밀도 = 최소값의 자리수(6) + 기본 스케일(2)

---
원문: 5.5 Advanced GORM Features
0 Comments
댓글쓰기 폼