상품 삼천여 건의 할인을 일괄로 없애야 했는데 건수가 많아 화면으로는 처리할 수 없었다. 직접 SQL이나 artisan 커맨드로 하되 정식 수정 경로와 같은 이력을 남기고 싶었고 그러려면 정식 경로가 무엇을 건드리는지부터 알아야 했다.
Table of contents
Open Table of contents
세 번의 탐색과 매번의 누락
첫 탐색에서 products와 전체 이력 product_histories 둘을 찾고 그것이 전부라고 봤다. 두 번째 탐색에서 가격 추적 price_trackings가 하나 더 나왔고 세 번째에서 변경 유형별 이력 product_change_logs와 정산가 settlement_prices가 또 나왔다.
볼 때마다 새로 나온다는 것 자체가 방법의 문제를 가리키고 있었다. grep -rn으로 키워드를 찾는 방식은 키워드를 바꿀 때마다 새 결과가 나오므로 못 찾은 것이 있는지를 알 수 없다.
검증 — 전수 추적으로 확정한 여섯
방식을 바꿔 ProductService::update()의 호출 흐름을 처음부터 끝까지 따라갔다. products 갱신과 전체 이력과 변경 유형별 이력과 가격 추적과 정산가와 MarketSyncJob 여섯으로 확정됐다.
여섯 중 항상 도는 것은 상품 본체와 전체 이력 둘뿐이고 나머지 넷은 조건부였다. 변경 유형별 이력은 가격이나 할인가가 바뀌었을 때 돌고 가격 추적은 신규 버전 상품만이며 정산가는 거기에 조건이 둘 더 붙고 마켓 연동 작업은 연동된 상품만 돈다. 조건이 전부 달라서 처리 대상 삼천여 건이 각 조건에 해당하는지를 따로 세어야 했다.
증상 — 빠뜨렸을 때의 분산
여섯 중 하나씩 빠뜨리면 어떻게 되는지도 정리했다. 전체 이력이 빠지면 이력 탭에 안 나오고 변경 유형별 이력이 빠지면 가격 변경 내역 조회에 안 나온다.
가격 추적이 빠지면 이력 화면이 옛 값을 보여 주고 정산가가 빠지면 정산 금액이 안 맞으며 마켓 작업이 빠지면 마켓에 반영되지 않는다. 증상이 전부 다르고 다른 시점에 다른 사람이 발견하므로 한꺼번에 안 터지고 그래서 원인이 하나라는 것을 나중에야 알게 된다.
판단 기준 — 재구현과 정식 경로 호출
이 여섯을 체크리스트로 고정하고 가격 관련 일괄 보정은 항상 그것을 놓고 시작하기로 했다. 각 항목마다 대상 여부를 먼저 정하고 들어간다.
정리하고 나서 방법을 다시 봤다. 두 가지 방법이 있었는데 여섯을 SQL로 직접 맞추는 것과 ProductService::update()를 호출하는 artisan 커맨드를 돌리는 것이다. 뒤쪽을 골랐다. 이유는 SQL로 다 맞추는 것은 정식 경로를 재구현하는 일이고 조건까지 전부 재현해야 해서다. 대신 한 건씩 도는 느린 실행 시간을 감수했고 정합성은 보장된다. 직접 SQL이 빠르다는 것이 항상 맞는 것은 아니고 재구현 비용과 정합성 위험을 같이 봐야 한다. 삼천여 건이면 커맨드로 감당 가능한 규모였고 수십만 건이면 다른 판단이 필요했을 것이다.
교훈 — 몇 개인가라는 질문
이 조사에서 정말 어려웠던 것은 각 테이블을 어떻게 갱신하느냐가 아니라 몇 개냐는 것이었다. 갱신 방법은 코드에 있지만 개수는 어디에도 적혀 있지 않았다.
키워드 검색으로는 확정이 안 되는데 못 찾은 것이 있는지를 알 방법이 없기 때문이다. 전수 추적을 하고 나서야 여섯이 전부라고 말할 수 있었고 앞선 다른 조사에서도 같은 한계를 겪었다.
정리
- 값 하나를 바꾸면 여러 테이블이 함께 갱신될 수 있다
- 그중 조건부로 도는 것들이 있고 조건이 전부 다르다
- 정식 경로를 우회하면 빠뜨린 만큼 정합성이 깨진다
- 빠뜨린 테이블마다 증상이 다르고 발견 시점도 다르다
- 일괄 보정은 체크리스트를 놓고 시작한다
- 정식 경로를 호출하는
artisan커맨드가 재구현보다 나을 수 있다 - 몇 개인지를 확정하려면 전수 추적이 필요하다
grep키워드 검색으로는 전부라고 말할 수 없다