잊지 않겠습니다.

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


queryDSL과 Spring Data JPA 모두 DAO에 대한 접근 방법을 제어하고, 코드양을 줄일 수 있는 좋은 방법입니다. 그렇지만, Spring Data JPA의 경우에는 규격대로 되어 있는 select 구문, update 구문 이외에는 사용할 수 없는 단점을 가지고 있습니다. 이러한 단점을 극복하는 좋은 방법은 Spring Data JPA에 queryDSL을 결합해서 사용하는 것입니다. 이렇게 되면, Spring Data JPA와 queryDSL의 모든 장점을 사용할 수 있습니다. 

queryDSL과 Spring Data JPA를 연동해서 사용하기 위해서는 maven repository에 다음과 같은 설정을 해야지 됩니다. 

    <repository>
      <id>spring-snapshot</id>
      <name>Spring Maven SNAPSHOT Repository</name>
      <url>http://repo.springsource.org/libs-snapshot</url>
    </repository>

그리고 버젼에 유의해서 Spring Data JPA와 queryDSL을 설정해줘야지 됩니다. queryDSL이 2.x에서 3.x대로 넘어가면서 객체의 이름이 많이 변경이 되었기 때문에 상호간의 호환에 문제가 있습니다. queryDSL 버젼은 3.1로, Spring Data JPA의 버젼은 1.4.0.BUILD.SNAPSHOT으로 설정해줍니다. 지금 queryDSL을 지원하는 Spring Data JPA의 경우에는 beta version이지만, 곧 정식버젼이 나온다고 하니 잠시 기다릴수 있을 것 같습니다.

다음은 설정될 maven pom 파일의 properties입니다. 

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
    <hibernate.version>4.1.10.Final</hibernate.version>
    <hibernate.validator.version>4.3.1.Final</hibernate.validator.version>
    <spring.version>3.2.2.RELEASE</spring.version>
    <querydsl.version>3.1.0</querydsl.version>
    <spring.jpa.version>1.4.0.BUILD-SNAPSHOT</spring.jpa.version>
  </properties>

이렇게 설정을 모두 마치고 코드를 한번 다시 살펴보도록 하겠습니다.

QueryDslPredicateExecutor

select 구문의 핵심입니다. QueryDslPredicateExecute는 JpaRespository interface와 같이 사용할 수 있습니다. interface는 다음과 같은 method를 포함합니다. 

public interface QueryDslPredicateExecutor<T> {
    T findOne(Predicate predicate);
    Iterable<T> findAll(Predicate predicate);
    Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
    Page<T> findAll(Predicate predicate, Pageable pageable);
    long count(Predicate predicate);
}


Predicate는 queryDSL의 query입니다. 이 interface를 상속하는 경우 BookRepository의 코드는 다음과 같이 구성될 수 있습니다. 

@Repositorypublic interface BookDao extends JpaRepository<Book, Integer>, QueryDslPredicateExecutor<Book> {

}

다른 코드는 전혀 필요없습니다. 이제 query를 한번 구성해보도록 하겠습니다. 

search에 대한 코드를 구성해보도록 하겠습니다. Spring Data JPA는 like 구문을 지원하지만, 모든 전/후에 대한 Like만 지원되기 때문에, 기존의 코드로는 구성이 불가능했습니다. 
다음과 같은 코드로 구성이 가능합니다. 

    @Test
    public void testProdicate() {
        String bookName = "책이름";

        QBook qBook = QBook.book;
        Predicate predicate = qBook.name.like(bookName + "%").and(qBook.status.eq(BookStatus.CanRent));
        Iterable<Book> books = bookDao.findAll(predicate);

        for(Book book : books) {
            assertThat(book.getName().contains(bookName), is(true));
        }
    }

Type에 safe 하고, 간단한 query 구문으로 code를 구성하는 것이 가능합니다. findAll method가 queryDSL의 Predicate를 지원하기 때문에 이러한 코드를 구성할 수 있습니다.
findAll을 이용하는 경우, 이제 select에 대한 이슈는 거의 해소가 가능합니다. 그래도 아직 문제가 조금 더 남아있습니다. 그 부분은 바로 각 항목에 대한 max, min 값을 구하는 Predicate query와 다양한 update,delete를 하는 query들을 만들어주는 것이 아직은 불가능합니다. 이러한 문제를 해결하고, queryDSL의 모든 기능을 지원하기 위해서는 QueryDslRepositorySupport를 사용해야지 됩니다. 

QueryDslRepositorySupport

QueryDslPredicateExecutor의 경우에는 select를 이용해서 객체를 얻어올 때 주로 사용됩니다. 그렇지만, 이에 대한 다른 접근이 필요합니다. 객체에 대한 update, delete의 경우에는 좀더 다양하게 처리하는 것이 필요합니다. 

예를 들어 다음 query를 처리할 때, hibernate의 경우에는 HQL 또는 Native query를 이용해서 처리하는 방법밖에는 없습니다. 

update users set name = 'ykyoon' where name='abc';

delete from users where name='abc';

이러한 일괄 변경 및 업데이트, 또는 특정 값의 sum, min, max, average 값을 도출하기 위해서 사용하는 것이 QueryDslRepositorySupport입니다. QueryDslRepositorySupport의 code를 한번 알아보도록 하겠습니다. 

@Repository
public abstract class QueryDslRepositorySupport {

    private final PathBuilder<?> builder;

    private EntityManager entityManager;
    private Querydsl querydsl;

    public QueryDslRepositorySupport(Class<?> domainClass) {
        Assert.notNull(domainClass);
        this.builder = new PathBuilderFactory().create(domainClass);
    }

    @PersistenceContext
    public void setEntityManager(EntityManager entityManager) {

        Assert.notNull(entityManager);
        this.querydsl = new Querydsl(entityManager, builder);
        this.entityManager = entityManager;
    }

    @PostConstruct
    public void validate() {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        Assert.notNull(querydsl, "Querydsl must not be null!");
    }

    protected EntityManager getEntityManager() {
        return entityManager;
    }

    protected JPQLQuery from(EntityPath<?>... paths) {
        return querydsl.createQuery(paths);
    }

    protected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) {
        return new JPADeleteClause(entityManager, path);
    }

    protected UpdateClause<JPAUpdateClause> update(EntityPath<?> path) {
        return new JPAUpdateClause(entityManager, path);
    }

    @SuppressWarnings("unchecked")
    protected <T> PathBuilder<T> getBuilder() {
        return (PathBuilder<T>) builder;
    }

    protected Querydsl getQuerydsl() {
        return this.querydsl;
    }
}

abstract class 이기 때문에, 상속을 받아서 구현해야지 됩니다. 먼저, User Id 값중 max 값을 얻어오는 code를 한번 알아보도록 하겠습니다.

    public int sumUserIds() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.id.max());
    }


다음은 Update입니다. 

    public Long updateNameByName() {
        QUser qUser = QUser.user;
        update(qUser).where(qUser.name.eq("abcde")).set(qUser.name, "가나다").execute();
        return 0L;
    }


마지막으로 Delete입니다. 

    public void deleteByName(String name) {
        QUser qUser = QUser.user;
        delete(qUser).where(qUser.name.eq(name)).execute();
    }

매우 간단하게 구현하는 것이 가능합니다. 우리가 지금까지 만든 JpaRepository와 QueryDslPredictExecutor와 같이 사용하는 경우에는 거의 모든 query들을 처리하는 것이 가능하게 됩니다. 전체 코드는 다음과 같습니다. 

@Repository
public class CalDao extends QueryDslRepositorySupport {
    public CalDao() {
        super(User.class);
    }

    public int sumUserIds() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.id.max());
    }

    public Long updateNameByName() {
        QUser qUser = QUser.user;
        update(qUser).where(qUser.name.eq("abcde")).set(qUser.name, "가나다").execute();
        return 0L;
    }

    public void deleteByName(String name) {
        QUser qUser = QUser.user;
        delete(qUser).where(qUser.name.eq(name)).execute();
    }
}

queryDSL + Spring Data JPA에서의 Hibernate 이용

사용하다보면 Hibernate의 기능을 사용해야지 될 때가 발생할 수 있습니다. 이는 기존의 coding이 Hibernate 기준으로 된 경우도 해당될 수 있을 것이고, Hibernate의 다양한 Criteria를 이용해보고 싶은 생각도 생길 수 있습니다. 
먼저, 계속해서 이야기드리는 것은 JPA라는 규격자체가 Hibernate라는 구현체 위에서 동작하는 것이기 때문에 Hibernate의 Session을 얻어내는것도 가능합니다. 다음과 같은 코드로 Session을 얻어서 Hibernate와 동일하게 처리하는 것도 가능합니다. 

    public Session getSession() {
        return (Session) getEntityManager().getDelegate();
    }


Summary

지금까지 queryDSL과 Spring Data JPA를 이용한 Model 구성에 대해서 알아봤습니다. 약 2개월간 계속해서 장/단을 뽑아보면서 제일 우리에게 좋은 Model Framework조합이 무엇인가를 고민했던 결과입니다. 이러한 개발 방법은 다음과 같은 장점을 가지고 있습니다. 

1. type-safe 한 query를 만들 수 있습니다.
2. 객체지향적인 코드 구성이 가능합니다.
3. Hibernate Criteria의 단점인 일괄 update / delete 문의 처리가 가능합니다.
4. Repository의 코딩양을 줄일 수 있습니다.
5. eclipse의 intellisense의 지원을 받을 수 있습니다.

최종적으로 구성되는 package입니다. 이 구성으로 대부분의 Model에 관련된 Project가 구성이 될 예정입니다. 




# config : ApplicationConfiguration class가 위치할 package입니다.
# entities : entity 객체들이 위치할 package입니다.
# repositories : JpaRepository, QueryDslPredictExecutor를 상속받은 Repository interface가 위치할 package입니다.
# repositories.support : QueryDslRepositorySupport를 상속받은 객체 또는 repositories에서 사용할 Predict를 지원하는 객체들이 위치할 package입니다.
# services : BL에 관련된 서비스가 위치할 package입니다.
# utils : 데이터의 변환 등 다양한 경우에 사용되는 utility class가 위치하는 package입니다.

여기서 지금까지 이야기하던 Dao와 Repository에 대한 정의를 다시 할 필요성이 있습니다. 

이 둘에 대한 정의는 다음과 같습니다. 

"DAO는 데이터베이스에서 값을 꺼내와 도메인 오브젝트로 반환해주거나 적절한 값으로 반환해주는 계층을 일컫는다. Repository는 한 도메인 오브젝트에 대해서 객체의 값을 보증해주기 위해 도메인 내부에서 데이터베이스와 소통하는 객체을 일컫는다."

말이 어렵습니다. 조금 더 단순하게 말하면....

# DAO는 DB과 연결되어, Domain Object(=Entity)로 변환하는 것을 의미합니다.
# Repository는 DB와 연결되어, Domain Object(=Entity)로 변환합니다. 단, Domain 내부에서만 사용됩니다.

뒤의 조건이 하나가 더 붙어있으면 Repository이고, DB에 접속하는 객체를 어느곳에서나 사용가능해야지 되면 DAO입니다. Repository pattern이란, DB에 접근하는 영역을 Domain Layer, 즉 Model 영역에서만 사용하게 되는 것을 의미하고, 이것은 n-tier system에 적합한 책임영역의 분리가 되는 코드를 의미합니다. 
만약에 Controller Layer에서 DB에 접근하게 되고, 사용하게 된다면, 그것은 더 이상 Repository가 될 수 없습니다. 그렇지만, Model/Domain Layer에서만 접근하게 된다면 이것은 DAO가 아닌 Repository가 되게 됩니다. 너무나 유사한 개념이지만, 영역을 나누는 의미에서 사용하는 용어의 차이라고 생각하시면 될 것 같습니다. 
우리가 개발하는 것은 DAO가 아닌 Repository가 되게 됩니다. (이것은 저도 잘못 개발하고 있던 내용중 하나입니다.) 이 둘간의 영역 차이는 매우 자주 나오는 문제입니다. 만약에 서비스가 아닌 다른 영역(BL이 아닌 다른 영역)에서 DB에 접근하게 되는 객체를 만든다면.. 이것은 DAO를 만들어주게 되는 것이 맞습니다. 
이건 pattern입니다. 무엇이 옳고 그른 문제는 아닙니다. Layer에 의한 명백한 의미를 나누기 위해서 Repository로 사용하는 것이 좀 더 나을 것 같습니다. 

정리입니다. 
queryDSL + Spring Data JPA를 이용한 Domain Layer의 개발을 주로 하게 될 것입니다. 이 부분은 지금 Open Source 측에서도 밀고 있는 추세이기도 하고, 계속해서 발전이 되어가고 있는 분야이기도 합니다. 
이제 다음시간에는 만약에 우리가 외부 SI를 나가게 된다면, 주로 사용하고 있는 myBatis(iBatis)에 대해서 간략하게 알아보도록 하겠습니다. 


Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 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
,

10. Hibernate

Java 2013. 9. 9. 11:13

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


 Hibernate는 지금까지 SQL을 이용한 DB query 방법이 아닌, 객체를 이용한 SQL auto generate를 통한 DB 접속 방법을 제공합니다. 이런 객체를 이용한 방법을 ORM(object relation model)이라고 합니다. 먼저, ORM에 대해서 간단한 소개를 해보도록 하겠습니다. 

ORM

java 언어의 발전은 sw 개발에 있어서 객체지향의 개발 방법으로 오는 혜택에 매료가 되었습니다. 그렇지만, 모든 웹 및 기업의 데이터가 저장되어 있는 RDBMS와 OOP간의 괴리의 차이에 의한 비용의 증가가 계속해서 발생하게 되었습니다. OOP적 개발 방법과 RDBMS의 관계형 개발 방법론이 끊임없이 충돌하게 되는 것이지요. 서로간에 전쟁(war) 라는 표현을 사용할 정도로 논란이 매우 큰 문제입니다. java 측에서는 relation 기술의 탓으로 돌리고, data 전문가들은 OOP 기술의 문제라고 약간은 소모적인 논쟁으로 계속해서 가게 되었지요.

이때, ORM(Object Relation Model)은 이러한 불일치 기술에 대한 solution을 지칭할 때 사용됩니다. 

ORM은 4가지로 구성이 되어 있습니다.

1. Persistence class에 대한 기본적인 CRUD를 수행하는 API
2. class 자체나 class의 property를 참조하는 query를 작성할 수 있는 API
3. mapping meta data 작성을 위한 기반 API
4. ORM 구성이 Transaction과 상호 동작하며 최적화를 수행하도록 돕는 API


왜 ORM을 사용해야지 되는가?

1. 생산성 : SQL관련 코드를 제거하고 객체간의 관계를 명확하게 그릴수 있습니다. 그리고 Domain에 집중할 수 있는 설계 구조를 가지고 옵니다.
2. 유지보수성 : Domain을 구현했기 때문에 Domain에 대한 명확한 정의가 나타납니다. Domain의 BL이 변경, 추가 되었을 때 그에 대한 수정이 쉽습니다.
3. 성능 : 가장 큰 이슈입니다. VM을 설명할 때, VM보다 일반 assembly로 compile되는 언어가 더 빠르지만 이제 사용되지 않는 이유랑 동일합니다. ORM 자체에 이미 많은 최적화 방법들이 구현되어 있고, 그 현인들의 지식을 사용할 수 있다는 점에서 더욱더 큰 장점을 가지고 올 수 있습니다.
4. 밴더 독립성 : ORM은 DB와 DB의 방언(각 DB만의 함수들)로부터 추상화 되어 있습니다. DB의 독립성은 매우 큰 장점을 가지고 오는데, 개발에 있어서 오히려 더 나은 장점을 가지고 옵니다. 개발자들은 자신의 개발 PC에 가벼운 DB(mysql)를 설치해서 개발을 하고, 실 서버에서는 Data에 최적화된 DB를 이용해서 서비스를 할 수 있는 장점을 가지고 있습니다. 이는 생산성과도 직결되는 문제입니다.
5. Java 표준 : ORM은 JSR 220에 의해서 java에 표준적인 기술로 인정을 받았습니다.


Domain Driven Design

에릭 에반스 (Eric Evans) 는 과거의 지혜와 경험들을 종합하여 도메인-드리븐 디자인 (Domain Driven Design) 이라는 방법론을 제시 했습니다.
단순 객체지향 세계에서 살던 개발자들은  이 굉장하지만 새로운 개념에 어려움을 느껴 발표된지 몇년이 지난 후에야 관심을 가지게 되었죠.
DDD가 도대체 뭔데? 어떻게 해서든지 돌아가기만 하면 되는거 아냐! 하면서 무심히 지나쳤던 것들에 대해 체계적으로 설명하는 방법론이죠. 하지만 여전히 DDD는 어려운 것, 그저 한때 유행하는 버즈 워드로 인식되고 있는 경향이 있습니다. DDD가 무엇인지 처음 듣는 개발자도 많을 것입니다. DDD의 전체적인 철학을 쉽게 요약하고 있는 블로그 포스트 DDD: How to tackle complexity  번역으로 DDD 카테고리를 시작합니다. 


DDD (Domain Driven Design) 에서는 어플리케이션 도메인을 표현하기 위한 오브젝트 모델을 만듭니다.
이 모델은 도메인의 모든 관계와 로직을 담고 있습니다. 이렇게 하는 목적은 도메인의 복잡성을 관리하기 위함 입니다. DDD 에는 매우 많은 개념과 패턴이 투입되어 있으나, 정제된 두개의 큰 그림으로 그 복잡성에 태클을 걸 수 있습니다.

1. 도메인의 개념을 명확하게 표현합니다.
2. 더욱 심도 있는 통찰을 위해 지속적인 리팩토링을 수행합니다.

복잡성이란 자체의 복잡한 정도를 의미합니다. 복잡한 것은 이해하기 어렵습니다. 이해하기 어려우면 금방 알아 들을 수 없습니다.

이것이 실제 이슈 입니다 : 복잡한 소프트웨어는 이해하기 어렵습니다. 이것이 바로 모든 사람이 업데이트 하기를 두려워 하여 아예 처음부터 다시 만드는 이유입니다. 아마 첫번째나 두번째는 해킹 하듯이 코드를 추가해서 원하는 바를 이룰 수 있을지 모르지만, 각각의 해킹은 더 이상 시도하는 것이 의미가 없을 때까지 복잡성과 추잡함을 증가시킵니다. 이것을 다른 말로 실패 라고 합니다.

그래서 우리는 이 복잡성을 극복해야 합니다. DDD의 첫번째 방법은 객체지향, 모델 과 추상화의 장점을 얻는 것 입니다. 하지만 이건 매우 광범위 하죠. 우리는 이 오브젝트와 모델들을 어떻게 구조화 해야 하는지 알아내야 합니다. 이것이 DDD가 도메인의 개념을 명시적으로 표현하자는 아이디어의 입니다.

아이디어는 간단합니다. 여러분의 도메인에 새로이 적용되는 개념이 있다면 모델에서 확인할 수 있어야 합니다. 중요한 개념을 확인하기 위해서 코드를 뒤져서는 안됩니다. 그 개념은 모델에서 오브젝트로써 표현되어야 합니다. 특정 조건에서만 발생하는 액션이 있다고 합시다, 이 조건들이 중요하지 않다면 그 액션을 수행하도록 그저 IF Statement 메소드로 처리하면 됩니다. 하지만, 그 조건들이 도메인에서 중요하다면 코드로 부터 감추는 것만으로는 부족합니다. 그 조건들을 수행하도록 Policy Object가 조건들을 표현해야 합니다. 이제 조건들은 당신의 도메인에서 명시적으로 표현됩니다.

이 아이디어 들은 Factories, Repositories, Services, Knowledge Levels 등등으로 표현될 수 있습니다. 이 것은 여러분의 시스템을 이해 가능하도록 만드는 중요한 부분입니다.

DDD가 작동하도록 만드는 두번째 아이디어는 "Deeper Insight"를 위한 지속적인 리팩토링 입니다. Deeper Insight 란 이미 가지고 있는 도메인 모델에서 새로운 어떤 것을 발견하게 된다면 대충 끼워넣지 말고 도메인에서 중요한 요소인지 반드시 알아내라는 것을 의미합니다. 만약 중요하다면 새로 이해한 것이 명확하게 표현 되도록 모델을 리팩토링 해야 합니다. 이 리팩토링은 사소할 때도 있고, 매우 중요 할 때도 있습니다.

도메인 모델이 표현성을 잃게 되면 점점 더 부서지고, 점점더 복잡해 지며 점점 더 어려워 집니다. 여러분의 모델이 단순하며  표현력과 정확성을 유지할 수 있도록 항상 싸워야 합니다. 당신이 운이 좋다면 에릭 에반스가 말하는 Break Through [전에는 불가능 했던 것이 새로운 가능성과 통찰력이 갑자기 나타나는 일] 것을 경험할  수도 있습니다. 그렇게 된다면 진짜 운이 좋은 것입니다. 당신이 운이 좋지 않더라도 리팩토링은 적어도 모델이 유연성을 요구할때 유연함을 만족 시킬 수는 있습니다. 이것은 미래에 나타날 통찰력과 리팩토링 요소를 더 쉽게 핸들링 할 수 있음을 의미합니다.


위 내용과 같이, DDD는 우리가 표현하고자 하는 실세계의 데이터 프로세스 자체를 컴퓨터 언어로 옮겨가는 과정입니다. 

예를 들어, SQL로 작업을 하게 되면 다음과 같은 대화가 나오게 됩니다. 

"TB_USER"에서 SELECT를 할때, POINT Column으로 Order By DESC로 얻어오고, 그 POINT값이 100점 이상인 경우에는 LEVEL column값을 2로 업데이트를 시키면 됩니다.

자, 방금 말한 내용을 실제 BL을 설계한 기획자에게 이야기를 해보도록 합시다. 과연 어떤 말인지 알아들을수 있을까요? SQL 개발자들이라면 가능하겠지요. DDD로 모델링을 거치면 다음과 같은 대화를 할 수 있습니다.

User를 표로 보여줄 때, Book Point값이 높은 순서대로 보여주고, Book Point값이 100점이상인 경우에는 Reader로 사용자 Level을 높여줘서 보여주면 됩니다.


자, 이제 좀더 말이 쉬워졌습니다. 이 말을 우리가 객체 지향 언어로 표현하면 어떻게 될까요? 

List<User> users = userDao.getAll();
users.sort("Book Point");
for(User user : users) {
    if(user.getBookPoint() >= 100) {
        user.setStatus("Reader");
    }
}
return users;

SQL query로 표현하면 다음과 같이 표현할 수 있습니다. 

UPDATE TB_USER SET LEVEL = 2 WHERE POINT >= 100;
SELECT * FROM TB_USER ORDER BY POINT DESC;


데이터 중심으로 보는 것과 모델링을 중심으로 보는것. 이 두가지의 차이는 그 데이터의 구조를 모르는 사람이 로직을 읽을수 있느냐, 없느냐에 따라 갈리게 됩니다. 그리고 우리가 문장으로 설명할 수 있는 BL을 code에 어떻게 녹여내느냐를 고민을 해야지 됩니다. 

최종적으로 DDD는 다음 목표들을 갖습니다.

Ubiquitous Language
Domain 중심의 SW팀에서는 모든 참가자들(사용자, 도메인전문가, 설계자, 프로그래머, 분석가)간에 동일한 의미를 갖는 공통된 언어를 갖는다. 심지어 공통된 언어는 Code의 Object로 구현이 가능해야지 된다.

Layered Architecture pattern
UI와 Model이 결합되어 있으면, UI가 바뀌는데에 따라 Model이 변경되어야지 됩니다. 따라서 UI, BL, Modeling간에 분리가 가능한 Layer들이 만들어져야지 됩니다. 기준에 따라 Layer를 나누고, 역할을 부여하는 작업이 필요합니다. 이에 대한 장점은 다음과 같습니다.

1. Layer들이 재사용될 수 있어야지 됩니다.
2. 표준을 지원한다.
3. 종속성을 국지적으로 최소화한다.
4. 교환 가능성이 확보된다.
5. 동작이 변경된 경우, 단계별 재작업이 필요하다.

기본적으로 Domain Model은 다음 4개의 Layer로 구분시켜서 사용하게 됩니다. (어디서 많이 본 내용입니다. ㅋ)




# Infrastructure Layer : 상위 계층을 지원하는 일반화된 기술적 기능을 제공. 공통 Library, Engine, Framework 영역
# Domain Layer : 업무 개념과 업무 상황에 대한 정보. 업무 규칙을 표현하는 Layer.
# Application Layer : 작업을 정의하고 조정하는 영역. Domain 객체로 작업을 위임하는 역활을 담당.
# UI Layer : 정보를 노출하고 입력을 받아들이는 영역

이러한 구조는 결국은 Domain의 격리, 즉 분리가 이루어지게 됩니다.  이러한 Layer architecture중 가장 대표적인 방식이 MVC 입니다. 



Model 은 기본 기능을, View는 User Interface를 의미하게됩니다. Controller는 M과 V간의 직접적인 연결을 막음으로서 독립성을 유지하는 역활을 하게 됩니다. 추후에 Spring @MVC에서 보다 더 설명이 될 영역이기도합니다.  

Smart UI anti pattern
모든 업무로직을 사용자 인터페이스에 넣는 설계 방법을 Smart UI pattern이라고 합니다. 

특징은 
- application을 작은 기능으로 잘게 나누고,
- 나뉜 기능을 분리된 UI로 구현. 업무 규칙이 분리된 UI에 들어가게 합니다.
- 분리된 업무 규칙은 RDBMS를 이용해서 데이터를 공유하고 실행합니다. 주로 SP가 이 용도로 사용됩니다.
- 주로 자동화된 UI 구축 도구와 시각적인 프로그래밍 도구를 이용합니다. 

DDD pattern에서 가장 피해야지 되는 것이 바로 UI Pattern입니다.
UI pattern이라는 것은 지금도 매우 자주 쓰이고 있는 패턴입니다. 이 패턴의 가장 큰 문제는 우리가 사용하는 데이터 및 Domain에 대한 모든 로직을 보여지는 View에 맞추게 됩니다. 실질적인 데이터의 흐름을 방해하고, 언제나 바뀔 수 있는 View 영역에 Model이 영속되기 때문에 application의 변경에 취약한 약점을 갖게 됩니다. 

대표적인 Smart UI pattern의 도구가 PowerBuilder입니다. Data Window의 Smart UI를 이용하기 위해, UI에 따른 Model과 로직이 꼬인 상태로 존재하게 됩니다. 이는 필연적으로 코드의 중복을 가지고 오게 되며, 아직까지 UI에 대한 명확한 테스트를 할 수 없는 현 시점에서 유지보수가 거의 불가능한 시스템을 만들게 됩니다. *anti pattern은 쓰지 말라고 있는겁니다.*

