C.W.K.
Stream
Lesson 04 of 05 · published

복사하지 말고, 투영해

~13 min · source-immutable, projection, recall, cqrs

Level 0릴 입문자
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Ashen Reel 은 투영을 읽어도 돼. 미래 쓰기는 typed Recall intent 야; 병렬 로컬 자막이나 마커 데이터베이스는 절대 아냐."

두 번째 진실이 되는 캐시

Ashen Reel 은 영상 옆에 자막 레일을 보여줘 — 현재 Recall 릴리스의 말한 단어들, 재생 위치의 줄을 강조하면서. 뻔한 최적화는 그 자막을 로컬에 캐시하는 거야: 로드 더 빠르고, Recall 느려도 되고, 착착 느껴져. 근데 함정이야. 자막 사본을 저장하는 순간, 같은 진실의 소유자가 둘이고, 누가 Recall 에서 줄을 고치는 날 네 캐시된 사본은 조용히, 자신 있게 틀려.

그래서 Ashen Reel 은 안 그래. 자막을 투영해: 세그먼트를 세션 메모리로 가져오고, 창이 열린 동안 쓰고, 아무것도 안 둬. Ashen Reel 데이터베이스엔 transcripts 테이블이 없어 — 일부러. 다음 실행이 Recall 에서 다시 가져와; Recall 이 유일한 소유자로 남아.

메모리에만 쥔 빌린 진실은 드리프트 못 해. 남의 데이터의 제일 안전한 사본은 네가 안 두는 사본이야. 필요할 때 읽고, 쓰고, 버려. 저장 안 한 건 소유자 등 뒤에서 절대 stale 될 수 없어.

투영은 read model 이야, 일부러 기억상실인:

이게 깔끔한 CQRS 쪼갬의 읽기 쪽이야. Recall 이 쓰기 쪽을 소유해 — canonical 자막, 교정, 릴리스. Ashen Reel 은 표시용 읽기 전용 투영을 만들고 버려. 밑 진실이 바뀌면, 투영은 다음 읽기에 그냥 재구축돼; 절대 패치 안 돼, 애초에 authoritative 가 아니었으니까. 누가 쓰는지랑 누가 읽는지를 분리하면 드리프트 버그가 사라져.

교정은 병합이 아니라 새 신원을 받아

우아한 대목이 여기야. 자막이 Recall 에서 교정되면, 그건 기존 것에 대한 편집이 아냐 — 자기 신원을 가진 새 릴리스야. Ashen Reel 은 옛 자막을 새 것과 diff 해서 병합 안 해; 그냥 새 릴리스 id 를 가져와서 보여줘. 상류의 교정이 하류의 fresh 투영이 돼, 자동으로. 넌 reconciliation 로직을 절대 안 짰어, 교정된 릴리스의 신원이 대신 reconcile 을 해줬으니까.

미래 쓰기는 로컬 그림자가 아니라 typed intent 로 집에 가. Ashen Reel 은 결국 순간이나 watch-span 을 표시하게 해줄 거야 — 근데 그것들은 그걸 소유하는 Recall 로 다시 보내는 typed intent 가 돼. 플레이어는 나중에 싱크해야 하는 사적 markers 테이블을 절대 안 키워. intent 를 받을 엔드포인트가 아직 없으면, 기능은 그 틈을 채울 로컬 저장소를 발명하는 대신 기다려(Track 7).

Code

자막 레일은 투영이야 — 빌리고, 쓰고, 버림·swift
/// The transcript rail: Recall's truth, borrowed for THIS session only.
final class TranscriptRail {
    private var segments: [Segment] = []   // in-memory; dies with the window

    func load(forRelease id: ReleaseID) async {
        // Fetch from Recall's loopback endpoint into memory. We do NOT persist.
        // There is no `transcripts` table in Ashen Reel's SQLite -- on purpose.
        // Next launch re-fetches; Recall stays the single owner of the truth.
        segments = await recall.segments(forRelease: id)
    }

    // A correction in Recall = a NEW release id = a fresh fetch here.
    // We never diverge, because we never stored a copy to diverge FROM.
}

External links

Exercise

네 시스템이 다른 시스템이나 서비스가 소유한 데이터를 캐시하거나 복사하는 곳을 찾아. 물어봐: 소유자가 그걸 바꾸면 무슨 일이 나? 싱크/무효화 메커니즘이 있고, 맞아? 이제 요청 시 재구축하는 메모리 투영이 영구 사본 없이 같은 UI 를 섬길지 생각해봐. 최소 한 케이스에, 기억상실 버전을 스케치하고 지울 수 있는 싱크 코드를 나열해.
Hint
냄새는 다른 시스템 레코드의 사본인 행을 가진 로컬 테이블, 더하기 걔넬 fresh 하게 지키려는 코드야. 테이블을 버리고 대신 읽을 때 다시 유도할 수 있는지 물어봐. 데이터가 표시 전용이고 네가 소유 안 하면, 버리는 투영이 보통 reconcile 해야 하는 사본을 이겨.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.