잊지 않겠습니다.

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



저번 과제를 진행하시면, 무언가 코드의 중복이 일어나는것 같은데... 하는 느낌을 가지실 수 있을겁니다. 

그 부분은 각 code의 get, add, delete, countAll, deleteAll code의 부분이 계속되는 코드 패턴의 반복으로 나타나게 됩니다. 이 부분에 대한 코드 수정을 한번 해보도록 하겠습니다. 

간단히 각 dao의 add method의 차이를 한번 알아보도록 하겠습니다. 

void add(User user);
void add(Book book);
void add(History history);

method의 형태가 매우 유사합니다. 세부 코드를 보시면 더 비슷합니다.

public void add(final User user) {
    executor.execute(new HibernateAction() {
         session.save(user);
    });
}

이 내부의 코드 중에서 변화하는 부분과 변화하지 않는 부분을 나누면, 객체의 Type 이외에는 변화하지 않는 것을 알수 있습니다. 이렇게 Type만이 다르고, 코드가 동일한 경우에는 Generic을 이용해서 중복 코드를 제거할 수 있습니다. 

이제 상속을 통해서 코드의 중복을 한번 제거해보도록 하겠습니다. 기존 interface의 중복이 될 수 있는 get, add, delete, countAll, deleteAll에 대한 interface를 GenericDao interface로 따로 구성하도록 합니다. 이제 다른 Interface들은 GenericDao interface를 상속받아 구성될 것입니다.

public interface GenericDao<T> {
    List<T> findAll();
    int countAll();
    T findById(final int id);
    void update(final T t);
    void add(final T t);
    void delete(final T t);
    void deleteAll();
}


그리고, 이 interface를 받는 GenericDaoImpl을 구성해보도록 하겠습니다. 이제 모든 DaoImpl은 GenericDaoImpl을 상속받아 사용할 예정입니다.

public abstract class GenericDaoImpl<T> implements GenericDao<T> {
    private final Class<T> type;
    protected final HibernateSqlExecutor executor;

    public GenericDaoImpl(Class<T> type, HibernateSqlExecutor executor) {
        this.type = type;
        this.executor = executor;
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<T> findAll() {
        return (List<T>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(type).list();
            }
        });
    }

    @Override
    public int countAll() {
        Long count = (Long) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(type).setProjection(Projections.rowCount()).uniqueResult();
            }
        });
        return count.intValue();
    }

보시면 생성자부분에 새로운 코드가 들어가게 됩니다. Class<T>가 바로 그것인데요. Class의 Type값을 생성자에 넣어서, Session의 대상이 되는 class를 지정하게 됩니다. 

이제 상속받은 BookDao interface와 BookDaoImpl을 살펴보도록 하겠습니다. 

public interface BookDao extends GenericDao<Book> {
    List<Book> search(String name);
}

public class BookDaoImpl extends GenericDaoImpl<Book> implements BookDao {

    public BookDaoImpl(HibernateSqlExecutor executor) {
        super(Book.class, executor);
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Book> search(final String name) {
        return (List<Book>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(Book.class)
                .add(Restrictions.like("name", name, MatchMode.ANYWHERE))
                .list();
            }
        });
    }
}

상당히 코드가 재미있게 변경이 됩니다. 

이렇게 변경할 수 있는 것은 객체만으로 통신할 수 있기 때문입니다. 따로 sql query문과 같은 다른 문자열을 다루는 것이 전혀 없고, 객체만으로 RDBMS와 통신할 수 있는 장점이 가장 보이는 코드이기도 하지요. 

한가지 더 이야기드린다면, 나중에 나올 Spring JPA Data의 경우에는 이 GenericDao interface를 극단적으로 발전시킨 경우가 만들어집니다. 

꼭 한번 이런식으로 코드를 변경시켜보시길 바랍니다. 그리고 덤으로 Generic에 대한 개념을 꼭 알아두시길 바랍니다. (C++의 경우에는 Template을 찾아보시면 됩니다.)


Spring + Hibernate

지금까지 작성되었던 Hibernate를 이용한 dao, service layer를 spring을 이용하도록 수정해보도록 하겠습니다. 

