누가 작업 로그의 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 (연동 중지)
작업 생성
Job 쪽 deleted_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() // 가장 최근 것
orderBy 와 latest 가 서로 다른 행을 낸다.
시험할 때는 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이 아니면 그 대상은 화면 기능으로 복원하지 않고 따로 다룬다.
근본은 대칭이 아니라는 데 있었다.
해제 → 플래그 + (조건부) 삭제 시각 + 작업
복원 → 작업 로그만
해제가 여러 값을 바꾸는데 복원이 하나만 되돌리고 있었다.
정식 경로가 무엇을 바꾸는지 목록으로 만들고 그것을 다 되돌려야 원래 상태가 된다. 되돌리는 기능은 되돌릴 대상을 원래 처리에서 그대로 가져와야 했다.
정리
- 해제가 플래그만 설정하고 소프트 삭제는 안 할 수 있다
- 그러면 로그를 지워도 플래그가 남아 원복이 안 된다
- 무엇이 상태를 결정하는지 먼저 확인한다
- 화면의 복원 기능이 일부만 되돌릴 수 있다
- 정렬 없는
first()는 어느 행이 나올지 모른다 - 시험에서는 대상이 하나뿐이라 안 드러난다
- 복원 전에 소프트 삭제 모집단을 세어 여럿인 대상을 찾는다
- 전수 확인해서 예외는 따로 뺀다
- 해제와 복원이 대칭인지 확인한다