동시 제보 요청의 10분 한도 초과 해결

이 글의 목차 7개 항목

문제와 원인

최근 10분 제보가 9건인 사용자에게 요청 2개가 동시에 들어오면, 분리된 COUNT와 INSERT 사이에서 두 요청 모두 9건을 읽었다. 각 요청이 저장을 진행해 최종 11건이 됐고 응답은 둘 다 HTTP 201이었다.

수정 전 코드

POST

const recentReportCount = await repository.countRecentForUser(
  userKey,
  new Date(Date.now() - REPORT_RATE_WINDOW_MS).toISOString(),
);
if (recentReportCount >= REPORT_RATE_LIMIT) {
  return json(
    { error: "제보 요청이 너무 많습니다. 10분 후 다시 시도해 주세요." },
    429,
    { "Retry-After": String(REPORT_RATE_WINDOW_MS / 1_000) },
  );
}
const id = crypto.randomUUID();
const createdAt = new Date().toISOString();
await repository.create({ id, userKey, category, title, description, questionId, createdAt });
return json({ report: { id, status: "new", createdAt } }, 201);

countRecentForUser / create

async countRecentForUser(userKey: string, since: string) {
  const row = await this.connection().prepare(`
    SELECT COUNT(*) AS count
    FROM user_reports
    WHERE user_key = ? AND created_at >= ?
  `).bind(userKey, since).first<{ count: number }>();
  return Number(row?.count ?? 0);
}
INSERT INTO user_reports (
  id, user_key, category, title, description, page_path,
  question_id, status, admin_note, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, '', ?, 'new', '', ?, ?)

수정 후 코드

제보 저장 SQL

사전 개수 조회를 제거하고 같은 사용자의 최근 10분 제보 수가 10건 미만일 때만 INSERT하도록 바꿨다. 사용자 키·구간 시작 시각·한도 10을 SQL에 바인딩한다.

INSERT INTO user_reports (
  id, user_key, category, title, description, page_path,
  question_id, status, admin_note, created_at, updated_at
) SELECT ?, ?, ?, ?, ?, '', ?, 'new', '', ?, ?
WHERE (
  SELECT COUNT(*) FROM user_reports
  WHERE user_key = ? AND created_at >= ?
) < ?;

영향받은 행 수가 1개면 저장 성공을 반환한다.

return Number(result.meta.changes ?? 0) === 1;

첫 요청이 10번째 행을 저장하면 다음 요청의 meta.changes는 0이 된다. 처리 함수는 저장 성공에 HTTP 201, 한도 거절에 HTTP 429·Retry-After: 600을 반환한다.

검증 결과

Wrangler의 로컬 D1에서 원본 POST 처리 함수·저장소를 수정 전후 각각 5회 실행했다. 매회 별도 테스트 사용자에게 최근 제보 9건을 넣고 요청 2개를 동시에 보냈으며, 수정 전에는 두 COUNT가 끝난 뒤 삽입하도록 실행을 겹쳤다.

실행초기 제보 수수정 전 응답·최종 행 수수정 후 응답·최종 행 수
1회9건201·201, 11건201·429, 10건
2회9건201·201, 11건201·429, 10건
3회9건201·201, 11건201·429, 10건
4회9건201·201, 11건201·429, 10건
5회9건201·201, 11건201·429, 10건
5회 집계수정 전수정 후
요청 10개 중 새 저장10건5건
최근 10분 한도 초과5회0회
최종 제보 수 최소·최대11·11건10·10건
D1 집계: 수정 전 5회 11건, 수정 후 5회 10건원본 보기 (새 창)

D1 집계: 수정 전 5회 11건, 수정 후 5회 10건

100%원본 보기 (새 창)

확대한 이미지는 상하좌우로 이동할 수 있습니다. 키보드는 이미지 영역에 초점을 두고 방향키를 사용하세요. Esc로 닫습니다.