토큰 값은 진실이 아니다

초기 모델은 단순했습니다. 토큰이 있으면 연동된 계정이다. 이 모델은 규모가 커지자 바로 무너졌습니다. 토큰이 있어도 만료됐을 수 있고, 유효해도 사용자가 탈퇴하려고 연동을 끊었을 수 있고, 계정이 비즈니스에서 개인 계정으로 바뀌어 API 자체가 막혔을 수도 있습니다. 그래서 토큰 문자열과 별개로 tokenStatus라는 명시적 상태를 만들어 운영의 단일 진실로 삼았습니다.

tokenStatus
  active            정상. 수집과 자동화 대상
  expired           만료. 재연동 안내 대상
  disconnected      사용자가 의도적으로 끊음.
                    API 호출 금지, 재연동 안내도 금지
  personal_account  개인 계정 전환. API 불가, 재시도 무의미

상태마다 시스템이 해도 되는 일이 정해져 있습니다. disconnected는 단순한 에러 상태가 아니라 사용자의 의사 표시라서, 이 상태의 계정에는 API 호출도 하지 않고 다시 연결하라는 알림도 보내지 않습니다.

에러는 의도를 덮어쓸 수 없다

이 설계에서 제일 비싸게 배운 규칙입니다. Meta API가 흔들리는 날에는 멀쩡한 토큰도 401과 타임아웃을 뱉습니다. 그때 에러 핸들러가 순진하게 상태를 expired로 바꿔버리면 외부 장애 한 번에 수천 계정이 만료로 오염되고, 사용자 수천 명에게 잘못된 재연동 안내가 나갑니다. 반대 방향도 마찬가지입니다. API 에러가 disconnected를 덮어쓰면 떠난 사용자를 시스템이 다시 건드리게 됩니다.

그래서 상태 전이에 방향 규칙을 뒀습니다. 일시적 실패는 상태를 바꾸지 못합니다. expired 판정은 명확한 영구 오류 코드에서만 내립니다. 사용자 의도 상태인 disconnected는 오직 사용자 행동으로만 바뀝니다. 상태 머신을 만들어 보니 가치는 상태의 개수가 아니라 금지된 전이에 있었습니다.

서킷 브레이커는 계정이 아니라 시스템을 지킨다

4,800개 계정을 35개 크론이 순회하는 구조에서 Meta가 전체적으로 흔들리면 재시도가 재시도를 부르면서 우리 쪽 워커와 상대의 레이트리밋을 같이 태웁니다. 그래서 토큰 처리 경로에 서킷 브레이커를 걸었습니다. 실패가 임계를 넘으면 해당 경로를 잠시 끊고, 끊겼다는 사실 자체를 치명 등급 경보로 올립니다.

개별 계정의 실패는 로그로 남기고, 서킷이 열린 건 사건으로 취급합니다. 경보 등급을 이 둘로 나눈 덕분에 알림이 많아서 아무도 안 보는 상황을 피할 수 있었습니다.

문서로 박제한 이유

이 규칙들은 운영 문서로도 남겨 뒀습니다. 새 기능이 토큰을 만질 때마다 같은 논쟁을 반복하고 싶지 않았기 때문입니다. 1인 개발이라도, 아니 1인 개발이라서 더, 미래의 나와 AI 에이전트가 따라야 할 운영 정책은 코드 바깥의 문장으로 존재해야 한다고 생각합니다.