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

<sqlMap namespace="TypeTest">
 
	<typeAlias alias="typeTestVO"
		type="egovframework.rte.psl.dataaccess.vo.TypeTestVO" />
 
	<!-- CalendarTypeHandler 는 sql-map-config.xml 에 등록하였음 -->
	<typeAlias alias="calendarTypeHandler" type="egovframework.rte.psl.dataaccess.typehandler.CalendarTypeHandler"/>
 
	<resultMap id="typeTestResult" class="typeTestVO">
		<result property="id" column="ID" />
		<result property="bigdecimalType" column="BIGDECIMAL_TYPE" />
		<result property="booleanType" column="BOOLEAN_TYPE" />
		<result property="byteType" column="BYTE_TYPE" />
		<result property="charType" column="CHAR_TYPE" />
		<result property="doubleType" column="DOUBLE_TYPE" />
		<result property="floatType" column="FLOAT_TYPE" />
		<result property="intType" column="INT_TYPE" />
		<result property="longType" column="LONG_TYPE" />
		<result property="shortType" column="SHORT_TYPE" />
		<result property="stringType" column="STRING_TYPE" />
		<result property="dateType" column="DATE_TYPE" />
		<result property="sqlDateType" column="SQL_DATE_TYPE" />
		<result property="sqlTimeType" column="SQL_TIME_TYPE" />
		<result property="sqlTimestampType" column="SQL_TIMESTAMP_TYPE" />
		<result property="calendarType" column="CALENDAR_TYPE" typeHandler="calendarTypeHandler" />
	</resultMap>
 
	<insert id="insertTypeTest" parameterClass="typeTestVO">
		<![CDATA[
			insert into TYPETEST
			           (ID,
			            BIGDECIMAL_TYPE,
			            BOOLEAN_TYPE,
			            BYTE_TYPE,
			            CHAR_TYPE,
			            DOUBLE_TYPE,
			            FLOAT_TYPE,
			            INT_TYPE,
			            LONG_TYPE,
			            SHORT_TYPE,
			            STRING_TYPE,
			            DATE_TYPE,
			            SQL_DATE_TYPE,
			            SQL_TIME_TYPE,
			            SQL_TIMESTAMP_TYPE,
			            CALENDAR_TYPE)
			values     (#id#,
			            #bigdecimalType#,
			            #booleanType#,
			            #byteType#,
			            #charType:CHAR#,
			            #doubleType#,
			            #floatType#,
			            #intType#,
			            #longType#,
			            #shortType#,
			            #stringType#,
			            #dateType#,
			            #sqlDateType#,
			            #sqlTimeType#,
			            #sqlTimestampType#,
			            #calendarType,handler=calendarTypeHandler#)
		]]>
	</insert>
 
	<select id="selectTypeTest" parameterClass="typeTestVO"
		resultMap="typeTestResult">
		<![CDATA[
			select ID,
			       BIGDECIMAL_TYPE,
			       BOOLEAN_TYPE,
			       BYTE_TYPE,
			       CHAR_TYPE,
			       DOUBLE_TYPE,
			       FLOAT_TYPE,
			       INT_TYPE,
			       LONG_TYPE,
			       SHORT_TYPE,
			       STRING_TYPE,
			       DATE_TYPE,
			       SQL_DATE_TYPE,
			       SQL_TIME_TYPE,
			       SQL_TIMESTAMP_TYPE,
			       CALENDAR_TYPE
			from   TYPETEST
			where  ID = #id#
		]]>
	</select>
 
</sqlMap>

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)
);
 
egovframework/rte/psl/dataaccess/data_type.txt · 마지막 수정: 2015/04/14 11:24 (외부 편집기)
 
이 위키의 내용은 다음의 라이센스에 따릅니다 :CC Attribution-Noncommercial-Share Alike 3.0 Unported
전자정부 표준프레임워크 라이센스(바로가기)

전자정부 표준프레임워크 활용의 안정성 보장을 위해 위험성을 지속적으로 모니터링하고 있으나, 오픈소스의 특성상 문제가 발생할 수 있습니다.
전자정부 표준프레임워크는 Apache 2.0 라이선스를 따르고 있는 오픈소스 프로그램입니다. Apache 2.0 라이선스에 따라 표준프레임워크를 활용하여 발생된 업무중단, 컴퓨터 고장 또는 오동작으로 인한 손해 등에 대해서 책임이 없습니다.
Recent changes RSS feed CC Attribution-Noncommercial-Share Alike 3.0 Unported Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki