-
스프링 DB - 트랜잭션 문제 해결(트랜잭션 템플릿)Data Base/스프링 DB 2023. 11. 28. 13:50
트랜잭션 문제 해결(트랜잭션 템플릿)
트랜잭션을 사용하는 로직을 살펴보녕 다음과 같은 패턴이 반복되는 것을 확인할 수 있다.
트랜잭션 사용 코드
TransactionStatus status = transactionMnager.getTransaction(new DefaultTransactionDefinition()); try{ //비지니스 로직 bizLogic(fromId,toId,money); //성공시 커밋 transactionManager.commit(status); }catch(Exception e){ //실패시 롤백 transactionManager.rollback(status); throw new IlleagalStateException(e); }
- 트랜잭션을 시작하고, 비지니스 로직을 실행하고, 성고하면 커밋하고, 예외가 발생해서 실패하면 롤백한다.
- 다른 서비스에서 트랜잭션을 시작하려면 `try`,`catch`,`finally`를 포함한 성공시 커밋, 실패시 롤백 코드가 반복될 것이다.
- 이런 형태는 각각의 서비스에서 반복된다. 달라지는 부분은 비지니스 로직 뿐이다.
- 이럴 때 템플릿 콜백 패턴을 활용하면 이런 반복 문제를 깔끔하게 해결할 수 있다.
트랜잭션 템플릿
템플릿 콜백 패턴을 적용하려면 템플릿을 제공하는 클래스를 작성해야 하는데, 스프링은 `TransactionTemplate`라는 템플릿 클래스를 제공한다.
TransactionTemplate
public class TransactionTemplate{ private PlatformTransactionMnager transactionManager; public <T> T execute(TransactionCallback<T> action){...} void executeWithourResult(Consumer<TransactionStatus>action){...} }
- execute(): 응답 값이 있을때 사용한다.
- executeWithoutResult(): 응답 값이 없을 때 사용한다.
MemberServiceV3_2
package hello.jdbc.service; import hello.jdbc.domain.Member; import hello.jdbc.repository.MemberRepositoryV3; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; import org.springframework.transaction.support.TransactionTemplate; import java.sql.SQLException; /** * 트랜잭션 - 트랜잭션 템플릿 */ @Slf4j public class MemberServiceV3_2 { // private final PlatformTransactionManager transactionManager; 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 { bizLogic( fromId, toId, money); } catch (SQLException e) { //체트 error를 런타임 error로 바꿔서 더진다. throw new RuntimeException(e); } }); } private void bizLogic(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); } private void validation(Member toMember) { if(toMember.getMemberId().equals("ex")){ throw new IllegalStateException("이체중 예외 발생"); } } }
- TransactionTemplate을 사용하려면 `transactionManager`가 필요하다. 생성자에서 `transactionManager`를 주입 받으면서 `TransactionTemplate`을 생성했다.
트랜잭션 템플릿 (템플릿 콜백 패턴) 사용 로직
public void accountTransfer(String fromId, String toId, int money) throws SQLException { txTemplate.executeWithoutResult((status)->{ //비지니스 로직 try { bizLogic( fromId, toId, money); } catch (SQLException e) { //체트 error를 런타임 error로 바꿔서 더진다. throw new RuntimeException(e); } }); }
- 트랜잭션 템플릿 덕부에 트랜잭션을 시작하고, 커밋하거나 롤백하는 코드가 모두 제거되었다.
- 트랜잭션 템플릿의 기본 동작은 다음과 같다.
- 비지니스 로직이 정상 수행되면 커밋한다.
- 언체크 예외가 발생하면 롤백한다. 그 외의 경우 커밋한다.(언체크 예외나 런타임 오류만 롤백하고,체크 예외의 경우에는 커밋한다.)
- 코드에서 예외 처리를 하기 위해 `try~catch`가 들어갔는데, `bizLogic()`메서드를 호출하면 `SQLException`체크 예외를 넘겨준다. 위 코드의 람다에서 체크 예외를 밖으로 던질 수 없기 때문에 언체크(런타) 예외로 바꾸어 던지도록 예외를 전환했다.
MemberServiceV3_2
package hello.jdbc.repository; import com.zaxxer.hikari.HikariDataSource; import hello.jdbc.domain.Member; import hello.jdbc.service.MemberServiceV3_1; import hello.jdbc.service.MemberServiceV3_2; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.*; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import java.sql.SQLException; import static hello.jdbc.connection.ConnectionConst.*; /** * 트랜잭션 - 트랜잭션 템플릿 */ @Slf4j class MemberRepositoryV3_2Test { public static final String MEMBER_A ="member_A"; public static final String MEMBER_B ="member_B"; public static final String MEMBER_EX ="ex"; private MemberRepositoryV3 memberRepository; private MemberServiceV3_2 memberService; @BeforeEach void before(){ HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl(URL); dataSource.setUsername(USERNAME); dataSource.setPassword(PASSWORD); memberRepository = new MemberRepositoryV3(dataSource); PlatformTransactionManager transactionManager= new DataSourceTransactionManager(dataSource); memberService = new MemberServiceV3_2(transactionManager,memberRepository); } @AfterEach void after() throws SQLException { memberRepository.delete(MEMBER_A); memberRepository.delete(MEMBER_B); memberRepository.delete(MEMBER_EX); } @Test @DisplayName("정상 이체") void accountTransfer() throws SQLException { //given Member memberA = new Member(MEMBER_A, 10000); Member memberB = new Member(MEMBER_B, 10000); memberRepository.save(memberA); memberRepository.save(memberB); //when memberService.accountTransfer(memberA.getMemberId(),memberB.getMemberId(),2000); //then Member findMemberA = memberRepository.findById(memberA.getMemberId()); Member findMemberB = memberRepository.findById(memberB.getMemberId()); Assertions.assertEquals(findMemberA.getMoney(),8000); Assertions.assertEquals(findMemberB.getMoney(),12000); } @Test @DisplayName("이체중 예외 발생") void accountTransferEx() throws SQLException { //given Member memberA = new Member(MEMBER_A, 10000); Member memberEx = new Member(MEMBER_EX, 10000); memberRepository.save(memberA); memberRepository.save(memberEx); //when Assertions.assertThrows(IllegalStateException.class, () -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(),2000)); //then Member findMemberA = memberRepository.findById(memberA.getMemberId()); Member findMemberB = memberRepository.findById(memberEx.getMemberId()); Assertions.assertEquals(findMemberA.getMoney(),10000); Assertions.assertEquals(findMemberB.getMoney(),10000); } }
- 테스트 내용은 기존과 같다.
- 테스트를 실행해보면 정상 동작하는 로직과, 실패시 로직도 잘 수행되는 것을 확인 할 수 있다.
정리
- 트랜잭션 템플릿 덕분에, 트랜잭션을 사용할 때 반복하는 코드를 제거 할 수 있다.
- 하지만 서비스 레이어에 비지니스 로직 뿐만 아니라 트랜잭션을 처리하는 기술 로직이 함께 포함되어 있다.
- 애플리케이션을 구성하는 로직을 핵심 기능과 부가 기능으로 구분하자면 서비스 입장에서 비지니스 로직은 핵심 기능이고, 트랜잭션은 부가 기능이다.
- 이렇게 비지니스 로직과 트랜잭션을 처리하는 기술 로직이 한곳에 있으면 두 관심사를 하나의 클래스에서 처리하게 된다. 결과적으로 코드를 유지보수하기 어려워진다.
- 서비스 로직은 가급적 핵심 비지니스 로직만 있어야 한다. 하지만 트랜잭션 기술을 사용하려면 어쩔 수 없이 트랜잭션 코드도 존재해야 한다. 어떻게 하면 이 문제를 해결할 수 있을까?
[출저 - 스프링 DB 1편 - 데이터 접근 핵심 원리, 김영한]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1
'Data Base > 스프링 DB' 카테고리의 다른 글
스프링 DB - 트랜잭션 문제 해결(트랜잭션 매니저2) (0) 2023.11.28 스프링 DB - 트랜잭션 문제 해결(트랜잭션 매니저1) (1) 2023.11.28 스프링 DB - 트랜잭션 동기화 (1) 2023.11.27 스프링 DB - 트랜잭션 추상화 (0) 2023.11.27 스프링 DB - 스프링과 문제 해결(트랜잭션) (1) 2023.11.27