MyStock - Dropbox 동기화 #2 DB File Sync 정책 설계

지난글에서 SQLite DB를 Snapshot으로 만들어 Dropbox에 올리고 다시 받을 수 있는지 POC를 통해 확인했다. Upload와 Download, DB 무결성 확인, 오래된 revision으로 최신 파일을 덮어쓰지 못하게 하는 충돌 처리까지 실제 Dropbox에서 검증했으니 처음에는 이제 MyStock에 붙이기만 하면 될 것 같았다.

 

그런데 파일을 안전하게 주고받을 수 있다는 것과 여러 PC에서 하나의 DB를 안전하게 이어서 사용하는 것은 전혀 다른 문제였다.

DB 서버 대신 File Sync

가장 단순한 방법은 사실 DB 서버를 두는 것이다. 회사 PC와 집 PC가 같은 DB 서버를 바라보면 어느 파일이 최신인지 비교할 필요가 거의 없다.

회사 PC ─┐
         ├─ DB Server
집 PC   ─┘

문제는 MyStock이 나 혼자 사용하는 개인 프로그램이라는 점이었다. 이 프로그램 하나 때문에 서버를 별도로 구축하고 계속 관리하는 것도 귀찮았고, 백업과 장애까지 신경 써야 할 대상이 하나 더 생긴다. 그렇다고 유료 DB 서비스를 사용하는 것도 처음부터 고려하고 싶지 않았다. 결국 원하는 조건은 다시 단순해졌다.

서버는 운영하지 않고, 지금의 SQLite 구조를 유지하면서 여러 PC가 같은 데이터를 안전하게 이어서 사용한다.

서버를 피한 대신 이제 MyStock이 DB File Sync의 기준을 직접 정해야 했다.

최신 DB라는 질문

POC에서 Dropbox의 revision을 이용하면 오래된 상태에서 최신 Remote 파일을 덮어쓰는 것은 막을 수 있었다. 하지만 revision이 바뀌었다고 해서 회사 PC와 집 PC 중 어느 DB가 더 최신인지 알 수 있는 것은 아니었다. MyStock DB에는 계좌정보 하나만 저장되는 것이 아니다. 가격 이력, ETF 구성종목, 거래내역, 시장지표처럼 서로 다른 데이터가 각자의 시점에 갱신된다. 한쪽에 더 최근 계좌정보가 있다고 해서 다른 쪽의 모든 데이터를 포함하고 있다고 볼 수는 없다.

 

수정 시간이 더 최근한 파일, 크기가 더 큰 파일, revision이 더 새로운 파일을 자동으로 승자로 정하는 방식은 사용하지 않기로 했다. ChatGPT와 이 문제를 검토하면서 질문을 바꿨다.

어느 DB가 더 최신인가가 아니라, 마지막으로 같은 상태였던 이후 Local과 Remote가 각각 바뀌었는가?

마지막으로 정상 Sync가 끝난 상태를 공통 기준점으로 두면 경우의 수가 단순해졌다.

Local DBDropbox DB처리

Local DB Dropbox DB 처리
변경 없음 변경 없음 아무것도 하지 않음
변경 있음 변경 없음 Push
변경 없음 변경 있음 Pull
변경 있음 변경 있음 Conflict

둘 중 한쪽만 바뀌었다면 자동으로 이어갈 수 있지만, 양쪽이 모두 바뀌었다면 MyStock이 임의로 승자를 정하지 않는다.

자동 Merge나 Last Write Wins보다는 Local과 Remote를 모두 보존하고 Sync를 멈추는 방향으로 정했다. 개인 프로그램이라 조금 불편하더라도 조용히 데이터를 잃는 것보다는 낫다고 봤다.

Startup Pull

다음 문제는 Dropbox의 DB를 언제 받아올 것인가였다. 처음에는 실행 중 Remote가 바뀌면 그때 받아오면 될 것 같았지만, Codex를 통해 실제 MyStock의 DB 사용 구조를 조사하면서 생각이 바뀌었다. GUI가 시작된 이후에는 Account, Daily, ETF, Trade, Market 등 여러 기능이 이미 DB를 읽고 쓰고 있었다.

 

이 상태에서 실행 중인 DB 파일을 다른 DB로 교체하려면 Worker와 Connection 상태까지 다시 맞춰야 했다. Windows도 함께 사용한다는 조건까지 넣으니 Runtime 중 DB 교체는 굳이 만들지 않는 편이 낫다고 판단했다.

Pull은 MyStock 시작 전에만 수행한다.

MyStock 실행
↓
Dropbox 상태 확인
↓
필요하면 DB Download / 검증 / 교체
↓
SQLite Bootstrap
↓
GUI 시작

실행 중 Remote가 변경되더라도 바로 DB를 갈아끼우지 않는다. 다음 실행에서 다시 확인하면 된다. 복잡한 상황을 모두 코드로 해결하기보다 동작 범위를 줄이는 쪽을 선택했다.

온라인 전용 정책

