한 일

발신자 오인 현상의 진짜 원인을 찾았다. /생성으로 파티를 만들면 만든 사람이 파티장(slot1)이 된다. 그런데 가끔 직전에 말한 사람이 파티장으로 잡혔다. “가끔"이라는 게 단서였다.

코드를 뒤졌는데 봇이나 서버 잘못은 아니었다. 카카오톡 알림을 읽어오는 안드로이드 앱(메신저봇R)이 문제였다. 메시지가 연달아 빠르게 오면, 발신자와 내용의 짝이 어긋나게 파싱됐다. 그래서 msg.author.name에 직전 사람이 들어왔다. 타이밍이 꼬이는 거라 가끔만 터졌다. 사용자가 말한 “간헐적"이라는 증언과 딱 맞았다.

봇과 서버는 받은 값을 그대로 넘길 뿐이다. 문제는 폰 위 알림 단계에서 생긴다. 그래서 스크립트로 미리 막을 수가 없었다. 막을 수 없다면, 일어난 뒤에 고칠 길을 만들기로 했다.

사후 정정 명령 /접수를 만들었다. 파티장이 잘못 잡혔을 때, 진짜 만든 사람이 /접수 <파티명>을 치면 자기로 바꿔주는 명령이다. 서버에는 POST /api/party/claim으로 붙였다.

아무나 남의 파티를 가져가면 안 되니 조건을 좁게 걸었다.

  • slot1만 찬 1인 파티만 (다른 멤버가 이미 있으면 거부)
  • 만든 지 5분 안쪽만 (CLAIM_WINDOW_MIN)
  • 행 잠금(FOR UPDATE)으로 동시에 들어와도 안전하게
  • 이미 본인이 파티장이면 그냥 통과(멱등), 만료·1인1파티·없는 파티는 각각 막음

영향 받은 곳은 서버의 claim 핸들러(server/routes/party.js)와 봇 스크립트의 접수 분기(bot-script/payload.js)다.

테스트를 7개 추가했다. 성공, 멤버가 이미 있어 거부, 5분 만료, 다른 파티 소속, 멱등, 없는 파티, 잘못된 요청(400). 이렇게 7케이스를 넣었다. 전체 테스트는 65개가 다 통과했다.

문서와 다이어그램을 맞췄다. CLAUDE.mdREADME.md의 명령어 표, API 표, 파티 규칙을 갱신했다. 테스트 수도 51개에서 65개로 고쳤다. README.md/설명 줄이 옛날 형식으로 남아 있어서 바로잡았다. 파티 흐름 다이어그램에는 /접수 정정 경로를 점선으로 하나 그려 넣었다.

운영에 배포했다. PR #68을 main에 머지했다. 테스트→스테이징→운영까지 파이프라인이 전부 초록불이었다. payload를 다시 서명하고, 버전 API가 새 값을 내보내는지 확인했다. 폰은 폴링으로 알아서 새 스크립트를 받아간다.

돌아보며

처음엔 내 코드 어딘가에 버그가 있다고 생각했다. 당연히 그럴 줄 알고 코드를 한참 뒤졌다. 그런데 봇도 서버도 깨끗했다. 범인은 더 아래, 폰 위 알림을 읽는 단계에 있었다.

이게 좀 묘했다. 내가 고칠 수 없는 곳에 원인이 있었으니까. 보통은 원인을 찾으면 거기를 고치면 된다. 그런데 이번엔 “여기는 못 고친다"가 결론이었다. 처음엔 답답했다.

그래서 생각을 바꿨다. 막을 수 없으면, 일어난 뒤에 되돌릴 길을 주자. /접수는 그렇게 나왔다. 완벽한 해결은 아니다. 진짜 만든 사람인지 서버가 증명할 방법이 없으니, 5분 안에는 남이 가로챌 틈도 생긴다. 그래서 창을 5분으로 좁히고, 1인 파티로만 막고, 그래도 남는 위험은 지켜보기로 했다.

“가끔"이라는 한마디를 흘리지 않은 게 다행이었다. 그 단어가 타이밍 문제를 가리키고 있었다. 증언을 곧이곧대로 믿고 따라간 게 맞았다. 못 고치는 문제 앞에서 포기하는 대신, 우회로를 내는 것도 일이라는 걸 다시 느꼈다.