2013년 3월 15일 금요일

Spring 을 이용한 hibernate 환경설정

진부한 내용일 수 있지만, Spring 을 이용해 hibernate 에 대한 환경 설정에 대한 정보가 대부분 단편적이거나, 하나의 DB에 대해서만 설명한 글이 대부분이라...
hibernate 가 여러 DB를 동시에 만족 시킬 수 있음을 강조하고, 솔루션을 만들 때 여러 DB에 대해 만족할 수 있도록 테스트를 쉽게 하기 위해 간단하게나마 hibernate 설정을 spring 의 @Configuration을 이용하여 제작해 보았습니다.

우선 모든 DB에 대해 공통적으로 적용되는 부분에 대해 다음과 같이 정의했습니다.

package kr.debop4j.data.hibernate.springconfiguration;
import kr.debop4j.core.tools.StringTool;
import kr.debop4j.data.hibernate.forTesting.UnitOfWorkTestContextBase;
import kr.debop4j.data.hibernate.interceptor.MultiInterceptor;
import kr.debop4j.data.hibernate.interceptor.StatefulEntityInterceptor;
import kr.debop4j.data.hibernate.interceptor.UpdateTimestampedInterceptor;
import kr.debop4j.data.hibernate.repository.HibernateRepositoryFactory;
import kr.debop4j.data.hibernate.unitofwork.UnitOfWorkFactory;
import kr.debop4j.data.jdbc.JdbcTool;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.ConnectionReleaseMode;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean;
import org.springframework.orm.hibernate4.HibernateTransactionManager;
import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;
/**
* hibernate 의 환경설정을 spring framework의 bean 환경설정으로 구현했습니다.
* User: sunghyouk.bae@gmail.com
* Date: 13. 2. 21.
*/
@Slf4j
public abstract class HibernateConfigBase {
@Getter
@Setter
private UnitOfWorkTestContextBase testContext;
abstract protected String getDatabaseName();
abstract protected String[] getMappedPackageNames();
protected Properties hibernateProperties() {
Properties props = new Properties();
props.put(Environment.FORMAT_SQL, "true");
props.put(Environment.HBM2DDL_AUTO, "create"); // create | spawn | spawn-drop | update | validate
props.put(Environment.SHOW_SQL, "true");
props.put(Environment.RELEASE_CONNECTIONS, ConnectionReleaseMode.ON_CLOSE);
props.put(Environment.AUTOCOMMIT, "true");
props.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, "thread");
props.put(Environment.STATEMENT_BATCH_SIZE, "50");
return props;
}
protected DataSource buildDataSource(String driverClass, String url, String username, String password) {
return JdbcTool.getDataSource(driverClass, url, username, password);
}
protected DataSource buildEmbeddedDataSource() {
EmbeddedDatabaseFactoryBean bean = new EmbeddedDatabaseFactoryBean();
bean.afterPropertiesSet();
return bean.getObject();
}
@Bean(destroyMethod = "close")
abstract public DataSource dataSource();
/**
* factoryBean 에 추가 설정을 지정할 수 있습니다.
*/
protected void setupSessionFactory(LocalSessionFactoryBean factoryBean) { }
@Bean
public SessionFactory sessionFactory() {
if (log.isInfoEnabled())
log.info("SessionFactory Bean을 생성합니다...");
LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean();
String[] packageNames = getMappedPackageNames();
if (packageNames != null) {
log.info("hibernate용 entity를 scan합니다. packages=[{}]", StringTool.listToString(packageNames));
factoryBean.setPackagesToScan(packageNames);
}
factoryBean.setHibernateProperties(hibernateProperties());
factoryBean.setDataSource(dataSource());
factoryBean.setEntityInterceptor(hibernateInterceptor());
// Drived class에서 추가 작업을 수행할 수 있도록 합니다.
setupSessionFactory(factoryBean);
try {
factoryBean.afterPropertiesSet();
if (log.isInfoEnabled())
log.info("SessionFactory Bean을 생성했습니다!!!");
return factoryBean.getObject();
} catch (IOException e) {
throw new RuntimeException("SessionFactory 빌드에 실패했습니다.", e);
}
}
@Bean
public HibernateTransactionManager transactionManager() {
return new HibernateTransactionManager(sessionFactory());
}
@Bean
public MultiInterceptor hibernateInterceptor() {
MultiInterceptor interceptor = new MultiInterceptor();
interceptor.getInterceptors().add(statuefulEntityInterceptor());
interceptor.getInterceptors().add(updateTimestampedInterceptor());
return interceptor;
}
@Bean
public StatefulEntityInterceptor statuefulEntityInterceptor() {
return new StatefulEntityInterceptor();
}
@Bean
public UpdateTimestampedInterceptor updateTimestampedInterceptor() {
return new UpdateTimestampedInterceptor();
}
@Bean
public UnitOfWorkFactory unitOfWorkFactory() {
UnitOfWorkFactory factory = new UnitOfWorkFactory();
factory.setSessionFactory(sessionFactory());
return factory;
}
@Bean
public HibernateRepositoryFactory hibernateRepositoryFactory() {
return new HibernateRepositoryFactory();
}
}

