Query Service - Extensions

프리젠테이션 레이어 개발 시 X-Internet 제품인 MiPlatform 또는 Gauce를 기반으로 할 경우 각 제품은 사용자 입력 사항을 고유의 데이터 형태에 저장하여 전달한다. 따라서 Query 서비스를 이용하여 DB의 데이터를 처리하기 위해서는 "제품 고유의 데이터 전달 형태 <-> Map 또는 VO 간의 변환"을 위한 추가 작업이 필요하며, 이로 인해 대량의 데이터를 다루는 경우 성능 저하가 발생할 가능성이 크다.
Query 서비스에서는 기본 QueryService를 확장하여, X-Internet 제품에 최적화된 형태의 MiPQueryService와 GauceQueryService를 제공함으로써 개발 편의성과 응답 속도 향상을 도모하고자 한다. 다음에서는 MiPQueryService와 GauceQueryService 사용 방법에 대해서 살펴보도록 한다.

MiPQueryService 활용

MiPQueryService는 MiPlatform 고유의 데이터 전달 형태로부터 사용자가 입력한 데이터를 추출하여 해당 DB에 반영하는 역할을 수행한다. 그러므로 투비소프트사의 X-Internet 제품인 MiPlatform 기반으로 프리젠테이션 레이어를 개발하는 경우, MiPQueryService를 통해 MiPlatform 고유의 데이터 전달 형태인 com.tobesoft.platform.data.Dataset, com.tobesoft.platform.data.VariableList를 그대로 이용할 수 있다.

MiPQueryService 속성 정의 파일 Sample

다음은 MiPQueryService를 정의한 applicationContext-query-mip.xml 파일의 일부이다. MiPQueryService는 내부적으로 RiaQueryService 를 통해 데이터 접근 처리를 수행하므로 RiaQueryService에 대한 참조 관계 설정이 필요하다.
<bean id="mipQueryService" 
  class="anyframe.core.query.ria.mip.impl.MiPQueryServiceImpl">
	<property name="riaQueryService" ref="riaQueryService"/>
</bean>	

<bean id="pagingNamedParamJdbcTemplate" 
  class="anyframe.core.query.impl.jdbc.PagingNamedParamJdbcTemplate">
	<constructor-arg index="0" ref="pagingJdbcTemplate"/>
 	<constructor-arg index="1" ref="dataSource"/>
</bean>
   
<bean name="riaQueryService" class="anyframe.core.query.ria.impl.RiaQueryServiceImpl">
	<property name="jdbcTemplate" ref="pagingNamedParamJdbcTemplate"/>
	<property name="lobHandler" ref="lobHandler"/>
	<property name="sqlRepository" ref="sqlLoader"/>
</bean>

매핑 XML 파일 샘플

다음은 앞서 정의한 RiaQueryService를 통해 로드된 mapping-mip-query.xml 로, Named Parameter를 이용한 쿼리문들을 포함하고 있다.
<queryservice>
	<queries>
		<query id="<strong>createMiPQueryService</strong>" isDynamic="true">
			<statement>
				INSERT INTO TB_MIP_TEST 
				(TEST_CHAR, TEST_VARCHAR2, TEST_NUMBER, TEST_DATE)
				 VALUES(:TEST_CHAR, :TEST_VARCHAR2, :TEST_NUMBER, :TEST_DATE)
			</statement>
		</query>
		
		<query id="<strong>updateMiPQueryService</strong>" isDynamic="true">
			<statement>
				UPDATE TB_MIP_TEST
				SET  TEST_VARCHAR2 = :TEST_VARCHAR2, 
				TEST_NUMBER = :TEST_NUMBER , TEST_DATE = :TEST_DATE
				WHERE TEST_CHAR = :TEST_CHAR
			</statement>
		</query>
		
		<query id="<strong>deleteMiPQueryService</strong>" isDynamic="true">
			<statement>
				DELETE FROM TB_MIP_TEST WHERE TEST_CHAR = :TEST_CHAR
			</statement>
		</query>
		
		<query id="<strong>findMiPQueryService</strong>" isDynamic="true">
			<statement>
				SELECT * 
				FROM TB_MIP_TEST 
				WHERE TEST_CHAR = :SEARCH_KEYWORD
			</statement>
		</query>
		
		<query id="<strong>findListMiPQueryService</strong>" isDynamic="true">
			<statement>
				SELECT * 
				FROM TB_MIP_TEST 
				WHERE TEST_CHAR like :SEARCH_KEYWORD
			</statement>
		</query>		
		
		<!-- 중략 -->
	</queries>
