트랜잭션
1. 개념
하나의 거래를 안전하게 처리하도록 보장해주는 것
커밋 : 모든 작업이 성공해서 데이터베이스에 정상 반영
롤백 : 하나라도 실패해서 거래 이전으로 되돌리는 것
트랜잭션 ACID
- 원자성(Atomicity) : 트랜잭션 내에서 실행하는 작업들은 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 함
- 일관성(Consistency) : 일관성 있는 데이터베이스를 유지해야 함. (무결성 제약 조건 항상 만족)
- 격리성(Isolation) : 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리해야 함. (동시에 같은 데이터 수정하지 못하도록) 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준을 선택할 수 있음
- 지속성(Durability) : 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 함. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공항 트랜잭션 내용을 복구해야 함
트랜잭션 격리 수준 - Isolation level
트랜잭션 간에 격리성을 완전히 보장하려면 트랜잭션을 거의 순서대로 실행해야 함. 이렇게 하면 동시 처리 성능이 매우 나빠짐. -> 트랜잭션 격리 수준을 4단계로 나누어 정의
- READ UNCOMMITED (커밋되지 않은 읽기) -> 성능은 빠르지만, 커밋하지 않은 데이터를 다른 곳에서 볼 수 있음 -> 데이터 정합성 문제
- READ COMMITTED (커밋된 읽기) ----> 일반적으로 많이 사용
- REPEATABLE READ (반복 가능한 읽기)
- SERIALIZABLE (직렬화 가능)
단계 높을수록 격리성 보장, 성능 느림
2. 데이터베이스 연결 구조와 DB 세션
트랜잭션을 이해하기 위한 데이터베이스 서버 연결 구조와 DB 세션
- 사용자는 웹 애플리케이션 서버(WAS)나 DB 접근 툴 같은 클라이언트를 사용해서 데이터베이스 서버에 접근
클라이언트는 데이터베이스 서버에 연결 요청, 커넥션 맺음
이때 데이터베이스 서버는 내부에 세션을 만듦. 커넥션을 통한 모든 요청은 세션을 통해 실행 - 개발자 -> 클라이언트를 통해 SQL 전달 -> 커넥션에 연결된 세션이 SQL 실행
- 세션 - 트랜잭션 시작, 커밋/롤백을 통해 트랜잭션 종료, 새로운 트랜잭션 시작
- 사용자가 커넥션을 닫거나, DB 관리자가 세션을 강제로 종료하면 세션 종료
- 커넥션 풀이 10개의 커넥션 생성 -> 세션 10개 생성
3. 트랜잭션 DB 예제
1) 자동 커밋, 수동 커밋
(1) 자동 커밋 설정
- 기본 자동 커밋
- 쿼리를 실행할 때마다 자동으로 커밋되기 때문에 원하는 트랜잭션 기능을 제대로 사용할 수 X
-> 커밋, 롤백 호출하면서 트랜잭션 기능을 제대로 수행하려면 수동 커밋 사용
set autocommit true; //자동 커밋 모드 설정
insert into member(member_id, money) values ('data1',10000); //자동 커밋
insert into member(member_id, money) values ('data2',10000); //자동 커밋
(2) 수동 커밋 설정
- 보통 수동 커밋 모드로 설정 (set autocommit false) : 트랜잭션을 시작한다고 봄
- 수동 커밋 설정 후에는 commit, rollback 필수 !
set autocommit false; //수동 커밋 모드 설정
insert into member(member_id, money) values ('data3',10000);
insert into member(member_id, money) values ('data4',10000);
commit; //수동 커밋
2) 계좌이체 예제
(1) 계좌이체 정상 흐름
기본 데이터 생성 -> 자동 커밋
계좌이체 실행 -> 수동 커밋 설정 -> 아직 커밋 안함
커밋 후 -> 데이터베이스에 결과 반영
(2) 계좌이체 문제 상황
기본 데이터 입력
🚨 문제 상황 발생
오타로 인해 오류 발생 -> memberA의 돈은 줄었는데, memberB의 돈은 증가하지 않음 !
이때 커밋을 해버리면 ?
계좌이체 실패 + memberA의 돈만 줄어드는 심각한 문제 발생
-> 커밋 X
롤백
트랜잭션 시작 시점으로 데이터 원복
* 정리
원자성 : 트랜잭션 내의 작업들은 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 함
원자성 덕분에 여러 SQL 명령어를 하나의 작업인 것처럼 처리 가능
오토 커밋 : 오토 커밋으로 동작하면 쿼리 실행 때마다 바로 커밋되기 때문에 문제 발생 -> 수동 커밋
트랜잭션 시작 : 자동 커밋 모드에서 수동 커밋 모드로 전환하는 것을 트랜잭션의 시작으로 봄
4. DB 락
1) 개념
트랜잭션을 실행하는 동안, 동시에 같은 데이터를 수정할 수 없도록 락을 걸어둠
1. 서로 같은 데이터를 수정하고 싶을 때, 좀더 빨리 요청한 세션 1이 트랜잭션 시작
2. 해당 로우의 락 획득
3. update 쿼리 실행
4. 세션 2가 트랜잭션 실행 -> 해당 로우의 락이 없으므로 대기 (무한정 대기 X, 락 대기 시간 넘어가면 타임아웃 오류)
5. 세션 1이 커밋 수행 -> 트랜잭션 종료 -> 락 반납
6. 세션 2가 락 획득
7. 세션2가 update 쿼리 실행
8. 세션 2가 커밋 수행 -> 트랜잭션 종료 -> 락 반납
2) 데이터 변경 시 DB 락
기본 데이터
세션 1이 트랜잭션 시작 -> memberA 로우의 락 획득 -> 데이터 업데이트 -> 커밋 아직 안함
세션 1의 트랜잭션 실행 중인데 세션 2가 수정하려고 함
세션 2는 락을 아직 획득하지 못했으므로 실행 대기 ( 락 획득 시간 60초로 설정 - 60초 안에 락 얻지 못하면 락 타임아웃 오류 발생 )
세션 1이 커밋 -> 트랜잭션 종료 -> 곧바로 세션 2가 락 획득 -> 세션 2의 업데이트 쿼리 실행
3) 데이터 조회 시 DB 락
일반적으로 조회할 때는 락을 사용하지 않음
세션 1이 락을 획득하고 데이터를 변경하고 있어도, 세션 2에서 데이터를 조회할 수는 있음
세션 1이 데이터를 조회하고 있을 때, 세션 2에서 데이터를 변경해버리는 경우
-> 조회할 때도 락을 획득
기본 데이터
세션 1이 데이터 조회 + 락 획득 ( select for update )
세션 2가 데이터 변경하려고 함 -> 락 획득할 때까지 대기
세션 1 커밋 -> 락 반납 -> 세션 2가 락 획득 -> 세션 2의 변경 쿼리 실행
세션 2 커밋 -> 데이터베이스에 변경 사항 반영
5. 트랜잭션 적용
1) 트랜잭션 없이 계좌이체 로직 (정상동작할 때, 이체중 예외 발생할 때)
* MemberRepositoryV1
- 원래 리포지토리에서 각각 메소드마다 커넥션 획득해서 사용
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.support.JdbcUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.NoSuchElementException;
/**
* JDBC - DataSource 사용, JdbcUtils 사용
*/
@Slf4j
public class MemberRepositoryV1 {
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values (?, ?)";
Connection con = null; // finally에서도 호출해야 하기 때문에 밖에 따로 선언
PreparedStatement pstmt = null;
try {
con = getConnection(); // 커넥션 획득 -> 메소드로 추출
pstmt = con.prepareStatement(sql); // sql에 파라미터 바인딩
pstmt.setString(1, member.getMemberId()); // sql의 첫번째 ?에 문자 저장
pstmt.setInt(2, member.getMoney()); // sql의 두번째 ?에 숫자 저장
pstmt.executeUpdate(); // 실행 (sql을 커넥션을 통해 데이터베이스에 전달)
return member;
} catch (SQLException e) {
log.error("db error", e); // 로그를 찍기 위해 예외 잡음
throw e;
} finally {
close(con, pstmt, null);
}
}
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
} else {
throw new NoSuchElementException("member not found memberId" + memberId);
}
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, rs);
}
}
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
public void delete(String memberId) throws SQLException {
String sql = "delete member where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
private Connection getConnection() throws SQLException {
Connection con = dataSource.getConnection();
log.info("get connection={}, class{}", con, con.getClass());
return con;
}
}
* 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 memberRepositoryV1;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepositoryV1.findById(fromId);
Member toMember = memberRepositoryV1.findById(toId);
memberRepositoryV1.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepositoryV1.update(toId, toMember.getMoney() + money);
}
private static void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
- @RequiredArgsConstructor : final 붙은 변수에 대해 생성자 생성
- MemberRepository는 스프링 빈에 등록돼서 주입받는게 X, 테스트할 때 new로 생성해서 사용함
* MemberServiceV1Test - 트랜잭션 없이 기본동작, 예외발생 테스트
package hello.jdbc.service;
import hello.jdbc.connection.ConnectionConst;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV1;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.sql.SQLException;
import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* 기본 동작, 트랜잭션이 없어서 문제 발생
*/
class MemberServiceV1Test {
// 상수 설정
public static final String MEMBER_A = "memberA";
public static final String MEMBER_B = "memberB";
public static final String MEMBER_EX = "ex";
private MemberRepositoryV1 memberRepository;
private MemberServiceV1 memberService;
@BeforeEach
void before() {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, 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());
assertThat(findMemberA.getMoney()).isEqualTo(8000);
assertThat(findMemberB.getMoney()).isEqualTo(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 (예외가 던져져야 함)
assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000))
.isInstanceOf(IllegalStateException.class);
// then (A의 잔고는 줄고, EX의 잔고는 그대로)
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberB = memberRepository.findById(memberEx.getMemberId());
assertThat(findMemberA.getMoney()).isEqualTo(8000);
assertThat(findMemberB.getMoney()).isEqualTo(10000);
}
}
- @AfterEach - 테스트 데이터 제거
- 테스트 실행 후마다 데이터 제거 -> 다음 테스트에서 데이터 중복 오류 방지
(1) 정상 이체 - accountTransfer()
memberA 금액 : 10000 -> 8000
memberB 금액 : 10000 -> 12000
(2) 이체중 예외 발생 - accountTransferEx()
shift + f6 : memberB -> memberEx로 모두 rename
서비스 로직에서 member의 ID가 ex일 때 예외 발생하도록 설정했음
-> memberA의 금액은 감소했는데, 받는 member의 ID가 없어서 이체 오류
-> memberA 금액 : 10000 -> 8000
memberEx 금액 : 10000 그대로
-> 엉망진창 이렇게 되어버리면 안됨 !!
2) 트랜잭션 적용
- 트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작해야 함
( 비즈니스 로직이 잘못되면 해당 로직으로 인해 문제되는 부분을 함께 롤백해야 하기 때문 ) - 트랜잭션을 시작하려면 커넥션 필요
-> 서비스 계층에서 커넥션 획득 -> 트랜잭션 커밋 후 커넥션 종료 - 트랜잭션을 사용하는 동안에는 같은 커넥션 유지해야 함
-> 그래야 같은 세션 사용
-> 같은 커넥션을 유지하려면 ?
-> 단순한 방법으로는, 커넥션을 파라미터로 전달해서 같은 커넥션 사용되도록 유지
* MemberRepositoryV2 - 회원 등록, 조회, 수정, 삭제
/**
* JDBC - ConnectionParam
*/
@Slf4j
public class MemberRepositoryV2 {
private final DataSource dataSource;
public MemberRepositoryV2(DataSource dataSource) {
this.dataSource = dataSource;
}
public Member save(Member member) throws SQLException {
...
}
public Member findById(Connection con, String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 새로운 커넥션을 get하지 않고 넘겨받아야 함 !
// con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
} else {
throw new NoSuchElementException("member not found memberId" + memberId);
}
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
// connection을 여기서 닫으면 안됨 ! 서비스에서 종료해야 함
// JdbcUtils.closeConnection(con);
}
}
public void update(Connection con, String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
PreparedStatement pstmt = null;
try {
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
// connection을 여기서 닫지 않는다.
JdbcUtils.closeStatement(pstmt);
}
}
public void delete(String memberId) throws SQLException {
...
}
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
private Connection getConnection() throws SQLException {
Connection con = dataSource.getConnection();
log.info("get connection={}, class{}", con, con.getClass());
return con;
}
}
- 서비스 로직에서 트랜잭션 적용 필요한 부분 : 회원 조회, 수정
- findById(Connection con, String memberId)
- update(Connection con, String memberId, int money)
- 커넥션 유지가 필요한 두 메소드는 커넥션 생성 X , 파라미터로 넘어온 커넥션 사용 !
- 커넥션 유지가 필요한 두 메소드는 리포지토리에서 커넥션을 닫으면 X
이후에도 커넥션을 계속 이어서 사용하기 때문
-> 서비스 로직이 끝날 때 트랜잭션 종료해야 함
* MemberServiceV2 - 계좌 이체 로직
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV2;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 트랜잭션 - 파라미터 연동, 풀을 고려한 종료
*/
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV2 {
private final DataSource dataSource;
private final MemberRepositoryV2 memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
// 서비스 로직에서 커넥션 획득
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false); // 트랜잭션 시작
// 비즈니스 로직
bizLogic(con, fromId, toId, money);
con.commit(); // 성공시 커밋
} catch (Exception e) {
con.rollback(); // 실패시 롤백
throw new IllegalStateException(e);
} finally {
release(con);
}
}
private void bizLogic(Connection con, String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(con, fromId);
Member toMember = memberRepository.findById(con, toId);
memberRepository.update(con, fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(con, toId, toMember.getMoney() + money);
}
private static void release(Connection con) {
if (con != null) {
try {
con.setAutoCommit(true); // 커넥션 풀 고려
con.close();
} catch (Exception e) {
log.info("error", e);
}
}
}
private static void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
- Connection con = dataSource.getConnection();
- 트랜잭션 시작하려면 커넥션 필요
- 트랜잭션 시작하려면 커넥션 필요
- con.setAutoCommit(false);
- 수동 커밋 모드 -> 트랜잭션 시작
- 세션에 set autocommit false 전달
- bizLogic(con, fromId, toId, money);
- 트랜잭션이 시작된 커넥션을 전달하면서 비즈니스 로직 수행
- 메소드를 따로 추출한 이유 - 트랜잭션 관리 로직과 실제 비즈니스 로직 구분하기 위함
- bizLoginc 내의 각 메소드를 호출할 때도 커넥션을 전달함 ( update(con, ...) )
- con.commit();
- 비즈니스 로직이 정상 수행되면 트랜잭션 커밋
- con.rollback();
- 비즈니스 로직 수행 중 예외가 발생하면 catch -> 트랜잭션 롤백
- 비즈니스 로직 수행 중 예외가 발생하면 catch -> 트랜잭션 롤백
- release(con);
- 커넥션을 모두 사용하고 나면 안전하게 종료 (커넥션 풀 사용시에는 풀에 커넥션 반납)
- 풀에 돌려주기 전에 수동 커밋 모드를 기본 값인 자동 커밋 모드로 변경해줌
* MemberServiceV2Test - 트랜잭션 적용 테스트
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV1;
import hello.jdbc.repository.MemberRepositoryV2;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.sql.SQLException;
import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* 트랜잭션 - 커넥션 파라미터 전달 방식 동기화
*/
class MemberServiceV2Test {
private MemberRepositoryV2 memberRepository;
private MemberServiceV2 memberService;
@BeforeEach
void before() {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
memberRepository = new MemberRepositoryV2(dataSource);
memberService = new MemberServiceV2(dataSource, memberRepository);
}
@AfterEach
void after() throws SQLException {
memberRepository.delete("memberA");
memberRepository.delete("memberB");
memberRepository.delete("ex");
}
@Test
@DisplayName("정상 이체")
void accountTransfer() throws SQLException {
// given 이렇게 주어졌을 때
Member memberA = new Member("memberA", 10000);
Member memberB = new Member("memberB", 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());
assertThat(findMemberA.getMoney()).isEqualTo(8000);
assertThat(findMemberB.getMoney()).isEqualTo(12000);
}
@Test
@DisplayName("이체중 예외 발생")
void accountTransferEx() throws SQLException {
// given 이렇게 주어졌을 때
Member memberA = new Member("memberA", 10000);
Member memberEx = new Member("ex", 10000);
memberRepository.save(memberA);
memberRepository.save(memberEx);
// when (예외가 던져져야 함)
assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000))
.isInstanceOf(IllegalStateException.class);
// then (A의 잔고는 줄고, EX의 잔고는 그대로)
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberB = memberRepository.findById(memberEx.getMemberId());
assertThat(findMemberA.getMoney()).isEqualTo(10000);
assertThat(findMemberB.getMoney()).isEqualTo(10000);
}
}
(1) 정상이체
기존 로직과 같음
(2) 이체중 예외 발생
- memberService.accountTransfer() 실행
- 커넥션 생성 후 트랜잭션 시작
- memberA의 금액 2000원 감소
- memberEx의 회원 ID는 ex이므로 예외 발생
- 예외 잡아서 트랜잭션 롤백
- 계좌이체 실패 -> 롤백 -> memberA의 돈이 기존 10000원으로 복구
- 정상적으로 초기화
-> 근데 비즈니스 로직 코드 < 트랜잭션 코드 -> 복잡하고 지저분한 문제
'Spring' 카테고리의 다른 글
[인프런/스프링 DB 1편] 5. 자바 예외 (0) | 2023.06.24 |
---|---|
[인프런/스프링 DB 1편] 4. 스프링과 문제 해결 - 트랜잭션 (0) | 2023.06.21 |
[인프런/스프링 DB 1편] 2. 커넥션풀과 데이터소스 (0) | 2023.06.19 |
[인프런/스프링 DB 1편] 1. JDBC (0) | 2023.06.17 |
[Spring] 파일저장 transferTo, InputStream OutputStream (0) | 2023.06.06 |