====== Data Type ======
어플리케이션을 작성할 때 Data Type 에 대한 올바른 사용과 관련 처리는 매우 중요하다. 특히 데이터베이스를 이용하여 데이터를 저장하고 조회할 때 Java 어플리케이션에서의 Type 과 DBMS 에서 지원하는 관련 매핑 jdbc Type 의 정확한 사용이 필요하며, 여기에서는 iBATIS 환경에서 javaType 과 특정 DBMS 의 jdbcType 의 적절한 매핑 사용예를 중심으로 일반적인 Data Type 의 사용 가이드를 참고할 수 있도록 한다.
===== 기본 Data Type 사용 방법 =====
iBATIS SQL Mapper 프레임워크는 Java 어플리케이션 영역의 표준 JavaBeans 객체(또는 Map 등)의 각 Attribute 에 대한 Java Type 과 JDBC 드라이버에서 지원하는 각 DBMS의 테이블 칼럼에 대한 Data Type 의 매핑을 기반으로 parameter / result 객체에 대한 바인딩/매핑 을 처리한다. 각 javaType 에 대한 매칭되는 jdbcType 은 일반적인 Ansi SQL 을 사용한다고 하였을 때 아래에서 대략 확인할 수 있을 것이다. 특정 DBMS 벤더에 따라 추가적으로 지원/미지원 하는 jdbcType 이 다를 수 있고, 또한 같은 jdbcType 을 사용한다 하더라도 타입에 따른 사용 가능한 경계값(boundary max/min value)은 다를 수 있다.
아래에서는 다양한 primitive 타입과 숫자 타입, 문자 타입, 날짜 타입에 대한 기본 insert/select 를 통해 iBATIS 사용 환경에서의 data type 에 대한 사용 예를 알아보겠다.
==== Sample Type VO ====
public class TypeTestVO implements Serializable {
private static final long serialVersionUID = -3653247402772333834L;
private int id;
private BigDecimal bigdecimalType;
private boolean booleanType;
private byte byteType;
private String charType;
private double doubleType;
private float floatType;
private int intType;
private long longType;
private short shortType;
private String stringType;
private Date dateType;
private java.sql.Date sqlDateType;
private Time sqlTimeType;
private Timestamp sqlTimestampType;
private Calendar calendarType;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public BigDecimal getBigdecimalType() {
return bigdecimalType;
}
public void setBigdecimalType(BigDecimal bigdecimalType) {
this.bigdecimalType = bigdecimalType;
}
public boolean isBooleanType() {
return booleanType;
}
public void setBooleanType(boolean booleanType) {
this.booleanType = booleanType;
}
public byte getByteType() {
return byteType;
}
public void setByteType(byte byteType) {
this.byteType = byteType;
}
public String getCharType() {
return charType;
}
public void setCharType(String charType) {
this.charType = charType;
}
public double getDoubleType() {
return doubleType;
}
public void setDoubleType(double doubleType) {
this.doubleType = doubleType;
}
public float getFloatType() {
return floatType;
}
public void setFloatType(float floatType) {
this.floatType = floatType;
}
public int getIntType() {
return intType;
}
public void setIntType(int intType) {
this.intType = intType;
}
public long getLongType() {
return longType;
}
public void setLongType(long longType) {
this.longType = longType;
}
public short getShortType() {
return shortType;
}
public void setShortType(short shortType) {
this.shortType = shortType;
}
public String getStringType() {
return stringType;
}
public void setStringType(String stringType) {
this.stringType = stringType;
}
public Date getDateType() {
return dateType;
}
public void setDateType(Date dateType) {
this.dateType = dateType;
}
public java.sql.Date getSqlDateType() {
return sqlDateType;
}
public void setSqlDateType(java.sql.Date sqlDateType) {
this.sqlDateType = sqlDateType;
}
public Time getSqlTimeType() {
return sqlTimeType;
}
public void setSqlTimeType(Time sqlTimeType) {
this.sqlTimeType = sqlTimeType;
}
public Timestamp getSqlTimestampType() {
return sqlTimestampType;
}
public void setSqlTimestampType(Timestamp sqlTimestampType) {
this.sqlTimestampType = sqlTimestampType;
}
public Calendar getCalendarType() {
return calendarType;
}
public void setCalendarType(Calendar calendarType) {
this.calendarType = calendarType;
}
}
위 TypeTestVO 의 각 attribute 는 다양한 data Type 에 대한 사용예의 샘플이며 이에 대한 매핑 jdbc 타입은 아래의 각 DBMS 별 DDL 을 통해 일차적으로 살펴보자.
==== Sample TYPETEST Table Hsqldb DDL script ====
create table TYPETEST (
id numeric(10,0) not null,
bigdecimal_type numeric(19,2),
boolean_type boolean,
byte_type tinyint,
char_type char(1),
double_type double,
float_type float,
int_type integer,
long_type bigint,
short_type smallint,
string_type varchar(255),
date_type date,
sql_date_type datetime,
sql_time_type time,
sql_timestamp_type timestamp,
calendar_type timestamp,
primary key (id)
);
위 create sql 문의 hsqldb 의 예이며, 실제로 Ansi SQL 의 Data Type 에 대한 표준을 잘 따르는 예이다.
boolean 타입을 직접 지원하고 있으며 tinyint, double, date, time 등 다양한 jdbcType 에 대하여 사용에 특별한 무리가 없음을 아래의 테스트케이스로 알 수 있을 것이다.
==== Sample SQL Mapping XML ====
TypeTestVO JavaBeans 객체를 통해 insert/select 를 처리하는 sql 매핑 xml 이다. resultMap 을 정의하여 select 결과 객체 매핑을 처리하고 있으며, 입력 및 조회 조건 의 파라메터 바인딩을 Inline Parameter 방법을 통해 처리하고 있다.
resultMap 이나 parameterMap(Inline Parameter 도 마찬가지) 에서는 javaType="string", jdbcType="VARCHAR" 와 같이 명시적으로 java/jdbc type 에 대한 지시를 할 수도 있다. (성능상으로는 추천, 그러나 실제와 맞지 않는 type 지시는 런타임에 오류 발생) 또한, 위의 Inline parameter 처리 시 calendar 속성에 대해 handler=calendarTypeHandler 로 지시한 것과 resultMap 처리 시 typeHandler="calendarTypeHandler" 로 지시한 것에서 확인할 수 있듯이 일반적인 java-jdbc 매핑으로 커버하지 못하는 부분에 대하여 사용자가 typeHandler 를 구현하여 타입 컨버전에 대한 로직 처리를 제공함으로써 위와 같이 calendar type <-> TIMESTAMP 변환이 가능한 것처럼 확장할 수도 있다.
위에서 TypeTestVO 와 SQL Mapping XML 은 아래의 추가적인 DBMS 테스트 시 변경없이 재사용하였고, 일부 Data Type 의 미지원/DBMS 별 매핑타입 사용/경계값 상이함 등에 대해서는 DDL / TestCase 에서 약간의 로직 분기나 회피를 통해 문제되는 부분을 피하고 전체적인 관점에서 재사용 할 수 있도록 테스트 하였으므로 참고하기 바란다.
==== Sample TestCase ====
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:META-INF/spring/context-*.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = false)
@Transactional
public class DataTypeTest extends TestBase {
@Resource(name = "typeTestDAO")
TypeTestDAO typeTestDAO;
@Before
public void onSetUp() throws Exception {
// 외부에 sql file 로부터 DB 초기화 (TypeTest 기존 테이블 삭제/생성)
SimpleJdbcTestUtils.executeSqlScript(
new SimpleJdbcTemplate(dataSource), new ClassPathResource(
"META-INF/testdata/sample_schema_ddl_typetest_" + usingDBMS
+ ".sql"), true);
}
public TypeTestVO makeVO() throws Exception {
TypeTestVO vo = new TypeTestVO();
vo.setId(1);
vo.setBigdecimalType(new BigDecimal("99999999999999999.99"));
vo.setBooleanType(true);
vo.setByteType((byte) 127);
// VO 에서 String 으로 선언했음. char 로 하고자 하는 경우 TypeHandler 작성 필요
vo.setCharType("A");
// Oracle 10g 에서 double precision 타입은 Double.MAX_VALUE 를 수용하지 못함.
// oracle jdbc driver 에서 Double.MAX_VALUE 를 전달하면 Overflow Exception trying to bind 1.7976931348623157E308 에러 발생
// mysql 5.0 에서 테스트 시 Double.MAX_VALUE 입력은 가능하나 조회 시 Infinity 로 되돌려짐
// tibero - Double.MAX_VALUE 입력 시 Exception 발생
vo.setDoubleType(isHsql ? Double.MAX_VALUE : 1.7976931348623157d);
// mysql 5.0 에서 테스트 시 Float.MAX_VALUE 를 입력할 수 없음
vo.setFloatType(isMysql ? (float) 3.40282 : Float.MAX_VALUE);
vo.setIntType(Integer.MAX_VALUE);
vo.setLongType(Long.MAX_VALUE);
vo.setShortType(Short.MAX_VALUE);
vo.setStringType("abcd가나다라あいうえおカキクケコ");
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
vo.setDateType(sdf.parse("2009-02-18"));
long currentTime = new java.util.Date().getTime();
vo.setSqlDateType(new java.sql.Date(currentTime));
vo.setSqlTimeType(new java.sql.Time(currentTime));
vo.setSqlTimestampType(new java.sql.Timestamp(currentTime));
vo.setCalendarType(Calendar.getInstance());
return vo;
}
public void checkResult(TypeTestVO vo, TypeTestVO resultVO) {
assertNotNull(resultVO);
assertEquals(vo.getId(), resultVO.getId());
assertEquals(vo.getBigdecimalType(), resultVO.getBigdecimalType());
assertEquals(vo.getByteType(), resultVO.getByteType());
// mysql 인 경우 timestamp 칼럼에 null 을 입력하면 현재 시각으로 insert 됨에 유의
if (vo.getCalendarType() == null && isMysql) {
assertNotNull(resultVO.getCalendarType());
// mysql 인 경우 java 의 timestamp 에 비해 3자리 정밀도 떨어짐
} else if (vo.getCalendarType() != null && isMysql) {
String orgSeconds =
Long.toString(vo.getCalendarType().getTime().getTime());
String mysqlSeconds =
Long.toString(resultVO.getCalendarType().getTime().getTime());
assertEquals(orgSeconds.substring(0, orgSeconds.length() - 3),
mysqlSeconds.substring(0, mysqlSeconds.length() - 3));
} else {
assertEquals(vo.getCalendarType(), resultVO.getCalendarType());
}
assertEquals(vo.getCharType(), resultVO.getCharType());
assertEquals(vo.getDateType(), resultVO.getDateType());
// double 에 대한 delta 를 1e-15 로 주었음.
assertEquals(vo.getDoubleType(), resultVO.getDoubleType(), isMysql
? 1e-14 : 1e-15);
// float 에 대한 delta 를 1e-7 로 주었음.
assertEquals(vo.getFloatType(), resultVO.getFloatType(), 1e-7);
assertEquals(vo.getIntType(), resultVO.getIntType());
assertEquals(vo.getLongType(), resultVO.getLongType());
assertEquals(vo.getShortType(), resultVO.getShortType());
// java.sql.Date 의 경우 Date 만 비교
if (vo.getSqlDateType() != null) {
assertEquals(vo.getSqlDateType().toString(), resultVO
.getSqlDateType().toString());
}
// mysql 인 경우 timestamp 칼럼에 null 을 입력하면 현재 시각으로 insert 됨에 유의
if (vo.getSqlTimestampType() == null && isMysql) {
assertNotNull(resultVO.getSqlTimestampType());
} else if (vo.getCalendarType() != null && isMysql) {
String orgSeconds =
Long.toString(vo.getSqlTimestampType().getTime());
String mysqlSeconds =
Long.toString(resultVO.getSqlTimestampType().getTime());
assertEquals(orgSeconds.substring(0, orgSeconds.length() - 3),
mysqlSeconds.substring(0, mysqlSeconds.length() - 3));
} else {
assertEquals(vo.getSqlTimestampType(), resultVO
.getSqlTimestampType());
}
// java.sql.Time 의 경우 Time 만 비교
if ((isHsql || isOracle || isTibero || isMysql)
&& vo.getSqlTimeType() != null) {
assertEquals(vo.getSqlTimeType().toString(), resultVO
.getSqlTimeType().toString());
} else {
assertEquals(vo.getSqlTimeType(), resultVO.getSqlTimeType());
}
assertEquals(vo.getStringType(), resultVO.getStringType());
assertEquals(vo.isBooleanType(), resultVO.isBooleanType());
}
@Test
public void testDataTypeTest() throws Exception {
// 값을 세팅하지 않고 insert 해 봄 - id 는 int 의 초기값에 따라 0 임
TypeTestVO vo = new TypeTestVO();
// insert
typeTestDAO.insertTypeTest("insertTypeTest", vo);
// select
TypeTestVO resultVO = typeTestDAO.selectTypeTest("selectTypeTest", vo);
// check
checkResult(vo, resultVO);
try {
// duplication 테스트
typeTestDAO.insertTypeTest("insertTypeTest", vo);
fail("키 값 duplicate 에러가 발생해야 합니다.");
} catch (Exception e) {
assertNotNull(e);
assertTrue(e instanceof DataIntegrityViolationException);
assertTrue(e.getCause() instanceof SQLException);
}
// DataType 테스트 데이터 입력 및 재조회
vo = makeVO();
// insert
typeTestDAO.insertTypeTest("insertTypeTest", vo);
// select
resultVO = typeTestDAO.selectTypeTest("selectTypeTest", vo);
// check
checkResult(vo, resultVO);
}
}
위에서는 TypeTestVO 의 각 속성에 값을 세팅하지 않고 입력/조회, 키 값 중복 시 spring 의 DataIntegrityViolationException 이 발생하는지, 각 속성에 테스트 데이터(경계값 또는 의미있는 사용예 로써의 값)를 세팅하여 입력/조회 에 대한 처리를 확인해 봄으로써 java <-> DBMS 의 타입 매핑의 예를 확인해 보았다.
특히 위의 makeVO 메서드 에서는 특정 javaType 에 대한 DBMS 의 db type 에 따라 경계값의 max value 가 달라질 수 있음을 확인할 수 있으며, checkResult 메서드에서는 특히 날짜 처리 타입과 관련하여 DBMS 에 따라 null 입력일 때 초기값 이나, 지원하는 정밀도(입력시 java 의 Date 류에서는 년월일시분초 를 넘어 상세하게 표현한 입력값 javaType 에 대해 jdbcType 의 결과 조회 시 날짜, 또는 시각 정보만으로 제한된다던지, 초 레벨의 정밀도가 java 에 비해 낮다던지)의 차이가 있음을 확인할 수 있다.
java-jdbc type 에 대한 일반적인 매핑은 위 Hsqldb 의 예를 기본으로 이해하면 적합할 것으로 보며, 아래에서 특정 DBMS 의 DDL 예를 통해 각 데이터베이스 환경에서 Data Type 사용의 참고가 될 수 있기 바란다.
==== Sample TYPETEST Table Oracle (10gR2 기준 테스트) DDL script ====
create table TYPETEST (
id number(10,0) not null,
bigdecimal_type number(19,2),
boolean_type number(1,0),
byte_type number(3,0),
char_type char(1),
double_type double precision,
float_type float,
int_type number(10,0),
long_type number(19,0),
short_type number(5,0),
string_type varchar2(255),
date_type date,
sql_date_type date,
sql_time_type timestamp,
sql_timestamp_type timestamp,
calendar_type timestamp,
primary key (id)
);
==== Sample TYPETEST Table Mysql (5.X) DDL script ====
create table TYPETEST (
id numeric(10,0) not null,
bigdecimal_type numeric(19,2),
boolean_type boolean,
byte_type tinyint,
char_type char(1),
double_type double,
float_type float,
int_type integer,
long_type bigint,
short_type smallint,
string_type varchar(255),
date_type date,
sql_date_type datetime,
sql_time_type time,
sql_timestamp_type timestamp,
calendar_type timestamp,
primary key (id)
);
==== Sample TYPETEST Table Tibero(3.x) DDL script ====
create table TYPETEST (
id numeric(10,0) not null,
bigdecimal_type number(19,2),
boolean_type number(1),
byte_type number(3),
char_type char(1),
double_type double precision,
float_type float,
int_type integer,
long_type integer, /* integer 가 bigint 의 범위까지 포함함 */
short_type smallint,
string_type varchar(255),
date_type date,
sql_date_type date,
sql_time_type date, /* time, timestamp 으로 설정 시 문제발생 */
sql_timestamp_type timestamp,
calendar_type timestamp,
primary key (id)
);