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'");

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

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

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

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

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

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";
    }
}

expectactual 이 다르면 그 자리에서 안다.

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

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

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

정리


Share this post on:

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