상세 컨텐츠

본문 제목

[6.1] 트랜잭션 코드의 분리

토비의스프링

by kwanghyup 2020. 11. 2. 00:32

본문

지금까지 작업 UserService 코드를 보면 

비즈니스 로직이 주인이어야 할 메소드안에 트랜잭션 코드가 더 많은 자리를 차지한다.

 

# 메소드 분리 

upgradeLevel()메소드를 다시 보자.

 

트랜잭션 경계설정과 비즈니스 로직이 공존하는 메소드 

public void upgradeLevels() {
	// 트랜잭션 경계 
	TransactionStatus status 
				= this.transactionManager.getTransaction(new DefaultTransactionDefinition());
	try {
		
		// 비즈니스 로직(s)
		List<User> users = userDao.getAll();
		for(User user : users) {
			if(canUpgradeLevel(user)) {
				upgradeLevel(user);
			}
		}// 비즈니스 로직(e)
		
		// 트랜잭션 경계 
		this.transactionManager.commit(status);
	} catch (RuntimeException e) {
		this.transactionManager.rollback(status);
		throw e;
	} 
}

비즈니스 로직 코드를 사이에 두고 트랜잭션 시작과 종료를 담당하는 코드가 앞뒤에 위치해 있다.

트랜잭션 경계설정의 코드와 비지니스 로직 코드간에 서로 주고 받는 정보가 없다.

다만 비지니스 로직을 담당하는 코드가 트랜잭션의 시작과 종료 작업사이에 

수행되어야 한다는 사항만 지키면된다. 

 

비지니스 로직을 담당하는 코드를 메소드로 추출해서 독립시켜보자.

 

비즈니스 로직과 트랜잭션 경계설정 분리 

public void upgradeLevels() {
	TransactionStatus status 
				= this.transactionManager.getTransaction(new DefaultTransactionDefinition());
	try {
		upgradeLevelsInternal();
		this.transactionManager.commit(status);
	} catch (RuntimeException 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 안에 자리 잡고 있다.

트랜잭션 코드가 존재하지 않은 것 처럼 사라지게 할 수 없을까?

간단하게 트랜잭션 코드를 UserService 밖으로 뽑아내면 된다.

 

#UserService 인터페이스 도입

 

기존의 UserService 클래스를 UserServiceImpl로 이름을 변경한다.

 

UserService 인터페이스 

package springbook.user.service;

import springbook.user.domain.User;

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

 

트랜잭션 코드를 제거한 UserService구현 클래스 

public class UserServiceImpl implements UserService{
	
    //PlatformTransactionManager인스턴스 변수와 수정자메소드를 제거한다.
    //...
    
	@Override
	public void upgradeLevels() {
		List<User> users = userDao.getAll();
		for(User user : users) {
			if(canUpgradeLevel(user)) {
				upgradeLevel(user);
			}
		}
	}    
    
}

 

# 분리된 트랜잭션 기능 

 

이제 비지니스 트랜잭션 처리를 담은 UserServiceTx를 만들어보자.

package springbook.user.service;

import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import springbook.user.domain.User;

public class UserServiceTx implements UserService{
	
	// UserService를 구현 다른 오브젝트를 DI 받는다.
	// 여기서는 비즈니스 로직을 담은 UserServcieImpl를 DI받을 예정이다.
	UserService userService;
	
	public void setUserService(UserService userService) {
		this.userService = userService;
	}

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

	
	// 위임 받은 UserService 오브젝트에 모든 기능을 위임한다. 
	@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 설정 

 

test-applicationContext.xml

트랜잭션 오브젝트가 추가된 설정파일 

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

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

클라이언트(여기서는 UserServiceTest)는 UserServcieTx 빈을 호출하도록 만들어야 한다.

따라서 userService라는 대표적인 빈 아이디는 UserServcieTx클래스로 정의된 빈에 부여한다.

 

 

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

 

UserServiceTest

 

수정한 스프링의 설정파일에는 UserService라는 인터페이스 타입을 가진 두 개의 빈이 존재한다.

@Autowired는 기본적으로 타입을 이용해 빈을 찾지만 만약 타입으로 

하나의 빈을 결정할 수 없을 경우에는 필드 이름을 이용해서 빈을 찾는다.

 

따라서 UserServcieTest에서 다음과 같은 userService변수를 설정해두면

아이디가 userService인 빈이 주입될 것이다.

@Autowired
UserService userService;

 

 MaiSender 목 오브젝트를 이용한 테스트에서는 테스트에서 직접 MailSerder를 DI 해줘야 할 필요가 있었다.

MailSender를 DI해줄 대상을 구체적으로 알고 있어야 하기 때문에 

UserServiceImpl클래스의 오브젝트를 가져올 필요가 있다.

 

목 오브젝트 설정이 필요한 테스트 코드 수정

@Autowired
UserServiceImpl userServiceImpl;

//...

@Test
@DirtiesContext //컨텍스트의 DI 설정을 변경하는 테스트라는 것을 알려준다. 
public void upgradeLevels() throws Exception {
	userDao.deleteAll();
	for(User user : users) userDao.add(user);
	
	MockMailSender mockMailSender = new MockMailSender();
	userServiceImpl.setMailSender(mockMailSender);
	//...
}

 

분리 테스트 기능이 포함되도록 수정한 upgradeAllOrNothing()

@Test
public void upgradeAllorNothing() throws Exception {
	UserServiceImpl 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 {
		txUserService.upgradeLevels();
		fail("TestUserServiceException exptected");
	} catch (TestUserServiceException e) {
		// TODO: handle exception
	}
		
	checkLevelUpgrade(users.get(1), false);
}

 

이제 테스트를 수행하여 확인한다.

 

 

관련글 더보기

댓글 영역