Entities pattern
ID를 갖는 unique한 object를 갖는 pattern입니다. 이는 RDBMS의 PK와 연결시켜, 이 객체가 유일한 어떤 값임을 나타내는 방법을 제공합니다.

Service pattern
서비스는 기능을 처리하거나, Entity로 구별할 수 없는 것을 지칭합니다. 가장 주로 사용되는 것은 BL의 Group을 표현하는 것이 가장 많습니다.

Factory pattern
각각의 Object, Service의 생성방법에 대한 통일성을 갖는것을 목표로 합니다. 어떤 객체를 생성할 때, 초기화 해줘야지 되는 값이라던지 실행시켜줘야지 되는 method가 독특하게 존재한다면 개발자 및 참여자들은 Domain에 집중할 수 없습니다. Spring의 ApplicationContext는 이런 경우 가장 좋은 해결 방법이 됩니다.

Repository pattern
생성된 객체나 Model이 외부(주로 DB)와 연동되어 생명주기를 가질때, 그 생명주기에 대한 관리 Focus가 되는 Layer가 존재해야지 됩니다. dao로 구현되는 것이 일반적이며, Spring에서는 @Repository로 지정하는 것이 일반적입니다.

DDD의 사상을 반영하기 위해 주로 사용되는 툴이 ORM이고, java에서 가장 오랜 시간동안 개발이 되고 사랑받고 있는 Hibernate가 그 선두 주자라고 할 수 있습니다.


Hibernate를 이용한 Dao의 개발

Hibernate를 이용한 Dao를 개발해보도록 하겠습니다. 먼저, 새로운 maven 프로젝트를 하나 만들어주세요. 그리고, 다음 jar들을 추가하도록 합니다.

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>4.1.9.Final</version>
    </dependency>
    <dependency>
      <groupId>c3p0</groupId>
      <artifactId>c3p0</artifactId>
      <version>0.9.1.2</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.22</version>
    </dependency>

그리고 entities와 dao, util package를 구성해주고, 기존의 dao의 interface와 entity를 모두 구성해주세요. file을 copy 한 후, package 명을 변경해도 좋습니다. 최종적으로 모든 객체가 다 완성되면 다음과 같은 형태가 될 것입니다. 


먼저, ORM 을 사용하기 위해서는 Hibernate가 정확히 어떻게 객체들과 DB간에 연결되어있는지 정의하는 방법이 필요합니다. Hibernate는 2개의 방법을 제공하고 있는데, hbm.xml 파일을 이용한 xml mapping과 annotation을 이용한 mapping을 제공하고 있습니다. 


지금까지 우리가 만든 객체들의 관계는 어떻게 될까요?

먼저, Book은 대여한 User와의 관계가 있습니다. 이 관계는 N:0의 관계를 갖습니다.  또한 여러개의 History를 가질 수 있습니다.
그리고 History는 관계된 User와 Book간의 모든 관계를 갖습니다. 
마지막으로 User는 어떤 관계를 가질까요? Table상에서는 관계가 나타나지 않습니다. 그렇지만, 하나의 User는 여러개의 Book을 대여할 수 있고, 여러개의 History를 가질 수 있습니다.  이를 반영할 수 있는 객체로 표현해보면 다음과 같은 객체를 선언할 수 있습니다.

public class Book {
    private int id;
    private String name;
    private String author;
    private BookStatus status;
    private Date publishDate;
    private User rentUser;
    private String comment;
    private List<History> histories;
}

public class History {
    private int id;
    private User user;
    private Book book;
    private ActionType action;
    private Date insertDate;
}

public class User {
    private int id;
    private String name;
    private String password;
    private UserLevel level;
    private int point;
    private List<History> histories;
}

각 객체들로 서로간에 동작을 이해할 수 있는 코드들이 나오게 됩니다. 이제 이 객체를 DB와 연결을 시켜야지 됩니다. DB와 연결을 시키는 방법은 전통적인 xml을 사용하는 방법과 @annotation을 이용한 방법 두가지로 나눌 수 있습니다.  기본적으로 이제 java 표준이 된 @annotation을 이용한 ORM mapping을 중심으로 알아보도록 하겠습니다. xml을 이용한 mapping 방법은 각자 숙제로 남겨두도록 하겠습니다. 

Hibernate를 이용하기 위해서는 먼저 DB의 접근을 제공해야지 됩니다. 이는 SessionFactory를 통해서 이루어지며, SessionFactory에서 얻어지는 Session을 이용해서 DB에 접근하게 됩니다. 그리고 만들어진 Session을 이용해서 DB에 객체의 query를 만들어주고, 그 query를 실행하게 됩니다.  

지금까지 나온 개념을 좀 정리해보도록 하겠습니다. 

1. Entity : Domain Model과 Persistence Object간의 연결 객체
2. SessionFactory : Entity를 얻어내기 위한 Session을 관리하는 객체
3. Session : Entity를 Persistence Layer에서 얻어내는 객체

가 됩니다. 조금 더 간단히 생각해보면, Session은 JdbcTemplate와 비슷한 개념을 가지고 있고, SessionFactory는 DataSource와 비슷한 개념을 가지게 됩니다. 단 기능면에서는 좀 많은 차이를 가지고 있지요. 

SessionFactory를 구성하기 위해서는 hibernate.cfg.xml 파일을 통해서 이루어지며, 파일의 구성은 다음과 같습니다.

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"><hibernate-configuration>
  <session-factory>
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.password">qwer12#$</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost/bookstore3</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
    <property name="hibernate.show_sql">true</property>
    <property name="hibernate.c3p0.min_size">5</property>
    <property name="hibernate.c3p0.max_size">20</property>
    <property name="hibernate.c3p0.timeout">300</property>
    <property name="hibernate.c3p0.max_statements">50</property>
    <property name="hibernate.c3p0.idle_test_period">3000</property>
    <mapping class="com.xyzlast.hibernate.bookstore.entities.User" />
    <mapping class="com.xyzlast.hibernate.bookstore.entities.Book" />
    <mapping class="com.xyzlast.hibernate.bookstore.entities.History" />
  </session-factory>
</hibernate-configuration>


파일이 매우 깁니다. 그리고 내용이 복잡해보이지만, 기존의 DataSource를 이용했을 때와 큰 차이가 없습니다. driver_class, password, username, url의 경우에는 기존의 DataSource와 완전히 동일합니다. 
dialect의 경우, hibernate와 db간의 연결을 해주기위한 설정입니다. 우리가 사용한 MySql5의 경우 MySQL5InnoDBDialect를 사용을 하며 이것은 Oracle을 사용하는 경우에는 Oracle10Dialect를 사용하면 됩니다. JDBC가 DB를 연결할 때 사용하는 것이 driver_class라면, 그 driver class와 Hibernate간에 연결하는 중간다리 역활을 하는 것이 dialect입니다. 

Hibernate는 기본적으로 c3p0 connection pool을 사용하도록 구성되어 있습니다. 앞으로는 사용하지는 않을것이지만, c3p0에 대해서 알아두시는것도 괜찮습니다. 
그리고, 나머지 c3p0로 시작되는 설정은 모두 connection pool에 대한 설정들입니다. 지금까지 우리가 만든 application은 한개의 DB connection만을 가지고, 처리를 했습니다. 만약에 web system에 올렸다면 이는 큰 문제가 되는 것으로, db에 query를 날릴때 딱 1개만의 연결을 갖게 됩니다. 여러 사용자가 동시에 사용을 할때, 먼저 사용자가 connection을 갖게 되면 그 다음 처리를 못해주게 됩니다. c3p0에 대한 설정들은 다음과 같습니다. 

1. min_size : connection pool의 최소 갯수를 지정합니다. connection을 application이 시작한 다음 최소 5개는 만들도록 설정한 상태입니다.
2. max_size : connection pool의 최대 갯수를 지정합니다. connection을 application이 시작한 후, 최대 20개까지 만들도록 설정했습니다.
3. timeout : connection의 생명 시간을 지정합니다. 처음 만들어진지 300sec가 지나면 connection을 소멸하도록 합니다.
4. max_statement : 1개의 connection당 최대 query문을 지정합니다. 최대 50개의 query문이 쌓이게 되면 DB측에 flush로 한번에 보내버립니다. 이는 Transaction의 성능에 영향을 미치고 db에 대한 bulk 작업에 도움을 줍니다.
5. idle_test_period : connection을 유지하고 있지만, db측에서 connection을 끊어버린 것을 확인하는 시간입니다. 이는 db server가 connection을 같이 관리할 때, 이 값을 통해서 자신의 connection을 계속해서 유효하도록 만드는 시간주기입니다. 

나머지 mapping class의 경우, mapping된 class를 추가해주는 방법입니다. 

자, 이제 객체 선언 코드들입니다. 너무 길어서 get/set code는 제거되었습니다. 한번 확인해보도록 하겠습니다. 

@Entity
@Table(name="books")
public class Book {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    @Column(name="name")
    private String name;
    @Column(name="author")
    private String author;
    @Column(name="status")
    @Enumerated(EnumType.ORDINAL)
    private BookStatus status;
    @Column(name="publishDate")
    private Date publishDate;
    @JoinColumn(name="rentUserId", nullable=true)
    @ManyToOne
    private User rentUser;
    @Column(name="comment", nullable=true)
    private String comment;
    @OneToMany(mappedBy="book")
    private Set<History> bookHistories = new ArrayList<>();
}


@Entity
@Table(name="histories")
public class History {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    @ManyToOne
    @JoinColumn(name="userId")
    private User user;
    @ManyToOne
    @JoinColumn(name="bookId")
    private Book book;
    @Enumerated(EnumType.ORDINAL)
    @Column(name="actionType")
    private ActionType action;
    @Column(name="insertDate")
    private Date insertDate;
}

@Entity
@Table(name="users")
public class User {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    @Column(name="name")
    private String name;
    @Column(name="password")
    private String password;
    @Enumerated(EnumType.ORDINAL)
    @Column(name="level")
    private UserLevel level;
    @Column(name="point")
    private int point;
    @OneToMany(mappedBy="user")
    private List<History> histories = new ArrayList<>();
    @OneToMany(mappedBy="rentUser")
    private List<Book> rentBooks = new ArrayList<>();
}

@annotation이 많이 생겼습니다. 각각의 @annotation에 대해서 간단히 알아보도록 하겠습니다. 

@Entityentity 객체임을 지정하는 annotation입니다.
@Tableentitty 객체와 연결되는 table을 지정하는 annotation입니다.
@IdPK를 지정하는 annotation입니다.
@ColumnDB의 Column과 1:1로 mapping되는 Property를 지정하는 annotation입니다.
@GeneratedValuePK의 값이 지정되는 방법을 결정할 수 있습니다. AUTO의 경우 mysql의 AUTO_INCREMENT에 해당됩니다. 각각의 DB에 따라 다른 값을 넣어주는 것도 가능합니다.(ex : oracle의 sequence)
@Enumeratedenum 값과 1:1로 mapping이 되는 것을 지정합니다.
@ManyToOne자신과 같은 객체들이 다른 한개의 객체에 연관이 있음을 지정합니다. 이는 N:1의 속성을 지정하게 됩니다.
@JoinColumn@ManyToOne과 같이 사용됩니다. Join 되는 Column을 지정합니다. (FK column)
@OneToMany자신이 다른 객체들에 연관이 있음을 지정합니다. 이는 1:N 또는 N:N의 속성을 지정합니다.

여기에서 개발자들에게 개념적으로 힘든 부분이 @ManyToOne과 @JoinColumn, @OneToMany입니다. 먼저 지금까지 만든 entity에 대한 개념을 한번 더 정립해보도록 하겠습니다. 


하나의 엔티티 클래스는 다음의 요구조건을 충족해야 합니다:

# 클래스 선언부에 javax.persistence.Entity 어노테이션을 반드시 명시하여야 합니다.
# 기본 생성자를 반드시 포함해야 합니다. 기본생성자는 인수가 없는 생성자를 의미합니다. 만일, 인수를 포함한 생성자를 사용한다면 명시적으로 기본 생성자를 만들어야만 합니다.
# 클래스를 fianl로 선언해서는 안됩니다. 영속화 대상이 아닌 필드나 메소드는 반드시 final로 선언합니다.
# 엔티티 인스턴스가 세션빈의 리모트 비지니스 인터페이스와 같이 detached object형태로 전달되는 경우, 클래스는 반드시 Serializble 인터페이스를 구현해야 합니다.
# 엔티티는 엔티티 또는 non-엔티티 클래스 모두 확장(extend)이 가능하며, non-엔티티 클래스는 엔티티 클래스를 확장할 수 있습니다.
# 영속화 인스턴스 변수는 반드시 private, protected, package-private중 하나로 선언되어야 하며, 엔티티 클래스의 메소드에 의해 직접 참조될 수 있습니다. 클라이언트는 엔티티의 상태를 접근자(accessor)또는 비지니스 메소드를 통해 접근이 가능합니다.

또한, 영속상태의 엔티티는 엔티티의 인스턴스 변수 또는 자바빈 스타일의 속성에 의해 접근할 수 있습니다. 필드 또는 속성은 반드시 다음의 자바 언어 타입에 따릅니다.

# 자바 원시 타입
# java.lang.String
# 그외 직렬화 가능(Serializable) 타입들
# 자바 원시타입의 Wrappers
# java.math.BigInteger
# java.math.BigDecimal
# java.util.Date
# java.util.Calendar
# java.sql.Date
# java.sql.Time
# java.sql.Timestamp
# 사용자 정의 직렬화 타입들
# byte[]
# Byte[]
# char[]
# Character[]
# 열거형(Enumerated) 타입들
# 다른 엔티티 또는 엔티티 컬렉션
# 내장형(Embeddable) 클래스

각 엔티티들은 다음과 같은 관계를 가질 수 있습니다. 

# One-to-one : 각 엔티티 인스턴스는 하나의 인스턴스가 다른 엔티티와 연관됩니다. One-to-one 관계는 javax.persistence.OneToOne 어노테이션으로 해당 필드에 정의합니다.
# One-to-many : 하나의 엔티티 인스턴스가 다수의 다른 엔티티 인스턴스와 연관됩니다. 영업주문의 경우 다수의 라인 아이템을 가집니다. 즉, Order 엔티티는 여러개의 LineItem 을 가지므로 이들 사이에는 One-to-many 관계가 선언되어야 하므로 javax.persistence.OneToMany 어노테이션을 사용합니다.
# Many-to-one : 다수의 인스턴스가 하나의 다른 엔티티와 연관됩니다. 이것은 One-to-many 와 반대입니다. 영업주문의 경우 LineItem은 Order에 대해 Many-to-one 관계가 성립되므로 javax.persistence.ManyToOne 어노테이션을 지정합니다.
# Many-to-many : 이 인스턴스는 다수의 인스턴스가 각기 다른 엔티티들과 연관됩니다. 예를들어, 학교에서 각 수업들은 다수의 학생들과 연관이 있으며, 학생 역시 다수의 수업을 듣고 있습니다. 이경우 수업과 학생은 Many-to-Many 관계가 성립되며 javax.persistence.ManyToMany 어노테이션을 사용합니다.

엔티티 연관의 방향 : 엔티티간의 연관 관계는 단방향 또는 양방향이 될 수 있습니다. 양방향 관계에서 한쪽은 소유자측이 되며 그 반대는 피소유자가 됩니다. 단방향 연관은 소유자측만 대변합니다. 연관관계에서 소유자측은 영속성 런타임(Persistence Runtime)이 연관관계에 있는 데이터를 어떻게 갱신할지 결정합니다.

양방향 관계

양방향 관계에서, 각 엔티티는 서로 상대필드나 속성에 대한 참조를 가집니다. 예를들어, User는 어떤 History 인스턴스들이 있는지 알고 있으며 History는 자신이 어떤 User에 속해있는지 알고 있습니다. 이 경우 이들은 양방향 관계를 가지고 있다고 볼 수 있습니다.
양방향 관계는 반드시 다음의 규칙을 따릅니다:

# 양방향 관계의 반대편(피소유자)은 반드시 소유자측을 mappedBy를 사용해 참조해야 합니다. mappdBy는 @OneToOne, @OneToMany 또는 @ManyToMany 어노테이션에서 사용할 수 있습니다. mappedBy 는 소유자 엔티티의 필드나 속성과 대응됩니다.
# Many-to-one 양방향 관계에서 Many측 관계는 mappedBy요소를 사용할 수 없습니다. Many측은 항상 관계에서 소유자측이어야 합니다.
# one-to-one 양방향 관계에서, 소유자측은 다른측에 대한 FK를 가지는쪽입니다.
# Many-to-many 양방향 관계는 양쪽중 둘 중 아무나 소유자가 될 수 있습니다.


entity와 entity간의 관계에 대한 정리가 조금 되셨나요? 이 부분이 이해가 되지 않는다면 Dao부분을 구성할 수 없습니다. 꼭 코드와 같이 이해를 해주시길 바랍니다. 

자, 이제까지 만들어진 객체를 기반으로 Dao를 작성해보도록 하겠습니다. 

먼저, SessionFactory를 생성해주는 객체를 구성해보도록 하겠습니다. SessionFactory는 Configuration 객체를 통해서 구현이 되며, 위에 소개된 hibernate.cfg.xml 파일을 기반으로 구성이 되게 됩니다. 코드는 다음과 같습니다. 

public class HibernateSessionFactoryBuilder {
    public static SessionFactory build(String filename) {
        Configuration cfg = new Configuration();
        cfg.configure(filename);
        ServiceRegistryBuilder serviceRegistryBuilder = new ServiceRegistryBuilder()
                                                            .applySettings(cfg.getProperties());
        SessionFactory sessionFactory = cfg.configure().buildSessionFactory(
                serviceRegistryBuilder.buildServiceRegistry());
        return sessionFactory;
    }
}

그리고, Hibernate는 기본적으로 Session을 통해서 DB와 연결합니다. 그리고 Session의 모든 Action은 Transaction을 기반으로 동작하게 됩니다. 따라서, Template-callback pattern을 이용해서 Hibernate의 Transaction을 처리하는 구문을 만들어주는 것이 구성이 용의합니다. HibernateSqlExecutor를 작성하고, 그 코드는 다음과 같이 동작하게 됩니다. 

public class HibernateSqlExecutor {
    private final SessionFactory sessionFactory;

    public HibernateSqlExecutor(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Object execute(HibernateAction action) {
        Session session = sessionFactory.openSession();
        Transaction transaction = session.beginTransaction();
        try {
            Object ret = action.doProcess(session);
            transaction.commit();
            return ret;
        } catch(Exception ex) {
            transaction.rollback();
            throw ex;
        } finally {
            session.close();
        }
    }
}

public interface HibernateAction {
    Object doProcess(Session session);
}

이 HibernateSqlExecutor를 기반으로 구성되는 Hibernate의 Dao는 다음과 같이 꾸며지게 됩니다. 

public class BookDaoImpl implements BookDao {
    @Autowired
    private SessionFactory sessionFactory;
    
    private HibernateSqlExecutor executor;
    
    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public HibernateSqlExecutor getExecutor() {
        return executor;
    }

    public void setExecutor(HibernateSqlExecutor executor) {
        this.executor = executor;
    }

    @Override
    public void add(final Book book) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.save(book);
                return null;
            }
        });
    }

    @Override
    public Book get(final int id) {
        return (Book) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                Book book = (Book) session.get(Book.class, id);
                return book;
            }
        });
    }

    @Override
    public void update(final Book book) {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.refresh(book);
                session.update(book);
                return null;
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public void deleteAll() {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                List<Book> books = session.createCriteria(Book.class).list();
                for(Book book : books) {
                    session.delete(book);
                }
                return null;
            }
        });
    }

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

    @Override
    public void delete(final Book book) {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.delete(book);
                return null;
            }
        });
    }

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

    @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();
            }
        });
    }
}


public class UserDaoImpl implements UserDao {
    @Autowired
    private SessionFactory sessionFactory;
    @Autowired
    private HibernateSqlExecutor executor;

    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public HibernateSqlExecutor getExecutor() {
        return executor;
    }

    public void setExecutor(HibernateSqlExecutor executor) {
        this.executor = executor;
    }


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

    @Override
    public User get(final int id) {
        return (User) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.get(User.class, id);
            }
        });
    }

    @Override
    public void update(final User user) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.refresh(user);
                session.update(user);
                return null;
            }
        });
    }

    @Override
    public void delete(final User user) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.refresh(user);
                session.delete(user);
                return null;
            }
        });
    }

    @Override
    public void add(final User user) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.save(user);
                return null;
            }
        });
    }

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

    @Override
    public void deleteAll() {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                @SuppressWarnings("unchecked")
                List<User> users = session.createCriteria(User.class).list();
                for (User user : users) {
                    session.delete(user);
                }
                return null;
            }
        });
    }
}

public class HistoryDaoImpl implements HistoryDao {
    @Autowired
    private SessionFactory sessionFactory;
    @Getter
    @Setter
    private HibernateSqlExecutor executor;
    
    @Override
    public void add(final History userHistory) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.save(userHistory);
                return null;
            }
        });
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public List<History> getByUser(final User user) {
       return (List<History>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(History.class)
                        .add(Restrictions.eq("user", user))
                        .addOrder(Order.desc("insertDate")).list();
            }
        });
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public void deleteAll() {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                List<History> userHistories = session.createCriteria(History.class).list();
                for(History userHistory : userHistories) {
                    session.delete(userHistory);
                }
                return null;
            }
        });
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public List<History> getByBook(final Book book) {
        return (List<History>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(History.class)
                        .add(Restrictions.eq("book", book))
                        .addOrder(Order.desc("insertDate")).list();
            }
        });
    }

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

코드가 많이 바뀌었습니다. 한번 코드에 대해서 논해보도록 하겠습니다. 
Hibernate는 Session을 통해서 DB에 대한 접근을 행하게 됩니다. 그리고 Session은 Criteria를 통해, 각 객체에 대한 query를 생성하게 됩니다. 그리고 Criteria는 Restriction과 Projection을 통해서 각각의 DB에 query를 행하고, CUD의 경우에는 save, update, delete라는 action을 통해서 구성하게 됩니다. 

먼저, countAll()을 하는 code를 간단하게 살펴보도록 하겠습니다. 

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

위 코드에서 setProjection을 통해서, rowCount를 하는 것을 지정합니다. 그리고 그 값을 uniqueResult를 통해서 얻어옵니다. rowCount라는 값 자체는 저 query가 수행되면 단일 값으로 나오기 때문에 uniqueResult를 통해서 얻어오게 됩니다. 

그럼 다음은 어떤 코드를 한번봐볼까요? HistoryDao의 getByUser를 한번 봐보도록 하겠습니다. 

    @SuppressWarnings("unchecked")
    @Override
    public List<History> getByUser(final User user) {
       return (List<History>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(History.class)
                        .add(Restrictions.eq("user", user))
                        .addOrder(Order.desc("insertDate")).list();
            }
        });
    }

보시면, History객체의 user property와 동일한 user를 list로 얻어오는 것을 직관적으로 지정하는 것을 알 수 있습니다. 


마지막으로, 테스트 코드를 통해서 Hibernate의 강력함을 한번 느껴보도록 하겠습니다. 지금 객체가 가장 복잡하게 꼬여있다고 생각되는 History에 대한 테스트 코드입니다. 

public class HistoryDaoImplTest {
    private HibernateSqlExecutor executor;
    private BookDaoImpl bookDao;
    private UserDaoImpl userDao ;
    private HistoryDaoImpl historyDao;
    
    @Before
    public void setUp() {
        SessionFactory sessionFactory = HibernateSessionFactoryBuilder.build("hibernate.cfg.xml");
        executor = new HibernateSqlExecutor();
        executor.setSessionFactory(sessionFactory);
        
        bookDao = new BookDaoImpl();
        bookDao.setExecutor(executor);
        
        userDao = new UserDaoImpl();
        userDao.setExecutor(executor);
        
        historyDao = new HistoryDaoImpl();
        historyDao.setExecutor(executor);
        
        historyDao.deleteAll();
        bookDao.deleteAll();
        userDao.deleteAll();
        
        assertThat(bookDao.countAll(), is(0));
        assertThat(userDao.countAll(), is(0));
        assertThat(historyDao.countAll(), is(0));
        
        for(Book book : getBooks()) {
            bookDao.add(book);
        }
        
        for(User user : getUsers()) {
            userDao.add(user);
        }
    }
    
    @Test
    public void addAndCount() {
        List<Book> books = bookDao.getAll();
        List<User> users = userDao.getAll();
        int count = 0;
        for(Book book : books) {
            for(User user : users) {
                History history = new History();
                history.setAction(HistoryActionType.RENT);
                history.setBook(book);
                history.setUser(user);
                history.setInsertDate(new Date());
                historyDao.add(history);
                count++;
                assertThat(historyDao.countAll(), is(count));
            }
        }
        
        List<User> users2 = userDao.getAll();
        for(User user : users2) {
            System.out.println("==========================================");
            System.out.println("User : " + user.getName());
            for(History history : user.getHistories()) {
                System.out.println("History : BOOK NAME >" + history.getBook().getName());
            }
        }
    }
    
    @Test
    public void getByUser() {
        List<Book> books = bookDao.getAll();
        List<User> users = userDao.getAll();
        int count = 0;
        for(Book book : books) {
            for(User user : users) {
                History history = new History();
                history.setAction(HistoryActionType.RENT);
                history.setBook(book);
                history.setUser(user);
                history.setInsertDate(new Date());
                historyDao.add(history);
                count++;
                assertThat(historyDao.countAll(), is(count));
            }
        }
        
        for(User user : users) {
            List<History> histories = historyDao.getByUser(user);
            assertThat(histories.size(), is(books.size()));
        }
    }
    
    @Test
    public void getByBook() {
        List<Book> books = bookDao.getAll();
        List<User> users = userDao.getAll();
        int count = 0;
        for(Book book : books) {
            for(User user : users) {
                History history = new History();
                history.setAction(HistoryActionType.RENT);
                history.setBook(book);
                history.setUser(user);
                history.setInsertDate(new Date());
                historyDao.add(history);
                count++;
                assertThat(historyDao.countAll(), is(count));
            }
        }
        
        for(Book book : books) {
            List<History> histories = historyDao.getByBook(book);
            assertThat(histories.size(), is(users.size()));
        }
    }
    
    private List<User> getUsers() {
        User user1 = new User();
        user1.setName("name01");
        user1.setPassword("password01");
        user1.setPoint(99);
        user1.setLevel(UserLevel.NORMAL);
        
        User user2 = new User();
        user2.setName("name02");
        user2.setPassword("password02");
        user2.setPoint(101);
        user2.setLevel(UserLevel.READER);
        
        User user3 = new User();
        user3.setName("name03");
        user3.setPassword("password03");
        user3.setPoint(301);
        user3.setLevel(UserLevel.MVP);
        
        return Arrays.asList(user1, user2, user3);
    }
    
