오늘은 어디로 갈까...

4. PersistenceManager 본문

ORM 만들기

4. PersistenceManager

剛宇 2009. 8. 25. 22:59

 

 어떤 놈을 먼저 설명해볼까 고민을 한 끝에, PersistenceManager를 해부대에 올려버렸다. 그 이유는 시작과 끝이 이놈을 통해서 일어나기 때문이다.
 예를 들어 등록을 실행할경우 아래 그림처럼 흘러간다.



 그림이 정확한것은 아니기에 흐름만을 파악하면 될것이다. PersistenceManager의 insert(EntityObject) 메소드가 호출되되면, QueryProvider에가 해당 쿼리를 요청한 후 결과를 받는다. 그림에는 없지만 세션이 존재한다면, 기존에 존재하는 세션을 사용하고 없다면 새로운 세션을 생성하여 할당받는다. 그리고 세션이 새로운것이나 트랜잭션이 시작되지 않은 상태라면, 트랜잭션을 시작한다. 그런다음 QueryExecutor를 통해 해당 쿼리를 실행 시킨후 트랜잭션 관련 작업을 한후 결과를 돌려주는것이다.

 그렇다면 소스를 한번 보도록 하자.

/*
 * 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;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

import kr.kangwoo.damo.config.DamoConfigLoader;
import kr.kangwoo.damo.engine.IBatchService;
import kr.kangwoo.damo.engine.IPersistenceService;
import kr.kangwoo.damo.engine.ISessionService;
import kr.kangwoo.damo.engine.ITransactionService;
import kr.kangwoo.damo.engine.cache.CacheProvider;
import kr.kangwoo.damo.engine.executor.QueryExecutor;
import kr.kangwoo.damo.engine.provider.QueryProvider;
import kr.kangwoo.damo.engine.query.Query;
import kr.kangwoo.damo.engine.query.RunnableQuery;
import kr.kangwoo.damo.engine.transaction.Transaction;
import kr.kangwoo.damo.engine.transaction.TransactionManager;

/**
 * 영속성 관리자(PersistenceManager)이다. 
 * 세션 서비스, 트랙잭션 서비스, 실행 서비스, 배치 서비스들을 제공한다.
 * 
 * <pre>
 * PersistenceManager pm = PersistenceManager.getPersistenceManager();
 * pm.beginTransaction();
 * try {
 * 	Entity entity.setId = new Entity();
 * 	entity.setId(1004);
 * 	entity.setName("바보");
 * 	int updateRows = pm.insert(entity);
 * 	pm.commitTransaction();
 * } finally {
 * 	pm.endTransaction();
 * }
 * 
 * 
 * </pre>
 * 
 * @author badnom
 * @author agoodguy
 *
 */
