Search
〰️

Transaction

트랜잭션 개념 이해

DB에서 트랜잭션이란?
하나의 거래를 안전하게 처리하도록 보장해주는 것
예) 5000원 계좌이체
A의 잔고를 5000원 감소
B의 잔고를 5000원 증가
→ 두가지의 작업이 하나처럼 동작해야한다!
모든 작업이 성공해서 데이터베이스에 정상 반영 : commit
작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것 : rollback

트랜잭션 ACID

트랜잭션은 원자성, 일관성, 격리성, 지속성을 보장해야한다.
원자성 : 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야한다.
일관성 : 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야한다.
격리성 : 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다.
지속성 : 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야한다.
트랜잭션은 원자성, 일관성, 지속성을 보장한다. 문제는 격리성인데… 트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해야한다. 이렇게 하면 동시 처리 성능이 매우 나빠진다. 이런 문제로 인해 ANSI표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다.
트랜잭션 격리 수준 - isolation level
READ UNCOMMITED (커밋되지 않은 읽기)
READ COMMITED (커밋된 읽기)
기본으로 많이 사용!
REPEATABLE READ (반복 가능한 읽기)
SERIALIZABLE (직렬화 가능)

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

사용자는 WAS나 DB 접근 툴 같은 클라이언트를 사용해서 DB 서버에 접근
클라이언트는 데이터베이스 서버에 연결을 요청하고 커넥션을 맺음
데이터베이스 서버는 내부에 세션을 만듦
해당 커넥션을 통한 모든 요청은 이 세션을 통해서 실행하게 한다.
쉽게 이야기해서 개발자가 클라이언트를 통해 SQL을 젇날하면 현재 연결된 세션이 SQL을 실행한다.
세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다.
이후에 새로운 트랜잭션을 다시 시작할 수 있다.
사용자가 커넥션을 닫거나, 또는 DBA가 세션을 강제로 종료하면 세션은 종료된다.
→ 커넥션풀이 10개의 커넥션을 생성하면, 세션도 10개 생성된다.

트랜잭션 개념 이해

트랜잭션 사용법
데이터 변경 쿼리를 실행하고 데이터베이스에 그 결과를 반영하려면 커밋 명령어인 commit을 호출하고, 결과를 반영하고 싶지 않으면 롤백 명령어인 rollback을 호출하면 된다.
커밋을 호출하기전까지는 임시로 데이터를 저장하는 것이다!
해당 트랜잭션을 시작한 세션(사용자)에게만 변경 데이터가 보이고, 다른 세션(사용자)에게는 변경 데이터가 보이지 않는다.
등록, 수정, 삭제 모두 같은 원리로 동작한다.
커밋하지 않은 데이터를 다른곳에서 조회할 수 있다면 어떤 문제가 발생할까? 세션2에서 세션1이 아직 커밋하지 않은 변경데이터가 보인다면, 세션1이 롤백 했을 때 심각한 문제가 발생할 수 있다.

트랜잭션 동기화

리소스 동기화
트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야한다.
connection.close()를 사용해서 커넥션을 직접 닫아버리면 커넥션이 유지되지 않는다.
커넥션과 세션
사용자 → 클라이언트 커넥션 → 데이터베이스서버 커넥션 → 세션 → SQL 실행 (트랜잭션 커밋)
트랜잭션 매니져와 트랜잭션 동기화 매니져
트랜잭션 동기화 매니져
Thread Local을 사용해서 커넥션을 동기화
트랜잭션 매니져는 내부에서 이 트랜잭션 동기화 매니져를 사용한다.
트랜잭션 동기화 매니져는 멀티쓰레드 상황에서도 안전하게 커넥션을 동기화 할 수 있다.
동작방식
트랜잭션 시작 → 트랜잭션 매니져가 데이터소스를 통해 커넥션을 만들고 커넥션 시작
트랜잭션 매니져는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니져에 보관
리포지토리는 트랜잭션 동기화 매니져에 보관된 커넥션을 꺼내서 사용
트랜잭션이 종료되면 트랜잭션 매니져는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고, 커넥션도 닫는다.
트랜잭션 동기화 매니져
TransactionSynchronizationManager

DataSourceTransactionManager을 통한 Transaction 추상화