    private List<Book> getBooks() {
        Book book1 = new Book();
        book1.setName("book name01");
        book1.setAuthor("autor name 01");
        book1.setComment("comment01");
        book1.setPublishDate(new Date());
        book1.setStatus(BookStatus.NORMAL);
        
        Book book2 = new Book();
        book2.setName("book name02");
        book2.setAuthor("autor name 02");
        book2.setComment("comment02");
        book2.setPublishDate(new Date());
        book2.setStatus(BookStatus.NOWRENT);
        
        Book book3 = new Book();
        book3.setName("book name03");
        book3.setAuthor("autor name 03");
        book3.setComment("comment03");
        book3.setPublishDate(new Date());
        book3.setStatus(BookStatus.MISSING);
        
        List<Book> books = Arrays.asList(book1, book2, book3);
        return books;
    }
}

대부분의 테스트코드와 비슷하게 데이터를 지워버리고, 신규 데이터를 넣도록 구성이 되어 있습니다. addAndCount의 실행결과는 다음과 같습니다. 

==========================================
User : name01
Hibernate: select histories0_.userId as userId0_3_, histories0_.id as id2_3_, histories0_.id as id2_2_, histories0_.actionType as actionType2_2_, histories0_.bookId as bookId2_2_, histories0_.insertDate as insertDate2_2_, histories0_.userId as userId2_2_, book1_.id as id1_0_, book1_.author as author1_0_, book1_.comment as comment1_0_, book1_.name as name1_0_, book1_.publishDate as publishD5_1_0_, book1_.rentUserId as rentUserId1_0_, book1_.status as status1_0_, user2_.id as id0_1_, user2_.level as level0_1_, user2_.name as name0_1_, user2_.password as password0_1_, user2_.point as point0_1_ from userHistories histories0_ left outer join books book1_ on histories0_.bookId=book1_.id left outer join users user2_ on book1_.rentUserId=user2_.id where histories0_.userId=?
History : BOOK NAME >book name02
History : BOOK NAME >book name01
History : BOOK NAME >book name03
==========================================
User : name02
Hibernate: select histories0_.userId as userId0_3_, histories0_.id as id2_3_, histories0_.id as id2_2_, histories0_.actionType as actionType2_2_, histories0_.bookId as bookId2_2_, histories0_.insertDate as insertDate2_2_, histories0_.userId as userId2_2_, book1_.id as id1_0_, book1_.author as author1_0_, book1_.comment as comment1_0_, book1_.name as name1_0_, book1_.publishDate as publishD5_1_0_, book1_.rentUserId as rentUserId1_0_, book1_.status as status1_0_, user2_.id as id0_1_, user2_.level as level0_1_, user2_.name as name0_1_, user2_.password as password0_1_, user2_.point as point0_1_ from userHistories histories0_ left outer join books book1_ on histories0_.bookId=book1_.id left outer join users user2_ on book1_.rentUserId=user2_.id where histories0_.userId=?
History : BOOK NAME >book name03
History : BOOK NAME >book name02
History : BOOK NAME >book name01
==========================================
User : name03
Hibernate: select histories0_.userId as userId0_3_, histories0_.id as id2_3_, histories0_.id as id2_2_, histories0_.actionType as actionType2_2_, histories0_.bookId as bookId2_2_, histories0_.insertDate as insertDate2_2_, histories0_.userId as userId2_2_, book1_.id as id1_0_, book1_.author as author1_0_, book1_.comment as comment1_0_, book1_.name as name1_0_, book1_.publishDate as publishD5_1_0_, book1_.rentUserId as rentUserId1_0_, book1_.status as status1_0_, user2_.id as id0_1_, user2_.level as level0_1_, user2_.name as name0_1_, user2_.password as password0_1_, user2_.point as point0_1_ from userHistories histories0_ left outer join books book1_ on histories0_.bookId=book1_.id left outer join users user2_ on book1_.rentUserId=user2_.id where histories0_.userId=?
History : BOOK NAME >book name01
History : BOOK NAME >book name03
History : BOOK NAME >book name02

내용이 복잡해보이지만, 신기한 특성이 보이지 않나요? User 객체를 한개 얻어왔을 뿐인데, User에 딸린 History가 모두 얻어집니다. 그리고 그 History는 Book객체를 모두 가지고 있고, 이건 DB에 저장된 결과를 보여주고 있습니다. 이와 같이 Hibernate는 DB와 실제 객체간의 연결을 매우 powerful 하게 지원하고 있습니다. DB에 저장된 결과를 마치 고구마 줄기 뽑듯이 객체에 대한 데이터를 들고올 수 있습니다.  

한번 여기까지 Dao 코드를 모두 구성해보고 동작을 확인해보시길 바랍니다. 그리고 console에 Hibernate의 action이 발생하는 경우, 모두 query가 console에 나오게 되어있기 때문에, action에 따른 query 발생이 어떻게 동작하고 있는지 확인하시면 더 재미있는 코드를 짜보실수 있습니다. 

Summary

Hibernate를 통한 Dao의 구성을 알아봤습니다. 여기서 지금 알아본 Hibernate에 대한 내용은 너무나 미약합니다. Hibernate의 경우 책 한권 이상으로 끝나는 내용이 아닙니다. Hibernate의 경우, 굉장히 방대한 양의 Framework입니다. 구성에 대해서 많은 고민을 해보시고, 코드를 한번 더 보시면서 이게 어떤 의미인지 고민을 해보시길 바랍니다. 

한번 더 정리를 하도록 하겠습니다. Hibernate의 경우에는 SessionFactory를 이용한 Session을 통해 DB와 데이터 교환을 행하게 됩니다. SessionFactory는 DataSource를 한번 감싼 형태로, 기존 Jdbc와 비교를 한다면 DataSource와 동일한 위치에 있습니다. 그리고 Session은 jdbcTemplate과 유사한 기능을 행합니다. 이번 예제에서는 HibernateSqlExecutor를 통해서 Session에 query를 보내는 action을 행했습니다. 
그리고, 오늘 제가 보여드린 Hibernate는 Hibernate라는 빙산의 일각일 뿐입니다. xml을 통한 mapping, Set이 아닌 List를 통한 OneToMany mapping 등, 여러가지 내용들이 너무나도 많습니다. 이 부분에 대해서는 좀더 좀더 고민해보시고, 찾아서 공부를 하시는것이 좋습니다.  Hibernate를 통한 지금 DB의 구성 코드에 대해서 전체를 다 작성을 해보시고, 기존 코드와 비교를 해보세요. 이 부분이 진행이 정상적으로 되지 못하면 뒷부분 부터가 많이 힘들어집니다.



Posted by Y2K
,

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



지금까지 우리는 1개의 Project를 이용해서 간단한 Dao와 Service를 구성해봤습니다. 그렇지만, 실제 프로젝트에서는 여러개의 Project간에 연결이 되어 구성이 되는 것이 일반적입니다. 일반적으로 프로젝트를 구성한다면 약 3개이상의 Project가 구성이 되는것이 대부분입니다. 

다음은 일반적인 Web Project의 구성입니다. 

# Core / Common : Entity, Dao, Service를 갖는 jar project 입니다.
# PublicWeb (CustomWeb) : 사용자에게 보여질 web page입니다. 
# PrivateWeb (AdminWeb) : 관리자용 web page입니다. 사용자에게 공개되는 page가 아닌, 개발자들이 유지보수를 위한 페이지로 만드는 경우가 많습니다.
# Resources : Web에 사용되는 java script 및 image resource, html tag 등을 관리하는 jar project 입니다.

이와 같이 구성이 된다면, 서로간에 build가 되는 순서 역시 변경이 되어야지 됩니다. Core/Common project가 먼저 build가 되고, Resource, PublicWeb, PrivateWeb 순으로 build가 되는 것이 일반적일 것입니다. 그런데, 지금까지 우리가 구성한 project는 pom.xml을 이용한 단일 project만이 지원이 되는 것이 사실입니다. 이런 단일 project가 아닌 multi project를 maven으로 관리하는 법에 대해서 알아보도록 하겠습니다. 

maven에서 multi project를 관리하기 위해서는 parent-child pom 관계가 만들어져야지 됩니다. parent project는 child project에 대한 root project로 구성이 되게 됩니다. 또한 child의 child는 구성이 불가능합니다. 

구성할 프로젝트는 다음과 같습니다. 

ProjectGroup IdArtifactIdcompile orderDescription
Parent Projectcom.xyzlast.bookstorebookstore-Root Project
CommonBLcom.xyzlast.bookstorecommon-bl1Business Logic 구성 Project (Entity, Dao, Service 구성)
WebResourcecom.xyzlast.bookstoreweb-resources2Web Resource를 위치 - image, css, javascript에 대한 common library
PublicWebcom.xyzlast.bookstorepublic-web3사용자들이 방문할 web page
AdminWebcom.xyzlast.bookstoreadmin-web3관리자가 방문할 web page

위 5개의 project는 서로간에 상호 연관성을 갖게 됩니다. 따라서, 위 표와 같이 compile order역시 지정이 가능해야지 됩니다. 

이 예제는 새로운 workspace를 구성해서 살펴보는 것이 좋습니다. 기존 eclipse에서 export preferance기능을 이용해서 사용자 설정을 모두 export 시켜두시고 작업에 임하시는것이 좋습니다. 

1. Parent Project의 생성

새로운 workspace folder에서 다음 명령어를 실행합니다. 

mvn archetype:create -DgroupId=com.xyzlast.bookstore -DartifactId=bookstore

만들어진 bookstore folder안에 있는 pom.xml 을 다음과 같이 수정합니다. 

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.xyzlast.bookstore</groupId>
  <artifactId>bookstore</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>
  <name>bookstore</name>
  <url>http://maven.apache.org</url>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
</project>

이제 다시 eclipse를 실행후, workspace를 새로만든 workspace로 변경합니다. 


그전까지 사용하던 설정을 다시 얻어와야지 되기 때문에 File > Import를 이용해서 preferance를 다시 얻어 설정합니다. 후에 eclipse를 종료 후, 다시 시작해주는것이 좋습니다.
File > Import를 이용해서 Existing Maven Projects 를 선택 후, bookstore를 import 시킵니다. 



이제 Parent Project의 구성은 모두 마쳐졌습니다. 


- Eclipse를 이용하는 방법

Eclipse에서 new maven project를 선택. Project type을 pom-root로 선택해서 신규 프로젝트를 선택, 생성합니다.



2. Child Project의 구성

child project를 구성하는 것은 eclipse에서 진행하도록 하겠습니다. build 순서에 따라 common-bl을 먼저 구성하도록 하겠습니다. 
New Project를 선택해서, Maven Module을 선택합니다. (Maven Project가 아닙니다!)



archeType은 기존 project를 만든것과 동일하게 maven-archetype-quickstart를 선택해줍니다.

groupId 값 및 만들어질 package 이름을 정합니다. package 이름은 jar 이름이 됩니다. 그리고 버젼을 기억하기 쉬운 1.0.0으로 변경합니다.





Finish를 누르면 다음과 같이 Project가 만들어집니다. 


이제 나머지 Project들을 모두 만들어줍니다. Resource의 경우에는 동일하게 maven-archetype-quickstart로 구성을 해주고, PublicWeb, PrivateWeb의 경우에는 maven-archetype-webapp 으로 구성을 해주세요. 모든 Project의 구성이 마쳐지면 다음과 같은 모습이 만들어집니다. 



bookstore(root) project를 확인해보면 다음과 같은 Folder구조를 갖게 됩니다.


그리고 실제 폴더 구조 역시 Root Project에 하위 폴더가 만들어져서 Project가 구성되게 됩니다. Root Project의 pom.xml 을 한번 확인해보도록 하겠습니다. 기존까지는 없던 modules 항목에 다음과 같은 항목들이 추가 되어 있을것입니다. 

  <modules>
    <module>common-bl</module>
    <module>web-resources</module>
    <module>public-web</module>
    <module>private-web</module>
  </modules>

modules 안에 있는 내용대로 build process가 진행되게 됩니다. 확인을 위해, command를 이용해서 mvn compile을 진행해보도록 하겠습니다.




이제 Project간에 서로간에 package를 copy 하는 과정을 추가하도록 하겠습니다. 먼저 Project는 common-bl과 web-resources는 서로간에 dependencies가 없고, public-web, private-web은 common-bl과 web-resources에 dependency를 가지게 됩니다. Root Project에 dependency를 갖는 project를 등록해서 각각의 Project에서 사용하도록 하겠습니다. 

Root Project (BookStore Project)의 pom.xml를 열어, 다음과 같은 항목을 추가합니다. 

 <dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.xyzlast.bookstore</groupId>
            <artifactId>common-bl</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.xyzlast.bookstore</groupId>
            <artifactId>web-resources</artifactId>
            <version>1.0.0</version>
        </dependency>        
    </dependencies>
  </dependencyManagement>

이제 dependency를 가질 private-web과 public-web에 다음과 같은 dependency를 추가하도록 합니다. 

  <dependencies>
    <dependency>
        <artifactId>common-bl</artifactId>
        <groupId>com.xyzlast.bookstore</groupId>
    </dependency>
    <dependency>
        <artifactId>web-resources</artifactId>
        <groupId>com.xyzlast.bookstore</groupId>
    </dependency>
  </dependencies>

버젼이 들어가지 않는것을 확인해주세요. 버젼정보은 root project에서 관리되는 버젼을 따라가게 됩니다. 
추가 된 후에, pom.xml의 dependencies를 보면 다음과 같이 표시됩니다. 




추가를 모두 마치고 나서 다시 maven에서 package를 진행해보도록 하겠습니다. 

mvn clean package

packaging 작업이 모두 마쳐진 후, private-web 또는 public-web의 target/private-web/WEB-INF/lib 에 보시면 작업한 common-bl과 web-resources가 jar 형태로 compile 되어서 copy 되어 있는 것을 확인할 수 있습니다. 





3. Parent Project 설정

maven을 이용한 multi project를 구성을 하면, parent의 속성이 child project들에게 상속이 됩니다. 
지금 구성은 특별히 고치지 않았다면 J2SE-1.5로 compile이 되도록 되어 있습니다. 이것을 java 1.7로 compile이 되도록 parent project를 수정해주도록 합니다. build의 PlugIn에 다음 항목을 추가하도록 하겠습니다. 

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.0</version>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
          <encoding>utf8</encoding>
        </configuration>
      </plugin>

추가후, eclipse에서 전체 project의 maven->update를 행하면, project의 build compiler가 변경됨을 알 수 있습니다. 이와 같이, Parent Project에 설정한 항목은 하위 Project에 전달되어 처리가 되게 됩니다. 또한, Parent Project에 dependency에 추가가 되면, Child Project 들에 모두 추가가 됩니다. junit 4.11를 Parent project에 추가하고, child Project에서는 모든 dependency를 제거시켜주세요. pom을 수정후, child project의 pom.xml의 dependency Hierarchy는 다음과 같이 표시가 됩니다. 




common-bl, web-resource는 폴더 모양으로, 자신과 같은 Maven Module로 인식이 되고, junit은 물병모양(jar)로 인식되는 것을 확인해주세요. 


4. Eclipse 설정

개발된 code를 이용해서 서로간에 개발을 하기 위해서 eclipse에 약간의 설정 변경이 필요합니다. common-bl과 web-resources를 참조하고 있는 public-web과 private-web의 project 설정을 변경해야지 됩니다. project의 property에서 java build path 항목에서 common-bl과 web-resources를 추가해주도록 합니다. 




이제 Eclipse에서 public-web과 private-web은 common-bl의 객체에 대한 접근을 할 수 있습니다.


 

Nexus 서버를 이용한 사내 Common Jar의 이용

사내에서 개발을 하게 되면, 기존의 개발 Library들에 대한 재사용을 하게 됩니다. 그리고 몇몇 팀들은 이러한 Common Jar만 계속해서 기능을 업데이트시키고, 개발을 하는 팀이 생기게 됩니다. 대체적으로 기술지원팀이나 RND 팀이 이런 일을 맡게 되지요. 

이렇게 개발된 Common Jar를 배포하기 위한 서버가 Nexus 서버입니다. 
Nexus 서버는 우리가 지금까지 libarary를 등록하기 위해서 사용한 maven central repository와 동일한 기능을 제공합니다. 다만, 관리자가 추가로 등록한 jar를 pom을 통해서 관리를 할 수 있는 기능을 따로 제공하는 차이가 있습니다. 사내에서 사용되는 maven central repository라고 보시면 기능이 완전히 동일합니다. 

다음 주소에 nexus 서버가 설치되어 있습니다.


nexus 서버의 이용은 매우 단순합니다. 기본적으로 pom.xml에 다음과 같은 설정을 추가하시면 nexus 서버를 사용하실 수 있습니다. 

  <repositories>
    <repository>
      <id>local</id>
      <url>http://192.168.13.209:8080/nexus/content/groups/public</url>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
  </repositories>

nexus 서버를 이용하는 경우에는 우리가 사내에서 개발한 jar를 서로간에 공유해서 사용하는 것이 가능합니다. 그리고 maven central에서 제공하지 않는 jar를 등록해서 사용하는 것 역시 가능합니다. 대체적으로 oracle, mssql과 같이 3rd party에서 제공하는 jar를 사용하는 경우에는 nexus 서버를 이용해서 사내에서 공유해서 사용하는 것이 일반적입니다. 

많은 것들을 등록해서 사용할 수 있지만, 제가 전에 즐거운 기다림 프로젝트를 하면서 외부 저장소가 필요했었던 jar들은 2개가 있었습니다. 아래 2개의 jar를 등록했으니, pom에 사내 nexus를 등록했을 때와, 등록하지 않았을때를 비교해보시길 바랍니다. 

mssql jdbc driver
<dependency>
  <groupId>com.microsoft.sqlserver.jdbc</groupId>
  <artifactId>sqljdbc</artifactId>
  <version>3.0</version>
</dependency>

java pns (iPhone push module)
<dependency>
  <groupId>com.googlecode.javapns</groupId>
  <artifactId>javapns-jdk16</artifactId>
  <version>2.2</version>
</dependency>


Summary

maven을 이용한 multi project 설정과 nexus 서버를 이용한 jar 구성에 대해서 알아봤습니다. 바로 전 시간까지 구성하던 BookStore는 한개의 Project로만 구성이 되어있습니다. 그렇지만, 이는 실전에서는 쓰이지 못합니다. 

오늘 보여드린 내용들이 결국은 실전에서 사용되는 구성이 됩니다. 우리가 곧 개발할 시스템의 경우에도 마찬가지고, 타팀에서 역시 마찬가지로 구성이 될 것이라고 생각합니다. 

그 이유는 공통된 Business Logic이 존재하고, 이 Logic에 대한 접근 방법이 다른 Web 또는 Application이 여러개 존재할 수 있기 때문입니다. 이러한 다각도적인 접근방법에 있어서, 이러한 multi project 구성은 필수적입니다. 이번에 보여드린 구성은 가장 대표적인 구성입니다. Domain(Model) Layer에 대한 project와 Controller와 View에 대한 project. 그리고 여러개의 Controller/View에 대한 공통 static resource (css, javascript, image)에 대한 Project 구성은 아주 당연하게 사용되는 영역입니다. 일반적으로 모든 web은 3개의 project를 이용해서 구성된다고 생각하시면 좀 더 고민이 줄 수 있을 것 같습니다. 





Posted by Y2K
,

8. ApplicationContext

Java 2013. 9. 9. 10:57

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



Spring에서 ApplicationContext는 IoC, AOP를 설정하는 Container 또는 Configuration이라고 할 수 있습니다.
ApplicationContext는 

# bean의 집합
# bean에 대한 Map
# bean에 대한 정의
# AOP에 의한 bean의 확장에 대한 정의

를 포함하고 있습니다. ApplicationContext에 대해서 좀 더 깊게 들어가보도록 하겠습니다.


ApplicationContext

Application Context는 org.springframework.context.ApplicationContext interface를 상속받은 객체입니다. interface의 정의는 다음과 같습니다. 

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
        MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
    String getId();
    String getApplicationName();
    String getDisplayName();
    long getStartupDate();
    ApplicationContext getParent();
    AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}

ApplicationContext는 bean에 대한 집합이라고 했습니다. bean은 일반적으로 POJO class로 구성이 되게 됩니다. bean과 POJO의 정의는 다음과 같습니다. 


Java Bean
원 목적은 Servlet에서 Java 객체를 가지고 와서 사용하기 위해서 작성된 객체입니다. 매우 간단한 객체이고, 사용이 편리해야지 된다. 라는 것을 원칙으로 가지고 있습니다. 특징으로는

# property를 갖는다. (private 변수와 get/set method를 갖는다.)
# serialization이 가능하다.

라는 특징을 갖습니다. 그렇지만 두번째 특징인 serialization이 가능한 특징은 지금은 거의 사용되고 있지 않습니다. property를 갖는 java 객체라는 의미로 생각해주시면 됩니다. 줄여서 bean이라는 표현을 많이 사용합니다. 

POJO
Plan Old Java Object의 약자입니다. POJO 객체는 특정 기술과 Spec에 독립적인 객체로 만들어지는 것을 원칙으로 삼습니다. 자신이 속한 package에 속한 POJO 객체 이외에는 Java의 기본 객체만을 이용해서 작성하는 것이 원칙입니다. 또한, 확장성을 위해 자신이 속한 package의 POJO 객체가 아닌 POJO 객체의 interface를 속성으로 갖는 bean 객체로서 만들어지는 것이 일반적입니다. 지금까지 작성된 Book, User, UserHistory 객체의 경우에 POJO 객체라고 할 수 있습니다. 

bean에 대한 집합인 ApplicationContext는 다음과 같은 특징을 갖습니다.

1. bean id, name, alias로 구분이 가능한 bean들의 집합입니다.
2. life cycle을 갖습니다. (singleton, prototype)
3. property는 value 또는 다른 bean을 참조합니다.

ApplicationContext는 bean들의 집합적인 특징 뿐 아니라, bean들의 loading 도 역시 담당하고 있습니다. ApplicationContext의 정보는 일반적으로 xml을 이용하고 있지만, 지금까지 저희가 사용한 내용을 보셨듯이 annotation을 이용한 bean의 등록 역시 가능합니다. 이번에 사용한 내용 그대로, xml과 annotation을 혼용하는 것이 일반적입니다. 그리고 아직까지 한번도 사용안해본 xml을 전혀 사용하지 않는 ApplicationContext역시 가능합니다. 

먼저 ApplicationContext를 수동으로 만들어보는 것을 알아보겠습니다.

프로젝트를 만듭니다. 지금까지 구성하던것 처럼, maven-archetype-quickstart로 maven project를 하나 생성합니다. 생성된 project에 spring context 선언을 pom.xml에 추가합니다.

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>


Hello 객체와 Printer interface를 선언하고 ConsolePrinter 객체를 만들어보도록 합시다.

public class Hello {
    private String name;
    private Printer printer;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Printer getPrinter() {
        return printer;
    }
    public void setPrinter(Printer printer) {
        this.printer = printer;
    }
    public String sayHello() {
        return "Hello " + name;
    }
    public void print() {
        this.printer.print(sayHello());
    }
}

public interface Printer {
    void print(String message);
}

public class ConsolePrinter implements Printer {
    public void print(String message) {
        System.out.println(message);
    }
}


먼저, Spring에서 제공하는 StaticApplicationContext를 사용해서 ApplicationContext에 직접 bean을 등록하고 얻어보는 것을 해보도록 하겠습니다.

테스트 코드를 간단히 작성해보도록 하겠습니다.

    @Test
    public void registerApplicationContext() {
        StaticApplicationContext ac = new StaticApplicationContext();
        ac.registerSingleton("hello1", Hello.class);
        Hello hello = ac.getBean("hello1", Hello.class);
        assertNotNull(hello);
    }

 
이렇게 만들어진 ApplicationContext에서 hello1이라는 이름의 객체를 계속해서 얻어오는 것이 가능합니다. 하나 재미있는 것이 다음 코드입니다. 

    @Test
    public void registerApplicationContext() {
        StaticApplicationContext ac = new StaticApplicationContext();
        ac.registerSingleton("hello1", Hello.class);
        Hello hello = ac.getBean("hello1", Hello.class);
        assertNotNull(hello);
        Hello hello2 = ac.getBean("hello1", Hello.class);
        assertThat(hello, is(hello2));
    }

    @Test
    public void registerApplicationContextWithPrototype() {
        StaticApplicationContext ac = new StaticApplicationContext();
        ac.registerPrototype("hello1", Hello.class);
        Hello hello = ac.getBean("hello1", Hello.class);
        assertNotNull(hello);
        Hello hello2 = ac.getBean("hello1", Hello.class);
        assertThat(hello, is(not(hello2)));
    }

registerApplicationContext와 registerApplicationContextWithProtytype은 Hello 객체를 Singleton 또는 Prototype으로 등록하게 됩니다. Singleton은 ApplicationContext에 등록된 모든 객체들을 재사용하게 되는데, registerPrototype으로 등록된 객체들은 ApplicationContext에서 얻어낼 때마다 객체를 다시 생성해서 얻어내게 됩니다. 이는 xml의 설정에서 scope와 동일합니다. 위 코드는 다음 xml로 표현이 가능합니다. 

<bean id="hello1" class="com.xyzlast.ac.Hello" scope="singleton"/>
<bean id="hello2" class="com.xyzlast.ac.Hello" scope="prototype"/>


이제 bean에 대한 property를 설정하는 코드에 대해서 알아보도록 하겠습니다. Property를 설정하기 위해서는 BeanDefinition 객체를 사용해야지 됩니다. 테스트 코드를 보면서 간단히 확인해보도록 하겠습니다. 

   @Test
    public void registerBeanDef() {
        BeanDefinition helloDef = new RootBeanDefinition(Hello.class);
        helloDef.getPropertyValues().add("name", "ykyoon");
        helloDef.getPropertyValues().add("printer", new ConsolePrinter());
        StaticApplicationContext ac = new StaticApplicationContext();
        ac.registerBeanDefinition("hello1", helloDef);
        
        Hello hello = ac.getBean("hello1", Hello.class);
        assertNotNull(hello);
        assertThat(hello.sayHello(), is("Hello ykyoon"));
    }


ApplicationContext의 종류

spring에서는 십여개의 applicationContext를 제공하고 있습니다. 다음은 Spring에서 제공하는 applicationContext의 종류입니다. 





StaticApplicationContext
Test code에서 사용한 ApplicationContext입니다. 실 프로젝트에서는 절대로 사용되지 않는 ApplicationContext입니다. xml로딩 기능이나 그런것들도 없고, 테스트 코드에서 사용해보신 것과 같이 객체에 대한 IoC 기능의 테스트에서만 사용되는 객체입니다.

