Spring/토비의 스프링 3.1
6.1 트랜잭션 코드의 분리
다비드박
2019. 2. 7. 19:40
트랜잭션 경계설정
- 비즈니스 로직보다 코드가 더 길다.
6.1.1 메소드 분리
트랜잭션 적용 코드 구조
- 트랜잭션 시작
- 비즈니스 로직
- 트랜잭션 종료
코드의 특징
- 두 가지 종류의 코드가 구분되어 있음
- 트랜잭션 & 비즈니스 로직
- 두 코드 간 주고받는 정보가 없음
- 완벽히 독립적인 코드
비즈니스 로직을 메소드로 추출하여 독립
public class UserService {
public void upgradeLvls() throws Exception {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
upgradeLvlsInternal();
this.transactionManager.commit(status);
} catch (Exception e) {
this.transactionManager.rollback(status);
throw e;
}
}
// 분리된 비즈니스 로직 코드
// 트랜잭션 적용 전과 동일
private void upgradeLvlsInternal() {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLvl(user)) {
upgradeLvl(user);
}
}
}
}
보기에 깔끔해지고, 비즈니스 로직이 독립된 메소드로 분리되어 이해하기도 편하며, 수정하기에도 부담이 없다.
6.1.2 DI를 이용한 클래스의 분리
여전한 문제점
- 트랜잭션을 담당하는 기술적인 코드가 UserService내에 위치하고 있다.
- 트랜잭션 코드를 클래스 밖으로 뽑아내자
DI 적용을 이용한 트랜잭션 분리
UserService는 클래스이고, 클라이언트는 UserService를 직접 참조하게 된다.
- 직접 참조를 할 경우, 트랜잭션 코드가 UserService 밖으로 빠져버리면 해당 기능이 빠진 UserService를 사용할 수 밖에 없다.
- 간접적으로 참조하도록 변경하자.
UserService를 인터페이스로 만들고, 기존 코드는 UserService를 구현한 클래스에 넣으면 된다.
- 한 번에 두 개의 UserService 구현 클래스를 사용해야 한다.
- UserServiceImpl(사용자관리) + UserServiceTx(트랜잭션)
UserService 인터페이스 도입
UserService를 인터페이스로 변환 후 UserServiceImpl과 UserServiceTx로 구현
public interface UserService {
void add(User user);
void upgradeLvls();
}
UserServiceImpl
- UserService의 내용을 대부분 유지
- 트랜잭션 관련 코드는 모두 제거
- PlatformTransactionManager 인스턴스 변수 및 수정자 메소드 제거
- upgradeLvls() 내 트랜잭션 코드 제거
- upgradeLvlsInternal() 코드를 다시 upgradeLvls()로 돌려놓음
public class UserServiceImpl implements UserService {
private UserDao userDao;
private MailSender mailSender;
@Override
public void upgradeLvls() {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLvl(user)) {
upgradeLvl(user);
}
}
}
}
트랜잭션을 고려하지 않았던 처음 코드로 되돌아옴
- 비즈니스 로직에만 충실한 코드
분리된 트랜잭션 기능
UserServiceTx
- UserService를 구현
- 같은 인터페이스를 구현한 다른 오브젝트에게 작업을 위임
public class UserServiceTx implements UserService {
UserService userService;
// UserService를 구현한 다른 오브젝트를 DI 받음
public void setUserService(UserService userService) {
this.userService = userService;
}
// DI 받은 UserService 오브젝트에 모든 기능을 위임
@Override
public void add(User user) {
this.userService.add(user);
}
@Override
public void upgradeLvls() {
this.userService.upgradeLvls();
}
}
UserService 인터페이스 구현
- UserService 타입 오브젝트의 하나로 행세 가능
- 사용자 관리 비즈니스 로직은 전혀 갖지 않고, 다른 오브젝트에 기능을 위임
트랜잭션 경계 설정 부가작업 부여
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) {
this.userService.add(user);
}
@Override
public void upgradeLvls() {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
this.userService.upgradeLvls();
this.transactionManager.commit(status);
} catch (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
트랜잭션 적용을 위한 DI 설정
빈 오브젝트와 의존관계
- Client(UserServiceTest) -> UserServiceTx -> UserServiceImpl
<bean id="userService" class="com.david.tobysspring.user.service.UserServiceTx">
<property name="transactionManager" ref="transactionManager" />
<property name="userService" ref="userServiceImpl" />
</bean>
<bean id="userServiceImpl" class="com.david.tobysspring.user.service.UserServiceImpl">
<property name="userDao" ref="userDao" />
<property name="mailSender" ref="mailSender" />
</bean>
클라이언트는 UserServiceTx 빈을 호출해서 사용해야 함
- userService 빈 아이디는 UserServiceTx 클래스로 정의된 빈에게 부여
- userService 빈은 UserServiceImpl 클래스로 정의되는 userServiceImpl 빈을 DI
트랜잭션 분리에 따른 테스트 수정
Autowired는 타입이 일치하는 빈을 찾아 줌
- UserService 타입의 빈이 2개 존재
- 타입으로 찾을 수 없는 경우 필드의 이름으로 빈을 찾아 줌
@Autowired UserService userService;
userService 라는 id를 가진 UserServiceTx 빈이 주입 됨
- UserServiceTest는 UserServiceImpl 빈도 가져와야 함
- 목 오브젝트를 이용한 테스트에서 직접 MailSender를 DI 해줘야하기 때문
목 오브젝트를 이용해 수동 DI를 적용하는 테스트에서는 어떤 클래스의 오브젝트인지 분명하게 알 필요가 있음
@Autowired UserServiceImpl userServiceImpl;
upgradeLvls() 메소드
- MailSender의 목 오브젝트 설정은 UserService의 인터페이스를 통해서는 불가능
- 별도로 가져온 userServieImpl 빈에 해주어야 함
public class UserServiceTest {
public void upgradeLvls() throws Exception {
// ...
MockMailSender mockMailSender = new MockMailSender();
userServiceImpl.setMailSender(mockMailSender);
// ...
}
}
upgradeAllOrNothing() 메소드
- 트랜잭션이 적용됐는지 확인하는 일종의 학습 테스트
- TestUserService는 트랜잭션 기능이 빠진 UserServiceImpl을 상속해야함
- 트랜잭션 롤백의 확인을 위한 예외 발생 코드가 UserServiceImpl에 있기 때문
- 그런데 트랜잭션 기능이 없어짐
- TestUserService 오브젝트를 UserServiceTx 오브젝트에 수동 DI 시킨 후 UserServiceTx의 메소드를 호출해야함
public class UserServiceTest {
// 클래스 선언 변경
static class TestUserService extends UserServiceImpl {
// ...
}
@Test
public void upgradeAllOrNothing() throws Exception {
TestUserService testUserService = new TestUserService(users.get(3).getId());
testUserService.setUserDao(userDao);
testUserService.setMailSender(mailSender);
// 트랜잭션 기능을 분리한 UserServiceTx는 예외 발생용으로 수정할 필요가 없으니 그대로 사용한다.
UserServiceTx txUserService = new UserServiceTx();
txUserService.setTransactionManager(transactionManager);
txUserService.setUserService(testUserService);
userDao.deleteAll();
for (User user : users) {
userDao.add(user);
}
try {
// 트랜잭션 기능을 분리한 오브젝트를 통해 예외 발생용 TestUserService가 호출되게 해야한다.
txUserService.upgradeLvls();
fail("TestUserServiceException expected");
} // ...
}
}
트랜잭션 경계설정 코드 분리의 장점
큰 변화를 주었는데 코드 분리의 장점은 무엇인가
- 비즈니스 로직을 담당하는 UserServiceImpl은 기술적 내용에 전혀 신경을 쓰지 않아도 된다.
- 따라서 언제든 트랜잭션을 도입할 수 있다.
- 비즈니스 로직에 대한 테스트를 손쉽게 만들어 낼 수 있다.(후술)