본문 바로가기
Back-End/토비의 스프링

토비의 스프링 | 6장. AOP

by Havi 2017. 4. 5.
반응형

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 목 오브젝트에 전달된 파라미터를 가져와 내용을 검증할 수 있다.
  • 이는 파라미터의 내부 정보를 확인하는 경우 유용하다.


반응형

댓글