docs(feedback): add F6 — 메모 데이터 백업 + 복원 (3-layer)
L1 로컬 원자 스냅샷 (db.backup + GFS 로테이션) + L2 git remote 마크다운 동기화 (F5 형식 그대로 추적, SQLite 바이너리 push 회피) + L3 F5+import. gitea 자체 호스팅 인프라 활용 가능. L2 는 별 spec, L1+L3 은 슬라이스 후속. '데이터 손실 0회' 를 slice §1.3 silent invariant 후보로 제안. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -577,6 +577,226 @@ inkling_export_version: 1
|
||||
|
||||
---
|
||||
|
||||
## F6. 메모 데이터 백업 + 복원 (3-layer 권장) (🌱 raw)
|
||||
|
||||
**발견:** 2026-04-26 dogfood 시작 직전 사고 실험. 슬라이스 v0.4 의 메모 데이터는 `%APPDATA%\Inkling\Inkling\profiles\default\` 단 한 위치에만 존재. 디스크 고장·실수 삭제·DB 손상·OS 재설치 = 총 손실. Strategy.md §1 의 "이제 잊어도 됩니다" 보상이 **데이터 영속성 신뢰** 위에 서 있어서, 이 신뢰가 깨지면 슬라이스 §1.3 의 종료 조건 ("본인 2주 dogfood 완주") 자체가 위협받음.
|
||||
|
||||
### 관찰
|
||||
|
||||
현재 단일 실패 지점 (SPOF):
|
||||
- `inkling.sqlite` (WAL 두 파일 포함) — 노트·태그·AI 메타·intent 전부
|
||||
- `media/` — 클립보드 이미지 바이너리 (DB 의 `rel_path` 와 짝)
|
||||
- 부팅 시 `MediaGc` 가 DB 미참조 미디어를 정리 — DB 가 손상되면 미디어도 GC 사이클에서 사라질 수 있음 (위험 증폭)
|
||||
|
||||
기존 부분 완화는 0:
|
||||
- 자동 백업 0
|
||||
- 외부 동기화 0
|
||||
- import 경로 0
|
||||
- F5 (export) 가 promoted 되어도 단방향 + 수동
|
||||
|
||||
본인 dogfood 운영 환경 신호:
|
||||
- 이미 `gitea.altair823.xyz` 자체 호스팅 중 — 사적 git remote 인프라가 있음
|
||||
- 프로젝트 메모리: Mac=업무 / Windows=개인+dogfood — 디바이스 전환 가능성 (단일 활성, 동시 X)
|
||||
- RTX 4070 Windows = 메인 dogfood 머신, 디스크 1대 SSD 가정 → 디스크 고장 1회 = 전체 손실
|
||||
|
||||
### 제안 방향
|
||||
|
||||
**3-layer 다층 백업.** 각 layer 가 다른 위협 모델을 커버.
|
||||
|
||||
| Layer | 위협 모델 | 비용 | 슬라이스 적합 |
|
||||
|-------|----------|------|--------------|
|
||||
| **L1 로컬 원자 스냅샷** | 실수 삭제, DB 손상, AI 마이그레이션 실패 | 작음 | ✅ 슬라이스 후속 가벼움 |
|
||||
| **L2 git remote 마크다운 동기화** | 디스크 고장, 디바이스 이동, 버전 이력 필요 | 중 | 🔬 별도 미니 spec |
|
||||
| **L3 전체 export/import** | OS 재설치, 디바이스 이주, 사용자 통제 백업 | 작음 (L1 + F5 위) | ✅ F5 위에 import 만 추가 |
|
||||
|
||||
#### L1 — 로컬 원자 스냅샷
|
||||
|
||||
`better-sqlite3` 의 `db.backup(path)` API 사용. WAL 활성 상태에서도 안전한 원자적 복제 (파일 단순 cp 와 다름 — WAL 미반영분 누락 위험 없음).
|
||||
|
||||
```ts
|
||||
// 의사코드
|
||||
async function snapshot(): Promise<void> {
|
||||
const ts = format(new Date(), 'yyyy-MM-dd');
|
||||
const dest = join(profileDir, 'backups', `inkling-${ts}.sqlite`);
|
||||
await db.backup(dest);
|
||||
await rotate({ daily: 14, weekly: 4, monthly: 6 });
|
||||
}
|
||||
```
|
||||
|
||||
**스케줄**:
|
||||
- 앱 종료 직전 1회 (`before-quit`)
|
||||
- 매일 첫 캡처 시 (`<profileDir>/backups/.last-snapshot` mtime 비교)
|
||||
- 명시 트리거: 트레이 메뉴 "지금 백업"
|
||||
|
||||
**저장 정책 — Grandfather-Father-Son**:
|
||||
- 일일 14개 → 주간 4개 → 월간 6개. 누적 24개 안팎, 평균 사이즈 가정 시 < 50MB.
|
||||
- `backups/` 는 미디어 미포함 (DB 만). 미디어는 L2 또는 L3 책임.
|
||||
|
||||
**위협 미커버**: 디스크 자체 고장. SSD 가 죽으면 backups/ 도 같이 죽음. L2 가 이 위협 담당.
|
||||
|
||||
#### L2 — git remote 동기화 (RECOMMENDED 핵심 layer)
|
||||
|
||||
**핵심 결정: SQLite 바이너리를 push 하지 말고, F5 마크다운 트리를 push 한다.**
|
||||
|
||||
| | SQLite 바이너리 | F5 마크다운 트리 |
|
||||
|--|----------------|-----------------|
|
||||
| diff 의미성 | 0 (전체 blob 변경) | ✅ 노트별 라인 diff |
|
||||
| repo 사이즈 | 매 push 마다 풀 DB | 변경 노트만 |
|
||||
| 멀티 디바이스 머지 | 불가 (binary conflict) | 가능 (텍스트 merge) |
|
||||
| 외부 도구 호환 | 0 | RAG / Obsidian / grep 즉시 |
|
||||
| F5 와 의 시너지 | 0 | F5 그대로 재사용 |
|
||||
|
||||
→ **F5 의 export 형식을 git 추적 대상으로 그대로 사용**. F5 가 promoted 되면 F6-L2 는 그 위에 자동화 layer 만 얹는 구조.
|
||||
|
||||
**아키텍처**:
|
||||
|
||||
```
|
||||
[CaptureService / NoteRepository]
|
||||
│ (write)
|
||||
▼
|
||||
inkling.sqlite ← Layer 0 (primary)
|
||||
│
|
||||
│ (DB write 후 dirty 마크)
|
||||
▼
|
||||
<profileDir>/sync/ ← Git working tree (L2)
|
||||
├── notes/ ← F5 형식 마크다운
|
||||
├── media/
|
||||
├── index.jsonl
|
||||
└── manifest.json
|
||||
│
|
||||
▼ (BackgroundSyncWorker, 5분 주기 또는 dirty=true 후 30초 debounce)
|
||||
git add . && git commit -m "..." && git push
|
||||
```
|
||||
|
||||
**커밋 메시지 컨벤션** (자동 생성):
|
||||
|
||||
```
|
||||
chore(notes): +3 ~1 -0 (2026-04-26T14:23+09:00)
|
||||
|
||||
added: 01H89aab... 주간 회고 PR 리뷰
|
||||
added: 01H89bcd... ...
|
||||
modified: 01H78xyz... 어제 회의 메모
|
||||
```
|
||||
|
||||
기존 inkling 본 저장소 commit 스타일과 분리되며, "automated note sync" 임이 명확.
|
||||
|
||||
**Auth & 보안**:
|
||||
- Personal Access Token 또는 SSH key. Electron `safeStorage` API (OS keychain 백엔드 — Windows 는 DPAPI) 로 평문 미저장.
|
||||
- 토큰은 절대 로그/오류 메시지에 노출 금지 (slice §1.1 invariant 4 확장).
|
||||
- repo 는 **반드시 private** — 평문 raw_text 노출 위험. 처음 설정 시 다이얼로그에 굵은 경고.
|
||||
|
||||
**Conflict 정책 — single-active-device 가정**:
|
||||
- push 가 거부되면 (다른 디바이스가 먼저 push) → `git pull --rebase` → 자동 머지 시도
|
||||
- 머지 실패 (같은 노트 양쪽 수정) → 트레이 알림 + 수동 해결 다이얼로그. 노트별 "내 버전 / 원격 버전 / 둘 다 보존" 3-way 선택
|
||||
- 본인 dogfood = 단일 활성 디바이스라 거의 발생 안 함 — 멀티 디바이스 시나리오 정식 지원은 L2 의 v2
|
||||
|
||||
**Repo 초기화**:
|
||||
- 첫 설정 시 사용자가 빈 remote URL 입력 → 앱이 `git init` + 초기 export + 첫 커밋 + push
|
||||
- 또는 기존 repo URL 입력 → clone → 검증 (이전 manifest 호환성) → 동기화 시작
|
||||
|
||||
**미디어 정책**:
|
||||
- 평문 push 가 default — 텍스트 노트와 함께 미디어도 git 에 올라감
|
||||
- repo 사이즈 폭발 위험 → 토큰 옵션: "이미지 제외" 토글 또는 Git LFS (선택). 1차는 옵션 X, 단순 push, 사이즈 모니터링만.
|
||||
- 이미지 제외 시 frontmatter 의 `images` 항목은 보존하되 파일은 미포함 → 복원 시 placeholder 표시
|
||||
|
||||
#### L3 — 수동 전체 export / import
|
||||
|
||||
- **export**: F5 가 그대로 담당. 변경 없음.
|
||||
- **import**: 신규. F5 형식 폴더를 읽고 DB 에 upsert. 충돌 정책:
|
||||
- id 충돌 + 본문 동일 → skip
|
||||
- id 충돌 + 본문 상이 → 사용자 선택 (덮어쓰기 / skip / 양쪽 보존하며 새 id 생성)
|
||||
- id 신규 → insert
|
||||
- 미디어 → MediaStore 에 복사
|
||||
- 트레이 메뉴 "백업에서 복원..." → 폴더 선택 → 미리보기 (n개 신규, m개 변경, k개 충돌) → 확인 → 적용
|
||||
|
||||
### 결정 대기
|
||||
|
||||
1. **3-layer 동시 도입 vs 단계적**: L1 → L3 → L2 순서가 비용·위험 단조 증가라 권장. L1 만으로도 SPOF 완화의 80% 커버.
|
||||
2. **L2 sync 단위**: 매 변경 vs 5분 debounce vs 종료 시 1회 vs 명시 동기화만. 실시간일수록 데이터 손실 윈도우 작지만 git push 빈도 폭발 + 네트워크 마찰. **5분 debounce + 종료 시 즉시 push** 가 1차 권장.
|
||||
3. **L2 repo 분리**: 기존 `gitea.altair823.xyz/altair823-org/inkling` (소스 코드) 와 분리된 별 repo (예: `altair823-org/inkling-data`) — **반드시 분리**. 데이터·코드 라이프사이클 다름, 외부 협업자에게 데이터 노출 위험.
|
||||
4. **L2 충돌 시 정책 — slice §1.1 vs 사용자 선택**: 자동 "내 디바이스 우선" 가속 vs 매번 묻기. dogfood 단일 디바이스 가정으론 자동 OK, but defensive 차원에서 충돌 발생 시 1회 확인이 안전.
|
||||
5. **media 의 git 추적**: 포함 vs 제외 vs LFS. 1차는 포함 + 사이즈 < 100MB 경고. 누적 시점에 후속 결정.
|
||||
6. **L1 백업 위치**: `<profileDir>/backups/` (현 프로필 안) vs 별 디렉터리 (`%APPDATA%\Inkling\backups\`) vs 사용자 지정 외부 경로. 외부 경로 옵션이 OneDrive 등 클라우드 sync 폴더 이용 가능 — 거의 공짜 cloud backup.
|
||||
7. **import 시 raw_text invariant 보호**: slice §1.1 "raw_text 불변" 은 *동일 id 내* 의미. import 가 같은 id 의 raw_text 를 다른 값으로 덮어쓰면 invariant 위반. 충돌 시 raw_text 다르면 **새 id 강제** 정책이 안전.
|
||||
8. **L2 첫 설정의 UX 부담**: token 입력 + remote 검증 + 초기 push 가 dogfood 1일차 첫 인상에 마찰. 첫 설치 후 N 일 (예: 7일) 까지는 L1 만 켜두고 L2 는 트레이 메뉴 "원격 백업 설정" 으로 opt-in 권장.
|
||||
9. **암호화 — local-first 라도 token 외 추가 보호 필요한가**: SQLite·미디어·git 모두 평문. 디스크 도난 시 노출. 1차는 평문 (slice §1.1 미적용 영역), 후속에 SQLCipher / age 암호화 검토.
|
||||
10. **slice §7 strict-pin invariant 영향**: L1 은 `better-sqlite3.backup()` 만 사용 — 추가 dep 0. L2 는 `simple-git` 또는 `nodegit` 같은 git 바인딩 또는 child_process 로 git CLI 호출. CLI 호출이 dep 0 + 사용자 git 환경 재사용. **CLI 호출 권장**.
|
||||
|
||||
### 가설·측정
|
||||
|
||||
| # | 가설 | 측정 |
|
||||
|---|------|------|
|
||||
| H1 | dogfood 2주 누적 동안 디스크 측 사건 (실수 삭제, DB 손상, 디스크 고장) ≥ 1회 발생할 정성 가능성 | 발생 시 라벨링 |
|
||||
| H2 | L1 단독 만 도입해도 SPOF 발생 시 회복 가능 (백업으로 ≥ 95% 데이터 복원) | 복원 시뮬레이션 1회 (의도적 DB 삭제 후 복원) |
|
||||
| H3 | L2 5분 debounce push 가 일평균 ≤ 30 commit. repo 사이즈 누적 < 100MB / 1년 | 로그 측정 |
|
||||
| H4 | L2 commit 메시지 통계 (added·modified·deleted) 가 dogfood 활동 회고 자료로 가치 발생 | 정성 평가 |
|
||||
| H5 | "이제 잊어도 됩니다" 보상의 신뢰도 — 백업이 있다는 인지가 capture 빈도 또는 심리적 부담 감소에 영향 | 본인 self-report |
|
||||
|
||||
### 범위
|
||||
|
||||
- **In (L1 — 슬라이스 후속 가벼움):**
|
||||
- `BackupService` 신규 — `db.backup()` 래핑 + 로테이션
|
||||
- 트레이 메뉴 "지금 백업" + 타임스탬프 표시
|
||||
- 종료·일일 1회 자동 트리거
|
||||
- `backups/` 디렉터리 — `.gitignore` 와 같은 .ignored 마커 고려
|
||||
- 단위 테스트 — 로테이션 GFS 정책
|
||||
- **In (L3 — F5 위에 import 만):**
|
||||
- `ImportService` 신규
|
||||
- 충돌 미리보기 다이얼로그
|
||||
- 트레이 메뉴 "백업에서 복원..."
|
||||
- **In (L2 — 별 spec, 가장 큼):**
|
||||
- `SyncService` (BackgroundSyncWorker)
|
||||
- F5 ExportService 의 incremental 모드 (변경 노트만)
|
||||
- git CLI 래퍼 + safeStorage 토큰 관리
|
||||
- 설정 UI — remote URL, 토큰, 동기화 주기, 미디어 포함 여부, 충돌 정책
|
||||
- 충돌 해결 다이얼로그
|
||||
- 상태 표시 (트레이 아이콘 색·tooltip)
|
||||
- **Out:**
|
||||
- SQLCipher 암호화
|
||||
- 다중 활성 디바이스 실시간 sync
|
||||
- 외부 SaaS (Dropbox API, Google Drive API) 직접 연동
|
||||
- Rsync 전송
|
||||
- SQLite WAL 의 logical replication
|
||||
|
||||
### 영향
|
||||
|
||||
- **Schema:** 없음
|
||||
- **신규 파일 (L1 + L3):**
|
||||
- `src/main/services/BackupService.ts`
|
||||
- `src/main/services/ImportService.ts`
|
||||
- `src/main/ipc/backupApi.ts`
|
||||
- 테스트 `tests/unit/BackupService.spec.ts`, `ImportService.spec.ts`
|
||||
- **신규 파일 (L2 별 spec):**
|
||||
- `src/main/services/SyncService.ts`
|
||||
- `src/main/services/GitClient.ts` (git CLI 래퍼)
|
||||
- `src/main/services/CredentialStore.ts` (safeStorage 래퍼)
|
||||
- 설정 UI (Settings 창 신설 — 슬라이스 §5 의 "Settings 창 없음" 결정 재검토 필요)
|
||||
- **외부 의존:**
|
||||
- L1: 0
|
||||
- L3: 0
|
||||
- L2: 사용자 머신의 git CLI 필요. README 사전 요구 항목 추가
|
||||
- **로깅:**
|
||||
- 백업 시작·완료·사이즈만. 본문·파일명 미기록
|
||||
- 동기화 push 결과·conflict 발생만. 토큰·URL 일부 마스킹
|
||||
- **문서:**
|
||||
- 본 항목 promoted 시 분리 권장:
|
||||
- `2026-04-26-local-snapshot.md` (L1)
|
||||
- `2026-04-26-import.md` (L3, F5 와 자매)
|
||||
- `2026-04-26-git-sync.md` (L2)
|
||||
- 또는 단일 `2026-04-26-backup-strategy.md` 로 통합 후 §A·§B·§C 로 분리
|
||||
|
||||
### 비고
|
||||
|
||||
본 항목과 F5 는 **완벽한 데이터 라이프사이클 그림** 의 두 절반:
|
||||
- F5 = 외부 회수 (read 방향)
|
||||
- F6 = 외부 백업 + 내부 복원 (write·sync 방향)
|
||||
|
||||
L2 (git sync) 가 dogfood 본인의 기존 인프라 (gitea 자체 호스팅) 와 자연스럽게 맞물리는 점은 본 사용자에게 특히 강한 가치. 다른 사용자였다면 GitHub Actions 등 외부 서비스 의존이라 우선순위 낮을 수 있음.
|
||||
|
||||
slice §1.3 종료 조건 ("크래시 0회") 와 별개로, **"데이터 손실 0회"** 가 silent invariant 로 추가되어야 함. 본 항목 → `slice spec §1.3` 추가 갱신 후보.
|
||||
|
||||
---
|
||||
|
||||
## (다음 항목 자리)
|
||||
|
||||
새 피드백 추가 시 `## F6. 짧은 제목 (🌱 raw)` 헤더로 시작. 표준 슬롯 6개 채우거나 비워둔 채 시작 가능.
|
||||
새 피드백 추가 시 `## F7. 짧은 제목 (🌱 raw)` 헤더로 시작. 표준 슬롯 6개 채우거나 비워둔 채 시작 가능.
|
||||
|
||||
Reference in New Issue
Block a user