-
스프링 DB - 트랜잭션 적용1Data Base/스프링 DB 2023. 11. 24. 17:23
트랜잭션 적용1
실제 애플리케이션에서 DB 트랜잭션을 사용해서 계좌이체 같이 원자성이 중요한 비지니스 로직을 어떻게 구현하는지 알아보자 먼저 트랜잭션 없이 단순하게 계좌이체 비지니스 로직만 구현해보자.
MemberServiceV1
package hello.jdbc.service; import hello.jdbc.domain.Member; import hello.jdbc.repository.MemberRepositoryV1; import lombok.RequiredArgsConstructor; import java.sql.SQLException; @RequiredArgsConstructor public class MemberServiceV1 { private final MemberRepositoryV1 memberRepository; public void accountTransfer(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("이체중 예외 발생"); } } }
- formId의 회원을 조회해서 toId의 회원에게 money만큼의 돈을 계좌이체 하는 로직이다.
- fromId 회원의 돈을 money 만큼 감소시킨다. → UPDATE SQL 실행
- toId 회원의 돈을 money 만큼 증가한다. → UPDATE SQL 실행
- 예외 상황을 테스트해보기 위해 toId가 "ex"인 경우 예외를 발생한다.
MemberServiceV1Test
package hello.jdbc.service; import com.zaxxer.hikari.HikariDataSource; import hello.jdbc.connection.ConnectionConst; import hello.jdbc.domain.Member; import hello.jdbc.repository.MemberRepositoryV1; import org.junit.jupiter.api.*; import java.sql.SQLException; import static hello.jdbc.connection.ConnectionConst.*; import static org.junit.jupiter.api.Assertions.*; /** * 기본 동작, 트랜잭션이 없어서 문제가 발생 */ class MemberServiceV1Test { public static final String MEMBER_A ="member_A"; public static final String MEMBER_B ="member_B"; public static final String MEMBER_EX ="ex"; private MemberRepositoryV1 memberRepository; private MemberServiceV1 memberService; @BeforeEach void before(){ HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl(URL); dataSource.setUsername(USERNAME); dataSource.setPassword(PASSWORD); memberRepository = new MemberRepositoryV1(dataSource); memberService = new MemberServiceV1(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(),8000); Assertions.assertEquals(findMemberB.getMoney(),10000); } }
정상이체 - accountTransfer();
- given : 다음 데이처를 저장해서 테스트를 준비한다.
- memberA 10000원
- memberB 10000원
- when : 계좌이체 로직을 실행한다.
- memberService.accountTransfer( )를 실행한다.
- memberA → memberB로 2000원 계좌에이체 한다.
- memberA의 금액이 2000원 감소한다.
- memberB의 금액이 2000원 증가한다.
- then : 계좌이체가 정상 수행되었는지 검증한다.
- memberA 8000원 - 2000원 감소
- memberB 12000원 - 2000원 감소
정상 이체 로직이 정상 수행 되는 것을 확인할 수 있다.
테스트 데이터 제거
테스트가 끝나면 다음 테스트에 영향을 주지 않기 위해 @AfterEach에서 테스트에 사용한 데이터를 모두 삭제한다.
- @BeforeEach : 각각의 테스트가 수행되기 전에 실행된다.
- @AfterEach : 각각의 테스트가 실행되고 난 이후에 실행된다.
@AfterEach void after() throws SQLException { memberRepository.delete(MEMBER_A); memberRepository.delete(MEMBER_B); memberRepository.delete(MEMBER_EX); }
- 테스트 데이터를 제거하는 과정이 불편하지만, 다음 테스트에 영향을 주지 않으려면 테스트에서 사요한 데이터를 모두 제거해야 한다. 그렇지 않으면 이번 테스트에서 사용한 데이터 때문에 다음 테스트에서 데이터 중복으로 오류가 발생할 수 있다.
- 테스트에서 사용한 데이터를 게거하는 더 나은 방법으로는 트랜잭션을 활용하면 된다. 테스트 전에 트랜잭션을 시작하고, 테스트 이후에 트랜잭션을 롤백해버리면 데이터가 처음 상태로 돌아돈다. 이 방법은 이후에 설명한다.
이체중 예외 발생 - accountTransferEx()
- given : 다음 데이터를 지정해서 테스트를 준비한다.
- memberA 1000원
- memberEx 1000원
- when : 계좌이체 로직을 실행한다.
- memberService.accountTransfer()를 실행한다.
- memberA → memberEx로 2000d원 계좌이체 한다.
- memberA의 금액이 2000원 감소한다.
- memberEx회원의 ID는 ex이므로 중간에예외가 발생한다. → 이부분이 중요하다.
- then : 계좌이체는 실패한다. memberA의 돈만 2000원 줄어든다.
- memberA 8000원 → 2000원 감소
- memberB 10000원 - 중간에 실패로 로직이 수행되지 않았다 따라서 그대로 10000원으로 남아있게 된다.
정리
이체중 예외가 발생하게 되면 memberA의 금액은 10000원 → 8000원으로 2000원 감소한다. 그런데 memberB의 돈은 그대로 10000원 인 남아있다. 결과적으로 memberA의 돈만 2000원 감소한 것이다.
[출저 - 스프링 DB 1편 - 데이터 접긎 핵심 원리, 김영한]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1
'Data Base > 스프링 DB' 카테고리의 다른 글
스프링 DB - 스프링과 문제 해결(트랜잭션) (1) 2023.11.27 스프링 DB - 트랜잭션 적용2 (1) 2023.11.27 스프링 DB - DB락 조회 (1) 2023.11.24 스프링 DB - DB락 개념 이해&실습 (0) 2023.11.24 스프링 DB - 트랜잭션 DB 예제(트랜잭션 실습) (0) 2023.11.24 - formId의 회원을 조회해서 toId의 회원에게 money만큼의 돈을 계좌이체 하는 로직이다.