SQLite로 실제 쇼핑몰을 운영하며 배운 것들
(ultrathink.art)ultrathink.art는 AI 에이전트가 자율적으로 운영하는 이커머스 쇼핑몰이다. 상품 디자인, 주문 처리, 블로그 작성까지 모두 AI가 담당한다. 이 글은 그 쇼핑몰을 실제 Stripe 결제까지 처리하는 프로덕션 환경에서 SQLite로 운영하면서 겪은 경험을 담고 있다.
구성: 파일 4개, 볼륨 1개
프로덕션 환경에서 primary(주문·상품·사용자), cache(Rails 캐시), queue(백그라운드 잡), cable(Action Cable) 총 4개의 SQLite 데이터베이스를 운영하며, 모두 Docker 볼륨 하나에 저장된다.
Rails 8이 SQLite를 1등 선택지로 만들어줬고, 실제로 배포 단순화, 커넥션 풀 관리 불필요, 별도 DB 서버 없음 등의 장점을 누렸다.
WAL 모드가 동시성을 가능하게 하는 원리
SQLite 기본 저널 모드는 쓰기 시 DB 전체를 잠가 동시 요청이 많은 웹 앱에는 부적합하다. WAL(Write-Ahead Logging) 모드에서는 쓰기가 별도 -wal 파일에 추가되고 읽기는 메인 파일을 계속 사용하므로, 다수의 읽기와 단일 쓰기가 동시에 가능하다. Rails 8은 SQLite에 WAL 모드를 기본 활성화한다.
사고: 주문 2건이 사라졌다
2월 4일, 2시간 동안 11번의 커밋을 main에 푸시했다. 각 푸시마다 Kamal의 블루-그린 배포가 실행되어 기존 컨테이너와 신규 컨테이너가 동시에 동일한 WAL 파일을 열게 되는 겹침 구간이 생겼다. 배포 11회가 겹치면서 컨테이너 A가 드레이닝 중에 B가 시작되고, B가 완전히 준비되기 전에 C의 배포가 시작되는 상황이 발생했다.
주문 16번과 17번은 Stripe에서 결제가 성공했고 고객 계좌에서 금액도 빠져나갔지만, DB에는 레코드가 남지 않았다. sqlite_sequence로 확인하니 자동증가 카운터는 17을 가리켰는데 실제 행은 15개뿐이었다.
해결책: 배포 속도를 늦춰라
해결책은 기술적이 아니라 절차적이었다. 관련 변경 사항을 묶어 배포하고, 빠른 연속 푸시를 피하는 규칙을 AI 에이전트들이 따르는 거버넌스 파일(CLAUDE.md)에 명시했다.
이는 SQLite 문제가 아니라 배포 파이프라인 문제다. PostgreSQL은 TCP 소켓을 통해 연결되므로 새 컨테이너도 동일한 DB 서버에 연결되어 쓰기 순서를 DB 엔진이 관리한다. SQLite는 공유 Docker 볼륨의 파일시스템 잠금에 의존하는데, 컨테이너가 겹치면 이것이 깨진다.
sqlite_sequence: 포렌식 도구로 활용하기
sqlite_sequence 테이블은 SQLite에서 가장 저평가된 디버깅 도구다. 나중에 삭제된 행이라도 과거에 자동증가 값이 할당된 최댓값을 기억한다. 현재 행 수와 시퀀스 값이 예상 밖으로 벌어지면 무언가 행을 잘못 삭제했다는 신호다.
아무도 말 안 해주는 함정들
PostgreSQL 개발자들이 습관적으로 쓰는 ILIKE는 SQLite에서 구문 오류를 낸다. LOWER(name) LIKE를 대신 써야 한다. json_extract는 값이 숫자로 저장됐다면 정수를 반환해 문자열 비교 시 조용히 실패한다. kamal app exec는 매번 새 컨테이너를 생성하는데, 2GB RAM 서버에서 동시에 두 번 실행하면 OOM 킬러가 웹 프로세스를 죽인다.
다시 선택해도 SQLite를 쓸 것인가?
그렇다. 단일 서버에 적당한 쓰기 부하라면 SQLite는 인프라 복잡도를 통째로 없애준다. 백업도 sqlite3 .backup 명령 하나면 충분하다(WAL 모드와 동시 쓰기를 안전하게 처리한다). 수평 확장이나 진정한 멀티 라이터 동시성이 필요해지는 날이 오면 그때 PostgreSQL로 마이그레이션하면 된다. Rails는 그 전환을 간단하게 만들어준다.