데이터를 저장할 때 단순히 파일에 저장해도 되지만, 데이터베이스에 저장하는 이유는 무엇일까 ?
가장 대표적인 이유는 데이터베이스는 트랜잭션이라는 개념을 지원하기 때문이다.
트랜잭션을 이름 그대로 번역하면 거래이다. 즉, 트랜잭션은 하나의 거래를 안전하게 처리하도록 보장해주는 것을 뜻한다.
즉, 데이터베이스의 상태를 바꾸기 위해 수행되는 작업의 최소 단위를 의미한다.
모든 작업이 성공해서 데이터베이스에 정상 반영하는 것을 커밋(Commit)이라 하고, 작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것을 롤백(Rollback)이라 한다.
트랜잭션 ACID
트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질
원자성 (Atomic)
트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다.
트랜잭션이 데이터베이스에 모두 반영되거나, 전혀 반영되지 않아야 한다.
트랜잭션은 사람이 설계한 논리적인 작업 단위로서, 일처리는 작업단위 별로 이루어져야 사람이 다루는데 무리가 없다.
만약, 트랜잭션 단위로 데이터가 처리되지 않는다면, 설계한 사람은 데이터 처리 시스템을 이해하기 힘들뿐만 아니라 오작동 했을시 원인을 찾기 매우 힘들어진다.
일관성 (Consistency)
트랜잭션의 작업 처리 결과가 항상 일관성이 있어야 한다는 것으로, 트랜잭션이 진행되는 동안에 데이터베이스가 변경 되더라도 업데이트된 데이터베이스로 트랜잭션이 진행되는 것이 아니라, 처음에 트랜잭션을 진행하기 위해 참조한 데이터베이스로 진행된다.
이로써 각각의 사용자는 일관성 있는 데이터를 볼 수 있다.
격리성 (Isolation)
동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리하는 것으로 하나의 트랜잭션이 실행하는 도중에 변경한 데이터는 이 트랜잭션이 완료될 때까지 다른 트랜잭션이 참조하지 못하게 하는 특성이다.
트랜잭션 격리 수준 (Isolation level)
트랜잭션은 원자성, 일관성, 지속성을 보장한다. 하지만 격리성을 완벽히 보장하려면 트랜잭션을 순차적으로 실행해야 한다. 이렇게 하면 동시 처리 성능이 매우 나빠진다. 이러한 문제를 해결하기 위해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정리했다.
- READ UNCOMMITED (아직 커밋되지 않은 데이터를 읽을 수 있다)
Dirty Read, Non-Repeatable-Read, Phantom-Read 문제 발생
작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있는 현상인 Dirty Read 문제 때문에 데이터 정합성 문제가 발생할 수 있다. 하지만, 해당 기능은 매우 단순하기 때문에 성능면에서는 유리하다는 장점이 있다.
🔔 Dirty Read
다른 트랜잭션에서 처리한 작업이 완료되지 않았음에도 불구하고 다른 트랜잭션에서 볼 수 있게되는 현상으로 데이터가 나타났다가 사라지는 현상을 초래할 수 있으므로 개발자와 사용자를 혼란스럽게 만든다.
따라서, READ UNCOMMITED 격리 수준은 트랜잭션의 격리 수준으로 인정하지 않을 정도로 데이터의 정합성에 악영향을 끼치기 때문에 해당 격리 수준은 피할 것을 권장한다.
⇒ 한 트랜잭션의 변경된 내용을 커밋이나 롤백과 상관 없이 다른 트랜잭션에서 읽을 수 있는 격리 수준이며, 모든 부정합 문제가 발생한다. - READ COMMITTED (커밋된 데이터만 읽을 수 있다)
Non-Repeatable-Read, Phantom-Read 문제 발생
어떠한 트랜잭션에서 데이터를 변경하더라도 커밋된 데이터만 읽을 수 있기 때문에 Dirty Read 문제는 해결할 수 있다.
하지만, 같은 트랜잭션 내에서 같은 행을 반복해서 읽었을 때 다른 트랜잭션이 그 행을 수정하거나 삭제하여 결과가 달라지는 현상인 Non-Repeatable-Read 문제가 발생한다.
🔔 Non-Repeatable-Read
하나의 트랜잭션 내에서 동일한 SELECT 쿼리를 실행했을 때 항상 같은 결과를 보장해야 한다는 Repeatable Read 정합성에 어긋나는 것을 말한다.
⇒ 커밋이 완료된 데이터만 조회 가능한 격리 수준으로 Dirty Read 문제를 해결한다. - REPEATABLE READ (특정 데이터를 반복 조회시 같은 값을 반환한다)
Phantom-Read 문제 발생
다른 트랜잭션에서 특정 데이터에 대해 커밋을 수행해도 해당 데이터를 반복 조회해도 항상 같은 값을 반환하기 때문에 Non-Repeatable-Read 문제는 해결한다.
하지만, 같은 트랜잭션 내에서 동일한 쿼리를 반복 실행했을 때 다른 트랜잭션에 의해 새로운 데이터 행이 삽입되거나, 기존 데이터 행이 삭제되는 현상인 Phantom-Read 문제는 발생한다.
⇒ 트랜잭션이 시작되기 전에 커밋된 내용에 관해서만 조회할 수 있는 격리 수준으로 Non-Repeatable-Read 문제를 해결하고 InnoDB 에서는 Phantom-Read 문제를 해결한다. - SERIALIZABLE (직렬화 기능)
가장 단순한 격리 수준이면서 동시에 가장 엄격한 격리 수준이다. 그만큼 동시 처리 성능도 다른 트랜잭션 격리 수준보다 떨어진다. InnoDB 테이블에서 순수한 SELECT 작업은 아무런 레코드 잠금도 설정하지 않고 실행되지만, 트랜잭션 격리 수준이 SERIALIZABLE로 설정되면 읽기 작업도 공유 잠금(읽기 잠금)을 획득해야 한다.
즉, 한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없는 것이다. 따라서 SERIALIZABLE 격리 수준에서는 모든 부정합 문제가 발생하지 않지만, 동시 처리가 거의 불가능하기 때문에 사용을 권장하지 않는다.
모든 작업의 독립성을 보장하기 위해 하나씩 순차적으로 진행하게 된다면,
CPU는 DBMS 보다 입/출력 작업을 빈번히 수행하기 때문에 CPU는 점점 응답을 기다리는 시간이 길어져 프로그램이 비효율적으로 돌아가는 문제가 발생한다.
이처럼 데이터베이스에 저장된 데이터의 무결성과 동시성의 성능을 지키기 위해 트랜잭션의 설정을 중요하게 고민해야 한다.
지속성 (Durability)
트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
트랜잭션이 완료되면, 주기억장치가 아닌 디스크와 같은 보조기억장치에 저장되거나 그렇지 않더라도 시스템 장애가 회복되고 난 후에 어떠한 형태로든지 그 데이터를 복구할 수 있게 해야 함을 뜻한다.
트랜잭션 상태
- 활성 (Active) : 트랜잭션이 정상적으로 실행중인 상태
- 부분 완료 (Partially Committed) : 트랜잭션의 마지막까지 실행되었지만, 커밋 연산이 실행되기 직전의 상태
- 완료 (Committed) : 트랜잭션이 성공적으로 종료되어 커밋 연산을 실행한 후의 상태
- 실패 (Failed) : 트랜잭션 실행에 오류가 발생하여 중단된 상태
- 철회 (Aborted) : 트랜잭션이 비정상적으로 종료되어 롤백 연산을 수행한 상태
트랜잭션 사용법
- 데이터 변경 쿼리를 실행하고 데이터베이스에 그 결과를 반영하기 위해선 커밋 명령어인
commit
을 호출하고, 결과를 반영하고 싶지 않은 경우는 롤백 명령어인rollback
을 호출하면 된다.
( 신규 데이터를 추가, 수정 및 삭제한 데이터도rollback
을 호출하면 모두 트래잭션을 시작하기 직전의 상태로 복구된다. ) - 커밋을 호출하기 전까지는 임시로 데이터를 저장하는 것이다. 따라서 해당 트랜잭션을 시작한 세션(사용자)에게만 변경 데이터가 보이고 다른 세션(사용자)에게는 변경 데이터가 보이지 않는다.
💡커밋하지 않은 데이터를 다른 곳(세션)에서 조회할 수 있다면 ?
만약, 세션A 가 트랜잭션을 시작하고 DB 새로운 데이터를 추가하고 커밋을 하지 않은 상태에서 세션B 가 조회를 하게 되면 추가한 데이터를 조회할 수 있게된다. 이 상황에서 세션B 는 해당 데이터가 있다고 가정해 추가적인 로직을 수행할 수 있게된다. 하지만 이때 세션A 가 롤백을 수행하게 되면, 새롭게 추가한 데이터가 사라지게 되고 데이터 정합성에 큰 문제가 발생한다.
따라서, 아직 커밋하지 않은 변경 데이터가 보인다면, 롤백 했을 때 심각한 문제가 발생할 수 있기 때문에 커밋 전의 데이터는 다른 세션에서 보이지 않는다.
- 등록, 수정, 삭제 모두 같은 원리로 동작한다.
자동 커밋, 수동 커밋
자동 커밋으로 설정하면 각각의 쿼리 실행 직후에 자동으로 커밋을 호출한다. 따라서 커밋이나 롤백을 직접 호출하지 않아도 되는 편리함이 있다. 하지만 쿼리를 하나하나 실행할 때 마다 자동으로 커밋이 되어버리기 때문에 우리가 원하는 트랜잭션 기능을 제대로 사용할 수 없다.
// 자동 커밋 모드 설정
set autocommit true;
insert into member(member_id, money) values ('data1', 1000); // 자동 커밋
따라서 commit, rollback
을 직접 호출하면서 트랜잭션 기능을 제대로 수행하기 위해선 수동 커밋을 사용해야 한다.
// 수동 커밋 모드 설정
set autocommit false;
insert into member(member_id, money) values ('data1', 1000);
insert into member(member_id, money) values ('data2', 1000);
commit; // 수동 커밋
보통 자동 커밋 모드가 기본으로 설정된 경우가 많기 때문에, 수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현할 수 있다.