대니
danny's blog@dannywon_dev
발행 129 · 대기 183

1인 개발자. 클로드 코드를 팀처럼 굴린다. 매일 겪은 것만 쓴다.

클로드 코드

pg_cron 이 학생 32명을 결석으로 만들었다

전제가 바뀐 뒤에도 크론은 그대로 돌았다. 아무도 저장 안 한 회차가 출석률 0%로 인쇄됐다.

아무도 저장한 적 없는 회차의 보고서가 「학생 12명 전원 결석 · 출석률 0%」로 인쇄되는 걸 보면, 처음엔 화면 버그라고 생각하게 된다.

방과후 이스포츠 출결 앱이다. 8/27 첫 수업을 사흘 앞두고 화면을 점검하다가 이걸 봤다. 코치가 그 회차를 열어본 적도 없는데 종이에는 전원 결석이 찍혀 나갔다.

범인은 pg_cron 작업 하나였다.

-- 10분마다: 마감 지난 회차의 미체크(expected) → 결석(absent)
update attendance_records r set status = 'absent'
  from class_sessions s
 where r.session_id = s.id and r.status = 'expected' and s.close_at < now()

이 크론은 틀린 코드가 아니었다

만들 때는 맞았다. 원래 설계에서는 학생이 자기 폰으로 QR 을 찍었고, 마감까지 안 찍힌 행은 진짜로 「안 왔다」였다. 미체크를 결석으로 굳히는 게 정확한 동작이었다.

2026-08-14에 전제가 뒤집혔다. 학교에서 학생이 폰을 못 쓴다는 걸 알고 「코치가 결석자만 눌러 저장」으로 바꿨다. 그 순간부터 마감 뒤에 남은 expected 의 뜻이 달라졌다 — 「학생이 안 왔다」가 아니라 「코치가 아직 저장을 안 했다」다.

크론은 그대로 돌면서 그걸 결석으로 굳혔다. 아무도 크론을 다시 안 봤다.

여기에 두 가지가 더 겹쳤다.

  1. syncAttendance — 명단을 저장할 때 (회차 × 학생) 출결 행을 expected 로 미리 깔아 둔다.
  2. 화면 배지가 그 행 개수를 셌다 — total: mine.length.

그래서 명단을 넣는 순간 모든 회차가 ✓ 출결 12/12 로 보였다. 코치가 저장 버튼을 한 번도 안 눌러도 그렇게 보였다.

코치가 저장을 깜빡함
  → 10분 뒤 크론이 전원 결석으로 굳힘
  → 화면 배지는 「✓ 출결 12/12」
  → 「덜 채운 수업」 목록에도 안 뜸
  → 아무도 못 잡음
  → 보고서에 「그날 전원 결석」으로 인쇄

화면만 고치면 된다고 봤다 — 아니었다

처음 판단은 화면 쪽이었다. 배지가 expected 를 세지 않게 counts.written(사람이 저장한 수)을 따로 만들었다. 그걸로 끝난 줄 알았다.

데모 데이터로 재현해 보니 아니었다. 01:34에 미체크 행을 넣었고, 01:40에 크론이 지나가면서 32명을 absent 로 바꿨다. 6분이었다.

그리고 결석은 「사람이 적은 것」으로 세어진다. 그게 맞다 — 결석도 기록이니까. 그러니 크론이 살아 있는 한 화면 고침은 무효였다. expectedabsent 로 바뀌는 순간 다시 「적힌 것」이 되기 때문이다.

가른 질문은 이거였다. 이 값이 「사람이 적은 것」인가, 「기계가 채운 것」인가. 화면은 그 둘을 구분할 수 있어도, 기계가 사람 흉내를 내면 구분할 방법이 없다.

크론을 껐다

마이그레이션 한 줄이다.

perform cron.unschedule('mark-absent');

껐더니 안 적은 회차는 종이에도 「미체크 ·」로 나간다. 그게 사실이다. 같은 보고서가 이렇게 바뀌었다.

출석 결석 출석률
0 12 (전원 ×) 0%
0 0 (전원 미체크 ·)

화면 판정도 같이 바꿨다. counts.writtenexpected 를 안 센다. 한 명도 안 적힌 회차는 출석률을 0% 가 아니라 로 찍는다. 전원이 미체크인데 0% 를 찍으면 「그날 아무도 안 왔다」는 뜻이 되어, 안 적었다는 사실이 자료로 둔갑한다.

남은 것

8/14에 출결 방식을 뒤집을 때 크론은 코드 리뷰 시야 밖에 있었다. DB 안에 있어서 grep 에도 안 걸리고, 마이그레이션 파일을 열어야 보인다.

체크리스트를 만들 만한 자리다. 설계 전제를 바꿀 때 「그 전제를 읽는 것」 목록을 먼저 적는다. 코드·크론·트리거·외부 스케줄러까지.

← 목록으로