분류 자료를 새벽에 다시 받는 작업이 있다. 어느 날 아침에 분류가 전부 사라져 있었다. 상품은 그대로인데 분류가 없어서 목록이 안 나왔다.
Table of contents
Open Table of contents
원인 — 지우고 나서 받고 있었다
코드는 이랬다.
$this->db->trans_start();
$this->db->empty_table('category');
foreach ($fetched as $c) {
$this->db->insert('category', $c);
}
$this->db->trans_complete();
empty_table 로 전부 지우고 받은 것을 넣는다. 트랜잭션 안에 있으니 실패하면 되돌아갈 것 같다.
로그를 보니 이랬다.
[분류동기화] 시작
[분류동기화] 원본 조회 실패: cURL error 28: Operation timed out
조회에서 실패해 $fetched 가 빈 배열이 됐다. 빈 배열로 반복문을 돌면 아무것도 안 넣고 그건 오류가 아니다.
트랜잭션은 정상 종료된다. empty_table 은 성공하고 insert 는 0건이다. 지우는 것과 넣는 것의 순서가 사고의 크기를 정하고 있었다.
받아서 확인한 뒤에 지웠다
순서를 바꿨다.
$fetched = $this->fetchCategories();
if (empty($fetched)) {
log_message('error', '분류 조회 0건 - 동기화 중단');
return false;
}
$this->db->trans_start();
$this->db->empty_table('category');
foreach ($fetched as $c) { $this->db->insert('category', $c); }
$this->db->trans_complete();
fetchCategories 로 받아서 확인한 뒤에 지운다. 0건이면 아예 시작을 안 하니 실패해도 원래 자료가 남는다.
여기서 걸리는 것이 있었다. 분류가 정말 0개가 되는 경우도 이론상 있는데 지금까지 200개 안팎이었으니 0개는 정상 상황이 아니다.
$prev = $this->db->count_all('category');
$now = count($fetched);
if ($prev > 0 && $now < $prev * 0.5) {
log_message('error', "분류 급감: {$prev} → {$now}. 동기화 중단");
return false;
}
count_all 로 이전 개수를 보고 절반 이하로 줄면 멈춘다. 기준을 절반으로 잡은 것은 임의지만 아무 기준도 없는 것보다 낫다.
지우지 않는 쪽으로 바꿨다
더 나은 방향은 지우지 않는 것이었다.
foreach ($fetched as $c) {
$this->db->replace('category', $c); // 있으면 갱신, 없으면 삽입
}
replace 로 받은 것만 갱신하거나 넣으면 기존 것은 그대로 있다. 다만 이러면 삭제된 분류가 남는 문제가 생겨서 표시 여부 컬럼으로 처리했다.
// 전부 미표시로 두고
$this->db->update('category', ['use_yn' => 'N']);
// 받은 것만 표시로
foreach ($fetched as $c) {
$c['use_yn'] = 'Y';
$this->db->replace('category', $c);
}
use_yn 이 Y 인 것만 화면에 보인다. 이것도 중간에 끊기면 전부 미표시로 남으므로 둘을 한 트랜잭션에 넣었다.
행이 지워지지 않으니 되돌리기도 쉽다. 지우는 동작 자체를 없애니 실패했을 때의 최악이 훨씬 가벼워졌다.
참조하는 쪽과 어제 상태
분류를 지우면 그 분류를 쓰는 상품이 붕 뜬다.
SELECT COUNT(*) FROM product p
LEFT JOIN category c ON c.category_no = p.category_no
WHERE c.category_no IS NULL;
FOREIGN KEY 가 걸려 있었으면 애초에 못 지웠을 텐데 그게 없었다. 그래서 동기화 뒤에 LEFT JOIN 으로 확인하게 했고 0이 아니면 목록으로 남긴다.
지워진 분류를 쓰던 상품은 어느 목록에도 안 나온다. 오류가 아니라서 눈에 안 띈다.
동기화 전 상태도 남겼다.
DROP TABLE IF EXISTS category_bak;
CREATE TABLE category_bak AS SELECT * FROM category;
문제가 생기면 되돌린다.
TRUNCATE category;
INSERT INTO category SELECT * FROM category_bak;
category_bak 은 하루치만 들고 있는다. 그 이상은 원본에서 다시 받는 것이 낫고 남기는 비용보다 없을 때의 비용이 훨씬 컸다.
정리
- 전부 지우고 다시 넣는 방식은 받기가 실패하면 빈 상태로 남는다
- 빈 배열로 반복문을 돌면 0건이 들어가고 그건 오류가 아니다
- 지우는 것과 넣는 것의 순서가 사고의 크기를 정한다
- 받아서 확인한 뒤에 지운다. 0건이면 시작하지 않는다
- 이전 개수와 비교해 급감하면 멈춘다. 임의 기준이라도 없는 것보다 낫다
- 지우지 않고
replace하고use_yn으로 거르는 쪽이 더 안전하다 FOREIGN KEY가 없으면LEFT JOIN으로 붕 뜬 것을 찾는다- 동기화 전 상태를 하루치 남기면 되돌릴 수 있다