토비의 스프링 정리
토비의 스프링 - 6.1 트랜젝션 코드의 분리
ksb-dev
2022. 10. 6. 20:28
6.1.1 AOP(Aspect Oriented Programming)
- AOP는 IoC/DI, 서비스 추상화와 더불어 스프링의 3대 기반기술임
- AOP는 스프링의 기술 중에서 가장 난해한 용어와 개념을 가진 기술임
- AOP는 주로 선언적 트랜잭션 기능에 많이 사용됨
- 서비스 추상화를 통해 근본적 문제를 해결 했지만, AOP로 더욱 세련되고 깔끔한 방식으로 바꿀 수 있음
- AOP 등장 배경, 스프링이 AOP를 도입한 이유, AOP 적용의 장점 등을 공부할 것임
6.1.2 메소드 분리
- 서비스 추상화 기법을 통해 트랜잭션의 근본적 문제를 해결했지만 UserService에는 트랜잭션 로직과 비즈니스 로직이 같이 존재함
- 스프링이 제공하는 트랜잭션 인터페이스 PlatformTransactionManager를 사용했지만, 메소드 내부에 트랜잭션 로직이 상당한 부분을 차지함
- 비즈니스 로직 전후에 트랜잭션 경계설정이 되어야 하므로 어쩔수 없다고 느껴짐
- 트랜잭션 로직과 비즈니스로직 사이에 주고받는 정보가 없기 때문에 메소드로 분리할 수 있음
public class UserService {
...
public void upgradeLevels() {
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);
}
}
}
}
6.1.3 DI를 이용한 클래스의 분리
- 메소드로 분리는 했지만, 여전히 트랜잭션 로직이 UserService 내부에 있음
- 트랜잭션 코드를 클래스 밖으로 추출하면 됨
- UserService는 클래스 이므로, 다른 코드에서 UserService의 오브젝트를 직접적으로 사용할 수 밖에 없음
- 직접 사용하는게 문제가 되므로 인터페이스를 통해 간접적으로 접근할 수 있게 함
- 보통 인터페이스를 통해 DI하는 이유는, 운영과 테스트 상에서 손쉽게 구현 클래스를 바꿔서 사용하기 위함임
- 하지만 꼭 그래야 하는 제약은 없음
- 두 개의 UserService 인터페이스 구현 클래스를 동시에 이용하면. 하나의 구현에는 순수한 비즈니스 로직만을 남기고, 다른 하나는 트랜잭션 로직의 책임만 가질 수 있음
- UserServiceTx는 비즈니스 로직이 없기 때문에 UserServiceImpl에 해당 부분을 위임함
6.1.4 UserService 인터페이스 도입
- UserService를 인터페이스로 변경하고, UserServiceImpl에 비즈니스 로직만 구현함
- 트랜잭션 코드를 UserServiceTx에서 구현하고 비즈니스는 위임함
- 먼저 UserServiceTx를 사용하여 트랜잭션 작업을 하고, 비즈니스 로직은 호출(위임)되어서 작업됨
public interface UserService {
void add(User user);
void upgradeLevels();
}
public class UserServiceImpl implements UserService {
...
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 setTransactionManager(
PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setUserService(UserService userService) {
this.userService = userService;
}
@Override
public void add(User user) {
//위임
this.userService.add(user);
}
@Override
public void upgradeLevels() {
TransactionStatus status =
this.transactionManager.getTransaction(
new DefaultTransactionDefinition());
try {
//위임
this.userService.upgradeLevels();
this.transactionManager.commit(status); //커밋
} catch (Exception e) {
this.transactionManager.rollback(status); //롤백
throw e;
}
}
}
<beans
.../>
...
<bean id="userService" class="com.ksb.spring.UserServiceTx">
<property name="transactionManager" ref="transactionManager"/>
<property name="userService" ref="userServiceImpl"/>
</bean>
<bean id="userServiceImpl" class="com.ksb.spring.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<property name="transactionManager" ref="transactionManager" />
<property name="mailSender" ref="mailSender"/>
</bean>
...
</beans>
6.1.5 테스트 코드 수정
- 단순히 UserService 기능을 테스트 할 때 구체적 클래스 정보를 노출하는것이 좋지는 않지만, 목 오브젝트를 통해 수동 DI를 적용하는 테스트라면 어떤 클래스인지 알아야 함
- 💡 테스트의 upgradeLevels() 메소드에서 수동 DI를 하기 때문에 테스트에 userServiceImpl 빈을 불어와야 함
- upgradeAllOrNothing()는 트랜잭션 동작 테스트이기 때문에, UserServiceTx를 수동 DI시켜 동작하게 함
public static class TestUserService extends UserServiceImpl {
...
}
public class UserServiceTest {
...
//수동 DI 때문에 필요함
@Autowired
UserServiceImpl userServiceImpl;
@Test
public void upgradeLevels() {
...
// MockMailSender mockMailSender = new MockMailSender();
userServiceImpl.setMailSender(mockMailSender);
}
@Test
public void upgradeAllOrNothing() {
UserServiceImpl.TestUserService testUserService =
new UserServiceImpl.TestUserService(users.get(3).getId());
testUserService.setUserDao(this.userDao);
testUserService.setMailSender(this.mailSender);
UserServiceTx txUserService = new UserServiceTx();
txUserService.setTransactionManager(transactionManager);
txUserService.setUserService(testUserService);
userDao.deleteAll();
for (User user : users) userDao.add(user);
try {
testUserService.upgradeLevels();
fail("TestUserServiceException expected");
} catch (UserServiceImpl.TestUserServiceException e) {
}
checkLevelUpgraded(users.get(1), false);
}
}
6.1.6 트랜잭션 경계설정 코드 분리의 장점
- 코드를 분리 함으로써 UserServiceImpl는 트랜잭션과 같은 기술적 내용에 전혀 신경을 쓰지 않아도 됨
- 즉, UserServiceImpl는 비즈니스 로직만 구현
- 트랜잭션은 DI를 통해 UserServiceTx를 먼저 실행되도록 하고, 호출(위임)을 통해 비즈니스 로직이 동작하게 함