모든 스마트폰 앱에 숨어 있던 16년 된 버그: SQLite 레이스 컨디션 추적기
2026년 8월, 원격 네트워크 연결 소프트웨어를 개발하는 Tailscale이 기술 회고 보고서를 발표했다. 지난 6개월 동안 이유 없이 데이터베이스가 19번이나 손상되었고, 그때마다 일부 사용자들의 네트워크 연결이 끊어지며 복구에 매번 1시간 이상이 걸렸다는 내용이었다. 엔지니어들이 수개월 동안 추적한 끝에 밝혀낸 원인은, 전 세계에서 가장 많이 설치된 데이터베이스 소프트웨어 속에 최소 16년 동안 숨어 있던 버그였다.
Tailscale은 여러 대의 컴퓨터를 안전하게 사설 네트워크로 연결해 주는 소프트웨어를 만든다. 이 회사를 처음 들어본 사람이라도 이 이야기는 읽어볼 가치가 있다. 이 사건의 주인공이 바로 여러분이 매일 사용하는 스마트폰 안에 들어있기 때문이다.
매일 사용하지만 들어본 적 없는 소프트웨어
먼저 주인공부터 소개하자면, SQLite는 전 세계에서 가장 널리 쓰이는 데이터베이스엔진이다. 공식 발표에 따르면 수십억 대가 넘는 기기에서 작동하고 있다. 여러분이 사용하는 모바일 앱, 웹 브라우저, 게임 등 거의 모든 앱이 데이터를 저장할 때 이 데이터베이스를 사용한다.
SQLite의 가장 큰 특징은 ‘임베디드(내장형)‘라는 점이다. 대형 데이터베이스가 전담 관리자와 별도 서버 건물이 필요한 은행 금고라면, SQLite는 각 앱이 들고 다니는 작은 손가방 금고와 같다. 설치도 필요 없고 관리도 필요 없으며, 실행하기만 하면 곧바로 데이터를 저장할 수 있다. 개발자는 데이터를 넣어두고 수십 년 동안 신경 쓰지 않아도 된다.
이 뛰어난 편의성 덕분에 SQLite는 디지털 세계의 기본 토대가 되었다. 수십억 대의 기기에서 매일 수많은 데이터 조회가 이루어지며 오랜 세월 검증받았기 때문에, 어떠한 오류가 발생할 확률도 극도로 낮아졌다. 심지어 개발자 자신조차 버그가 없을 것이라고 믿을 정도였다.
토대 아래 숨어 있던 오류
Tailscale은 2022년부터 모든 핵심 데이터를 SQLite에 저장해 왔다. “신뢰할 수 있고 널리 쓰이는”, 이른바 ‘지루한 기술(Boring Technology)‘이었기 때문이다. 여기서 ‘지루하다’는 말은 문제가 생기지 않고 안정적이라는 최고의 찬사다. 실제로 2023년 초부터 2025년 여름까지는 아무런 문제도 발생하지 않았다.
그러나 2025년 8월, 백업 파이프라인에서 갑자기 경고가 발생했다. 특정 데이터베이스 파일이 손상되었다는 것이었다. 데이터베이스를 복구하고 조사했지만 원인을 찾지 못했다. 그리고 두 번째, 세 번째 손상이 이어졌고, 6개월 동안 총 19번의 사고가 발생했다. 엔지니어링 관점에서 단일 사고의 발생 확률이 아무리 희박하더라도, 규모가 커지고 트랜잭션 횟수가 충분히 누적되면 그 희귀한 사건도 결국 일상적인 일이 된다.
더욱 괴로운 점은 19번의 사고 사이에 아무런 공통점이 없었다는 사실이다. 동일한 서버도 아니었고, 동일한 사용자 집단도 아니었으며, 특정 시간대나 동일한 부하 형태도 아니었다. 엔지니어들은 재현을 위한 아무런 단서도 얻지 못했다.
”메모지에 먼저 적고, 나중에 장부에 옮긴다”
이후의 추적 과정을 이해하려면 SQLite의 작업 모드 중 하나인 WAL(Write-Ahead Log, 쓰기 전 로그) 방식을 알아야 한다.
장부 기입을 떠올려 보자. 일반 모드는 새로운 거래가 생길 때마다 장부를 직접 펼쳐서 기록하는 방식이다. 반면 WAL 모드는 먼저 메모지 뭉치에 빠르게 적어두었다가, 일정량이 쌓이면 메인 장부에 한 번에 옮겨 적는다. 이 메모지가 ‘WAL 파일’이고, 메인 장부가 진짜 ‘데이터베이스 파일’이다. 그리고 메모지의 내용을 장부에 옮겨 적는 작업을 ‘체크포인트(checkpoint)‘라고 부른다.

