본문 바로가기
C.W.K.
Stream
Lesson 03 of 05 · published

ambiguous 상태

~12 min · ambiguous, state-machine, billing, uncertainty

Level 0Empty Shelf
0 XP0/35 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"'결제했을지도 모르는데 손에 아무것도 없다' 는 진짜 상태야. 이름을 줘."

대부분의 시스템이 모델링을 거부하는 상태

유료 제출은 작은 상태 기계를 지나가. preparingsubmittingcompleted. 깔끔해 보이지. 근데 대부분의 설계가 없는 척하는 갈림길이 하나 있고, 하필 그게 돈이 드는 놈이야. executor 가 submitting 에 있다고 해보자. 요청은 보냈는데 응답이 오기 전에 연결이 죽었어. 지금 상태가 뭐야?

진짜 몰라. provider 가 요청을 받아서 전사를 돌리고 청구까지 했는데 응답만 집에 못 온 걸 수도 있고, 요청이 아예 도착도 안 한 걸 수도 있어. Recall 이 서 있는 자리에서 그 두 세계는 구별이 안 되는데, 재정적 결과는 정반대야. 성공인 척하면 결과를 잃고, 깔끔한 실패인 척하면 이중 청구가 날 수 있는 재시도를 부르고. 정직한 답은 이거야. 이건 세 번째 상태고, 자기 이름이 필요해.

추측하지 말고 이름 지어

Recall 은 그걸 ambiguous 라고 부르고, 해결될 때까지 terminal 인 진짜 상태로 모델링해. 규칙은 이래.

  • completed 는 새 호출 없이 저장된 응답을 돌려줘. 이미 결제가 끝난 안전한 경로야.
  • executor 의 outbox 에 provider 응답이 저장돼 있는 submitting그 저장된 응답으로 완료돼(다음 레슨). 새로 청구되는 건 없고.
  • 저장된 응답이 없는 submittingambiguous 야. 청구됐을 수도 있고 아닐 수도 있는데, 손에 결과는 없는 상태.

그리고 결정적 규칙: ambiguous 호출은 절대 자동 재시도 안 해. blind 재시도는 진짜 돈을 건 동전 던지기거든. 앞면이면 필요한 재시도였고, 뒷면이면 방금 두 번 결제한 거야. 그래서 Recall 은 거기서 멈추고 불확실성을 사람한테 넘겨. 사람이 provider 의 실제 청구 기록을 확인해서 두 세계 중 어느 쪽이 진짜인지 알아내고, 호출을 명시적으로 해결해. 청구 안 됨(다시 해도 안전), 청구됐는데 결과는 잃음(손실 인정, 공짜로 다시 안 함), 청구됐고 진짜로 다시 결제해야 함(별도의 명시적 승인). 추측이 돈을 쓰는 자리에서는 기계가 추측을 거부하는 거야.

왜 일급 상태가 최선의 추측을 이겨

코드 분기를 줄이려고 모호함을 성공이나 실패 한쪽으로 접어버리고 싶은 유혹이 늘 있어. 그 깔끔함이 정확히 버그고. '아마 실패했겠지, 재시도하자' 로 접은 불확실성은 조용히 이중 지출을 승인해. '아마 괜찮겠지' 로 접은 불확실성은 조용히 데이터를 잃고. ambiguous 상태에 자기 이름을 주고 그대로 모델링하는 게 돈을 정직하게 지키는 방법이야. 한쪽에 세워두고, 재시도하지 않고, 기계한테는 없는 ground truth(provider 청구 기록) 를 볼 수 있는 사람을 기다리는 거지. 기계가 알 수 없을 때 맞는 수는 자신만만한 추측이 아냐. 사람이 해결할 수 있는 명시적인 '난 몰라' 야.

Code

유료 제출 상태 기계 (갈림길이 핵심)·text
preparing ──▶ submitting ──▶ completed
                   │
                   └──▶ ambiguous   (제출됨, 응답 없음,
                                      저장된 outbox 사본 없음)

completed  : 저장된 응답 반환, 새 POST 없음
submitting + outbox 응답 : 저장된 사본에서 완료
submitting + 응답 없음   : AMBIGUOUS  (청구됐나?)

ambiguous 는 절대 자동 재시도 안 함. 사람이 청구를 조사한 뒤:
  resolve --not-billed     -> 다시 해도 안전
  resolve --billed-lost    -> 손실 인정, 공짜로 다시 안 함
  resolve --billed-lost --allow-paid-rerun -> 명시적 재결제

External links

Exercise

네 일에서, 부작용이 일어난 뒤에 그게 일어났는지 알 수 없는 방식으로 실패할 수 있는 작업을 찾아봐. timeout 난 결제, 확인 안 된 메시지 전송, 착지했을지도 모르는 write 같은 거. 지금 그런 경우에 네 코드는 뭘 해? 성공으로 치나, 실패로 치나, 재시도하나? 거기다 명시적인 'ambiguous' 상태를 설계해. 뭐가 한쪽에 세워지고, 사람이 어떤 ground truth 를 확인하고, 해결 선택지가 뭔지.
Hint
신호는 '보낸 뒤에 난 timeout' 을 '아예 안 보냄' 과 똑같이 처리하는 catch 블록이야. 그 둘은 비용이 다른 서로 다른 상태거든. '알 수 없음, 재시도 금지' 를 뜻하는 세 번째 결과를 더하고, 나중에 조사할 수 있을 만큼 기록해두고, 사람이 명시적으로 해결하는 길(일어남 / 안 일어남 / 일부러 다시 실행) 을 정의해. 목표는 기계가 추측만으로 돈을 쓰거나 뭔가를 부수지 않게 하는 거야.

Progress

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

댓글 0

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

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