6.1) 트랜잭션 코드의 분리
메소드 분리
- 메일 발송 기술과 환경에도 종속적이지 않은 깔끔한 코드로 다듬어온 UserService이지만, 코드를 볼 때마다 찜찜하다.
- 스프링이 제공하는 트랜잭션 인터페이스를 썼음에도 비즈니스 로직이 주인이어야 할 메소드 안에
트랜잭션 경계설정을 위해 넣은 코드가 이름도 길고 더 많은 자리를 차지하고 있다. - 하지만 논리적으로 따져봐도 트랜잭션의 경계는 분명 비즈니스 로직의 전후에 설정돼야 한다.
- 스프링이 제공하는 트랜잭션 인터페이스를 썼음에도 비즈니스 로직이 주인이어야 할 메소드 안에
- 트랜잭션이 적용된 코드를 다시 한번 살펴보자.
- 자세히 살펴보면 비즈니스 로직 코드를 사이에 두고 트랜잭션 시작과 종료를 담당하는 코드가 앞뒤에 위치하고 있다.
- 또, 이 코드는 비즈니스 로직 코드에서 직접 DB를 사용하지 않기 때문에
트랜잭션 경계설정의 코드와 비즈니스 로직 코드 간에 서로 주고받는 정보가 없다. - 따라서 이 코드는 성격이 다를 뿐 아니라 서로 주고받는 것도 없는, 완벽하게 독립적인 코드다.
다만 이 비즈니스 로직을 담당하는 코드가 트랜잭션의 시작과 종료 작업 사이에서 수행돼야 한다는 사항만 지켜지면 된다.
- 그렇다면 이 성격이 다른 코드를 두 개의 메소드로 분리할 수 있지 않을까?
- 비즈니스 로직을 담당하는 코드를 메소드로 추출해서 독립시켜 보자.
- 코드를 분리하고 나니 보기가 한결 깔끔해졌다.
/**
* UserService 클래스
*/
public class UserService {
...
// 사용자 레벨 업그레이드 메소드
// 스프링의 트랜잭션 추상화 API를 적용
public void upgradeLevels() throws Exception {
// 트랜잭션 매니저를 빈으로 분리시킨 후 DI 받아 트랜잭션 시작
// DI 받은 트랜잭션 매니저를 공유해서 사용하므로 멀티스레드 환경에서도 안전한다.
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
// 트랜잭션 안에서 진행되는 작업
try {
upgradeLevelsInternal();
// 정상적으로 작업을 마치면 트랜잭션 커밋
transactionManager.commit(status);
} catch (RuntimeException e) {
// 예외가 발생하면 롤백
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를 호출해 사용할 경우
UserService는 현재 클래스로 되어 있으니 다른 코드에서 사용한다면 UserService 클래스를 직접 참조하게 된다. - 이때 트랜잭션 코드를 UserService에서 빼버리면 UserService 클래스를 직접 사용하는 클라이언트 코드에서는
트랜잭션 기능이 빠진 UserService를 사용하게 되어 문제가 발생한다. - 직접 사용하는 것이 문제가 된다면 간접적으로 사용하면 된다.
- DI의 개념을 이용하면 실제 사용할 오브젝트의 클래스 정체는 감춘 채 인터페이스를 통해 간접으로 접근하므로
구현 클래스는 얼마든지 외부에서 변경할 수 있다.
- 다른 클래스나 모듈에서 UserService를 호출해 사용할 경우
- 그러므로 UserService를 인터페이스로 만들고 기존 코드는 UserService 인터페이스의 구현 클래스를 만들어넣도록 한다.
- 그러면 클라이언트와 결합이 약해지고, 직접 구현 클래스에 의존하고 있지 않기 때문에 유연한 확장이 가능해진다.
- 또한 구현 클래스를 바꿔가면서 사용할 수 있게 된다.
- 게다가 한 번에 한 가지 클래스를 선택해서 적용하는 것 뿐만 아니라, 한 번에 두 개의 구현 클래스를 이용할 수 있다.
- 한 번에 두 개의 UserService 인터페이스 구현 클래스를 동시에 이용한다면 어떨까?
- 지금 해결하려고 하는 문제는 UserService에는 순수하게 비즈니스 로직을 담고 있는 코드만 놔두고
트랜잭션 경계설정을 담당하는 코드를 외부로 빼내려는 것이다.
하지만 클라이언트가 UserService의 기능을 제대로 이용하려면 트랜잭션이 적용돼야 한다. - 그러므로 클라이언트가 사용할 로직을 담은 핵심 메소드만 UserService 인터페이스로 만든다.
- 그리고 UserService를 구현한 또 다른 구현 클래스인 UserServiceTx를 만들고
단지 트랜잭션의 경계설정이라는 책임을 맡도록 한다. - UserServiceTx는 스스로 비즈니스 로직을 담고 있지 않기 때문에
트랜잭션과 관련된 코드는 제거되었으며 UserService 클래스의 비즈니스 로직을 담고 있는
UserService의 구현 클래스인 UserServiceImpl에 실제적인 로직 처리 작업을 위임하도록 한다. - 그 위임을 위해 transactionManager이라는 이름의 빈으로 등록된 트랜잭션 매니저를 DI로 받아뒀다가
트랜잭션 안에서 동작하도록 만들어줘야 하는 호출 작업 이전과 이후에
필요한 트랜잭션 경계설정 API를 사용해 적절한 트랜잭션 경계를 설정해준다. - 마지막으로 설정파일을 수정하여
클라이언트가 UserService라는 인터페이스를 통해 사용자 관리 로직을 이용하려고 할 때
먼저 트랜잭션을 담당하는 오브젝트가 사용돼서 트랜잭션에 관련된 작업을 진행해주고,
실제 사용자 관리 로직을 담은 오브젝트가 이후에 호출돼서 비즈니스 로직에 관련된 작업을 수행하도록 한다. - 이를 위해 transactionManager는 UserServiceTx의 빈이,
userDao와 mailSender는 UserServiceImpl 빈이 각각 의존하도록 프로퍼티 정보를 분리한다.
그리고 클라이언트는 UserServiceTx 빈을 호출해서 사용하도록 만들어
userService라는 빈 아이디는 UserServiceTx 클래스로 정의된 빈에 부여해주고,
userService 빈은 UserServiceImpl 클래스 정의되는, userServiceImpl인 빈을 DI하게 만든다. - 이를 통해 클라이언트 입장에서는 결국 트랜잭션이 적용된 비즈니스 로직의 구현이라는 기대하는 동작이 일어날 것이다.
- 지금 해결하려고 하는 문제는 UserService에는 순수하게 비즈니스 로직을 담고 있는 코드만 놔두고
/**
* UserService 인터페이스
*/
public interface UserService {
void add(User user);
void upgradeLevels();
}
/**
* UserService 클래스
*/
public class UserServiceImpl implements UserService {
public static final int MIN_LOGIN_FOR_SILVER = 50;
public static final int MIN_RECOMMEND_FOR_GOLD = 30;
UserDao userDao;
private MailSender mailSender;
// UserDao 인터페이스 타입으로 userDao 빈을 DI 받아 사용한다.
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
/* UserServiceTx로 분리
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
} */
// 메일 전송 기능을 가진 오브젝트를 DI 받아 사용한다.
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
// 사용자 신규 등록 로직
@Override
public void add(User user) {
if (user.getLevel() == null)
user.setLevel(Level.BASIC);
userDao.add(user);
}
// 사용자 레벨 업그레이드 메소드
/* 스프링의 트랜잭션 추상화 API를 적용 -> UserServiceTx로 분리
public void upgradeLevels() {
// 트랜잭션 매니저를 빈으로 분리시킨 후 DI 받아 트랜잭션 시작
// DI 받은 트랜잭션 매니저를 공유해서 사용하므로 멀티스레드 환경에서도 안전한다.
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
// 트랜잭션 안에서 진행되는 작업
try {
upgradeLevelsInternal();
// 정상적으로 작업을 마치면 트랜잭션 커밋
transactionManager.commit(status);
} catch (RuntimeException e) {
// 예외가 발생하면 롤백
transactionManager.rollback(status);
throw e;
}
} */
// 사용자 레벨 업그레이드 메소드
@Override
public void upgradeLevels() {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
// 업그레이드 가능 확인 메소드
private boolean canUpgradeLevel(User user) {
Level currentLevel = user.getLevel();
// 레벨별로 구분해서 조건을 판단한다.
switch (currentLevel) {
case BASIC:
return (user.getLogin() >= MIN_LOGIN_FOR_SILVER);
case SILVER:
return (user.getRecommend() >= MIN_RECOMMEND_FOR_GOLD);
case GOLD:
return false;
// 현재 로직에서 다룰 수 없는 레벨이 주어지면 예외를 발생시킨다.
// 새로운 레벨이 추가되고 로직을 수정하지 않으면 에러가 나서 확인할 수 있다.
default:
throw new IllegalArgumentException("Unknown level: " + currentLevel);
}
}
// 레벨 업그레이드
protected void upgradeLevel(User user) {
user.upgradeLevel();
userDao.update(user);
sendUpgradeEMail(user);
}
// 스프링의 MailSender를 이용한 메일 발송 메소드
private void sendUpgradeEMail(User user) {
// MailMessage 인터페이스의 구현 클래스 오브젝트를 만들어 메일 내용을 작성한다.
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(user.getEmail());
mailMessage.setFrom("useradmin@ksug.org");
mailMessage.setSubject("Upgrade 안내");
mailMessage.setText("사용자님의 등급이 " + user.getLevel().name() + "로 업그레이드되었습니다.");
this.mailSender.send(mailMessage);
}
}
/**
* 위임 기능을 가진 UserServiceTx
*/
public class UserServiceTx implements UserService {
// UserService를 구현한 다른 오브젝트를 DI 받는다.
UserService userService;
PlatformTransactionManager transactionManager;
public void setUserService(UserService userService) {
this.userService = userService;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
// DI 받은 UserService 오브젝트에 모든 기능을 위임한다.
public void add(User user) {
userService.add(user);
}
public void upgradeLevels() {
// 트랜잭션 매니저를 빈으로 분리시킨 후 DI 받아 트랜잭션 시작
// DI 받은 트랜잭션 매니저를 공유해서 사용하므로 멀티스레드 환경에서도 안전한다.
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
// 트랜잭션 안에서 진행되는 작업
try {
userService.upgradeLevels();
// 정상적으로 작업을 마치면 트랜잭션 커밋
this.transactionManager.commit(status);
} catch (RuntimeException e) {
// 예외가 발생하면 롤백
transactionManager.rollback(status);
throw e;
}
}
}
/**
* 위임 기능을 가진 UserServiceTx
*/
public class UserServiceTx implements UserService {
// UserService를 구현한 다른 오브젝트를 DI 받는다.
UserService userService;
PlatformTransactionManager transactionManager;
public void setUserService(UserService userService) {
this.userService = userService;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
// DI 받은 UserService 오브젝트에 모든 기능을 위임한다.
public void add(User user) {
userService.add(user);
}
public void upgradeLevels() {
// 트랜잭션 매니저를 빈으로 분리시킨 후 DI 받아 트랜잭션 시작
// DI 받은 트랜잭션 매니저를 공유해서 사용하므로 멀티스레드 환경에서도 안전한다.
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
// 트랜잭션 안에서 진행되는 작업
try {
userService.upgradeLevels();
// 정상적으로 작업을 마치면 트랜잭션 커밋
this.transactionManager.commit(status);
} catch (RuntimeException e) {
// 예외가 발생하면 롤백
transactionManager.rollback(status);
throw e;
}
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="userService" class="com.gaga.springtoby.user.service.UserServiceTx">
<property name="transactionManager" ref="transactionManager"/>
<property name="userService" ref="userServiceImpl"/>
</bean>
<bean id="userServiceImpl" class="com.gaga.springtoby.user.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<property name="mailSender" ref="mailSender"/>
</bean>
<bean id="userDao" class="com.gaga.springtoby.user.dao.UserDaoJdbc">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="mailSender" class="com.gaga.springtoby.user.service.DummyMailSender"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/toby"/>
<property name="username" value="toby"/>
<property name="password" value="gaga"/>
</bean>
</beans>


- 트랜잭션 분리에 따른 테스트를 수정해보자.
- 기존의 UserService 클래스가 인터페이스와 두 개의 클래스로 분리된 만큼
테스트에서도 적합한 타입과 빈을 사용하도록 변경해야 한다. - @Autowired는 기본적으로 타입이 일치하는 빈을 찾아주는데
UserService 인터페이스 타입을 가진 두 개의 빈이 존재하므로 하나의 빈을 결정할 수 없어
필드 이름을 이용해 빈을 찾게 된다.
그러므로 빈 아이디가 userService인 UserServiceTx가 빈으로 주입되게 된다. - 일반적인 UserService 기능의 테스트에서는 UserService 인터페이스를 통해 결과를 확인하는 것으로 충분하지만
MailSender 목 오브젝트를 이용한 테스트에서는 테스트에서 직접 MailSender를 DI 해줘야 할 필요가 있다.
그러므로 수동 DI를 적용하기 위해 어떤 클래스의 오브젝트인지 분명히 알 필요가 있으므로
@Autowired를 지정해서 UserServiceImpl 클래스로 만들어진 빈을 주입받도록 한다.
이후 목 오브젝트를 설정해주는 건 이제 UserService 인터페이스를 통해서는 불가능하므로
별도로 가져온 userServiceImple 빈에 해주도록 한다. - 트랜잭션 기술이 바르게 적용됐는지를 확인하기 위해 만든 upgradeAllOrNothing() 테스트의 경우
직접 테스트용 확장 클래스인 TestUserService도 만들고 수동 DI도 적용했으므로
기존의 오브젝트를 가지고 테스트해버리면, 트랜잭션이 빠져버렸으므로 트랜잭션 테스트가 정상적으로 되지 않는다. - 그러므로 트랜잭션 테스트용으로 특별 정의한 TestUserService 오브젝트를 UserServiceTx 오브젝트에 수동 DI 시킨 후
트랜잭션 기능까지 포함한 UserServiceTx의 메소드를 호출하면서 테스트를 수행하도록 한다.
그 후 TestUserService 클래스는 UserServiceImpl을 상속하도록 하면 비즈니스 로직을 가질 수 있게 된다.
- 기존의 UserService 클래스가 인터페이스와 두 개의 클래스로 분리된 만큼
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
@Autowired
UserService userService; // 빈 아이디가 userService인 UserServiceTx가 빈으로 주입
@Autowired
UserServiceImpl userServiceImpl;
@Autowired
PlatformTransactionManager transactionManager;
@Autowired
private UserDao userDao;
@Autowired
MailSender mailSender;
List<User> users; // 테스트 픽스처
@Before
public void setUp() {
users = Arrays.asList(
new User("bumjin", "bumjin@email,com", "박범진", "p1", Level.BASIC, MIN_LOGIN_FOR_SILVER - 1, 0),
new User("joytouch", "joytouch@email,com", "강명성", "p2", Level.BASIC, MIN_LOGIN_FOR_SILVER, 0),
new User("erwins", "erwins@email,com", "신승한", "p3", Level.SILVER, 60, MIN_RECOMMEND_FOR_GOLD - 1),
new User("madnite1", "madnite1@email,com", "이상호", "p4", Level.SILVER, 60, MIN_RECOMMEND_FOR_GOLD),
new User("green", "green@email,com", "오민규", "p5", Level.GOLD, 100, Integer.MAX_VALUE)
);
}
// userService 빈의 주입을 확인하는 테스트
@Test
public void bean() {
assertThat(this.userServiceImpl, is(notNullValue()));
}
// 목 오브젝트로 만든 메일 전송 확인용 MailSender 구현 클래스
static class MockMailSender implements MailSender {
// UserService로부터 전송 요청을 받은 메일 주소를 저장해두고 이를 읽을 수 있게 한다.
private List<String> requests = new ArrayList<String>();
public List<String> getRequests() {
return requests;
}
public void send(SimpleMailMessage mailMessage) throws MailException {
// 전송 요청을 받은 이메일 주소를 저장해준다.
// 간단하게 첫 번째 수신자 메일 주소만 저장했다.
requests.add(mailMessage.getTo()[0]);
}
public void send(SimpleMailMessage[] mailMessage) throws MailException {
}
}
// 사용자 레벨 업그레이드 테스트이자 메일 발송 대상을 확인하는 테스트
@Test
@DirtiesContext // 컨텍스트의 DI 설정을 변경(DummyMailSender -> MockMailSender)하는 테스트라는 것을 알려준다.
public void upgradeLevels() throws Exception {
userDao.deleteAll();
for (User user : users)
userDao.add(user);
// 메일 발송 결과를 테스트할 수 있도록 목 오브젝트를 만들어 userService의 의존 오브젝트로 주입해준다.
MockMailSender mockMailSender = new MockMailSender();
userServiceImpl.setMailSender(mockMailSender);
// 업그레이드 테스트, 메일 발송이 일어나면 MockMailSender 오브젝트의 리스트에 그 결과가 저장된다.
userService.upgradeLevels(); // UserService -> UserServiceImpl
// 각 사용자별로 업그레이드 후의 예상 레벨을 검증한다.
checkLevelUpgraded(users.get(0), false);
checkLevelUpgraded(users.get(1), true);
checkLevelUpgraded(users.get(2), false);
checkLevelUpgraded(users.get(3), true);
checkLevelUpgraded(users.get(4), false);
// 목 오브젝트에 저장된 메일 수신자 목록을 가져와 업그레이드 대상과 일치하는지 확인한다.
List<String> request = mockMailSender.getRequests();
assertThat(request.size(), is(2));
assertThat(request.get(0), is(users.get(1).getEmail()));
assertThat(request.get(1), is(users.get(3).getEmail()));
}
// add() 메소드의 테스트
@Test
public void add() {
userDao.deleteAll();
// GOLD 레벨이 이미 지정된 User라면 레벨을 초기화하지 않아야 한다.
User userWithLevel = users.get(4);
// 레벨이 비어 있는 사용자로 수정한다.
// 로직에 따라 등록 중에 BASIC 레벨이 설정되어야 한다.
User userWithoutLevel = users.get(0);
userWithoutLevel.setLevel(null);
userService.add(userWithLevel);
userService.add(userWithoutLevel);
// DB에 저장된 결과를 가져와 확인한다.
User userWithLevelRead = userDao.get(userWithLevel.getId());
User userWithoutLevelRead = userDao.get(userWithoutLevel.getId());
assertThat(userWithLevelRead.getLevel(), is(userWithLevel.getLevel()));
assertThat(userWithoutLevelRead.getLevel(), is(userWithoutLevel.getLevel()));
}
// DB에서 사용자 정보를 가져와 레벨을 확인하는 코드를 헬퍼 메소드로 분리한다.
// 어떤 레벨로 바뀔 것인가가 아니라, 다음 레벨로 업그레이드할 것인가 아닌가를 지정하도록 개선한다.
private void checkLevelUpgraded(User user, boolean upgraded) {
User userUpdate = userDao.get(user.getId());
if (upgraded) {
// 업그레이드가 일어났는지 확인한다.
assertThat(userUpdate.getLevel(), is(user.getLevel().nextLevel()));
} else {
// 업그레이드가 일어나지 않았는지 확인한다.
assertThat(userUpdate.getLevel(), is(user.getLevel()));
}
}
// UserService의 테스트용 대역 클래스
static class TestUserService extends UserServiceImpl {
private String id;
// 예외를 발생시킬 User 오브젝트의 id를 지정할 수 있게 만든다.
private TestUserService(String id) {
this.id = id;
}
// UserService의 메소드를 오버라이딩한다.
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 testUserService = new TestUserService(users.get(3).getId());
// userDao를 수동 DI 해준다.
testUserService.setUserDao(this.userDao);
// 테스트용 UserService를 위한 메일 전송 오브젝트 빈인 MailSender를 수동 DI 해준다.
testUserService.setMailSender(this.mailSender);
// 트랜잭션 기능을 분리한 UserServiceTx는 예외 발생용으로 수정할 필요가 없으니 그대로 사용한다.
UserServiceTx txUserService = new UserServiceTx();
txUserService.setTransactionManager(transactionManager);
txUserService.setUserService(testUserService);
userDao.deleteAll();
for (User user : users)
userDao.add(user);
// TestUserService는 업그레이드 작업 중에 예외가 발생해야 한다.
try {
// 트랜잭션 기능을 분리한 오브젝트를 통해 예외 발생용 TestUserService가 호출되게 해야 한다.
txUserService.upgradeLevels(); // UserService (UserServiceTx) -> UserServiceImpl
fail("TestUserServiceException expected");
}
// TestUserService가 던져주는 예외를 잡아서 계속 진행되도록 한다.
catch (TestUserServiceException e) {
}
// 예외가 발생하기 전에 레벨 변경이 있었던 사용자의 레벨이 처음 상태로 바뀌었나 확인한다.
checkLevelUpgraded(users.get(1), false);
}
}
- 이렇게 복잡한 트랜잭션 경계설정 코드를 분리할 경우 어떤 장점을 얻을 수 있을까?
- 첫째, 비즈니스 로직을 담당하는 UserServiceImpl를 작성할 때는 트랜잭션같은 기술적 내용에는 신경 쓰지 않아도 된다.
스프링의 JDBC나 JTA 같은 로우레벨의 트랜잭션 API는 물론이고 스프링의 트랜잭션 추상화 API조차 필요 없다.
트랜잭션은 DI를 이용해 UserServiceTx와 같은 트랜잭션 기능을 가진 오브젝트가 먼저 실행되도록 만들기만 하면 된다.
따라서 언제든지 트랜잭션을 도입할 수 있다. - 둘째, 비즈니스 로직에 대한 테스트를 손쉽게 만들어낼 수 있다.
- 첫째, 비즈니스 로직을 담당하는 UserServiceImpl를 작성할 때는 트랜잭션같은 기술적 내용에는 신경 쓰지 않아도 된다.
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (3) (0) | 2023.12.19 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (2) (0) | 2023.12.16 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (0) (0) | 2023.12.15 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 서비스 추상화 (4) (0) | 2023.12.14 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 서비스 추상화 (3) (0) | 2023.12.12 |
6.1) 트랜잭션 코드의 분리
메소드 분리
- 메일 발송 기술과 환경에도 종속적이지 않은 깔끔한 코드로 다듬어온 UserService이지만, 코드를 볼 때마다 찜찜하다.
- 스프링이 제공하는 트랜잭션 인터페이스를 썼음에도 비즈니스 로직이 주인이어야 할 메소드 안에
트랜잭션 경계설정을 위해 넣은 코드가 이름도 길고 더 많은 자리를 차지하고 있다. - 하지만 논리적으로 따져봐도 트랜잭션의 경계는 분명 비즈니스 로직의 전후에 설정돼야 한다.
- 스프링이 제공하는 트랜잭션 인터페이스를 썼음에도 비즈니스 로직이 주인이어야 할 메소드 안에
- 트랜잭션이 적용된 코드를 다시 한번 살펴보자.
- 자세히 살펴보면 비즈니스 로직 코드를 사이에 두고 트랜잭션 시작과 종료를 담당하는 코드가 앞뒤에 위치하고 있다.
- 또, 이 코드는 비즈니스 로직 코드에서 직접 DB를 사용하지 않기 때문에
트랜잭션 경계설정의 코드와 비즈니스 로직 코드 간에 서로 주고받는 정보가 없다. - 따라서 이 코드는 성격이 다를 뿐 아니라 서로 주고받는 것도 없는, 완벽하게 독립적인 코드다.
다만 이 비즈니스 로직을 담당하는 코드가 트랜잭션의 시작과 종료 작업 사이에서 수행돼야 한다는 사항만 지켜지면 된다.
- 그렇다면 이 성격이 다른 코드를 두 개의 메소드로 분리할 수 있지 않을까?
- 비즈니스 로직을 담당하는 코드를 메소드로 추출해서 독립시켜 보자.
- 코드를 분리하고 나니 보기가 한결 깔끔해졌다.
/**
* UserService 클래스
*/
public class UserService {
...
// 사용자 레벨 업그레이드 메소드
// 스프링의 트랜잭션 추상화 API를 적용
public void upgradeLevels() throws Exception {
// 트랜잭션 매니저를 빈으로 분리시킨 후 DI 받아 트랜잭션 시작
// DI 받은 트랜잭션 매니저를 공유해서 사용하므로 멀티스레드 환경에서도 안전한다.
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
// 트랜잭션 안에서 진행되는 작업
try {
upgradeLevelsInternal();
// 정상적으로 작업을 마치면 트랜잭션 커밋
transactionManager.commit(status);
} catch (RuntimeException e) {
// 예외가 발생하면 롤백
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를 호출해 사용할 경우
UserService는 현재 클래스로 되어 있으니 다른 코드에서 사용한다면 UserService 클래스를 직접 참조하게 된다. - 이때 트랜잭션 코드를 UserService에서 빼버리면 UserService 클래스를 직접 사용하는 클라이언트 코드에서는
트랜잭션 기능이 빠진 UserService를 사용하게 되어 문제가 발생한다. - 직접 사용하는 것이 문제가 된다면 간접적으로 사용하면 된다.
- DI의 개념을 이용하면 실제 사용할 오브젝트의 클래스 정체는 감춘 채 인터페이스를 통해 간접으로 접근하므로
구현 클래스는 얼마든지 외부에서 변경할 수 있다.
- 다른 클래스나 모듈에서 UserService를 호출해 사용할 경우
- 그러므로 UserService를 인터페이스로 만들고 기존 코드는 UserService 인터페이스의 구현 클래스를 만들어넣도록 한다.
- 그러면 클라이언트와 결합이 약해지고, 직접 구현 클래스에 의존하고 있지 않기 때문에 유연한 확장이 가능해진다.
- 또한 구현 클래스를 바꿔가면서 사용할 수 있게 된다.
- 게다가 한 번에 한 가지 클래스를 선택해서 적용하는 것 뿐만 아니라, 한 번에 두 개의 구현 클래스를 이용할 수 있다.
- 한 번에 두 개의 UserService 인터페이스 구현 클래스를 동시에 이용한다면 어떨까?
- 지금 해결하려고 하는 문제는 UserService에는 순수하게 비즈니스 로직을 담고 있는 코드만 놔두고
트랜잭션 경계설정을 담당하는 코드를 외부로 빼내려는 것이다.
하지만 클라이언트가 UserService의 기능을 제대로 이용하려면 트랜잭션이 적용돼야 한다. - 그러므로 클라이언트가 사용할 로직을 담은 핵심 메소드만 UserService 인터페이스로 만든다.
- 그리고 UserService를 구현한 또 다른 구현 클래스인 UserServiceTx를 만들고
단지 트랜잭션의 경계설정이라는 책임을 맡도록 한다. - UserServiceTx는 스스로 비즈니스 로직을 담고 있지 않기 때문에
트랜잭션과 관련된 코드는 제거되었으며 UserService 클래스의 비즈니스 로직을 담고 있는
UserService의 구현 클래스인 UserServiceImpl에 실제적인 로직 처리 작업을 위임하도록 한다. - 그 위임을 위해 transactionManager이라는 이름의 빈으로 등록된 트랜잭션 매니저를 DI로 받아뒀다가
트랜잭션 안에서 동작하도록 만들어줘야 하는 호출 작업 이전과 이후에
필요한 트랜잭션 경계설정 API를 사용해 적절한 트랜잭션 경계를 설정해준다. - 마지막으로 설정파일을 수정하여
클라이언트가 UserService라는 인터페이스를 통해 사용자 관리 로직을 이용하려고 할 때
먼저 트랜잭션을 담당하는 오브젝트가 사용돼서 트랜잭션에 관련된 작업을 진행해주고,
실제 사용자 관리 로직을 담은 오브젝트가 이후에 호출돼서 비즈니스 로직에 관련된 작업을 수행하도록 한다. - 이를 위해 transactionManager는 UserServiceTx의 빈이,
userDao와 mailSender는 UserServiceImpl 빈이 각각 의존하도록 프로퍼티 정보를 분리한다.
그리고 클라이언트는 UserServiceTx 빈을 호출해서 사용하도록 만들어
userService라는 빈 아이디는 UserServiceTx 클래스로 정의된 빈에 부여해주고,
userService 빈은 UserServiceImpl 클래스 정의되는, userServiceImpl인 빈을 DI하게 만든다. - 이를 통해 클라이언트 입장에서는 결국 트랜잭션이 적용된 비즈니스 로직의 구현이라는 기대하는 동작이 일어날 것이다.
- 지금 해결하려고 하는 문제는 UserService에는 순수하게 비즈니스 로직을 담고 있는 코드만 놔두고
/**
* UserService 인터페이스
*/
public interface UserService {
void add(User user);
void upgradeLevels();
}
/**
* UserService 클래스
*/
public class UserServiceImpl implements UserService {
public static final int MIN_LOGIN_FOR_SILVER = 50;
public static final int MIN_RECOMMEND_FOR_GOLD = 30;
UserDao userDao;
private MailSender mailSender;
// UserDao 인터페이스 타입으로 userDao 빈을 DI 받아 사용한다.
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
/* UserServiceTx로 분리
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
} */
// 메일 전송 기능을 가진 오브젝트를 DI 받아 사용한다.
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
// 사용자 신규 등록 로직
@Override
public void add(User user) {
if (user.getLevel() == null)
user.setLevel(Level.BASIC);
userDao.add(user);
}
// 사용자 레벨 업그레이드 메소드
/* 스프링의 트랜잭션 추상화 API를 적용 -> UserServiceTx로 분리
public void upgradeLevels() {
// 트랜잭션 매니저를 빈으로 분리시킨 후 DI 받아 트랜잭션 시작
// DI 받은 트랜잭션 매니저를 공유해서 사용하므로 멀티스레드 환경에서도 안전한다.
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
// 트랜잭션 안에서 진행되는 작업
try {
upgradeLevelsInternal();
// 정상적으로 작업을 마치면 트랜잭션 커밋
transactionManager.commit(status);
} catch (RuntimeException e) {
// 예외가 발생하면 롤백
transactionManager.rollback(status);
throw e;
}
} */
// 사용자 레벨 업그레이드 메소드
@Override
public void upgradeLevels() {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
// 업그레이드 가능 확인 메소드
private boolean canUpgradeLevel(User user) {
Level currentLevel = user.getLevel();
// 레벨별로 구분해서 조건을 판단한다.
switch (currentLevel) {
case BASIC:
return (user.getLogin() >= MIN_LOGIN_FOR_SILVER);
case SILVER:
return (user.getRecommend() >= MIN_RECOMMEND_FOR_GOLD);
case GOLD:
return false;
// 현재 로직에서 다룰 수 없는 레벨이 주어지면 예외를 발생시킨다.
// 새로운 레벨이 추가되고 로직을 수정하지 않으면 에러가 나서 확인할 수 있다.
default:
throw new IllegalArgumentException("Unknown level: " + currentLevel);
}
}
// 레벨 업그레이드
protected void upgradeLevel(User user) {
user.upgradeLevel();
userDao.update(user);
sendUpgradeEMail(user);
}
// 스프링의 MailSender를 이용한 메일 발송 메소드
private void sendUpgradeEMail(User user) {
// MailMessage 인터페이스의 구현 클래스 오브젝트를 만들어 메일 내용을 작성한다.
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(user.getEmail());
mailMessage.setFrom("useradmin@ksug.org");
mailMessage.setSubject("Upgrade 안내");
mailMessage.setText("사용자님의 등급이 " + user.getLevel().name() + "로 업그레이드되었습니다.");
this.mailSender.send(mailMessage);
}
}
/**
* 위임 기능을 가진 UserServiceTx
*/
public class UserServiceTx implements UserService {
// UserService를 구현한 다른 오브젝트를 DI 받는다.
UserService userService;
PlatformTransactionManager transactionManager;
public void setUserService(UserService userService) {
this.userService = userService;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
// DI 받은 UserService 오브젝트에 모든 기능을 위임한다.
public void add(User user) {
userService.add(user);
}
public void upgradeLevels() {
// 트랜잭션 매니저를 빈으로 분리시킨 후 DI 받아 트랜잭션 시작
// DI 받은 트랜잭션 매니저를 공유해서 사용하므로 멀티스레드 환경에서도 안전한다.
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
// 트랜잭션 안에서 진행되는 작업
try {
userService.upgradeLevels();
// 정상적으로 작업을 마치면 트랜잭션 커밋
this.transactionManager.commit(status);
} catch (RuntimeException e) {
// 예외가 발생하면 롤백
transactionManager.rollback(status);
throw e;
}
}
}
/**
* 위임 기능을 가진 UserServiceTx
*/
public class UserServiceTx implements UserService {
// UserService를 구현한 다른 오브젝트를 DI 받는다.
UserService userService;
PlatformTransactionManager transactionManager;
public void setUserService(UserService userService) {
this.userService = userService;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
// DI 받은 UserService 오브젝트에 모든 기능을 위임한다.
public void add(User user) {
userService.add(user);
}
public void upgradeLevels() {
// 트랜잭션 매니저를 빈으로 분리시킨 후 DI 받아 트랜잭션 시작
// DI 받은 트랜잭션 매니저를 공유해서 사용하므로 멀티스레드 환경에서도 안전한다.
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
// 트랜잭션 안에서 진행되는 작업
try {
userService.upgradeLevels();
// 정상적으로 작업을 마치면 트랜잭션 커밋
this.transactionManager.commit(status);
} catch (RuntimeException e) {
// 예외가 발생하면 롤백
transactionManager.rollback(status);
throw e;
}
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="userService" class="com.gaga.springtoby.user.service.UserServiceTx">
<property name="transactionManager" ref="transactionManager"/>
<property name="userService" ref="userServiceImpl"/>
</bean>
<bean id="userServiceImpl" class="com.gaga.springtoby.user.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<property name="mailSender" ref="mailSender"/>
</bean>
<bean id="userDao" class="com.gaga.springtoby.user.dao.UserDaoJdbc">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="mailSender" class="com.gaga.springtoby.user.service.DummyMailSender"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/toby"/>
<property name="username" value="toby"/>
<property name="password" value="gaga"/>
</bean>
</beans>


- 트랜잭션 분리에 따른 테스트를 수정해보자.
- 기존의 UserService 클래스가 인터페이스와 두 개의 클래스로 분리된 만큼
테스트에서도 적합한 타입과 빈을 사용하도록 변경해야 한다. - @Autowired는 기본적으로 타입이 일치하는 빈을 찾아주는데
UserService 인터페이스 타입을 가진 두 개의 빈이 존재하므로 하나의 빈을 결정할 수 없어
필드 이름을 이용해 빈을 찾게 된다.
그러므로 빈 아이디가 userService인 UserServiceTx가 빈으로 주입되게 된다. - 일반적인 UserService 기능의 테스트에서는 UserService 인터페이스를 통해 결과를 확인하는 것으로 충분하지만
MailSender 목 오브젝트를 이용한 테스트에서는 테스트에서 직접 MailSender를 DI 해줘야 할 필요가 있다.
그러므로 수동 DI를 적용하기 위해 어떤 클래스의 오브젝트인지 분명히 알 필요가 있으므로
@Autowired를 지정해서 UserServiceImpl 클래스로 만들어진 빈을 주입받도록 한다.
이후 목 오브젝트를 설정해주는 건 이제 UserService 인터페이스를 통해서는 불가능하므로
별도로 가져온 userServiceImple 빈에 해주도록 한다. - 트랜잭션 기술이 바르게 적용됐는지를 확인하기 위해 만든 upgradeAllOrNothing() 테스트의 경우
직접 테스트용 확장 클래스인 TestUserService도 만들고 수동 DI도 적용했으므로
기존의 오브젝트를 가지고 테스트해버리면, 트랜잭션이 빠져버렸으므로 트랜잭션 테스트가 정상적으로 되지 않는다. - 그러므로 트랜잭션 테스트용으로 특별 정의한 TestUserService 오브젝트를 UserServiceTx 오브젝트에 수동 DI 시킨 후
트랜잭션 기능까지 포함한 UserServiceTx의 메소드를 호출하면서 테스트를 수행하도록 한다.
그 후 TestUserService 클래스는 UserServiceImpl을 상속하도록 하면 비즈니스 로직을 가질 수 있게 된다.
- 기존의 UserService 클래스가 인터페이스와 두 개의 클래스로 분리된 만큼
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
@Autowired
UserService userService; // 빈 아이디가 userService인 UserServiceTx가 빈으로 주입
@Autowired
UserServiceImpl userServiceImpl;
@Autowired
PlatformTransactionManager transactionManager;
@Autowired
private UserDao userDao;
@Autowired
MailSender mailSender;
List<User> users; // 테스트 픽스처
@Before
public void setUp() {
users = Arrays.asList(
new User("bumjin", "bumjin@email,com", "박범진", "p1", Level.BASIC, MIN_LOGIN_FOR_SILVER - 1, 0),
new User("joytouch", "joytouch@email,com", "강명성", "p2", Level.BASIC, MIN_LOGIN_FOR_SILVER, 0),
new User("erwins", "erwins@email,com", "신승한", "p3", Level.SILVER, 60, MIN_RECOMMEND_FOR_GOLD - 1),
new User("madnite1", "madnite1@email,com", "이상호", "p4", Level.SILVER, 60, MIN_RECOMMEND_FOR_GOLD),
new User("green", "green@email,com", "오민규", "p5", Level.GOLD, 100, Integer.MAX_VALUE)
);
}
// userService 빈의 주입을 확인하는 테스트
@Test
public void bean() {
assertThat(this.userServiceImpl, is(notNullValue()));
}
// 목 오브젝트로 만든 메일 전송 확인용 MailSender 구현 클래스
static class MockMailSender implements MailSender {
// UserService로부터 전송 요청을 받은 메일 주소를 저장해두고 이를 읽을 수 있게 한다.
private List<String> requests = new ArrayList<String>();
public List<String> getRequests() {
return requests;
}
public void send(SimpleMailMessage mailMessage) throws MailException {
// 전송 요청을 받은 이메일 주소를 저장해준다.
// 간단하게 첫 번째 수신자 메일 주소만 저장했다.
requests.add(mailMessage.getTo()[0]);
}
public void send(SimpleMailMessage[] mailMessage) throws MailException {
}
}
// 사용자 레벨 업그레이드 테스트이자 메일 발송 대상을 확인하는 테스트
@Test
@DirtiesContext // 컨텍스트의 DI 설정을 변경(DummyMailSender -> MockMailSender)하는 테스트라는 것을 알려준다.
public void upgradeLevels() throws Exception {
userDao.deleteAll();
for (User user : users)
userDao.add(user);
// 메일 발송 결과를 테스트할 수 있도록 목 오브젝트를 만들어 userService의 의존 오브젝트로 주입해준다.
MockMailSender mockMailSender = new MockMailSender();
userServiceImpl.setMailSender(mockMailSender);
// 업그레이드 테스트, 메일 발송이 일어나면 MockMailSender 오브젝트의 리스트에 그 결과가 저장된다.
userService.upgradeLevels(); // UserService -> UserServiceImpl
// 각 사용자별로 업그레이드 후의 예상 레벨을 검증한다.
checkLevelUpgraded(users.get(0), false);
checkLevelUpgraded(users.get(1), true);
checkLevelUpgraded(users.get(2), false);
checkLevelUpgraded(users.get(3), true);
checkLevelUpgraded(users.get(4), false);
// 목 오브젝트에 저장된 메일 수신자 목록을 가져와 업그레이드 대상과 일치하는지 확인한다.
List<String> request = mockMailSender.getRequests();
assertThat(request.size(), is(2));
assertThat(request.get(0), is(users.get(1).getEmail()));
assertThat(request.get(1), is(users.get(3).getEmail()));
}
// add() 메소드의 테스트
@Test
public void add() {
userDao.deleteAll();
// GOLD 레벨이 이미 지정된 User라면 레벨을 초기화하지 않아야 한다.
User userWithLevel = users.get(4);
// 레벨이 비어 있는 사용자로 수정한다.
// 로직에 따라 등록 중에 BASIC 레벨이 설정되어야 한다.
User userWithoutLevel = users.get(0);
userWithoutLevel.setLevel(null);
userService.add(userWithLevel);
userService.add(userWithoutLevel);
// DB에 저장된 결과를 가져와 확인한다.
User userWithLevelRead = userDao.get(userWithLevel.getId());
User userWithoutLevelRead = userDao.get(userWithoutLevel.getId());
assertThat(userWithLevelRead.getLevel(), is(userWithLevel.getLevel()));
assertThat(userWithoutLevelRead.getLevel(), is(userWithoutLevel.getLevel()));
}
// DB에서 사용자 정보를 가져와 레벨을 확인하는 코드를 헬퍼 메소드로 분리한다.
// 어떤 레벨로 바뀔 것인가가 아니라, 다음 레벨로 업그레이드할 것인가 아닌가를 지정하도록 개선한다.
private void checkLevelUpgraded(User user, boolean upgraded) {
User userUpdate = userDao.get(user.getId());
if (upgraded) {
// 업그레이드가 일어났는지 확인한다.
assertThat(userUpdate.getLevel(), is(user.getLevel().nextLevel()));
} else {
// 업그레이드가 일어나지 않았는지 확인한다.
assertThat(userUpdate.getLevel(), is(user.getLevel()));
}
}
// UserService의 테스트용 대역 클래스
static class TestUserService extends UserServiceImpl {
private String id;
// 예외를 발생시킬 User 오브젝트의 id를 지정할 수 있게 만든다.
private TestUserService(String id) {
this.id = id;
}
// UserService의 메소드를 오버라이딩한다.
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 testUserService = new TestUserService(users.get(3).getId());
// userDao를 수동 DI 해준다.
testUserService.setUserDao(this.userDao);
// 테스트용 UserService를 위한 메일 전송 오브젝트 빈인 MailSender를 수동 DI 해준다.
testUserService.setMailSender(this.mailSender);
// 트랜잭션 기능을 분리한 UserServiceTx는 예외 발생용으로 수정할 필요가 없으니 그대로 사용한다.
UserServiceTx txUserService = new UserServiceTx();
txUserService.setTransactionManager(transactionManager);
txUserService.setUserService(testUserService);
userDao.deleteAll();
for (User user : users)
userDao.add(user);
// TestUserService는 업그레이드 작업 중에 예외가 발생해야 한다.
try {
// 트랜잭션 기능을 분리한 오브젝트를 통해 예외 발생용 TestUserService가 호출되게 해야 한다.
txUserService.upgradeLevels(); // UserService (UserServiceTx) -> UserServiceImpl
fail("TestUserServiceException expected");
}
// TestUserService가 던져주는 예외를 잡아서 계속 진행되도록 한다.
catch (TestUserServiceException e) {
}
// 예외가 발생하기 전에 레벨 변경이 있었던 사용자의 레벨이 처음 상태로 바뀌었나 확인한다.
checkLevelUpgraded(users.get(1), false);
}
}
- 이렇게 복잡한 트랜잭션 경계설정 코드를 분리할 경우 어떤 장점을 얻을 수 있을까?
- 첫째, 비즈니스 로직을 담당하는 UserServiceImpl를 작성할 때는 트랜잭션같은 기술적 내용에는 신경 쓰지 않아도 된다.
스프링의 JDBC나 JTA 같은 로우레벨의 트랜잭션 API는 물론이고 스프링의 트랜잭션 추상화 API조차 필요 없다.
트랜잭션은 DI를 이용해 UserServiceTx와 같은 트랜잭션 기능을 가진 오브젝트가 먼저 실행되도록 만들기만 하면 된다.
따라서 언제든지 트랜잭션을 도입할 수 있다. - 둘째, 비즈니스 로직에 대한 테스트를 손쉽게 만들어낼 수 있다.
- 첫째, 비즈니스 로직을 담당하는 UserServiceImpl를 작성할 때는 트랜잭션같은 기술적 내용에는 신경 쓰지 않아도 된다.
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (3) (0) | 2023.12.19 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (2) (0) | 2023.12.16 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (0) (0) | 2023.12.15 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 서비스 추상화 (4) (0) | 2023.12.14 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 서비스 추상화 (3) (0) | 2023.12.12 |