Spring/Spring Data

트랜잭션

wampy 2023. 3. 6.

트랜잭션 - 개념 이해

  • 데이터를 DB에 저장하는 이유?
    • DB는 트랜잭션을 지원
    • 하나의 거래를 안전하게 처리하도록 보장해주는 것
  • e. g 5000원 계좌 이해
    1. A의 잔고를 5000원 감소
    2. B의 잔고를 5000원 증가

2가지 가업이 합쳐져서 하나의 작업처럼 동작해야 함

1번은 성공했는데 2번은 실패한다? 심각...

  • 커밋(commit) : 모든 작업이 성공해서 데이터베이스에 정상 반영하는 것
  • 롤백(rollback) : 작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것

ACID

  • 원자성
    • 트랜잭션 내에서 실행한 작업들은 모두 성공 하거나 모두 실패해야 함
  • 일관성
    • 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 함
  • 격리성
    • 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리
    • 트랜잭션 간에 격리성을 완벽하게 보장하려면 트랜잭션을 거의 순서대로 실행해야 함
      • 동시 처리 성능이 매우 나빠짐.. 병렬처리 x
      • 트랜잭션 격리 수준
        • READ UNCOMMITED (커밋되지 않은 읽기)
        • READ COMMITED (커밋된 읽기) -> 일반적
        • REPEATABLE READ (반복 가능한 읽기)
        • SERIAILZABLE (직렬화 가능)
  • 지속성
    • 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 함

데이터베이스 연결 구조와 DB 세션

사용자 -> 클라이언트 (WAS)-> 커넥션 -> 데이터베이스 서버 -> 세션 -> 트랜잭션 시작 SQL 실행..

  • 사용자는 WAS나 DB 접근 툴 같은 클라이언트를 사용해 DB 서버에 접근
    • 클라이언트는 데이터 서버에 연결을 요청하고 커넥션을 맺음
    • DB 서버는 내부에 세션을 만든다
    • 앞으로 해당 커넥션을 통한 모든 요청은 세션을 통해 실행하게 됨
  • 개발자 -> SQL 전달 -> 커넥션에 연결된 세션이 SQL 실행
  • 세션
    • 트랜잭션 시작
    • 트랙재션 종료 - 커밋, 롤백
    • 새로운 트랜잭션 다시 시작
  • 커넥션 닫거나 세션 강제 종료하면 세션 종료됨

트랜잭션 사용법

  • 데이터 변경 쿼리 실행
    • 결과 반영 commit
    • 반영 x rollback
  • 커밋을 호출하기 전까지는 -> 임시로 데이터를 저장하는 것
  • 해당 트랜잭션을 시작한 세션에게만 변경 데이터가 보이고 다른 세션에게는 변경 데이터가 보이지 않음

자동 커밋, 수동 커밋

  • 자동 커밋
    • 각각의 쿼리 실행 직후에 자동으로 커밋을 호출
    • 커밋이나 롤백을 직접 호출하지 않아도 됨
    • 트랜잭션 기능을 사용할 수 없음
  • 수동 커밋
    • 수동 커밋 설정을 하면 이후에 꼭 commit rollback을 호출해야 함
    • set autocommit false;

DB 락 - 개념 이해

  • 세션 1이 트랜잭션을 시작하고 데이터를 수정하는 동안
    • 아직 커밋 수행 안했는디
    • 세션 2에서 동시에 같은 데이터를 수정하게 되면?
    • 트랜잭션의 원자성이 깨지게 된다...........
      • 세션 1이 중간에 롤백하면.. 세션2는 잘못된 데이터를 수정하는 문제가 발생
  • 이런 문제를 방지하려면 세션이 트랜잭션을 시작하고 데이터를 수정하는 동안에는 커밋이나 롤백 전까지 다른 세션에서 해당 데이터를 수정할 수 없게 막아야 함

  • 동시에 데이터를 수정하는 문제를 해결
  • e.g
    1. 세션 1은 트랜잭션을 시작
    2. 세션 1은 membe_id=memberAmoney를 500으로 변경 시도
      • 해당 로우의 락을 먼저 획득해야 함
      • 락이 비어있으므로 세션 1은 락을 획득
    3. 세션 1은 락을 획득했으므로 해당 로우에 update sql을 수행
    4. update 500
    5. 세션 2는 트랜잭션을 시작
    6. 세션 2도 memberA의 money 데이터를 변경하려고 시도
      • 해당 로우의 락을 먼저 획득해야함..
      • 락이 없으므로 락이 돌아올 떄까지 대기
      • 락 대기 시간을 넘어가면 락 타임아웃 오류가 발생
        • 설정 가능
    7. 세션 1은 커밋을 수행. 트랜잭션 종료. 락 반납
    8. 락을 획득하기 위해 대기하던 세션 2가 락을 획득
    9. 세션 2는 update sql 수행
    10. 세션 2는 커밋을 수행하고 트랜잭션이 종료되었으므로 락을 반납

