[DB] 군대로 알아보는 트랜잭션 - 3. 독립성 편

25166 단어 DatabaseDatabase

이번 편에서는 트랜잭션의 독립성(또는 고립성, 격리성)에 대해 알아보려고 합니다.

1. 정의

  • 독립성(Isolation)은 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것을 의미한다. 이것은 트랜잭션 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없음을 의미한다. (출처)
  • 독립성은 ACID 원칙들 중에서 가장 유연성 있는 제약조건입니다.

2. 트랜잭션의 격리 수준

트랜잭션의 격리 수준은 크게 4가지로 나뉩니다.

  • Read Uncommitted
  • Read Committed
  • Repeatable Read
  • Serializable

Read Uncommitted

  • Read uncommitted는 커밋되지 않은 정보까지도 읽는 것을 허용합니다

  • Dirty Read, Non-repeatable read, Phantom read가 발생할 수 있습니다

Dirty Read의 예시를 살펴보겠습니다

입대 신청이 다음과 같은 로직으로 구동된다고 가정하겠습니다

public void 입대_신청() {
        System.out.print("이름을 입력해주세요: ");
        String name = sc.next();

        militaryDB.입대(name);

        System.out.print("정말로 입대하시겠습니까? [Y/N]: ");
        String answer = sc.next();


        if(answer.equals("Y")) {
            System.out.println("입대 신청이 완료되었습니다.");
        } else {
            militaryDB.삭제(name);
            System.out.println("입대 신청이 취소되었습니다.");
        }
    }
  • 이 때 Y/N에 대답하기 전에 훈련소로 오라는 명령을 누군가가 실행한다면 다음과 같은 결과가 발생하게 될 것입니다

  • 입대 신청에서 '정말로 입대하시겠습니까? [Y/N]?'에 N이라고 답하는 것은, DB Transaction에서 rollback과 같은 기능입니다.

  • 그런데 Uncommitted Read로 격리 수준을 설정하면 위와 같이 커밋되지 않은 정보를 읽어오게 되는 불상사가 발생할 수 있습니다.

  • 따라서 Uncommitted Read는 데이터베이스 정합성을 심각한 수준으로 침해하게 되고, 이 때문에 격리 수준으로 인정되지도 않는 수준입니다.


Read Committed

  • 이제 한 트랜잭션에서 변경 내용이 commit 되어야만 다른 트랜잭션에서 조회할 수 있습니다!

  • 가장 보편적인 격리 수준입니다

  • Non-repeatable read, Phantom Read 문제가 발생할 수 있습니다

Non-repeatable read의 예시를 살펴보겠습니다

훈련병들을 식당으로 입장시키는 도중, 한 훈련병의 이탈 발생!

class MilitaryDB{
    int idx = 0;
    HashMap<Integer, String> newSoldiers = new HashMap<>();

    public void 입대(String name) {
        newSoldiers.put(idx++, name);
    }

    public void 훈련소로_입장() {
        System.out.println("훈련병들은 차례로 훈련소로 입장한다, 실시!");

        ArrayList<Integer> allSoldiersId = getAllSolders(); //select id from new_soldiers;

        for (int id : allSoldiersId) {
            String name = newSoldiers.get(id);
            if(name != null) System.out.println(name + " 훈련병, 훈련소로 입장!");
            else System.out.println("번호가 " +id +  "인 훈련병? 분명 있었는데 어디갔지?");
        }
    }

    public ArrayList<Integer> getAllSolders() {
        ArrayList<Integer> arr = new ArrayList<>();
        for (int id : newSoldiers.keySet()) {
            arr.add(id);
        }
        return arr;
    }

