[MSSQL] SELECT 락과 WITH (NOLOCK) 지옥에서 탈출하기: RCSI 설정 가이드
MSSQL 기반 서비스를 운영하다 보면 트래픽이 늘어나는 순간 어김없이 마주치는 문제가 있습니다. 바로 "누군가 데이터를 수정(UPDATE/DELETE)하거나 대량 조회할 때 서비스 전체가 멈추는 SELECT 락(Lock) 현상"입니다.
보통 이를 해결하기 위해 급한 대로 쿼리마다 WITH (NOLOCK)을 덕지덕지 붙이곤 합니다. 하지만 이는 완전한 해결책이 아니며, 오히려 원인 모를 데이터 꼬임 버그를 유발합니다.
왜 이런 문제가 발생하고, MySQL처럼 락 없이 깔끔하게 서비스하려면 어떻게 해야 하는지 정리합니다.
1. 왜 MSSQL은 툭하면 락이 걸릴까?
MSSQL의 기본 격리 수준: 비관적 동시성 제어
MSSQL의 기본 격리 수준(Read Committed)은 기본적으로 "데이터를 수정 중이면 읽지 말고 기다려!" 방식입니다.
- UPDATE 트랜잭션이 걸리면 행(Row)에 배타적 락(X-Lock)이 잡힙니다.
- 뒤이어 들어온 SELECT는 공유 락(S-Lock)을 잡으려다 대기 상태에 빠집니다.
- 요청이 누적되면서 DB 커넥션 풀이 마르고, 서비스 전체가 타임아웃으로 뻗어버립니다.
MySQL(InnoDB)이나 Oracle은 왜 멀쩡할까?
MySQL과 오라클은 기본적으로 MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)를 사용합니다.
- 데이터를 수정할 때 변경 전 원본 데이터를 별도 공간(Undo Log)에 스냅샷으로 남겨둡니다.
- 다른 트랜잭션이 조회할 때는 락을 기다리지 않고 이 수정 전 스냅샷을 읽어갑니다.
- 즉, "읽기는 쓰기를 막지 않고, 쓰기는 읽기를 막지 않는 구조"가 기본 동작입니다.
2. 임시방편 WITH (NOLOCK)의 치명적인 함정
많은 레거시 프로젝트나 개발자들이 락을 피하려고 WITH (NOLOCK)을 습관적으로 사용합니다. 하지만 이는 Read Uncommitted 상태로 조회하는 것이라 심각한 사이드 이펙트가 따릅니다.
- 더티 리드(Dirty Read): 다른 트랜잭션이 작업하다 에러 나서 취소(Rollback)할 쓰레기 데이터를 그대로 읽어 화면에 노출합니다.
- 데이터 누락 및 중복 조회: 인덱스 페이지 분할(Page Split)이 일어나는 찰나에 조회하면, 방금 등록된 행이 목록에서 통째로 빠지거나 같은 행이 2번 카운트되는 유령 버그가 발생합니다.
- 유지보수 피로도: 새로 짜는 모든 SELECT 쿼리에 일일이 NOLOCK을 붙여야 하며, 한 군데라도 빼먹으면 다시 락 병목이 터집니다.
3. 완벽한 해결책: RCSI (Read Committed Snapshot Isolation)
MSSQL에도 MySQL처럼 동작하게 해주는 내장 MVCC 기능이 있습니다. 바로 RCSI(읽기 커밋 스냅샷 격리)입니다.
이 옵션을 켜면 데이터가 수정 중일 때 이전 커밋 버전을 tempdb의 버전 저장소(Version Store)에 임시 보관하며, SELECT는 락 대기 없이 이 스냅샷을 즉시 읽어갑니다. 트랜잭션이 끝나면 스냅샷은 백그라운드에서 자동으로 삭제됩니다.
- 코드 수정 불필요: 쿼리에 WITH (NOLOCK)을 일일이 붙이지 않아도 엔진 레벨에서 락이 걸리지 않습니다.
- 데이터 정합성 보장: 더티 리드나 페이지 스플릿 오류 없이 정상적으로 커밋 완료된 최신 데이터만 안전하게 읽습니다.
4. RCSI 설정 및 원복 명령어
RCSI는 데이터베이스(DB) 단위로 설정합니다. 옵션 변경 시 배타적 잠금이 필요하므로, 기존 세션을 강제로 정리하고 즉시 적용하는 WITH ROLLBACK IMMEDIATE 옵션을 함께 사용합니다.
⚠️ 서비스 커넥션이 일시적으로 끊어지므로 새벽 점검 시간이나 배포 타임에 실행하는 것을 권장합니다.
-- 1. RCSI 활성화 (적용)
ALTER DATABASE [데이터베이스명]
SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;
-- 2. RCSI 비활성화 (기본값 원복)
ALTER DATABASE [데이터베이스명]
SET READ_COMMITTED_SNAPSHOT OFF WITH ROLLBACK IMMEDIATE;
-- 3. 적용 여부 확인 (1 = ON, 0 = OFF)
SELECT name, is_read_committed_snapshot_on
FROM sys.databases
WHERE name = '데이터베이스명';
5. 실무 적용 시 참고사항
- tempdb 여유 공간 확인: 이전 버전 스냅샷을 tempdb에 기록하므로, tempdb 디스크 여유 공간이 충분한지 사전에 점검해야 합니다.
- 기존 소스의 WITH (NOLOCK): RCSI를 켠 후 기존 코드에 남아있는 NOLOCK 때문에 에러가 나지는 않습니다. 다만 NOLOCK이 적힌 쿼리는 RCSI 대신 여전히 비정상 스냅샷을 읽으므로, 결제·정산·재고 등 민감한 비즈니스 로직부터 점진적으로 제거해 주는 것이 좋습니다.