GenericApplicationContext
xml을 이용하는 ApplicationContext입니다. 우리가 만든 xml 파일과 동일한 xml을 이용한 ApplicationContext 구성이 가능합니다. 지금까지 사용한 Test code에서 사용된 ApplicationContext가 모두 GenericApplicationContext입니다.

GenericXmlApplicationContext
GenericApplicationContext의 확장판입니다. xml 파일의 경로가 생성자에 들어가 있어서, 좀더 편하게 xml을 로딩할 수 있는 장점 이외의 차이점은 없습니다.

WebApplicationContext
web project에서 사용되는 ApplicationContext입니다. web.xml의 org.springframework.web.context.ContextLoaderListener를 통해서 로드 및 생성이 됩니다.

이 4개의 ApplicationContext는 매우 자주 사용되는 형태입니다. 각각의 간단한 특징들만 알아두는것이 좋습니다.


ApplicationContext의 계층 구조


spring forum에서 가장 많이 나오는 질문들이 있습니다.
"bean 을 등록했는데, 사용할 수가 없어요."
Bean이 xml에 등록이 되어있으나, 사용하지 못하는 경우가 간간히 나옵니다. 그 이유는 Spring에 있는것이 아니라 Bean의 계층구조를 이해하지 못하고 Bean을 등록해서 사용하고 있기 때문입니다. 

ApplicationContext는 계층 구조를 가질 수 있습니다. 다음과 같은 구조화가 가능합니다.

여기서 주의할 점은 형제 node끼리는 bean을 검색할 수 없습니다. upper node에 있는 객체와 자신의 객체만을 사용할 수 있고, 형제 node에 있는 bean들은 검색할 수 없습니다. 그리고, upper node에 있는 bean 이름과 동일한 bean 이름을 갖는 객체를 선언하면, 자식의 node에 있는 객체로 덧씌워져 버립니다. 이런 계층구조 사이의 혼란한 bean 구조는 매우 힘든 버그를 발생시킬 수 있습니다. bean을 정의할 때, 이런 부분을 주의해야지 될 필요성이 있습니다. 

그럼, 이런 ApplicationContext간의 계층구조는 왜 만들게 되는지가 궁금해질 수 가 있습니다.
이 부분은 webApplicationContext를 만들때 이런 구조가 만들어집니다. Spring Web MVC를 사용하는 경우, Root ApplicationContext는 web.xml에 정의되고 로드됩니다. 그리고 Servlet을 정의하고 DispatcherServlet을 사용하면, DispatcherServlet에서 사용되는 child ApplicationContext가 로드가 되게 됩니다. Spring Web은 기본적으로 Front Controller Pattern을 사용하기 때문에 child ApplicationContext가 하나만 로드가 되는 것이 일반적이지만, 간혹 경우에 따라 child ApplicationContext를 여러개를 로드시켜야지 되는 때가 있습니다. 각 url에 따라 다른 Servlet을 사용하고 싶은 경우도 생길수 있으니까요. 그때는 여러개의 형제 ApplicationContext가 만들어지게 되고 이 ApplicationContext는 서로간에 bean을 사용할 수 없게 됩니다. 그리고, 각 ApplicationContext에서 따로 bean을 등록하는 경우에는 각각 다른 bean 정보를 갖게 됩니다. 

다음은 Web application에서 ApplicationContext의 기본 구조입니다.





ApplicationContext.xml의 등록 방법

applicationContext 의 등록방법은 spring에서 계속해서 발전되어가는 분야중 하나입니다. 총 3가지의 방법으로 나눌 수 있으며, 이 방법들은 같이 사용되는 것도 가능합니다. 

1. applicationContext.xml 을 이용하는 방법
bookDao를 이용할 때, 처음 사용한 방법입니다. bean을 선언하고, id값을 이용해서 사용하는 방법으로 이 방법은 가장 오랫동안 사용해왔기 때문에 많은 reference들이 존재합니다. 그렇지만, 객체가 많아질수록 파일의 길이가 너무나 길어지고 관리가 힘들어지는 단점 때문에 요즘은 잘 사용되지 않습니다. 

2. @annotation과 aplicationContext.xml을 이용하는 방법
@Component, @Repository, @Service, @Controller, @Value 등을 사용하고, component-scan 을 이용해서 applicationContext.xml에서 등록하는 방법입니다. 이 방법은 지금 가장 많이 사용되고 있는 방법입니다. applicationContext.xml의 길이가 적당히 길고, 구성하기 편하다는 장점을 가지고 있습니다. 
이 방법은 반드시 알아둬야지 됩니다. 지금 정부표준 프레임워크 및 대부분의 환경에서 이 방법을 사용하고 있습니다.

3. @Configuration을 이용한 applicationContext 객체를 이용하는 방법
Spring에서 근간에 밀고 있는 방법입니다. 이 방법은 다음과 같은 장점을 가지고 있습니다. 

1) 이해하기 쉽습니다. - xml에 비해서 사용하기 쉽습니다.
2) 복잡한 Bean 설정이나 초기화 작업을 손쉽게 적용할 수 있습니다. - 프로그래밍 적으로 만들기 때문에, 개발자가 다룰수 있는 영역이 늘어납니다.
3) 작성 속도가 빠릅니다. - eclipse에서 java coding 하는것과 동일하게 작성하기 때문에 작성이 용의합니다. 

그리고, 개인적으로는 2, 3번 방법을 모두 알아둬야지 된다고 생각합니다. 이유는 2번 방법의 경우, 가장 많이 사용되고 있다는 점이 가장 큰 장점입니다. 또한 아직 Spring의 하부 Project인, Spring Security를 비롯하여 Work Flow등은 아직 3번 방법을 지원하지 않습니다. (다음 버젼에서 지원 예정입니다.) 그래도 3번 방법을 알아야지 됩니다. 이유는 다음과 같습니다. 지금 Spring에서 밀고 있습니다. 그리고, 최근에 나온 외국 서적들이 모두 이 방법을 기준으로 책을 기술하고 있습니다. 마지막으로, 나중에 나올 web application의 가장 핵심인 web.xml이 없는 개발 방법이 servlet 3.0에서 지원되기 시작했습니다. Java 언어에서 xml을 이용한 설정 부분을 배재하는 분위기로 흘러가고 있다는 것이 제 개인적인 판단입니다. 

그럼, 이 3가지 방법을 모두 이용해서 기존의 BookStore를 등록하는 applicationContext를 한번 살펴보도록 하겠습니다.

applicationContext.xml만을 이용하는 방법
초기 Spring 2.5 이하 버젼에서 지원하던 방법입니다. 일명 xml 지옥이라고 불리우는 어마어마한 xml을 자랑했습니다. 최종적으로 만들어져 있는 applicationContext.xml의 구성은 다음과 같습니다. Transaction annotation이 적용되지 않기 때문에, Transaction에 대한 AOP code 까지 추가되는 엄청나게 긴 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: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">
  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <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>
  <context:property-placeholder location="classpath:spring.properties" />
  <bean id="bookDao" class="com.xyzlast.bookstore02.dao.BookDaoImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="userDao" class="com.xyzlast.bookstore02.dao.UserDaoImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="historyDao" class="com.xyzlast.bookstore02.dao.HistoryDaoImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="userService" class="com.xyzlast.bookstore02.services.UserServiceImpl">
    <property name="bookDao" ref="bookDao"/>
    <property name="userDao" ref="userDao"/>
    <property name="historyDao" ref="historyDao"/>
  </bean>
  <bean id="bookService" class="com.xyzlast.bookstore02.services.HistoryServiceImpl">
    <property name="bookDao" ref="bookDao"/>
    <property name="userDao" ref="userDao"/>
    <property name="historyDao" ref="historyDao"/>
  </bean>
  
  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  <bean id="transactionManagerAdvisor" class="com.xyzlast.bookstore02.utils.TransactionAdvisor">
    <property name="transactionManager" ref="transactionManager"/>
  </bean>
  
  <bean
    class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames">
      <list>
        <value>userService</value>
        <value>bookService</value>
      </list>
    </property>
    <property name="interceptorNames">
      <list>
        <value>transactionManagerAdvisor</value>
      </list>
    </property>
  </bean>
</beans>

@annotation + applicationContext.xml을 이용한 방법
@Repository, @Component, @Service를 이용해서 편한 방법을 제공합니다. 특히 component-scan과 @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: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">
  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <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>
  <context:property-placeholder location="classpath:spring.properties" />
  <context:component-scan base-package="com.xyzlast.bookstore02.dao" />
  <context:component-scan base-package="com.xyzlast.bookstore02.services" />
  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

@annotation + @Configuration을 이용한 방법
이 방법은 xml을 아애 없애버릴 수 있습니다. xml이 제거된 ApplicationContext의 내용은 다음과 같습니다.
@Configuration
@PropertySource("classpath:spring.properties")
@ComponentScan(basePackages = {"com.xyzlast.bookstore02.dao", "com.xyzlast.bookstore02.services"})
@EnableTransactionManagement
public class BookStoreConfiguration {
    @Autowired
    private Environment env;

    @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 JdbcTemplate jdbcTemplate() {
        JdbcTemplate template = new JdbcTemplate();
        template.setDataSource(dataSource());
        return template;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource());
        return transactionManager;
    }
}

지금까지 작성하던 xml과는 완전히 다른 모습의 ApplicationContext입니다. ApplicationContext Config 객체는 다음과 같은 특성을 갖습니다. 

1. @Configuration annotation을 갖는다.
2. bean으로 등록되는 객체는 method로 관리되고, @Bean annotation을 갖습니다.
3. method의 이름이 <bean id="">값과 매칭됩니다.
4. component-scan, property-place-holder의 경우, class의 annotation으로 갖습니다.
5. @EnableTransactionManagement와 같이 @Enable** 로 시작되는 annotation을 이용해서 전역 annotation을 구성합니다. 
6. Properties 파일을 사용하기 위해서는 반드시 static method로 PropertySourcesPlaceholderConfigurer를 return 시켜줘야지 됩니다.

앞으로 적용되는 모든 Project는 @Configuration을 이용한 3번 방법으로 구성하도록 하겠습니다. 그리고 xml 역시 같이 소개하도록 하겠습니다.

Summay

지금까지 사용하던 ApplicationContext에 대한 기본 개념을 정리해봤습니다. ApplicationContext는 Spring의 핵심 기능입니다. DI를 통한 IoC를 가능하게 하고, AOP에 대한 설정 등 모든 Spring에서 하는 일이 설정되어 있는 것이 ApplicaionContext라고 할 수 있습니다. 이에 대한 설정 및 구성을 명확하게 알아놓을 필요가 있습니다. 그리고, 내부에서 어떤 일을 해서 Spring에서 하는 이런 일들이 가능하게 되는지에 대한 이해가 필요합니다. 마지막으로 ApplicationContext를 구성하는 방법에 대해서 알아봤습니다. 제공되는 3가지 방법에 대해 자유자재로 사용할 수 있는 능력이 필요합니다.

감사합니다. 




Posted by Y2K
,

7. AOP

Java 2013. 9. 9. 10:54

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



lombok의 소개

지금까지 많은 property를 작성하다보면 코드의 작성이 매우 짜증이 나는 경우가 많습니다. 특히 Property의 경우에는 매우 반복적인 코드를 적게 되는 것이 일반적입니다. 그래서 이 부분에 대해서 보다 획기적인 방법이 없을까.. 하는 개발자들의 노력으로 바로 이런것이 나왔습니다. 
어마어마한 기능을 가지고 있습니다. 

http://projectlombok.org/  에서 받을 수 있습니다.

eclipse에 설치 후 (설치는 더블 클릭만 하면 됩니다.), pom.xml에 반드시 아래 코드를 추가하셔야지 됩니다. 

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>0.11.6</version>
      <scope>provide</scope>
    </dependency>


AOP에 대해서 설명을 하기 위해서는 먼저, 기존의 코드를 다시 한번 볼 필요가 있습니다.

    @Override
    public boolean rent(int userId, int bookId) {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            User user = userDao.get(userId);
            Book book = bookDao.get(bookId);
    
            book.setRentUserId(user.getId());
            book.setStatus(BookStatus.RentNow);
            bookDao.update(book);
            
            user.setPoint(user.getPoint() + 10);
            user.setLevel(userLevelLocator.getUserLevel(user.getPoint()));
    
            UserHistory history = new UserHistory();
            history.setUserId(userId);
            history.setBookId(book.getId());
            history.setAction(HistoryActionType.RENT);
            
            userDao.update(user);
            userHistoryDao.add(history);
            this.transactionManager.commit(status);
            return true;
        } catch(Exception ex) {
            this.transactionManager.rollback(status);
            throw ex;
        }
    }

위 코드를 보면 Transaction의 경계설정 코드와 BL 코드가 복잡하고 연결이 되어있는것 같이 보이지만, 명확하게 코드는 분리가 될 수 있습니다. transactionManager를 사용하는 코드와 BL 코드로 분리가 깔끔하게 가능합니다. 여기서 전에 사용한 Template-callback 으로 변경시키는 것 역시 쉽게 가능합니다.

    public boolean rent(final int userId, final int bookId) {
        doBLWithTransaction(new BusinessLogic() {
            @Override
            public void doProcess() {
                User user = userDao.get(userId);
                Book book = bookDao.get(bookId);
        
                book.setRentUserId(user.getId());
                book.setStatus(BookStatus.RentNow);
                bookDao.update(book);
                
                user.setPoint(user.getPoint() + 10);
                user.setLevel(userLevelLocator.getUserLevel(user.getPoint()));
        
                UserHistory history = new UserHistory();
                history.setUserId(userId);
                history.setBookId(book.getId());
                history.setAction(HistoryActionType.RENT);
                
                userDao.update(user);
                userHistoryDao.add(history);
            }
        });
        return true;
    }
    
    interface BusinessLogic {
        void doProcess();
    }
    
    private void doBLWithTransaction(BusinessLogic loc) {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            loc.doProcess();
            this.transactionManager.commit(status);
        } catch(Exception ex) {
            this.transactionManager.rollback(status);
            throw ex;
        }
    }

그런데, 조금 걸리는 것이 지금까지 만들어진 모든 코드는 경계가 매우 확실합니다. DB에 대한 내용은 DAO 측으로, BL측은 Service로 분리가 되었지만 Transaction에 대한 코드가 이곳에 들어가게 되는 것이 조금 안좋아보이긴 합니다. 그리고, 이런 코드는 계속되는 반복에 의해서 만들어지기 때문에 코드를 관리하는데 조금 복잡해보이는것이 사실입니다. 이와 같이 분리된 코드를 도식화 하면 다음과 같습니다.

그리고 Transaction에 대한 코드를 아애 분리를 시켜주는것도 가능합니다. BL에 대한 코드와 Transaction을 갖는 코드로서 분리를 하는것이 가능하지요. 다음은 분리된 서비스들을 보여줍니다. 


public class UserServiceTx implements UserService {
    @Autowired
    PlatformTransactionManager transactionManager;
    @Autowired
    UserService userServiceImpl;
    
    interface BusinessLogic {
        void doProcess();
    }
    
    private void doBLWithTransaction(BusinessLogic loc) {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            loc.doProcess();
            this.transactionManager.commit(status);
        } catch(Exception ex) {
            this.transactionManager.rollback(status);
            throw ex;
        }
    }
    
    @Override
    public boolean rent(final int userId, final int bookId) {
        doBLWithTransaction(new BusinessLogic() {
            @Override
            public void doProcess() {
                userService.rent(userId, bookId);
            }
        });
        return true;
    }


위의 코드는 실제 구현된 UserServiceImpl을 한번 감싼 UserService를 또 만드는겁니다. UserServiceTx를 보면 실행하는 입장에서는 다음과 같이 사용하게 됩니다.




이와 같은 개발 패턴을 proxy pattern이라고 합니다. proxy pattern의 정의는 다음과 같습니다. 

Proxy패턴은 object를 호출할 때, 부가적인 처리를 실행할 수 있도록 하기 위한 패턴입니다. 이와 동시에 그 오브젝트의 이용자(클라이언트)에 대한 변경을 최소화할 수도 있다. Proxy 패턴을 잘 사용한다면, 어플리케이션의 사용편리성을 향상시키는데 이용됩니다. 

말이 어렵습니다. 간단히 말을 풀면, UserService를 실행하는 Test 객체 입장에선 UserServiceImpl을 실행을 하나, UserServiceTx를 실행하나 동일한 interface를 실행하게 됩니다.  동일한 Interface로 접근을 하되, 다른 객체를 통해서 원 객체를 접근하게 만드는 방식을 Proxy pattern 이라고 합니다. 

또한, Proxy pattern은 거의 또 다른 pattern을 동시에 실행하게 됩니다. 그건 decorate pattern 입니다. decorate patten의 정의는 다음과 같습니다. 

기능의 '장식'(Decorator)이라는 개념을 클래스로 만들어 분화시킨 후, 이를 기존 객체가 참조하는 형태로 덧붙여나갈 수 있게 한다. Decorator와 꾸미는 대상이 되는 클래스는 같은 기반 클래스를 상속받는다. 그리고 두 클래스 모두 새로운Decorator 객체를 참조할 수 있는 구조로 만든다.

말이 어렵습니다. decorate pattern은 동일한 interface에 접근되는 객체에 새로운 기능이 추가되는 것을 decorate pattern이라고 합니다. proxy pattern과 decorate pattern은 매우 헛갈리면서 같이 사용이 되는 것이 일반적입니다. 접근되는 객체에 대한 관점은 proxy pattern이라고 생각하시면 되고, decorate pattern은 접근된 기능에 추가 기능을 같이 실행시키는 것을 의미합니다. 

위 객체를 다시 설명을 하면, UserServiceTx라는 Proxy를 만들어서 UserServiceImpl에 접근을 하게 되고, UserServiceTx는 decorate pattern을 이용해서 transaction 기능을 추가하고 있습니다. 

최종적으로, Spring의 @Transaction 역시 Proxy Pattern과 Decorate Pattern이 결합된 형태로 구성되어 있습니다. Transaction 기능을 추가하기 위해서 Decorate Pattern으로 기능이 추가 되어 있으며, 실 객체가 아닌 (예제에서는 UserServiceImpl) Decorate가 추가된 객체에 접근하도록 객체의 접근을 제어한 Proxy Pattern이 결합되어 있습니다. 이 부분을 구현하는 Java의 기본 코드 구조를 Dynamic Proxy라고 합니다. 



Dynamic Proxy


이러한 Proxy 객체를 어떻게 만들어주게 되는 걸까요? 지금 저희 코드는 UserServiceTx라는 Proxy를 작성해줬습니다. Spring과 같은 Framework는 범용적이며, 가변적인 Proxy를 만드는 방법들을 가지고 있습니다. 가장 대표적인 것은 relection입니다. Spring은 기본적으로 reflection을 통해서 이 부분을 처리합니다. 이 부분에 대해서 좀 더 깊게 들어가보도록 하겠습니다. 먼저 간단한 Relection 코드를 확인해보도록 하겠습니다.

    @Test
    public void invokeMethod() throws Exception {
        String name = "ykyoon";
        
        Method lengthMethod = String.class.getMethod("length");
        assertThat((int) lengthMethod.invoke(name), is(6));
    }

위 코드를 보시면 좀 재미있는 내용들이 보이게 됩니다. String 객체의 length method를 문자열로 mehtod의 이름을 이용해서 호출할 수 있는 것을 확인할 수 있는데요. 
java의 Method object는 각 객체의 method에 대한 정의를 갖는 객체입니다. 이를 이용해서 dynamic한 method 호출을 할 수 있지요. 이제 본격적인 Relection 코드를 확인해보도록 하겠습니다. 

Dynamic Proxy에 대해서 깊게 알아보기 위해서 가장 먼저 알아봐야지 될 내용은  InvocationHandler interface입니다. InvocationHandler는 Proxy.newInstance에서 새로운 객체를 만들고, 그 객체에 Proxy를 통해서 접근하는 방식을 제공하는 interface입니다. 

한번 예시를 사용해보도록 하겠습니다. InvocationHandler를 이용해서 객체의 모든 String output을 대문자로 자동으로 변경시키는 Proxy를 만들어보도록 하겠습니다. 

먼저, Proxy의 target이 되는 interface와 객체입니다. 

public interface Hello {
    String sayHello(String name);
    String sayHi(String name);
    String sayThankYou(String name);
}

public class HelloImpl implements Hello {
    @Override
    public String sayHello(String name) {
        return "Hello " + name + "!!";
    }
    @Override
    public String sayHi(String name) {
        return "Hi " + name + "!!";
    }
    @Override
    public String sayThankYou(String name) {
        return "Thank you. " + name + "!!";
    }
}

그리고, 모든 output을 UpperCast로 변경시키는 InvocationHandler를 구성하도록 하겠습니다. 

public class UppercaseHandler implements InvocationHandler {
    private final Object target;
    
    public UppercaseHandler(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        String ret = (String) method.invoke(this.target, args);
        return ret.toUpperCase();
    }
}


코드는 매우 단순합니다. 

그리고, Hello interface를 통해서 HelloImpl로 접근하는 Proxy 객체를 만들어보도록 하겠습니다.  HelloImpl에 대한 test 를 작성해서 다음 코드를 실행해보면 다음 결과를 얻어낼 수 있습니다. 

    @Test
    public void buildHelloImplWithProxy() {
        Hello proxiedHello = (Hello) Proxy.newProxyInstance(getClass().getClassLoader(),
                new Class[] { Hello.class},
                new UppercaseHandler(new HelloImpl()));

        String output = proxiedHello.sayHello("ykyoon");
        System.out.println("Output은 다음과 같습니다. : " + output);
        System.out.println("Proxy의 이름은 다음과 같습니다. : " + proxiedHello.getClass().getName());
    }


Output은 다음과 같습니다. : HELLO. YKYOON
Proxy의 이름은 다음과 같습니다. : $Proxy4

Spring에서 @Transaction에서 보시던 결과가 나왔습니다.! Spring에서 @Transaction은 다음과 같은 코드로 구성되어 있습니다.  (Sudo code입니다.)

public class SpringTransactionProxy implements InvocationHandler {
    private final Object targetService;
    @Autowired
    private PlatformTransactionManager transactionManager;
    
    public SpringTransactionProxy(Object targetService) {
        this.targetService = targetService;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        TransactionStatus status = transactionManager.beginTransaction(new DefaultTransaction());
        try {
            method.invoke(this.targetService, args);
            transactionManager.commit(status);
        } catch(Exception ex) {
            transactionManager.rollback(status);
            throw ex;
        }
    }
}

잘 보시면, 기존의 template-callback pattern과 유사한 코드가 만들어집니다. 단, 구현 방법이 InvocationHandler를 이용한 Proxy객체를 만들어서, 그 객체를 통해 method의 전/후에 특정한 action을 집어 넣고 있는 방식을 사용하고 있는 것입니다. 이렇게 method의 전/후에 특정 action을 삽입하는 개발 방법을 AOP(aspect oriented programming)라고 합니다.


AOP

Spring은 Transaction 뿐 아니라, 다른 method들에서도 이와 같은 Decorate와 Proxy patten들을 사용할 수 있는 멋진 방법들을 제공합니다. 지금까지 보던 Transaction에 대한 코드를 보시면 Transaction의 대상이 되는 method의 전/후에 transaction.begin() / commit()이 실행되고 있습니다. 좀더 이것을 발전시킨다면 특정 method가 시작되기 전에, 또는 method가 실행 된 후에,아니면 method에서 exception이 발생한 후에 수행되는 특정 InvocationHandler를 지정해주는 것도 가능하지 않을까? 라는 의문이 생깁니다. 이러한 개발 방법을 이용하면, 지금까지 공부해왔던 OOP에 대한 다른 개념으로 발전하게 됩니다. 

OOP를 공부하면 가장 많이 나오는 말은 상속 그리고 구현입니다. extends, implements에 대한 이야기들이 대부분이지요. 이는 object의 종적인 연결관계를 나타냅니다. 그렇지만, 이와 같이 method가 실행되기전, 후, exception에 대해서 보게 된다면 method의 횡적인 연결관계에 대한 논의를 해야지 됩니다. 





이러한 개념은 여러 곳에서 사용될 수 있습니다. 예를 들어...
# 사용자의 권한 체크
# In/Out에 대한 로그 기능
# Return값에 특정한 값이 있는 경우에 공통되는 Action을 해야지 되는 경우
# Transaction과 같은 method의 실행에 있어서 전/후 처리를 해줘야지 될때
# Return값에 특정한 데이터 양식을 추가해야지 될 때
# Exception의 처리

제가 생각하는 기능들은 이정도인데, 다른 분들은 어떤 기능을 추가할 수 있을까요?

AOP의 확장은 거의 무한대에 가깝습니다. 기존의 OOP 적 사고방식을 크게 확장시킬 수 있는 개념이기도 하면서, 기존의 OOP의 설계를 무너트릴수도 있는 개념입니다. OOP는 종적, AOP는 횡적 객체의 확장이다. 라고 이해를 하시고 다음 용어들을 이해하시면 좀 더 이해가 편할 것 같습니다. 

Target
AOP의 Target이 되는 객체입니다. 지금까지 만들었던 Service객체 또는 HelloImpl과 같이 직접 AOP 당하는 객체를 Target이라고 합니다.

Advice
위에 구성된 InvocationHandler에 의해서 사용될 interface가 구현된 객체입니다. 다른 객체에 추가적인 action을 확장시키기 위한 객체입니다. Spring은 총 3개의 Advice interface를 제공하고 있습니다. 아래의 interface를 상속받아 Advice를 구성합니다.

namedescription
MethodBeforeAdvicemethod가 실행되기 전에 실행되는 Advicer를 구성합니다.
AfterReturningAdvicemethod가 실행 후, return 값을 보내기 직전에 실행되는 Advicer를 구성합니다.
MethodInterceptormethod의 전/후 모두에 실행 가능한 Advicer를 구성합니다.


Pointcut
Advice를 적용할 Point를 선별하는 작업을 하는 객체를 말합니다. Spring에서는 @Transactional이 적용된 모든 method에 대한 Transaction Advice의 Pointcut은 
<tx:annotation-driven transaction-manager="transactionManager" />
으로 선언되고 있습니다.

Advisor
Advisor = Advice + PointCut
이라고 생각하시면 됩니다. 부가기능 + 적용대상이 반영된 interface라고 하면 설명이 좀더 쉽습니다. 

기존 InvocationHandler가 아닌 MethodInterceptor를 이용한 Advice를 구성하는 코드를 한번 알아보도록 하겠습니다. 

public class UppercastAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("method pre in : " + invocation.getMethod().getName());
        Object ret = invocation.proceed();
        System.out.println("method after : " + invocation.getMethod().getName());
        return ret;
    }
}

