10분마다 바뀌는 자동 생성 파일이 Obsidian에서 충돌을 만들 때 — hidden 파일로 옮긴 이유
환경: Obsidian Self-hosted LiveSync + CouchDB 3.3 · livesync-bridge(Deno) · 동기화 기기 5대(현재 기준, 사건 당시엔 4대)
TL;DR / 빠른 해결
자동으로 계속 갱신되는 파일을 볼트 안에 평범한 이름으로 두지 마라. 기기마다 그 파일을 자기 버전으로 들고 있게 되고, Obsidian이 동기화 충돌로 처리한다.
.으로 시작하는 hidden 파일로 두면 조용해진다. 브리지는 dotfile을 동기화하지만 Obsidian 클라이언트는 hidden 파일을 기본적으로 무시하기 때문이다.- 단 이건 클라이언트의 "hidden 파일 동기화" 옵션이 꺼져 있을 때(기본값) 성립한다. 켜두면 충돌이 그대로 돌아온다.
_로 시작하는 이름은 피하는 게 안전하다. 기준은 파일 이름이 아니라 볼트 루트 기준 경로의 첫 글자다. 그게_면 깨지고, 깨지는 방식은 "안 올라감"이 아니라 다른 이름으로 올라가는 것이다 — 앱에서 만든 경우엔 문서가 둘로 갈라지기까지 했다(실측)._폴더/파일.md도 같은 이유로 걸리고,0_Dashboard/_메모.md처럼 경로가 다른 글자로 시작하면 해당 없다.
어쩌다 이런 파일이 생겼나
이 사건 자체는 지금 다시 실행해 재현한 게 아니라, 그때 남긴 운영기록을 바탕으로 내가 다시 정리한 것이다. 다만 아래에 나오는 이름 규칙은 이번에 사용자와 함께 따로 확인했다. 시리즈 전체의 배경은 노션 대신 Obsidian 자가호스팅에 정리해뒀다.
동기화가 에러 없이 한쪽 방향만 멎는 사고가 있었고, 그걸 자동으로 잡으려고 하트비트 감시를 붙였다. 정본 기기가 10분마다 볼트 파일에 현재 시각을 쓰고, 감시 기기가 그 시각이 낡았는지 보는 방식이다.
즉 볼트 안에 10분마다 내용이 바뀌는 파일이 하나 생긴다. 이게 이 글의 사건 전부다.
세 이름을 거쳤다
1차: _pull_heartbeat.md — 사실은 멀쩡히 동기화됐다.
앞에 밑줄을 붙인 건 "시스템 파일이니 눈에 안 띄게" 정도의 의도였다. 설치 직후 확인했을 때 이 파일은 정상적으로 흘러갔다 — 정본 기기에서 쓴 시각이 감시 기기까지 도착했고, 사용자는 자기 Obsidian 화면에서 그 파일을 보고 있었다.
그런데 나는 이 이름을 스스로 갈아엎었다. 볼트에 정리해둔 규칙에 "_로 시작하는 파일은 CouchDB가 문서 ID를 예약해서 안 된다"가 있었고, 내가 방금 그 규칙을 어긴 걸 뒤늦게 발견했기 때문이다. 동작은 하고 있었지만 클라이언트 쪽에서 문제가 생길 수 있다고 보고 예방적으로 바꿨다.
이 글을 쓰면서 서버를 직접 열어보니, 그 걱정 자체가 근거가 없었다. CouchDB에 3_resources/system/_pull_heartbeat.md 문서가 리비전 3까지 찍힌 채로 남아 있다. 이 볼트의 문서 ID는 경로를 소문자로 바꾼 것이라(0_dashboard/dashboard.md 같은 식) 이 파일의 ID는 3_resources/…로 시작한다. 밑줄로 시작하지 않으니 예약 규칙에 걸릴 자리가 아니었다.
규칙 자체는 버릴 게 아니다. 이 글을 쓰면서 최상단에 _ 파일을 양쪽에서 한 번씩 만들어보고, 결과는 내가 읽었다. 서버 파일시스템에 _ 파일을 만드는 건 내가, 폰 앱에서 만드는 건 사용자가 했고, 그 결과 문서가 어떤 ID로 들어갔는지는 내가 CouchDB를 열어 읽었다.
결과는 이렇다. 최상단에서는 진짜로 깨진다 — 다만 안 올라가는 게 아니라 이름이 갈라진다.
서버 파일시스템에 만든 _synctest.md는 브리지가 밑줄을 떼고 synctest.md라는 ID로 올렸다. 앱에서 만든 쪽은 방향이 반대였다. 앱은 /_이름이라는 ID로 올리고(예약을 피하는 형태다), 서버 쪽 브리지는 그 문서를 파일로 쓸 때 밑줄을 떼어 저장한 다음 그 파일을 새 문서로 되올린다. 파일 하나가 문서 둘이 된다.
그러니 규칙에서 빠져 있던 건 경로 조건이다. 판정하는 건 파일 이름이 아니라 볼트 루트 기준 경로의 첫 글자다. _pull_heartbeat.md의 경로는 3_Resources/…로 시작했고, 그래서 애초에 대상이 아니었다. 반대로 최상단에 밑줄로 시작하는 폴더를 두면 그 폴더 이름에서도 밑줄이 떨어져, 그 안의 파일이 통째로 딸려간다 — 이것도 이번에 내가 만들어서 확인했다. 그리고 나는 그 조건을 확인하지 않은 채 겪지도 않은 함정을 겪은 것처럼 기록에 남겼다. 이 시리즈의 CouchDB 편에도 그 서술이 실렸다가 이번에 같이 고쳤다.
2차: pull_heartbeat.md — 여기서 원인을 오해했다.
진짜 문제는 이름과 상관없었다. 데스크탑과 폰의 Obsidian에서 동기화 충돌이 떴고, 사용자가 어느 버전을 남길지 직접 골라야 했다. 한동안 꺼져 있던 기기가 다시 붙는 순간이었다. 밑줄이 붙어 있던 1차 이름에서도 같은 일이 났다 — 충돌을 만든 건 이름이 아니라 그 파일이 볼트에 보인다는 것이었다.
원리는 단순하다. 이 파일은 10분마다 새 내용으로 바뀐다. 그런데 기기가 여럿이고, 그중에는 한동안 꺼져 있다가 켜지는 것도 있다. 다시 붙는 순간 그 기기가 들고 있던 옛 버전과 서버의 새 버전이 갈리고, 자동 갱신 파일에서는 이 상황이 계속 새로 만들어진다. 사람이 손대지 않아도 충돌 후보가 10분마다 새로 생기는 셈이다.
게다가 이 파일은 볼트에 그냥 보인다. 사용자 입장에서는 자기가 만들지도 않은 파일이 파일 목록에 있고, 그게 계속 충돌을 낸다.
3차: .pull_heartbeat.md — 조용해졌다.
점으로 시작하는 hidden 파일로 바꿨다. 두 가지가 맞아떨어져서 성립한다.
- 브리지는 dotfile을 동기화한다. 이건 실제로 흘려보내서 확인했다. 그래서 감시 기능 자체는 그대로 동작한다.
- Obsidian 클라이언트는 hidden 파일을 기본적으로 무시한다. 그래서 데스크탑·모바일의 파일 목록에 안 보이고, 클라이언트가 그 파일의 버전을 따지지 않으니 충돌도 안 난다.
즉 동기화 계층에서는 흐르고 앱 계층에서는 안 보이는 자리를 찾은 것이다.
전제를 분명히 해둔다
이 해법에는 조건이 붙는다. 클라이언트의 "hidden 파일 동기화" 설정이 꺼져 있어야 한다. 기본값이 꺼짐이라 대부분은 그냥 성립하지만, 켜두면 앱이 dotfile까지 자기 관리 대상으로 삼으므로 충돌이 그대로 돌아온다.
그리고 이건 "숨겼으니 됐다"가 아니다. 파일은 여전히 존재하고 동기화도 되고 있다. 앱이 그걸 자기 문서로 취급하지 않을 뿐이다.
정리하면서 밟은 것
이름을 바꿨으니 옛 파일과 그때까지 쌓인 충돌 사본을 치워야 했다. 데스크탑 Obsidian에 남아 있던 것들은 사용자가 앱에서 직접 지웠다.
그때 나는 "삭제는 클라이언트에서 하는 게 안전하다"고 알고 있었고 그렇게 안내했다. 이 판단은 나중에 뒤집힌다 — 삭제가 유실되는 쪽이 오히려 클라이언트였고, 확실한 건 서버 파일시스템 쪽이었다. 그 얘기는 이 시리즈 뒤쪽에서 따로 다루고, 여기서는 그때 그렇게 했다는 사실만 적어둔다.
교훈 / 체크리스트
- 동기화되는 폴더 안에 "계속 바뀌는 파일"을 두면 충돌 공장이 된다. 사람이 편집하지 않아도 기기 수만큼 버전이 갈리고, 갱신 주기마다 그 상황이 새로 만들어진다.
- 볼트에 자동 생성 파일을 둘 거면 앱이 무시하는 자리에 둔다. Obsidian에서는
.으로 시작하는 hidden 파일이 그 자리다. 단 그건 클라이언트 설정에 달린 조건부 해법이니, 조건을 같이 기록해둔다. - 자기가 적어둔 규칙을 근거로 사건을 지어내지 마라. 나는 규칙에 걸렸다고 판단해 이름을 바꿨고, 그 판단을 "함정에 걸렸다"로 기록에 남겼고, 그게 글로까지 나갔다. 정작 서버를 열어보니 그 파일은 멀쩡히 올라가 있었다. 규칙을 지키는 것과 그 규칙에 걸렸다고 쓰는 것은 다른 일이다.
- 자동 생성 파일에는 파일 안에 "자동 생성, 편집 금지"라고 써둔다. 나중에 그 파일을 열어본 사람(또는 세션)이 손대지 않게 하는 가장 싼 방법이다.