AOP는 IOC/DI, 서비스 추상화와 더불어 스프링의 3대 기반기술의 하나이다. 이 장에서는 AOP의 등장배경, 도입 이유, 장점이 무엇인지 살펴본다.
6.1 트랜잭션 코드의 분리
비즈니스 로직과 트랜잭션 경계설정의 분리를 통해 성격이 다른 코드를 각각 독릭적인 코드로 만들 수 있다.
public void upgradeLevels() throws Exception { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { upgradeLevelsInternal(); this.transactionManager.commit(status); } catch (Exception e) { this.transactionManager.rollback(status); throw e; } } private void upgradeLevelsInternal() { //비즈니스 로직 분리 List<User> users = userDao.getAll(); for(User user : users) { if(canUpgradeLevel(user)) { upgradeLevel(user); } } }
DI 적용을 이용한 트랜잭션 분리
위의 코드에서 트랜잭션이 처리되는 부분을 아에 코드상에서 제거할 수는 없을까? 다음과 같이 UserService를 구현하면서 비즈니스 로직과 트랜잭션 처리 부분이 분리된 코드를 생각해 볼 수 있다.
코드로 구현하면 다음과 같다.
public interface UserService { void add(User user); void upgradeLevels(); }
public class UserServiceImpl implements UserService { UserDao userDao; MailSender mailSender; public void upgradeLevels() { List<User> users = userDao.getAll(); for(User user : users) { if(canUpgradeLevel(user)) { upgradeLevel(user); } } } ... }
public class UserServiceTx implements UserService { UserService userService; PlatformTransactionManager transactionManager; public void serTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } public void setUserService(UserService userService) { this.userService = userService; } public void add(User user) { this.userService.add(user); } public void upgradeLevels() { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { userService.upgradeLevels(); this.transactionManager.commit(status); } catch (RuntimeException e) { this.transactionManager.rollback(status); throw e; } } }
- 추상화된 트랜잭션 구현 오브젝트를 DI 받을 수 있도록 PlatformTransactionManager 타입의 프로퍼티도 추가되었다.
- 이전과 동일한 기능을 수행하지만 비즈니스 로직과 트랜잭션 처리 부분이 완전히 분리되어 추상화되었다.
트랜잭션 분리에 따른 테스트 수정
- @Autowired로 UserService를 가져오면 UserService가 인터페이스더라도 기본적으로 아무런 문제가 없다.
- 하지만 구현 클래스가 두 개라면 기본적으로 타입을 이용해 빈을 찾미낭 하나의 빈을 결정할 수 없는 경우에는 필드 이름을 이용하여 찾는다.
- 하지만 테스트에서는 MailSender라는 빈을 하나 더 가져와야 한다.
- 이 때에는 목 오브젝트를 이용해 수동 DI를 적용하는 테스트이기에 어떠한 오브젝트인지 분명하게 선언하는게 좋다.
@Test public void upgradeLevels() throws Exception { ... MockMailSender mockMailSender = new MockMailSender(); userServiceImpl.setMailSender(mockMailSencder); } @Test public void upgradeAllorNothing() throws Exception { TestUserService testUserService = new TestUserService(users.get(3).getId()); testUserService.setUserDao(userDao); testUserService.setMailSender(mailSender); UserServiceTx txUserService = new UserServiceTx(); txUserService.setTransactionManager(transactionManager); txUserService.setUserService(testUserSerivce); userDao.deleteAll(); for(User user : users) userDao.add(user); try { txUserService.upgradeLevels(); fail("TestUserServiceException expected"); } } static class TestUserService extends UserServiceImpl { ... }
- 트랜잭션 주입을 위해 UserServiceTx 오브젝트를 수동 DI 시킨 후에 트랜잭션 기능까지 포함해서 테스트를 진행한다.
- TestUserService 클래스는 이제 UserServiceImpl 클래스를 상속하도록 바꿔주면 된다.
트랜잭션 경계설정 코드 분리의 장점: 첫 번째는 이제 비느지스 로직을 수정할 때는 트랜잭션과 같은 기술적인 내용은 전혀 신경쓰지 않아도 된다. 두 번째는 이를 통해 비느지스 로직에 대한 테스트를 손쉽게 만들어낼 수 있다.
Mockito 프레임워크
단위 테스트를 만들기 위해서는 스텁이나 목 오브젝트의 사용은 필수적이다. 하지만 목 오브젝트의 작성은 매우 번거롭다. Mockito 프레임워크는 간단한 메소드 호출만으로 다이내믹하게 특정 인터페이스를 구현한 클래스용 오브젝트를 만들 수 있다. 다음과 같이 간단하게 UserDao의 인터페이스를 파라미터로 받아 목 오브젝트를 만들 수 있다.
UserDao mockUserDao = mock(UserDao.class);
목 오브젝트를 생성한 후 getAll() 메서드를 호출하였을 때 사용자 목록을 리턴하도록 스텁 기능을 추가해 준다.
when(mockUserDao.getAll()).thenReturn(this.users);
테스트를 진행하는 동안 mockUserDao의 update() 메소드가 두 번 호출됐는지 확인하고 싶다면 다음과 같은 코드를 넣어주면 된다.
verify(mockUserDao, time(2)).update(any(User.class));
Mockito는 다음과 같은 4 단계를 거쳐서 사용하면 된다.(두 번째와 네 번째는 필요하면 경우에만 사용할 수 있다.)
- 인터페이스를 이용해 목 오브젝트를 만든다.
- 목 오브젝트가 리턴할 값이 있으면 이를 지정해 준다. 메소드가 호출되면 예외를 강제로 던지게 만들 수 있다.
- 테스트 대상 오브젝트에 DI 해서 목 오브젝트가 테스트 중에 사용되도록 만든다.
- 테스트 대상 오브젝트를 사용한 후에 목 오브젝트의 특정 메소드가 호출됐는지, 어떤 값을 가지고 몇 번 호출됐는지를 검증한다.
@Test public void mockUpgradeLevels() throws Exception { UserServiceImpl userServiceImpl = new UserServiceImpl(); UserDao mockUserDao = mock(UserDao.class); when(mockUserDao.getAll()).thenReturn(this.users); userServiceImpl.setUserDao(mockUserDao); MailSender mockMailSender = mock(MailSender.class); userServiceImpl.setMailSender(mockMailSender); userServiceImpl.upgradeLevels(); verify(mockUserDao, time(2)).update(any(User.class)); verify(mockUserDao, time(2)).update(any(User.class)); verify(mockUserDao).update(users.get(1)); assertThat(users.get(1).getLevel(), is(Level.SILVER)); verify(mockUserDao).update(users.get(3)); assertThat(users.get(3).getLevel(), is(Level.GOLD)); ArgumentCaptor<SimpleMailMessage> mailMessageArg = ArgumentCaptor.forClass(SimpleMailMessage.class); verify(mockMailSender, time(2)).send(mailMessageArg.captur()); List<simpleMailMessage> mailMessages = mailMessageArg.getAllValues(); asserThat(mailMessages.get(0),getTo()[0], is(users.get(1).getEmail())); asserThat(mailMessages.get(1),getTo()[0], is(users.get(3).getEmail())); }
- times()는 메소드 호출 횟수를 검증해 준다. any()를 사용하면 파라미터의 내용은 무시하고 호출 횟수만 확인한다.
- ArgumentCaptor라는 것을 사용하여 실제 MailSender 목 오브젝트에 전달된 파라미터를 가져와 내용을 검증할 수 있다.
- 이는 파라미터의 내부 정보를 확인하는 경우 유용하다.
'Back-End > 토비의 스프링' 카테고리의 다른 글
토비의 스프링 | 6장. AOP3 - 프록시 팩토리빈과 포인트컷 (0) | 2017.04.17 |
---|---|
토비의 스프링 | 6장. AOP2 - 다이내믹 프록시 (0) | 2017.04.07 |
토비의 스프링 | 3장. 템플릿2 - 응용 (0) | 2017.03.03 |
토비의 스프링 | 3장. 템플릿 (0) | 2017.01.27 |
토비의 스프링 | 2장. 테스트 (0) | 2017.01.13 |
댓글