Skip to content
isdnetworks
Go back

돌리기 전에 읽는 이행 스크립트

(수정됨)

데이터 이행 스크립트를 넘겨받았다. 이름만 보고 돌렸다가 예상과 다른 일이 일어났다.

Table of contents

Open Table of contents

증상 — 이름만 보고 돌린 결과

파일 이름은 이랬다.

migrate_product_category.php

상품 분류를 옮기는 것으로 보여서 그대로 돌렸다.

끝나고 보니 분류만 바뀐 것이 아니었다. 상품 상태도 함께 바뀌어 있었다.

이름이 그 스크립트가 하는 일을 전부 담고 있다는 보장이 없다. 만든 사람에게는 자명했을 정리 작업이 안에 같이 들어 있을 수 있다.

안을 열어 봤다

안에는 문장이 셋 있었다.

// 분류 이행
$this->db->query("UPDATE product SET category_no = ? WHERE old_category = ?", ...);

// 분류가 없는 상품은 판매 중지
$this->db->query("UPDATE product SET use_yn = 'N' WHERE category_no IS NULL");

// 이행 완료 표시
$this->db->query("UPDATE migration_log SET done = 'Y' WHERE name = 'product_category'");

두 번째 줄 때문에 분류가 안 맞은 상품 수천 개가 판매 중지됐다.

주석을 보면 의도 자체는 분명하다. 옮긴 뒤에 분류가 비어 버린 상품을 그대로 두면 안 된다는 판단이 있었을 것이다.

읽고 나면 납득이 되지만 그 사실을 돌리기 전에 알았어야 했다. 돌리기 전에 아는 것과 돌린 뒤에 아는 것의 차이는 물어볼 기회가 있느냐다.

조치 — 돌리기 전에 읽는 순서

그래서 순서를 다시 잡았다.

1. 전체를 읽는다. 몇 줄인지, 무엇을 건드리는지
2. UPDATE / DELETE / INSERT 를 전부 찾는다
3. 각각 몇 건이 영향받는지 SELECT 로 미리 센다
4. 원래 값으로 돌아갈 수 있는지 본다
5. 조건이 없는 것이 있는지 본다

3번이 핵심이었고 방법은 간단하다.

-- 원래
UPDATE product SET use_yn = 'N' WHERE category_no IS NULL;

-- 미리 센다
SELECT COUNT(*) FROM product WHERE category_no IS NULL;

WHERE 를 그대로 두고 앞만 SELECT COUNT(*) 로 바꾸면 된다.

4,200

이 숫자를 미리 봤으면 이게 맞는지 물어봤을 것이다. 4,200이라는 수는 상품 전체에서 무시할 수 있는 양이 아니었다.

조건 없는 구문을 찾았다

조건이 없는 변경 구문이 있는지도 훑었다.

$ grep -n "UPDATE\|DELETE" migrate_*.php | grep -v "WHERE"

이 스크립트에는 없었지만 다른 파일에서 하나 나왔다.

$this->db->query("DELETE FROM product_cache");

product_cache 는 다시 채워지는 표라 문제가 아니었다.

다만 그 판단을 돌리기 전에 했느냐가 다르다. WHERE 가 없는 줄은 그것이 캐시든 원본이든 한 번 보고 넘어가야 하는 줄이다.

사전 준비 — 원래 값으로 돌아갈 자리

돌리기 전에 돌아갈 자리를 먼저 만들었다.

CREATE TABLE product_backup_20171101 AS
SELECT no, category_no, use_yn FROM product;

전체를 복사하는 대신 바뀔 컬럼과 식별자만 남겼다. 이유는 그쪽이 훨씬 가벼워서다.

돌아가는 쿼리도 미리 적어 뒀다.

UPDATE product p JOIN product_backup_20171101 b ON p.no = b.no
SET p.category_no = b.category_no, p.use_yn = b.use_yn;

이 두 줄을 써 두고 나서 돌렸고 실제로 한 번 썼다.

돌아갈 자리가 있는 상태와 없는 상태는 작업 중의 판단이 다르다. 없으면 조금만 이상해도 멈추게 되고 있으면 확인하고 진행할 수 있다.

나눠서 돌리고 기록했다

한 번에 다 돌리는 대신 끊었다.

$this->db->query("UPDATE product SET category_no = ? WHERE old_category = ? LIMIT 1000", ...);

천 건씩 돌리고 결과를 본 다음에 다음 천 건으로 갔다.

실제로 처음 천 건에서 매칭 규칙 하나가 틀린 것이 나왔다. 전부 돌린 뒤였으면 틀린 분류가 상품 전체에 퍼진 상태에서 찾았을 것이다.

단계마다 예상과 실제를 비교하고 남기게 했다.

function runStep(string $name, string $sql, array $params = []): void {
    $before = $this->countAffectedRows($sql, $params);
    echo "{$name}: {$before}건이 바뀝니다.\n";
    if (!$this->confirm()) { exit; }

    $this->db->query($sql, $params);
    $after = $this->db->affectedRows();

    file_put_contents(LOG, sprintf("%s %s expect=%d actual=%d\n",
        date('c'), $name, $before, $after), FILE_APPEND);

    if ($before !== $after) {
        echo "경고: 예상 {$before}, 실제 {$after}\n";
    }
}

expect 와 actual 이 다르면 그 자리에서 안다.

넘겨받은 스크립트가 열네 개였고 아직 안 돌린 것이 여섯 개였다. 여섯을 다 읽어 무엇을 하는지 적었더니 둘이 이름과 다른 일을 하고 있었다.

migrate_option_price.php   옵션 가격 이행 + 옵션 없는 상품 삭제  ←
migrate_member_grade.php   등급 이행 + 6개월 미접속 휴면 처리   ←

이름에 없는 일이 붙어 있는 것이 드물지 않았다. 돌릴 때가 되면 급해서 못 읽으니 미리 읽어 두는 편이 쌌다.

정리


Share this post on:

Previous Post
탭 뒤의 두 시스템
Next Post
순서를 바꾸니 기다리는 시간이 달라졌다