그림: 새 데이터는 먼저 WAL 파일에 기록된 후 메인 데이터베이스 파일로 복사된다. 출처: tailscale.com
WAL 모드의 장점은 분명하다. 두꺼운 장부를 매번 수정하는 것보다 메모지에 적는 것이 훨씬 빠르고, 옮겨 적는 작업은 한가한 시간에 비동기로 진행할 수 있다. 또한 장부를 읽는 사람과 메모를 적는 사람이 서로 방해받지 않는다. 성능을 중시하는 많은 앱이 이 모드를 사용한다. Tailscale은 WAL 모드를 사용할 뿐만 아니라, 빠른 백업을 위해 체크포인트를 매우 자주 수동으로 실행하도록 설정했다. 이 설계상의 결정이 이후 모든 사건의 도화선이 되었다.
경합 조건: 두 작업이 충돌한 순간
버그의 핵심 원인을 한 문장으로 요약하자면, 옮겨 적는 작업과 새로운 메모를 적는 작업이 동시에 일어날 때 순서가 꼬인 것이다. 전문 용어로 ‘경합 조건(Race Condition)‘이라고 한다.
구체적인 과정은 다음과 같다. 체크포인트 작업자가 메모지를 확인하고 10장 분량의 데이터를 메인 장부에 옮겨 적기 시작했다. 그런데 적는 도중에 다른 작업이 새로운 메모를 추가하면서 메모지의 번호 카운트를 리셋해 버렸다. 버그의 이름인 ‘WAL-Reset’이 여기에서 유래했다. 체크포인트 작업자는 리셋 사실을 알지 못한 채 이전 번호 기준으로 작업을 계속 진행했다. 그 결과, 아직 옮겨 적지 않은 데이터 페이지를 ‘이미 옮겨 적었음’으로 오인하여 그냥 지나쳐 버렸다.