public abstract class PersistenceManager extends ExtendedExecutor implements IBatchService, ISessionService,
		ITransactionService, IPersistenceService {

	private static class PersistenceManagerHolder {
		public static PersistenceManager persistenceManager = new PersistenceManagerImpl(DamoConfigLoader.getDamoConfig());
	}
	
	protected SessionProvider sessionProvider;
	protected DataSource dataSource;
	protected CacheProvider cacheProvider;
	protected QueryExecutor queryExecutor;
	protected QueryProvider queryProvider;
	protected TransactionManager transactionManager;

	protected abstract Session getLocalSession();
	
	public static PersistenceManager getPersistenceManager() {
		return PersistenceManagerHolder.persistenceManager;
	}
	
	public void shutdown() {
	}

	public Session openSession() {
		return sessionProvider.createSession(this);
	}

	public Session openSession(Connection conn) throws SQLException {
		Session session = sessionProvider.createSession(this);
		session.setUserConnection(conn);
		return session;
	}

	public void beginTransaction() throws SQLException {
		getLocalSession().beginTransaction();
	}

	public void beginTransaction(int transactionIsolationLevel)
			throws SQLException {
		getLocalSession().beginTransaction(transactionIsolationLevel);
	}

	public void commitTransaction() throws SQLException {
		getLocalSession().commitTransaction();
	}

	public void rollbackTransaction() throws SQLException {
		getLocalSession().rollbackTransaction();
	}

	public void endTransaction() throws SQLException {
		try {
			getLocalSession().endTransaction();
		} finally {
			getLocalSession().close();
		}
	}

	public Connection getCurrentConnection() throws SQLException {
		return getLocalSession().getCurrentConnection();
	}

	public void setUserConnection(Connection conn) throws SQLException {
		getLocalSession().setUserConnection(conn);
	}

	public SessionProvider getSessionProvider() {
		return sessionProvider;
	}
	
	public DataSource getDataSource() {
		return dataSource;
	}

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public CacheProvider getCacheProvider() {
		return cacheProvider;
	}
	
	public QueryExecutor getQueryExecutor() {
		return queryExecutor;
	}

	public void setQueryExecutor(QueryExecutor queryExecutor) {
		this.queryExecutor = queryExecutor;
	}

	public QueryProvider getQueryProvider() {
		return queryProvider;
	}
	
	public void setQueryProvider(QueryProvider queryProvider) {
		this.queryProvider = queryProvider;
	}

	public TransactionManager getTransactionManager() {
		return transactionManager;
	}

	public void setTransactionManager(TransactionManager transactionManager) {
		this.transactionManager = transactionManager;
	}

	@Override
	public Query getQuery(String queryId) throws SQLException {
		return queryProvider.getQuery(queryId);
	}
	
	/**
	 * <p> 일괄 작업(Batch)을 추가한다.</p>
	 * <p>주의 : 트랜잭션을 직접 시작했을 경우만 사용할수 있다.</p>
	 * 
	 * @param query
	 * @param parameterObject
	 * @throws SQLException
	 */
	public void addBatch(RunnableQuery query, Object parameterObject)
			throws SQLException {
		Session session = getLocalSession();
		Transaction transaction = session.getTransaction();
		if (transaction == null) {
			throw new SQLException(Messages.getString("PersistenceManager.BATCH_TRAN_NOT_BEGIN")); //$NON-NLS-1$
		}
		session.addBatch(query, parameterObject);
	}

	/**
	 * <p>일괄 작업(Batch)을 실행한다.</p>
	 * <p>주의 : 트랜잭션을 직접 시작했을 경우만 사용할수 있다.</p>
	 * 
	 * @param query
	 * @return
	 * @throws SQLException
	 */
	public int[] executeBatch(RunnableQuery query) throws SQLException {
		Session session = getLocalSession();
		Transaction transaction = session.getTransaction();
		if (transaction == null) {
			throw new SQLException(Messages.getString("PersistenceManager.BATCH_TRAN_NOT_BEGIN")); //$NON-NLS-1$
		}
		return session.executeBatch(query);
	}
	

}

/*
 * 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;

import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import kr.kangwoo.damo.config.DamoConfig;
import kr.kangwoo.damo.config.DamoConfigLoader;
import kr.kangwoo.damo.config.IDamoConfig;
import kr.kangwoo.damo.engine.batch.BatchCallback;
import kr.kangwoo.damo.engine.batch.BatchResult;
import kr.kangwoo.damo.engine.query.Query;
import kr.kangwoo.damo.engine.query.result.RowHandler;
import kr.kangwoo.damo.engine.transaction.BaseTransactionProvider;
import kr.kangwoo.damo.engine.transaction.Transaction;
import kr.kangwoo.damo.engine.transaction.TransactionException;
import kr.kangwoo.damo.engine.transaction.TransactionManager;
import kr.kangwoo.damo.engine.transaction.TransactionProvider;
import kr.kangwoo.damo.engine.transaction.jdbc.JdbcTransactionProvider;
import kr.kangwoo.util.logging.Logger;
import kr.kangwoo.util.logging.LoggerFactory;

/**
 * 영속성 관리자(PersistenceManager)의 최종 구현체이다.
 * 
 * @author badnom
 * @author agoodguy
 *
 */
public class PersistenceManagerImpl extends PersistenceManager {
	
	private Logger logger = LoggerFactory.getLogger(PersistenceManagerImpl.class);
	
	private final ThreadLocal<Session> localSession = new ThreadLocal<Session>();
	private TransactionProvider transactionProvider;
	
	public PersistenceManagerImpl() {
		this(new DamoConfig());
	}
	
	public PersistenceManagerImpl(IDamoConfig damoConfig) {
		if (damoConfig == null) {
			logger.warn(Messages.getString("PersistenceManagerImpl.DAMO_CONFIG_IS_NULL")); //$NON-NLS-1$
			damoConfig = DamoConfigLoader.getDamoConfig();
		}
		init(damoConfig);
	}
	
	public PersistenceManagerImpl(Properties properties) {
		this(new DamoConfig(properties));
	}
	
