데이터베이스 면접에서 트랜잭션 격리 수준은 거의 단골로 나온다. 처음 보면 이름이 조금 딱딱하다. READ COMMITTED, REPEATABLE READ, SERIALIZABLE 같은 용어가 먼저 나오기 때문이다. 그런데 핵심은 단순하다. 여러 트랜잭션이 동시에 같은 데이터를 읽고 쓸 때, 서로의 작업을 어디까지 보이게 할 것인지 정하는 기준이다.
트랜잭션은 보통 ACID로 설명한다. 그중 격리성은 동시에 실행되는 작업들이 서로를 망가뜨리지 않게 만드는 성질이다. 문제는 격리를 강하게 걸수록 안전하지만 느려질 수 있고, 느슨하게 걸수록 빠르지만 이상한 읽기가 생길 수 있다는 점이다. 격리 수준은 이 둘 사이의 선택지라고 보면 된다.
동시에 읽고 쓰면 무슨 일이 생길까
예를 들어 어떤 사용자의 잔액이 10만 원이라고 해보자. A 트랜잭션은 잔액을 읽고 송금을 처리하고 있다. 그런데 그 사이에 B 트랜잭션도 같은 잔액을 읽고 다른 결제를 처리한다. 둘 다 처음 읽은 10만 원만 믿고 계산하면, 마지막에 저장되는 값이 의도와 달라질 수 있다.
읽기에서도 비슷한 문제가 생긴다. 어떤 트랜잭션이 아직 커밋하지 않은 값을 다른 트랜잭션이 읽을 수도 있고, 같은 트랜잭션 안에서 같은 쿼리를 두 번 실행했는데 결과가 달라질 수도 있다. 조건에 맞는 행 목록을 다시 조회했더니 처음에는 없던 행이 갑자기 끼어드는 경우도 있다.
이런 현상들을 보통 더티 리드, 반복 불가능 읽기, 팬텀 리드라고 부른다. 이름을 외우는 것도 필요하지만, 면접에서는 이름보다 “어떤 상황에서 왜 문제가 되는지”를 설명할 수 있어야 한다.
READ COMMITTED는 커밋된 것만 읽는다
READ COMMITTED는 말 그대로 커밋된 데이터만 읽게 하는 수준이다. 아직 커밋되지 않은 변경 사항은 다른 트랜잭션에서 보이지 않는다. 그래서 더티 리드는 막을 수 있다.
다만 같은 트랜잭션 안에서 같은 데이터를 다시 읽었을 때 값이 달라질 수 있다. 첫 번째 조회 이후 다른 트랜잭션이 값을 바꾸고 커밋했다면, 두 번째 조회에서는 그 바뀐 값이 보일 수 있기 때문이다. 많은 시스템에서는 이 정도를 현실적인 기본값으로 받아들인다. 매번 최신 커밋 데이터를 보는 편이 자연스러운 업무도 많다.
PostgreSQL의 기본 격리 수준도 READ COMMITTED다. 그래서 PostgreSQL 기준으로 설명한다면, “커밋된 데이터만 읽지만 같은 트랜잭션 안에서 반복 조회 결과가 달라질 수 있다” 정도로 잡으면 된다.
REPEATABLE READ는 같은 트랜잭션 안의 시야를 고정한다
REPEATABLE READ는 이름처럼 같은 트랜잭션 안에서 같은 데이터를 반복해서 읽어도 같은 결과를 보장하려는 수준이다. 트랜잭션이 시작된 시점의 일관된 스냅샷을 보고 작업한다고 이해하면 편하다.
이 수준에서는 중간에 다른 트랜잭션이 데이터를 바꾸고 커밋하더라도, 이미 시작된 트랜잭션 입장에서는 그 변경이 보이지 않을 수 있다. 그래서 반복 불가능 읽기를 막을 수 있다.
MySQL InnoDB의 기본 격리 수준은 REPEATABLE READ다. 다만 MySQL은 MVCC와 next-key lock 같은 구현 방식이 섞여 있어서, 단순히 표준 격리 수준 이름만으로 모든 동작을 설명하기는 어렵다. 면접에서는 이 부분을 너무 깊게 외우기보다, “MySQL은 기본이 REPEATABLE READ이고, 스냅샷 읽기와 락을 조합해서 일관성을 맞춘다” 정도로 말하면 충분한 경우가 많다.
SERIALIZABLE은 가장 안전하지만 비용이 크다
SERIALIZABLE은 여러 트랜잭션이 동시에 실행되더라도 결과만큼은 마치 하나씩 순서대로 실행한 것처럼 만들려는 수준이다. 가장 강한 격리 수준이다.
안전한 만큼 비용도 있다. 동시에 처리할 수 있는 작업이 줄어들고, 락 대기나 재시도 가능성이 늘어난다. 그래서 모든 요청에 무조건 SERIALIZABLE을 거는 방식은 현실적이지 않은 경우가 많다. 정말로 순서와 정합성이 중요한 부분에 제한적으로 쓰거나, 애플리케이션 레벨의 재시도와 함께 설계한다.
예를 들어 쿠폰 선착순 발급, 재고 차감, 금융성 정산처럼 중복 처리나 초과 차감이 치명적인 영역에서는 강한 제어가 필요하다. 반대로 단순 조회나 통계성 화면에서는 높은 격리 수준보다 처리량과 지연 시간이 더 중요할 수 있다.
격리 수준은 정답보다 선택의 문제다
트랜잭션 격리 수준을 공부할 때 자주 빠지는 함정은 “무조건 높은 수준이 좋은 것 아닌가?”라고 생각하는 것이다. 실제 시스템에서는 그렇지 않다. 높은 격리는 더 안전하지만, 동시성을 줄이고 지연 시간을 늘릴 수 있다. 낮은 격리는 빠르지만, 애플리케이션이 감당해야 하는 일관성 문제가 생긴다.
그래서 면접에서 좋은 답변은 격리 수준 이름을 나열하는 답이 아니다. 어떤 이상 현상을 막고 싶은지, 그 대신 어떤 성능 비용을 받아들일 수 있는지, DB 기본값과 애플리케이션 로직을 어떻게 함께 설계할 것인지까지 이어져야 한다.
예를 들어 “결제 승인 결과를 정산 테이블에 반영하는 작업이라면 같은 요청이 두 번 처리되지 않게 멱등키를 두고, 필요한 구간에서는 락이나 높은 격리 수준을 검토하겠다”처럼 말할 수 있다. 반면 “상품 목록 조회처럼 약간의 최신성 차이가 허용되는 곳은 기본 격리 수준에서 처리하고 캐시를 함께 본다”고 말하면 현실적인 답변이 된다.
면접에서는 이렇게 정리하면 좋다
트랜잭션 격리 수준은 동시에 실행되는 트랜잭션 사이에서 읽기와 쓰기의 가시성을 조절하는 장치다. 낮은 격리 수준은 동시성에 유리하지만 더티 리드, 반복 불가능 읽기, 팬텀 리드 같은 문제가 생길 수 있다. 높은 격리 수준은 이런 문제를 줄이지만 락 경합과 재시도 비용이 커질 수 있다.
실무적으로는 DB의 기본 격리 수준을 먼저 이해하고, 모든 작업을 강하게 묶기보다 정합성이 중요한 구간을 좁게 잡는 편이 좋다. 결국 중요한 건 격리 수준 자체를 외우는 것이 아니라, 이 데이터가 틀어졌을 때 어떤 문제가 생기는지부터 판단하는 것이다.