[EC-Spring] 6주차-Transaction

umtuk·2022년 5월 21일
0

EC-Spring

목록 보기
6/6

트랜잭션 관리의 중요성

트랜잭션 관리는 엔터프라이즈 애플리케이션에서 데이터 무결성과 일관성을 보장하는 필수 기법
트랜잭션 관리를 하지 않으면 데이터나 리소스가 더럽혀지고 두서없는 상태
동시성concurrency, 분산distributed 환경에서는 예기치 않은 에러가 발생 시 데이터를 복원해야 하므로 트랜잭션 관리는 매우 중요

트랜잭션은 연속된 여러 액션을 한 단위의 작업으로 집합
이 액션들은 전체가 완전히 끝나거나 아무련 영향도 미치지 않아야 함
모든 액션이 제대로 잘 끝나면 트랜잭션은 영구 커밋되지만 하나라도 잘못되면 초기 상태로 롤백

트랜잭션의 속성 ACID(원자성, 일관성, 격리성, 지속성)

  • 원자성(Automicity): 트랜잭션은 연속잭인 액션들로 이루어진 원자성 작업. 트랜잭션의 액션은 전부 다 수행되저나 아무것도 수행되지 않도록 보장
  • 일관성(Consistency): 트랜잭션의 액션이 모두 완료되면 커밋되고 데이터 및 리소스는 비즈니스 규칙에 맞게 일관된 상태를 유지
  • 격리성(Isolation): 동일한 데이터를 여러 트랜잭션이 동시에 처리할 경우 데이터가 변질되지 않게 하려면 각각의 트랜잭션을 격리
  • 지속성(Durability): 트랜잭션 완료 후 그 결과는 설령 시스템이 실패(트랜잭션 커밋 도중 전기가 끊어지는 등)하더라도 살아남아 보통 그 결과물은 퍼시스턴스 저장소에 씌어짐

@Transactional을 붙여 선언적으로 트랜잭션 관리하기

@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);
        }
    }
}

REQURIED 전달 속성

전달 속성설명
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가 있을 때 동시성 트랜잭션으로 발생할 수 있는 문제

  • 오염된 값 읽기(Dirty read): T2가 수정 후 커밋하지 않은 필드를 T1이 읽는 상황에서 나중에 T2가 롤백되면 T1이 읽은 필드는 일시적인 값으로 더 이상 유효하지 않음
  • 재현 불가한 읽기(Nonrepeatable read): 어떤 필드를 T1이 읽은 후 T2가 수정할 경우, T1이 같은 필드를 다시 읽으면 다른 값을 얻음
  • 허상 읽기(Phantom read): T1이 테이블의 로우 몇 개를 읽은 후 T2가 같은 테이블에 새 로우를 삽입할 경우, 나중에 T1이 같은 테이블을 다시 읽으면 T2가 삽입한 로우가 보임
  • 소실된 수정(Lost updates): T1, T2 모두 어떤 로우를 수정하려고 읽고 그 로우의 상태에 따라 수정하려는 경우. T1이 먼저 로우를 수정 후 커밋하기 전, T2가 T1이 수정한 로우를 똑같이 수정헀다면 T1이 커밋한 후에 T2 역시 커밋을 하면 T1이 수정한 로우를 T2가 덮어쓰게 되어 T1이 수정한 내용이 소실
격리 수준설명
DEFAULTDB 기본 격리 수준 사용. 대다수 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로 격상
전체 테이블에 읽기 잠금을 걸기 때문에 실행 속도가 가장 느림
실무에서는 요건을 충족하는 가장 낮은 수준으로 격리 수준을 선택

참고

스프링 5 레시피

profile
https://github.com/umtuk

0개의 댓글