Skip to content
isdnetworks
Go back

정렬 없는 first가 엉뚱한 걸 복원한다

누가 작업 로그의 deleted_at 만 비웠다. 그러면 원상복구될 줄 알았는데 1만 건이 넘게 연동 중지 상태로 남았다.

Table of contents

Open Table of contents

증상 — 로그를 지웠는데 안 돌아왔다

해제 경로를 봤다.

private function distributionProductDisconnect($dp)
{
    $dp->approve_type = 40;     // 플래그 설정
    $dp->save();

    Job::create(['job' => 'disconnect', ...]);
}

approve_type 을 40으로 세우고 작업을 만들 뿐 소프트 삭제는 하지 않는다.

그러면 순서가 이렇게 된다.

해제 실행

플래그 = 40  (연동 중지)
작업 생성

Jobdeleted_at 만 비워도 approve_type 은 그대로 40이었다.

원인 — 신규 작업을 막는 플래그

approve_type 40이 신규 작업 생성을 차단하고 있었다.

Job 을 되살려도 상태를 정하는 값은 distribution_products 쪽에 그대로 남아 있는 것이다. 무엇이 상태를 정하는지 확인하지 않고 되돌린 것이 이 사고의 원인이었다.

검증 — 화면의 복원 기능

화면에 일괄 복구 기능이 있어서 그것으로 되는지 봤다.

public function restoreAllProduct(Request $request)
{
    $dp = DistributionProduct::withTrashed()
        ->where('channel', $channel)
        ->first();                          // ← 정렬 없음

    $dp->restore();
}

restore 는 삭제 시각만 되돌리고 approve_type 은 건드리지 않는다.

restoreAllProduct 라는 이름과 실제로 하는 일이 일치하지 않았다. 그리고 더 나쁜 것이 그 위에 하나 더 있었다.

주의 — 정렬 없는 first

withTrashed 로 꺼내는 first() 에 정렬이 없었다.

원본 (소프트 삭제됨)
실패한 복제본 (소프트 삭제됨)

같은 대상에 deleted_at 이 찬 행이 여럿이면 어느 것이 나올지 모른다.

MySQL 이 반환하는 순서에 달렸고 인덱스 상태나 실행 계획에 따라 바뀔 수 있다. 엉뚱한 복제본을 복원할 위험이 그대로 열려 있었다.

같은 자리를 정렬로 명시하면 결과가 갈린다.

->first()                  // 어느 행인지 모른다
->orderBy('id')->first()   // 가장 오래된 것
->latest()->first()        // 가장 최근 것

orderBylatest 가 서로 다른 행을 낸다.

시험할 때는 withTrashed 결과가 하나뿐이라 이 차이가 드러나지 않는다. 운영에서 여럿이 된 순간에 터진다.

조치 — 플래그만 되돌렸다

이번 건은 다행히 단순했다.

소프트 삭제  →  안 일어남
플래그       →  설정됨

삭제 시각 복원이 필요 없고 플래그만 되돌리면 되는 상태였다.

UPDATE distribution_products SET approve_type = NULL WHERE id IN (...);

approve_type 을 비우는 단일 조작으로 끝났다.

전수 확인하니 1건만 달랐고 그것은 deleted_at 까지 차 있었다. 그 1건은 별도 검토로 뺐는데 전부 같다고 가정하지 않고 확인한 것이 유효했다.

재발 방지 — 모집단 정량화

유사 복원 작업에는 앞 단계를 하나 붙였다.

SELECT product_id, COUNT(*), COUNT(channel_product_id) AS with_id
  FROM distribution_products
 WHERE deleted_at IS NOT NULL AND product_id IN (...)
 GROUP BY product_id HAVING COUNT(*) > 1;

소프트 삭제된 행이 여럿인 대상을 먼저 찾아 원본과 잔재 복제본을 구분한다.

COUNT(*) 가 1을 넘는 것이 없으면 무정렬 복원이 위험할 자리가 없다. 0이 아니면 그 대상은 화면 기능으로 복원하지 않고 따로 다룬다.

근본은 대칭이 아니라는 데 있었다.

해제  →  플래그 + (조건부) 삭제 시각 + 작업
복원  →  작업 로그만

해제가 여러 값을 바꾸는데 복원이 하나만 되돌리고 있었다.

정식 경로가 무엇을 바꾸는지 목록으로 만들고 그것을 다 되돌려야 원래 상태가 된다. 되돌리는 기능은 되돌릴 대상을 원래 처리에서 그대로 가져와야 했다.

정리


Share this post on:

Previous Post
채널마다 매핑이 다르다
Next Post
비슷해 보이는 엔드포인트 셋