NOLOCK을 붙이기 전에 알아야 할 것 — 더티 리드보다 무서운 두 가지

MSSQL에서 조회가 느리거나 잠긴다는 이야기가 나오면 가장 먼저 등장하는 처방이 NOLOCK이다. 붙이면 실제로 블로킹이 사라지고 응답이 돌아온다. 그래서 어느새 조회 쿼리 전체에 기본으로 붙어 있는 시스템을 종종 보게 된다.

문제는 이 힌트가 락을 안 걸게 해 주는 게 아니라 격리 수준을 낮추는 것이라는 점이다. 무엇을 포기하는지 알고 쓰는 것과 모르고 쓰는 것의 차이가 크다.

NOLOCK이 실제로 하는 일

select ord_no, amt from orders with (nolock);

-- 아래와 사실상 동일하다
set transaction isolation level read uncommitted;
select ord_no, amt from orders;

즉 공유 락을 걸지 않고, 다른 세션이 잡고 있는 락도 무시하고 읽는다. 기다리지 않으니 빠르고 블로킹도 없다. 대신 읽는 데이터가 확정된 데이터라는 보장이 사라진다.

포기하는 것 세 가지

첫째, 더티 리드. 다른 트랜잭션이 수정했지만 아직 커밋하지 않은 값을 읽는다. 그 트랜잭션이 롤백되면, 방금 읽은 값은 데이터베이스에 존재한 적 없는 값이 된다. 화면에 잠깐 잘못된 금액이 뜨는 정도로 끝나면 다행이지만, 그 값으로 다른 테이블에 집계를 쌓았다면 되돌릴 방법이 없다.

둘째, 같은 행을 두 번 읽는다. 이건 더 알려져 있지 않다. 스캔이 진행되는 동안 다른 세션의 업데이트로 인해 페이지 분할이 일어나면, 이미 읽은 행이 아직 읽지 않은 위치로 이동할 수 있다. 그러면 그 행은 두 번 집계된다.

셋째, 있는 행을 빠뜨린다. 반대 방향의 이동이 일어나면 스캔이 이미 지나간 자리로 행이 옮겨 가서 아예 읽히지 않는다.

뒤의 두 가지가 위험한 이유는 에러가 나지 않기 때문이다. 쿼리는 정상 종료되고 숫자만 틀린다. 그리고 부하가 없는 시간에는 재현되지 않는다.

-- 같은 시점에 두 번 세었는데 값이 다르다
select count(*) from orders with (nolock) where ord_dt >= '20260901';
-- 1,284,559
select count(*) from orders with (nolock) where ord_dt >= '20260901';
-- 1,284,551

그럼 어떻게 하나

대부분의 경우 원하는 것은 “확정된 데이터를 읽되 쓰기 작업을 기다리지 않는 것”이다. NOLOCK은 첫 번째 조건을 버려서 두 번째를 얻는 방식이고, 더 나은 방법이 따로 있다.

RCSI(Read Committed Snapshot Isolation)를 켜면 읽기 작업이 락을 기다리는 대신 버전 저장소에 보관된 커밋된 이전 버전을 읽는다. 블로킹은 사라지면서 더티 리드는 발생하지 않는다.

-- 현재 설정 확인
select name, is_read_committed_snapshot_on, snapshot_isolation_state_desc
from sys.databases
where name = db_name();

-- 켜기 (배타적 접근이 필요하므로 작업 시간대에 수행)
alter database [mydb] set read_committed_snapshot on with rollback immediate;

공짜는 아니다. 행 버전이 tempdb에 쌓이므로 tempdb 용량과 I/O를 먼저 확인해야 하고, 긴 트랜잭션이 많은 시스템에서는 버전 저장소가 커진다. 설정 변경 시점에 다른 연결이 모두 끊기는 점도 고려해야 한다. 그래도 조회 쿼리 수백 개에 NOLOCK을 붙이는 것보다는 관리 가능한 비용이다.

블로킹 자체가 원인인 경우도 많다. 조회가 느려서가 아니라 갱신 쿼리가 필요 이상으로 오래 락을 쥐고 있어서 생기는 문제라면, 그 갱신 쿼리의 인덱스와 트랜잭션 범위를 손보는 게 근본 해결이다. NOLOCK은 그 증상만 가린다.

그래도 NOLOCK이 괜찮은 경우

  • 정확한 수치가 목적이 아닌 대략적인 모니터링 조회
  • 이미 변경이 끝나 더 이상 갱신되지 않는 과거 기간 데이터
  • 개발자가 운영 DB에서 상태를 눈으로 확인하는 일회성 조회

공통점은 그 값이 다른 곳으로 흘러가지 않는다는 것이다. 정산, 집계, 마감, 대사처럼 결과가 다시 저장되거나 판단 근거가 되는 조회에는 쓰지 않는 게 맞다.

정리

NOLOCK은 성능 힌트가 아니라 정확성을 내주고 응답 시간을 사는 거래다. 거래 조건을 알고 하면 합리적인 선택일 수 있지만, 관행처럼 붙여 두면 언젠가 숫자가 안 맞는 날이 오고 그때는 원인을 찾기 대단히 어렵다. 에러 로그에 아무것도 남지 않기 때문이다.

기존 시스템에 NOLOCK이 광범위하게 퍼져 있다면 한 번에 걷어내기는 어렵다. 다만 정산이나 마감처럼 숫자가 중요한 경로부터 먼저 떼어 내고, 신규 쿼리에는 붙이지 않는 것으로 시작할 수는 있다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