====== Data Access 서비스 ====== ===== 개요 ===== Data Access 서비스는 다양한 데이터베이스 솔루션 및 데이터베이스 접근 기술에 일관된 방식으로 대응하기 위한 서비스로서,데이터를 조회하거나 입력, 수정, 삭제하는 기능을 수행하는 메커니즘을 단순화한다. 또한 데이터베이스 솔루션이나 접근 기술이 변경될 경우에도 데이터를 다루는 시스템 영역의 변경을 최소화할 수 있도록 데이터베이스와의 접점을 추상화하며, 추상화된 데이터 접근 방식을 템플릿(Template)으로 제공함으로써, 개발자들의 업무 효율을 향상시킨다. ==== iBATIS 프레임워크 ===== 전자정부 프레임워크에서는 JDBC 를 사용한 Data Access 를 추상화하여 간편하고 쉽게 사용할 수 있는 Data Mapper framework 인 iBATIS 를 Data Access 기능의 기반 오픈 소스로 채택하였다. iBATIS 를 사용하면 관계형 데이터베이스에 억세스하기 위해 필요한 일련의 자바 코드 사용을 현저히 줄일 수 있으며 간단한 XML 기술을 사용하여 SQL 문을 JavaBeans (또는 Map) 에 간편하게 맵핑할 수 있다. * 추상화된 접근 방식 제공 : JDBC 데이터 억세스에 대한 추상화된 접근 방식으로 간편하고 쉬운 API, 자원 연결/해제, 공통 에러 처리 등을 통합 지원한다. * 코드로부터 SQL 분리 지원 : 소스코드로부터 SQL 문을 분리하여 별도의 repository(의미있는 문법의 XML)에 유지하고 이에 대한 빠른 참조구조를 내부적으로 구현하여 관리/유지보수/튜닝의 용이성을 보장한다. * 쿼리 실행의 입/출력 객체 바인딩/맵핑 지원 : 쿼리문의 입력 파라메터에 대한 바인딩과 실행결과 resultset 의 가공(맵핑) 처리시 객체(VO, Map, List) 수준의 자동화를 지원한다. * Dynamic SQL 지원 : 코드 작성, API 직접 사용없이 입력 조건에 따른 동적인 쿼리문 변경을 지원한다. * 다양한 DB 처리 지원 : 기본 질의 외에 Batch SQL, Paging, Callable Statement, BLOB/CLOB 등 다양한 DB처리를 지원한다. === 세부 사항 설명 === - [[.:dataaccess:iBATIS Configuration]] - [[.:dataaccess:Spring iBATIS Integration]] - [[.:dataaccess:Data Type]] - [[.:dataaccess:parameterMap]] - [[.:dataaccess:Inline parameters]] - [[.:dataaccess:resultMap]] - [[.:dataaccess:Dynamic SQL]] iBATIS Data Mapper API 는 XML을 사용하여 SQL 문에 대한 객체 맵핑을 간편하게 기술할 수 있도록 지원하며, 자바빈즈 객체와 Map 구현체, 다양한 원시 래퍼 타입(String, Integer..) 등을 PreparedStatement 의 파라메터나 ResultSet에 대한 결과 객체로 쉽게 맵핑해준다. {{:egovframework:rte:psl:ibatis_architecure.png|}} - 파라메터 객체를 제공한다. (마찬가지로 자바빈즈, Map 또는 원시 래퍼 일 수 있다.) 파라메터 객체는 update 문, 쿼리의 where 절의 input 변수로 세팅될 것이다. - mapped statement 를 실행한다. Data Mapper 프레임워크는 PreparedStatement 의 인스턴스를 생성하여 위에서 제공된 파라메터 객체를 이용해 파라메터를 세팅하고(바인드 변수처리),쿼리문을 실행하고 결과를 ResultSet 으로 부터 결과 객체로 작성한다. - update 문의 경우에는 변경 반영된 rows 수를 리턴하고, 조회의 경우 단건 조회 용 single 객체 또는 여러건 조회를 위한 Collection (객체의 List) 을 리턴하게 된다. 파라메터 객체와 마찬가지로 결과 객체의 JavaBean 이나 Map, primitive type wrapper 또는 XML 문서가 될 수 있다. ===== 설명 ===== Data Access 서비스에 대한 자세한 설명에 앞서 간단하게 Data Access 서비스를 시작하는데 필요한 것에 대한 설명을 하고자 한다. ==== Step1. 사전 준비 ==== === 필요 Library === 본 서비스를 활용하기 위해서 필요한 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 드라이버에 대한 라이브러리가 추가적으로 필요하다. ==== Step2. sql-map-config.xml 설정 및 기본 Spring 설정 ==== Spring 프레임워크 기반 어플리케이션에서 iBATIS 를 연동하여 사용하고자 하는 경우 Spring의 SqlMapClientFactoryBean 에 대한 설정이 필요하며 여기서는 실제 대상 sql-map-config 설정 파일과 iBATIS 에 제공될 dataSource 에 대한 설정을 지시한다. * Spring 연동 기능을 사용하면 iBATIS 의 SqlMapClient(a thread safe client for SQL Maps) 를 별도의 iBATIS API 없이도 얻을 수 있게 된다. * 실행환경 3.5 부터는 Spring 4 변경에 따라 org.springframework.orm.ibatis.SqlMapClientFactoryBean 클래스가 egovframework.rte.psl.orm.ibatis.SqlMapClientFactoryBean 로 변경된다. 아래는 주된 iBATIS 의 SQL Map XML Configuration 파일(sql-map-config.xml 설정 파일)이다. iBATIS 단독으로 쓰일 때는 transactionManager, dataSource 설정 등을 추가로 포함해야 하지만 Spring 연동 환경에서는 이 부분은 Spring 이 넘겨주는 dataSource 를 자동으로 사용하게 되고 transaction 관리는 비즈니스 서비스 영역에 선언적으로 설정하여 iBATIS 관련 모듈에서는 고민할 필요가 없게 된다. .. * sqlMapConfig : iBATIS 설정 파일의 root 태그 * settings : 다양한 옵션 설정을 지시할 수 있는 태그 (ex. cacheModel 사용여부, Runtime Bytecode Enhance 사용여부, 쿼리문에 대한 Namespace 사용여부 등의 옵션 설정 가능. cf. transaction/dataSource 연결 관련한 설정은 Spring 연동 환경에서는 필요 없음.) * typeHandler : javaType <-> jdbcType 간의 type 변환 처리가 별도로 필요한 경우 typeHandler 를 구현하고 이를 sql-map-config 에 등록함. * typeAlias : global 하게 사용할 typeAlias (클래스 풀 패키지명에 대한 간략한 별칭) 를 지정할 수 있음. * sqlMap : 각 SQL Mapping XML 파일을 등록함. classpath 나 url 로부터 해당 자원을 stream 형식으로 로딩하게 됨. resource 속성은 기본으로 classpath 경로를 바라본다. Spring 2.5.5 이상, iBATIS 2.3.2 이상 인 경우에는 iBATIS 연동을 위한 SqlMapClientFactoryBean 정의 시 mappingLocations 속성으로 Sql 매핑 파일에 대한 패턴 표현식으로 일괄 지정도 가능하다. mappingLocations 속성 사용 예는 다음과 같다. 이 경우는 "configLocation" 속성이 필요하지 않지만, 현재 해당 속성이 없는 경우 SqlMapClientFactoryBean의 초기화되지 않기 때문에 "configLocation" 속성을 유지하셔야 한다. 이 때 해당 sql-map-config.xml은 다음과 같이 dummy.xml query를 갖도록 처리하여야 한다. dummy.xml은 다음과 같이 처리한다. ==== Step3. sql mapping xml 설정 ==== iBATIS 에서 정의한 SQL Map 문서 구조 내에서 다양한 옵션 설정과 Mapped statement 정의를 작성하게 된다. 아래는 부서정보에 대한 CRUD 와 관련한 쿼리와 이에 대한 In/Out 객체 맵핑을 포함하는 간략한 매핑 파일이다. insert into DEPT (DEPT_NO, DEPT_NAME, LOC) values (#deptNo#, #deptName#, #loc#) update DEPT set DEPT_NAME = #deptName#, LOC = #loc# where DEPT_NO = #deptNo# delete from DEPT where DEPT_NO = #deptNo# * typeAlias : 현재 매핑 파일내에서 객체에 대한 간략한 alias 명을 지정함. * resultMap : DB 의 칼럼명과 객체의 Attribute 명에 대한 매핑을 작성함. javaType, jdbcType, columnIndex, typeHandler 등 다양한 추가 옵션 지정이 가능함. * insert : 입력 쿼리(insert 문)에 대한 Mapped Statement 정의 태그 * select : 조회 쿼리에 대한 Mapped Statement 정의 태그 * update : 수정 쿼리(update 문)에 대한 Mapped Statement 정의 태그 * delete : 삭제 쿼리(delete 문)에 대한 Mapped Statement 정의 태그 위의 CRUD 관련 매핑 파일에서는 기본으로 DeptVO 라는 JavaBeans 객체를 Parameter/Result 객체로 사용하고 있으며 이를 typeAlias 로 간략하게 지정하여 사용하고 있다. 위에서는 주로 parameterClass 로 지정하여 파라메터 객체에 대한 명시적 사용을 지시하고 있으며, 실제 바인드 변수 처리 시에는 #attribute명# 과 같이 Inline Parameter 형식을 사용하였다. 또한 resultMap 정의를 통하여 ResultSet 에 따른 결과 칼럼정보에 대한 결과 객체(DeptVO)의 필드별 매핑을 별도로 정의하였고 이를 select 문의 resultMap 속성에 명시하여 select 의 결과를 resultMap 을 통해 처리하고 있다. 이러한 방식 외에 다양한 방식으로 Mapped Statement 처리를 정의할 수 있으나, 위와 같이 JavaBeans 객체를 사용하고 또 resultMap 을 정의하여 결과객체 처리를 하며 Inline Parameter 방식으로 바인드 변수 처리하는 스타일로 사용하기를 권고하는 바이다. ==== 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 클래스 === .. @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 selectDeptList(DeptVO searchVO) { return list("selectDeptList", searchVO); } } * @Repository : DAO 에 대한 @Repository 스테레오 타입 Annotation 을 사용한 Spring Bean 정의 각 CRUD 에 관련한 메서드에서는 queryId 와 파라메터 객체(여기서는 DeptVO) 를 인자로 iBATIS 의 Mapped Statement 을 실행하고 있으며, 조회의 경우에는 단건 조회는 DeptVO 객체로, 리스트 조회는 DeptVO 에 대한 List 를 돌려주도록 하고 있다. iBATIS 내부적으로는 java 1.5 이상의 Generics (type 이 정의된 Collection 처리) 로 처리하지 않으나 sql 매핑 파일에 따라 실제 데이터는 List 로 처리가 될 것이므로 @SuppressWarnings("unchecked") 을 지시하여 호출 이전 모듈에서는 불필요한 Type Casting 을 최소화하고 있다. 만약 sql-map-config.xml 의 settings 옵션으로 useStatementNamespaces="true" 를 설정한 경우라면 위의 queryId 는 "Dept.insertDept" 와 같이 sql 맵핑 파일에 지정한 Namespace prefix 을 포함하는 형태여야 함에 유의한다. ==== Step5. 테스트 클래스 생성 ==== 위에서 정의한 설정 파일 및 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 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 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 테스트 로직을 구성하였다. ==== Step6. 실 행 ==== - 파일을 다운로드 받아서 압축을 푼다. - 이클립스에서 압축 푼 폴더를 선택하여 프로젝트를 Import 한다. - 프로젝트내 src 폴더에 DeptVO.java, DeptDAO.java, BasicDataAccessTest.java, Spring, iBATIS 설정파일 및 Sql 맵핑파일, 테스트 데이터용 초기화 스크립트 파일 및 log4j.xml 가 정상적으로 있는지 확인한다. - lib에 라이브러리 파일이 있는지 확인한다. - BasicDataAccessTest.java를 선택하여 마우스 오른쪽 클릭하여 Run As > JUnit Test 실행한다. - JUnit 결과창에서 정상적으로 수행된 것을 확인한다. ※ 해당 프로젝트는 Hsqldb (현재 메모리구동 방식)을 사용하고 있으나 다른 DBMS 를 쓰고자 하는 경우 jdbc.properties 에 관련 접속정보를 추가하고 JDBC 드라이버 jar 파일을 라이브러리로 추가하여 테스트 해 볼 수 있다. ===== 참고 자료 ===== * http://ibatis.apache.org * [[http://svn.apache.org/repos/asf/ibatis/trunk/java/ibatis-2/ibatis-2-docs/en/iBATIS-SqlMaps-2_en.pdf|iBATIS-SqlMaps-2 Developer Guide]] * [[http://kldp.net/frs/download.php/5035/iBATIS-SqlMaps-2_ko.pdf|iBATIS-SqlMaps-2 개발자 가이드 (이동국님 번역)]] * [[http://static.springframework.org/spring/docs/2.5.6/reference/orm.html#orm-ibatis|Spring Framework - Reference Documentation]]