트랜잭션 - 개념 이해
- 데이터를 DB에 저장하는 이유?
- DB는 트랜잭션을 지원
- 하나의 거래를 안전하게 처리하도록 보장해주는 것
- e. g 5000원 계좌 이해
- A의 잔고를 5000원 감소
- 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은
membe_id=memberA
의money
를 500으로 변경 시도- 해당 로우의 락을 먼저 획득해야 함
- 락이 비어있으므로 세션 1은 락을 획득
- 세션 1은 락을 획득했으므로 해당 로우에 update sql을 수행
update 500
- 세션 2는 트랜잭션을 시작
- 세션 2도 memberA의 money 데이터를 변경하려고 시도
- 해당 로우의 락을 먼저 획득해야함..
- 락이 없으므로 락이 돌아올 떄까지 대기
- 락 대기 시간을 넘어가면 락 타임아웃 오류가 발생
- 설정 가능
- 세션 1은 커밋을 수행. 트랜잭션 종료. 락 반납
- 락을 획득하기 위해 대기하던 세션 2가 락을 획득
- 세션 2는 update sql 수행
- 세션 2는 커밋을 수행하고 트랜잭션이 종료되었으므로 락을 반납
락 - 조회
- 일반적인 조회는 락을 사용하지 않음
- 보통 데이터를 조회할 때는 락을 획득하지 않고 바로 데이터를 조회할 수 있다...
- 세션1이 락을 획득하고 변경하고 있어도
- 세션 2에서는 데이터를 조회하는 할 수 있음
- 조회가 아니라 데이터를 변경하려면 락이 필요하기 때문에 락이 돌아올 때 까지 대기해야 함
- 보통 데이터를 조회할 때는 락을 획득하지 않고 바로 데이터를 조회할 수 있다...
- 조회와 락
- 데이터를 조회할 때도 락을 획득하고 싶을 때
select for update
- 세션 1이 조회 시점에 락을 가져가버리기 때문에 해당 데이터를 변경할 수 없음
- 이 경우도 트래잭션을 커밋하면 락을 반납
- 데이터를 조회할 때도 락을 획득하고 싶을 때
- 조회 시점에 락이 필요한 경우는?
- 트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때 사용
- 예를 들어
- 애플리케이션 로직에서 memberA의 금액을 조회한 다음에
- 이 금액 정보로 애플리케이션에서 어떤 계산을 수행
- 계산이 완료될 때까지 memberA의 금액을 다른곳에서 변경하면 안됨
- 세션 1트랜잭션 시작
- select for update
- 세션 2 트랜잭션 시작
- 세션 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 |
댓글