트랜잭션 이해하기
트랜잭션은 왜 필요할까?
상품 주문과 재고 차감은 하나의 논리적 단위로 이루어져야한다. 또한 대표적인 예 중 하나로 계좌 이체 시 차감 과 가산은 하나의 논리적 단위이다.
- 이는, 주문 후 재고 차감이 이루어지지 않은 경우와 차감 후 가산이 되지 않는 경우 처럼 실행 흐름 중 에러가 발생하여 작업의 일부만 실행이 되는 경우를 뜻한다.
이러한 연쇄적인 행위가 하나의 논리적 단위의 흐름이라면 트랜잭션이 필요하다.
트랜잭션이란?
논리적으로 쪼개질 수 없는 하나 이상의 데이터베이스 작업 묶음을 의미한다.
트랜잭션은 전부 성공 하거나 전부 실패해야 한다 라는 원칙을 준수한다.
- 트랜잭션으로 묶인 작업들은 모두 성공해야만 그 결과를 실제 데이터베이스에 영구적으로 반영한다.
- 만약, 작업 그룹 내에서 단 하나의 작업이라도 실패하면 그 전에 성공했던 모든 작업들을 전부 없었던 일로 되돌려 버린다.
한 번에 같이 실행 되어야 하는 상황을 아토믹 연산이라고도 한다.
시나리오
- 주문 처리 트랜잭션 시작
- INSERT 문으로 orders 테이블에 주문 기록
- UPDATE 문으로 products 테이블의 재고 차감 중 실패
- 하나의 작업이라도 실패했으므로, 트랜잭션은 실패로 간주된다.
- 데이터베이스는 2번에서 성공했던 INSERT 작업까지 자동으로 취소한다.
- 결과적으로, orders 테이블과 products 테이블은 이 트랜잭션이 시작되기 전의 완벽하게 일관된 상태로 남게 된다.
트랜잭션의 ACID 속성
트랜잭션이 신뢰성을 갖추도록 반드시 지켜야 할 4가지 속성인 ACID 라는 약어로 부른다.
- Atomicity
- Consistency
- Isolation
- Durability
Atomicity (원자성)
트랜잭션은 하나의 원자처럼 더 이상 쪼갤 수 없는 논리적 단위이며, 전부 성공하거나 전부 실패한다. 원자성 덕분에, 돈이 A 의 계좌에서 출금이 발생했는데, B 의 계좌에서 입금이 발생하지 않는 상태는 절대 발생하지 않는다.
Consistency (일관성)
트랜잭션이 성공적으로 완료되면, 데이터베이스는 항상 일관된 상태를 유지해야 한다.
일관성은 트랜잭션의 실행 결과가 데이터베이스에 설정된 모든 규칙을 위반하지 않음을 보장하는 속성이다.
일관성은 결과의 유효성을 보장하는 속성이다.
Isolation (격리성)
하나의 트랜잭션이 실행 중일 때, 다른 트랜잭션이 해당 트랜잭션의 중간 결과에 끼어들어 간섭할 수 없다. 격리성은 여러 사용자가 동시에 데이터베이스에 접근하는 환경에서 데이터가 엉망이 되는 것을 막아주는 중요한 속성이다.
이러한 격리성을 준수하기 위해 “어느정도로 엄격하게 격리 시킬 것인가” 에 대한 격리 수준이 나뉜다. 완벽하게 격리시키면 데이터는 매우 안전하지만 동시에 많은 작업을 처리하기 어려워져 성능이 저하되고, 격리 수준을 낮추면 성능은 올라가지만 데이터 불일치 문제가 발생할 수 있다.
격리성이 없다면?
트랜잭션이 작업을 실행하는 동안 다른 트랜잭션이 간섭하여 아직 완료되지 않은 상태를 읽어 논리적으로 불안정한 결과를 반환한다. 이를 Dirty Read 라고한다.
이 문제를 해결하기 위해서는 다른 트랜잭션이 현재 실행중인 트랜잭션의 임시 저장 값을 읽지 못하도록 막아야한다.
격리성이 있다면?
하나의 트랜잭션이 실행 중일 때, 다른 트랜잭션은 실행중인 트랜잭션이 임시로 변경한 값을 읽지 못한다. 이로인해 임시로 변경되기 전의 원본 값을 읽어 자신이 수행해야 할 작업을 수행한다.
Durability (지속성)
성공적으로 완료되어 COMMIT 된 트랜잭션의 결과는, 시스템에 장애가 발생하더라도 영구적으로 보존된다.
커밋된 데이터는 절대 사라지지 않는다는 약속이다.
- 커밋이 발생한 이후는 데이터베이스 시스템 내부적으로 커밋 로그를 기록하여, 장애가 발생하더라도 이 로그를 읽어 처리하기 때문에 영구적으로 보존이 가능하다.
트랜잭션의 격리 수준
완벽한 격리는 항상 좋을까?
만약 모든 트랜잭션이 완벽하게 격리 되어, 마치 한 줄로 서서 한 번에 하나씩만 순서대로 실행된다면 데이터는 매우 안전한다.
하지만, 수천 수만 명의 사용자가 동시에 접속하여 주문하고, 상품을 조회하는 온라인 쇼핑몰 같은 어플리케이션은 성능이 매우 저하된다.
실무 수준에서는 데이터 정합성 vs 동시성과 성능 관계의 트레이드 오프가 항상 존재한다.
데이터베이스는 이러한 트레이드오프를 개발자가 직접 조절할 수 있도록 트랜잭션 격리 수준이라는 몇 가지 단계를 제공한다.
동시성 문제
더티 리드 - Dirty Read 한 트랜잭션이 아직 COMMIT 하지 않은, 수정 중인 데이터를 다른 트랜잭션이 읽는 것
예시 트랜잭션 A 가 특정 상품의 가격을 100원에서 120원으로 바꾸고 아직 COMMIT 하지 않았다. 이 때 트랜잭션 B 가 이 상품의 가격을 조회 했더니 “120원”이 조회 되었다. 잠시 후, 트랜잭션 A 가 작업을 취소 해버리면 가격은 다시 “100원”이 된다. 트랜잭션 B는 결국 존재하지 않는 오염된 데이터를 읽게 된 셈이다.
반복 불가능한 읽기 - Non Repeatable Read 한 트랜잭션 내에서 똑같은 SELECT 쿼리를 두 번 실행했는데, 그 사이에 다른 트랜잭션이 값을 수정하고 COMMIT 하는 바람에 두 쿼리의 결과가 다르게 나오는 현상
예시 트랜잭션 A가 특정 상품의 재고 필드 값이 ‘10개’인 것을 확인했다. 잠시 다른 작업을 하다가 다시 재고를 확인했더니, 그 사이에 다른 트랜잭션 B가 그 상품을 하나 사가서 재고 필드 값이 ‘9개’로 바뀌어 있다. 트랜잭션 A 안에서 같은 데이터의 반복 조회가 불가능해진 것이다.
유령 읽기 - Phantom Read 한 트랜잭션 내에서 특정 범위의 데이터를 두 번 읽었는데, 첫 번째 조회에서 없었던 새로운 행이 두 번째 조회에서 나타나는 현상이며, 이는 다른 트랜잭션이 새로운 행을 INSERT 하고 COMMIT 했기 때문에 발생한다.
예시 트랜잭션 A가 ‘전자기기’ 카테고리의 상품 수를 세었더니 ‘5개’였다. 잠시 후 똑같이 수를 세었더니, 그 사이에 다른 트랜잭션 B가 새로운 ‘전자기기’ 상품을 등록해서 ‘6개’가 되었다. 즉, 없었던 유령 상품이 나타난 것이다.
격리 수준
제공 되는 격리 수준의 엄격함이 높을수록 성능이 저하되지만, 데이터 정합성을 보장할 수 있다. 데이터베이스는 이로인한 트레이드 오프로 개발자가 직접 선택할 수 있게 격리 수준을 레벨 별로 제공한다.;

