오늘은 어디로 갈까...

6. QueryExecutor 본문

ORM 만들기

6. QueryExecutor

剛宇 2009. 8. 28. 14:39

 QueryExecutor는 말 그대로 쿼리 실행기이다. 

 여기서는 기본적으로 제공하는 SimpleQueryExecutor를 기준으로 설명하겠다. SimpleQueryExecutor는 쿼리와 해당 파라메터를 넘겨받아, PreparedStatement을 사용해서 실행하는 역할을 한다.

 넘어온 쿼리와 파라메터를 이용해서 실행 할수 있는 쿼리(RunnableQuery)를 생성해 낸 다음, SQL문으로 PreparedStatement를 생성해하고, 파라메터들 값을 설정한다. 만약 세션에 동일한 SQL문이 있다면 PreparedStatement를 재사용하도록 되어있다.

	/**
	 * <p>결과값을 돌려주는 SQL을 실행한다.</p>
	 * 
	 * @param <E>
	 * @param session
	 * @param conn
	 * @param query 쿼리
	 * @param parameterObject 파라메터 객체
	 * @param resultClass
	 * @param rowHandler
	 * @throws SQLException
	 * @see kr.kangwoo.damo.engine.executor.QueryExecutor#executeQuery(kr.kangwoo.damo.Session, java.sql.Connection, kr.kangwoo.damo.engine.query.Query, java.lang.Object, java.lang.Class, kr.kangwoo.damo.engine.query.result.RowHandler)
	 */
	public <E> void executeQuery(Session session, Connection conn,
			Query query, Object parameterObject, Class<E> resultClass, RowHandler<E> rowHandler) throws SQLException {
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try {
			final RunnableQuery runnableQuery = query.getRunnableQuery(parameterObject);
			ExecuteInterceptor[] interceptors = query.getExecuteInterceptors();
			boolean hasInterceptors = (interceptors != null) && interceptors.length > 0;
			if (hasInterceptors) {
				for (ExecuteInterceptor interceptor : interceptors) {
					interceptor.beforeExecute(session, conn, runnableQuery, parameterObject);	
				}
			}
			pstmt = prepareStatement(session, conn, runnableQuery);
			bindParameters(session, pstmt, runnableQuery, parameterObject);
			pstmt.execute();
			rs = handleResult(session, conn, pstmt, runnableQuery, parameterObject, resultClass, rowHandler, null, null);
			if (hasInterceptors) {
				for (ExecuteInterceptor interceptor : interceptors) {
					interceptor.afterExecute(session, conn, runnableQuery, parameterObject, rowHandler);	
				}
			}
		} finally {
			closeResultSet(rs);
			closeStatement(session, pstmt);
		}
	}
	
	/**
	 * <p>결과값을 돌려주는 SQL을 실행한다.(자동적으로 페이징 처리를 한후, 해당 페이지만 돌려준다.)</p>
	 * <p>(여기서는 커서를 이용해서 페이지 이동을 하였다.)</p>
	 * @param <E>
	 * @param session
	 * @param conn
	 * @param query
	 * @param parameterObject 파라메터 객체
	 * @param resultClass
	 * @param rowHandler
	 * @param pageNo 페이지번호
	 * @param pageSize 페이지크기
	 * @throws SQLException
	 */
	public <E> void executeQuery(Session session, Connection conn,
			Query query, Object parameterObject, Class<E> resultClass, RowHandler<E> rowHandler, int pageNo, int pageSize) throws SQLException {
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try {
			final RunnableQuery runnableQuery = query.getRunnableQuery(parameterObject);
			ExecuteInterceptor[] interceptors = query.getExecuteInterceptors();
			boolean hasInterceptors = (interceptors != null) && interceptors.length > 0;
			if (hasInterceptors) {
				for (ExecuteInterceptor interceptor : interceptors) {
					interceptor.beforeExecute(session, conn, runnableQuery, parameterObject);	
				}
			}
			pstmt = prepareStatement(session, conn, runnableQuery);
			bindParameters(session, pstmt, runnableQuery, parameterObject);
			pstmt.execute();
			rs = handleResult(session, conn, pstmt, runnableQuery, parameterObject, resultClass, rowHandler, pageNo, pageSize);
			if (hasInterceptors) {
				for (ExecuteInterceptor interceptor : interceptors) {
					interceptor.afterExecute(session, conn, runnableQuery, parameterObject, rowHandler);	
				}
			}
		} finally {
			closeResultSet(rs);
			closeStatement(session, pstmt);
		}
	}

 executeQuery() 메소드보를 보면 RowHandler란 것이 있다. 이것은 실행 결과(ResultSet)을 처리하여, 요청한 객체에게 넘겨주기 위한 작업을 한다. 기본적으로는 ListRowHandler 클래스가 구현되어 있다. 이 ListRowHandler는 java.utli.List 객체를 생성하여 값을 추가한다음 반환하주는 역할을 한다.
 그렇다면 그냥 List 객체를 만들어서 반환해주면 되지, 왜 RowHandler란 인터페이스를 만들어놓은것일까?
 그것은, 데이터량이 작거나 메모리가 충분한 경우에는 List 같은 Collection 객체를 사용하여 요청 객체에 값을 전달해줄 수 있지만, 대량의 데이터를 처리할 경우 문제가 발생할 수도 있다는 것이다. 즉, OOE(OutOfMemeory)가 발생할 수 있다. 이럴 경우에는 RowHandler를 직접 구현하여 한 로우(row)씩 처리할 수 있는 방법을 제공해 주는 것이다. 레이어(Layer) 구분없는 구조일 경우에는 이런 일이 없지만, 비지니스 로직 부분(Business Logic Layer)와 데이터 저장 부분(Persistence Layer)이 구분되어 있는 경우에는 이런 경우를 항상 신경써야하는것이다.

 현재 QueryExecutor에는 ExecuteInterceptor와 HandleRowInterceptor라는 두가지의 Interceptor가 정의되어 있다. ExecuteInterceptor는 쿼리를 실행하기 전/후의 이벤트를 처리하기 위함이고, HandleRowInterceptor는 ResuletSet에서 값을 가져올 때의 이벤트를 처리한다. 아직 이 이벤트 처리에 대한 부분은 명확한 확신을 가지고 있지 못함으로 나중에 변경될지도 모른다. 
 
 kr.kangwoo.damo.engine.executor 패키지를 살펴보신분은 알겠지만, OracleQueryExecutor 클래스가 있을것이다.
