SELECT 중 UPDATE 발생 시 처리 메커니즘
👉 궁금한 부분 : SELECT 중에 계속 UPDATE가 발생해서 생기는 문제나 해결 방안
- 게시글에 조회수 컬럼을 추가하려다가 생긴 궁금증
- 조회(SELECT) 중에 다른 사람이 같은 글을 조회하게 되어서 조회수 UPDATE가 일어나게 되는 경우 어떻게 처리되는지
- 조회수 등을 UPDATE 과정에서 다른 사람이 해당 글을 조회(SELECT)하면 어떻게 되는지
- ⇒ 이 궁금증을 해결하려면 아래의 개념들이 필요
👉 트랜잭션
- 일련의 데이터베이스 작업을 논리적으로 묶은 것으로, ACID(Atomicity, Consistency, Isolation, Durability) 속성을 가짐
- 원자성(Atomicity) : 작업들이 모두 성공하거나 모두 실패함을 보장
- 일관성(Consistency) : 트랜잭션이 성공하면 데이터베이스는 항상 일관된 상태를 유지해야 함(데이터베이스의 모든 규칙을 준수해야 함)
- 격리성(Isolation) : 동시에 실행되는 트랜잭션이 서로에게 영향을 주지 않도록 함
- 지속성(Durability) : 트랜잭션이 성공적으로 완료되면, 그 결과는 영구적으로 저장
👉 트랜잭션의 격리
- 📌 트랜잭션 격리성
- 트랜잭션의 네 가지 특성 중 격리성에 해당하는 개념
- 격리성은 여러 트랜잭션이 동시에 데이터베이스에 접근할 때 서로 간섭하지 않도록 분리하는 능력
- 격리성을 완벽하게 보장하면 동시성(시스템이 많은 작업을 동시에 처리하는 능력)이 저하될 수 있기 때문에, 다양한 '격리 수준(Isolation Levels)'을 통해 필요에 따라 동시성과 격리성 사이의 균형을 조절할 수 있음
- 📌 트랜잭션의 격리 수준 설명
- 트랜잭션 격리 수준은 격리성을 어느 정도까지 보장할지를 결정함
- 격리 수준을 낮추면 동시성이 향상되지만, 여러 문제들(더티 리드, 논리적 판독, 팬텀 리드 등)이 발생할 수 있음
- MySQL/InnoDB에서 기본 격리 수준은 REPEATABLE READ
- 📌 트랜잭션의 격리 수준 종류
- READ UNCOMMITTED (읽기 미확정)
- 가장 낮은 격리 수준으로, 다른 트랜잭션에서 아직 확정되지 않은 변경 내용을 다른 트랜잭션이 읽을 수 있음
- 이로 인해 더티 리드(Dirty Read)가 발생할 수 있음
- 다른 트랜잭션에 의해 수정되었지만 아직 완료되지 않은 데이터를 읽을 수 있음
- READ COMMITTED (읽기 확정)
- 대부분의 데이터베이스 시스템의 기본 격리 수준
- 한 트랜잭션이 커밋을 완료하여 확정된 데이터만 다른 트랜잭션이 읽을 수 있음
- 논리적 판독(Non-Repeatable Read)은 여전히 문제가 될 수 있음
- 즉, 한 트랜잭션 내에서 같은 데이터를 두 번 조회했을 때 다른 결과를 볼 수 있음
- REPEATABLE READ (반복 읽기 가능)
- 트랜잭션이 실행되는 동안 해당 트랜잭션에서 참조하는 데이터베이스 레코드에 대해 다른 트랜잭션이 변경(업데이트, 삭제)을 할 수 없게 하는 격리 수준
- 트랜잭션이 시작되어 처음 읽은 데이터를 트랜잭션이 끝날 때까지 계속 동일하게 읽을 수 있음
- 그러나 '팬텀 리드(Phantom Read)'가 발생할 수 있음
- SERIALIZABLE (직렬화 가능)
- 가장 높은 격리 수준으로, 트랜잭션들이 순서대로 실행되는 것처럼 보장하여 모든 읽기 및 쓰기 문제를 방지
- 동시성이 가장 많이 저하될 수 있음
👉 MVCC(Multi-Version Concurrency Control)
- 설명
- 동시성 제어를 위한 한 방법
- 여러 사용자가 동시에 데이터베이스에 액세스할 때 발생할 수 있는 문제들을 관리
- 한 사용자의 작업이 다른 사용자의 작업에 어떤 영향을 끼칠 수 있는지 고려하고 각 트랜잭션들이 서로 간섭 없이 데이터를 읽고 쓸 수 있게
- MVCC는 이를 위해 데이터의 여러 버전을 만들어서 관리
- 간단히 말해서, 사용자가 데이터를 읽을 때, 그들은 다른 사용자가 동시에 같은 데이터를 변경하고 있을 때에도 데이터의 일관된 스냅샷을 볼 수 있음
- 일관된 스냅샷(Consistent Snapshot)
- 데이터베이스에서 특정 시점의 데이터 상태를 가리키는 용어
- = 데이터베이스 트랜잭션이 시작될 때의 데이터 모습
- 각 트랜잭션은 데이터베이스의 특정 시점의 스냅샷을 보게 되며, 이는 일관된 읽기를 가능하게 하여 트랜잭션이 실행되는 동안 다른 트랜잭션에 의한 변경이 결과에 영향을 미치지 않음
- 예시
- 도서관의 책
- 도서관에서 여러 사람들이 같은 책을 참조할 수 있도록, 도서관은 그 책의 여러 복사본을 가지고 있다고 가정
- 한 사람이 책의 내용을 변경하고 있더라도 (예: 필기를 하고 있더라도), 다른 사람은 원본 책을 참조할 수 있음
- 즉, 한 사람의 작업이 다른 사람에게 방해가 되지 않음
- 시간 여행
- MVCC를 시간 여행에 비유해 볼 수도 있음
- 만약 과거로 돌아가 사진을 찍는다면, 현재에 있는 다른 사람들은 내가 과거에서 무엇을 하든 상관없이 현재의 모습을 계속 볼 것
- 여러분이 과거에서 무언가를 바꾸기 전까지 현재는 바뀌지 않음
- 데이터베이스에서는 이런 식으로 각 트랜잭션은 자신만의 '시간대'를 가지고 작업을 진행할 수 있음
👉 트랜잭션 처리 흐름 예시(REPEATABLE READ인 경우)
- 📌 SELECT(조회) 중 UPDATE가 발생하는 경우
- 상황
- 사용자 A가 게시글의 조회수를 SELECT로 읽고 있을 때, 만약 다른 사용자 B가 동시에 그 게시글을 조회하고, 이로 인해 조회수에 대한 UPDATE가 발생하는 경우
- 흐름
- 사용자 A가 트랜잭션을 시작하고 게시글의 조회수를 읽음
- 동시에 사용자 B가 해당 게시글을 조회하여, 조회수를 증가시키는 UPDATE 쿼리를 실행
- 사용자 A의 트랜잭션이 REPEATABLE READ 격리 수준에서 실행 중이라면, A는 여전히 B가 UPDATE 하기 전의 데이터를 볼 것
- 조회수 증가가 A의 트랜잭션에는 반영되지 않음
- 사용자 A가 트랜잭션을 커밋하거나 롤백할 때까지, A는 조회수에 대한 변경 사항을 볼 수 없음
- A의 트랜잭션이 종료된 후 새로운 SELECT 쿼리 실행 시, B가 증가시킨 조회수를 확인할 수 있음
- = 사용자 A가 트랜잭션을 커밋한 후에 새 트랜잭션을 시작하지 않으면, REPEATABLE READ에서는 여전히 기존의 조회수를 볼 것
- 설명
- REPEATABLE READ에서는 하나의 트랜잭션 내에서의 SELECT 쿼리 결과는 일관성을 유지하지만, 다른 트랜잭션에서의 변경 사항은 새로운 트랜잭션을 시작할 때까지 반영되지 않음
- 이는 동시에 여러 트랜잭션이 데이터를 읽을 때 발생하는 데이터 불일치 문제를 방지하도록 설계된 것
- 📌 UPDATE 중 SELECT(조회)가 발생하는 경우
- 상황
- 누군가가 게시글의 조회수를 UPDATE 하는 중에 다른 사용자가 동시에 해당 게시글을 조회하려 하는 경우
- 흐름
- 사용자 A가 게시글의 조회수를 증가시키기 위한 UPDATE 트랜잭션을 시작
- InnoDB는 해당 행에 대해 X-Lock (배타적 락)을 걸게 됨
- 이는 다른 트랜잭션이 해당 행을 수정하거나 읽지 못하게 함
- 사용자 B가 동시에 해당 게시글을 조회하려고 SELECT 쿼리를 실행하면, REPEATABLE READ 격리 수준에서는 다음과 같은 상황이 발생할 수 있음
- MVCC를 통해 사용자 B는 UPDATE 이전의 데이터, 즉 조회수가 증가되기 이전의 값을 볼 수 있음
- 이는 B의 트랜잭션이 UPDATE 쿼리의 결과를 기다리지 않고, 즉시 스냅샷 데이터를 제공받기 때문
- 만약 격리 수준이 SERIALIZABLE이라면, 사용자 B는 사용자 A의 트랜잭션이 완료될 때까지 기다려야 할 수 있음
- 왜냐하면 이 격리 수준에서는 SELECT 쿼리도 S-Lock (공유 락)을 걸기 때문에 UPDATE 작업이 불가해서 기다려야 함
- 사용자 A의 트랜잭션이 커밋되면, 그 변경 사항은 데이터베이스에 반영되고, 해당 락이 해제
- 이후 사용자 B가 새로운 트랜잭션을 시작하면 변경된 조회수를 볼 수 있게 됨
👉 발생할 수 있는 성능 문제
- Locking Overhead
- 많은 사용자가 동시에 같은 글을 조회할 때, 조회수를 업데이트하기 위한 락이 자주 발생하게 되고, 이는 성능 저하로 이어질 수 있음
- I/O Overhead
- 조회수를 업데이트할 때 마다 디스크 I/O가 발생하게 되며, 이는 데이터베이스의 응답 시간을 늦출 수 있음
- Replication Lag
- 조회수 업데이트가 복제 환경에서 슬레이브에 복제될 때, 복제 지연(replication lag)이 발생할 수 있음
👉 조치 방법
- Buffering Updates
- 애플리케이션 레벨에서 조회수 업데이트를 버퍼링할 수 있음
- 예를 들어, 레디스(Redis)와 같은 인-메모리 데이터 스토어에 카운터를 임시로 저장하고, 주기적으로 배치로 데이터베이스에 업데이트를 할 수 있음
- Asynchronous Updates
- 조회수를 비동기적으로 업데이트하는 방식을 고려할 수 있음
- 사용자의 조회 요청 처리와 별개로, 메시지 큐 등을 통해 조회수 업데이트를 요청하고, 백그라운드에서 이를 처리할 수 있음
- Optimistic Locking
- 낙관적 락을 사용하여, 실제 업데이트가 일어날 때만 락을 거는 방식으로 충돌을 최소화할 수 있음
- Counter Table
- 조회수 전용의 별도 테이블을 만들고, 이 테이블만 빈번히 업데이트하여 메인 테이블의 락 경합을 줄일 수 있음