Spring/DB
[Spring/DB] 03. 트랜잭션 이해
lumana
2025. 3. 25. 00:04
3. 트랜잭션 이해
#Spring/DB
정리
트랜잭션 - 개념 이해
- 파일이 아닌 데이터베이스에 데이터를 저장하는 이유 트랜잭션
- 하나의 거래를 안전하게 처리하도록 보장
- 커밋(
Commit
): 모든 작업이 성공해서 데이터베이스에 정상 반영하는 것 - 롤백(
Rollback
): 작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것
트랜잭션 ACID
- 트랜잭션은 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 함
- 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.
- 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
- 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation level)을 선택할 수 있다.
- 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
- 트랜잭션은 원자성, 일관성, 지속성을 보장
- 트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해함 성능이 나빠짐 격리 수준을 나눔
트랜잭션 격리 수준 - Isolation level
- READ UNCOMMITED (커밋되지 않은 읽기)
- READ COMMITTED (커밋된 읽기)
- REPEATABLE READ (반복 가능한 읽기)
- SERIALIZABLE (직렬화 가능)
- 단계가 높아질 수록 속도가 느려짐. 일반적으로 2, 3번 사용.
- 이 강의는 READ COMMITTED 기준으로 설명
데이터베이스 연결 구조와 DB 세션
- WAS 등을 통해 DB와 연결을 맺으면 DB에 세션이 만들어지고, 커넥션의 요청은 세션을 통해 실행됨
- 세션이 SQL을 실행한다.
- 세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료
- 사용자가 커넥션을 닫거나, 또는 DBA(DB 관리자)가 세션을 강제로 종료하면 세션은 종료됨
- 커넥션 풀이 10개의 커넥션을 생성하면, 세션도 10개 만들어진다.
트랜잭션 사용법 - 데이터 추가 트랜잭션
- 커밋을 호출하기 전까지는 임시로 데이터를 저장하는 것이다. 따라서 해당 트랜잭션을 시작한 세션(사용자)에게만 변경 데이터가 보이고 다른 세션(사용자)에게는 변경 데이터가 보이지 않는다.
- 만약 커밋하지 않은 데이터를 다른 곳에서 조회할 수 있다면 어떤 문제가 발생? (READ UNCOMITTED)
- 조회 후 로직 수행하고 있는데 다른 세션에서 롤백해버리면 데이터 정합성에 큰 문제가 발생
트랜잭션 - DB 예제2 - 자동 커밋, 수동 커밋
- 자동 커밋: 각각의 쿼리 실행 직후에 자동으로 커밋을 호출. 트랜잭션이 가져다주는 기능 사용할 수 없음.
- 커밋, 롤백 이런 거 쓰려면 수동 커밋 사용해야 함.
- 자동 커밋 설정:
set autocommit true
- 수동 커밋 설정:
set autocommit false
- 보통 자동 커밋 모드가 기본으로 설정된 경우가 많기 때문에, 수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현할 수 있다.
- 한 번 설정하면 세션 중에 유지되고, 중간에 변경하는 것도 가능함.
트랜잭션 - DB 예제3 - 트랜잭션 실습: 단순 데이터 추가 예제
- 커밋을 하든 롤백을 하든 문제 발생 X
트랜잭션 - DB 예제4 - 계좌이체
- 계좌이체 정상 상황
- 세션 1에서 memberA 2000원 빼고, memberB 2000원 더한 뒤 커밋하면 성공
- 계좌이체 문제 상황 - 커밋
- SQL에 문제가 있어
memberA
의 돈을 2000원 줄이는 것에는 성공했지만,memberB
의 돈을 2000원 증가시키는 것에 실패한다. - 이 때 강제로 커밋하면 memberA의 돈만 줄어드는 문제가 발생한다.
- SQL에 문제가 있어
- 계좌이체 문제 상황 - 롤백
- 중간에 문제가 발생했을 때 롤백을 호출해서 트랜잭션 시작 시점으로 데이터를 원복해야한다.
- 정리
- 원자성: 작업은 모두 성공하거나 실패해야 한다. 트랜잭션 덕분에 가능함을 확인했다.
- 오토 커밋: 오토 커밋 모드에서 계좌이체 중간에 실패하면 memberA의 돈만 줄어드는 문제가 발생한다.
- 수동 커밋을 통해 커밋, 롤백할 수 있도록 하자.
DB 락 - 개념 이해
- 세션 1이 데이터 수정하는 동안 세션2가 데이터 동시에 수정하면 여러 문제가 발생한다. 대표적으로 원자성이 깨진다.
- 세션 1이 심지어 롤백까지 해버리면 세션 2는 잘못된 데이터를 수정하는 작업을 하게 된다.
- 세션이 트랜잭션을 시작하고 데이터를 수정하는 동안에는 커밋이나 롤백 전까지 다른 세션에서 해당 데이터를 수정할 수 없게 막자.
DB 락 - 변경
- 수동 커밋으로 설정
- SET LOCK_TIMEOUT 60000 락 획득 시간 설정.
- 시간 지나면 락 타임아웃 오류가 발생
- 세션 1이 락 획득
- 세션 2는 락 획득 대기
- 세션 1이 커밋 후 락 반납
- 세션 2는 락 획득
- 세션 2가 커밋 후 락 반납
DB 락 - 조회
일반적인 조회는 락을 사용하지 않는다
데이터베이스마다 다르지만, 보통 데이터를 조회할 때는 락을 획득하지 않고 바로 데이터를 조회할 수 있다.
조회와 락
- 데이터를 조회할 때도 락을 획득하고 싶을 때는
select for update
구문을 사용.- 조회 시점에 락을 가져가버리기 때문에 다른 세션에서 해당 데이터를 변경
조회 시점에 락이 필요한 경우는 언제일까?
- 트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때 사용
- 정산 로직과 같이 조회한 정보를 바탕으로 로직을 수행하는 경우
select for update
구문을 사용하면 조회를 하면서 동시에 선택한 로우의 락도 획득한다.
트랜잭션 - 적용1
@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("이체중 예외 발생");
}
}
}
fromId
의 회원을 조회해서toId
의 회원에게money
만큼의 돈을 계좌이체하는 로직- 트랜잭션 없이 로직을 작성한 예제
memberA
memberEx
로 2000원 계좌이체 한다.memberA
의 금액이 2000원 감소한다.memberEx
의 금액을 업데이트 하기 전에 예외가 발생한다- 계좌이체는 실패한다.
memberA
의 돈만 2000원 줄어든다.
트랜잭션 - 적용2
- DB 트랜잭션을 사용해서 앞서 발생한 문제점을 해결해보자.
비즈니스 로직과 트랜잭션
- 트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작해야 한다.
- 비즈니스 로직으로 인해 문제가 되는 부분을 함께 롤백해야 하기 때문
- 트랜잭션을 시작하려면 커넥션이 필요하다. 결국 서비스 계층에서 커넥션을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료해야 한다.
- 애플리케이션에서 DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야 한다. 그래야 같은 세션을 사용할 수 있다.
커넥션과 세션
- 애플리케이션에서 같은 커넥션을 유지하려면
- 파라미터로 같은 커넥션을 넘겨줘야 한다.
- 커넥션 유지가 필요한 메서드는 리포지토리에서 커넥션을 닫으면 안 된다.
- 서비스 로직이 끝날 때 트랜잭션을 종료하고 닫아야 한다.
Connection con = dataSource.getConnection();
로 커넥션 획득con.setAutoCommit(false); //트랜잭션 시작
bizLogic(con, fromId, toId, money);
- 트랜잭션 관리 로직과 실제 비즈니스 로직을 구분해주자.
con.commit(); //성공시 커밋
con.rollback(); //실패시 롤백
release(con);
- 커넥션을 모두 사용하고 나면 안전하게 종료한다.
- 실제로는 커넥션이 종료되는게 아니라 커넥션 풀에 반납되기 때문에, 기본 값인 자동 커밋 모드로 변경해주자.
- 결과: 계좌이체 실패 시 트랜잭션 덕분에 데이터를 정상적으로 초기화하였다.
남은 문제
애플리케이션에서 DB 트랜잭션을 적용하려면 서비스 계층이 매우 지저분해지고, 생각보다 매우 복잡한 코드를 요구한다.
- 커넥션 유지 등