</queryservice>

Query 서비스 테스트 코드 Sample

다음에서는 MiPQueryService를 이용하여 앞서 언급한 매핑 XML 파일에 정의된 INSERT, SELECT, UPDATE, DELETE 쿼리문을 실행하는 소스 코드 MiPQueryServiceTestGeneral.java 이다.
/**
 * MiPlatform용 Query 서비스를 통해 DB에 신규 데이터를 입력하는 테스트 코드
 */    
public void testInsertDataSet() throws Exception {
    IMiPQueryService mipQueryService =
        (IMiPQueryService) context.getBean("mipQueryService");

    Map queryMap = new HashMap();
    queryMap.put(IMiPQueryService.QUERY_INSERT, "createMiPQueryService");

	// INSERT, UPDATE, DELETE 유형별로 사용할 query id를 정의한 Map과 DB에 반영해야 할 
	// Dataset을 전달. Dataset에는 다수의 데이터를 저장할 수 있음.
    int resultInsert =
        mipQueryService.update(queryMap, makeInsertDataSet());

    if (resultInsert != 3)
        throw new Exception("MiPlatform Dataset Insert failed");
}

/**
 * MiPlatform용 Query 서비스를 통해 DB에 입력된 데이터를 수정하는 테스트 코드
 */   
public void testUpdateDataSet() throws Exception {
    IMiPQueryService mipQueryService =
        (IMiPQueryService) context.getBean("mipQueryService");

    Map queryMap = new HashMap();
    queryMap.put(IMiPQueryService.QUERY_UPDATE, "updateMiPQueryService");
    
	// INSERT, UPDATE, DELETE 유형별로 사용할 query id를 정의한 Map과 DB에 반영해야 할 
	// Dataset을 전달. Dataset에는 다수의 데이터를 저장할 수 있음.    
    int resultUpdate =
        mipQueryService.update(queryMap, makeUpdateDataSet());

    if (resultUpdate != 1)
        throw new Exception("MiPlatform Dataset Update failed");
}

/**
 * MiPlatform용 Query 서비스를 통해 DB에 데이터를 입력/수정/삭제하는 테스트 코드
 */    
public void testProcessAllDataSet() throws Exception {
    IMiPQueryService mipQueryService =
        (IMiPQueryService) context.getBean("mipQueryService");

    Map queryMap = new HashMap();
    queryMap.put(IMiPQueryService.QUERY_UPDATE, "updateMiPQueryService");
    queryMap.put(IMiPQueryService.QUERY_INSERT, "createMiPQueryService");
    queryMap.put(IMiPQueryService.QUERY_DELETE, "deleteMiPQueryService");
    
   	// INSERT, UPDATE, DELETE 유형별로 사용할 query id를 정의한 Map과 DB에 반영해야 할
   	// Dataset을 전달. Dataset에는 다수의 데이터를 저장할 수 있음.            
    int resultUpdate = mipQueryService.update(queryMap, makeAllDataSet());

    if (resultUpdate != 3)
        throw new Exception("MiPlatform Dataset ProcessAll failed");
}

/**
 * MiPlatform용 Query 서비스를 통해 DB에 데이터를 입력/수정/삭제하는 테스트 코드
 * 이때, ActionCommand를 통해 전후처리 수행
 */    
