잊지 않겠습니다.

Spring 3.1에서 강화된 @Configuration을 사용한 설정에서 재미있는 @EnableXX annotation을 이용한 @EnableOrm을 만들어보고자한다. 

먼저, 요구사항

1. 기본적으로 BoneCP를 이용
2. packagesToScan 을 통해서 entity가 위치한 package를 지정해줄 수 있어야지 된다.
3. Hibernate와 Hibernate를 이용한 JPA를 모두 지원하는 형태로 구성한다.
4. Ehcache를 사용할 수 있어야지 된다. 

기본적으로 구성되는 pom.xml의 구성은 다음과 같습니다. 

        <dependency>
            <groupId>com.jolbox</groupId>
            <artifactId>bonecp</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-core</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-sql</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>provided</scope>
        </dependency>

먼저, @EnableOrm interface는 다음과 같이 정의 될 수 있습니다. 

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(OrmConfigurationSelector.class)
public @interface EnableOrm {
    String[] packagesToScan() default {};

    boolean enableCache() default false;

    boolean showSql() default false;

    OrmType ormType() default OrmType.Hibernate;
}

public enum OrmType {
    Hibernate, Jpa;
}

packageToScan과 enableCache, 그리고 console 창에 sql query문을 출력해야지 되는 경우를 기본적으로 고려해줄 수 있습니다.  그리고 OrmType을 통해서 기본 Hibernate를 이용한 Orm과 Jpa를 이용하는 두가지를 모두 사용할 수 있도록 합니다.  OrmType에 따라 각각 Load되는 Configuration이 바뀌어야지 되기 때문에 Import class는 ImportSelector를 구현한 객체여야지 됩니니다. 

ImportSelector를 구현한 객체는 다음과 같이 구성됩니다. 

public class OrmConfigurationSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        Map<String, Object> metadata = importingClassMetadata.getAnnotationAttributes(EnableOrm.class.getName());
        OrmType ormType = (OrmType) metadata.get("ormType");
        if (ormType == OrmType.Hibernate) {
            return new String[] { HibernateConfiguration.class.getName() };
        } else {
            return new String[] { JpaConfiguration.class.getName() };
        }
    }
}

그리고, Hibernate를 이용할 때와 JPA를 이용할 때의 공통 코드가 존재하는 abstract 객체를 하나 만들어주는 것이 좋을 것 같습니다. 기본적으로 Hibernate JPA를 사용할 예정이기 때문에 공통적으로 DataSource와 Hibernate Property는 완벽하게 중복되는 코드가 만들어질테니까요. 
공통 Configuration객체인 OrmConfiguration객체의 기능은 다음과 같습니다.

1. BoneCP datasource 제공
2. Hibernate Property의 제공
3. enableCache, packateToScan, showSql 등 protected 변수의 제공

OrmConfiguration 객체는 다음과 같이 구성될 수 있습니다. 

public abstract class OrmConfiguration implements ImportAware {
    public static final String HIBERNATE_DIALECT = "hibernate.dialect";
    public static final String CONNECT_USERNAME = "connect.username";
    public static final String CONNECT_PASSWORD = "connect.password";
    public static final String CONNECT_DRIVER = "connect.driver";
    public static final String CONNECT_URL = "connect.url";

    public static final String HIBERNATE_SHOW_SQL = "hibernate.show_sql";
    public static final String ORG_HIBERNATE_CACHE_EHCACHE_EH_CACHE_REGION_FACTORY = "org.hibernate.cache.ehcache.EhCacheRegionFactory";
    public static final String HIBERNATE_CACHE_USE_QUERY_CACHE = "hibernate.cache.use_query_cache";
    public static final String HIBERNATE_CACHE_USE_SECOND_LEVEL_CACHE = "hibernate.cache.use_second_level_cache";
    public static final String HIBERNATE_CACHE_REGION_FACTORY_CLASS = "hibernate.cache.region.factory_class";

    @Autowired
    protected Environment env;

    protected boolean showSql;
    protected boolean enableCache;
    protected String[] packagesToScan;

    @Bean
    public DataSource dataSource() {
        BoneCPDataSource dataSource = new BoneCPDataSource();
        dataSource.setUsername(env.getProperty(CONNECT_USERNAME));
        dataSource.setPassword(env.getProperty(CONNECT_PASSWORD));
        dataSource.setDriverClass(env.getProperty(CONNECT_DRIVER));
        dataSource.setJdbcUrl(env.getProperty(CONNECT_URL));
        dataSource.setMaxConnectionsPerPartition(20);
        dataSource.setMinConnectionsPerPartition(3);
        return dataSource;
    }

    @Bean
    public HibernateExceptionTranslator hibernateExceptionTranslator() {
        return new HibernateExceptionTranslator();
    }

    @Bean
    public abstract PlatformTransactionManager transactionManager();

    protected Properties getHibernateProperties() {
        Properties properties = new Properties();
        properties.put(HIBERNATE_DIALECT, env.getProperty(HIBERNATE_DIALECT));
        if (enableCache) {
            properties.put(HIBERNATE_CACHE_REGION_FACTORY_CLASS, ORG_HIBERNATE_CACHE_EHCACHE_EH_CACHE_REGION_FACTORY);
            properties.put(HIBERNATE_CACHE_USE_SECOND_LEVEL_CACHE, true);
            properties.put(HIBERNATE_CACHE_USE_QUERY_CACHE, true);
        }
        return properties;
    }

