사용자 레벨 조정 작업은 중간에 문제가 발생해서
작업이 중단되면 그때까지 진행된 변경작업도
모두 취소하기로했다고 하자.
UserService 상속하여 테스트용 UserService를 만들자.
먼저 UserService의 upgradeLevel()메소드 접근권한을
protected로 수정해서 상속을 통해 오버라이딩 가능하게 한다.
이제 UserService대역을 맡을 클래스를 UserServiceTest안에 추가한다.
static class TestUserService extends UserService{
private String id;
// 예외를 발생시킬 User 오브젝트의 id를 지정할 수 있게한다.
private TestUserService(String id) {
this.id = id;
}
@Override
protected void upgradeLevel(User user) {
// 지정된 id의 User오브젝트가 발견되면 예외를 던져 작업을 강제로 중단시킨다.
if(user.getId().equals(this.id)) throw new TestUserServiceException();
super.upgradeLevel(user);
}
}
static class TestUserServiceException extends RuntimeException{
// 테스트용 예외
}
이제 테스트를 만들자.
@Test
public void upgradeAllorNothing() {
// 예외를 발생시킬 네 번째 사용자 id를 넣어서 오브젝트를 생성한다.
UserService testUserService = new TestUserService(users.get(3).getId());
testUserService.setUserDao(this.userDao); // userDao를 수동 DI한다.
userDao.deleteAll();
for(User user : users) userDao.add(user);
try {
testUserService.upgradeLevels();
// 작업중 예외가 발생해야한다. 정상 종료라면 문제가 있으니 실패다.
fail("TestUserServiceException exptected");
} catch (TestUserServiceException e) {
// TODO: handle exception
}
// 예외가 발생하기 전에 레벨변경이 있었던 사용자의 레벨이 처음 상태로 바뀌었나 확인
// 레벨 변경이 안되는 것을 의도함
checkLevelUpgrade(users.get(1), false);
}
테스트를 수행하면 실패한다.
예외는 발생했지만 두 번째 사용자의 레벨이 바뀌었다.
upgradeLevels()메소드의 작업은 하나의 작업 단위 트랜잭션이
적용되지 않았기 때문에 새로 추가된 기술 요건을 만족하지 못하기 때문에
테스트에 실패한다.
트랜잭션 동기화 적용
트랜잭션 동기화 방식을 적용한 UserService
//생략 ...
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
//생략 ...
public void upgradeLevels() throws Exception {
//트랜잭션 동기화 관리자를 이용해 동기화 작업을 초기화한다.
TransactionSynchronizationManager.initSynchronization();
//DB커넥션을 생성하고 트랜잭션을 시작한다.
//이후의 DAO작업은 모두 여기서 시작한 트랜잭션 안에서 진행된다.
Connection c = DataSourceUtils.getConnection(dataSource);
c.setAutoCommit(false);
try {
List<User> users = userDao.getAll();
// 업그레이드 가능한 사용자인지 확인하고 업그레이드 한다.
for(User user : users) {
if(canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
// 정상적으로 작업을 끝내면 트랜잭션 커밋
c.commit();
} catch (Exception e) {
//예외가 발생하면 롤백한다.
c.rollback();
throw e;
} finally {
// 스프링 유틸리티 메소드를 이용해 DB커넥션을 안전하게 닫는다.
DataSourceUtils.releaseConnection(c, dataSource);
// 동기화 작업 종료 및 정리
TransactionSynchronizationManager.unbindResource(this.dataSource);
TransactionSynchronizationManager.clearSynchronization();
}
}
동기화가 적용된 UserService에 따라 수정된 UserServerTest
@Autowired
DataSource dataSource;
// 생략...
@Test
public void upgradeAllorNothing() throws Exception {
UserService testUserService = new TestUserService(users.get(3).getId());
testUserService.setUserDao(this.userDao);
testUserService.setDataSource(this.dataSource); // 추가
// 생략...
}
UserService와 마찬가지로 트랜잭션 동기화에 필요한
DataSource를 DI해주어야 한다.
스프링 빈 설정 : test-applicationContext.xml
<bean class="springbook.user.service.UserService">
<property name="userDao" ref="userDao"/>
<property name="dataSource" ref="dataSource"/> <!-- 추가 -->
</bean>
이제 테스트를 수행하면 성공한다.
스프링이 제공하는 트랜잭션 추상화 방법을 UserService에 적용하자.
public void upgradeLevels() {
// PlatformTransactionManager은 트랜잭션 경계설정을 위한 추상 인터페이스이다.
// 로컬 JDBC의 로컬 트랜잭션을 이용한다면 DataSourceTransactionManager 사용한다.
PlatformTransactionManager transactionManager
= new DataSourceTransactionManager(dataSource);
// 트랜잭션 매니져가 DB커넥션을 가져오는 작업도 같이 수행한다.
// getTransaction은 트랜잭션을 가져온다는 의미이다.
// DefaultTransactionDefinition은 트랜잭션의 속성을 담고있다.
TransactionStatus status
= transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
List<User> users = userDao.getAll();
for(User user : users) {
if(canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
//트랜잭션에 조작이 필요할때 TransactionStatus을 PlatformTransactionManager의 파라미터로 전달한다.
//트랜잭션 커밋
transactionManager.commit(status);
} catch (RuntimeException e) {
// 롤백
transactionManager.rollback(status);
throw e;
}
}
테스트를 수행하여 확인한다.
트랜잭션 매니저 구현 클래스를 사용할지 UserService코드가 알고 있는 것은 DI원칙에 위배된다.
자신이 사용할 구체적인 클래스를 스스로 결정하고 생성하지 말고
컨테이너를 통해 외부에서 제공받게 하는 스프링 DI방식으로 바꾸자.
public class UserService {
// ...
// DataSource와 그 수정자 메소드는 제거했다.
private PlatformTransactionManager transactionManager;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
//...
public void upgradeLevels() {
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 (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
// ...
}
DataSource를 삭제했기때문에 UserServiceTest도 수정해야한다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/test-applicationContext.xml")
public class UserServiceTest {
//...
@Autowired
PlatformTransactionManager transactionManager;
//...
@Test
public void upgradeAllorNothing() throws Exception {
//...
testUserService.setTransactionManager(transactionManager);
//...
}
//...
}
UserService에 DI될 transacitonManager빈을 설정파일에 등록한다.
<bean id="userService" class="springbook.user.service.UserService">
<property name="userDao" ref="userDao"/>
<property name="transactionManager" ref="transactionManager"/> <!-- 수정 -->
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
테스트를 수행하여 확인한다.
이제 UserService는 트랜잭션 기술에서 완전히 독립적인 코드가 됐다.
트랜잭션을 JTA를 이용하고 싶다면 설정파일의 transactionManager빈의 설정만
다음과 같이 고치면 된다.
<bean id="transactionManager" class="org.springframework.jta.JtaTransactionManager"/>
JtaTransactionManager는 애플리케이션 서버의 트랜잭션 서비스를 이용하기때문에
직접 DataSource와 연동할 필요는 없다.
DAO를 하이버 네이트나 JPA, JDO등을 사용하도록 수정했다면
그에 맞게 transactionManager의 클래스만 변경해주면된다.
[6.2] 고립된 단위 테스트 (0) | 2020.11.02 |
---|---|
[6.1] 트랜잭션 코드의 분리 (0) | 2020.11.02 |
[5.1] 사용자 레벨 관리 기능 추가 (0) | 2020.10.30 |
[3.6] 스프링의 JdbcTemplate (0) | 2020.10.29 |
[3.5] 템플릿과 콜백 (0) | 2020.10.29 |
댓글 영역