public void testProcessAllDataSetWithActionCommand() throws Exception {
    IMiPQueryService mipQueryService =
        (IMiPQueryService) context.getBean("mipQueryService");

    Map queryMap = new HashMap();
    queryMap.put(IMiPQueryService.QUERY_INSERT, "createMiPQueryService");
    queryMap.put(IMiPQueryService.QUERY_UPDATE, "updateMiPQueryService");
    queryMap.put(IMiPQueryService.QUERY_DELETE, "deleteMiPQueryService");
    
   	// INSERT, UPDATE, DELETE 유형별로 사용할 query id를 정의한 Map, DB에 반영해야 할 
   	// Dataset, DB에 접근하기 전과 후에 처리해야 할 작업을 정의한 ActionCommand를 전달
   	// Dataset에는 다수의 데이터를 저장할 수 있음.     
    int resultUpdate =
        mipQueryService.update(queryMap, makeAllDataSet(),
            new IMiPActionCommand() {

                public void postDelete(Dataset record, int currentRow) {
                }

                public void postInsert(Dataset record, int currentRow) {
                }

                public void postUpdate(Dataset record, int currentRow) {
                }

                public void preDelete(Dataset record, int currentRow) {
                }

                public void preInsert(Dataset record, int currentRow) {
                    Variant variant = new Variant();
                    variant.setObject("Anyframe preUpdate");
                    record.setColumn(currentRow, "TEST_VARCHAR2", variant);
                }

                public void preUpdate(Dataset record, int currentRow) {
                }
            });

    if (resultUpdate != 3)
        throw new Exception(
            "MiPlatform Dataset ProcessAll With ActionCommand failed");
}

/**
 * MiPlatform용 Query 서비스를 통해 DB에 데이터를 조회하는 테스트 코드
 * 이때, 검색조건은 VariableList 형태임
 */     
public void testFindDataSetWithVariant() throws Exception {
    IMiPQueryService mipQueryService =
        (IMiPQueryService) context.getBean("mipQueryService");

	// 실행할 query id와 VariableList 형태의 검색 조건을 전달
    Dataset resultDataSet =
        mipQueryService.search("findMiPQueryService", makeVariantList());
    if (resultDataSet.getRowCount() != 1)
        throw new Exception("MiPlatform VariableList Find failed");
}

/**
 * MiPlatform용 Query 서비스를 통해 DB에 데이터를 조회하는 테스트 코드
 * 이때, 검색조건은 Dataset 형태임
 */    
public void testFindListDataSet(int expected) throws Exception {
    IMiPQueryService mipQueryService =
        (IMiPQueryService) context.getBean("mipQueryService");
    
    // 실행할 query id와 Dataset 형태의 검색 조건을 전달    
    Dataset resultDataSet =
        mipQueryService.search("findListMiPQueryService",
            makeSelectDataSet("%anyframe%"));

    if (resultDataSet.getRowCount() != expected)
        throw new Exception("MiPlatform Dataset Find failed");
}
위 소스 코드 중 testProcessAllDataSetWithActionCommand() 메소드에서는 ActionCommand를 이용하여 DB에 데이터를 입력하기 전에 특정 칼럼의 값을 변경하고 있다. 이와 같이 MiPQueryService는 anyframe.core.query.ria.mip.IMiPActionCommand를 구현한 별도 ActionCommand를 인자로 함께 전달하는 경우 입력 데이터를 DB에 반영하기 전후에 대한 공통 처리를 수행할 수 있게 된다. 예를 들어 입력받은 개별 Row를 DB에 신규 등록하기 전에 신규 식별자 값이 셋팅되어야 한다면, Loop을 돌면서 각 Row를 추출한 뒤 식별자를 셋팅하는 별도 로직없이 preInsert() 로직 내에 식별자 생성 구문이 추가된 ActionCommand 객체만 전달하면 되는 것이다.

GauceQueryService 활용