먼저 조금 소개를 한다면, Spring과 Hibernate는 원래 사이가 좋은 편은 아니였습니다.
예전 Spring 1.x와 Hibernate 2.x 간에는 개발자간에 매우 심각한 대립이 존재를 했었고, 그로 인한 키보드 배틀이 엄청나게 있었지요.
가장 큰 이유는 바로 전까지 제가 만들었던 HibernateSqlExecutor와 같은 HibernateTemplate을 Spring에서 제공하고 있었습니다. Hibernate는 DB에 접근하는 방법을 Session을 바로 얻어서 사용하도록 되어 있는데, 이러한 Raw level 접근을 Spring을 사용함으로서 제약을 걸게 되어 있었던 것이 사실입니다. 

그렇지만, Hibernate 3.x대로 넘어가고, Spring이 2.x대로 넘어가면서 화해(?)를 하게 됩니다. Spring 측에서 HibernateTemplate을 사용하지 않고, Spring에서 Hibernate를 사용할 수 있도록 Spring Framework의 큰 부분을 변경했습니다. 그리고 그에 맞추어 Hibernate에서는 SessionFactory에서 getCurrentSession() method를 추가함으로서, Spring의 기본적인 Transaction 방법에 맞추어 Hibernate를 이용한 DB 접근을 가능하게 하였습니다. 

둘간의 관계는 Spring 자체는 application framework입니다. Hibernate는 ORM framework고요. Hibernate 자체가 Spring보다 범위가 작은 Framework라고도 할 수 있습니다. Java의 모든 application은 Spring을 사용할 수 있지만, DB를 사용하지 않는 Application은 Hibernate를 사용하지 않을테니까요. 그럼에도 불구하고, Java의 최고 양대 open source framework는 spring과 hibernate입니다. 둘은 open source가 세상을 얼마나 바꾸어 놓을 수 있는지 보여주었고, 상업적으로도 엄청난 성공을 거뒀습니다. spring은 지금 vmware에서 제공되고 있고, hibernate는 weblogic을 제공하는 JBOSS에서 제공되고 있습니다. 

잡설이 길었습니다. spring은 이러한 hibernate를 위한 library를 따로 분리해서 사용하고 있습니다. pom.xml에 다음 jar가 추가되어야지 됩니다. 

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.2.0.RELEASE</version>
    </dependency>

Hibernate 까지 포함한 전체 jar의 구성은 다음과 같습니다. (BoneCP를 사용해서 ConnectionPool을 사용할 예정입니다.)

    <!-- Test 관련 jar -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>3.2.1.RELEASE</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.22</version>
    </dependency>
    <!-- Hibernate -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>4.1.9.Final</version>
    </dependency>

    <!-- Spring 관련 jar -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <!-- boneCP 관련 jar -->
    <dependency>
      <groupId>com.jolbox</groupId>
      <artifactId>bonecp</artifactId>
      <version>0.7.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>14.0</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.2</version>
    </dependency>

사용되는 jar들이 꽤나 많습니다. 이건 세발의 피입니다. ㅠㅠ 나중에 좀 사용하다보면 pom.xml 파일이 600 line이 넘어가는 경우가 허다하게 나옵니다. ㅠㅠ

이제 spring을 이용한 transaction 구성과 BookDao, UserDao, HistoryDao의 구현 객체를 수정하도록 하겠습니다. 이제 Transaction을 spring에서 관리할 예정이기 때문에 기존의 HibernateSqlExecutor를 제거할 예정입니다. 그리고 Transaction을 맺는것은 한개의 Session을 Dao Action에 계속해서 사용하는 형식이 됩니다. 
이 부분은 Hibernate에서 Transaction/Session에 대한 관리 부분과 연관이 됩니다. 기본적으로 Hibernate는 Session을 beginTransaction(), commit(), rollback()을 이용해서 Session의 변경사항을 db에 반영하도록 되어 있습니다. 그런데, Spring의 @Transactional을 이용하면 @Transactional의 시작시점에서 자동으로 session.beginTransaction()의 시작과, commit(), rollback()을 구성해주게 됩니다. 따라서 전체 코드는 다음과 같이 수정이 되게 됩니다. (GenericDaoImpl만 코드를 살펴보도록 하겠습니다.)

public abstract class GenericDaoImpl<T, K extends Serializable> implements GenericDao<T, K> {
    private final Class<T> type;
    protected SessionFactory sessionFactory;

