다른 요청의 저장값을 돌려주는 것은 이 요청의 저장이 아니다

오개념 후보에 두 교수자가 각각 유효·무효 판정을 제출한다고 해 보자. 아직 판정 행이 없으면 두 upsert가 생성 경합을 할 수 있다. 먼저 저장한 요청이 유효이고, 무효를 제출한 요청이 중복 키 오류를 받았을 때 기존 행을 다시 읽어 반환하면 어떻게 될까. 응답은 성공처럼 끝나지만 무효 판정은 한 번도 저장되지 않는다.

질문 분석의 피드백 구현은 이 처리를 재조회에서 갱신 재시도로 바꿨다. 제품 통합 전의 기능 구현이다.

숫자 11000인 생성 경합에서는 같은 filter, 같은 $set, 같은 upsert를 한 번 더 실행한다. 두 번째 호출도 실패하면 오류를 전달한다. 월 카운터의 첫 문서 경합과 같은 오류를 만나지만, 여기서 중요한 것은 행의 존재가 아니라 호출자가 제출한 판정까지 썼는가이다.

판정은 교수자가 아니라 과목과 후보 설명에 붙는다

유일 키는 (course_id, topic_segment_id, description_hash)다. 교수자별 행을 만들지 않고 같은 후보의 최신 판정을 과목에서 공유한다. reviewed_by는 그 최신값을 남긴 사람이며 식별 키가 아니다.

hash는 마스킹 전 설명 원문에서 계산한다. 표시용 마스킹 규칙이 바뀌었다는 이유로 같은 후보의 판정이 다른 대상을 가리키지 않게 하려는 것이다. 아래는 실제 공용 함수의 반환식이다.

return createHash('sha1').update(rawDescription, 'utf8').digest('hex');

서버가 만든 hash를 화면이 그대로 되돌려 보내고, 조회 결과도 segment와 hash를 함께 키로 병합한다. 설명이 달라지면 다른 hash의 후보로 구분된다. 철회는 hash 누락·공백을 먼저 거절한 뒤 같은 세 키로 deleteOne한다. 식별 조건이 빠진 삭제로 범위를 넓히지 않기 위해서다.

최신값을 덮는 정책은 충돌 검출을 대신하지 않는다

재시도는 이 요청의 쓰기를 적용할 기회를 한 번 더 준다. 같은 후보에 서로 다른 판정이 이어지면 마지막으로 적용된 쓰기가 남는다. 현재 설명이나 예상 이전 판정을 조건으로 비교하는 CAS는 아니다. hash가 후보를 구분한다고 해서 오래된 화면의 제출을 자동 거절하지도 않는다.

테스트는 후보의 과목 소속 확인, 재판정 filter, 중복 키 이후 동일 갱신 두 번과 호출자값 반환, 다른 오류 전달을 확인한다. 철회의 hash 검사와 이미 없는 행 삭제, segment·hash별 조회 병합도 다룬다. MongoDB의 실제 병렬 경합을 실행한 테스트는 아니므로 요청 순서나 격리 수준의 검증으로 읽을 수는 없다. 저장의 성공을 정의할 때는 “행이 있다”와 “이 요청의 값이 적용됐다”를 구분해야 한다.