READ UNCOMMITTED - LV 0
가장 낮은 수준의 격리 수준으로 아직 커밋 되지 않은 데이터 조차 읽을 수 있도록 허용한다. 이로인해 정합성 이슈가 많아 거의 사용되지 않는 격리 수준이다.
READ COMMITTED - LV 1
커밋된 데이터만 읽을 수 있어, 더티 리드를 기본적으로 방지할 수 있지만 넌 리피터블 리드나 팬텀 리드는 방지할 수 없다.
- 다른 트랜잭션이 트랜잭션 수행중에 커밋한 문제기 때문에 값의 추가나 변경은 방지할 수 없음
오라클, SQL Server 등 많은 데이터베이스의 기본 격리 수준으로 사용된다.
REPEATABLE READ - LV 2
트랜잭션이 처음 읽은 데이터와 다음에 읽은 데이터의 값이 같도록 보장 한다.
- 값의 추가인 팬텀 리드만 발생하고 더티 리드나 넌 리피터블 리드는 방지할 수 있다.
SERIALIZABLE - LV 3
가장 엄격한 수준으로 동시성 문제를 완벽하게 차단 하지만, 트랜잭션을 거의 순서대로 실행 시켜 동시 처리 성능이 가장 낮다.
어떤 격리 수준을 선택해야 할까?
특별한 이유가 없다면 사용자가 사용하는 데이터베이스 시스템의 격리 수준을 따르는 것을 권장한다.
- MySQL: REPEATABLE READ
- Oracle: READ COMMITTED
- PostgreSQL: READ COMMITTED
READ UNCOMMITTED 로 낮추는 것을 고려하는 경우 오염된 데이터를 읽는 행위는 데이터 정합성에 매우 심각한 문제를 일으킬 수 있기 때문에, 실무에서는 이런 개념도 있다는 것만 알아두고 사용할 일은 없다.
MySQL 을 포함한 현대의 데이터베이스들은 다양한 최적화 기법을 제공하기 때문에 READ COMMITTED 와 성능 차이가 거의 나지 않는다.
만약 그럼에도 불구하고 사용한다면, 경향성을 파악하기 위한 대용량 데이터에 대한 실시간 집계나 통계 작업을 수행할 때
- 다른 트랜잭션이 특정 상품의 가격을 수정하는 동안 대기할 필요 없이 바로 접근이 가능하기 때문
READ COMMITTED 로 낮추는 것을 고려하는 경우 성능과 동시성 확보가 매우 중요하고, 약간의 데이터 비일관성을 감수할 수 있는 특정 상황에서 선택할 수 있다.
웹 어플리케이션은 하나의 요청이 하나의 짧은 트랜잭션으로 동작하는 경우가 대부분이기 때문에, 트랜잭션 하나에서 여러 조회 쿼리를 실행할 일이 없기 때문에 동시성 문제가 발생할 경우가 드물다.