    public GenericDaoImpl(Class<T> type) {
        this.type = type;
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<T> findAll() {
        Session session = sessionFactory.getCurrentSession();
        return session.createCriteria(type).list();
    }

    @Override
    public int countAll() {
        Session session = sessionFactory.getCurrentSession();
        return ((Long) session.createCriteria(type).setProjection(Projections.rowCount()).uniqueResult()).intValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public T findById(final K id) {
        Session session = sessionFactory.getCurrentSession();
        return (T) session.get(type, id);
    }

    @Override
    public void update(final T t) {
        Session session = sessionFactory.getCurrentSession();
        session.update(t);
    }

    @Override
    public void add(final T t) {
        Session session = sessionFactory.getCurrentSession();
        session.save(t);
    }

    @Override
    public void delete(final T t) {
        Session session = sessionFactory.getCurrentSession();
        session.delete(t);
    }

    @Override
    public void deleteAll() {
        Session session = sessionFactory.getCurrentSession();
        @SuppressWarnings("unchecked")
        List<T> result = (List<T>) session.createCriteria(type).list();
        for(T r : result) {
            session.delete(r);
        }
    }
}


Spring에서 SessionFactory를 @Autowired해서 사용할 예정입니다. 이제 applicationContext.xml을 작성하도록 하겠습니다. 

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">  
  <context:property-placeholder location="classpath:spring.properties" />
  <!--   Repository scan -->
  <context:component-scan base-package="com.xyzlast.bookstore.dao"/>
    
  <!-- BoneCP connection Pool DataSource 설정 -->
  <bean id="dataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
    <property name="driverClass" value="${connect.driver}" />
    <property name="jdbcUrl" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
    <property name="idleConnectionTestPeriodInMinutes" value="60" />
    <property name="idleMaxAgeInMinutes" value="240" />
    <property name="maxConnectionsPerPartition" value="30" />
    <property name="minConnectionsPerPartition" value="10" />
    <property name="partitionCount" value="3" />
    <property name="acquireIncrement" value="5" />
    <property name="statementsCacheSize" value="100" />
    <property name="releaseHelperThreads" value="3" />
  </bean>
  <!--   Spring에서 제공하는 Hibernate4용 SessionFactory 생성 -->
  <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:hibernate.cfg.xml"/>
    <!--     entity가 위치한 package를 scan -->
    <property name="packagesToScan" value="com.xyzlast.bookstore.entities"/>
  </bean>
  <!--   Hibernate4용 TransactionManager 설정 -->
  <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
  </bean>
  <!--   TransactionManager 등록 -->
  <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

@Configuration을 이용한 code base configuration은 다음과 같이 구성됩니다. 

@Configuration
@EnableTransactionManagement
@PropertySource("classpath:spring.properties")
@ComponentScan(basePackages = { "com.xyzlast.bookstore.dao", "com.xyzlast.bookstore.services", "com.xyzlast.bookstore.utils" })
public class HibernateBookStoreConfiguration {
    @Autowired
    private Environment env;

    @Autowired
    private SessionFactory sessionFactory;

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        PropertySourcesPlaceholderConfigurer configHolder = new PropertySourcesPlaceholderConfigurer();
        return configHolder;
    }

    @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 LocalSessionFactoryBean sessionFactory() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.dialect", env.getProperty("hibernate.dialect"));
        properties.setProperty("hibernate.show_sql", env.getProperty("hibernate.show_sql"));

        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
        sessionFactory.setDataSource(dataSource());
        sessionFactory.setHibernateProperties(properties);
        sessionFactory.setPackagesToScan("com.xyzlast.bookstore.entities");

        return sessionFactory;
    }

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

    // Hibernate를 이용하는 경우, 반드시 HibernateExceptionTranslator가 Bean에 등록되어야지 된다.
    @Bean
    public HibernateExceptionTranslator hibernateExceptionTranslator() {
        return new HibernateExceptionTranslator();
    }
}


하나 재미있는 코드 구성이 Configuration에서 나오게 됩니다. 먼저, sessionFactory입니다. SessionFactory를 반환하는 것이 아닌, LocalSessionFactoryBean을 반환하게 됩니다. 이는 Spring ApplicationContext에서 FactoryBean을 등록시켜, 그 FactoryBean을 통한 재 생성이 되게 됩니다. 마치 prototype scope로 SessionFactory를 생성해서 사용하는 것 처럼, SessionFactory를 사용할 때마다 새로 반환시키는 구성이 되게 됩니다. 마지막으로 HibernateExceptionTranslator 입니다. Hibernate에서 반환되는 Exception을 Spring JDBCTemplate과 유사한 Exception으로 변경시키는 Bean입니다. 이 Bean이 등록되지 않으면 Exception이 발생되게 됩니다. 