GauceQueryService는 Gauce 고유의 데이터 전달 형태로부터 사용자가 입력한 데이터를 추출하여 해당 DB에 반영하는 역할을 수행한다. 그러므로 쉬프트정보통신의 X-Internet 제품인 Gauce 기반으로 프리젠테이션 레이어를 개발하는 경우, GauceQueryService를 통해 Gauce 고유의 데이터 전달 형태인 com.gauce.GauceDataSet을 그대로 이용할 수 있다.

Query 서비스 속성 정의 파일 Sample

다음은 GauceQueryService를 정의한 applicationContext-query-gauce.xml 파일의 일부이다. GauceQueryService는 내부적으로 RiaQueryService 를 통해 데이터 접근 처리를 수행하므로 RiaQueryService에 대한 참조 관계 설정이 필요하다.
<bean id="gauceQueryService" class="anyframe.core.query.ria.gauce.impl.GauceQueryServiceImpl">
	<property name="riaQueryService" ref="riaQueryService"/>
</bean>	

<bean id="pagingNamedParamJdbcTemplate" 
  class="anyframe.core.query.impl.jdbc.PagingNamedParamJdbcTemplate">
	<constructor-arg index="0" ref="pagingJdbcTemplate"/>
 	<constructor-arg index="1" ref="dataSource"/>
</bean>
   
<bean name="riaQueryService" class="anyframe.core.query.ria.impl.RiaQueryServiceImpl">
	<property name="jdbcTemplate" ref="pagingNamedParamJdbcTemplate"/>
	<property name="lobHandler" ref="lobHandler"/>
	<property name="sqlRepository" ref="sqlLoader"/>
</bean>

매핑 XML 파일 Sample

다음은 앞서 정의한 RiaQueryService를 통해 로드된 mapping-gauce-query.xml 로, Named Parameter를 이용한 쿼리문들을 포함하고 있다.
<queryservice>
	<queries>
		<query id="createGauceQueryService" isDynamic="true">
			<statement>
				INSERT INTO TBL_GAUCE_TEST 
				(testchar, testvarchar, testint, testnumber, 
				  testbignumber, testdate)
				 VALUES(:testchar, :testvarchar, :testint, :testnumber, 
				   :testbignumber, TO_DATE(:testdate, 'YYYY-MM-DD HH24:MI:SS') )
			</statement>
		</query>
		
		<query id="updateGauceQueryService" isDynamic="true">
			<statement>
				UPDATE TBL_GAUCE_TEST
				SET	testvarchar = :testvarchar, 
					testint = :testint, 
					testnumber = :testnumber,
					testbignumber = :testbignumber,
					testdate = TO_DATE(:testdate, 'YYYY-MM-DD HH24:MI:SS')
				WHERE testchar = :testchar
			</statement>
		</query>
		
		<query id="deleteGauceQueryService" isDynamic="true">
			<statement>
				DELETE FROM TBL_GAUCE_TEST 
				WHERE testchar = :testchar
			</statement>
		</query>
		
		<query id="findGauceQueryService" isDynamic="true">
			<statement>
				SELECT * FROM TBL_GAUCE_TEST 
				WHERE testchar = :testchar
			</statement>
		</query>
	</queries>
</queryservice>	

Query 서비스 테스트 코드 Sample

다음에서는 GauceQueryService를 이용하여 앞서 언급한 매핑 XML 파일에 정의된 INSERT, SELECT, UPDATE, DELETE 쿼리문을 실행하는 소스 코드 GauceQueryServiceTestGeneral.java 이다.
/**
 * Gauce용 Query 서비스를 통해 DB에 신규 데이터를 입력하는 테스트 코드
 */
public void testInsertGauceDataSet() throws Exception {
	IGauceQueryService gauceQueryService = (IGauceQueryService) context
			.getBean("gauceQueryService");
	Map queryMap = new HashMap();
	queryMap.put(new Integer(GauceDataRow.TB_JOB_INSERT),
			"createGauceQueryService");

	// JOB의 유형별로 사용할 query id를 정의한 Map과 DB에 반영해야 할
	// GauceDataSet을 전달. GauceDataSet에는 다수의 데이터를 저장할 수 있음.
	int resultInsert = gauceQueryService.update(queryMap,
			makeInsertGauceDataSet());
	if (resultInsert != 3)
		throw new Exception("GauceDataSet Insert failed");
}