	protected void init(IDamoConfig damoConfig) {
		sessionProvider = new SessionProvider();
		sessionProvider.setMaxActive(damoConfig.getSessionMaxActive());
		sessionProvider.setTimeout(damoConfig.getSessionMaxWait());
		
		cacheProvider = damoConfig.getCacheProvider();
		
		queryProvider = damoConfig.getQueryProvider();
		
		queryExecutor = damoConfig.getQueryExecutor();
		
		transactionProvider = damoConfig.getTransactionProvider();
		DataSource dataSource = damoConfig.getDataSource();
		transactionManager = new TransactionManager(transactionProvider);
		transactionManager.setMaxActive(damoConfig.getTransactionMaxActive());
		transactionManager.setTimeout(damoConfig.getTransactionMaxWait());
		if (dataSource != null) {
			setDataSource(dataSource);
		}
	}
	
	@Override
	protected Session getLocalSession() {
		Session session = localSession.get();
		if (session == null || session.isClosed()) {
			session = sessionProvider.createSession(this);
			localSession.set(session);
		}
		return session;
	}
	
	@Override
	public void setDataSource(DataSource dataSource) {
		super.setDataSource(dataSource);
		if (transactionProvider == null) {
			logger.warn(Messages.getString("PersistenceManagerImpl.TRANSACTION_PROVIDER_IS_NULL")); //$NON-NLS-1$
			transactionProvider = new JdbcTransactionProvider();	
			transactionManager.setTransactionProvider(transactionProvider);
		}
		transactionProvider.setDataSource(dataSource);	
	}
	
	public void setTransactionProvider(BaseTransactionProvider transactionProvider) {
		this.transactionProvider = transactionProvider;
		if (transactionProvider != null && transactionProvider.getDataSource() == null && dataSource != null) {
			transactionProvider.setDataSource(dataSource);	
		}
		this.transactionManager.setTransactionProvider(transactionProvider);
	}
	
	public int executeUpdate(Query query, Object entityObject) throws SQLException {
		Session session = getLocalSession();
		int updateCount = 0;
		Transaction transaction = session.getTransaction();
		boolean autoBegin = (transaction == null);
		try {
			transaction = autoBeginTransaction(session, autoBegin, transaction);
			updateCount = session.executeUpdate(query, entityObject);
			autoCommitTransaction(session, autoBegin);
		} finally {
			autoEndTransaction(session, autoBegin);
		}
		return updateCount;
	}
	
	public <E> void executeQuery(Query query, Object parameterObject, Class<E> resultClass, RowHandler<E> rowHandler) throws SQLException {
		Session session = getLocalSession();
		Transaction transaction = session.getTransaction();
		boolean autoBegin = (transaction == null);
		try {
			transaction = autoBeginTransaction(session, autoBegin, transaction);
			session.executeQuery(query, parameterObject, resultClass, rowHandler);
			autoCommitTransaction(session, autoBegin);
		} finally {
			autoEndTransaction(session, autoBegin);
		}
	}
	
	public <E> E executeCallable(Query query, Object parameterObject,
			Class<E> resultClass) throws SQLException {
		E e = null;
		Session session = getLocalSession();
		Transaction transaction = session.getTransaction();
		boolean autoBegin = (transaction == null);
		try {
			transaction = autoBeginTransaction(session, autoBegin, transaction);
			e = session.executeCallable(query, parameterObject, resultClass);
			autoCommitTransaction(session, autoBegin);
		} finally {
			autoEndTransaction(session, autoBegin);
		}
		return e;
	}
	
	public <E> void executeQuery(Query query, Object parameterObject, Class<E> resultClass, RowHandler<E> rowHandler, int pageNo, int pageSize) throws SQLException {
		Session session = getLocalSession();
		Transaction transaction = session.getTransaction();
		boolean autoBegin = (transaction == null);
		try {
			transaction = autoBeginTransaction(session, autoBegin, transaction);
			session.executeQuery(query, parameterObject, resultClass, rowHandler, pageNo, pageSize);
//			autoCommitTransaction(session, autoBegin);
		} finally {
			autoEndTransaction(session, autoBegin);
		}
	}
	
