트랜잭션 관리는 엔터프라이즈 애플리케이션에서 데이터 무결성과 일관성을 보장하는 필수 기법
트랜잭션 관리를 하지 않으면 데이터나 리소스가 더럽혀지고 두서없는 상태
동시성concurrency, 분산distributed 환경에서는 예기치 않은 에러가 발생 시 데이터를 복원해야 하므로 트랜잭션 관리는 매우 중요
트랜잭션은 연속된 여러 액션을 한 단위의 작업으로 집합
이 액션들은 전체가 완전히 끝나거나 아무련 영향도 미치지 않아야 함
모든 액션이 제대로 잘 끝나면 트랜잭션은 영구 커밋되지만 하나라도 잘못되면 초기 상태로 롤백
트랜잭션의 속성 ACID(원자성, 일관성, 격리성, 지속성)
@Transactional
을 붙여 트랜잭션을 적용
public class BookShopCashier implements Cashier {
private BookShop bookShop;
public void setBookShop(BookShop bookShop) {
this.bookShop = bookShop;
}
@Transactional
public void checkout(List<String> isbns, String username) {
for (String isbn : isbns) {
bookShop.purchase(isbn, username);
}
}
}
전달 속성 | 설명 |
---|---|
REQUIRED | 진행 중인 트랜잭션이 있으면 현재 메서드를 그 트랜잭션에서 실행, 그렇지 않을 경우 새 트랜잭션을 시작해서 실행 |
REQUIRES_NEW | 항상 새 트랜잭션을 시작해 현재 메서드를 실행, 진행 중인 트랜잭션이 있으면 잠시 중단 |
SUPPORTS | 진행 중인 트랜잭션이 있으면 현재 메서드를 그 트랜잭션 내에서 실행, 그렇지 않을 경우 트랜잭션 없이 실행 |
NOT_SUPPORTED | 트랜잭션 없이 현재 메서드를 실행하고 진행 중인 트랜잭션이 있으면 잠시 중단 |
MANDATORY | 반드시 트랜잭션을 걸고 현재 메서드를 실행, 진행 중인 트랜잭션이 없으면 예외 |
NEVER | 반드시 트랜잭션 없이 현재 메서드를 실행, 진행 중인 트랜잭션이 없으면 예외 |
NESTED | 진행 중인 트랜잭션이 있으면 현재 메서드를 이 트랜잭션의 중첨 트랜잭션 내에서 실행. 진행 중인 트랜잭션이 없으면 새 트랜잭션을 시작해 실행 |
트랜잭션 전달 방식은 @Transactional(propagation = Propagation.*)
로 지정
REQURIED
는 기본 전달 방식이라서 굳이 명시할 필요가 없음
public class BookShopCashier implements Cashier {
@Transactional(propagation = Propagation.REQURIED)
public void checkout(List<String> isbns, String username) {
...
}
}
public class BookShopCashier implements Cashier {
@Transactional(propagation = Propagation.REQURIED)
public void checkout(List<String> isbns, String username) {
...
}
}
두 트랜잭션 T1, T2가 있을 때 동시성 트랜잭션으로 발생할 수 있는 문제
격리 수준 | 설명 |
---|---|
DEFAULT | DB 기본 격리 수준 사용. 대다수 DB는 READ_COMMITTED이 기본 격리 수준 |
READ_UNCOMMITTED | 다른 트랜잭션이 아직 커밋하지 않은(UNCOMMITED) 값을 한 트랜잭션이 읽을 수 있음. 오염된 값 읽기, 재현 불가한 읽기, 허상 읽기 문제가 발생할 가능성 존재 |
READ_COMMITED | 한 트랜잭션이 다른 트랜잭션에 커밋한 값만 읽을 수 있음. 오염된 값 읽기 문제는 해결, 재현 불가한 읽기, 허상 읽기 문제는 존재 |
REPEATABLE_READ | 트랜잭션이 어떤 필드를 여러 번 읽어도 동일한 값을 읽도록 보장. 트랜잭션이 지속되는 동안에는 다른 트랜잭션이 해당 필드를 변경 불가능. 오염된 값 읽기, 재현 불가한 읽기 문제는 해결, 허상 읽기 문제는 존재 |
SERIALIZABLE | 트랜잭션이 테이블을 여러 번 읽어도 정확히 동일한 로우를 읽도록 보장. 트랜잭션이 지속되는 동안에는 다른 트랜잭션이 해당 테이블을 삽입, 수정, 삭제 불가. 동시성 문제는 모두 해소. 성능은 현저히 떨어짐 |
public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
...
@Transactional
public void increaseStock(String isbn, int stock) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " - Prepare to increase book stock");
getJdbcTemplate().update("UPDATE BOOK_STOCK SET STOCK = STOCK + ? WHERE ISBN = ?", stock, isbn);
System.out.println(threadName + " - Book stock increased by " + stock);
sleep(threadName);
System.out.println(threadName + " - Book stock rolled back");
throw new RuntimeException("Increased by mistake");
}
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public int checkStock(String isbn) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " - Prepare to check book stock");
int stock = getJdbcTemplate().queryForObject("SELECT STOCK FROM BOOK_STOCK WHERE ISBN = ?", Integer.class, isbn);
System.out.println(threadName + " - Book stock is " + stock);
sleep(threadName);
return stock;
}
private void sleep(String threadName) {
System.out.println(threadName + " - Sleeping");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
System.out.println(threadName + " - Wake up");
}
}
READ_UNCOMMITTED
는 한 트랜잭션이 다른 트랜잭션을 아직 커밋하기 전에 변경한 내용을 읽을 수 있는 가장 하위의 격리 수준
public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
...
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public int checkStock(String isbn) {
...
}
}
스레드 1의 트랜잭션이 롤백되지 않은 상태에서 스레드 2가 시작되고 도서 재고를 읽음
격리 수준이 READ_UNCOMMITTED
이므로 스레드 2는 스레드 1의 트랜잭션이 아직 변경 후 커밋하지 않은 재곳값을 그대로 읽음
스레드 1이 깨어나면 RuntimeException
때문에 트랜잭션을 롤백되고 스레드 2가 읽은 값은 더 이상 유효하지 않은 일시적인 값(오염된 값)
checkStock()
의 격리 수준을 READ_COMMITTED
로 한 단계 올리면 해결
public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
...
@Transactional(isolation = Isolation.READ_COMMITTED)
public int checkStock(String isbn) {
...
}
}
REPEATABLE_READ 격리 수준
스레드 1이 도서 재고를 체크, 스레드 2가 도서 재고를 늘리는 일
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(BookstoreConfiguration.class);
final BookShop bookShop = context.getBean(BookShop.class);
Thread thread1 = new Thread(() -> bookShop.checkStock("0001"), "Thread 1");
Thread thread2 = new Thread(() -> {
try {
bookShop.increaseStock("0001", 5);
} catch (RuntimeException e) {}
}, "Thread 2");
thread1.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {}
thread2.start();
}
}
처음에 스레드 1이 도서 재고를 체크하고 sleep
스레드 1의 트랜잭션은 아직 커밋되지 않은 상태
스레드 1이 sleep하는 동안 스레드 2가 시작되어 도서 재고를 늘림
격리 수준이 READ_COMMITTED
이므로 스레드 2는 아직 커밋되지 않은 트랜잭션이 읽은 재곳값을 수정 가능
스레드 1이 깨어나 도서 재고를 다시 읽으면 그 값은 이미 처음에 읽은 그 값이 아니므로 재현 불가한 읽기 문제이며 한 트랜잭션이 동일한 필드를 다른 값으로 읽어들이는 모순 발생
격리 수준을 REPEATABLE_READ
로 한 단계 올림
public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
...
@Transactional(isolation = Isolation.REPEATABLE_READ)
public int checkStock(String isbn) {
...
}
}
스레드 2는 스레드 1이 트랜잭션을 커밋하기 전까지 재곳값을 수정 불가능
재현 불가한 읽기 문제는 커밋되지 않은 한 트랜잭션이 읽은 값을 다른 트랜잭션이 수정하지 못하게 차단해 방지
REPEATABLE_READ
격리 수준을 지원하는 DB는 조회는 되지만 아직 커밋하지 않은 로우에 읽기 잠금을 걸어둔 상태
다른 트랜잭션은 이 트랜잭션이 커밋/롤백하여 읽기 잠금이 풀릴 때까지 기다렸다가 수정
SERIALIZABLE 격리 수준
트랜잭션 1이 테이블에서 여러 로우를 읽은 후, 트랜잭션 2가 같은 테이블에 여러 로우를 새로 추가
트랜잭션 1이 같은 테이블을 다시 읽으면 자신이 처음 읽었을 때와 달리 처음 읽었을 때와 달리 새로 추가된 로우가 있음을 감지(허상 읽기 문제)
허상 읽기는 여러 로우가 연관되는 점만 빼면 불가한 읽기 문제와 비슷
허상 읽기 문제를 해결하려면 최고 격리 수준 SERIALIZABLE
로 격상
전체 테이블에 읽기 잠금을 걸기 때문에 실행 속도가 가장 느림
실무에서는 요건을 충족하는 가장 낮은 수준으로 격리 수준을 선택