본문 바로가기

Back-End/토비의 스프링3

토비의 스프링 6장(1) - AOP 트랜잭션 코드의 분리

지난 포스팅

에서 UserService에서 트랜잭션이 필요했다.

스프링에서 제공해주는 인터페이스를 썼음에도 불구하고

비즈니스 로직에 길고 더러운 코드가 있어 찝찝함을 감출 수 없다.

개요

  • 등장배경
  • 스프링이 도입하려는 이유
  • 장점 ( 예제로 선언적 트랜잭션 기능을 살펴보자 )

메소드 분리

[ 리팩토링 전 - 서로 정보도 주고 받지 않고 관련이 없는 트랜잭션 코드 / 비즈니스 코드가 섞여 있다. ]
public void upgradeLevels() throws Exception {

TransactionStatus status =
this.transactionManager.getTransaction(new DefaultTransactionDefinition());

//////////////////////////////////////

try {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user))
upgradeLevel(user);
}
////////////////////////////////////
this.transactionManager.commit(status);// 트랜잭션 커밋
} catch (Exception e) {
this.transactionManager.rollback(status);// 트랜잭션 롤백
throw e;
}
}

[ 리팩토링 후 - 비즈니스 로직을 담당하는 코드를 메소드로 추출 ]
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;
}
}

public void upgradeLevelsInternal() {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user))
upgradeLevel(user);
}
}

DI를 이용한 클래스의 분리 / 트랜잭션 분리

메소드 추출을 통해 책임을 분리하였지만,

아직 UserService에 트랜잭션을 담당하는 코드가 존재한다.

UserService에서는 이 코드가 보이지 않도록 클래스 밖으로 뽑아내자.


지금 UserService를 사용하는 곳은 UserServiceTest이다. 

현재는 구체적인 클래스를 참조하고 있기 때문에 외부에서 변경이 불가능하다.

즉, UserService를 사용하는 곳에서 구현 클래스를 바꿔 다른 것으로 사용하지 못한다.

인터페이스를 이용하여 아래와 같이 수정

00.png


UserService 인터페이스 도입

public interface UserService {
void add(User user);
void upgradeLevels();
}

UserServiceImpl - 트랜잭션 관련된 코드가 제거 ( 비즈니스 로직에만 충실 )

public void upgradeLevels(){
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user))
upgradeLevel(user);
}
}


분리된 트랜잭션 기능

UserServiceTx에서는 트랜잭션 경계만 설정해주고, 비즈니스 로직은 UserServiceImpl에게 모두 위임하는 구조이다.
public class UserServiceTx implements UserService {

UserService userService;

PlatformTransactionManager transactionManager;

public void setUserService(UserService userService) {
this.userService = userService;
}

public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}

@Override
public void add(User user) {
userService.add(user);
}

@Override
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설정

아래와 같이 DI 설정을 해주면 된다.

01.png

<bean id="userService" class="springbook.user.service.UserServiceTx">
<property name="userService" ref="userServiceImpl" />
<property name="transactionManager" ref="transactionManager" />
</bean>

<bean id="userServiceImpl" class="springbook.user.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>


트랜잭션 분리에 따른 테스트 수정

  1. 트랜잭션 경계 설정이 제대로 되어 롤백이 잘 되는지
  2. 비즈니스 로직에는 문제가 없는지
  3. 수정하면서 문제가 생긴 코드는 없는지
위의 수정을 거치면서 테스트해야 할 항목이 많이 생겼다.

하지만 몇가지 문제가 있다.
  • 테스트코드에서 UserService에는 어떤 것을 주입해야 하는가?
  • 롤백이 제대로 되는지 확인은 어떻게 하는가?
이 문제를 해결하기 위해 테스트 코드를 수정해보자.

  • 개발자가 코드 검증을 위한 테스트인 만큼 내부 구조를 알고 있는 채로 테스트를 만드는 것에 문제가 없다
    @Autowired
    UserServiceImpl userServiceImpl;
  • 비즈니스 로직 안에서 예외를 발생시키기 위해 UserServiceImpl을 상속받도록 하자.
static class TestUserService extends UserServiceImpl {
private String id;

private TestUserService(String id) {
this.id = id;
}

@Override
protected void upgradeLevel(User user) {
if (user.getId().equals(this.id)) // 지정된 id의 User 오브젝트가 발견되면 예외를 던져서 작업을 강제로 중단시킨다.
throw new TestUserServiceException();
super.upgradeLevel(user);
}

static class TestUserServiceException extends RuntimeException {

}
}

/**
* 레벨 업그레이드를 시도하다가 중간에 예외가 발생한 경우, 원래 상태로 돌아가는지 테스트
*/
@Test
public void upgradeAllOrNothing() {
TestUserService testUserService = new TestUserService(users.get(3).getId());
testUserService.setUserDao(this.userDao);
testUserService.setMailSender(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 (TestUserServiceException e) { // 예외를 잡아서 어떤 작업을 진행

}


트랜잭션 경계설정 코드 분리의 장점

이렇게 복잡하게 분리를 하였는데 도대체 얻는 장점은 뭘까?
  1. 비즈니스 로직과 트랜잭션 코드의 분리
    - 잘 만들어놓은 비즈니스 로직 코드에 괜히 손을 대서 엉망으로 만드는 불상사가 일어나지 않음
    - 트랜잭션 지식이 부족해도, 비즈니스 로직 개발하는데 문제가 없음
  2. 비즈니스 로직에 대한 테스트 손쉽게 만들 수 있음
    - 다음 포스팅으로~