    @Override
    public void setImportMetadata(AnnotationMetadata importMetadata) {
        if (env.getProperty(HIBERNATE_DIALECT) == null || env.getProperty(CONNECT_USERNAME) == null
                || env.getProperty(CONNECT_PASSWORD) == null || env.getProperty(CONNECT_DRIVER) == null
                || env.getProperty(CONNECT_URL) == null) {
            throw new IllegalArgumentException("properties is not completed! check properties (hibernate.dialect, "
                    + "connec.username, connect.password, connect.driver, connect.url)");
        }
        Map<String, Object> metaData = importMetadata.getAnnotationAttributes(EnableOrm.class.getName());
        enableCache = (boolean) metaData.get("enableCache");
        packagesToScan = (String[]) metaData.get("packagesToScan");
        showSql = (boolean) metaData.get("showSql");
    }
}

기본적인 Property들은 모두 Properties 파일에 정의되지 않으면 에러가 발생하도록 객체들을 구성하였습니다. 이제 HibernateConfiguration을 한번 구성해보도록 하겠습니다. 

@Configuration
@EnableTransactionManagement
public class HibernateConfiguration extends OrmConfiguration implements HibernateConfigurer {
    private static final String HIBERNATE_SHOW_SQL = "hibernate.show_sql";

    @Bean
    public LocalSessionFactoryBean sessionFactory() {
        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
        sessionFactory.setDataSource(dataSource());
        setLocalSessionFactoryBean(sessionFactory);
        return sessionFactory;
    }

    @Override
    @Bean
    public PlatformTransactionManager transactionManager() {
        HibernateTransactionManager transactionManager = new HibernateTransactionManager();
        transactionManager.setDataSource(dataSource());
        transactionManager.setSessionFactory(sessionFactory().getObject());
        return transactionManager;
    }

    @Override
    public void setLocalSessionFactoryBean(LocalSessionFactoryBean localSessionFactoryBean) {
        Properties properties = getHibernateProperties();
        if (showSql) {
            properties.put(HIBERNATE_SHOW_SQL, "true");
        }
        localSessionFactoryBean.setHibernateProperties(properties);
        localSessionFactoryBean.setPackagesToScan(packagesToScan);
    }
}


기본적으로 항시 사용되는 SessionFactory와 그에 대한 설정부분을 Load 시켜주고, PlatformTransactionManager를 return 시켜주는 매우 단순한 @Configuration class입니다. 

이제 JpaConfiguration입니다. 
@Configuration
@EnableTransactionManagementpublic class JpaConfiguration extends OrmConfiguration implements JpaConfigurer {
    @Override
    public void setEntityManagerFactoryBeanProperties(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
        entityManagerFactoryBean.setPackagesToScan(packagesToScan);
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
        setEntityManagerFactoryBeanProperties(entityManagerFactory);
        entityManagerFactory.setDataSource(dataSource());
        entityManagerFactory.setJpaVendorAdapter(hibernateJpaVendorAdapter());
        entityManagerFactory.setJpaProperties(getHibernateProperties());
        return entityManagerFactory;
    }

    @Bean
    public HibernateJpaVendorAdapter hibernateJpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(showSql);
        return hibernateJpaVendorAdapter;
    }

    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
        return new PersistenceExceptionTranslationPostProcessor();
    }

    @Override
    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setDataSource(dataSource());
        transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return transactionManager;
    }
}


둘의 코드는 거의 완전히 동일합니다. Hibernate를 이용할 것인가, 아니면 Jpa를 이용할 것인가에 대한 기본적인 차이만이 존재합니다. 
테스트 코드 구성은 다음과 같습니다. 

@Configuration
@EnableOrm(ormType = OrmType.Hibernate, enableCache = true, packagesToScan = "com.xyzlast.domain.configs", showSql = true)
@PropertySource(value = "classpath:spring.properties")
@ComponentScan("com.xyzlast.domain.repositories")
public class TestHibernateConfiguration {
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        PropertySourcesPlaceholderConfigurer configHolder = new PropertySourcesPlaceholderConfigurer();
        Properties properties = new Properties();
        properties.setProperty("org.jboss.logging.provier", "slf4j");
        configHolder.setProperties(properties);
        return configHolder;
    }
}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestHibernateConfiguration.class)
public class HibernateConfigurationTest {

    @Autowired
    private ApplicationContext applicationContext;

    @Before
    public void setUp() {
        assertThat(applicationContext, is(not(nullValue())));
    }

    @Test
    public void dataSource() {
        DataSource dataSource = applicationContext.getBean(DataSource.class);
        assertThat(dataSource, is(not(nullValue())));
    }

    @Test
    public void transactionManager() {
        PlatformTransactionManager transactionManager = applicationContext.getBean(PlatformTransactionManager.class);
        assertThat(transactionManager, is(not(nullValue())));
    }

    @Test
    public void sessionFactory() {
        SessionFactory sessionFactory = applicationContext.getBean(SessionFactory.class);
        assertThat(sessionFactory, is(not(nullValue())));
    }
}

이제 Hibernate와 JPA에 따른 각각의 configuration을 따로 구성하지 않아도 되는 멋진 코드가 만들어졌습니다.
회사에서 다른 팀원들이 사용할 수 있도록 jar를 만들어서 사내 nexus 서버에 올려야지 되겠습니다. ㅋㅋ




Posted by Y2K
,