* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.
<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>
<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>
# 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가 남는다.
* UserService : 사용자가 action의 주체가 되는 서비스입니다.
* BookService : Book이 주체가 되는 서비스입니다.
서비스의 명명법은 영문법을 따르게 되며, 다음과 같은 영문장으로 구성을 하면 좋습니다.
User.rentBook(Book book) User.returnBook(Book book) User.listUpHistory() Book.listUp()
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(); }
@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); } }
# 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)); } }
@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; }
일단 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; }
public void doSomething() { TransactionStatus status = transactionManager.getTransaction(definition); try { // ..do something transactionManager.commit(status); } catch(Exception ex) { transactionManager.rollback(status) } }
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>3.2.0.RELEASE</version>
</dependency>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
@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; } }
Transaction을 어떻게 테스트를 할 수 있을까?
일단, 이렇게 코드를 구성하면 안되지만, Transaction 중간에 에러가 발생하는 상황을 만들어보도록 합시다. 이 에러가 발생할 수 있는 상황은 DB에 접근하는 중에 오류가 발생하거나 db를 업데이트 하는 도중, Business Logic 상의 에러가 발생하는 것을 의미합니다. 이 두가지 모두 테스트를 작성하기가 매우 힘든 상황입니다. 이런 상황을 어떻게 하면 직접 만들수 있을까요?
user.setPoint(user.getPoint() + 10); user.setLevel(getUserLevel(user.getPoint()));
# point가 100점이 넘어가면 READER가 된다.
# point가 300점이 넘어가면 MVP가 된다.
# point가 100점 이하인 경우, 일반유저(NORMAL)이다.
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); } } }
@Autowired private UserLevelRole userLevelRole; public UserLevelRole getUserLevelRole() { return this.userLevelRole; } public void setUserLevelRole(UserLevelRole userLevelRole) { this.userLevelRole = userLevelRole; }
<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>
User user = userDaoImplWithJdbcTemplate.get(userId); assertThat(user.getPoint(), is(oldUser.getPoint() + + userLevelRole.getAddRentPoint())); assertThat(user.getLevel(), is(UserLevel.READER));
@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; }
@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())); } }
@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; }
<?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>
.png)
<tx:annotation-driven transaction-manager="transactionManager"/>
@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; } }
@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입니다.
UserService의 class 이름은 $Proxy15입니다.