다음과 같은 코드로 구성이 되게 됩니다. method의 pre/after를 잡아서 AOP를 구성하는 것이 가능합니다. 여기에 PointCut을 적용하면 다음과 같습니다. 
    @Test
    public void pointcutAdvisor() {
        ProxyFactoryBean pfBean = new ProxyFactoryBean();
        pfBean.setTarget(new HelloImpl());
        
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedName("sayH*");
        pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercastAdvisor()));
        
        Hello proxiedHello = (Hello) pfBean.getObject();
        proxiedHello.sayHello("ykyoon");
        proxiedHello.sayHi("ykyoon");
        proxiedHello.sayThankYou("ykyoon");
    }


실행 결과는 다음과 같습니다. 

method pre in : sayHello
method after : sayHello
method pre in : sayHi
method after : sayHi

결과를 보시면, sayThankYou는 Pointcut에 포함되지 않기 때문에 실행이 되지 않는 것을 볼 수 있습니다. 


ApplicationContext를 이용한 AOP의 설정

ApplicationContext.xml을 이용한 Spring AOP를 설정하는 방법을 알아보도록 하겠습니다. 가장 많이 사용되는 bean의 이름을 이용해서 일괄적인 AOP를 설정하는 sample code를 작성해보도록 하겠습니다.  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:aop="http://www.springframework.org/schema/aop"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
        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">
    <context:component-scan base-package="com.xyzlast.mvc.ac"/>
  <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames">
      <list>
        <value>helloImpl</value>
      </list>
    </property>
    <property name="interceptorNames">
      <list>
        <value>uppercastAdvice</value>
      </list>
    </property>
  </bean></beans>

간단한 테스트 코드를 작성해보면 다음과 같은 결과를 볼 수 있습니다. 
@SuppressWarnings("unused")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class HelloImplWithAOPTest {
    @Autowired
    private Hello hello;

    @Test
    public void doProcessWithAop() {
        hello.sayHello("ykyoon");
    }
}

method pre in : sayHello
method after : sayHello

위 코드는 bean의 이름으로 AOP의 pointcut을 작성한 사례입니다. 이 부분에 있어서는 다양한 방법으로 AOP가 가능합니다. Bean의 이름 뿐 아니라 method의 이름, 상속받는 객체에 따른 AOP 등 다양한 방식의 AOP가 가능합니다.


Summary

AOP는 spring에서 매우 중요한 개념입니다. 무엇보다 OOP의 한계인 횡적인 객체의 확장이 가능하게 하는 놀라운 기술중 하나입니다. 그렇지만, 알아야지 될 내용들도 무척 많습니다. 테스트 코드에서는 NameMatchMethodPointcut 만을 사용했지만, Pointcut의 종류가 매우 많습니다. Pointcut에 따라, 특정 객체, method, 그리고 advice에 따라 method의 실행 전/후를 결정을 해서 많은 일들을 할 수 있습니다.

개발을 하다보면 좀더 많은 활용법을 같이 생각해볼 수 있는 구조입니다. 꼭 깊게 공부를 해보시길 바랍니다. 그리고 추가로 이야기드릴것이 Spring은 AspectJ라는 AOP 개발 기법을 지원합니다. 지금까지 proxy를 이용한 AOP 방법이였다면, AspectJ는 compile 시에 기존 class를 변경시켜서 새로운 class를 만들어줍니다. 아애 변형된 객체를 구성시켜주는 것이지요. 이는 성능상에서 이익을 가지고 오고, AOP의 pointcut과 acdvice에 대한 테스팅이 매우 쉬운 장점을 가지고 있습니다. AspectJ에 대해서는 개인적인 공부를 좀 더 해주시길 바랍니다.

이제 JDBC를 직접 이용하는 Simple application의 제작이 모두 마쳐졌습니다. 모두들 수고하셨습니다. 기존의 버릇과는 다른 개발 방법에 대해 고민을 많이 하셨을 것 같은데, 이쪽까지 잘 해주셔서 정말 감사드립니다. 지금까지 하신 내용을 잊어먹지는 말아주세요. 잠시 2개의 chapter 정도는 이론을 좀 더 깊게 들어가보도록 하겠습니다. 




Posted by Y2K
,

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



DataSource의 변경

DriverManagerDataSource는 매우 단순한 DataSource입니다. 1개의 Connection에 대한 open/close만을 담당하고 있는 매우 단순한 형태의 DataSource입니다. 실무에서는 절대로 사용되면 안되는 형태의 DataSource이기도 합니다. 실무에서는 Connection Pool을 이용해서 DB에 대한 연결을 담당합니다. Connection Pool은 미리 Connection을 준비해두고, dao에서 사용된 Connection을 Pool에 넣어서 관리하는 형태입니다. 이 방식은 다음과 같은 장점을 갖습니다. 

# DB에 대한 가장 큰 부하인 Connection open / close 횟수를 줄여, System의 부하를 줄일수 있습니다.
# Web과 같은 동시접근성이 보장되어야지 되는 시스템에서 Connection의 여유분을 만들어서, 시스템의 성능을 높일 수 있습니다.
# DB System에 대한 max connection 숫자를 파악할 수 있기 때문에, DB System에 대한 부하 및 성능에 대한 예측이 가능합니다. 

실무에서는 무조건! Connection Pool을 사용해야지 됩니다. java 진영에서 주로 사용되는 connection pool에는 다음 두가지가 있습니다.

# c3p0
# BoneCP

둘에 대한 간단한 설명을 하자면, c3p0의 경우, Hibernate에서 기본으로 사용되는 Connnection Pool입니다. 오래된 Connection Pool이기도 합니다. 다만, 근간에는 DB Connection의 Deadlock 문제가 간간히 발표가 되고 있어, BoonCP에 비하여 밀리고 있는 것이 사실입니다. BoneCP는 아직까지 DB Connection에 대한 Deadlock이 보고된 적은 없습니다. BoonCP를 이용한 DataSource는 다음과 같이 구성이 됩니다. 

    <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>

DataSource를 BoneCP로 변경해서 사용해보도록 하겠습니다. 

  <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>

기본적으로 가지고 있던 driverClass, jdbcUrl, username, password의 경우에는 완전히 동일합니다. 다른 여러가지 property들이 추가로 들어가게 되는데 각 property들은 다음과 같은 의미를 가지고 있습니다. 

# idleConnectionTestPeriodInMinutes : connection이 쉬고 있는 상태인지 확인하는 주기입니다.
# idleMaxAgeInMinutes : connection이 유휴상태로 놓어지는 최대시간입니다. 이 시간 이후, connection은 소멸됩니다.
# maxConnectionsPerPartition : Partition 당 최대 connection의 갯수입니다.
# minConnectionsPerPartition : Partition 당 최소 connection의 갯수입니다. (DB에 연결되는 최소 connection의 갯수는 partitionCount * minConnectionsPerPartition이 됩니다.)
# partitionCount : partition 갯수입니다. BoneCP는 partition 이라는 개념으로 Connection Pool의 영역을 나눠서 관리를 합니다. 
# acquireIncrement : 한번에 connection을 얻어낼 때, 얻어내는 숫자입니다. 
# releaseHelperThread : connection의 반환시에 사용되는 thread의 갯수입니다. 

spring에 BooneCPDataSource를 추가 후에, 기존 DataSource의 spring bean 설정을 제거하면 기존 코드의 아무런 변경이 없이, BoneCP가 적용되고 있습니다. 이는 interface가 잘 되어 있는 객체의 활용이 되며, DI에 의한 IoC의 대표적 한 예가 될 수 있을 것 같습니다.


Service의 구성

지금까지 Dao Layer를 구성하는 방법과 Spring을 통해 bean application Context를 구성하는 방법에 대해서 알아봤습니다. Dao Layer는 일반적으로 Table에 대한 CRUD를 지원하게 되는 것이 일반적이며, Day Layer는 Book, User, History와 같은 entity객체들을 return 시켜주는 것이 일반적인 개발 방법입니다. 마지막으로, Service Layer는 일반적인 Business Logic을 구성하는 layer입니다. 일반적으로 Service Layer는 여러 Dao를 이용해서 많은 Table에 CRUD를 동시에 하게 되고, 그 결과를 Controller Layer에 전달하는 구조로 구성되게 됩니다. 

지금까지 구성된 bookstore는 다음과 같은 Business Logic을 가질 예정입니다. 

# user는 book을 빌리거나 반납할 수 있다.
# user가 book을 빌리면, point가 10점씩 쌓인다.
# point가 100점이 넘어가면 READER가 된다.
# point가 300점이 넘어가면 MVP가 된다.
# point가 100점 이하인 경우, 일반유저(NORMAL)이다.
# 전체 book을 list up 할 수 있으며, 대출이 가능한 책 우선으로 Sort된다.
# book은 대출 가능, 대출중, 분실의 3가지의 상태를 갖는다.
# user는 자신이 지금까지 빌린 book들의 기록(대출,반납)을 최신 순으로 볼 수 있다.
# user의 RENT/RETURN은 모두 History가 남는다.

매우 간단한 BL입니다. 그리고, 이 BL에 대한 Service의 주체를 기준으로 다음과 같이 명명한 서비스들을 구상할 수 있습니다.

* UserService : 사용자가 action의 주체가 되는 서비스입니다.
* BookService : Book이 주체가 되는 서비스입니다.

서비스의 명명법은 영문법을 따르게 되며, 다음과 같은 영문장으로 구성을 하면 좋습니다.

User.rentBook(Book book)
User.returnBook(Book book)
User.listUpHistory()

Book.listUp()

서비스의 설계가 될 수 있는 interface는 다음과 같이 구성이 가능합니다. 

public interface UserService {
    public boolean rent(int userId, int bookId);
    public boolean returnBook(int userId, int bookId);
    public List<User> listup();
    public List<History> getHistories(int userId);
}

public interface BookService {
    public List<Book> listup();
}

이와 같이 interface는 단순히 객체에 대한 프로그래밍적 요소로만 사용되는 것이 아닌, 프로그램의 in/out에 대한 설계로서 사용이 가능합니다. 우리가 어떠한 application을 작성을 할때, input/output에 대한 정의를 명확히 할 수 있는 경우, interface를 이용해서 코드를 명확히 구성하는 것이 가능합니다. 만들어진 UserService와 BookStoreService를 구현해보도록 하겠습니다.

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private BookDao bookDao;
    @Autowired
    private UserDao userDao;
    @Autowired
    private HistoryDao historyDao;

    public BookDao getBookDao() {
        return bookDao;
    }

    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public HistoryDao getHistoryDao() {
        return historyDao;
    }

    public void setHistoryDao(HistoryDao userHistoryDao) {
        this.historyDao = userHistoryDao;
    }

    @Override
    public boolean rent(int userId, int bookId) {
        User user = userDao.get(userId);
        Book book = bookDao.get(bookId);

        user.setPoint(user.getPoint() + 10);
        user.setLevel(getUserLevel(user.getPoint()));
        book.setRentUserId(user.getId());
        book.setStatus(BookStatus.RentNow);

        History history = new History();
        history.setUserId(userId);
        history.setBookId(book.getId());
        history.setAction(HistoryActionType.RENT);

        userDao.update(user);
        bookDao.update(book);
        historyDao.add(history);
        return true;
    }

    private UserLevel getUserLevel(int point) {
        if(point < 100) {
            return UserLevel.NORMAL;
        }
        else if(point >= 100 && point < 300) {
            return UserLevel.READER;
        }
        else {
            return UserLevel.MASTER;
        }
    }

    @Override
    public boolean returnBook(int userId, int bookId) {
        Book book = bookDao.get(bookId);
        book.setStatus(BookStatus.CanRent);
        book.setRentUserId(null);

        History history = new History();
        history.setUserId(userId);
        history.setBookId(book.getId());
        history.setAction(HistoryActionType.RETURN);

        bookDao.update(book);
        historyDao.add(history);

        return true;
    }

    @Override
    public List<User> listup() {
        return userDao.getAll();
    }

    @Override
    public List<History> getHistories(int userId) {
        return historyDao.getByUser(userId);
    }
}

그리고, 이에 대한 테스트 코드를 작성해서 Business Logic이 무사히 통과되고 있는지를 확인하도록 합니다. 이는 매우 중요한 작업입니다. 우리는 지금까지 DB에 대한 CRUD만을 통과를 시켰습니다. 우리의 BL이 정확하게 구성이 가능한것인지를 파악하는 수단으로 Test code는 최적의 방법입니다. 테스트 없이는 개발이 되지 않는다. 라는 원칙을 유념해주세요. 지금 구성되는 UserService의 테스트의 포인트는 무엇일까요? 테스트를 구현을 할때, 이제는 BL을 같이 생각을 하고 구현을 들어가야지 됩니다. 제가 생각하는 지금 UserService의 테스트 포인트는 다음과 같습니다. 

# point가 1, 99, 299, 301 인 사용자가 책을 빌릴때 사용자의 Level이 정상적으로 변경이 되는지 확인
# book의 status가 RentNow로 변경이 되었는지 확인
# User History의 Action이 정상적으로 설정되었는지 확인

이러한 점에 주안점을 두고, 테스트 코드를 하나하나 작성해보도록 하겠습니다. 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContextWithAutowired.xml")
public class UserServiceImplTest {
    @Autowired
    private UserService userService;
    @Autowired
    private UserDao userDaoImplWithJdbcTemplate;
    @Autowired
    private BookDao bookDaoImplWithJdbcTemplate;
    @Autowired
    private HistoryDao userHistoryDaoImplWithJdbcTemplate;

    @Before
    public void setUp() {
        assertNotNull(userService);
        userHistoryDaoImplWithJdbcTemplate.deleteAll();
        bookDaoImplWithJdbcTemplate.deleteAll();
        userDaoImplWithJdbcTemplate.deleteAll();

        List<User> users = getUsers();
        for(User user : users) {
            userDaoImplWithJdbcTemplate.add(user);
        }
        List<Book> books = getBooks();
        for(Book book : books) {
            bookDaoImplWithJdbcTemplate.add(book);
        }
    }

    private List<User> getUsers() {
        User user1 = new User();
        user1.setName("name01");
        user1.setPassword("password01");
        user1.setPoint(99);
        user1.setLevel(UserLevel.NORMAL);

        User user2 = new User();
        user2.setName("name02");
        user2.setPassword("password02");
        user2.setPoint(101);
        user2.setLevel(UserLevel.READER);

        User user3 = new User();
        user3.setName("name03");
        user3.setPassword("password03");
        user3.setPoint(301);
        user3.setLevel(UserLevel.MVP);

        User user4 = new User();
        user4.setName("name04");
        user4.setPassword("password04");
        user4.setPoint(290);
        user4.setLevel(UserLevel.READER);

        return Arrays.asList(user1, user2, user3, user4);
    }

    private List<Book> getBooks() {
        Book book1 = new Book();
        book1.setName("book name01");
        book1.setAuthor("autor name 01");
        book1.setComment("comment01");
        book1.setPublishDate(new Date());
        book1.setStatus(BookStatus.CanRent);

        Book book2 = new Book();
        book2.setName("book name02");
        book2.setAuthor("autor name 02");
        book2.setComment("comment02");
        book2.setPublishDate(new Date());
        book2.setStatus(BookStatus.RentNow);

        Book book3 = new Book();
        book3.setName("book name03");
        book3.setAuthor("autor name 03");
        book3.setComment("comment03");
        book3.setPublishDate(new Date());
        book3.setStatus(BookStatus.Missing);

        List<Book> books = Arrays.asList(book1, book2, book3);
        return books;
    }

    @Test
    public void rentWithNoLevelUp() {
        Book oldBook = bookDaoImplWithJdbcTemplate.getAll().get(0);
        int bookId = oldBook.getId();

        User oldUser = null;
        for(User user : userDaoImplWithJdbcTemplate.getAll()) {
            if(user.getName().equals("name02")) {
                oldUser = user;
                break;
            }
        }

        int userId = oldUser.getId();
        userService.rent(userId, bookId);
        User user = userDaoImplWithJdbcTemplate.get(userId);
        assertThat(user.getPoint(), is(oldUser.getPoint() + 10));
        assertThat(user.getLevel(), is(UserLevel.READER));

        Book book = bookDaoImplWithJdbcTemplate.get(bookId);
        assertThat(book.getRentUserId(), is(user.getId()));
        assertThat(book.getStatus(), is(BookStatus.RentNow));

        List<History> histories = userHistoryDaoImplWithJdbcTemplate.getByUser(userId);
        assertThat(histories.size(), is(1));

        History history = histories.get(0);
        assertThat(history.getUserId(), is(userId));
        assertThat(history.getBookId(), is(bookId));
        assertThat(history.getAction(), is(HistoryActionType.RENT));
    }

    @Test
    public void rentWithLevelUpForREADER() {
        Book oldBook = bookDaoImplWithJdbcTemplate.getAll().get(0);
        int bookId = oldBook.getId();

        User oldUser = null;
        for(User user : userDaoImplWithJdbcTemplate.getAll()) {
            if(user.getName().equals("name01")) {
                oldUser = user;
                break;
            }
        }

        int userId = oldUser.getId();
        userService.rent(userId, bookId);
        User user = userDaoImplWithJdbcTemplate.get(userId);
        assertThat(user.getPoint(), is(oldUser.getPoint() + 10));
        assertThat(user.getLevel(), is(UserLevel.READER));

        Book book = bookDaoImplWithJdbcTemplate.get(bookId);
        assertThat(book.getRentUserId(), is(user.getId()));
        assertThat(book.getStatus(), is(BookStatus.RentNow));

        List<History> histories = userHistoryDaoImplWithJdbcTemplate.getByUser(userId);
        assertThat(histories.size(), is(1));

        History history = histories.get(0);
        assertThat(history.getUserId(), is(userId));
        assertThat(history.getBookId(), is(bookId));
        assertThat(history.getAction(), is(HistoryActionType.RENT));
    }

    @Test
    public void rentWithLevelUpForMVP() {
        Book oldBook = bookDaoImplWithJdbcTemplate.getAll().get(0);
        int bookId = oldBook.getId();

        User oldUser = null;
        for(User user : userDaoImplWithJdbcTemplate.getAll()) {
            if(user.getName().equals("name04")) {
                oldUser = user;
                break;
            }
        }

        int userId = oldUser.getId();
        userService.rent(userId, bookId);
        User user = userDaoImplWithJdbcTemplate.get(userId);
        assertThat(user.getPoint(), is(oldUser.getPoint() + 10));
        assertThat(user.getLevel(), is(UserLevel.MVP));

        Book book = bookDaoImplWithJdbcTemplate.get(bookId);
        assertThat(book.getRentUserId(), is(user.getId()));
        assertThat(book.getStatus(), is(BookStatus.RentNow));

        List<History> histories = userHistoryDaoImplWithJdbcTemplate.getByUser(userId);
        assertThat(histories.size(), is(1));

        History history = histories.get(0);
        assertThat(history.getUserId(), is(userId));
        assertThat(history.getBookId(), is(bookId));
        assertThat(history.getAction(), is(HistoryActionType.RENT));
    }

    @Test
    public void returnBook() {
        int bookId = bookDaoImplWithJdbcTemplate.getAll().get(0).getId();
        int userId = userDaoImplWithJdbcTemplate.getAll().get(0).getId();

        userService.rent(userId, bookId);
        userService.returnBook(userId, bookId);

        Book book = bookDaoImplWithJdbcTemplate.get(bookId);
        assertThat(book.getStatus(), is(BookStatus.CanRent));

        List<History> histories = userHistoryDaoImplWithJdbcTemplate.getByUser(userId);
        assertThat(histories.size(), is(2));

        History history = histories.get(1);
        assertThat(history.getUserId(), is(userId));
        assertThat(history.getBookId(), is(bookId));
        assertThat(history.getAction(), is(HistoryActionType.RETURN));
    }
}

코드가 매우 깁니다. 이 긴 코드를 한번 살펴보도록 하겠습니다. 지금 구성된 BL은 point값이 정상적으로 증가하는지, 그리고 그 증가된 point에 따라서 User Level이 정상적으로 승급되는지를 알아보는것이 포인트입니다. 따라서, 이 경우에는 UserLevel을 각각 NORMAL, READER, MVP로 나눠서 각 사용자들의 LEVEL이 올라가는 것을 하나하나 확인하는 것이 좋습니다. 그리고, 각각의 업무가 발생했을 때, DB에 정상적인 값들이 insert되었는지를 명확히 확인하는 것이 필요합니다. 이러한 테스트 코드는 후에, 에러가 발생했을때 그 에러에 대한 tracing역시 이 테스트 코드를 통해서 에러를 검증하게 됩니다. in/out이 정상적인지, 그리고 그 in/out에서 어떤 에러가 발생하는지를 확인하는 것 역시 테스트 코드에서 하게 되는 일입니다. 

이제 BookServiceImpl에 대한 테스트 코드를 작성해주세요. BookServiceImpl의 테스트 코드는 매우 단순합니다. Sort가 정상적으로 되어서 나오고 있는지를 확인해주면 됩니다. 이 방법은 Dao에 새로운 method를 넣어서 Sort Order를 넣어 구현도 가능하고, 아니면 Dao에서 얻어온 List를 Service Layer에서 재 Sort 하는 것으로도 구현 가능합니다. 어느 방법이던지 한번 구현해보시길 바랍니다. 


Transaction의 적용

지금까지 구현된 Service, Dao Layer는 결정적인 문제를 가지고 있습니다. 예를 들어, rentBook action에서 book의 상태를 업데이트 한 후에, DB의 문제나 application의 exception이 발생했다면 어떤 문제가 발생할까요? 
지금의 JdbcTemplate은 각 Dao Layer단으로 Connection이 분리 되어 있습니다. 따라서 한쪽에서 Exception이 발생하더라도, 기존 update 사항에 대해서는 DB에 그대로 반영이 되어버립니다. 이건 엄청난 문제를 발생시킵니다. DAO는 우리가 서비스를 만드는 도구이고, 결국은 사용자나 BL의 한개의 action으로 DB의 Transaction이 적용이 되어야지 되는데, 이러한 규칙을 모두 날려버리게 되는 것입니다. 

다시 한번 정리하도록 하겠습니다. BL상으로, Service의 method는 BL의 기본 단위가 됩니다. 기술적으로는 Transaction의 단위가 Service의 method 단위가 되어야지 됩니다. 간단히 sudo 코드를 작성한다면 rent method는 다음과 같이 작성되어야지 됩니다.

    @Override
    public boolean rent(int userId, int bookId) {
        Transaction.begin();
        User user = userDao.get(userId);
        Book book = bookDao.get(bookId);
        
        user.setPoint(user.getPoint() + 10);
        user.setLevel(getUserLevel(user.getPoint()));
        book.setRentUserId(user.getId());
        book.setStatus(BookStatus.RentNow);
        
        UserHistory history = new UserHistory();
        history.setUserId(userId);
        history.setBookId(book.getId());
        history.setAction(HistoryActionType.RENT);
        
        userDao.update(user);
        bookDao.update(book);
        userHistoryDao.add(history);
        
        Transaction.commit();
        
        return true;
    }

Transaction은 매우 골치아픈 개념입니다. 먼저 지금 사용중인 DataSource는 직접적으로 JDBC에 연결되는 Connection입니다. 그런데, 이를 Hibernate의 session 또는 MyBatis의 ObjectMapper들을 사용한다면 완전히 다른 Transaction 기술을 사용해야지 됩니다. 지금까지 기술에 종속적이지 않은 서비스 코드를 작성하고 있는데, 이제 다시 Transaction에 의한 기술 종속적 코드로 변경이 되어야지 되는 상황이 되어버린것입니다. 그래서, 이 경우를 해결하기 위해서 Transaction의 기술들에 대한 interface를 spring은 제안하고 있습니다. 바로 org.springframework.transaction.PlatformTransactionManager가 바로 그 interface입니다.

일단 spring에서 제공되는 JdbcTemplate은 spring DataSource를 이용합니다. 이 DataSource에 대한 TransactionManager 역시 제공이 되고 있으며, Hibernate와 같은 orm에 대한 기본 TransactionManager들도 역시 모두 제공되고 있습니다. PlatformTransactionManager의 구조를 살펴보도록 하겠습니다. 

public interface PlatformTransactionManager {
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}

Transaction을 얻어내고, Transaction을 commit, rollback하는 간단한 구조로 되어 있습니다. 이러한 기본 구조는 우리가 Dao Layer를 이용해서 Transaction을 사용하는데 충분합니다.
PlatformTransactionManager를 이용한 Transaction 구현을 간단한 sudo code로 구현하면 다음과 같습니다. 

    public void doSomething() {
        TransactionStatus status = transactionManager.getTransaction(definition);
        try {
            // ..do something
            transactionManager.commit(status);
        }
        catch(Exception ex) {
            transactionManager.rollback(status)
        }
    }


전에 보던 Template-callback pattern과 동일한 패턴의 코드가 완성됩니다. TransactionManager의 생성자에는 DataSource interface를 구현하고 있기 때문에 Dao Layer에서 사용하는 Connection을 한번에 묶어서 처리가 가능합니다. Spring Transaction을 한번 구현해보도록 하겠습니다.

먼저, spring transaction을 추가해야지 됩니다. pom.xml에 spring transaction을 추가합니다.
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>3.2.0.RELEASE</version>
</dependency>


JdbcTemplate에서 사용될 TransactionManager를 bean에 선언합니다. 일반적으로 PlatformTransactionManager는 Spring에서 transactionManager라는 이름으로 사용됩니다. 관례적으로 사용되는 이름이니 이를 따르도록 하겠습니다. 

 <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
  </bean> 

그리고, TransactionManager를 이용한 코드로 rentBook method를 구현해보도록 하겠습니다. 

    @Override
    public boolean rent(int userId, int bookId) {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            User user = userDao.get(userId);
            Book book = bookDao.get(bookId);
            
            user.setPoint(user.getPoint() + 10);
            user.setLevel(getUserLevel(user.getPoint()));
            book.setRentUserId(user.getId());
            book.setStatus(BookStatus.RentNow);
            
            UserHistory history = new UserHistory();
            history.setUserId(userId);
            history.setBookId(book.getId());
            history.setAction(HistoryActionType.RENT);
            
            userDao.update(user);
            bookDao.update(book);
            userHistoryDao.add(history);
            transactionManager.commit(status);
            return true;
        } catch(RuntimeException e) {
            this.transactionManager.rollback(status);
            throw e;
        }
    }
 
이제 다른 method의 Transation을 한번 구현해보도록 하겠습니다. 코딩을 해주세요. 


Transaction을 어떻게 테스트를 할 수 있을까?

