잊지 않겠습니다.

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
,