    public void 삭제(int id) {
        newSoldiers.remove(id);
    }
}
public static void main(String[] args) throws InterruptedException {
        MilitaryDB militaryDB = new MilitaryDB();

        militaryDB.입대("Jake");
        militaryDB.입대("Sam");
        militaryDB.입대("Kim");
        militaryDB.입대("Park");
        militaryDB.입대("Lee");

        Thread thread1 = new Thread(() -> militaryDB.훈련소로_입장());
        thread1.start();
        Thread thread2 = new Thread(() -> militaryDB.삭제(3));
        thread2.start();

    }

결과

  • 트랜잭션의 시작에서 읽었을 때는, 훈련병이 5명이었습니다

  • 트랜잭션 중간에 삭제 메서드가 개입하여 훈련병이 4명으로 줄었습니다

  • 이처럼, 동일한 트랜잭션에서 select 쿼리를 실행했을 때 결과가 달라지는 경우를 non-repeatable read라고 합니다

  • 일반적으로는 크게 문제되지 않지만, 금융 서비스 등에서는 치명적일 수 있습니다


Repetable Read

  • 위에서 언급했던 non-repeatable read가 해결된 상태입니다

  • 자신의 트랜잭션 번호보다 낮은 트랜잭션 번호에서 커밋된 내용만 보게 됩니다

  • Repeatable Read를 구현하는 방법은 크게 2가지가 있습니다

    • Lock을 이용한 Concurrecy control
    • MVCC(Multi Version Concurrency Control)
  • Repeatable read에서도 phamtom read와 같은 문제가 발생합니다

Phantom Read란

public static void main(String[] args) throws InterruptedException {
        MilitaryDB militaryDB = new MilitaryDB();

        militaryDB.입대("Jake");
        militaryDB.입대("Sam");
        militaryDB.입대("Kim");
        militaryDB.입대("Park");
        militaryDB.입대("Lee");

        Thread thread1 = new Thread(() -> militaryDB.훈련소로_입장());
        Thread thread2 = new Thread(() -> militaryDB.입대("Late"));
        thread1.start();
        thread2.start();

    }
    
class MilitaryDB{
    int idx = 0;
    HashMap<Integer, String> newSoldiers = new HashMap<>();

    public void 입대(String name) {
        newSoldiers.put(idx++, name);
    }

    public void 훈련소로_입장() {
        System.out.println("훈련병들은 차례로 훈련소로 입장한다, 실시!");

        HashMap<Integer, String> allSoldiersMap = newSoldiers; //select id from new_soldiers;


        for (String name : allSoldiersMap.values()) {
            System.out.println(name + " 훈련병, 훈련소로 입장!");
        }

        //조교가 잠시 한 눈을 팔다가 돌아온 사이 훈련병 한 명이 새로 들어왔다!

        if(newSoldiers.size() != allSoldiersMap.size())
            System.out.println("왜 훈련병 한 명이 남지?");
    }
}
  • 위 상황처럼, 트랜잭션 중간에 insert 쿼리로 인해 새로운 데이터가 생겼을 때 처음에는 읽지 못했던 자료가 갑자기 등장하게 되는 현상이 발생할 수 있습니다

  • 이처럼 트랜잭션 과정에서 기존에 없던 자료가 새로 읽히게 되는 것을 phantom read라고 합니다


Serializable

  • 읽기에서도 해당 자원에 잠금을 거는 방식입니다
  • 정합성 측면에서는 완벽하지만, 동시성 측면에서는 최악의 퍼포먼스를 보이게 됩니다
  • 때문에 실제로는 많이 적용하기 힘든 방식입니다

3. 정리

  • Uncommitted Read

    • Dirty read, non-repeatable read, phantom read 발생 가능
    • 격리 수준으로 인정받지 못하는 수준

  • Read Committed

    • Non-repeatable read, phantom read 발생 가능
    • 보편적인 격리 수준

  • Repeatable Read

    • phantom read 발생 가능
    • concurrency control을 통해 데이터의 버전을 관리해주어야 함

  • Serializable

    • 데이터베이스 정합성의 무결함을 보장
    • 동시성에서 최악의 퍼포먼스를 보임

좋은 웹페이지 즐겨찾기