명품몰 상품 일괄 등록 ProductImportService 를 고치고 같은 xlsx 를 여러 번 넣으면서 결과를 봐야 했다. 두 번째 시도에서 이미 있다는 메시지가 나와서 무엇을 기준으로 중복이라고 하는지부터 찾았다.
Table of contents
Open Table of contents
검증 — 무엇이 0이어야 하는가
검사 코드는 brand_id 와 supplier_sku 두 컬럼의 조합으로 COUNT(*) 를 세고 있었다. 그 개수가 0이어야 다시 가져올 수 있다는 뜻이다.
반복 테스트의 조건은 이렇게 코드 안에 적혀 있었다. 무엇을 지워야 하는지는 삭제 쪽이 아니라 검사 쪽을 읽어야 나온다.
원인 — 소프트 삭제로 부족한 이유
두 가지 방법이 있었다. deleted_at 표식만 세우는 것과, 실제로 행을 지우는 것이다. 처음에는 앞쪽으로 지웠는데 여전히 같은 메시지가 나왔다. 중복 검사 쿼리에 deleted_at IS NULL 조건이 없었고 JPA 의 @Where 필터도 네이티브 쿼리인 이 경로에서는 걸리지 않았다.
뒤쪽을 골랐다. 이유는 검사가 표식을 안 보면 표식만으로는 개수가 줄지 않아서다. 대신 되돌릴 수 없는 삭제라 대상 목록을 먼저 뽑는 절차가 붙는 것을 감수했다.
참조의 역순
하나를 지우려면 연결된 것을 먼저 지워야 하므로 참조 관계를 그렸다. product_images 와 product_options 와 stock 과 option_groups 와 product_categories 와 product_brands 와 product_detail_html 이 products 에 매달려 있었다.
삭제는 잎에서 뿌리로 가는 참조의 역순이다. 순서를 뒤집으면 외래키 제약에 걸려 중간에 멈추고 반쯤 지워진 상태가 남는다.
주의 — 공유되는 자원의 참조 수
product_detail_html 이 특이했는데 여러 상품이 같은 본문을 참조할 수 있는 구조였다. 그래서 그 본문을 가리키는 상품 수를 먼저 세고 하나일 때만 함께 지웠다.
확인하지 않고 지우면 멀쩡한 다른 상품의 본문이 사라진다. 공유되는 자원은 지우는 쪽이 아니라 남는 쪽을 먼저 세야 한다.
조치 — 명령 성공과 실제 삭제는 다르다
MariaDB 만 지우면 버킷에 파일이 남으므로 images/{image_id}/ 접두사 단위로 deleteObjects() 했다. 접두사가 정확한지 먼저 확인했는데 잘못된 값으로 지우면 남의 것까지 사라지기 때문이다.
지운 뒤에는 같은 조건으로 다시 COUNT(*) 를 세어 0인지 확인했다. 조건이 안 맞아 아무것도 안 지워졌을 수도 있고 다른 경로로 다시 생겼을 수도 있으므로 명령의 성공을 결과로 삼지 않았다.
이 과정을 반복해야 해서 reset_import.sh 로 만들되 --dry-run 으로 지우기 전에 대상 목록을 먼저 출력하게 했다. 실제로 한 번 멈췄는데 조건이 넓어서 다른 브랜드 상품까지 잡혀 있었다.
정리
- 반복 테스트의 조건은 중복 검사 쿼리 안에 적혀 있다
- 무엇을 지워야 하는지는 검사 쪽을 읽어야 나온다
- 검사가
deleted_at을 안 보면 소프트 삭제로는 개수가 안 준다 - 삭제는 잎에서 뿌리로 참조의 역순으로 한다
- 순서를 뒤집으면 반쯤 지워진 상태가 남는다
- 공유되는
product_detail_html은 참조 수를 세고 하나일 때만 지운다 - 버킷도 함께 지우되 접두사가 정확한지 먼저 확인한다
- 지운 뒤 되읽어 0인지 확인하고
--dry-run으로 대상 목록을 먼저 출력한다