MVCC는 효과적인 동시성 제어를 위해 등장했다. MVCC 이전엔 동시성 제어를 위해 락 기반 동시성 제어 프로토콜인2 Phase-Locking을 사용했다. 락 기반 동시성 제어는 쿼리에 개발자들이 락을 명시하지 않아도 read-lock, write-lock이 자동으로 걸려 SQL 구문을 실행했다. 이러한 락 기반 동시성 제어의 문제점은 조회-쓰기간 잦은 락 경합으로 트랜잭션 처리량이 낮다는 것이다.

조회-쓰기의 락 경합은(베타락, 공유락을 생각해보자) 데이터를 단순히 읽기만 하면되는데 쓰기 작업이 끝날 때 까지 기다려야 하고, 반대로 쓰기작업은 데이터 쓰기만하면 되는데 데이터를 읽는 작업을 기다리는 것이 특징적이다.
이러한 구조에서 서비스의 동시 접속자 수가 늘어난다면 락 경합도 그만큼 늘어날 것이고 처리량이 급격히 낮아질 것이다.
MVCC는 락 기반 동시성 제어보다 처리량을 높이면서 동시성을 제어하기 위해 등장했다. 락 기반 동시성 제어는 트랜잭션 충돌시 같은 데이터를 동시에 조회하는 것을 락으로 방지했다면, MVCC는 같은 데이터를 여러개의 버전으로 나누고 트랜잭션마다 특정 버전을 보여줌으로 일관된 읽기를 보장한다.
MVCC의 멀티 버전은 하나의 레코드에 대해 시간에 따른 여러 개의 버전이 존재함을 의미한다. MySQL InnoDB에선 언두로그를 사용해 MVCC를 구현한다.
언두 로그는 InnoDB에서 DML로 변경되기 이전 버전의 데이터를 별도로 백업하는 로그이다. 언두 로그는 트랜잭션, 격리 수준을 보장하기 위해 사용한다.
트랜잭션 보장 - 트랜잭션이 예외에 의해 롤백되어야 할 상황이라면 데이터를 변경 이전으로 되돌려야한다. 이때, 언두 로그를 사용해 이전 버전의 데이터를 불러와 복구한다.
격리 수준 보장 - 서로 다른 트랜잭션에서 데이터를 변경하는 도중에 데이터를 조회하게 되면 트랜잭션의 격리 수준에 맞게 언두 로그의 데이터를 읽어 반환한다. 이 부분은 아래에 더 설명하겠다.
언두 로그는 MVCC의 버전별 레코드를 저장하여 특정 시점의 스냅샷 역할을 한다. 트랜잭션 롤백에 사용되거나 격리 수준에 따른 읽기 결과를 결정하는데 사용한다.
MVCC는 언두 로그를 사용한다. 트랜잭션에서 레코드의 값 변경 요청이 있을 때 데이터를 변경하기 전의 값을 언두로그에 기록하고 InnoDB 버퍼의 값을 변경한다. 언두로그에 기록된 값은 어떤 트랜잭션도 언두로그의 데이터를 사용하지 않는다면
MVCC는 레코드의 변경 버전을 기록하고 각 트랜잭션마다 읽을 수 있는 범위를 한정한다. 이를 위해 Read View개념이 필요하다.
Read View를 이해하기 위해 m_ids, min_trx_id, max_trx_id, creator_trx_id 4가지 상태 필드를 알아야한다.
Read View를 만들기 위한 준비가 아직 끝나지 않았다. InnoDB의 클러스터드 인덱스에 레코드마다 히든 컬럼이 존재한다. 히든 컬럼을 알아보자.

