본문 바로가기

프로그래밍/Hibernate

Hibernate에서의 CompositeUserType 사용에 대한 주의점..

요즘 들어 Hibernate를 공부하다보니 점점 응용학습을 하는 시간을 갖게 되는데 이번 글에서는 CompositeUserType에 대한 설명을 좀 해보고자 한다.

현재 내가 하는 샘플은 기존 Mybatis를 연동해서 만드는 게시판을 Hibernate로 연동하는 방법으로 바꾸고 있다. 그래서 바꾸는 작업을 하던 중에 CompositeUserType을 사용하고 있는 상황을 만들게 되었다.

 

게시판을 만들다보면 작성일시와 수정일시를 넣어야 하는 부분이 있다. 비단 게시판 뿐만 아니라 작성일시와 수정일시 컬럼은 다른 테이블에서도 자주 사용되는 컬럼이다. 그래서 이 컬럼을 CompositeUserType을 사용해서 특정 타입으로 재정의를 하고 싶어졌다. 그래서 다음과 같이 재정의를 했다

 

import java.io.Serializable;
import java.util.Calendar;

public class RegUpdateDate implements Serializable{

	private static final long serialVersionUID = 843516664050338429L;

	private final Calendar regdate;
	private final Calendar upddate;
	
	public RegUpdateDate(){
		this.regdate = Calendar.getInstance();
		this.upddate = Calendar.getInstance();
	}
	
	public RegUpdateDate(Calendar regdate, Calendar upddate){
		this.regdate = regdate;
		this.upddate = upddate;
	}
	
	public Calendar getRegdate() {
		return regdate;
	}
	
	public Calendar getUpddate() {
		return upddate;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((regdate == null) ? 0 : regdate.hashCode());
		result = prime * result + ((upddate == null) ? 0 : upddate.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		RegUpdateDate other = (RegUpdateDate) obj;
		if (regdate == null) {
			if (other.regdate != null)
				return false;
		} else if (!regdate.equals(other.regdate))
			return false;
		if (upddate == null) {
			if (other.upddate != null)
				return false;
		} else if (!upddate.equals(other.upddate))
			return false;
		return true;
	}
	
}

 

regdate 필드와 upddate 필드를 각각 등록일시와 수정일시로 저장하는 RegUpdateDate 클래스를 따로 만들어서 이 두 속성을 저장하는 하나의 클래스를 새로 만들었다. 그리고 이를 사용하는 게시판 모델에서 다음과 같이 사용했다.

 

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotBlank;

import com.terry.springhibernate.common.vo.SearchVO;

@Entity
@Table(name="USERTBL")
public class User extends SearchVO {

	...
	
	@org.hibernate.annotations.Type(type="com.terry.springhibernate.common.hibernate.RegUpdateDateUserType")
	@org.hibernate.annotations.Columns(
		columns={
				@Column(name="REGDATE", columnDefinition="DATE DEFAULT SYSDATE", insertable=true, updatable=false),
				@Column(name="UPDDATE", columnDefinition="DATE DEFAULT SYSDATE", insertable=false, updatable=true)
		}		
	)
 	private RegUpdateDate regUpdateDate;
	
