트랜잭션을 사용하는 기존 로직의 문제점
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
businessLogic(fromId, toId, money);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw new IllegalStateException(e);
}
}
private void businessLogic(String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
memberRepository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(toId, toMember.getMoney() + money);
}
- 트랜잭션 시작과 비즈니스 로직을 실행하고 성공하면 커밋, 예외가 발생하여 실패하면 롤백한다.
- 다른 서비스에서 트랜잭션을 시작하려면
try, catch, finally를 포함한 성공시 커밋, 실패시 롤백 코드가 반복될 것이다.
- 이러한 문제는 템플릿 콜백 패턴이 적용된 스프링에서 제공하는
TransactionTemplate 클래스를 활용하여 해결할 수 있다.
트랜잭션 템플릿을 사용한 로직
public class MemberServiceV3_2 {
private final TransactionTemplate txTemplate;
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepository) {
this.txTemplate = new TransactionTemplate(transactionManager);
this.memberRepository = memberRepository;
}
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
txTemplate.executeWithoutResult((status) -> {
try {
businessLogic(fromId, toId, money);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
}
...
}
excuteWithoutResult() : 응답값이 없을 때 사용
excute() : 응답값이 있을 때 사용
- 트랜잭션 템플릿 덕분에 트랜잭션을 시작하고, 커밋하거나 롤백하는 코드가 제거되었다.
- 트랜잭션 템플릿의 기본 동작
- 비즈니스 로직이 정상 수행되면 커밋
- 언체크 예외가 발생하면 롤백하고 그 외의 경우 커밋
bizLogic()을 호출하면 SQLException체크 예외가 발생한다. 해당 람다에서는 체크 예외를 밖으로 던질 수 없기 때문에 언체크 에외로 바꾸여 던지도록 예외를 전환했다.
남아있는 문제점
- 서비스 로직에 비즈니스 로직 뿐만 아니라 트랜잭션을 처리하는 기술 로직이 함께 포함되어 있다.
- 비즈니스 로직과 기술 로직이 한 곳에 있으면 두 관심사를 하나의 클래스에서 처리하게 되므로 유지보수성이 떨어진다.