설계 과정에서는 Offline 상태에서 한쪽 PC가 변경되고 다른 PC도 Remote를 변경하는 경우까지 검토했다. 하지만 MyStock은 KIS OpenAPI를 기반으로 데이터를 조회하는 프로그램이기 때문에 인터넷 연결이 없는 상태에서 굳이 Local DB만 수정하며 실행할 이유가 크지 않았다. 그래서 네트워크를 사용할 수 없으면 안내 메시지를 보여주고 MyStock을 시작하지 않는 방향으로 정했다. Dropbox Sync를 사용하는 경우라면 Remote 상태를 확인할 수 있어야 정상 실행한다. Offline 충돌 복구를 계속 복잡하게 만드는 대신, 애플리케이션의 사용 조건 자체를 명확하게 한 셈이다.

DB와 장치 상태의 분리

Sync 정보를 어디에 저장할지도 고민했다. 처음에는 DB 안에 마지막 Dropbox revision 같은 값을 같이 넣으면 편할 것 같았다. 하지만 그 DB를 다른 PC가 Download하면 이전 장치의 Sync 상태도 그대로 따라온다.

 

회사 PC와 집 PC는 각각 마지막으로 확인한 Remote 상태와 진행 중이던 Sync 상태가 다를 수 있기 때문에 이 정보는 공유 DB의 속성이 아니었다. 그래서 Dropbox에는 실제 DB만 두고, 장치별 Sync 상태는 각 PC에 따로 두기로 했다.

Dropbox
└─ mystock.sqlite3

회사 PC
└─ mystock.sync.json

집 PC
└─ mystock.sync.json

DB는 공유하지만 장치 상태는 공유하지 않는다. 이 Local JSON에는 마지막으로 정상 Sync한 Remote revision과 Snapshot identity, 진행 중이던 Upload 또는 Pull 같은 정보를 기록할 수 있다.

성공과 실패 사이

POC 이후 실제 Production 구조를 검토하면서 가장 애매했던 경우는 전송 결과를 확실히 알 수 없는 순간이었다.

Snapshot 생성
↓
Dropbox Upload 성공
↓
Remote 파일 교체 완료
↓
Local Sync 정보 기록 전
↓
프로그램 종료

다음 실행에서 Local에는 이전 Sync 정보만 남아 있고 Dropbox에는 새 파일이 있다. 이것만 보면 다른 PC가 Remote를 변경한 것처럼 보일 수 있다. 그래서 Upload 전에 지금 어떤 Snapshot을 어떤 Remote 상태에서 전송하려는지 작은 Journal에 먼저 기록하고, 다음 실행에서 Remote 파일과 다시 비교해 직전 작업이 실제로 성공했던 것인지 복구할 수 있게 하는 방향으로 정리했다. 여기서부터 Sync는 단순 파일 복사가 아니라 상태 관리 문제가 됐다.

Runtime Push

마지막으로 오래 고민한 것은 언제 Dropbox에 올릴 것인가였다. 처음에는 ETF 구성종목이나 여러 Background 작업이 모두 끝난 시점을 찾으려고 했다. 그런데 Codex 조사 결과 MyStock의 DB 갱신은 여러 Lifecycle에서 따로 일어나고 있었고, 모든 작업이 끝났다는 하나의 전역 지점을 새로 만드는 것도 생각보다 복잡했다.

 

반면 실시간 주가 정보는 SQLite에 계속 저장되는 데이터가 아니다. 결국 Dropbox가 알아야 할 것은 각 Worker의 세부 상태가 아니라 마지막 Sync 이후 DB 내용이 실제로 달라졌는지였다. 그래서 Production Sync에서는 일정 주기로 Snapshot을 확인하고, 마지막으로 Sync한 Snapshot과 내용이 다른 경우에만 Push하는 쪽으로 정리했다. 기본 주기는 5분으로 잡았다.

5분 경과
↓
SQLite Snapshot 생성
↓
마지막 Sync Snapshot과 비교
↓
변경 없음 → NOOP
변경 있음 → Conditional Push

즉 5분마다 DB를 무조건 Upload하는 것이 아니라 5분마다 확인하고, 실제 변경이 있을 때만 전송한다. 이렇게 하면 Dropbox Sync가 Account나 ETF 같은 각 기능의 내부 Lifecycle을 모두 이해할 필요가 없다.

최종 Sync 정책

여러 경우를 검토하면서 최종적으로 남긴 규칙은 생각보다 많지 않았다.

  • MyStock 시작 전에 Network와 Dropbox 상태를 확인한다.
  • Pull은 Startup에서만 수행한다.
  • Local만 변경됐으면 Snapshot을 만들어 Conditional Push한다.
  • Remote만 변경됐으면 다음 Startup에서 Pull한다.
  • Local과 Remote가 모두 변경됐으면 Conflict로 중단한다.
  • Runtime Pull과 자동 Merge는 하지 않는다.
  • 수정 시간으로 최신 DB를 결정하지 않는다.
  • Dropbox에는 DB만 두고 Sync 상태는 장치별 Local JSON으로 관리한다.
  • 상태가 불명확하면 기존 DB를 보존한다.

1부 POC에서 확인한 것은 파일을 안전하게 주고받을 수 있는가였다. 이번에는 그 다음 질문인 어떤 상황에서 그 파일을 주고받아도 되는가를 정했다. 이제 남은 것은 이 구조를 실제 MyStock의 Startup과 Runtime에 연결하고, 사용자가 현재 Sync 상태를 확인할 수 있도록 만드는 일이었다.

 

MyStock - Dropbox 동기화 #3 Sync 구현과 UI에서 이 부분을 이어서 정리하려고 한다.