/**
 * Gauce용 Query 서비스를 통해 DB에 입력된 데이터를 수정하는 테스트 코드
 */
public void testUpdateGauceDataSet() throws Exception {
	IGauceQueryService gauceQueryService = (IGauceQueryService) context
			.getBean("gauceQueryService");
	Map queryMap = new HashMap();
	queryMap.put(new Integer(GauceDataRow.TB_JOB_UPDATE),
			"updateGauceQueryService");

	// JOB의 유형별로 사용할 query id를 정의한 Map과 DB에 반영해야 할
	// GauceDataSet을 전달. GauceDataSet에는 다수의 데이터를 저장할 수 있음.
	int resultUpdate = gauceQueryService.update(queryMap,
			makeUpdateGauceDataSet());
	if (resultUpdate != 1)
		throw new Exception("GauceDataSet Update failed");
}

/**
 * Gauce용 Query 서비스를 통해 DB에 데이터를 입력/수정/삭제하는 테스트 코드
 */
public void testProcessAllGauceDataSet() throws Exception {
	IGauceQueryService gauceQueryService = (IGauceQueryService) context
			.getBean("gauceQueryService");
	Map queryMap = new HashMap();
	queryMap.put(new Integer(GauceDataRow.TB_JOB_UPDATE),
			"updateGauceQueryService");
	queryMap.put(new Integer(GauceDataRow.TB_JOB_INSERT),
			"createGauceQueryService");
	queryMap.put(new Integer(GauceDataRow.TB_JOB_DELETE),
			"deleteGauceQueryService");

	// JOB의 유형별로 사용할 query id를 정의한 Map과 DB에 반영해야 할
	// GauceDataSet을 전달. GauceDataSet에는 다수의 데이터를 저장할 수 있음.
	int resultUpdate = gauceQueryService.update(queryMap,
			makeAllGauceDataSet());
	if (resultUpdate != 2)
		throw new Exception("GauceDataSet ProcessAll failed");
}

/**
 * Gauce용 Query 서비스를 통해 DB에 입력된 데이터를 조회하는 테스트 코드
 */
public void testFindGauceDataSet() throws Exception {
	IGauceQueryService gauceQueryService = (IGauceQueryService) context
			.getBean("gauceQueryService");

	// 실행할 query id와 GauceDataSet 형태의 검색 조건을 전달
	GauceDataSet resultDataSet = gauceQueryService.search(
			"findGauceQueryService", makeFindGauceDataSet());
	if (resultDataSet.getDataRowCnt() != 1)
		throw new Exception("GauceDataSet find failed");

	if (!"애니프레임자바 테스트".equals(resultDataSet.getDataRow(0).getColumnValue(
			resultDataSet.indexOfColumn("testvarchar"))))
		throw new Exception("GauceDataSet find failed");
}

/**
 * Gauce용 Query 서비스를 통해 DB에 입력된 데이터를 조회하는 테스트 코드 이때, ActionCommand를 통해 전후처리
 * 수행
 */
