본문 바로가기

개발이야기

SQLite 백업 자동화, launchd로 매일 04:10 실행한 katsubox 점검 기록

SQLite 백업 자동화, launchd로 매일 04:10 실행한 katsubox 점검 기록

  • 앨범, 공유 링크, 페이지, 댓글 메타데이터가 katsubox.db 한 파일에만 있어 이 파일을 잃으면 되돌릴 방법이 없습니다.
  • 앱 안에는 스케줄러를 두지 않고, Mac의 launchd가 매일 04:10에 백업 API를 호출하면 SQLite 온라인 백업이 일관된 스냅샷을 만듭니다.
  • 설치 후 6일 동안 정기 백업이 5번 돌았고 10월 23일만 빠졌는데, 그 시간에 Mac이 꺼져 있었던 것을 기록으로 확인했습니다.

katsubee와 katsubox를 소개한 글에서 개선점으로 남겨 둔 항목 가운데 katsubox의 DB 백업을 다룹니다. 시리즈로 치면 네 번째 글입니다.

katsubox.db 하나에 몰린 메타데이터

katsubox는 업로드된 파일을 데이터 폴더에 그대로 쌓는 구조입니다. 폴더 계층이 곧 정보여서, 파일만 놓고 보면 별도 백업이 필요 없어 보입니다.

걸리는 곳은 같은 위치에 놓인 katsubox.db입니다. 앨범 구성, 공유 링크, 페이지의 제목과 설명, 방문자가 남긴 댓글, 첨부, 파일 메모가 전부 이 파일 안에 있습니다. 실수로 지우거나 망가뜨리면 파일은 그대로 남아도 앨범과 공유 링크와 댓글은 돌아오지 않습니다.

데이터 폴더에는 오래된 파일을 지우는 정리 스크립트(prune-old.sh)도 돕니다. 폴더 전체를 믿고 맡길 수 있는 곳이 아니라서, DB는 따로 지킬 방법이 필요했습니다.

백업 호출을 launchd에 맡긴 설계

제일 먼저 정할 것은 백업을 누가 시작하느냐였습니다. 앱 안에 스케줄러를 넣는 선택지도 있었지만, katsubox는 컨테이너 하나로 단순하게 운영하는 서비스라 채택하지 않았습니다. 앱은 POST /api/maintenance/backup-db 엔드포인트 하나만 열고, 호출 시각은 Mac의 launchd가 정합니다.

백업 스크립트(backup-db.sh)는 DB 파일에 손대지 않습니다. 컨테이너 밖에서 파일을 직접 복사하면 앱 바깥에서 쓰는 경로가 하나 더 생기고, 그만큼 경로 동기화가 어긋날 수 있기 때문입니다. 스크립트가 하는 일은 curl로 API를 부르는 것까지입니다.

비밀번호는 launchd 환경에 변수가 없으므로 프로젝트의 .env에서 읽어 옵니다. plist 파일에는 비밀번호를 적지 않았습니다.

WAL 모드에서 온라인 백업을 쓰는 이유

스냅샷은 sqlite3의 온라인 백업 API로 만듭니다. DB가 WAL 모드로 동작하는 중에 파일을 그냥 복사하면, 쓰기가 한창인 순간의 상태가 섞여 들어갈 위험이 있습니다. 온라인 백업은 이런 상황에서도 일관된 복사본을 보장합니다.

만들어진 스냅샷은 데이터 폴더 아래 .backups에 타임스탬프 이름으로 쌓입니다. 보관 개수는 기본 10개이고, KATSUBOX_DB_BACKUP_KEEP 환경변수로 조절합니다. 리브랜딩 이전 이름인 katsudrop으로 만든 스냅샷도 같은 로테이션에 포함해 셉니다.

매일 04:10 실행과 정리 전 스냅샷

launchd 설정은 매일 04:10에 스크립트를 실행하게 되어 있습니다. 대시보드의 지금 백업 버튼도 동일한 API를 호출하므로 수동 실행과 자동 실행이 한 경로를 공유합니다.

같은 스냅샷 기능을 DB 정리에도 씁니다. 정리 기능은 디스크에서 사라진 파일에 딸린 낡은 DB 행, 즉 메모와 댓글과 첨부와 앨범 연결을 지웁니다. 기본 동작은 미리보기이고, 실제로 지울 때는 먼저 DB를 스냅샷합니다.

저장소 폴더가 비어 있으면 모든 행이 낡은 것으로 보이는 문제도 있습니다. 그래서 비어 있는 저장소는 건너뜁니다. 마운트가 풀린 날 정리 버튼을 눌러도 메타데이터가 통째로 사라지지 않게 하려는 장치입니다.

로그와 폴더로 확인한 실제 실행 결과