트랜젝션에 대한 코드를 완성했습니다. 그런데, 이 코드는 어떻게 테스트를 해야지 될까요? 코드에 Exception을 발생시키는 로직을 추가하는 것이 좋을까요? 그렇게 된다면 코드 안에 버그를 심게 되기 때문에 문제가 발생할 수 있습니다. 아니면 다른 방법이 있을까요?
일단, 이렇게 코드를 구성하면 안되지만, Transaction 중간에 에러가 발생하는 상황을 만들어보도록 합시다. 이 에러가 발생할 수 있는 상황은 DB에 접근하는 중에 오류가 발생하거나 db를 업데이트 하는 도중, Business Logic 상의 에러가 발생하는 것을 의미합니다. 이 두가지 모두 테스트를 작성하기가 매우 힘든 상황입니다. 이런 상황을 어떻게 하면 직접 만들수 있을까요? 

한번 생각의 전환을 해보도록 하겠습니다. Service는 Business Logic의 모음이라고 했습니다.  `모음' 이라는 용어에 주의할 필요가 있습니다. 이는 Business Logic 역시 한개의 객체로서 표현이 가능하다는 뜻이 될 수 있습니다. 지금 구성된 코드에서 조금 맘에 걸리는 부분이 있습니다. 다음 코드를 봐주세요.

user.setPoint(user.getPoint() + 10);
user.setLevel(getUserLevel(user.getPoint()));

위 코드는 user에 point를 더하고, user의 level을 결정해주는 method입니다. getUserLevel이라는 private method 자체가 하나의 BL이 되는 것입니다. 다시 한번 생각해보도록 하겠습니다. 우리는 다음과 같이 BL을 정했습니다.

# user가 book을 빌리면, point가 10점씩 쌓인다.
# point가 100점이 넘어가면 READER가 된다.
# point가 300점이 넘어가면 MVP가 된다.
# point가 100점 이하인 경우, 일반유저(NORMAL)이다.

저 BL은 언제든지 바뀔 수 있는 BL입니다. 어느 순간에 정책의 변경으로 인하여 point의 증감폭이 5점으로 바뀐다던지, 아니면 point의 level 단계의 100, 300에서 1000, 3000으로도 언제든지 변경할 수 있는 것입니다. 따라서, 이런 변화 가능한 부분은 객체에서 외부에서 변경이 가능하도록 따로 객체나 Property로 뽑는 것이 맞습니다. UserLevelRole 이라는 interface를 만들어 이런 BL을 따로 객체화 해보도록 합시다. 다음은 UserLevelRole의 interface와 객체입니다.

public interface UserLevelRole {
    void updatePointAndLevel(User user);
    int getAddRentPoint();
    void setAddRentPoint(int addRentPoint);
    int getReaderThreshold();
    void setReaderThreshold(int readerThreshold);
    int getMvpThreashold();
    void setMvpThreashold(int mvpThreashold);
}

public class UserLevelRoleImpl implements UserLevelRole {
    private int addRentPoint;
    private int readerThreshold;
    private int mvpThreashold;

    @Override
    public int getAddRentPoint() {
        return addRentPoint;
    }

    @Override
    public void setAddRentPoint(int addRentPoint) {
        this.addRentPoint = addRentPoint;
    }

    @Override
    public int getReaderThreshold() {
        return readerThreshold;
    }

    @Override
    public void setReaderThreshold(int readerThreshold) {
        this.readerThreshold = readerThreshold;
    }

    @Override
    public int getMvpThreashold() {
        return mvpThreashold;
    }

    @Override
    public void setMvpThreashold(int mvpThreashold) {
        this.mvpThreashold = mvpThreashold;
    }

    @Override
    public void updatePointAndLevel(User user) {
        user.setPoint(user.getPoint() + addRentPoint);
        if(user.getPoint() >= mvpThreashold) {
            user.setLevel(UserLevel.MVP);
        } else if(user.getPoint() >= readerThreshold) {
            user.setLevel(UserLevel.READER);
        } else {
            user.setLevel(UserLevel.NORMAL);
        }
    }
}

자. 이렇게 구성된 UserLevelRole을 이제 서비스에 반영해주도록 하겠습니다. 

UserServiceImpl에 UserLevelRole에 대한 property를 다음과 같이 추가합니다. 

    @Autowired
    private UserLevelRole userLevelRole;

    public UserLevelRole getUserLevelRole() {
        return this.userLevelRole;
    }

    public void setUserLevelRole(UserLevelRole userLevelRole) {
        this.userLevelRole = userLevelRole;
    }

추가된 property를 applicationContext.xml에서 구성해주도록 합니다. 다음과 같이 처리되면 됩니다.

  <bean id="userLevelRole" class="com.xyzlast.bookstore02.services.UserLevelRoleImpl">
    <property name="addRentPoint" value="10"/>
    <property name="readerThreshold" value="100"/>
    <property name="mvpThreashold" value="300"/>
  </bean>


이제 지금까지 구성된 테스트 코드를 수정해주도록 하겠습니다. 지금까지 상수로 10씩 더한것을 확인하던 테스트 코드를 이제 설정된 값으로 변경되고 있는 것을 확인할 수 있어야지 됩니다.

        User user = userDaoImplWithJdbcTemplate.get(userId);
        assertThat(user.getPoint(), is(oldUser.getPoint() + + userLevelRole.getAddRentPoint()));
        assertThat(user.getLevel(), is(UserLevel.READER));


그리고 Service에 대한 코드를 조금 수정해주도록 하겠습니다. 

    @Override
    public boolean rent(int userId, int bookId) {
        User user = userDaoImplWithJdbcTemplate.get(userId);
        Book book = bookDaoImplWithJdbcTemplate.get(bookId);

        book.setRentUserId(user.getId());
        book.setStatus(BookStatus.RentNow);

        History history = new History();
        history.setUserId(userId);
        history.setBookId(book.getId());
        history.setAction(HistoryActionType.RENT);
        
        bookDaoImplWithJdbcTemplate.update(book);
        
        userLevelRole.updatePointAndLevel(user);
        userDaoImplWithJdbcTemplate.update(user);

        historyDaoWithJdbcTemplate.add(history);
        
        return true;
    }


userLevelRole에 user에 대한 Point와 Level을 업데이트 하는 로직을 위임하고 있는 것을 알 수 있습니다. 자, 이제 에러를 발생시켜보도록 하겠습니다. 단순하게 userLevelRole을 null로 만들어주면 저 code에서 NullPointException이 발생하게 됩니다. Null이 발생되도록 만들어주는 테스트 코드입니다. 

    @Test(expected=NullPointerException.class)
    @DirtiesContext
    public void rentBookWithException() {
        ((UserServiceImpl) userService).setUserLevelRole(null);
        Book oldBook = bookDaoImplWithJdbcTemplate.getAll().get(0);
        int bookId = oldBook.getId();

        User oldUser = null;
        for(User user : userDaoImplWithJdbcTemplate.getAll()) {
            if(user.getName().equals("name02")) {
                oldUser = user;
                break;
            }
        }
        int userId = oldUser.getId();
        try {
            userService.rent(userId, bookId);
        } finally {
            //Exception이 발생한 이후에, 값이 업데이트 되지 않고, 기존값과 동일해야지 됨
            Book updatedBook = bookDaoImplWithJdbcTemplate.get(bookId);
            assertThat(updatedBook.getStatus(), is(oldBook.getStatus()));
            assertThat(updatedBook.getRentUserId(), is(nullValue()));
        }
    }

이 테스트에는 다음 3개의 특징을 가지고 있습니다. 
먼저, @DirtiesContext입니다. 이는 이 test를 통과하게 되면 applicationContext에서 설정한 객체의 특성이 변경되기 때문에, 여기서 사용한 객체를 제거하고 다시 applicationContext에 있는 객체로 사용하기를 설정하는 것입니다. 다음은 @Test에 expected가 추가 된 것입니다. 내부 코드에서 예상된 exception이 발생되는지 확인하는 코드로, exception이 발생하지 않으면 test가 실패하게 됩니다. 마지막으로, finally 코드를 봐주시길 바랍니다. exception이 발생하더라도, Book의 값이 update되지 않았는지를 확인하는 코드입니다. 만약에 Transaction이 정상적으로 처리가 되었다면 테스트가 통과가 될 것입니다. 테스트 결과를 한번 확인해보도록 하겠습니다. 



테스트 결과는 보시다시피 실패했습니다. 에러 내용을 확인해보도록 하겠습니다. NullPointException이 발생할 줄 알았지만, AssertionError가 발생된 것을 알 수 있습니다. AssertionError의 경우, Book의 Status가 다르게 나와서 에러가 발생되었음을 알 수 있습니다. DB의 값이 Transaction 처리가 되지 않아 업데이트가 되었다는 뜻입니다. 자, 이제 PlatformTransactionManager를 이용한 Transaction을 반영해보도록 하겠습니다. 

    @Override
    public boolean rent(int userId, int bookId) {
        TransactionStatus status =  transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            User user = userDaoImplWithJdbcTemplate.get(userId);
            Book book = bookDaoImplWithJdbcTemplate.get(bookId);

            book.setRentUserId(user.getId());
            book.setStatus(BookStatus.RentNow);

            History history = new History();
            history.setUserId(userId);
            history.setBookId(book.getId());
            history.setAction(HistoryActionType.RENT);

            bookDaoImplWithJdbcTemplate.update(book);

            userLevelRole.updatePointAndLevel(user);
            userDaoImplWithJdbcTemplate.update(user);

            historyDaoWithJdbcTemplate.add(history);
            transactionManager.commit(status);
        }
        catch(Exception ex) {
            transactionManager.rollback(status);
            throw ex;
        }
        return true;
    }

transactionManager가 반영된 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: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">

  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <bean id="dataSource"
    class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${connect.driver}" />
    <property name="url" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
  </bean>
  <context:property-placeholder location="classpath:spring.property" />
  <context:component-scan base-package="com.xyzlast.bookstore02.dao" />
  <context:component-scan base-package="com.xyzlast.bookstore02.services" />
  <bean id="userLevelRole" class="com.xyzlast.bookstore02.services.UserLevelRoleImpl">
    <property name="addRentPoint" value="10" />
    <property name="readerThreshold" value="100" />
    <property name="mvpThreashold" value="300" />
  </bean>
  <bean id="transactionManager"
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
  </bean></beans>


이제 테스트를 돌리면 정상적으로 도는 것을 확인할 수 있습니다. 





annotation을 이용한 Transaction 구현

지금까지 구현된 코드에는 한가지 문제가 있습니다. Transaction은 기술적인 영역으로 Service 객체에는 어울리지 않는 내용입니다. Service는 BL의 집합이라는 것을 다시 한번 상기해주시길 바랍니다. BL에 기술적인 요소가 들어가게 되면, 기술적인 요소에 따른 BL의 수정이 가해질 수 있습니다. 따라서, Spring에서는 이를 분리하는 것을 제안하고 있으며, 특히 Transaction에서는 @Transactional annotaion을 이용한 분리를 제안하고 있습니다. 

@Transactional은 method, class에 모두 적용 가능한 annotation입니다. @Transactional을 사용하기 위해서는 applicationContext.xml에 다음 설정을 추가하면 됩니다. 

  <tx:annotation-driven transaction-manager="transactionManager"/>

그리고, 지금까지 작성된 class의 선언부에 @Transactional을 선언해주면 class의 모든 public method에 Transaction이 설정되게 됩니다. @Transaction이 구성된 전체 UserServiceImpl의 코드입니다. 

@Service
@Transactional
public class UserServiceImpl implements UserService {
    @Autowired
    private BookDao bookDao;
    @Autowired
    private UserDao userDao;
    @Autowired
    private HistoryDao userHistoryDao;
    @Autowired
    private UserLevelRole userLevelRole;
    @Autowired
    private PlatformTransactionManager transactionManager;

    public BookDao getBookDao() {
        return bookDao;
    }

    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public HistoryDao getUserHistoryDao() {
        return userHistoryDao;
    }

    public void setUserHistoryDao(HistoryDao userHistoryDao) {
        this.userHistoryDao = userHistoryDao;
    }

    @Override
    public boolean rent(final int userId, final int bookId) {
        User user = userDao.get(userId);
        Book book = bookDao.get(bookId);

        book.setRentUserId(user.getId());
        book.setStatus(BookStatus.RentNow);
        bookDao.update(book);

        userLevelRole.updatePointAndLevel(user);

        UserHistory history = new UserHistory();
        history.setUserId(userId);
        history.setBookId(book.getId());
        history.setAction(HistoryActionType.RENT);

        userDao.update(user);
        userHistoryDao.add(history);
        return true;
    }

    @Override
    public boolean returnBook(int userId, int bookId) {
        Book book = bookDao.get(bookId);
        book.setStatus(BookStatus.CanRent);
        book.setRentUserId(null);

        UserHistory history = new UserHistory();
        history.setUserId(userId);
        history.setBookId(book.getId());
        history.setAction(HistoryActionType.RETURN);

        bookDao.update(book);
        userHistoryDao.add(history);

        return true;
    }

    @Override
    public List<User> listup() {
        return userDao.getAll();
    }

    @Override
    public List<UserHistory> getHistories(int userId) {
        return userHistoryDao.getByUser(userId);
    }

    @Override
    public void setUserLevelRole(UserLevelRole userLevelRole) {
        this.userLevelRole = userLevelRole;
    }
}


이제 Service에서는 모든 Business Logic을 구현할 수 있게 되었고, 기술적으로 독립적인 코드로 구성되었습니다. BookService에 대해서도, 또는 자신이 서비스를 직접 만들어서 코드를 확장시켜보시길 바랍니다. 
그럼 이와 같은 코드는 어떻게 구성이 된 것일까요? Spring은 어떤 일을 해서 이와 같은 Transaction을 구성할 수 있을지 한번 알아보도록 하겠습니다. 


Spring @Transactional의 구현 방법

Spring은 이러한 문제를 어떻게 해결하고 있을까요? 전에 Spring에서 자주 사용되는 pattern으로 Template-callback pattern을 봤습니다. Transaction 역시 Template-callback pattern으로 처리가 가능합니다. 그렇지만, 지금 사용한 @Transactional와 같은 annotation을 이용해서는 처리가 불가능합니다. 

구현 원리를 알아보기 전에 한번 다음 코드를 실행해보도록 하겠습니다. 먼저 방금 붙였던 @Transactional을 제거하고, 다음 테스트 코드를 돌려보도록 하겠습니다.

    @Test
    public void displayUserServiceObjectName() {
        System.out.println("UserService의 구현 객체는 " + userService.getClass().getName() + "입니다.");
        assertThat("userService는 UserServiceImpl이 할당되어 있지 않습니다.", userService instanceof UserServiceImpl, is(true));
    }

결과는 다음과 같이 나타납니다. 

UserService의 구현 객체는 com.xyzlast.bookstore03.services.UserServiceImpl입니다.


이 결과는 지금까지 보셨던것과 같이, 객체 이름 + Instance Key의 형태로 객체를 표현하게 됩니다. 그리고 UserService interface를 상속받은 UserServiceImpl임을 알 수 있습니다. 

그럼 @Transactional을 붙였을 때, 어떻게 나오는지 확인해보도록 하겠습니다. 


UserService의 class 이름은 $Proxy15입니다.

테스트가 실패하고, UserService의 class 이름은 듣도보지도 못한 $Proxy라는 이상한 객체로 변경되어 있습니다. 
이게 어떻게 된걸까요? 

이 부분을 이해하기 위해서는 Spring의 이제 2번째 개념인 AOP에 대한 이해가 필요합니다. 다음 장에서는 AOP에 대해서 깊게 들어가보도록 하겠습니다. @Transactional의 경우, UserServiceImpl을 다른 객체로 한번 더 감싼 Proxy객체로 사용하게 된다. 라는 개념으로 일단 이 장을 마무리 하도록 하겠습니다.






Posted by Y2K
,

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



지금까지 우리는 Template-callback 구조의 SqlExecutor와 Connection을 처리하는 ConnectionFactory를 이용한 Dao 객체들을 구성하였습니다. 지금까지 만든 Dao 객체들을 Spring에서 제공하는 Jdbc객체들을 이용해서 변환시키는 과정을 한번 알아보도록 하겠습니다. 

Spring JDBC를 이용한 Dao 의 개발

Spring JDBC는 지금까지 이야기한 모든 기능들이 다 포함되어있습니다.
# Template, callback 구조
# DataSource를 이용한 ConnectionFactory 구현
# Checked Exception을 Runtime Exception으로 변경

먼저, maven을 이용해서 spring jdbc를 추가하도록 합니다. 

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

먼저, DataSource를 구성합니다. DataSource는 기존 ConnectionFactory를 대신합니다. 이는 이미 Spring JDBC에서 제공하는 객체이기 때문에 따로 구현할 필요가 없습니다. ApplicationContext에 다음 항목을 추가해주면 됩니다. 
여기서 보시면 아시겠지만, 지금 여기서 사용하는 DataSource는 DriverManagerDataSource객체이고, DataSource는 하나의 interface입니다. Spring에서 제공되는 모든 DB connection은 DataSource interface를 구현하고 있습니다. 이번에 사용한 DriverManagerDataSource는 가장 단순한 DataSource라고 할 수 있습니다.

  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost/bookstore" />
    <property name="username" value="root" />
    <property name="password" value="qwer12#$" />
  </bean>

지금까지 개발한 BookDao를 변경합니다. BookDaoImplWithSqlExecutor로 이름을 변경시킵니다. JdbcTemplate을 이용해서 같은 기능의 코드를 작성할 예정이기 때문에 지금까지 구현된 method들을 모든 method를 interface로 따로 뽑습니다. 
구성된 interface BookDao는 다음과 같습니다. 이러한 Interface를 위주로하는 설계는 프로그램의 확장성을 높입니다. 지금 저희는 기존에 만들어진 것과 기능적으로는 완벽하게 동일한 객체를 만들어서 테스트코드를 통과시킬 예정입니다. 기존 테스트 코드가 객체 자체를 가지고 왔다면, 이제는 Interface를 가지고 오는 형식으로 변경을 시킬 예정입니다. 

public interface BookDao {
    int countAll();
    void add(Book book);
    void update(Book book);
    void delete(Book book);
    void deleteAll();
    Book get(int id);
    List<Book> getAll();
    List<Book> search(String name);
}

그리고, 변경시킨 BookDaoImplWithSqlExecutor에서 BookDao interface를 implements 해주도록 합니다. 
마지막으로 class를 추가합니다. BookDaoImplWithJdbcTemplate를 추가하고, JdbcTemplate을 사용하도록 코드를 작성합니다. 지금까지 만들었던 SqlExecutor와 설정이 너무나도 유사합니다. Spring을 통해 다음과 같이 선언하도록 합니다. 

  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost/bookstore" />
    <property name="username" value="root" />
    <property name="password" value="qwer12#$" />
  </bean>
  
  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
  </bean>


다음은 JdbcTemplate를 이용한 BookDao 코드입니다. 기존 코드를 비교해보도록 합니다.

public class BookDaoImplWithJdbcTemplate implements BookDao {
    private JdbcTemplate jdbcTemplate;

    public JdbcTemplate getJdbcTemplate() {
        return jdbcTemplate;
    }
    
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    private Book convertToBook(ResultSet rs) throws SQLException {
        Book book = new Book();
        book.setId(rs.getInt("id"));
        book.setName(rs.getString("name"));
        book.setAuthor(rs.getString("author"));
        java.util.Date date = new java.util.Date(rs.getTimestamp("publishDate").getTime());
        book.setPublishDate(date);
        book.setComment(rs.getString("comment"));
        book.setStatus(BookStatus.get(rs.getInt("status")));
        int rentUserId = rs.getInt("rentUserId");
        if(rentUserId == 0) {
            book.setRentUserId(null);
        }
        else {
            book.setRentUserId(rentUserId);
        }
        return book;
    }
    
    private Book convertToBook(Map<String, Object> rs) {
        Book book = new Book();
        book.setId((int) rs.get("id"));
        book.setName((String) rs.get("name"));
        book.setAuthor((String) rs.get("author"));
        book.setPublishDate((Timestamp) rs.get("publishDate"));
        book.setComment((String) rs.get("comment"));
        book.setStatus(BookStatus.get((int) rs.get("status")));
        return book;
    }
    @Override
    public void add(final Book book) {      
        this.jdbcTemplate.update("insert books(id, name, author, publishDate, comment, status, rentUserId) values(?, ?, ?, ?, ?, ?, ?)", 
                book.getId(), book.getName(), book.getAuthor(), book.getPublishDate(), book.getComment(), book.getStatus().intValue(), book.getRentUserId());
    }
    @Override
    public Book get(final int id) {
        return this.jdbcTemplate.queryForObject("select id, name, author, publishDate, comment, status, rentUserId from books where id=?", new Object[] { id}, 
                new RowMapper<Book>() {
            @Override
            public Book mapRow(ResultSet rs, int rowNum)
                    throws SQLException {
                return convertToBook(rs);
            }
        });
    }
    @Override
    public List<Book> search(final String name) {
        List<Book> books = new ArrayList<>();
        String query = "select id, name, author, publishDate, comment, status, rentUserId from books where name like '%" + name +"%'";
        List<Map<String, Object>> rows = getJdbcTemplate().queryForList(query);
        for(Map<String, Object> row : rows) {
            books.add(convertToBook(row));
        }
        return books;
    }
    @Override
    public int countAll() {
        return this.jdbcTemplate.queryForInt("select count(*) from books");
    }
    @Override
    public void update(final Book book) {
        this.jdbcTemplate.update("update books set name=?, author=?, publishDate=?, comment=?, status=?, rentUserId=? where id=?",
                book.getName(), book.getAuthor(), book.getPublishDate(), book.getComment(), book.getStatus().intValue(), book.getRentUserId(), book.getId());
    }
    @Override
    public List<Book> getAll() {
        List<Book> books = new ArrayList<>();
        List<Map<String, Object>> rows = getJdbcTemplate().queryForList("select id, name, author, publishDate, comment, status, rentUserId from books");
        for(Map<String, Object> row : rows) {
            books.add(convertToBook(row));
        }
        return books;
    }
    @Override
    public void deleteAll() {
        this.jdbcTemplate.update("delete from books");
    }

    @Override
    public void delete(Book book) {
        this.jdbcTemplate.update("delete from books where id = ?", book.getId());
    }
}


매우 비슷한 코드가 나오게 됨을 알 수 있습니다. 이와 같이 Spring을 사용해서 코드를 만드는 과정 자체는 좋은 코드를 만드는 과정으로 이끌어가게 됩니다. 결론만 나오는 것 같아도, 기본적으로 이러한 과정을 무조건 거치게 만드니까요. 

Spring Jdbc는 다음 method를 주로 사용하게 됩니다. 

# queryForObject : 객체 한개를 return 하는 method. ResultMap<T>를 재정의 해서 사용
# update : insert, update, delete query에서 사용. return값이 존재하지 않고 query를 바로 반영할 때 사용
# queryForList : select에 의해서 List return이 발생할 때 사용. List<Map<String, Object>>가 return 되어, 객체로 변경하거나 map을 읽어서 사용 가능
# queryForInt, queryForLong, queryForDate : query 결과에 따른 값을 return 받을 때 사용.

위 method를 이용해서 UserDao, HistoryDao에 대한 JdbcTemplate 구현 코드를 모두 작성해주세요. 

모든 객체를 구현한다면 다음과 같은 코드 구조를 가지게 될 것입니다. 



각 Dao Impl 들의 테스트 코드 역시 2개씩 존재하게 됩니다. 그리고, 그에 따른 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: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">
  <bean id="sqlExecutor" class="com.xyzlast.bookstore02.dao.SqlExecutor">
    <property name="connectionFactory" ref="connectionFactory" />
  </bean>
  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  <bean id="connectionFactory" class="com.xyzlast.bookstore02.dao.ConnectionFactory"
    init-method="init">
    <property name="connectionString" value="jdbc:mysql://localhost/bookstore" />
    <property name="driverName" value="com.mysql.jdbc.Driver" />
    <property name="username" value="root" />
    <property name="password" value="qwer12#$" />
  </bean>
  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost/bookstore" />
    <property name="username" value="root" />
    <property name="password" value="qwer12#$" />
  </bean>
  
  <bean id="bookDaoImplWithJdbcTemplate" class="com.xyzlast.bookstore02.dao.BookDaoImplWithJdbcTemplate">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="bookDaoImplWithSqlExecutor" class="com.xyzlast.bookstore02.dao.BookDaoImplWithSqlExecutor">
    <property name="sqlExecutor" ref="sqlExecutor"/>
  </bean>
  <bean id="userDaoImplWithJdbcTemplate" class="com.xyzlast.bookstore02.dao.UserDaoImplWithJdbcTemplate">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="userDaoImplWithSqlExecutor" class="com.xyzlast.bookstore02.dao.UserDaoImplWithSqlExecutor">
    <property name="sqlExecutor" ref="sqlExecutor"/>
  </bean>  
  <bean id="historyDaoImplWithJdbcTemplate" class="com.xyzlast.bookstore02.dao.HistoryDaoImplWithJdbcTemplate">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="historyDaoImplWithSqlExecutor" class="com.xyzlast.bookstore02.dao.HistoryDaoImplWithSqlExecutor">
    <property name="sqlExecutor" ref="sqlExecutor"/>
  </bean>
  
  <bean id="userService" class="com.xyzlast.bookstore02.services.UserServiceImpl">
    <property name="bookDao" ref="bookDaoImplWithJdbcTemplate"/>
    <property name="userDao" ref="userDaoImplWithJdbcTemplate"/>
    <property name="historyDao" ref="historyDaoImplWithJdbcTemplate"/>
  </bean>
  <bean id="bookService" class="com.xyzlast.bookstore02.services.BookServiceImpl">
    <property name="bookDao" ref="bookDaoImplWithJdbcTemplate"/>
  </bean>
</beans>


applicationContext.xml의 내용이 점점 복잡해지기 시작합니다. 이렇게 복잡해져가는 applicationContext.xml 의 정리 방법을 한번 알아보도록 하겠습니다. 

properties file을 이용한 중복 데이터의 설정파일화

구성된 applicationContext.xml에서 중복된 데이터가 지금 존재합니다. ConnectionFactory와 DataSource가 바로 그것인데요. 중복되는 String일 뿐 아니라, 환경의 구성에 따라 달리 되는 환경상의 설정이기 때문에 applicationContext.xml과는 달리 관리가 되는 것이 좋을 것 같습니다. 따로 spring.properties 파일을 작성하도록 합니다. 파일 내용은 다음과 같습니다. 

connect.driver=com.mysql.jdbc.Driver
connect.url=jdbc:mysql://localhost/bookstore
connect.username=root
connect.password=qwer12#$

그리고, applicationContext 파일에서 namespace 항목에서 context를 추가합니다. 
context 항목을 추가후, applicationContext에 다음 항목을 추가하고 변경하도록 합니다. 



<?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: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">
  <context:property-placeholder location="spring.property"/>
  <bean id="sqlExecutor" class="com.xyzlast.bookstore02.dao.SqlExecutor">
    <property name="connectionFactory" ref="connectionFactory" />
  </bean>
  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  <bean id="connectionFactory" class="com.xyzlast.bookstore02.dao.ConnectionFactory"
    init-method="init">
    <property name="connectionString" value="${connect.url}" />
    <property name="driverName" value="${connect.driver}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
  </bean>
  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${connect.driver}" />
    <property name="url" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
  </bean>

${PropertyName} 이라는 표현을 통해서, property 파일내에 있는 속성 값을 applicationContext에 등록하게 되는 것을 알 수 있습니다. 이와 같이 property file은 중복되는 applicationContext의 설정을 통합 관리하거나, 환경이나 build path에 종속적이고 개발자 PC에 종속적인 항목들을 관리할 때 주로 사용됩니다. 그리고 ${PropertyName} 표현은 spring, jsp 등에서 객체에 직접 접근할 때 사용되는 표현법으로 자주 나오게 됩니다. 꼭 익혀두도록 합시다. 


annotation을 이용한 bean의 자동 등록

기존의 bean들의 등록은 applicationContext.xml을 통해서 명시적으로 해주고 있습니다. 그리고, applicationContext를 통해서 객체를 직접 가지고 오는 코드를 구성해서 테스트를 하고 있습니다. 전에 소개되었던 @Autowired를 이용하면 applicationContext.xml을 간소화 시킬 수 있습니다.
먼저, Spring에서 bean들의 종류를 나누는 기준에 대해서 알아보겠습니다. Spring에서 보는 bean의 기준에 따라 적용되는 @Autowired annotation이 달라집니다. 

종류설명
@Component일반적인 Util 객체에 사용됩니다. 지금 구성되는 bookstore에서는 ConnectionFactory가 이에 해당됩니다.
@RepositoryDB에 접근되는 객체에 사용됩니다. 일반적으로 dao 객체에 적용이 됩니다.
@ServiceBusiness Logic이 구성되는 Service 객체에 사용됩니다. Business Logic은 여러개의 @Repository의 구성으로 만들어지게 됩니다. Service에 대해서는 다음에 좀더 깊숙히 들어가보도록 하겠습니다.
@ControllerWeb에서 사용되는 객체입니다. Url Request가 연결되는 객체를 지정할 때 사용됩니다. Spring Controller 때 깊게 들어가보도록 하겠습니다.
@Value위 annotation과는 성격이 조금 다릅니다. 다른 annotation은 객체에 할당이 되는 형태이지만, Value annotation은 property의 값을 지정할 때 사용됩니다. property file의 값을 대응시킬때 사용됩니다.


위의 기준으로 지금까지 만들어진 객체들을 나누면 다음과 같습니다.

종류annotation
ConnectionFactory@Component
SqlExecutor@Component
BookDaoImplWithJdbcTemplate@Repository
BookDaoImplWithSqlExecutor@Repository
UserDaoImplWithJdbcTemplate@Repository
UserDaoImplWithSqlExecutor@Repository
HistoryDaoImplWithJdbcTemplate@Repository
HistoryDaoImplWithSqlExecutor@Repository

객체들의 class 선언부에 각 annotation을 적용하고, 자동 등록될 ConnectionFactory와 SqlExecutor에 모두 @Autowired를 달아주도록 합시다. BookDaoImplWithSqlExecutor의 코드는 다음과 같이 변경됩니다.

@Repository
public class BookDaoImplWithSqlExecutor implements BookDao {
    @Autowired
    private SqlExecutor sqlExecutor;
    
    public SqlExecutor getSqlExecutor() {
        return sqlExecutor;
    }

    public void setSqlExecutor(SqlExecutor sqlExecutor) {
        this.sqlExecutor = sqlExecutor;
    }

    private Book convertToBook(ResultSet rs) throws SQLException {
        Book book = new Book();
        book.setId(rs.getInt("id"));
        book.setName(rs.getString("name"));
        book.setAuthor(rs.getString("author"));
        java.util.Date date = new java.util.Date(rs.getTimestamp("publishDate").getTime());
        book.setPublishDate(date);
        book.setComment(rs.getString("comment"));
        book.setStatus(com.xyzlast.bookstore02.entities.BookStatus.get(rs.getInt("status")));
        int rentUserId = rs.getInt("rentUserId");
        if(rentUserId == 0) {
            book.setRentUserId(null);
        }
        else {
            book.setRentUserId(rentUserId);
        }
        return book;
    }
    
    @Override
    public int countAll() {
        return (int) sqlExecutor.execute(new ExecuteSelectQuery() {
            @Override
            public Object execute(Connection conn, PreparedStatement st, ResultSet rs) {
                try {
                    st = conn.prepareStatement("select count(*) from books");
                    rs = st.executeQuery();
                    rs.next();
                    return rs.getInt(1);
                }
                catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @Override
    public void add(final Book book) {
        sqlExecutor.execute(new ExecuteUpdateQuery() {
            @Override
            public void execute(Connection conn, PreparedStatement st) {
                try {
                    st = conn.prepareStatement("insert books(id, name, author, publishDate, status, comment, rentUserId) values(?, ?, ?, ?, ?, ?, ?)");
                    st.setInt(1, book.getId());
                    st.setString(2, book.getName());
                    st.setString(3, book.getAuthor());
                    st.setTimestamp(4, new Timestamp(book.getPublishDate().getTime()));
                    st.setInt(5, book.getStatus().intValue());
                    st.setString(6, book.getComment());
                    if(book.getRentUserId() == null) {
                        st.setNull(7, Types.INTEGER);
                    }
                    else {
                        st.setInt(7, book.getRentUserId());
                    }
                    st.executeUpdate();
                }
                catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @Override
    public void update(final Book book) {
        sqlExecutor.execute(new ExecuteUpdateQuery() {
            @Override
            public void execute(Connection conn, PreparedStatement st) {
                try {
                    st = conn.prepareStatement("update books set name=?, author=?, publishDate=?, status=?, comment=?, rentUserId=? where id=?");
                    st.setString(1, book.getName());
                    st.setString(2, book.getAuthor());
                    st.setTimestamp(3, new Timestamp(book.getPublishDate().getTime()));
                    st.setInt(4, book.getStatus().intValue());
                    st.setString(5, book.getComment());
                    if(book.getRentUserId() == null) {
                        st.setNull(6, Types.INTEGER);
                    }
                    else {
                        st.setInt(6, book.getRentUserId());
                    }
                    st.setInt(7, book.getId());
                    st.executeUpdate();
                } catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @Override
    public void delete(final Book book) {
        sqlExecutor.execute(new ExecuteUpdateQuery() {
            @Override
            public void execute(Connection conn, PreparedStatement st) {
                try {
                    st = conn.prepareStatement("delete from books where id = ?");
                    st.setInt(1, book.getId());
                } catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @Override
    public void deleteAll() {
        sqlExecutor.execute(new ExecuteUpdateQuery() {
            @Override
            public void execute(Connection conn, PreparedStatement st) {
                try {
                    st = conn.prepareStatement("delete from books");
                    st.executeUpdate();
                } catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @Override
    public Book get(final int id) {
        return (Book) sqlExecutor.execute(new ExecuteSelectQuery() {
            @Override
            public Object execute(Connection conn, PreparedStatement st, ResultSet rs) {
                try {
                    st = conn.prepareStatement("select id, name, author, publishDate, comment, status, rentUserId from books where id = ?");
                    st.setInt(1, id);
                    rs = st.executeQuery();
                    rs.next();
                    return convertToBook(rs);
                } catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Book> getAll() {
        return (List<Book>) sqlExecutor.execute(new ExecuteSelectQuery() {
            @Override
            public Object execute(Connection conn, PreparedStatement st, ResultSet rs) {
                try {
                    st = conn.prepareStatement("select id, name, author, publishDate, comment, status, rentUserId from books");
                    rs = st.executeQuery();
                    List<Book> books = new ArrayList<>();
                    while(rs.next()) {
                        books.add(convertToBook(rs));
                    }
                    return books;
                }catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Book> search(final String name) {
        return (List<Book>) sqlExecutor.execute(new ExecuteSelectQuery() {
            @Override
            public Object execute(Connection conn, PreparedStatement st, ResultSet rs) {
                try {
                    st = conn.prepareStatement("select id, name, author, publishDate, comment, status, rentUserId from books where name like '%" + name + "%'");
                    rs = st.executeQuery();
                    List<Book> books = new ArrayList<>();
                    while(rs.next()) {
                        books.add(convertToBook(rs));
                    }
                    return books;
                }catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }
}

다른 코드들 역시 같이 변경해보도록 합니다. JdbcTemplate을 사용하는 객체 역시 Property의 jdbcTemplate에 @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: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">

  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${connect.driver}" />
    <property name="url" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
  </bean>
  <context:property-placeholder location="classpath:spring.property"/>
  <context:component-scan base-package="com.xyzlast.bookstore02.dao"/>
</beans>

지금까지 만들어진 코드가 엄청나게 많이 바뀌게 됩니다. 이렇게 된 후에 다음 테스트 코드를 작성해서 ApplicationContext.xml을 통해 객체가 어떻게 구성이 되었는지 알아보도록 하겠습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContextWithAutowired.xml")
public class ApplicationContextTest {
    @Autowired
    ApplicationContext applicationContext;

    @Test
    public void getBeansInApplicationContext() {
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        for(String beanName : beanNames) {
            System.out.println(beanName + " : " + applicationContext.getBean(beanName));
        }
    }
}


jdbcTemplate : org.springframework.jdbc.core.JdbcTemplate@5d63838
dataSource : org.springframework.jdbc.datasource.DriverManagerDataSource@3304e786
org.springframework.context.support.PropertySourcesPlaceholderConfigurer#0 : org.springframework.context.support.PropertySourcesPlaceholderConfigurer@6fc2895
bookDaoImplWithJdbcTemplate : com.xyzlast.bookstore02.dao.BookDaoImplWithJdbcTemplate@14cc51c8
bookDaoImplWithSqlExecutor : com.xyzlast.bookstore02.dao.BookDaoImplWithSqlExecutor@720d2c22
connectionFactory : com.xyzlast.bookstore02.dao.ConnectionFactory@3ecca6ad
historyDaoImplWithJdbcTemplate : com.xyzlast.bookstore02.dao.HistoryDaoImplWithJdbcTemplate@6dd2c810
sqlExecutor : com.xyzlast.bookstore02.dao.SqlExecutor@294ccac4
userDaoImplWithJdbcTemplate : com.xyzlast.bookstore02.dao.UserDaoImplWithJdbcTemplate@70941f0a
userDaoImplWithSqlExecutor : com.xyzlast.bookstore02.dao.UserDaoImplWithSqlExecutor@c820344
org.springframework.context.annotation.internalConfigurationAnnotationProcessor : org.springframework.context.annotation.ConfigurationClassPostProcessor@2ba46bc6
org.springframework.context.annotation.internalAutowiredAnnotationProcessor : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@379faa8c
org.springframework.context.annotation.internalRequiredAnnotationProcessor : org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor@5375e9db
org.springframework.context.annotation.internalCommonAnnotationProcessor : org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@624c53ab
org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor : org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor@2af9150

보시면, context:component-scan을 통해서 package안에 annotation이 된 모든 객체들이 모두 ApplicationContext에 등록된 것을 알 수 있습니다. 

@Autowired는 어떻게 처리가 되는 것일까요? @Autowired는 먼저 @Autowired가 필요 없는 bean들이 먼저 등록이 된 후에 @Autowired로 property를 설정할 수 있는 객체들을 차례로 등록하게  됩니다. 지금 코드에서는 jdbcTemplate와 dataSource는 @Autowired되는 속성이 하나도 없기 때문에 먼저 ApplicationContext에 등록이 되고 나머지 객체들이 등록되고 있는 것을 알 수 있습니다. 그럼 spring은 어떻게 Autowired를 이용해서 bean들을 등록할 수 있을까요? Autowired 동작은 다음 조건 중 선행되는 조건을 확인하고 우선적으로 wired되게 됩니다. 

1. 객체 type이 동일하고, property의 이름이 bean 이름과 동일할 때
2. 객체 type이 동일 할때

여기서 문제가 될 수 있는 항목이 2번째입니다. 객체 type이 동일하지만 bean의 이름이 property랑 맞지 않는 객체가 여러개가 존재를 할 수 있습니다. 이때, Spring은 Not Unique Beans in application context 에러를 발생시키며 applicationContext.xml 로드를 실패하게 됩니다. 여러 객체가 한개의 interface를 구현해서 사용하게 되었을 때, 객체를 Autowired해서 사용하기 위해서는 사용할 객체의 이름을 Spring 규칙에 맞추어 변경한 Property 이름으로 정해줘야지 됩니다. Spring은 기본적으로 객체 이름의 첫자를 소문자로 만들어 bean 이름으로 등록하게 됩니다. 아니면 다른 방법이 있습니다. @Repository, @Component와 같은 Autowird annotation은 생성자로 객체의 자동 등록 이름을 정해줄 수 있습니다. 자동 등록 이름을 이용해서 객체를 등록하고 그 이름에 맞는 Property 이름으로 사용하는 경우 동일한 결과를 가지고 올 수 있습니다. Spring에서 이름을 만드는 규칙과 같이 이를 이해하는 것이 중요합니다. 


Summary

JdbcTemplate을 통한 Dao의 구성방법에 대해서 알아봤습니다. 그리고 applicationContext의 autowired 방법에 대해서 알아봤습니다. applicationContext의 autowired의 사용법은 잘 알아두시길 바랍니다. 객체의 type위주로 autowired되는 것을 명시하고, 그 사용법을 잘 익혀두지 않으면 실제 프로젝트에서 어떻게 사용되는지 알기가 힘들어집니다. 지금까지 구성된 BookDao, UserDao, HistoryDao를 모두 JdbcTemplate으로 구성해보시고, Autowired를 이용해서 applicationContext에 모두 등록시켜주세요. 



Posted by Y2K
,

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


코드 정리 - 객체간의 중복 코드 정리

지금까지 우리는 books라는 한개의 table에 CRUD action을 진행해왔습니다. 그렇지만, 개발에서는 한개의 Table만에 CRUD를 하는 경우보다, 한 Action에 대하여 여러개의 Table을 CRUD 하게 되는 것이 일반적입니다. 

그래서, 이 두개의 개념을 나누게 되는데요.
하나의 Action이라는 것은 Business Logic이라고 할 수 있고, 한 Table에 대한 CRUD는 DB에 종속적인 작업이라고 할 수 있습니다.
전자를 일반적으로 Service라고 지칭하고, 후자를 DAO (Data Access Object)라고 지칭하는 것이 일반적입니다. 

이는 java에서 개발중에 package로 나누게 되는 것일 일반적입니다. dao package와 service package를 나누것과 같이 이런 식으로 나눠주는 것이 좋습니다.

1개의 Service는 여러개의 DAO를 가지고 있고, DAO를 이용한 BL을 서술하는 것이 일반적입니다. DAO는 최대한 단순하게 Table에 대한 Query들로 구성이 되고, 그 Query를 통해서 결과를 얻어내는 수단으로 주로 사용됩니다.
bookstore에 Business Logic(BL)을 추가하기 전에 book의 상태를 서술할수 있는 property와 현재 책을 빌려간 사용자의 정보를 저장할 수 있는 Column을 두개 추가하고, users table을 추가해서 Dao를 좀더 구성해보도록 하겠습니다. 

먼저 books table에 대한 작업부터 진행하도록 하겠습니다.
책의 상태를 나타낼 수 있는 state property를 추가합니다.
create table books (
  id Integer AUTO_INCREMENT PRIMARY KEY,
  name varchar(255) NOT NULL,
  author varchar(50) NOT NULL,
  publishDate timestamp NOT NULL,
  comment varchar(255),
  status Integer NOT NULL,
  rentUserId Integer
);
ALTER TABLE bookstore.books ADD status Integer NOT NULL;
ALTER TABLE bookstore.books ADD rentUserId Integer;


int type으로 status의 값을 0은 bookstore에 있는 대여 가능, 1은 대여중, 2는 분실 상태로 만들어줍니다. 그런데, 이런 식의 status code는 따로 code table을 만들어주거나 code 상의 enum 값을 이용해서 명확하게 구분하는 것이 훨씬 코드 관리에 용의합니다.  enum의 code는 다음과 같습니다.

public enum BookStatus {
    CanRent(0),
    RentNow(1),
    Missing(2);

    private int value;
    private BookStatus(int value) {
        this.value = value;
    }

    public int intValue() {
        return this.value;
    }

    public static BookStatus valueOf(int value) {
        switch(value) {
        case 0 : return CanRent;
        case 1 : return RentNow;
        case 2 : return Missing;
        default:
            throw new IllegalArgumentException();
        }
    }
}

불분명한 숫자값에 명확한 의미의 enum을 적용함으로서, 코드를 보다더 보기 편하게 만들어줬습니다. 이제 BookStatus를 Book에 추가하도록 하겠습니다.
또한, rentUserId column이 nullable인것에 주목해주세요. 이 부분을 반영하기 위한 Entity는 어떻게 작성해야지 될까요?

마지막으로 기존의 BookApp은 BookDao의 의미가 더 강하기 때문에, package의 이름을 dao로 변경하고, BookApp을 BookDao로 변경하도록 하겠습니다. 전체 코드에 대한 손을 좀 봐야지 됩니다. 한번 고쳐보시길 바랍니다.
코드의 수정이 모두 완료되었으면 테스트 코드를 통해서 코드가 정상적으로 수정이 된 것을 확인해야지 됩니다. 

public class BookDao {
    private ConnectionFactory connectionFactory;

    interface ExecuteUpdateQuery {
        PreparedStatement getPreparedStatement(Connection conn) throws SQLException;
    }

    interface ExecuteSelectQuery {
        PreparedStatement getPreparedStatement(Connection conn) throws SQLException;
        Object parsetResultSet(ResultSet rs) throws SQLException;
    }

    private void execute(ExecuteUpdateQuery query)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        Connection conn = this.connectionFactory.getConnection();
        PreparedStatement st = null;
        try {
            st = query.getPreparedStatement(conn);
            st.executeUpdate();
        }
        finally {
            if(st != null) {
                try {
                    st.close();
                }catch(Exception ex) {}
            }
            if(conn != null) {
                try {
                    conn.close();
                }catch(Exception ex) {}
            }
        }
    }

    private Object execute(ExecuteSelectQuery query)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        Connection conn = this.connectionFactory.getConnection();
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            st = query.getPreparedStatement(conn);
            rs = st.executeQuery();
            return query.parsetResultSet(rs);
        }
        finally {
            if(rs != null) {
                try {
                    rs.close();
                } catch(Exception ex) {}
            }
            if(st != null) {
                try {
                    st.close();
                }catch(Exception ex) {}
            }
            if(conn != null) {
                try {
                    conn.close();
                }catch(Exception ex) {}
            }
        }
    }

    private Book convertToBook(ResultSet rs) throws SQLException {
        Book book = new Book();
        book.setId(rs.getInt("id"));
        book.setName(rs.getString("name"));
        book.setAuthor(rs.getString("author"));
        java.util.Date date = new java.util.Date(rs.getTimestamp("publishDate").getTime());
        book.setPublishDate(date);
        book.setComment(rs.getString("comment"));
        book.setStatus(BookStatus.valueOf(rs.getInt("status")));

        return book;
    }

    public void add(final Book book) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        execute(new ExecuteUpdateQuery() {
            @Override
            public PreparedStatement getPreparedStatement(Connection conn) throws SQLException {
                PreparedStatement st = conn.prepareStatement("insert books(id, name, author, publishDate, comment, status) values(?, ?, ?, ?, ?, ?)");
                st.setInt(1, book.getId());
                st.setString(2, book.getName());
                st.setString(3, book.getAuthor());
                java.sql.Date sqlDate = new java.sql.Date(book.getPublishDate().getTime());
                st.setDate(4, sqlDate);
                st.setString(5, book.getComment());
                st.setInt(6, book.getStatus().intValue());
                return st;
            }
        });
    }

    public Book get(final int id) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        return (Book) execute(new ExecuteSelectQuery() {
            @Override
            public PreparedStatement getPreparedStatement(Connection conn) throws SQLException {
                PreparedStatement st = conn.prepareStatement("select id, name, author, publishDate, comment from books where id=?");
                st.setInt(1, id);
                return st;
            }

            @Override
            public Object parsetResultSet(ResultSet rs) throws SQLException {
                rs.next();
                Book book = convertToBook(rs);
                return book;
            }
        });
    }

    @SuppressWarnings("unchecked")
    public List<Book> search(final String name) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        return (List<Book>) execute(new ExecuteSelectQuery() {
            @Override
            public PreparedStatement getPreparedStatement(Connection conn) throws SQLException {
                String query = "select id, name, author, publishDate, comment from books where name like '%" + name +"%'";
                return conn.prepareStatement(query);
            }

            @Override
            public Object parsetResultSet(ResultSet rs) throws SQLException {
                List<Book> books = new ArrayList<>();
                while(rs.next()) {
                    books.add(convertToBook(rs));
                }
                return books;
            }
        });
    }

    public int countAll() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        return (Integer) execute(new ExecuteSelectQuery() {
            @Override
            public PreparedStatement getPreparedStatement(Connection conn) throws SQLException {
                return conn.prepareStatement("select count(*) from books");
            }

            @Override
            public Object parsetResultSet(ResultSet rs) throws SQLException {
                rs.next();
                return rs.getInt(1);
            }
        });
    }

    public void update(final Book book) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        execute(new ExecuteUpdateQuery() {
            @Override
            public PreparedStatement getPreparedStatement(Connection conn) throws SQLException {
                PreparedStatement st = conn.prepareStatement("update books set name=?, author=?, publishDate=?, comment=?, status=? where id=?");
                st.setInt(6, book.getId());
                st.setString(1, book.getName());
                st.setString(2, book.getAuthor());
                st.setTimestamp(3, new Timestamp(book.getPublishDate().getTime()));
                st.setString(4, book.getComment());
                st.setInt(5, book.getStatus().intValue());
                return st;
            }
        });
    }

    @SuppressWarnings("unchecked")
    public List<Book> getAll() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        return (List<Book>) execute(new ExecuteSelectQuery() {
            @Override
            public PreparedStatement getPreparedStatement(Connection conn) throws SQLException {
                return conn.prepareStatement("select id, name, author, publishDate, comment from books");
            }

            @Override
            public Object parsetResultSet(ResultSet rs) throws SQLException {
                List<Book> books = new ArrayList<>();
                while(rs.next()) {
                    books.add(convertToBook(rs));
                }
                return books;
            }
        });
    }

    public void deleteAll() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        execute(new ExecuteUpdateQuery() {
            @Override
            public PreparedStatement getPreparedStatement(Connection conn) throws SQLException {
                return conn.prepareStatement("delete from books");
            }
        });
    }

    public ConnectionFactory getConnectionFactory() {
        return connectionFactory;
    }

    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
    }
}

이제 사용자를 추가해보도록 하겠습니다. users는 다음과 같습니다. 그리고, books와 users 간의 FK를 잡아주도록 합시다.

create table users (
    id Integer AUTO_INCREMENT PRIMARY KEY,
    name varchar(50) NOT NULL,
    password varchar(12) NOT NULL,
    point Integer NOT NULL,
    level Integer NOT NULL  
);
ALTER TABLE bookstore.books ADD CONSTRAINT books_users_FK FOREIGN KEY (rentUserId) REFERENCES bookstore.users(id) ON DELETE SET NULL;

간단히 이름과 비밀번호를 가진 너무나 단순한 키입니다. 역시 마찬가지로 User의 level에 대한 enum값을 만들고, User에 대한 entity를 정의하고, BookDao와 동일한 method들을 모두 만들어주겠습니다.
level에 대한 enum은 NORMAL, READER, MVP로 만들어주세요.

마지막으로 histories Table을 추가합니다. table query는 다음과 같고, users와 books에 FK를 갖습니다.
create table histories (
    id Integer AUTO_INCREMENT PRIMARY KEY,
    userId Integer NOT NULL,
    bookId Integer NOT NULL,
    actionType Integer NOT NULL,
    insertDate timestamp NOT NULL
);
ALTER TABLE bookstore.histories ADD CONSTRAINT history_userFK FOREIGN KEY (userId) REFERENCES bookstore.users(id);
ALTER TABLE bookstore.histories ADD CONSTRAINT history_bookFK FOREIGN KEY (bookId) REFERENCES bookstore.books(id);
 
users와 histories에 대한 모든 dao class를 만들어주세요.  그리고 Histoary의 ActionType은 RENT/RETURN을 각각 0, 1로 잡는 enum으로 구성해주면 전체 table의 구성이 모두 마쳐집니다. 
여기까지 구성을 해보는 것이 이번주 과제입니다. 조금 코드양이 많을것 같네요.  각각의 Dao class의 method 정의는 다음과 같습니다.  이 두개의 Dao 객체를 모두 작성하고, Test code를 작성해주세요.

public class UserDao {
    public User get(int userId) {

    }
    public void deleteAll() {

    }
    public int countAll() {

    }

    public void add(User user) {

    }

    public void update(User user) {

    }

    public List<User> getAll() {

    }
}

public class HistoryDao {
    public void deleteAll() {
        
    }
    
    public void add(History history) {
        
    }
    
    public int countAll() {
        
    }
    
    public List<History> getAll() {
        
    }
    
    public List<History> getByUser(int userId) {
        
    }
    
    public List<History> getByBook(int bookId) {
        
    }
}




Dao를 구현하면 또 다른 중복 코드가 발견되는 것을 알 수 있습니다. 지금까지 구현했던 Template-callback 코드가 계속해서 나타나게 됩니다. 이 부분에 대한 중복 코드를 제거하는 것이 필요합니다. SQL에 대한 직접적인 실행을 하는 객체로 SqlExecutor로 객체를 새로 생성하고, SqlExecutor를 외부에서 DI 할 수 있도록 코드를 수정하도록 합시다. 
SqlExecutor의 코드는 다음과 같습니다. 

public class SqlExecutor {
    private ConnectionFactory connectionFactory;

    public ConnectionFactory getConnectionFactory() {
        return this.connectionFactory;
    }

    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
    }

    public void execute(ExecuteUpdateQuery query)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        Connection conn = this.connectionFactory.getConnection();
        PreparedStatement st = null;
        try {
            st = query.getPreparedStatement(conn);
            st.executeUpdate();
        }
        finally {
            if(st != null) {
                try {
                    st.close();
                }catch(Exception ex) {}
            }
            if(conn != null) {
                try {
                    conn.close();
                }catch(Exception ex) {}
            }
        }
    }

    public Object execute(ExecuteSelectQuery query)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        Connection conn = this.connectionFactory.getConnection();
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            st = query.getPreparedStatement(conn);
            rs = st.executeQuery();
            return query.parsetResultSet(rs);
        }
        finally {
            if(rs != null) {
                try {
                    rs.close();
                } catch(Exception ex) {}
            }
            if(st != null) {
                try {
                    st.close();
                }catch(Exception ex) {}
            }
            if(conn != null) {
                try {
                    conn.close();
                }catch(Exception ex) {}
            }
        }
    }
}

지금까지 구현된 모든 Dao를 SqlExecutor를 사용하도록 코드를 작성하고, spring을 사용하도록 DI를 구성한 테스트 코드를 작성해서 검증하도록 합시다.

과제가 나타났습니다!!!! 


<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="connectionFactory" class="com.xyzlast.bookstore02.dao.ConnectionFactory" init-method="init">
        <property name="connectionString" value="jdbc:mysql://localhost/bookstore"/>
        <property name="driverName" value="com.mysql.jdbc.Driver"/>
        <property name="username" value="root"/>
        <property name="password" value="qwer12#$"/>
    </bean>
    <bean id="sqlExecutor" class="com.xyzlast.bookstore02.dao.SqlExecutor">
        <property name="connectionFactory" ref="connectionFactory"/>
    </bean>
    <bean id="bookDao" class="com.xyzlast.bookstore02.dao.BookDao">
        <property name="sqlExecutor" ref="sqlExecutor"/>
    </bean>
    <bean id="userDao" class="com.xyzlast.bookstore02.dao.UserDao">
        <property name="sqlExecutor" ref="sqlExecutor"/>
    </bean>
    <bean id="historyDao" class="com.xyzlast.bookstore02.dao.HistoryDao">
        <property name="sqlExecutor" ref="sqlExecutor"/>
    </bean></beans>



코드 정리 - Exception의 처리

지금까지 보시면 SqlExecutor를 이용한 코드의 간결화 및 spring을 이용한 전체 코드의 최적화를 해온것을 알 수 있습니다. 

마지막으로, 지금 저희 코드에 중복이 되는 코드를 찾아보도록 합시다. 
딱히 문제가 특출나게 보이지는 않습니다. 지금까지 상당한 refactoring을 통해서 변경시킨 코드에 문제가 쉽게 보이면 그것 역시 문제가 될 수 있습니다.

지금 모든 코드에 나타나 있는 Exception이 선언되어 있습니다. DB access 코드에 일괄적으로 들어가 있는 Exception들은 다음과 같습니다.

# InstantiationException : Class.forName 에서 객체의 이름이 아닌, interface의 이름이 들어간 경우에 발생하는 에러.
# IllegalAccessException : Db Connection시, 권한이 없거나 id/password가 틀린 경우에 발생하는 에러
# ClassNotFoundException : Class.forName 을 이용, DB Connection 객체를 생성할 때 객체의 이름이 틀린 경우에 발생하는 에러
# SQLException : SQL query가 잘못된 Exception입니다. 

Java는 2개의 Exception type을 가지고 있습니다. checked exception과 Runtime Exception인데요. checked exception의 경우, 이 exception이 발생하는 경우에는 반드시 exception을 처리해줘야지 됩니다. 또는 상위 method로 throw를 해줘야지 됩니다.
Runtime exception은 상위에서 처리를 안해줘도 되고요. 대표적인 것은 NullPointerException, UnsupportedOperationException, IllegalArgumentException 등이 있습니다.

이 부분은 매우 중요한 개념입니다. java에서의 exception은 사용자가 처리해줘야지 될 것(체크 해야지 될 exception)과 Runtime 시(실행시에) 확인되어야지 될 것들로 나뉘게 됩니다. exception에 대하여 보다더 확실한 처리를 해주길 바란 java의 설계 원칙이지만, 근간에는 비판이 좀 많은 부분이기도 합니다. java의 exception에 대한 정리를 한번 해주시는 것이 필요합니다. 

그럼, 지금 저희가 사용한 코드중에서 getConnection() method를 확인해보겠습니다. 

    public Connection getConnection() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        Connection conn = DriverManager.getConnection (this.connectionString, this.username, this.password);
        return conn;
    }


이 method는 이 4개의 exception을 반드시 처리하도록 되어 있습니다. 또는 이 4개의 exception을 사용한 method로 던져줘서 상위 method에서 처리하도록 되어 있는데요. DB 접속과 SQL query가 잘못된 경우에 대한 exception 처리는 과연 할 수 있을까요? 이 Exception은 처리할 수 없는 Exception을 넘겨줘서 되는 것이 아닌가. 라는 생각을 할 수 있습니다. 잘 보면 대부분의 DB 접속시에 나오는 대부분의 Exception은 처리가 불가능한 Exception이라고 할 수 있습니다. 에러의 내용은 도움이 될 수 있지만, 이 에러가 발생했을 때 어떠한 처리를 하지를 못하는 경우가 대다수라는거지요. 그래서 SQL Exception들을 다음과 같이 처리해줘도 괜찮습니다. 

    public Connection getConnection() {     
        Connection conn = null;
        try {
            conn = DriverManager.getConnection (this.connectionString, this.username, this.password);
        } catch (SQLException e) {
            throw new IllegalArgumentException(e);
        }
        return conn;
    }
    
    public void init() {
        try {
            Class.forName(this.driverName).newInstance();
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
            throw new IllegalArgumentException(e);
        }
    }


모든 Exception들은 초기 값들. id, password, connectionString, driver name 등이 잘못된 것이기 때문에 input 값이 잘못되었다는 의미의 IllegalArgumentException으로 변경해서 던져줬습니다. IllegalArgumentException은 Runtime Exception 이기 때문에 처리를 해줄 필요가 없습니다. 이렇게 Dao 객체들의 method를 다시 처리해주면 좀더 코드가 깔끔해지는 것을 알 수 있습니다. 




Posted by Y2K
,

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


지난 장에서는 JDBC를 이용한 db의 접근. 그리고 간단한 table의 CRUD를 하는 방법에 대해서 조금 깊게 들어가봤습니다. 또한 저번 시간의 최대 포인트는 테스트입니다. 테스트를 어떻게 작성을 하는지에 대한 논의와 테스트 코드를 직접 사용해보는 시간을 가져봤습니다. 이번 시간에는 드디어 Spring을 이용한 코드에 대해서 논의해보도록 하겠습니다.기존 코드의 가장 큰 문제는 무엇인가요?

1.  DB connection이 변경되는 경우, compile을 다시 해줘야지 된다.
2.  DB connection의 open/close가 모든 method에서 반복된다.

크게 보면 이 두가지의 문제가 나오게 됩니다. 

Connection은 외부에서 설정할 수 있는 영역입니다. 특히 이런 부분은 config file로 관리가 되는 것이 일반적이고, Enterprise 구성환경에서는 JNDI를 이용한 DB Connection이 제공되는 경우도 많습니다. 따라서, 외부에서 설정이 가능한 것이라고 생각해도 좋습니다. 또한, BookApp은 books table에 CRUD를 하는 것을 목적으로 하는 객체입니다. 객체에 대한 가장 큰 원칙인 단일 책임의 원칙에 의해서, BookApp에서 Connection까지 관리가 되는 것은 영역을 넘어가게 됩니다. 또한, books 이외의 table이 존재할 때, Connection에 대한 코드는 중복될 수 밖에 없는 코드가 됩니다. 따라서 Connection을 제공하는 객체로 따로 분리를 해줍시다. 

Connection을 관리, 생성하는 객체이기 때문에 ConnectionFactory라고 명명하고, 객체를 구성합니다. 객체의 코드는 다음과 같습니다.

public class ConnectionFactory {
     private String connectionString;
     private String driverName;
     private String username;
     private String password;
    
     public Connection getConnection() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          Class.forName(this.driverName).newInstance();
          Connection conn = DriverManager.getConnection (this.connectionString, this.username, this.password);
          return conn;
     }
     public String getConnectionString() {
          return connectionString;
     }
     public void setConnectionString(String connectionString) {
          this.connectionString = connectionString;
     }
     public String getDriverName() {
          return driverName;
     }
     public void setDriverName(String driverName) {
          this.driverName = driverName;
     }
     public String getUsername() {
          return username;
     }
     public void setUsername(String username) {
          this.username = username;
     }
     public String getPassword() {
          return password;
     }
     public void setPassword(String password) {
          this.password = password;
     }
}

그리고, ConnectionFactory를 이용한 코드로 TestCode를 다시 구성해보도록 하겟습니다. 

public class BookAppTest {
     private BookApp bookApp = new BookApp();
     private ConnectionFactory connectionFactory = new ConnectionFactory("com.mysql.jdbc.Driver", "jdbc:mysql://localhost/bookstore", "root", "qwer12#$");
    
     private List<Book> getBooks() {
          Book book1 = new Book();
          book1.setId(1);
          book1.setName("book name01");
          book1.setAuthor("autor name 01");
          book1.setComment("comment01");
          book1.setPublishDate(new Date());
         
          Book book2 = new Book();
          book2.setId(2);
          book2.setName("book name02");
          book2.setAuthor("autor name 02");
          book2.setComment("comment02");
          book2.setPublishDate(new Date());
          Book book3 = new Book();
          book3.setId(3);
          book3.setName("book name03");
          book3.setAuthor("autor name 03");
          book3.setComment("comment03");
          book3.setPublishDate(new Date());
         
          List<Book> books = Arrays.asList(book1, book2, book3);
          return books;
     }
    
     private void compareBook(Book book) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          Book dbBook = bookApp.get(book.getId());
          assertThat(dbBook.getName(), is(book.getName()));
          assertThat(dbBook.getAuthor(), is(book.getAuthor()));
          assertThat(dbBook.getComment(), is(book.getComment()));
          assertThat(dbBook.getPublishDate().toString(), is(book.getPublishDate().toString()));
     }
    
     @Before
     public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          bookApp.setConnectionFactory(connectionFactory);
          bookApp.deleteAll();
          assertThat(bookApp.countAll(), is(0));
     }
    
     @Test
     public void addAndCount() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          List<Book> books = getBooks();
          int count = 0;
          for(Book book : books) {
               bookApp.add(book);
               count++;
               assertThat(bookApp.countAll(), is(count));
          }
     }
    
     @Test
     public void update() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          List<Book> books = getBooks();
          int count = 0;
          for(Book book : books) {
               bookApp.add(book);
               count++;
               assertThat(bookApp.countAll(), is(count));
              
               book.setName("changed name");
               book.setPublishDate(new Date());
               book.setAuthor("changed author");
               bookApp.update(book);
              
               compareBook(book);
          }
     }
    
     @Test
     public void getAll() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          List<Book> books = getBooks();
          int count = 0;
          for(Book book : books) {
               bookApp.add(book);
               count++;
               assertThat(bookApp.countAll(), is(count));
          }
         
          List<Book> books2 = bookApp.getAll();
          assertThat(books2.size(), is(books.size()));
     }
    
     @Test
     public void search() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          List<Book> books = getBooks();
          int count = 0;
          for(Book book : books) {
               bookApp.add(book);
               count++;
               assertThat(bookApp.countAll(), is(count));
          }
         
          List<Book> searchedBooks = bookApp.search("01");
          assertThat(searchedBooks.size(), is(1));
         
          searchedBooks = bookApp.search("02");
          assertThat(searchedBooks.size(), is(1));
         
          searchedBooks = bookApp.search("03");
          assertThat(searchedBooks.size(), is(1));
         
          searchedBooks = bookApp.search("name");
          assertThat(searchedBooks.size(), is(3));
     }
}

지금까지 구성된 BookApp을 지금 사용하는 객체는 test 객체만이 유일한 client입니다. 사용하는 객체(client)에서 자신이 사용할 객체의 구성을 결정하는 행위를 IoC(inverse of control)이라고 합니다. 

여기서, Spring의 가장 주요한 기능을 사용할 때가 왔습니다. 

Spring은 IoC를 지원하는 경량 container입니다. 

그런데, IoC를 (어떻게) 지원하는 지에 대한 설명을 한다면 어려운 말로 DI를 통해서 지원한다고 할 수 있습니다. DI란 Dependency Injection의 약자입니다. 사용할 객체의 Dependency를 inject 한다는 뜻입니다. 여기서 말이 좀더 어려워서 예시를 이용해서 바꿔주도록 하겠습니다. 

BookApp은 ConnectionFactory에 의존한다.
BookAppTest는 BookApp에 의존한다.
BookAppTest는 BookApp의 ConnectionFactory를 변경시켜서 사용한다.

최종적으로, BookAppTest는 BookApp을 사용하지만, BookApp이 종속되어 있는 ConnectionFactory를 변경시켜서 사용하게 됩니다. 이때, 의존되어 있는 ConnectionFactory를 변경하는 행위를 DI(dependency Injection)이라고 합니다. 따라서, 위 코드를 좀 유식한 말로 풀어서 한다면, BookApp을 DI를 통해 사용하는 코드 라고 할 수 있습니다. 이를 그림으로 도식화 해보면 다음과 같습니다. 


지금 이야기드린 IoC, DI는 Spring에서 굉장히 중요한 개념입니다. Spring은 light DI container라고 할 정도로 Spring의 핵심 기능중 하나입니다. 



지금까지 작성된 코드를 보면 처음과는 다른 특징을 가지고 있습니다.
1. 테스트를 통해서 검증이 가능합니다.
2. connectionString 및 jdbc Driver를 사용하는 code내에서 찾아서 사용합니다.
3.중복 코드가 존재하지 않습니다.

위의 특징은 잘 짜진 코드라면 당연히 가져야지 되는 특징입니다.


Spring을 이용한 IoC/DI

spring을 이용하면 지금까지 구성되었던 코드를 보다 더 깔끔하게 만들어줄 수 있습니다. 먼저 spring에 대한 dependency를 추가시켜줍니다.

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>3.2.0.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>3.2.0.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>3.2.0.RELEASE</version>
      <scope>test</scope>
    </dependency>

spring-test는 scope를 이용해서, test code에서만 사용하도록 지정한것을 제외하고는 mysql dependency를 추가하는 방법과 동일하게 설정해줍니다.
applicationContext.xml 파일을 src/test/resource에 추가하고 다음과 같이 적어줍니다.
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="connectionFactory" class="com.xyzlast.bookstore01.domain.ConnectionFactory">
        <property name="connectionString" value="jdbc:mysql://localhost/bookstore"/>
        <property name="driverName" value="com.mysql.jdbc.Driver"/>
        <property name="username" value="root"/>
        <property name="password" value="qwer12#$"/>
    </bean>
    <bean id="bookApp" class="com.xyzlast.bookstore01.domain.BookApp">
        <property name="connectionFactory" ref="connectionFactory"/>
    </bean></beans>

solution의 directory 구조는 다음과 같습니다. 





테스트 코드를 다음과 같이 수정합니다.


    @Before
    public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        ApplicationContext context = new GenericXmlApplicationContext("/appContext.xml");
        bookApp = (BookApp) context.getBean("bookApp");
        System.out.println(bookApp);
        bookApp.deleteAll();
        assertThat(bookApp.countAll(), is(0));
    }


테스트 코드가 모두 통과됨을 알 수 있습니다. 어떻게 해서 이렇게 된 것인지 서술해보도록 하겠습니다.
applicationContext.xml 파일은 사용할 객체에 대한 정의 파일입니다. spring에서는 ApplicationContext로 불리우는 객체에 대한 Hashtable 이라고 생각하시면 됩니다.
appContext에 정의된 ConnectionFactory 객체와 BookApp 이라는 객체에 대한 관계를 봐보시면 좀 더 생각이 쉬울 수 있습니다. 먼저 connectionFactory 라는 이름으로 ConnectionFactory를 정의합니다. 저희가 property로 뽑은 connectionString, driverName, username, password에 대한 값들을 모두 설정하는 것을 볼 수 있습니다. 그리고, bookApp이라는 이름으로 설정된 BookApp 객체를 보시면 좀더 재미잇는 코드를 볼 수 있습니다. ref 라는 키워드로 값을 설정하고 있는데. 이는 먼저 설정된 connectionFactory 객체를 bookApp에 주입(Inject) 시키고 있는 것을 알 수 있습니다.
그리고, 테스트 코드에 대해서 알아보도록 하겠습니다. 테스트 코드에서는 먼저 GenericXmlApplicationContext 객체를 이용해서 applicationContext를 전체 로드합니다. 로드된 xml을 기초로 객체들을 생성하고, 그 객체를 Hashtable 형태로 저장하는 일을 합니다.
Hashtable 형태로 저장된 객체들은 각각의 id를 통해서 로드가 가능하며, bookApp = (BookApp) context.getBean("bookApp"); 코드를 통해서 BookApp을 로드할 수 있습니다. 이렇게 spring을 통해서 객체를 사용하는 것이 어떤 장점을 갖게 될까요?

Spring을 사용한 객체의 사용의 장점은 크게 3가지로 볼 수 있습니다.

1. 정해진 규칙에 따른 객체의 선언
2. xml로 정의된 객체의 dependency 파악이 가능
3. 테스트의 편의성

먼저 정해진 규칙에 따른 객체의 선언은 팀단위의 개발자들에게 일정한 개발 패턴을 만들어줍니다. 정해진 개발 패턴은 정형화된 코드를 만들고, 서로간에 코드의 공유가 원활하게 할 수 있습니다. 그리고, xml을 통한 객체의 의존성 관리는 객체들이 어떠한 구조를 가지고 있는지를 파악하는데 도움을 줍니다. 마지막으로 테스트의 편의성을 들 수 있습니다. spring은 테스트 코드를 작성하는데, 지금까지 보던 코드보다 더욱 깔끔하고 쉬운 테스트 패턴을 제공하고 있습니다. Spring을 사용한 테스트 코드를 다시 한번 알아보도록 하겠습니다. 

지금까지 작성된 코드를 기반으로, Spring을 이용한 테스트 코드는 다음과 같이 구성될 수 있습니다. 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/applicationContext.xml")
public class BookAppTest {
    @Autowired
    private ApplicationContext context;
    private BookApp bookApp;

    @Before
    public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        bookApp = (BookApp) context.getBean("bookApp");
        System.out.println(bookApp);
        bookApp.deleteAll();
        assertThat(bookApp.countAll(), is(0));
    }

Test class에 @RunWith와 @ContextConfiguration annotation이 붙어 있는 것을 알 수 있습니다. @RunWith는 Spring을 이용한 JUnit4 test class임을 명시하는 선언입니다. 중요한것은 @ContextConfiguration인데, 이는 방금 작성한 applicationContext.xml를 지정하는 영역입니다. @ContextConfiguration이 설정된 경우, Test class가 로드 되면서, xml 파일안에 위치한 객체를 생성해서 spring application context에 저장하고 있습니다. 4 line에 위치한 @Autowired는 spring application context 중에 type이 동일한 객체를 자동으로 할당하는 annotation입니다. spring test는 1개의 applicationContext를 로드하기 때문에, 그 객체를 context 변수에 자동으로 할당하게 됩니다. 자신이 구성한 applicationContext.xml은 반드시 이러한 과정을 거쳐서 테스트를 통과시켜야지 됩니다. 


Spring에서의 객체의 생명 주기


spring에서의 객체의 생명 주기는 기본적으로 한번 사용된 객체를 재사용합니다. 테스트 코드를 통해서 이를 확인해보도록 하겠습니다.
구성된 코드에 System.out.println 을 이용해서 bookApp 객체의 instance id를 확인해보도록 하겠습니다. 

     @Before
     public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          //context = new GenericXmlApplicationContext("/appContext.xml"); @ContextConfiguration에 의하여 필요없는 코드가 되었습니다.
          bookApp = (BookApp) context.getBean("bookApp");
          System.out.println(bookApp);
          bookApp.deleteAll();
          assertThat(bookApp.countAll(), is(0));
     }

테스트 코드의 console 창의 결과는 다음과 같습니다.

INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@61305d5b: defining beans [connectionFactory,bookApp,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor]; root of factory hierarchy
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@240615ef
2월 18, 2013 2:57:58 오후 org.springframework.context.support.AbstractApplicationContext doClose

보시면 BookApp의 모든 객체 Id가 동일함을 알 수 있습니다. ApplictionContext에 저장된 객체를 얻을 때, 새로 생성하는 것이 아닌 기존의 객체를 계속해서 사용하는 것을 알 수 있습니다.
왜 모든 객체들을 기본적으로 재사용하게 될까요? 기본적으로 spring은 enterprise development framework입니다. enterprise급의 대규모 시스템에서는 객체의 생성/삭제가 많은 부담을 주게 됩니다. 이에 대한 해결 방법으로 spring은 application context가 로드 될 때, 기본 객체들을 모두 생성, 로드 하는 것을 기본으로 하고 있습니다. 물론, 다른 방법역시 가능합니다.

Spring에서의 객체의 생명주기는 scope로 불리고, 다음 4가지로 관리가 가능합니다.

1.singleton
2.prototype
3.session
4.request

singleton은 default 값입니다. scope를 따로 설정하지 않으면 모두 singleton으로 동작합니다. 이는 객체를 static object와 동일하게 사용하게 됩니다.
prototype은 일반적으로 저희가 사용하던 객체의 생성방법과 동일합니다. new 를 통해서 객체를 생성하고, property 값을 모두 설정시킨 후, 그 객체를 넘겨주게 됩니다.
session과 request는 web programming에서 사용되는 scope입니다. 새로운 session이 생성될 때, 새로운 request가 생성이 될때 사용될 수 있는 scope 입니다.
크게는 singleton과 prototype을 이용하면 대부분의 객체 생명주기 관리는 가능하게 되며 그 차이를 한번 알아보도록 하겠습니다. scope를 prototype으로 선언해보도록 하겠습니다.

    <bean id="bookApp" class="com.xyzlast.bookstore01.domain.BookApp" scope="prototype">
        <property name="connectionFactory" ref="connectionFactory"/>
    </bean>

그리고, 테스트코드를 수행해보도록 하겠습니다.

2월 18, 2013 2:45:30 오후 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@61305d5b: defining beans [connectionFactory,bookApp,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor]; root of factory hierarchy
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@3622102f
com.xyzlast.bookstore01.domain.BookApp@78f14616
com.xyzlast.bookstore01.domain.BookApp@c4e6fe5
2월 18, 2013 2:45:31 오후 org.springframework.context.support.AbstractApplicationContext doClose

각각 4개의 BookApp bean이 생성됨을 알 수 있습니다. spring 설정 만으로 다음과 같은 코드가 만들어지는 것과 동일한 효과를 가지고 오게 되는 것입니다.

BookApp bookApp = new BookApp();
bookApp.setConnectionFactory(new ConnectionFactory);

이 부분을 보면 Factory Pattern에서의 Factory와 동일한 기능을 가지게 되는 것을 알 수 있는데요. ApplicationContext는 bean의 Map과 Factory를 지원한다. 라고 할 수 있습니다.
spring을 사용하게 되면, 객체들을 new로 새롭게 할당하는 일들이 얼마 없습니다. 모든 객체 bean들은 spring을 통해서 관리가 되고, bean들간의 데이터 교환을 위한 POJO(Plain Old Java Object)들만 new로 생성되어서 데이터 교환이 되는 것이 일반적인 패턴입니다. POJO에 대해서는 좀더 나중에 알아보도록 하겠습니다.


Spring에서의 객체 주입과 이용에 대한 심화 학습

먼저 이야기한 내용에서 bean의 Map을 제공한다는 이야기를 했습니다. Spring은 ApplicationContext라는 bean Map / Factory를 가지고 있고, ApplicationContext를 통해서 객체를 얻어내거나 생성하게 됩니다. 이때, 객체의 초기화 방법은 다음 두가지로 나눌 수 있습니다. 
1. property를 이용한 주입
2. 생성자를 이용한 주입

property를 이용한 주입은 기존 connectionFactory의 xml 선언을 보면 쉽게 알 수 있습니다.

<bean id="connectionFactory" class="com.xyzlast.bookstore01.domain.ConnectionFactory">
    <property name="connectionString" value="jdbc:mysql://localhost/bookstore"/>
    <property name="driverName" value="com.mysql.jdbc.Driver"/>
    <property name="username" value="root"/>
    <property name="password" value="qwer12#$"/></bean>


생성자를 이용한 주입은 constructor-arg를 이용해서 선언 가능합니다.

<bean id="connectionFactory" class="com.xyzlast.bookstore01.domain.ConnectionFactory">
    <constructor-arg index="0" value="jdbc:mysql://localhost/bookstore"/>
    <constructor-arg index="1" value="com.mysql.jdbc.Driver"/>
    <constructor-arg index="2" value="root"/>
    <constructor-arg index="3" value="qwer12#$"/>
</bean>   


어떤 방법이 좋은지에 대해서는 찬/반이 나뉘고 있습니다. 장점과 단점은 다음과 같습니다.
방법장점단점
Property를 이용한 방법1. 설정값에 대한 설명을 Property를 통해 명확히 알 수 있다.
2. 객체가 변경되었을 때, 확장이 용의하다.
1. 사용되는 Bean의 내용을 정확히 알지 못하면, Property 값을 설정하는 것을 빼먹을 수 있다.
생성자를 이용한 방법1. 필요한 값을 모두 설정하는 것이 가능하다.2. 값을 index와 같이, 순서를 이용하기 때문에 어떤 값을 설정하는지 파악하기 힘들다.

둘다 좋은 방법이지만, spring을 이용하는 대부분의 library 들은 property를 이용한 주입 을 주로 하고 있습니다. 이건 개발을 하는 팀에서 한가지로 정해서 가는 것이 좋습니다.

객체를 사용하다보면, 객체를 초기화 시켜야지 되는 경우가 자주 생깁니다. 지금 코드에서는 ConnectionFactory의 Class.forName().newInstace()의 경우에는 한번만 실행되어도 코드의 동작에는 아무런 문제가 없기 때문에, 생성 후, 한번만 실행이 되어도 아무런 문제가 없습니다. 이러한 초기화 method를 실행시키는 xml선언은 다음과 같습니다. 

    <bean id="connectionFactory" class="com.xyzlast.bookstore01.domain.ConnectionFactory" init-method="init">
        <property name="connectionString" value="jdbc:mysql://localhost/bookstore"/>
        <property name="driverName" value="com.mysql.jdbc.Driver"/>
        <property name="username" value="root"/>
        <property name="password" value="qwer12#$"/>
    </bean>


init-method가 포함된 객체의 생성 process는 다음과 같습니다. (singleton scope의 경우)

1. 객체의 생성
2. property 값들의 설정
3. init-method 의 실행

init-method가 property값들이 모두 설정 된 후에 실행됨을 확인해주셔야지 됩니다. 


Summary

이번 장에서는 Spring을 이용한 객체의 관리를 위주로 Simple Application을 작성해봤습니다. 다음 개념들은 반드시 다시 정리해보시는 것이 좋습니다.

1. ApplicationContext : Spring에서 제공하는 bean의 Map/ObjectFactory
2. property, constructor, init-method를 이용한 객체 초기화 방법
3. IoC, DI


Posted by Y2K
,