	...

}

 

여러가지 속성과 메소드가 클래스에 있지만 일단 위에서 언급한 클래스 속성에 대한 정의를 하는 부분만 보여줬다. regdate 속성은 REGDATE 컬럼으로 이름을 정의했고 upddate 속성은 UPDDATE 컬럼으로 이름을 정의했다. 각 컬럼의 @Column 어노테이션 속성에 insertable과 updatatable 속성을 위의 소스와 같이 정의했다. 등록일시를 저장하는 regdate의 경우는  insert 작업할 땐 사용되지만 update시엔 사용되면 안되기 때문에(등록일시의 성격을 보자, 글을 등록할땐 insert 문으로 사용될것이고 그럴 경우엔 이 컬럼에 값이 insert 되어야 하지만 수정일땐 등록일시의 값을 update를 시키면 기존 등록일시 값이 수정되어버리기때문에 값이 의미가 없어지기 때문이다) insertable은 true로 주어서 insert 작업시엔 이 필드가 사용이 되도록 했고 updatable을 false로 주어서 update 작업시엔 이 필드가 사용되지 않도록 했다. 이런 관점하에서 upddate 속성을 따져보면 insert 작업시엔 이 필드가 사용이 되도록 하면 안되고 update 작업시엔 이 필드가 사용이 되도록 해야 하기 때문에 insertable은 false, updatable은 true로 주었다.

 

근데 이런 이유로 이렇게 속성을 정의하고 Tomcat을 띄워보니 다음과 같은 에러가 발생한다

 

Caused by: org.hibernate.AnnotationException: Mixing insertable and non insertable columns in a property is not allowed: regUpdateDate

at org.hibernate.cfg.Ejb3Column.checkPropertyConsistency(Ejb3Column.java:582)

at org.hibernate.cfg.annotations.SimpleValueBinder.validate(SimpleValueBinder.java:324)

at org.hibernate.cfg.annotations.SimpleValueBinder.make(SimpleValueBinder.java:329)

at org.hibernate.cfg.annotations.PropertyBinder.makePropertyAndValue(PropertyBinder.java:193)

...

 

에러로그가 밑에 더 있지만 일단 예외가 발생하는 부분을 강조하기 위해 상위 4개 줄만 표현했다. 에러 문구를  보면 Mixing insertable and non insertable columns in a property is not allowed 라고 보여준다. 즉 insertable 과 noninsertable 속성을 섞어서 사용할 수 없다고 나온다. 지금 보면 regdate는 insertable이 true이지만 upddate는 insertable이 false이기 때문에 insertable과 non insertable 이 섞여 있다는 것은 맞는 말이다. 그러면 이 예외가 어떤 이유에서 발생하는지 보자. 에러 로그를 보면 예외가 던져지는 위치는 org.hibernate.cfg.Ejb3Column 클래스의 582번째 라인에서 던져지고 있다. 이제 이 부분에 대해 얘기해보도록 하겠다

 

위의 예외가 발생하는 위치의 라인번호로 해서 찾아보면 이 예외가 발생하는 곳은 EJB3Column 클래스의  checkPropertyConsistency 메소드에서 발생하게 된다. 이제 이 메소드를 살펴보자.

 

public static void checkPropertyConsistency(Ejb3Column[] columns, String propertyName) {
		int nbrOfColumns = columns.length;

		if ( nbrOfColumns > 1 ) {
			for (int currentIndex = 1; currentIndex < nbrOfColumns; currentIndex++) {

				if (columns[currentIndex].isFormula() || columns[currentIndex - 1].isFormula()) {
					continue;
				}

				if ( columns[currentIndex].isInsertable() != columns[currentIndex - 1].isInsertable() ) {
					throw new AnnotationException(
							"Mixing insertable and non insertable columns in a property is not allowed: " + propertyName
					);
				}
				if ( columns[currentIndex].isNullable() != columns[currentIndex - 1].isNullable() ) {
					throw new AnnotationException(
							"Mixing nullable and non nullable columns in a property is not allowed: " + propertyName
					);
				}
				if ( columns[currentIndex].isUpdatable() != columns[currentIndex - 1].isUpdatable() ) {
					throw new AnnotationException(
							"Mixing updatable and non updatable columns in a property is not allowed: " + propertyName
					);
				}
				if ( !columns[currentIndex].getTable().equals( columns[currentIndex - 1].getTable() ) ) {
					throw new AnnotationException(
							"Mixing different tables in a property is not allowed: " + propertyName
					);
				}
			}
		}

}

 

위의 소스는 checkPropertyConsistency 메소드 소스이다. 이 소스를 살펴보면 현재 클래스의 멤버필드(여기서는 regdate)과 다음 클래스의 멤버필드(여기서는 upddate)의 hibernate 관련 속성중 insertable, updatable, 테이블명 4가지 속성의 값을 판단해서 속성의 값들이 서로 다를 경우 예외를 던지고 있다.(단 formular의 경우 formula 속성이 사용된 상황이면 continue를 이용해서 다음 멤버필드로의 검증으로 넘어가 버린다). 이를 미루어보면 CompositeUserType에서 사용되는 멤버필드의 insertable, updatable, 사용되는 테이블명은 모두 같은 값을 사용해야 한다는 것이다. 그러나 여기서는 regdate는 insertable은 true로 되어 있으나 upddate의 insertable은 false이기 때문에 예외가 발생되는 것이다. 실제로 예외가 발생했을때의 문구인 Mixing insertable and non insertable columns in a property is not allowed 문구를 예외로 던지는 부분ㅇ은 if문으로 inserable을 체크하는 부분에서 작업하는 것임을 알 수 있다.

 

이것과 관련되서 StackOverflow에서 조회해보면 대부분 Join 설정 과정에서 발생하는 경우는 있었으나 나와 같이 Join과는 상관없는 상황에서 발생하는 경우는 없었다. 현재 사용된 Hibernate 버전이 4.2.19.Final 버전이었다. StackOverflow의 글을 보면 Hibernate 5 버전대에서는 수정된다는 내용도 있긴 한데 직접 확인해본 바는 아니어서 글을 올리기는 좀 그렇다. 이것에 대해서 내 나름대로의 추측은 다음과 같다. JPA의 정의(?)로 유추해보면 속성이 DB의 컬럼과 매핑된다는 관점에서 보면 설령 2개 이상의 속성을 결합한 하나의 복합된 클래스로 속성을 정의해도 그 속성은 하나의 DB 컬럼으로 이해되어야 하며 이런 이해에서 봤을때 insertable도 되고 notinsertable도 된다는 식의 논리는 맞지 않다는 정의로써 이러한 동작제한을 걸었을 것 같다는 추측을 해보게 된다. 암튼 현재로써는 CompositeUserType 사용시 구성되는 멤버들(여기서는 regdate, upddate)의 insertable, updatable, 테이블 명은 모두 동일한 값을 사용해야 한다는 점이다. 이 점 때문에 등록일시, 수정일시는 불편하더라도 매번 중복 코드로 써야 할듯 싶다..ㅠㅠ..혹 이 글을 읽은 독자분들중 해결할 수 있는 방법이 있으면 댓글로 남겨주시면 고맙겠다. 꼭 insertable, updatable이 아니어도 좋다.