	/**
	 * <p>배치를 실행한다.</p>
	 * 
	 * @param batchCallback
	 * @throws SQLException
	 */
	public BatchResult executeBatch(BatchCallback batchCallback) throws SQLException {
		BatchResult result = null;
		Session session = getLocalSession();
		Transaction transaction = session.getTransaction();
		boolean autoBegin = (transaction == null);
		try {
			transaction = autoBeginTransaction(session, autoBegin, transaction);
			result = session.executeBatch(batchCallback);
			autoCommitTransaction(session, autoBegin);
		} finally {
			autoEndTransaction(session, autoBegin);
		}
		return result;
		
	}
	/**
	 * <p>자동 시작일 경우 트랜잭션을 새로 만들어서 반환하고, 아니면 기존 트랜잭션을 반환한다.</p>
	 * 
	 * @param session
	 * @param autoBegin 자동 시작 여부
	 * @param transaction
	 * @return
	 * @throws SQLException
	 * @throws TransactionException
	 */
	private Transaction autoBeginTransaction(Session session,
			boolean autoBegin, Transaction transaction) throws SQLException, TransactionException {
		if (autoBegin) {
			session.beginTransaction();
			transaction = session.getTransaction();
		}
		return transaction;
	}
	
	/**
	 * <p>자동 시작일 경우 커밋을 한다.</p>
	 * 
	 * @param session
	 * @param autoBegin 자동 시작 여부
	 * @throws SQLException
	 * @throws TransactionException
	 */
	private void autoCommitTransaction(Session session, boolean autoBegin) throws SQLException, TransactionException {
		if (autoBegin) {
			session.commitTransaction();
		}
	}
	
	/**
	 * <p>자동 시작일 경우 트랜잭션을 종료 한다.</p>
	 * 
	 * @param session
	 * @param autoBegin
	 * @throws SQLException
	 * @throws TransactionException
	 */
	private void autoEndTransaction(Session session, boolean autoBegin) throws SQLException, TransactionException {
		if (autoBegin) {
			try {
				session.endTransaction();
			} finally {
				session.close();
			}
		}
	}

}


 소스를 보면 별로 어려운것은 없을 것이다. 설정 파일을 가지고 PersistenceManager를 초기화 시키고, DAMO에 필요한 QueryProvider, QueryExecutor, TransactionManager등을 설정할 수 있게 해주는것이다.

 위 두 클래스에서 기술적으로 설명해야할 부분은 두가지 인거 같다.

 첫번째는 싱글톤(Singleton) 패턴이다.

 PersistenceManager은 getPersistenceManager() 메소드를 통해 인스턴스를 반환하게 되어있다. 해당 인서턴스는 하나만 존재해야한다. 그래서 싱글톤 패턴을 이용한것이다.
 싱글톤을 가장 쉽게 구현하는 방법은 아래처럼 하는것이다. 이렇게 하면 안정성과 성능 두가지 모두를 만족 시킬 수 있다.

	private static final PersistenceManager persistenceManager = new PersistenceManagerImpl(DamoConfigLoader.getDamoConfig());
	
	public static PersistenceManager getPersistenceManager() {
		return persistenceManager;
	}


 하지만, 한 가지 문제가 있는데, 사용하지 않음에도 불구하고 인스턴스가 생성될 수 있다는것이다. getPersistenceManager() 메소드를 이용할 경우에는 문제가 전혀 없으나, 이 메소드를 사용하지 않고 다른 PersistenceManager 객체를 생성해서 사용 할 경우 불필요하게 싱글톤 객체가 생성되어버리는것이다.

 그러면 어떤식으로 싱글톤을 구현해야하는것인가? 가장 간단한 방법은 getPersistenceManger() 메소드를 동기화(synchronized) 가능하게 하게 만들어버리는것이다.

	private static PersistenceManager persistenceManager;

	public static synchronized PersistenceManager getPersistenceManager() {
		if (persistenceManager == null) {
			persistenceManager = new PersistenceManagerImpl(DamoConfigLoader.getDamoConfig());
		}
		return persistenceManager;
	}

 이 방법이 틀린것은 아니지만, 성능상에서 나은 결과를 보여주지 않는다. 물룬 자바 1.6부터는 동기화의 성능이 상당히 나아지긴 했지만, DAMO가 돌아가는것은 1.5부터이기에 이런것을 고려해야한다. 더군다나 초기 인스턴스를 만들때는 동기화가 필요하지만, 생성된 후에 동기화는 전혀 무의미한 짓이기때문이다.
 본인같은 하수가 머리를 굴리면 아마 아래처럼 DCL(Double Checked Lock)을 이용해서 구현할 것이다. 소스상으로 별 문제가 없어보이지만, 1.4 이하 버젼에서는 정상적인 작동을 보장하지 못한다. 그 이유는.. 찾아보길 바란다.(참소 주소를 할당하는 문제랄까...) 들리는 소문에 1.5부터는 정상적으로 작동한다고 한다.
	private static volatile PersistenceManager persistenceManager;
	private static Object lock = new Object();
	
	public static PersistenceManager getPersistenceManager() {
		if (persistenceManager == null) {
			synchronized(lock) {
				if (persistenceManager == null) {
					persistenceManager = new PersistenceManagerImpl(DamoConfigLoader.getDamoConfig());	
				}
			}
		}
		return persistenceManager;
	}
 여기서는 좀더 깔끔한 Holder라는 방법을 사용하였다. 이 방법이 가장 세련되어 보이기는 하는데, 솔직히 자신은 없다...
	private static class PersistenceManagerHolder {
		public static PersistenceManager persistenceManager = new PersistenceManagerImpl(DamoConfigLoader.getDamoConfig());
	}
	
	public static PersistenceManager getPersistenceManager() {
		return PersistenceManagerHolder.persistenceManager;
	}
	

 두번째는, ThreadLocal이다. 자바 1.2부터 제공되고 있지만, 아마 대부분의 사람들이 구경해보지 못했을것이다.
	private final ThreadLocal localSession = new ThreadLocal();
	protected Session getLocalSession() {
		Session session = localSession.get();
		if (session == null || session.isClosed()) {
			session = sessionProvider.createSession(this);
			localSession.set(session);
		}
		return session;
	}