그림: 체크포인트 프로세스가 WAL의 데이터 페이지를 메인 데이터베이스 파일로 복사한다. 출처: tailscale.com
결국 특정 거래 기록이 흔적도 없이 사라졌다. 더욱 심각한 문제는 메인 장부의 다른 페이지에 지워진 기록을 가리키는 지시자(인덱스)가 여전히 남아 있어서, 데이터베이스 전체가 ‘손상됨’ 상태로 판정되었다는 점이다. Tailscale 엔지니어들은 나중에 로그에서 모순된 숫자를 발견했다. WAL 파일에는 10페이지 분량의 데이터만 존재하는데, 체크포인트 프로세스는 20페이지를 복사했다고 보고한 것이다. 늘어난 10페이지는 이전 번호를 참조하면서 발생한 착시였으며, 이 숫자가 사건 해결의 결정적인 단서가 되었다.
어떻게 16년 동안 숨어 있을 수 있었나
SQLite 공식 개발팀의 추정에 따르면, 이 버그는 최소 16년 전부터 코드베이스에 존재했다. 이렇게 오랫동안 발견되지 않은 이유는 발생 조건이 극도로 까다로웠기 때문이다. 특정 라이브러리 버전의 조합, 체크포인트 처리 중 단 몇 마이크로초 사이의 쓰기 작업 타이밍, 그리고 특정 운영체제의 파일 시스템 동작이 모두 일치해야 했다. 세 가지 조건이 모두 들어맞을 확률은 공식 개발자의 표현을 빌리자면 “일반적인 사용 환경에서는 거의 일어날 수 없는 수준”이었다.
실제로 SQLite 개발자들은 수정 코드가 제대로 작동하는지 검증하기 위해 코드 내부에 특정한 테스트용 로직을 추가하여 의도적으로 충돌 상황을 만들어내야만 했다. 단 하나의 버그를 위해 이런 조치를 취한 것은 SQLite 역사상 처음 있는 일이었다.
대부분의 사용자는 기본 설정을 사용하므로 평생 이 버그를 만날 일이 없다. Tailscale이 19번이나 이 버그를 겪은 이유는 자체적으로 체크포인트를 극도로 자주 실행하도록 만들어 충돌 확률을 엄청나게 높였기 때문이다. ‘극히 드문 사건’이 그들의 환경에서는 ‘언젠가 반드시 일어날 일’로 바뀐 것이었다.
탐정들은 어떻게 사건을 해결했는가
많은 장애 회고 글이 있지만, 이번 보고서의 치밀함과 깊이는 업계에서도 보기 드문 수준이다.
처음 몇 달 동안은 조사가 완전히 막혀 있었다. 자체 코드를 모두 검토해도 오류가 없었고, 재현도 불가능했다. 중간에 6주 동안 아무런 사고가 발생하지 않은 평온한 시기도 있었는데, 이는 사고가 없다고 해서 버그가 고쳐진 것은 아니라는 사실을 보여준다. 이에 팀은 데이터베이스의 모든 변경 명령을 별도의 로그 파일에 기록하기로 결정했다.
이 로그가 상황을 반전시켰다. 이후 발생한 두 번의 사고에서 로그를 재현해 본 결과, 분명히 성공적으로 쓰여진 데이터가 나중에 ‘보이지 않는’ 현상이 확인되었다. 쓰인 데이터가 아무런 오류 메시지도 없이 허공으로 사라진 것이다. 이는 ACID 원칙을 따르는 데이터베이스 시스템에서는 결코 일어날 수 없는 일이었다.
이어서 SQLite 공식 개발진이 합류했다(Tailscale은 이를 위해 유료 전문 지원 서비스를 구매했다). 양측은 함께 tmstmpvfs shim이라는 전용 디버깅 도구를 개발했다(shim은 기존 모듈 겉면을 감싸는 모니터링 껍데기를 의미한다). SQLite의 내부 구조는 크게 3개 레이어로 나누어진다. 최상단은 SQL 명령어를 해석하고, 중간 레이어는 데이터를 블록 단위로 나누며, 최하단은 실제로 디스크에 데이터를 기록한다. 이 최하단 레이어를 VFS(Virtual File System, 가상 파일 시스템)라고 부른다. 모니터링 shim을 VFS 레이어 겉면에 씌워 디스크에서 일어나는 모든 입출력 동작을 상세히 기록했다.

