"payload 는 제출 누른 순간 얼어. 그 뒤로 세상이 건드릴 수 있는 건 상태 플래그 하나뿐이야."
턴 하나 안에 상태가 두 종류 있어
캡처한 턴 안엔 성격이 완전히 다른 상태 두 종류가 들어 있어. 그 둘을 갈라놓는 게 끊김을 무해하게 만드는 핵심이야.
- payload. 질문 텍스트랑 이미지 blob 이야. 캡처하는 순간부터 안 바뀌어. 네트워크에서 무슨 일이 나든, 앱이 죽든, 재시도를 하든 이건 다시 안 써.
- sync 상태.
pending → sending → answered | failed로 움직여. 바뀌는 건 여기뿐이고, 한 칸 옮겨갈 때마다 저장까지 끝나는 쓰기가 일어나.
턴을 이렇게 보면 '전송 중에 앱이 죽으면 어떻게 돼?' 에 시시하고 정확한 답이 나와. payload 는 손도 안 탔고(얼어 있고 이미 박혀 있었으니까), 상태는 아직 sending 이거나 벌써 failed 로 넘어갔거나 둘 중 하나야. 어느 쪽이든 다음에 앱을 켰을 때 뭘 할지가 명확해. 끊김은 데이터가 날아가는 사건이 아냐. 상태가 한 칸 움직이는 사건이야.
payload 가 안 바뀌는 게 왜 중요하냐면
끊김이 payload 를 건드릴 수 있으면 실패 하나하나가 전부 손상 후보가 돼. 질문을 다시 인코딩하는 재시도, 반쯤 쓰인 blob 을 다시 읽는 백그라운드 sync, 필드 두 개 업데이트하다 난 크래시. 하나하나가 사용자가 실제로 쓴 말을 동의도 없이 바꿔놓는 통로야. 캡처할 때 payload 를 얼려놓고 상태만 움직이게 하면, 그 넓은 실패 모드 공간이 작고 훤히 아는 상태 기계 하나로 접혀. '크래시 나고 내 데이터 아직 멀쩡한가?' 라는 무서운 질문이 '지금 어느 상태지?' 로 바뀌어. 답이 넷뿐이고 넷 다 안전한 질문으로.
상태 기계가 곧 계약
상태 전이를 대놓고 설계해 놓으면 견고함이 따라와. payload 를 지우는 전이는 없어. '잃음'을 뜻하는 상태도 없고. 제일 나쁜 게 failed 인데, 그것도 턴을 통째로 쥔 채로 다시 해보자고 할 뿐이야. 약속이 눈에 보이는 형태로 나온 게 이거야. 끊긴 턴은 절대 사라지지 않아. 작고 안전한 지도 어딘가에서 기다리고 있을 뿐이야.