락 - 조회

  • 일반적인 조회는 락을 사용하지 않음
    • 보통 데이터를 조회할 때는 락을 획득하지 않고 바로 데이터를 조회할 수 있다...
      • 세션1이 락을 획득하고 변경하고 있어도
      • 세션 2에서는 데이터를 조회하는 할 수 있음
        • 조회가 아니라 데이터를 변경하려면 락이 필요하기 때문에 락이 돌아올 때 까지 대기해야 함
  • 조회와 락
    • 데이터를 조회할 때도 락을 획득하고 싶을 때
      • select for update
    • 세션 1이 조회 시점에 락을 가져가버리기 때문에 해당 데이터를 변경할 수 없음
    • 이 경우도 트래잭션을 커밋하면 락을 반납
  • 조회 시점에 락이 필요한 경우는?
    • 트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때 사용
    • 예를 들어
      • 애플리케이션 로직에서 memberA의 금액을 조회한 다음에
      • 이 금액 정보로 애플리케이션에서 어떤 계산을 수행
      • 계산이 완료될 때까지 memberA의 금액을 다른곳에서 변경하면 안됨
        1. 세션 1트랜잭션 시작
        2. select for update
        3. 세션 2 트랜잭션 시작
        4. 세션 2 lock 획득 시도 - 대기

트랜잭션 적용해보기

  • 트랜잭션 없이 단순하게 계좌이체 비즈니스 로직 구현
    • fromId의 회원을 조회해서 toId의 회원에게 money만큼의 돈을 게좌이체
    • fromId 회원의 돈을 money 만큼 감소 -> UPDATE SQL 발생
    • toId 회원의 돈을 money만큼 증가 -> UDPATE SQL 실행
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository 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, formMember.getMoney() - money);
        if (toMember.getMemberId().equals("ex")) {
            throw new IllegalStateException("이체 중 예외 발생");
        }
        memberRepository.update(toId, toMember.getMoney() + money);


    }
}
public static final String MEMBER_A = "memberA";
public static final String MEMBER_B = "memberB";
public static final String MEMBER_EX = "ex";

private MemberRepository memberRepository;
private MemberSerivce memberService;

@BeforEach
void before() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    memberRepository = new MemberRepository(dataSource);
    memberSerivce = new MemberSerivce(memberRepository);
}

@AfterEach
void after() {
    memberRepository.delete(MEMBER_A);
    memberRepository.delete(MEMBER_B);
    memberRepository.delete(MEMBER_EX);
}


@Test
@DisplayName("정상 이체")
void accountTransfer() {
    // given
    Member memberA = new Member(MEMBER_A, 10000);
    Member memberB = new Member(MEMBER_B, 10000);
    memberRepository.save(memberA);
    memberRepository.save(memberB);

    // when
    // A -> B 계좌이체
    memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);

    // then
    Member findMemberA = memberRepository.findById(memberA.getMemberId());
    Member findMemberB = memberRepository.findById(memberB.getMemberId());
    assertThat(findMemberA.getMoney()).isEquals(A);
    assertThat(findMemberB.getMoney()).isEquals(B);
}

@Test
@DisplayName("이체 중 예외 발생")
void accountTransfer() {
    // given
    Member memberA = new Member(MEMBER_A, 10000);
    Member memberB = new Member(MEMBER_EX, 10000);
    memberRepository.save(memberA);
    memberRepository.save(memberB);

    // when
    // A -> B 계좌이체
    // 예외 
    assertThatThrownBy (()->memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000)).isInstanceOf(IllgegalStateException.class);
    // 멤버 A의 돈만 까지고
    // B는 변동 없음;;
}
  • 트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작해야 함
  • 비즈니스 로직이 잘못되면 해당 비즈니스 로직으로 인해 문제가 되는 부분을 함께 롤백해야 하기 때문
  • 트랜잭션을 시작하려면 커넥션이 필요
    • 서비스 계층에서 커넥션을 만들고
    • 트랜잭션 커밋 이후에 커넥션을 종료해야 함
  • 애플리케이션에서 DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야 함 그래야 같은 세션을 사용할 수 있음 (다른 커넥션 -> 다른 세션)
    • 같은 커넥션을 유지하려면?
      • 커넥션을 파라미터로 전달해서 같은 커넥션이 사용되도록 유지하는 것
public class MemberRepository {
    private final DataSource dataSource;
    public MemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        Connection con = dataSource.getConnection();
        try {
            con.setAutoCommit(false); // 트랜잭션 시작
            // 비즈니스 로직    
            memberRepository.update(con);
            memberRepository.update(con);
            con.commit(); // 성공시 커밋
        } catch (Exception e) {
            con.rollback(); // 실패시 롤백
            throw new IllegalStateException(e);
        } finally {
            if (con != null) {
                try {
                    // 커넥션 풀로 돌아간다.. 커넥션이 종료되는 것이 아님...
                    // setAutoCommit ->false로 돌아감
                    // 대부분 자동커밋모드기때문에..
                      con.setAutoCommit(true); // 커넥션 풀을 고려
                    con.close();  
                } catch (Exception e) {
                    log.info("error", e);
                }
            }
        }
    }
}
  • 애플리케이션에서 DB 트랜잭션을 적용하려면 서비스 계층이 매우 지저분해짐..
  • 커넥션을 유지하도록...하는것도 문제
  • 스프링에서 깔끔하게 ^_^해줌

'Spring > Spring Data' 카테고리의 다른 글

spring transaction  (1) 2023.03.06
커넥션 풀  (0) 2023.03.06

댓글