/*
 * Copyright 2002-2009 Team Jaru.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package kr.kangwoo.damo.engine.executor;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.ConcurrentHashMap;

import kr.kangwoo.damo.Session;
import kr.kangwoo.damo.engine.query.Query;
import kr.kangwoo.damo.engine.query.RunnableQuery;
import kr.kangwoo.damo.engine.query.StaticQuery;
import kr.kangwoo.damo.engine.query.parameter.ParameterInfo;
import kr.kangwoo.damo.engine.query.result.RowHandler;
import kr.kangwoo.util.SystemUtils;

/**
 * 오라클용 쿼리 실행기이다.
 * 
 * @author badnom
 *
 */
public class OracleQueryExecutor extends SimpleQueryExecutor {

	private ConcurrentHashMap<String, RunnableQuery> pagingQueryMap = new ConcurrentHashMap<String, RunnableQuery>();
	
	public OracleQueryExecutor() {
	}

	private RunnableQuery getPagingQuery(RunnableQuery runnableQuery) throws SQLException {
		RunnableQuery pagingQuery = pagingQueryMap.get(runnableQuery.getSql());
		if (pagingQuery == null) {
			StringBuilder sql = new StringBuilder();
			sql.append("SELECT * FROM (").append(SystemUtils.LINE_SEPARATOR); //$NON-NLS-1$
			sql.append("\tSELECT ROWNUM T_RNUM, T_PAGING.* FROM (").append(SystemUtils.LINE_SEPARATOR); //$NON-NLS-1$
			sql.append(runnableQuery.getSql()).append(SystemUtils.LINE_SEPARATOR);
			sql.append("\t) T_PAGING ").append(SystemUtils.LINE_SEPARATOR); //$NON-NLS-1$
			sql.append("\tWHERE ROWNUM <= ? ").append(SystemUtils.LINE_SEPARATOR); //$NON-NLS-1$
			sql.append(") WHERE T_RNUM > ?").append(SystemUtils.LINE_SEPARATOR); //$NON-NLS-1$
			StaticQuery query = new StaticQuery(runnableQuery.getCommandType(), sql.toString(), runnableQuery.getParameterInfos(), runnableQuery.getDescription());
			query.setFetchSize(runnableQuery.getFetchSize());
			query.setResultSetType(runnableQuery.getResultSetType());
			query.setResultSetConcurrency(runnableQuery.getResultSetConcurrency());
			pagingQuery = query;
			
			RunnableQuery temp = pagingQueryMap.putIfAbsent(runnableQuery.getSql(), pagingQuery);
			if (temp != null) {
				pagingQuery = temp; 
			}
		}
		return pagingQuery;
	}
	
