[BE-74] 매칭 소급 재평가 백필 · 신규 매칭 공고 알림 억제 (FR-87)
작업 내용 (설계 의도)
근거 TDD: 20260808-지원관리-확장-tdd.md — “방안 6 Spring Batch 미채택”, “Release Scenario 3-5·3-7”
변경 사항
FR-87은 매칭 범위 확대 적용 시 저장된 전체 공고를 새 기준으로 1회 소급 재평가하되 신규 매칭 공고에 알림을 보내지 않을 것을 요구합니다.
POST /api/matching/field-scope-backfills— 실행 시 ①match_criteria_revisions에MATCH_FIELD_SCOPE1행 삽입(새 revision) ② 500건 페이지 순회로 전 공고 재평가 ③ 이전 결과 미매칭 && 새 결과 매칭인 공고의 알림 억제.- revision 삽입이 트리거입니다 — 기존 재평가 로직이
criteria_revision비교로 대상을 판별하므로(JobPostingEvaluationDomainService.kt:97-98), 새 revision을 만들면 전 공고가 자동으로 재평가 대상이 됩니다. 별도 대상 목록을 만들지 않습니다. - Spring Batch를 도입하지 않습니다 — 기존
EvaluateJobPostingsUseCase.kt:83-88(500건 페이지)·:48-50·:109-118(대상별REQUIRES_NEW격리) 패턴이 청크 커밋·락 범위 축소·멱등이라는 5단계 절차의 취지를 이미 충족합니다. 일생 1회 백필에 메타 테이블 6개는 과합니다. 이 예외 근거를 코드 주석에 남깁니다. - 알림 억제(FR-87) —
JobPosting.suppressNotification()으로notification_eligible=0을 세웁니다. 대상은 “이전 평가에서 미매칭 && 새 평가에서 매칭”인 공고만입니다 — 이미 매칭이던 공고는 건드리지 않습니다.- 크로스 컨텍스트(matching 결과로 posting을 수정)라 application 레이어 UseCase가 조합합니다.
- 이 조치가 없으면 최근 7일 내 발견된 공고가 소급 재평가로 새로 매칭되며 09:00 배치가 알림을 쏟습니다(
findAllNotificationCandidates(firstSeenAfter)).
dryRun=true— revision을 만들지 않고 재평가 결과만 계산해 통계를 반환합니다. Release Scenario 3-5에서 오탐 규모를 사전 점검하는 수단입니다.unmatchedNowCount(기존 매칭이 새 기준에서 풀린 건)가 0이어야 안전합니다.- 멱등 — 이미 새 revision으로 평가된 공고는 skip되므로 중단 후 재실행이 안전합니다.
- 근거 쓰기량 증가는 실측상 미미합니다 (A-2, dba 실측 정정) — BE-69가
match_score > 0인 전부를 저장하도록 확대됐지만, 전량 재평가 시 추가되는 자식 행은 공고당 평균 +0.09행(6,633건 기준 총 +602행)입니다. 본문 보유 공고가 13.8%뿐이라 확대분 자체가 작습니다. 최초 판정 “자식 쓰기 최대 6.7배”는 과대 추정이었고, 락 범위·소요 시간 모두 사실상 불변입니다.dryRun사전 점검은 그대로 수행합니다.
롤백 지점: 플래그 matching.field-weighted-scope OFF로 매칭 규칙은 즉시 제목 단독으로 복귀합니다. 단 억제된 notification_eligible=0은 되돌리지 않습니다 — 이는 “알림 과다 발송”이 아니라 “알림 누락” 방향이고, 그 공고들은 이미 목록에 노출되므로 사용자 손실이 없습니다. 이 판단을 티켓·코드 주석에 명시합니다.
의존
- BE-69 (필드별 가중치 매칭)
다이어그램
처리 흐름
sequenceDiagram participant Op as 운영자 participant C as JobPostingEvaluationApiController participant U as BackfillFieldScopeMatchingUseCase participant M as MatchCriteriaRepository participant E as JobPostingEvaluationDomainService participant P as PostingDomainService Op->>C: POST /api/matching/field-scope-backfills C->>U: execute(dryRun) alt dryRun=false U->>M: bumpRevision(MATCH_FIELD_SCOPE) end loop 500건 페이지 U->>P: 평가 대상 페이지 조회 U->>E: 이전 결과 조회 → evaluateOne (REQUIRES_NEW) alt 미매칭 → 매칭 전환 U->>P: suppressNotification() end end U-->>C: processed / newlyMatched / suppressed / unmatchedNow
클래스 의존
flowchart LR subgraph Presentation["presentation/matching"] Api[JobPostingEvaluationApiController] end subgraph Application["application/matching"] UC[BackfillFieldScopeMatchingUseCase] end subgraph Domain["domain"] EDS[JobPostingEvaluationDomainService] MRepo[MatchCriteriaRepository] PDS[PostingDomainService] Posting[JobPosting] end Api --> UC UC --> EDS UC --> MRepo UC --> PDS PDS --> Posting
테스트 케이스
- 백필 실행 시
MATCH_FIELD_SCOPErevision이 1건 생성된다 - 전 공고가 새 revision으로 재평가되고
processedCount가 대상 수와 같다 - 이전 미매칭 → 새 매칭인 공고의
notification_eligible이 0이 된다 - 이미 매칭이던 공고의
notification_eligible은 변경되지 않는다 - 억제된 공고가 09:00 알림 배치의 후보에서 제외된다
- 백필 후 신규 매칭 공고에 대한 알림이 0건이다 (FR-87 핵심)
- 백필을 재실행하면 이미 새 revision으로 평가된 공고가 skip된다 (멱등)
- 중단 후 재실행하면 남은 공고만 처리한다
dryRun=true면 revision이 생성되지 않고 통계만 반환된다dryRun=true면notification_eligible이 변경되지 않는다unmatchedNowCount가 기존 매칭이 새 기준에서 풀린 건수를 정확히 센다- 대상 1건이 실패해도 나머지가 계속 처리된다 (REQUIRES_NEW 격리)
- 대상이 0건이면 즉시 200을 반환한다 (0건 경계)
- 페이지마다 커밋해 단일 트랜잭션으로 전체를 잠그지 않는다
- 미매칭 공고에도 근거가 저장되어 백필 후 자식 행 수가 매칭 성립분만 저장할 때보다 많다 (A-2)
- 본문이 없는 애그리게이터 공고에는
DESCRIPTION_BODY근거가 생성되지 않는다 (확대분이 작은 구조적 이유) - 근거 쓰기량이 늘어도 페이지 단위 커밋이 유지되어 락 범위가 행 단위다