public void testFindGauceDataSetWithActionCommand() throws Exception {
	IGauceQueryService gauceQueryService = (IGauceQueryService) context
			.getBean("gauceQueryService");
	Map queryMap = new HashMap();
	queryMap.put(new Integer(GauceDataRow.TB_JOB_UPDATE),
			"updateGauceQueryService");
	queryMap.put(new Integer(GauceDataRow.TB_JOB_INSERT),
			"createGauceQueryService");
	queryMap.put(new Integer(GauceDataRow.TB_JOB_DELETE),
			"deleteGauceQueryService");

	// JOB의 유형별로 사용할 query id를 정의한 Map과 DB에 반영해야 할
	// GauceDataSet, DB에 접근하기 전과 후에 처리해야 할 작업을 정의한 ActionCommand를 전달
	// Dataset에는 다수의 데이터를 저장할 수 있음.
	int resultUpdate = gauceQueryService.update(queryMap,
			makeAllGauceDataSet(), new TestGauceActionCommand(
					"command_test"));
	if (resultUpdate != 3)
		throw new Exception("GauceDataSet find with ActionCommand failed");
}
위 소스 코드 중 testFindGauceDataSetWithActionCommand() 메소드에서는 ActionCommand를 이용하여 DB에 데이터를 입력하기 전에 특정 칼럼의 값을 변경하고 있다. 이와 같이 GauceQueryService는 anyframe.core.query.ria.gauce.IGauceActionCommand를 구현한 별도 ActionCommand를 인자로 함께 전달하는 경우 입력 데이터를 DB에 반영하기 전후에 대한 공통 처리를 수행할 수 있게 된다. 예를 들어 입력받은 개별 Row를 DB에 신규 등록하기 전에 신규 식별자 값이 셋팅되어야 한다면, Loop을 돌면서 각 Row를 추출한 뒤 식별자를 셋팅하는 별도 로직없이 preInsert() 로직 내에 식별자 생성 구문이 추가된 ActionCommand 객체만 전달하면 되는 것이다.

RiaQueryService

앞서 언급한 바와 같이 MiPQueryService, GauceQueryService는 DB에 사용자 입력 사항을 반영하기 위해서는 RiaQueryService를 이용해야 하며, RiaQuerySerivce, MiPQueryService, GauceQueryService 각각의 역할은 다음과 같다.
  1. MiPQueryService, GauceQueryService는 입력 데이터를 ParameterSource에 담고, 조회 결과를 처리할 개별 RowCallbackHandler와 함께 실제 데이터 접근 로직을 수행하는 RiaQueryService 에게 전달
  2. RiaQueryService는 MiPQueryService나 GauceQueryService 내에서 구현한 ParameterSource의 getValue() 메소드를 호출하며 입력값을 셋팅하고 해당 쿼리문 수행
  3. RiaQueryService는 MiPQueryService나 GauceQueryService 내에서 구현한 RowCallbackHandler의 콜백 메소드를 호출함으로써 MiPlatform이나 Gauce에서 사용할 수 있는 고유의 데이터 형태로 조회 결과값을 변환
따라서 RiaQueryService는 개발자가 직접 API를 호출하여 사용하는 서비스라기 보다는 MiPQueryService나 GauceQueryService 내부적으로 참조하는 서비스이다.

Query 서비스 속성 정의 파일 Sample

다음은 RiaQueryService를 정의한 applicationContext-query-mip.xml 파일의 일부이다. RiaQueryService 기본 Query 서비스를 확장하고 있으므로 Query 서비스 속성 정의 방법 과 동일하게 속성을 정의해주면 된다.
<!--중략-->
<bean id="pagingNamedParamJdbcTemplate" 
  class="anyframe.core.query.impl.jdbc.PagingNamedParamJdbcTemplate">
	<constructor-arg index="0" ref="pagingJdbcTemplate"/>
 	<constructor-arg index="1" ref="dataSource"/>
</bean>
   
<bean name="riaQueryService" class="anyframe.core.query.ria.impl.RiaQueryServiceImpl">
	<property name="jdbcTemplate" ref="pagingNamedParamJdbcTemplate"/>
	<property name="lobHandler" ref="lobHandler"/>
	<property name="sqlRepository" ref="sqlLoader"/>
</bean>
앞서 소개된 샘플 테스트 코드를 포함하여 Query 서비스 소개 페이지에서 제공하는 모든 샘플 테스트 코드는 HSQL DB를 기반으로 실행된다. ( 단, ※ CallableStatement, LOB의 경우는 Oracle 9i, 10g를 기반으로 함. )