Data Access 서비스는 다양한 데이터베이스 솔루션 및 데이터베이스 접근 기술에 일관된 방식으로 대응하기 위한 서비스로서,데이터를 조회하거나 입력, 수정, 삭제하는 기능을 수행하는 메커니즘을 단순화한다. 또한 데이터베이스 솔루션이나 접근 기술이 변경될 경우에도 데이터를 다루는 시스템 영역의 변경을 최소화할 수 있도록 데이터베이스와의 접점을 추상화하며, 추상화된 데이터 접근 방식을 템플릿(Template)으로 제공함으로써, 개발자들의 업무 효율을 향상시킨다.
전자정부 프레임워크에서는 JDBC 를 사용한 Data Access 를 추상화하여 간편하고 쉽게 사용할 수 있는 Data Mapper framework 인 iBATIS 를 Data Access 기능의 기반 오픈 소스로 채택하였다. iBATIS 를 사용하면 관계형 데이터베이스에 억세스하기 위해 필요한 일련의 자바 코드 사용을 현저히 줄일 수 있으며 간단한 XML 기술을 사용하여 SQL 문을 JavaBeans (또는 Map) 에 간편하게 맵핑할 수 있다.
iBATIS Data Mapper API 는 XML을 사용하여 SQL 문에 대한 객체 맵핑을 간편하게 기술할 수 있도록 지원하며, 자바빈즈 객체와 Map 구현체, 다양한 원시 래퍼 타입(String, Integer..) 등을 PreparedStatement 의 파라메터나 ResultSet에 대한 결과 객체로 쉽게 맵핑해준다.
Data Access 서비스에 대한 자세한 설명에 앞서 간단하게 Data Access 서비스를 시작하는데 필요한 것에 대한 설명을 하고자 한다.
본 서비스를 활용하기 위해서 필요한 Library 목록과 설명은 아래와 같다.
라이브러리 | 설 명 | 연관 라이브러리 |
---|---|---|
ibatis-sqlmap-2.3.4.726.jar | iBATIS 라이브러리(필수) | |
commons-dbcp-1.2.2.jar | database connection pooling 지원 라이브러리(선택) | |
commons-logging-1.1.1.jar | commons 로깅(선택) | |
log4j-1.3alpha-8.jar | log4j(선택) | |
oscache-2.4.jar | 중앙집중 또는 분산 캐슁 지원(선택) | |
cglib-nodep-2.1_3.jar | Runtime Bytecode Enhancing 필요 시(선택) |
ibatis-sqlmap-2.3.4.726.jar 만이 필수 라이브러리이다. 그러나 일반적으로 commons-dbcp 와 같은 커넥션풀링 라이브러리 및 로깅 처리를 위한 라이브러리는 반드시 필요로 하며, 추가적으로 iBATIS 에서 지원하는 개선된 기능으로 cache 지원 이나 Runtime Bytecode Enhancement 관련 기능을 쓰고자 할 경우는 위의 참조 라이브러리를 추가로 설정할 수 있다. 또한 우리는 Spring-iBATIS 연동 형태의 어플리케이션 개발을 선호하므로 Spring 관련 라이브러리 및 이에 대한 dependency 라이브러리가 일반적으로 함께 포함될 것이다. 여기에 덧붙여 실제 Data Access 처리의 대상의 되는 DBMS(Oracle, Mysql, Hsqldb, Tibero ..) 에 따라 적절한 jdbc 드라이버에 대한 라이브러리가 추가적으로 필요하다.
Spring 프레임워크 기반 어플리케이션에서 iBATIS 를 연동하여 사용하고자 하는 경우 Spring의 SqlMapClientFactoryBean 에 대한 설정이 필요하며 여기서는 실제 대상 sql-map-config 설정 파일과 iBATIS 에 제공될 dataSource 에 대한 설정을 지시한다.
<!-- SqlMap setup for iBATIS Database Layer --> <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <property name="configLocation" value="classpath:/META-INF/sqlmap/sql-map-config.xml"/> <property name="dataSource" ref="dataSource"/> </bean>
아래는 주된 iBATIS 의 SQL Map XML Configuration 파일(sql-map-config.xml 설정 파일)이다. iBATIS 단독으로 쓰일 때는 transactionManager, dataSource 설정 등을 추가로 포함해야 하지만 Spring 연동 환경에서는 이 부분은 Spring 이 넘겨주는 dataSource 를 자동으로 사용하게 되고 transaction 관리는 비즈니스 서비스 영역에 선언적으로 설정하여 iBATIS 관련 모듈에서는 고민할 필요가 없게 된다.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE sqlMapConfig PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN" "http://www.ibatis.com/dtd/sql-map-config-2.dtd"> <sqlMapConfig> <settings useStatementNamespaces="false" .. /> <typeHandler javaType="java.util.Calendar" jdbcType="TIMESTAMP" callback="egovframework.rte.psl.dataaccess.typehandler.CalendarTypeHandler" /> <sqlMap resource="META-INF/sqlmap/mappings/testcase-basic.xml" /> <sqlMap ../> .. </sqlMapConfig>
Spring 2.5.5 이상, iBATIS 2.3.2 이상 인 경우에는 iBATIS 연동을 위한 SqlMapClientFactoryBean 정의 시 mappingLocations 속성으로 Sql 매핑 파일에 대한 패턴 표현식으로 일괄 지정도 가능하다. mappingLocations 속성 사용 예는 다음과 같다.
<!-- SqlMap setup for iBATIS Database Layer --> <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <property name="configLocation" value="classpath:/META-INF/sqlmap/sql-map-config.xml"/> <!-- Java 1.5 or higher and iBATIS 2.3.2 or higher REQUIRED --> <property name="mappingLocations" value="classpath:/META-INF/sqlmap/mappings/**/*.xml" /> <property name="dataSource" ref="dataSource"/> </bean>
이 경우는 “configLocation” 속성이 필요하지 않지만, 현재 해당 속성이 없는 경우 SqlMapClientFactoryBean의 초기화되지 않기 때문에 “configLocation” 속성을 유지하셔야 한다. 이 때 해당 sql-map-config.xml은 다음과 같이 dummy.xml query를 갖도록 처리하여야 한다.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE sqlMapConfig PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN" "http://www.ibatis.com/dtd/sql-map-config-2.dtd"> <sqlMapConfig> <sqlMap resource="sqlmap/sql/common/dummy.xml"/> </sqlMapConfig>
dummy.xml은 다음과 같이 처리한다.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd"> <sqlMap namespace="Dummy"> </sqlMap>
iBATIS 에서 정의한 SQL Map 문서 구조 내에서 다양한 옵션 설정과 Mapped statement 정의를 작성하게 된다. 아래는 부서정보에 대한 CRUD 와 관련한 쿼리와 이에 대한 In/Out 객체 맵핑을 포함하는 간략한 매핑 파일이다.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd"> ==== Step4. DAO 클래스 생성 ==== 간단한 형태의 DAO 클래스를 생성한다. 아래의 DAO 에서 상속하고 있는 EgovAbstractDAO 에서는 SqlMapClientDaoSupport 를 extends 하고 있으며 iBATIS SQL Map 상호 작용을 위한 기본 클래스인 SqlMapClient (위에서 "sqlMapClient" 로 정의한 iBATIS 연동 FactoryBean 에 의해 제공됨) 에 대한 injection 처리가 내부적으로 적용되어 있고, 기본 CRUD 에 대한 iBATIS 실행을 위해서는 간략한 메서드 래핑도 제공한다. 상세 API 를 사용하고자 하는 경우 getSqlMapClientTemplate() 를 통해(ex. getSqlMapClientTemplate().queryWithRowHandler("selectEmpListToOutFileUsingRowHandler", paramObject, rowHandler); ) 사용할 수 있다. === DAO 클래스 === <code java> .. @Repository("deptDAO") public class DeptDAO extends EgovAbstractDAO { public void insertDept(DeptVO vo) { insert("insertDept", vo); } public int updateDept(DeptVO vo) { return update("updateDept", vo); } public int deleteDept(DeptVO vo) { return delete("deleteDept", vo); } public DeptVO selectDept(DeptVO vo) { return (DeptVO)selectByPk("selectDept", vo); } @SuppressWarnings("unchecked") public List<DeptVO> selectDeptList(DeptVO searchVO) { return list("selectDeptList", searchVO); } }
각 CRUD 에 관련한 메서드에서는 queryId 와 파라메터 객체(여기서는 DeptVO) 를 인자로 iBATIS 의 Mapped Statement 을 실행하고 있으며, 조회의 경우에는 단건 조회는 DeptVO 객체로, 리스트 조회는 DeptVO 에 대한 List 를 돌려주도록 하고 있다. iBATIS 내부적으로는 java 1.5 이상의 Generics (type 이 정의된 Collection 처리) 로 처리하지 않으나 sql 매핑 파일에 따라 실제 데이터는 List<DeptVO> 로 처리가 될 것이므로 @SuppressWarnings(“unchecked”) 을 지시하여 호출 이전 모듈에서는 불필요한 Type Casting 을 최소화하고 있다. 만약 sql-map-config.xml 의 settings 옵션으로 useStatementNamespaces=“true” 를 설정한 경우라면 위의 queryId 는 “Dept.insertDept” 와 같이 sql 맵핑 파일에 지정한 Namespace prefix 을 포함하는 형태여야 함에 유의한다.
위에서 정의한 설정 파일 및 DAO 를 이용하여 간단한 입력,조회 처리에 대한 JUnit TestCase 형태(JUnit 4 스타일)로 구성하였다.
.. @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath*:META-INF/spring/context-*.xml" }) @TransactionConfiguration(transactionManager = "txManager", defaultRollback = false) @Transactional public class BasicDataAccessTest { @Resource(name = "dataSource") DataSource dataSource; @Resource(name = "deptDAO") DeptDAO deptDAO; @Before public void onSetUp() throws Exception { // 외부에 sql file 로부터 DB 초기화 (기존 테이블 삭제/생성) SimpleJdbcTestUtils.executeSqlScript( new SimpleJdbcTemplate(dataSource), new ClassPathResource( "META-INF/testdata/sample_schema_ddl_hsql.sql"), true); } public DeptVO makeVO() { DeptVO vo = new DeptVO(); vo.setDeptNo(new BigDecimal(90)); vo.setDeptName("test 부서"); vo.setLoc("test 위치"); return vo; } public void checkResult(DeptVO vo, DeptVO resultVO) { assertNotNull(resultVO); assertEquals(vo.getDeptNo(), resultVO.getDeptNo()); assertEquals(vo.getDeptName(), resultVO.getDeptName()); assertEquals(vo.getLoc(), resultVO.getLoc()); } @Test public void testBasicInsert() throws Exception { DeptVO vo = makeVO(); // insert deptDAO.insertDept(vo); // select DeptVO resultVO = deptDAO.selectDept(vo); // check checkResult(vo, resultVO); } @Test public void testBasicUpdate() throws Exception { DeptVO vo = makeVO(); // insert deptDAO.insertDept(vo); // data change vo.setDeptName("upd Dept"); vo.setLoc("upd loc"); // update int effectedRows = deptDAO.updateDept(vo); assertEquals(1, effectedRows); // select DeptVO resultVO = deptDAO.selectDept(vo); // check checkResult(vo, resultVO); } @Test public void testBasicDelete() throws Exception { DeptVO vo = makeVO(); // insert deptDAO.insertDept(vo); // delete int effectedRows = deptDAO.deleteDept(vo); assertEquals(1, effectedRows); // select DeptVO resultVO = deptDAO.selectDept(vo); // null 이어야 함 assertNull(resultVO); } @Test public void testBasicSelectList() throws Exception { DeptVO vo = makeVO(); // insert deptDAO.insertDept(vo); // 검색조건으로 key 설정 DeptVO searchVO = new DeptVO(); searchVO.setDeptNo(new BigDecimal(90)); // selectList List<DeptVO> resultList = deptDAO.selectDeptList(searchVO); // key 조건에 대한 결과는 한건일 것임 assertNotNull(resultList); assertTrue(resultList.size() > 0); assertEquals(1, resultList.size()); checkResult(vo, resultList.get(0)); // 검색조건으로 name 설정 - '%' || #deptName# || '%' DeptVO searchVO2 = new DeptVO(); searchVO2.setDeptName(""); // '%' || '' || '%' --> '%%' // selectList List<DeptVO> resultList2 = deptDAO.selectDeptList(searchVO2); // like 조건에 대한 결과는 한건 이상일 것임 assertNotNull(resultList2); assertTrue(resultList2.size() > 0); } }
기본적으로 Annotation 형식 Bean 생성 및 Dependency Injection 을 적용한 Spring 기반의 어플리케이션으로 구성하였으며, dataSource, transactionManager 등의 Spring Bean 이 함께 사용되고 있고, 테스트 케이스는 JUnit 4 형식으로 Spring 설정 파일 로딩 및 transactionManager, dataSource(DB 초기화를 위해 SimpleJdbcTemplate 사용 시 참조)를 얻을 수 있도록 되어 있음을 참고한다. 테스트 편의를 위해 매 테스트 메서드에 우선하여 @Before 로 정의된 메서드에서 기존 테이블 삭제 및 재생성 처리를 하고 있으며, makeVO 라는 별도 메서드로 테스트용 VO 작성 부분을 분리하고, checkResult 라는 메서드로 original VO 와 조회 결과 resultVO 에 대한 assert 비교 로직을 분리하여 재사용하고 있다. @Test 로 지시한 각 메서드가 테스트 메서드이고 입력-조회-결과체크, 입력-변경-조회-결과체크, 입력-삭제-조회-null체크, 검색조건 설정-조회-결과체크 의 flow 에 대한 검증으로 DeptDAO 에 대한 기본 CRUD 테스트 로직을 구성하였다.
파일을 다운로드 받아서 압축을 푼다.
※ 해당 프로젝트는 Hsqldb (현재 메모리구동 방식)을 사용하고 있으나 다른 DBMS 를 쓰고자 하는 경우 jdbc.properties 에 관련 접속정보를 추가하고 JDBC 드라이버 jar 파일을 라이브러리로 추가하여 테스트 해 볼 수 있다.