1.
서비스 계층에서 transactionManager.getTransaction() 을 호출해서 트랜잭션을 시작한다.
2.
트랜잭션을 시작하려면 먼저 데이터베이스 커넥션이 필요하다. 트랜잭션 매니저는 내부에서 데이터소스를 사용해서 커넥션을 생성한다.
3.
커넥션을 수동 커밋 모드로 변경해서 실제 데이터베이스 트랜잭션을 시작한다.
4.
커넥션을 트랜잭션 동기화 매니저에 보관한다.
5.
트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관한다.
6.
서비스는 비즈니스 로직을 실행하면서 리포지토리의 메서드들을 호출한다. 이때 커넥션을 파라미터로 전달하지 않는다.
7.
리포지토리 메서드들은 트랜잭션이 시작된 커넥션이 필요하다. 리포지토리는 DataSourceUtils.getConnection()을 사용해서 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. 이 과정을 통해서 자연스럽게 같은 커넥션을 사용하고, 트랜잭션도 유지된다.
8.
획득한 커넥션을 사용해서 SQL을 데이터베이스에 전달해서 실행한다.
9.
비즈니스 로직이 끝나고 트랜잭션을 종료한다. 트랜잭션은 커밋하거나 롤백하면 종료된다.
10.
트랜잭션을 종료하려면 동기화된 커넥션이 필요하다. 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득한다.
11.
획득한 커넥션을 통해 데이터베이스에 트랜잭션을 커밋하거나 롤백한다.
12.
전체 리소스를 정리한다.
a.
트랜잭션 동기화 매니저를 정리한다. 쓰레드 로컬은 사용후 꼭 정리해야 한다.
b.
con.setAutoCommit(true) 로 되돌린다. 커넥션 풀을 고려해야 한다.
c.
con.close() 를 호출해셔 커넥션을 종료한다. 커넥션 풀을 사용하는 경우 con.close() 를 호출하면 커넥션 풀에 반환된다.

Transaction Template

Template Callback Pattern
Transaction 시작, 커밋, 롤백 코드 공통화
비즈니스 로직이 정상 수행되면 커밋
Uncheced Exception (RunTimeException)이 발생하면 Rollback
그외의 경우 Commit (Checked Exception인 경우에도 Commit)

Transaction AOP

선언적 트랜잭션 관리 vs 프로그래밍 방식 트랜잭션 관리

선언적 트랜잭션 관리
@Transactional
선언하면 트랜잭션이 적용
프로그래밍 방식의 트랜잭션 관리
TransactionManager 또는 TransactionTemplate 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 코드
스프링 컨테이너나 스프링 AOP기술없이 간단히 사용할 수 있다.

Springboot Datasource, TransactionManager

Springboot는 datasource를 Spring 빈에 자동으로 등록한다
dataSource
Springboot는 적절한 트랜잭션 매니저 (PlatformTransactionManager)를 자동으로 스프링빈에 등록한다.
transactionManager
어떤 트랜잭션 매니저를 선택할지는 현재 등록된 라이브러리를 보고 판단한다.
application.yml 의 설정을 기반으로 한다.

Propagation

트랜잭션 매니저에 커밋을 호출한다고해서 항상 실제 커넥션에 물리 커밋이 발생하지는 않는다는 점이다.
신규 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행한다. 신규 트랜잭션이 아니면 실제 물리커넥션을 사용하지 않는다.
트랜잭션이 내부에서 추가로 사용되면, 트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고, 모든 논리트랜잭션이 커밋되면 물리 트랜잭션이 커밋된다!
스프링은 여러 트랜잭션이 함께 사용되는 경우, 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다.

Rollback-only

외부 트랜잭션 시작 → 내부 트랜잭션 시작 → 내부 트랜잭션 롤백 → 외부 트랜잭션 커밋
내부트랜잭션을 롤백하면 실제 물리 트랜잭션은 롤백하지 않는다.
대신에, 기존 트랜잭션을 롤백 전용으로 표시한다. (트랜잭션 동기화 매니져 rollbackOnly=true)
외부 트랜잭션을 커밋하면 전체 트랜잭션이 롤백 전용으로 표시되어 있으므로 물리 트랜잭션을 롤백한다.
원칙 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백된다.

REQUIRES_NEW

@Test void inner_rollback_requires_new() { log.info("외부 트랜잭션 시작"); TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute()); log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); // true log.info("내부 트랜잭션 시작"); DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); // 신규 물리트랜잭션 생성 TransactionStatus inner = txManager.getTransaction(definition); log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); // true log.info("내부 트랜잭션 롤백"); txManager.rollback(inner); // 롤백 log.info("외부 트랜잭션 커밋"); txManager.commit(outer); // 커밋 }
Java
복사
내부 트랜잭션을 시작할 때 전파 옵션인 propagationBehavior에 PROPAGATION_REQUIRES_NEW 옵션을 주었다.