이 놈이 하는 기능은 쓰레드 별로 로컬 변수를 할당하는 일을 한다. 즉, 같은 이름의 변수이지만, 쓰레드 별로 다른 값들을 가지고 있다는것이다. 여기서는 멀티 쓰레드 상에서 요청을 처리하기 위한 세션을 만들어 내기 위해 사용하였다.
 자신은 멀티 쓰레드상에서 프로그래밍을 안한다고 생각할지도 모르지만, 많은 사람들이 멀티 쓰레드 환경에서 작업을 하고 있다. 아무 생각 없이 짜도 되는 JSP도 멀티 쓰레드상에서 작동하니 말이다. (그러고 보면 servlet을 모르고 jsp를 이해하는 사람들은 보면 참 대단하다. ^^;)
 DAMO는 PersistenceManager의 openSession()을 이용해서 명시적으로 세션을 생성할 수도 있고, 명시적인 요청이 없을 경우 쓰레드별로 세션을 만들어서 사용을 한다. 그래서 TheadLocal을 이용해서 세션을 저장하는것이다.
주의해야할것은 TheadLocal을 쓰는 환경이 쓰레드 풀이라면, 기존 데이터를 삭제해주는것이 좋아다. 그렇지 않다면 재사용되는 쓰레드가 올바르지 않은 데이터를 참조할 수 있기 때문이다.

 기술적인 부분과는 상관없지만 한가지 얘기를 더하자면 autoXXXTransaction() 메소들이다. 소스를 보면 알겠지만, 세션에 트랙잭션이 할당되지 않았을 경우 자동으로 트랜잭션을 할당해주고, 처리해주는 역할을 하는것이다. 단순히 조회 쿼리 한번 실행하는데 세션 생성해라~ 트랜잭션 시작해라~ 트랜잭션 종료해라~ 이런 명령을 일일이 지정해주면 얼마나 번거롭겠는가..? ^^
 autoXXXTransaction() 메소들 덕분에 아래 두 코드는 결국 동일한 일을 하는것이다.
PersistenceManager pm = PersistenceManager.getPersistenceManager();
pm.beginTransaction();
try {
	Entity param = new Entity();
	param.setMonth(7);
	List rows = pm.getList(param);
} finally {
	pm.endTransaction();
}

PersistenceManager pm = PersistenceManager.getPersistenceManager();
Entity param = new Entity();
param.setMonth(7);
List rows = pm.getList(param);

 



DAMO v0.4의 모든 소스 코드는 파일로 첨부합니다. svn 주소를 공개하려고 했으나, 여러가지 개인 정보(?)들이 있어서, 소스만 첨부합니다. 아직 정리가 안되어서, 보기 힘들수도 있겠지만 참조하시기 바랍니다. (spring 연동 소스도 있는데, 그것까지 필요하신분 없겠죠? ^^;)  TODO
 (1) 설정 파일 부분 개선 필요
 (2) 동적 쿼리 최적회 필요
 (3) EL 사용자 function 가능하도록 구조 변경
 (4) foreach문을 위한 statement 별로 context 생성하는 구조 필요
 (5) 기타 등등

 혹시, 잘못된 점이나 개선할 점, 좋은 아이디어 있으신분은 언제든 연락바랍니다.. ^^