trx_id < min_trx_id - 해당 레코드를 수정한 트랜잭션은 Read View 생성 이전에 이미 커밋된 트랜잭션이므로 현재 트랜잭션에서 값을 읽을 수 있다.
trx_id ≥ max_trx_id - 해당 레코드를 수정한 트랜잭션은 Read View 생성 이후에 시작된 트랜잭션에 의해 수정되었으므로 현재 트랜잭션에서 읽을 수 없다. (미래의 변경사항을 현재 트랜잭션이 읽을 수 없다.)
min_trx_id ≤ trx_id ≤ max_trx_id - 이 경우는 m_ids 목록을 추가 탐색해야한다. 만약, m_ids에 포함되어 있다면 현재 실행중인 트랜잭션이라는 것을 의미한다. 따라서, 이 버전을 읽을 수 없다.
만약, m_ids에 포함되어 있지 않다면 Read View 생성 시점에 이미 커밋된 트랜잭션이므로 이 버전은 읽을 수 있다.
read committed 환경에서도 MVCC를 사용하여 레코드를 읽는다. 하지만, 격리 환경에 맞춘 데이터 읽기를 지원하기 위해 Read View 생성 과정이 다르다.
REPETABLE READ 격리 레벨에서는 Read View를 트랜잭션이 첫 SELECT 쿼리를 실행할 때 만들고 이후의 SELECT 쿼리는 READ VIEW를 재활용하여 일관된 읽기를 지원한다. 하지만, READ COMMITTED 격리 레벨에서는 SELECT 쿼리를 실행할 때 마다 READ VIEW를 만들어 non repetable read 문제가 발생할 수 있다.
언두 로그는 해당 버전을 참조할 수 있는 트랜잭션이 없을 때 삭제된다. 언두로그는 현재 실행중인 트랜잭션 중에서 가장 오래된 Read View가 언두로그 버전을 볼 수 없다면 아무도 사용할 수 없다. 아무도 사용하지 않는 언두 로그들은 purge 스레드에 의해 삭제당한다.
이러한 Read View, 언두 로그의 특징들을 생각했을 때 트랜잭션은 최대한 짧게 실행되어야한다. 특정 트랜잭션이 긴 시간동안 끝나지 않고 매우 많은 레코드를 수정하고 있다고 생각해보자. 많은 행의 버전이 바뀌었고 다른 트랜잭션들도 데이터를 수정할 것이기 때문에 언두 로그는 점점 더 거대하게 쌓일 것이다. 이렇게 쌓인 언두 로그는 공간적인 비용을 차지하는 것 뿐만이 아닌 조회 성능을 악화시킨다. MVCC에 의해 SELECT 조회 시 알맞은 Read View를 만들기 위해 언두 로그 체인을 타고 타고 내려가며 읽을 수 있는 과거 버전을 찾을 것이기 때문이다.
Consistent Read는 MVCC 기반의 스냅샷을 읽는 것을 말한다. 트랜잭션의 Read View를 바탕으로 트랜잭션마다 볼 수 있는 버전을 만든다. 이러한 방식으로 논리적인 읽기의 일관성을 지킬 수 있다. Repetable Read에서 일반적인 SELECT 문을 실행시키면 MVCC Consistent Read가 적용된다.
Current Read는 현재 시점의 최신 커밋된 데이터를 읽는 것을 의미한다. UPDATE,DELETE와 같은 DML 문은 MVCC의 영향을 받지 않는다. 항상 최신 존재하는 데이터를 기준으로 동작한다. MVCC는 오직 읽기 동시성을 늘리기 위한 목표이며 DML에 영향을 주지 않음을 기억하자.
Consistent Read와 Current Read의 차이를 아는 것은 중요하다. 트랜잭션에서 Consitent Read, Current Read를 혼용해서 사용할 경우 DBMS가 개발자의 의도와 다르게 작동할 수 있기 때문이다.

Consistent Read와 Current Read를 더 알아보기 위해 예제를 만들었다.
REPETABLE READ 레벨에서 트랜잭션을 시작한 후에 첫 번째 SELECT문이 실행되면 InnoDB는 해당 시점의 Read View를 만든다. 만들어진 Read View는 트랜잭션 종료까지 유지되어 이후의 SELECT도 이 스냅샷을 기준으로 데이터를 읽는다. 예제를 보면 tx1에서 INSERT로 두 개의 레코드를 삽입했지만 tx2의 두번쨰 SELECT에서도 여전히 데이터를 읽을 수 없음을 알 수 있다.
하지만, tx2의 UPDATE 문은 이전과 다르게 작동한다. InnoDB에서 UPDATE,DELETE는 Current Read로 실제로 존재하는 최신 커밋된 레코드를 기준으로 동작하고 레코드 락을 획득한다. 그래서, 트랜잭션에서 보이지 않던 데이터여도 다른 트랜잭션이 INSERT를 커밋한 경우 UPDATE 문을 실행시킬 수 있는 것이다.
이렇게 UPDATE로 데이터를 수정하고 SELECT를 실행하면 신기한 현상이 일어난다. Read View에 의해 이전에 안보이던 레코드중 UPDATE한 레코드 하나만 보이기 시작하는 것이다. InnoDB는 트랜잭션이 자기가 직접 수정한 레코드는 Read View와 무관하게 볼 수 있다. 그래서, 새로 삽입한 레코드 두 개중 UPDATE한 레코드만 보이고 나머지 레코드는 보이지 않는 것이다.
이 예제는 InnoDB에서 트랜잭션 내부에 Consitence Read, Current Read를 사용함으로 동시성과 데이터 정합성을 모두 만족하려는 결과이며, REPETABLE READ는 모든 SQL문이 같은 데이터를 바라보는 것이 아닌 Consistent Read를 사용하는 일반적인 SELECT에서만 적용된다는 얘기임을 알 수 있다. 데이터를 변경하는 DML은 Current Read를 사용하여 언제나 최신 커밋된 데이터를 보는 것을 주의해야한다. 만약, REPETABLE READ 레벨의 트랜잭션에서 최신 커밋된 레코드 데이터를 일고 싶은 경우에는 SELECT … FOR ( SHARE, UPDATE) 락을 걸어 레코드 값을 읽을 수 있다.