그림: SQLite의 최하위 스토리지 인터페이스에 모니터링 레이어(shim)가 씌워져 있다. 출처: tailscale.com
모니터링 장치를 설치한 후 다음 사고를 기다렸다. 그리 오래 걸리지 않아 사고가 재발했고, 전체 작업 기록이 SQLite 핵심 개발자에게 전달되었다. 그리고 체크포인트가 진행되는 중간에 쓰기 트랜잭션이 WAL을 리셋했고 체크포인트가 이를 감지하지 못했다는 경합 조건의 현장이 마침내 적발되었다.
비슷한 시기에 다른 쪽에서도 결정적인 증거가 나왔다. 소프트웨어 테스트 전문 업체인 Antithesis가 ‘속성 기반 테스트(property-based testing)’ 기법을 사용하여 이 버그를 독립적으로 재현하는 데 성공한 것이다. 수많은 무작위 동시 쓰기 및 동시 체크포인트 작업 시퀀스를 자동 생성하면서 “커밋된 쓰기는 절대로 손실되면 안 된다”, “데이터베이스는 절대로 손상되면 안 된다”라는 두 가지 절대 규칙을 검증했다. 테스트 도구는 이전 버전에서 규칙 위반을 찾아냈고, 패치된 새 버전에서는 정상 작동함을 확인했다. 사람이 생각하기 힘든 복잡한 작업 순서를 기계가 스스로 찾아낸 것이다.
수정, 그리고 한 번의 해프닝
버그 수정 자체는 단 한 곳의 코드 변경으로 끝났다. 체크포인트 함수 내부에서 WAL 상태가 다른 스레드에 의해 리셋되었는지 확인하는 검사를 추가하고, 리셋이 감지되면 체크포인트를 안전하게 중단하도록 만든 것이다. 이 수정 사항은 SQLite 3.51.3 버전으로 정식 출시되었다.
하지만 이야기는 여기서 끝나지 않았다. Tailscale이 수정 버전을 적용하자마자 백업 모니터링 시스템 전체에 손상 경고가 빨갛게 들어왔다. 다행히 이는 해프닝이었다. 이전 버전에서 생성되었던 인덱스의 미세한 불일치가 새 버전의 성능 최적화 로직과 부딪히면서 겉으로 드러난 것이었다. SQLite 공식 팀은 해당 버전을 즉시 철회하고 WAL-Reset 수정 사항만 포함된 버전을 다시 출시했다. Tailscale 또한 데이터 저장 방식을 변경하여 이 문제를 우회했다. 하나의 버그를 고치면 또 다른 문제가 드러나는 연쇄 반응은 소프트웨어 공학에서 흔히 발생하며, ‘수정완료’가 결코 쉬운 끝이 아님을 보여준다.
마지막으로 가장 중요한 검증 단계가 남아 있었다. 버그가 실제 운영 환경에서 정말로 해결되었는지 확인하기 위해, Tailscale은 ‘쓰기’ 동작과 ‘WAL 리셋’ 동작이 충돌할 때마다 기록을 남기는 경고 시스템을 배포했다. 적용 후 두 달 동안은 조용했다. 그러나 두 달 뒤 경고음이 울렸다. 16년 동안 숨어 있던 경합 조건이 실제 운영 환경에서 다시 발생했지만, 이번에는 수정 코드가 이를 성공적으로 차단했음이 증명된 순간이었다. 이후 4개월 동안 데이터베이스 손상 사고는 단 한 건도 발생하지 않았다.
가장 신뢰받는 대상일수록 가장 엄격하게 검증해야 한다
이 사건은 소프트웨어 엔지니어링 분야에 깊은 시사점을 준다. SQLite는 전 세계에서 가장 높은 신뢰를 받는 소프트웨어 중 하나이며, 어떤 회사도 프로젝트를 시작할 때 SQLite의 안정성을 의심하지 않는다. 그러나 바로 이러한 ‘무조건적인 신뢰’ 때문에 오류가 16년 동안 숨어 있을 수 있었고 수십억 대의 기기에 영향을 미칠 수 있었다. 가장 밑바닥의 토대에 오류가 생기면 그 위에 쌓아 올린 모든 시스템이 함께 흔들리며, 아무도 토대 자체를 의심할 생각을 하지 않는다.
이는 엔지니어들에게 중요한 교훈을 준다. 의존도가 높은 부품일수록 가장 까다롭고 엄격한 방법으로 검증해야 한다는 사실이다. 이번 사건에서 Tailscale이 보인 솔직하고 책임감 있는 대응은 업계 동료들의 큰 찬사를 받았다. 커뮤니티 댓글에서 가장 많이 등장한 단어는 ‘신선하다(refreshing)‘였다. 이들은 남에게 책임을 전가하지 않고, SQLite 공식 전문 지원 서비스를 구매했으며, 오픈소스 디버깅 도구 개발에 자금을 지원하여 전 세계 개발자들이 향후 유사한 문제를 해결할 수 있도록 기여했다.
자사의 장애를 해결하기 위해 오픈소스 프로젝트에 전용 디버깅 도구 개발 비용을 지원하는 기업은 상업 세계에서 그리 흔치 않다. 일반 사용자 관점에서도 이 사건은 특별한 의미를 갖는다. 여러분의 스마트폰 속에서 아무런 소리도 없이 묵묵히 작동하는 하위 소프트웨어 뒤에는, 거의 일어날 것 같지 않은 오류 단 하나를 없애기 위해 6개월 동안 집요하게 추적한 수많은 엔지니어들이 서 있다는 사실이다.
16년 된 버그가 마침내 해결되었다. 그렇다면 다음 버그는 어디에 숨어 있을까? 어쩌면 지금 여러분이 가장 신뢰하는 스마트폰 앱 속에서 조용히 때를 기다리고 있을지도 모른다.
참고 링크:
- Tailscale: SQLite WAL-Reset Bug 회고
- Antithesis: Breaking the WAL
- HN 토론 (item?id=49272832)