약간 설정이 어려운 감이 없지는 않습니다. 그렇지만 기본적으로 DataSource를 만들어주고, 만들어진 DataSource를 기반으로 SessionFactory를 등록한다. 라는 기본 개념은 JdbcTemplate을 사용할때와 별반 차이가 없습니다. 
그리고, Hibernate를 사용하는 경우에는 CUD에 대한 처리가 매우 단순해지는 코드적 장점을 가지고 있으며, 무엇보다 DB의 객체 자체를 Domain의 Model로 대응시켜서 사용가능하다는 큰 장점을 가지고 있습니다. 꼭 사용법을 익혀두시길 바랍니다. 


Hibernate Criteria Examples

Hibernate로 하는 query문은 Criteria라는 객체지향적 문법으로 만들어집니다. 이 문법의 장점은 다음과 같습니다. 

# DB에 종속적이지 않은 문법을 만들어냅니다. : Criteria로 만들어진 코드는 Dialect에 의해서 DB의 종류에 맞는 SQL로 변환되어 실행됩니다.
# BL 로직을 표현하기 쉽다. : SQL이라는 DB의 언어에서 객체지향적 java code로 만들어진 문법은 알아보기가 쉽습니다.

다음은 Criteria 문법으로 표현되는 SQL query 문에 대한 예시들입니다.

Entity Select query

select * from SM_WAITING where openDate = '20120510' and phone_id = '123456' and enteranced = 'N' and deleted = 'N' order by seq;

        List<Waiting> waitings = session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.openDate", yyyymmdd))
                .add(Restrictions.eq("phoneNumber", phoneNumber))
                .add(Restrictions.eq("entranced", false))
                .add(Restrictions.eq("deleted", false))
                .addOrder(Order.asc("id.sequence")).list();


Count Row

select count(*) from SM_WAITING where shopId = 'sm00000001' and openDate = '20120101' and seq > 3 and enteranced = 'N' and deleted = 'N';

        Long remainTeamCount = (Long) session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.shop", waiting.getId().getShop()))
                .add(Restrictions.eq("id.openDate", waiting.getId().getOpenDate()))
                .add(Restrictions.lt("id.sequence", waiting.getId().getSequence()))
                .add(Restrictions.eq("entranced", false))
                .add(Restrictions.eq("deleted", false))
                .setProjection(Projections.rowCount()).uniqueResult();


Get MAX/MIN/SUM value

select max(seq) from SM_WAITING where shopId = 'sm00000001' and openDate = '20120101'

        Integer maxSeq = (Integer) session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.shop", shop))
                .add(Restrictions.eq("id.openDate", yyyymmdd))
                .setProjection(Projections.max("id.sequence"))
                .uniqueResult();


Paging query (oracle 기준)

select * from (
         select rownum as rnum, name, address from Waiting where phonenumber = '1011234550'
    ) where rnum between (:PageNo * (:PageNo-1)) and ((:PageNo * (:PageNo-1)) + :PageSize )

        List<Waiting> waitings = session.createCriteria(Waiting.class)
                .add(Restrictions.eq("phoneNumber", phoneNumber))
                .setFirstResult(pageIndex * pageSize).setMaxResults(pageSize)
                .list();


SubQuery

SELECT *
  FROM PIZZA_ORDER
 WHERE EXISTS (SELECT 1
                 FROM PIZZA
                WHERE PIZZA.pizza_size_id = 1
                  AND PIZZA.pizza_order_id = PIZZA_ORDER.pizza_order_id)

Criteria criteria = Criteria.forClass(PizzaOrder.class,"pizzaOrder");
DetachedCriteria sizeCriteria = DetachedCriteria.forClass(Pizza.class,"pizza");
sizeCriteria.add("pizza_size_id",1);
sizeCriteria.add(Property.forName("pizza.pizza_order_id").eqProperty("pizzaOrder.pizza_order_id"));
criteria.add(Subqueries.exists(sizeCriteria.setProjection(Projections.property("pizza.id"))));
List<pizzaOrder> ordersWithOneSmallPizza = criteria.list();


JOIN

select * from smWait w, smShop s where s.shop_name = 'ykyoon'

        List<Waiting> waits = session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.shop", shop))
                .list();


몇몇 대표적인 query들을 뽑아봤습니다. where 조건은 add로 Restrictions로 처리하고, count와 같은 계산 절은 Projections 로 처리가 되는 것을 알 수 있습니다. 여러 query 들을 한번 만들어보시길 바랍니다. 많이 익숙해질 필요가 있습니다. 


Posted by Y2K
,