public SessionFactory sessionFactory() {...} 함수가 가장 중요하고, 나머지는 뭐 별로...
당연히 hibernate 설정 클래스이니까... ㅎㅎ
sessionFactory를 만드는데, 여러가지 필요한 설정들을 지정하게 하는데, 미리 정의된 부분도 있고, 사용자가 더 필요한 부분은 "setupSessionFactory(factoryBean)" 위임 메소드를 이용하여, 추가로 정의 할 수 있도록 했습니다.

그럼 대표적으로 사용하는 DB인 HSQL, MySQL, PostgreSQL 에 대한 기본 환경설정 클래스를 보겠습니다.

1. HSql 메모리 DB 사용 (단순 테스트시 유용)
package kr.debop4j.data.hibernate.springconfiguration;
import kr.debop4j.data.hibernate.forTesting.DatabaseEngine;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
import java.util.Properties;
/**
* HSql 을 DB로 사용하는 Hibernate Configuration
* User: sunghyouk.bae@gmail.com
* Date: 13. 2. 21.
*/
public abstract class HSqlConfigBase extends HibernateConfigBase {
public DatabaseEngine getDatabaseEngine() {
return DatabaseEngine.HSql;
}
public String getDatabaseName() {
return "mem";
}
@Bean(destroyMethod = "close")
public DataSource dataSource() {
return buildDataSource("org.hsqldb.jdbcDriver",
"jdbc:hsqldb:" + getDatabaseName() + ":test",
"sa",
"");
}
protected Properties hibernateProperties() {
Properties props = super.hibernateProperties();
props.put(Environment.DIALECT, "org.hibernate.dialect.HSQLDialect");
return props;
}
}

2. MySQL
package kr.debop4j.data.hibernate.springconfiguration;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.Properties;
/**
* kr.debop4j.data.hibernate.springconfiguration.MySqlDbConfiguration
* User: sunghyouk.bae@gmail.com
* Date: 13. 2. 21.
*/
@Configuration
@EnableTransactionManagement
public abstract class MySqlConfigBase extends HibernateConfigBase {
@Override
public String getDatabaseName() {
return "hibernate";
}
@Bean(destroyMethod = "close")
public DataSource dataSource() {
return buildDataSource("com.mysql.jdbc.Driver",
"jdbc:mysql://localhost/" + getDatabaseName(),
"root",
"root");
}
@Override
@Bean
public Properties hibernateProperties() {
Properties props = super.hibernateProperties();
props.put(Environment.DIALECT, "org.hibernate.dialect.MySQL5InnoDBDialect");
return props;
}
}

3. PostgreSql
package kr.debop4j.data.hibernate.springconfiguration;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.Properties;
/**
* PostgreSQL DB를 사용하는 Hibernate 용 환경설정입니다.
* User: sunghyouk.bae@gmail.com
* Date: 13. 2. 21.
*/
@Configuration
@EnableTransactionManagement
public abstract class PostgreSqlConfigBase extends HibernateConfigBase {
@Override
public String getDatabaseName() {
return "hibernate";
}
@Bean(destroyMethod = "close")
public DataSource dataSource() {
return buildDataSource("org.postgresql.Driver",
"jdbc:postgresql://localhost/" + getDatabaseName() + "?Set=UTF8",
"root",
"root");
}
@Bean
public Properties hibernateProperties() {
Properties props = super.hibernateProperties();
props.put(Environment.DIALECT, "org.hibernate.dialect.PostgreSQL82Dialect");
return props;
}
}

4. PostgreSql with pgpool-II
package kr.debop4j.data.hibernate.springconfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
/**
* PostgreSQL 용 ConnectionPool과 Replication을 제공하는 PgPool 로 Connection을 만듭니다. (포트 9999를 사용합니다)
* User: sunghyouk.bae@gmail.com
* Date: 13. 2. 26.
*/
@Configuration
@EnableTransactionManagement
public abstract class PgPoolConfigBase extends PostgreSqlConfigBase {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
return buildDataSource("org.postgresql.Driver",
"jdbc:postgresql://localhost:9999/" + getDatabaseName() + "?Set=UTF8",
"root",
"root");
}
}

과 같다.