주간 보고에는 이 자동화가 실제로 도는지 확인하지 못했다고 적혀 있었습니다. 가장 최근 백업 시각이 10월 20일 00:08이었는데, 매일 04:10이라는 설정과 맞지 않았기 때문입니다. 이번에 .backups 폴더와 로그를 직접 열어 확인했습니다.

자동으로 돌고 있었습니다. 파일 이름에 찍힌 시각이 UTC라서 04:10 KST가 19:10으로 보였을 뿐입니다. 파일 수정 시각을 보면 10월 20일부터 25일까지 매일 04:10 KST에 만들어졌습니다. 로그에는 성공 응답이 다섯 줄 남아 있고 오류 로그는 비어 있습니다. 10월 20일 00:08의 파일은 정기 실행이 아닌 수동 1회로 보입니다.

10월 20일부터 25일까지 일별 정기 백업 성공 횟수. 23일만 0회다.

10월 23일 공백과 백업 크기 변화

6일 중 10월 23일 04:10 백업만 없습니다. 성공 로그도 오류 로그도 남지 않았고, 원인은 Mac의 전원이었습니다. 시스템 재부팅 기록을 보면 10월 23일 02:36에 종료되어 같은 날 20:38에 다시 켜졌습니다. 그 사이에 걸린 04:10 작업은 실행될 방법이 없었습니다.

같은 새벽의 다른 작업도 함께 빠졌습니다. 03:30의 RAG 로우데이터 수집과 04:00의 일반 질의 수집 파일 역시 10월 23일 하루만 없습니다. 서로 다른 서비스의 작업 세 개가 같은 날 사라진 것도 이 종료로 설명됩니다. 종료된 이유 자체는 기록에서 확인하지 못했습니다. 문서의 한계 항목에는 Mac이 꺼져 있거나 로그아웃 상태면 돌지 않는다고 이미 적어 두었는데, 그 상황이 실제로 벌어진 셈입니다.

정기 백업 파일의 크기는 10월 20일 88KB에서 21일부터 168KB로 커졌습니다. 같은 주에 페이지, 댓글, 공유 링크 관련 테이블에 데이터가 쌓이기 시작한 시기와 겹칩니다. 다만 커진 이유를 파일 내용으로 열어 확인하지는 않았습니다.

정기 백업 파일 크기. 20일 88KB에서 21일부터 168KB로 늘었다.
보관 중인 스냅샷은 7개이고 상한은 10개다.

지금 폴더에는 스냅샷이 7개 있습니다. 정기 5개, 수동 1개, 리브랜딩 전 katsudrop 이름의 1개입니다. 상한 10개까지 여유가 있어 오래된 스냅샷을 지우는 로테이션은 아직 한 번도 작동하지 않았습니다. 하루 한 번 기준으로 약 열흘치를 들고 있는 셈이라, 더 오래전 시점으로 돌아가야 하는 사고에는 모자랍니다.

복원 절차는 아직 문서에 없다

백업은 복원할 수 있어야 값을 합니다. 저장소 문서에는 대시보드에서 백업을 만드는 방법과 launchd 설치 방법이 있지만, 스냅샷으로 DB를 되돌리는 절차는 없습니다. 온라인 백업으로 만든 파일이니 앱을 멈추고 DB 자리에 놓으면 될 것으로 짐작합니다. 실제로 해 본 적은 없습니다.

복원 리허설을 거치지 않은 백업은 가정에 가깝습니다. 그래서 복원을 한 번 직접 해 보고 그 순서를 문서로 남기는 일을 개선 목록 맨 앞에 올렸습니다.

남은 개선점 두 가지

현재 구조에는 약점이 둘 있습니다. 하나는 놓친 백업을 알아채거나 메울 방법이 없다는 점입니다. 10월 23일의 공백도 파일 목록을 열어 보고서야 알았습니다. launchd는 꺼져 있던 동안 지나간 실행 시각을 나중에 보충해 주지 않습니다. 부팅할 때 한 번 더 실행하는 RunAtLoad 설정을 추가하는 방법을 검토할 예정입니다. 최근 백업 시각은 대시보드 응답에 이미 들어 있으므로, 그 시각이 하루 넘게 지났을 때 알림을 보내는 것이 가장 작은 수정이 됩니다.

다른 하나는 백업이 원본과 같은 Mac의 같은 디스크에 있다는 점입니다. DB 파일을 실수로 지우거나 망가뜨리는 사고에는 대비가 되지만 디스크 고장에는 대비가 되지 않습니다. 데이터 폴더가 이미 Parallels 공유 폴더와 FTP 경로를 거치므로, 스냅샷을 다른 위치에 한 벌 더 복사하는 방법을 검토할 예정입니다. 복원을 실제로 해 본 기록이 없다는 점도 같은 점검 목록에 함께 올려 두었습니다.