[FEAT] 기존 문서 참조 backfill 및 검증 기능 구현 - #207
Conversation
전체 문서를 한 번에 적재하지 않고 안정적인 ID 오름차순으로 크루 문서 식별자만 페이지 조회할 수 있도록 read model과 조회 메서드를 추가한다.
문서마다 REQUIRES_NEW transaction을 시작해 source를 잠금 조회하고 기존 동기화 service를 재사용한 뒤 저장 결과를 다시 읽어 불일치를 검증한다. 실패 정보는 문서 UUID와 예외 유형만 담아 본문과 raw 메시지를 남기지 않는다.
기존 크루 문서를 안정적인 순서로 페이지 반복하며 문서별 처리 결과를 모으고, 처리·성공·실패·추가·삭제·제외·불일치 집계를 담은 결과를 반환한다. 전체를 하나의 transaction으로 감싸지 않아 문서별 실패가 격리된다.
빈 문서, 페이지 미만, 페이지 경계 초과, 정확한 배수 실행을 검증하고 두 번째 실행의 추가·삭제·불일치가 0인지 확인한다. 한 문서 실패가 다음 문서 처리를 막지 않고 해당 문서 변경만 롤백되는지, 페이지 크기 검증이 동작하는지 함께 검증한다.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| CrewDocument sourceDocument = crewDocumentRepository.findByUuidForUpdate(sourceDocumentUuid) | ||
| .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); | ||
| DocumentReferenceSyncResult syncResult = documentReferenceSyncService.synchronize(sourceDocument); | ||
| boolean mismatched = isMismatched(sourceDocument, syncResult.expectedTargetDocumentUuids()); |
There was a problem hiding this comment.
현재는 참조 불일치가 발생해도 성공으로 커밋됩니다.
불일치 시 해당 문서의 트랜잭션만 롤백하고 실패로 집계하면 좋을 것 같습니다. 관련 테스트도 추가해주세요!
There was a problem hiding this comment.
확인해보니 지적하신 것보다 더 근본적인 문제가 있었습니다. 결론부터 말씀드리면 불일치가 발생할 수 있는 경로 자체가 없어서, 롤백을 붙여도 실행되지 않는 방어 코드가 됩니다. 그래서 검증 로직과 mismatchCount 지표를 제거하는 쪽으로 반영했습니다.
왜 항상 일치하는가
DocumentReferenceSyncService의 diff 계산이 이렇습니다.
referencesToRemove=existing중 target이expected에 없는 것referencesToAdd=expected중existing에 없는 것
따라서 동기화 후 집합 = (existing ∩ expected) ∪ (expected \ existing) = 정확히 expected 입니다.
그런데 제 isMismatched는 이걸 다시 읽어서 syncResult.expectedTargetDocumentUuids(), 즉 같은 expected 와 비교하고 있었습니다. 정의상 항상 같습니다.
나머지 경로도 막혀 있습니다.
deleteAll/saveAll이 실패하면 불일치가 아니라 예외가 나서 이미 해당 문서 transaction이 롤백됩니다.- 같은 source-target 중복 행은
uk_document_reference_source_target제약이 막습니다. - 같은 문서 동시 수정은
findByUuidForUpdate의 X-lock으로 직렬화됩니다.
즉 mismatchCount: 0은 "검증 통과"가 아니라 "측정한 적 없음" 이었습니다. 이 지표를 남긴 채 머지하면 나중에 운영자가 0을 보고 검증됐다고 오독할 위험이 있어서, 롤백을 붙이는 것보다 지표를 걷어내는 쪽이 안전하다고 판단했습니다.
그럼 검증은 무엇으로 하는가
재실행 자체가 커밋 후 검증입니다.
2차 backfill은 새 transaction에서 DB의 existing을 다시 읽어 본문에서 계산한 expected와 diff합니다. 따라서
2차 실행 added == 0 && removed == 0 ⟺ 저장 집합 == 본문 기대 집합
이 성립합니다. 같은 transaction 안에서 자기가 방금 쓴 값을 되읽던 기존 방식과 달리, 커밋된 실제 DB 상태를 별도 transaction에서 확인하는 것이라 훨씬 강한 검증입니다.
backfill_success_bySecondExecution이 이미 이걸 검증하고 있고, 운영 절차도 "backfill을 두 번 실행해 2차의 added/removed가 0인지 확인"으로 정리하면 됩니다.
요청하신 불일치 테스트를 작성하려다 작성이 불가능하다는 걸 발견한 게 이 판단의 출발점이었습니다. 짚어주셔서 감사합니다. 덕분에 의미 없는 지표를 머지 전에 걷어냈습니다.
다르게 보시는 부분 있으면 말씀해주세요.
동기화 결과 집합은 정의상 기대 집합과 항상 같아 같은 transaction 안의 재조회 검증은 불일치를 감지할 수 없었다. 항상 0인 지표는 검증됐다는 오해를 부르므로 검증 로직과 mismatchCount를 제거한다. 저장 결과 검증은 재실행으로 대체한다. 두 번째 실행은 새 transaction에서 커밋된 상태를 다시 읽어 diff하므로 added와 removed가 0이면 저장 집합이 본문 기대 집합과 같음이 보장된다. 문서마다 참조 재조회 쿼리 1회도 함께 줄어든다.
관련 이슈
Closes #206
변경 배경
쓰기 동기화(#204)가 적용되기 전에 이미 존재하던 크루 문서는 본문을 수정하기 전까지 참조 행이 생기지 않습니다.
영속 참조 기반 그래프 조회로 전환하기 전에, 기존 문서 전체의 참조를 한 번 재구축하고 저장 결과가 현재 본문과 일치하는지 검증할 수 있어야 합니다.
변경 내용
CrewDocumentIdentifierReadModel(id, uuid)projection만 조회해 전체 entity를 적재하지 않습니다.DocumentReferenceBackfillItemService: 문서마다REQUIRES_NEWtransaction에서 source를 잠금 조회하고DocumentReferenceSyncService.synchronize를 재사용합니다. 참조 추출·유효성·diff 정책은 다시 구현하지 않았습니다.added == 0 && removed == 0이면 저장 집합이 본문 기대 집합과 같음이 보장됩니다.DocumentReferenceBackfillService: transaction 없이 페이지를 반복하는 orchestration. self-invocation 없이 proxy가 적용되도록 item transaction과 bean을 분리했습니다.DocumentReferenceBackfillResult(processedCount, succeededCount, failedCount, addedCount, removedCount, excludedSelfCount, excludedMissingCount, failures). 실패 상세는DocumentReferenceBackfillFailure(sourceDocumentUuid, causeType)로 문서 UUID와 예외 유형만 담아 본문·raw 예외 메시지를 남기지 않습니다.검증
JAVA_HOME=$(/usr/libexec/java_home -v 17) ./gradlew test --tests '*DocumentReferenceBackfillServiceTest'JAVA_HOME=$(/usr/libexec/java_home -v 17) ./gradlew testbackfill(10)페이지 미만,backfill(4)마지막 페이지가 작은 경우,backfill(3)정확한 배수, 빈 데이터backfill(2)processed=6, succeeded=6, failed=0, added=5, removed=0, source별 저장 집합이 기대값과 일치. 빈 데이터는 전부 0backfill(4)2회added=0, removed=0, 저장 집합 불변DataIntegrityViolationException발생processed=5, succeeded=4, failed=1. 실패 문서 참조는 이전 상태 유지로 해당 transaction만 롤백, 뒤 순서 문서는 정상 처리backfill(0),backfill(-1)WikiException(PAGE_BAD_REQUEST)영향 및 참고사항
build.gradle도 변경하지 않았습니다. 운영 실행은 별도 배포 계획에서 수행합니다.ErrorCode.PAGE_BAD_REQUEST를 사용했고 새 에러 코드를 만들지 않았습니다.DOCUMENT_NOT_FOUND실패로 집계되며 재실행으로 해소됩니다.리뷰 반영
(existing ∩ expected) ∪ (expected \ existing)=expected인데, 검증이 그expected와 비교하고 있어 정의상 항상 일치했습니다. 항상 0인 지표는 검증됐다는 오해를 부르므로9a6054a에서 검증 로직과mismatchCount를 제거하고, 저장 결과 검증은 재실행 멱등성으로 대체했습니다.리뷰 시 봐주시면 좋은 점
EXPECTED_CHANGE초안에 없던DocumentReferenceBackfillItemResult와DocumentReferenceBackfillAccumulator를 추가했습니다. 문서별 결과 전달과 집계 책임을 분리하기 위해서인데, 과한 분리인지 봐주세요.@Transactional환경에서REQUIRES_NEW가 테스트 transaction을 suspend하는 문제 때문에 비transactional@SpringBootTest+@DirtiesContext로 구성했습니다. 롤백을 실제로 증명하려고 spy가 real sync를 수행한 뒤 예외를 던지게 했는데, 더 나은 방식이 있다면 알려주세요.