아주 사소한 port 번호 같은 것은 기본 값을 사용했다.
뭐 각각 다른 것은 DB명, ConnectionString, Dialect 등이다. 별 차이 없지만, 이렇게 구조적으로  상속체계를 해 놓으면, 실전에서 아주 편리하다.

실제 사용하는 예는 HSql 과 PostgreSql 의 예를 들어보자.

1. HSql 메모리 DB를 이용한 환경 설정 (2nd 캐시도 적용됨)
package kr.debop4j.access;
import kr.debop4j.access.model.organization.Company;
import kr.debop4j.access.model.product.Product;
import kr.debop4j.access.model.workcalendar.WorkCalendar;
import kr.debop4j.data.hibernate.springconfiguration.HSqlConfigBase;
import org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Properties;
/**
* HSql 메모리 DB를 사용하는 Hibernate 환경 설정입니다.
* User: sunghyouk.bae@gmail.com
* Date: 13. 3. 7.
*/
@Configuration
@EnableTransactionManagement
public class UsingHSqlConfiguration extends HSqlConfigBase {
@Override
protected String[] getMappedPackageNames() {
return new String[]{
Company.class.getPackage().getName(),
Product.class.getPackage().getName(),
WorkCalendar.class.getPackage().getName(),
};
}
@Override
public Properties hibernateProperties() {
Properties props = super.hibernateProperties();
props.put(Environment.USE_SECOND_LEVEL_CACHE, true);
props.put(Environment.USE_QUERY_CACHE, true);
props.put(Environment.CACHE_REGION_PREFIX, "debop4j-access");
props.put(Environment.CACHE_REGION_FACTORY, SingletonEhCacheRegionFactory.class.getName());
props.put(Environment.CACHE_PROVIDER_CONFIG, "classpath:ehcache.xml");
return props;
}
}


2. PostgreSql 의 "HAccess" DB 를 이용한 환경 설정 (2nd Cache도 적용됨)
package kr.debop4j.access;
import kr.debop4j.access.model.organization.Company;
import kr.debop4j.access.model.organization.CompanyCode;
import kr.debop4j.access.model.product.Product;
import kr.debop4j.access.model.workcalendar.WorkCalendar;
import kr.debop4j.data.hibernate.springconfiguration.PostgreSqlConfigBase;
import org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory;
import org.hibernate.cfg.Environment;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Properties;
/**
* PostgreSQL DB를 사용하는 Hibernate 환경설정입니다.
* User: sunghyouk.bae@gmail.com
* Date: 13. 3. 7.
*/
@Configuration
@EnableTransactionManagement
public class UsingPostgreSqlConfiguration extends PostgreSqlConfigBase {
@Override
public String getDatabaseName() {
return "HAccess";
}
@Override
protected String[] getMappedPackageNames() {
return new String[]{
CompanyCode.class.getPackage().getName(),
Company.class.getPackage().getName(),
Product.class.getPackage().getName(),
WorkCalendar.class.getPackage().getName(),
};
}
@Override
public Properties hibernateProperties() {
Properties props = super.hibernateProperties();
props.put(Environment.USE_SECOND_LEVEL_CACHE, true);
props.put(Environment.USE_QUERY_CACHE, true);
props.put(Environment.CACHE_REGION_FACTORY, SingletonEhCacheRegionFactory.class.getName());
props.put(Environment.CACHE_PROVIDER_CONFIG, "classpath:ehcache.xml");
return props;
}
}

이 것으로 끝나는게 아니고, Application 용 환경설정에서는 다음과 같이 위의 두 가지 환경설정 Class 를 입맛에 따라 Import 해서 테스트를 수행하면 된다.
package kr.debop4j.access;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* kr.debop4j.access.AppConfig
* User: sunghyouk.bae@gmail.com
* Date: 13. 3. 2.
*/
@Configuration
@EnableTransactionManagement
@ComponentScan({"kr.debop4j.access.repository", "kr.debop4j.access.service"})
@Import({UsingPostgreSqlConfiguration.class})
public class AppConfig {
// @ComponentScan 으로 @Repository, @Service 는 정의할 필요 없다.
}
view raw AppConfig.java hosted with ❤ by GitHub

정말 쉽죠?

이제 실제 단위 테스트 클래스에서 AppConfig를 지정해서 테스트를 수행하시면 됩니다^^

ORM의 장점 중에 여러 DB에 대해 일관된 작업이 가능하므로, 솔루션을 만들때는 최선의 선택이라 생각하는지라, 위와 같은 설정으로 개발을 수행합니다. (비록 한가지 DB만을 대상으로 하는 시스템이라도... 향후 어떻게 될지 모르고, 코드 재활용성도 생각하고^^)

댓글 없음: