트랜잭션이란?
트랜젝션은 데이터베이스의 상태를 변화시키기 위한 작업 수행의 논리적 단위를 의미한다.
우리는 데이터베이스로 SQL 쿼리를 보냄으로써 데이터베이스의 상태를 변화시킨다. 그렇다면 작업 단위라는 것은 무엇을 말하는 것일까? 작업 단위는 사용자가 특정 기능의 수행을 위해 SQL 작업을 묶은 단위를 의미한다.
송금 서비스를 예로 들어보자. 송금 서비스는 1. A가 돈을 보내고, 2. B가 그 돈을 안전하게 받았을때 완전히 실행이 됐다고 할 수 있다. 두 과정이 다른 트랜젝션, 즉 다른 작업으로 분리되었다고 생각해보자.
A가 돈을 10000원 송금하면 A의 돈 데이터에서 10000원이 차감될 것이다. 그에 맞춰서 B의 잔고는 10000원이 추가 될 것이다. 만약 A의 로직에서 오류가 발생해 트랜젝션 rollback이 일어난다면 A의 돈 데이터는 다시 10000원이 추가됨으로써 원상복구가 될 것이다. 그치만 B의 돈은 어떠한가? 다른 트랜젝션으로 분류되었기에 아무런 rollback도 일어나지 않기 때문에 돈이 추가된 상태로 남게 된다.
송금 과정을 하나의 작업 단위로 묶지 않으면 당사자들간의 데이터 불일치가 나타날 수 있다. 우리는 두 과정을 하나의 트랜젝션으로 묶어야 한다. 그래야 중간에 문제가 발생해도 두 과정 모두 rollback이 일어날 수 있다.
이처럼 작업 단위를 잘 설정하는 것은 서비스를 구성함에 있어서 매우 중요한 부분이다. 트랜젝션이 성공적으로 마무리 되었다면 commit을 호출해서 수정 사항을 DB에 영구적으로 반영하고, 중간에 오류가 발생했다면 rollback을 호출해서 트랜젝션 내부의 모든 작업을 원상복구 시킨다.
DBMS의 성능은 초당 트랜잭션의 실행 수( TPS : Transaction per second )로 측정한다.
트랜잭션의 특성(ACID)
트랜젝션은 ACID라는 4가지의 특성을 만족해야 한다.
원자성(Automicity)
트랜젝션 내부에서 실행된 작업들은 모두 성공해서 commit이 되거나, 문제가 발생한다면 rollback을 통해 모두 취소되야 한다. 즉 작업 중 일부분만 성공 할 수 없다. 위의 송금서비스의 예시가 원자성에 대한 설명이다.
- 원자성 보장
트랜젝션 내에서 데이터베이스를 수정할때 지금까지의 성공적인 상태가 롤백 이미지로 롤백 세그먼트라는 임시 영역에 저장 된다. 만약 문제가 생겨서 롤백이 일어난다면 롤백 세그먼트 내의 상태가 복구된다. 참고로 트랜젝션을 commit한 후에는 DB에 수정사항이 영구적으로 반영되기 때문에 롤백 세그먼트의 롤백 이미지도 날라간다.
만약 긴 트랜젝션 범위 내에서 대부분의 로직이 성공적으로 수행됐는데 마지막에 문제가 생겨서 롤백이 된다고 생각해보자. 문제가 발생하지 않은 영역까지 반복적으로 수행해야 하는건 손해이기 때문에 성공적인 지점 까지는 savePoint를 설정할 수 있다.
savePoint
savePoint는 트랜젝션 내부에서 사용자가 지정할 수 있는 세부 작업 단위라고 생각할 수 있다. 아래의 그림처럼 특정 지점에 savePoint를 설정 한 후 'Rollback to savePoint1' 을 통해 해당 지점으로 롤백을 할 수 있다.
기억해야할 점은 아래 그림에서 SP1으로 롤백을 한 뒤에는 미래 시점인 SP2의 savePoint는 삭제 된다는 것이다.
일관성(Consistency)
모든 트랜젝션은 일관성 있는 데이터베이스 상태를 유지해야 한다.
트랜젝션 전 후의 데이터베이스의 상태가 Correct State여야 한다고 하는데, Correct State는 도메인의 유효범위, 무결성 제약조건 등의 제약조건을 위배하지 않는 정상적인 상태를 의미한다.
예를 들어 데이터 타입이 정수형이라면 트랜젝션 이후에도 정수형으로 유지되어야 하며, null값을 제한해놓은 컬럼에 null값이 들어가 있으면 안된다. 또한 연관관계에서 변경이 일어났으면 외래키 값도 정상적으로 변경되어야 한다. 이런 제약조건을 변함없이 만족해야 일관성을 유지한 것이다.
격리성(Isolation)
각각의 트랜젝션은 서로 간섭 없이 독립적으로 수행되어야 한다.
트랜젝션은 기본적으로 원자성, 일관성 그리고 지속성을 보장해준다. 하지만 격리성의 경우에는 완전히 보장하려면 각각의 트랜젝션을 순서대로 처리해야한다.
이는 동시성 처리 성능을 매우 나쁘게 한다. 따라서 격리성의 수준을 여러 단계로 나눠놓았다.
READ UNCOMMITED
상대방에 커밋하지 않은 데이터를 읽을 수 있다. 예를 들어 사용자 A가 트랜젝션 내에서 데이터를 수정하고 있는데 사용자 B가 그 데이터를 조회할 수 있다. 커밋되지 않은 데이터를 조회하는걸 Dirty Read라고 말한다. 사용자 B가 데이터를 사용하는 도중에 사용자A가 데이터를 롤백시켜 버리면 데이터 정합성이 깨질 수 있다.
READ COMMITED
상대방이 커밋한 데이터만 조회할 수 있다. 따라서 Dirty Read는 발생하지 않는다. 하지만 NON-REPEATABLE READ가 발생할 수 있다. 예를 들어 사용자A가 커밋한 데이터를 사용자B가 트랜젝션 내에서 조회하고 있는데 이후에 사용자 A가 데이터를 수정 후 다시 커밋한다면 사용자B는 같은 트랜젝션 내에서 다른 데이터를 조회하게 되는 것이다.
REPEATABLE READ
한번 조회한 데이터는 트랜젝션 내에서 다시 조회해도 같은 데이터가 나오는게 보장된다. 하지만 PHANTOM READ가 발생할 수 있다. 예를 들어 사용자A가 트랜젝션 내에서 20살 이하의 회원 리스트를 조회했을때, 중간에 사용자B가 회원을 20살 이하 회원을 한명 추가한다면, 다시 리스트를 조회했을때 나오는 결과가 달라질 수 있다. 이처럼 결과 집합이 달라지는 것을 PHANTOM READ라고 한다.
SERIALABLE
가장 엄격한 수준의 격리성이지만 동시성 처리에는 매우 약하다.
데이터베이스들은 보통 동시성 처리가 중요하기 때문에 READ COMMITED 수준의 격리성을 사용한다.
지속성(Durability)
성공적으로 수행된 트랜젝션은 영원히 기록된다. 중간에 시스템 문제가 발생했을때 데이터베이스 로그를 참고해서 성공했던 트랜젝션을 복구 할 수 있다.