Data Base/스프링 DB
스프링 DB - 트랜잭션 적용1
Surge100
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
스프링 DB 1편 - 데이터 접근 핵심 원리 - 인프런 | 강의
백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔
www.inflearn.com