메소드 분리
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;
}
}
위 코드를 보면 뚜렷하게 두 가지 종류의 코드가 구분되어 있습니다. 비즈니스 로직 코드를 사이에 두고 트랜잭션 시작과 종료를 담당하는 코드가 앞뒤에 위치하고 있습니다. 또, 이 코드는 트랜잭션 경계 설정의 코드와 비즈니스 로직 코드 간에 서로 주고받는 정보가 없습니다. 비즈니스 로직 코드에서 직접 DB를 사용하지 않기 때문에 트랜잭션 준비 과정에서 만들어진 DB 커넥션 정보 등을 직접 참조할 필요가 없기 때문입니다. 즉, 이 두 코드는 완벽하게 독립적인 코드입니다. 6장은 이러한 성격이 다른 두 코드를 두 개의 메소드로 분리할 수 있지 않을까? 하는 의문에서 시작됩니다.
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;
}
}
private void upgradeLevelsInternal() {
List<User> users = userDAO.getAll();
for (User user: users){
if(canUpgradeLevel(user)){
upgradeLevel(user);
}
}
}
분리된 비즈니스 로직 코드는 트랜잭션을 적용하기 전과 동일합니다. 하지만 코드를 분리하고 나니 보기 한결 깔끔해졌습니다.
DI를 이용한 클래스 분리
하지만 위 코드에서 트랜잭션을 담당하는 기술적인 코드가 UserService 안에 자리잡고 있습니다. 트랜잭션 코드를 클래스 밖으로 뽑아내면 UserService에서는 보이지 않게 됩니다.
DI의 기본 아이디어는 실제 사용할 오브젝트 클래스의 정체는 감춘 채 인터페이스를 통해 간접으로 접근하는 것 입니다. 그렇기에 구현 클래스는 얼마든지 외부에서 변경할 수 있습니다.
현재 구조는 위 그림처럼 UserService클래스와 이를 사용하는 클라이언트 간의 관계가 강한 결합도로 구성되어 있습니다. 이 결함을 약하게 하기 위해서는 UserService를 인터페이스로 만들고 기존 코드는 UserService 인터페이스의 구현 클래스를 만들어 넣으면 됩니다.
바뀐 구조입니다. 직접적으로 구현 클래스에 의존하지 않아 유연한 확장이 가능해졌습니다.
이렇듯 인터페이스를 통해 구현 클래스를 클라이언트에 노출하지 않고 런타임 시에 DI를 통해 적용하는 방법을 쓰는 이유는, 일반적으로 구현 클래스를 바꿔가면서 사용하기 위해서입니다.
만약 한 번에 두 개의 UserService 인터페이스 구현 클래스를 동시에 이용하면 어떨까요? 클라이언트가 UserService의 기능을 제대로 이용하려면 트랜잭션이 적용되어야 하기 때문에 아래와 같은 구조를 생각할 수 있습니다.
위와 같은 구조로 트랜잭션 경계설정 코드를 분리해 낸 결과를 보겠습니다.
- UserService 인터페이스
public interface UserService{
void add(User user);
void upgradeLevels();
}
- 트랜잭션 코드를 제거한 UserService 구현 클래스
package springbook.user.service;
...
public class UserServiceImpl implements UserService{
UserDAO userDao;
MailSender mailSender;
public void upgradeLevels() {
List<User> users = userDAO.getALL();
for (User user:users) {
if(canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
...
위와 같이 수정하고 나니 메일 발송 기능을 추가한 것을 제외하면 트랜잭션을 고려하지 않고 단순히 로직만을 구현했던 처음 모습으로 돌아왔습니다. 코드 어디에도 기술이나 서버환경, 스프링에 관련된 코드가 보이지 않습니다. 즉, User라는 도메인 정보를 가진 비즈니스 로직에만 충실한 깔끔한 코드가 된 것입니다.
트랜잭션 처리는 UserServiceTx에 담습니다. UserServiceTx는 기본적으로 UserService를 구현하게 만듭니다.
위 코드에 위임 기능과 트랜잭션을 적용한다면 이러한 UserServiceTx가 됩니다.
Public class UserServiceTx implements UserService {
UserService userService;
PlatformTransactionManager transactionManager;
public void setTransactionManager ( PlatformTransactionManager transactionManager ) {
this.transactionManager = transactionManager;
}
public void serUserService(UserService userService) {
this.userService = userService;
}
public void add(User user) {
this.userService.add(user);
}
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를 설정하면, 만들어질 빈 오브젝트와 그 의존관계는 아래와 같을 것입니다.
기존에 userService 빈이 의존하고 있던 transactionManager은 UserServiceTx의 빈이, userDAO와 mailSender는 UserServiceImpl빈이 각각 의존하도록 프로퍼티 정보를 분리하면 스프링 설정파일의 내용은
<bean id="userService" class="springbook.user.service.UserServiceTx">
<property name="transactionManager" ref="transactionManager" />
<property name~="userService" ref="userServicelmpl" />
</bean>
<bean id = "userServiceImpl" class="pringbook.user.service.UserServicelmpl">
<property name="userDao" ref="userDao" />
<property name="mailSender" ref="mailSender" />
</bean>
위와 같이 변경됩니다. userService 빈은 userServiceImpl빈을 DI하게 만듭니다.
트랜잭션 분리가 끝났으니 테스트도 수정이 필요해집니다.
@Test
public void upgradeLevels() throws Exception {
...
MockMailSender mockMailSender = new MockMailSender();
userServicelmpl.setMailSender(mockMailSender);
이 코드는 upgradeLevels() 테스트 메소드 입니다. 기존에 UserService 를 통했던 MailSender의 목 오브젝트를 UserServiceImp빈으로 수정하였습니다. (@Autowired UserServiceImpl userServiceImpl; )
@Test
public void upgradeAIIOrNothing() throws Exception {
TestUserService testUserService =new TestUserService(users.get(3).getld());
testUserService.setUserDao(userDao);
testUserService.setMailSender(mailSender);
UserServiceTx txUserService =new UserServiceTx();
txUserService.setTransactionManager(transaction삐anager);
txUserService.setUserService(testUserService);
// 위 부분은 예외발생용으로 수정할 필요가 없으니 그대로 사용
userDao .deleteAll () ;
for(User user users) userDao.add(user);
try {
txUserService.upgradeLevels();
fai1("TestUserServiceException expected");
}
...
upgradeAllOrNothing() 테스트는 트랜잭션이 제대로 적용했는지 확인하는 학습 테스트라 바뀐 구조를 적용해주어야합니다.
static class TestUserService extends UserServiceImpl {
마지막으로 트랜잭션 테스트용인 TestUserService클래스가 UserServiceImpl 클래스를 상속하도록 바꿔주어야 합니다.
그럼 위와 같은 과정을 진행한 이유가 무엇일까요? 트랜잭션 경계설정 코드를 분리하면, 비즈니스 로직(위 예제에서는 UserServiceImpl)에서 트랜잭션과 같은 기술적인 내용을 신경 쓰지 않아도 됩니다. 트랜잭션은 DI를 이용하여 트랜잭션 기능을 가진 오브젝트가 먼저 실행되도록 하면 되기 때문입니다. 즉, 언제든 트랜잭션을 도입할 수 있게되며, 잘 만든 비즈니스 코드를 잘 지켜낼 수 있게 됩니다. 또한 비즈니스 로직에 대한 테스트를 손쉽게 만들어 낼 수 있습니다.
'Backend > Spring Boot' 카테고리의 다른 글
[SpringBoot] 실행 속도 더 빠르게 (0) | 2023.02.16 |
---|---|
[SpringBoot] lombok 사용 시 설정 (0) | 2023.02.16 |
[토비의 스프링 3.1] 6장 고립된 단위 테스트 (0) | 2022.12.06 |
[토비의 스프링 3.1] 4장 예외 정리 (0) | 2022.11.05 |
[Spring] 세 가지 핵심 프로그래밍 모델 (0) | 2022.10.12 |