	private void setParameter(Session session, PreparedStatement pstmt,
			RunnableQuery query, Object parameterObject, Integer pageNo, Integer pageSize) throws SQLException {
		super.bindParameters(session, pstmt, query, parameterObject);
		if (pageNo != null && pageSize != null) {
			ParameterInfo<?>[] inputParameterInfos = getInputParameterInfos(query);
			int index = (inputParameterInfos != null) ? inputParameterInfos.length + 1 : 1;
			int endRow = ((pageNo - 1) * pageSize);
			int beginRow = endRow - pageSize;
			
			pstmt.setInt(index++, endRow);
			pstmt.setInt(index++, beginRow);
		}
	}
	/**
	 * <p>결과값을 돌려주는 SQL을 실행한다.(자동적으로 페이징 처리를 한후, 해당 페이지만 돌려준다.)</p>
	 * <p>(여기서는 서브쿼리를 이용해서 페이지 이동을 하였다.)</p>
	 * 
	 * @param <E>
	 * @param session
	 * @param conn
	 * @param query
	 * @param parameterObject 파라메터 객체
	 * @param resultClass
	 * @param rowHandler
	 * @param pageNo 페이지번호
	 * @param pageSize 페이지크기
	 * @throws SQLException
	 */
	@Override
	public <E> void executeQuery(Session session, Connection conn,
			Query query, Object parameterObject, Class<E> resultClass, RowHandler<E> rowHandler, int pageNo, int pageSize) throws SQLException {
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try {
			RunnableQuery runnableQuery = query.getRunnableQuery(parameterObject);
			runnableQuery = getPagingQuery(runnableQuery);
			// ResultSetType
			pstmt = prepareStatement(session, conn, runnableQuery);
			// FetchSize
			if (runnableQuery.getFetchSize() != null) {
				pstmt.setFetchSize(runnableQuery.getFetchSize());
			}
			setParameter(session, pstmt, runnableQuery, parameterObject, pageNo, pageSize);
			pstmt.execute();
			rs = handleResult(session, conn, pstmt, runnableQuery, parameterObject, resultClass, rowHandler, null, null);
		} finally {
			closeResultSet(rs);
			closeStatement(session, pstmt);
		}
	}

}

이 클래스는 SimpleQueryExecutor 클래스를 상속받았는데, 페이징 처리 부분과 다시 구현하고 있다. SimpleQueryExecutor 는 자동 페이정 처리 부분을 커서를 이용해서 한다. OracleQueryExecutor 클래스에서는 서브 쿼리를 이용해서 페이징 처리를 구현하였다. 물론 최적하된 쿼리는 아니지만 말이디.
 오라클 사용자일 경우 damo.properties 파일에
  QueryExecutor=kr.kangwoo.damo.engine.executor.OracleQueryExecutor
을 설정하여. 페이징을 서브쿼리로 처리할 수 있는것이다.