작업 실패 처리에서 일부 오류가 안 잡히고 있었다. catch (Exception $e) 만 있어서 Error 계열은 빠져나가는 구조라 PHP 매뉴얼대로 Error 와 Exception 을 다 포함하는 Throwable 로 넓히면 되겠다는 계획을 세웠고 검토를 받았다.
Table of contents
Open Table of contents
문제 1 — 통째로 열리는 안쪽
1차 검토를 반영해 다시 쓴 계획서를 2차로 검증하면서 새 차단 사유가 셋 나왔다. 첫째는 handleFailure() 가 무엇을 하는지였다.
그 함수가 특정 채널이면 Guzzle 로 라이브 HTTP 조회를 하고 결과를 가드 없이 쓰고 있었다. 지금은 Error 계열이 거기까지 안 오지만 범위를 넓히면 오게 되므로 한 줄짜리 패치로 그 오염 경로가 통째로 열리고 상태가 이미 이상한 상황에서 외부 조회와 쓰기가 일어난다.
여기서 규칙이 나왔는데 잡는 범위를 넓히는 패치는 그 블록 안에서 새로 실행되기 시작하는 하위 호출까지 추적해야 한다는 것이다. 기존에는 그 상황에서 프로세스가 죽었고 이제는 그 블록이 돌기 시작하므로 안에 무엇이 있는지를 봐야 한다.
문제 2 — 진단을 막는 진단 코드
둘째는 진단 메시지였다. 사람이 읽을 메시지를 만들려고 Optimus::encode() 를 부르는데 그 대상이 삭제 처리가 만드는 합성 레코드였다.
합성 레코드의 product_id 가 null 이고 encode() 가 숫자가 아닌 값을 받으면 TypeError 를 던진다. 진단 메시지를 만들다가 예외가 나서 의도한 메시지는 생성 도중에 죽고 로그에 아무것도 안 남는다.
문제 3 — 패치가 재현한 원래 증상
셋째가 가장 나빴다. 건너뛴 건수를 세려고 $this->skipped++ 를 하는데 그 프로퍼티가 선언돼 있지 않았다.
이 프로젝트는 Laravel 의 오류 핸들러가 미선언 프로퍼티 알림을 ErrorException 으로 승격시키고 그 줄이 이미 실행 중인 catch 블록 안에 있으므로 그 블록이 자기 예외를 못 잡는다. 예외가 그대로 탈출해 커맨드 전체가 죽는데 원래 고치려던 증상을 패치 코드 자신이 재현하는 형태였다.
판단 기준 — 무방비인 블록 안
이것이 핵심이었다. 예외를 잡는 블록 안에서 난 예외는 그 블록이 못 잡는다.
그 안은 안전한 곳이 아니라 오히려 잡아 줄 사람이 없는 자리다. 두 가지 방법이 있었는데 계획대로 Throwable 로 넓히고 catch 안의 코드를 그대로 두는 것과 넓히되 catch 안을 로그 한 줄로 줄이고 나머지 처리를 밖으로 빼는 것이다. 뒤쪽을 골랐다. 이유는 catch 안에서 난 예외를 잡아 줄 곳이 없어서다. 대신 실패 처리 흐름이 두 단계로 나뉘어 읽기 번거로워지는 것을 감수했다.
다시 검증한 계획서
이 셋이 2차 검토에서 나왔다는 점이 남는다. 1차에서 나온 것을 반영해 다시 쓴 계획서를 다시 검증한 것이다.
같은 계획서를 두 번 본 것이 아니라 고친 계획서를 본 것이고 고치면 새 코드가 들어가며 새 코드에는 새 문제가 있을 수 있다. 수정이 새 문제를 만든다는 것을 세 번째 사유가 그대로 보여 줬다.
정리
Throwable로 잡는 범위를 넓히는 것은 새 실행 경로를 여는 것이다- 그 안에서 새로 돌기 시작하는 하위 호출을 추적한다
- 합성 레코드는 필드가 비어 있을 수 있다
- 진단 코드가 진단을 막는 경우가 있다
- 예외를 잡는 블록 안은 무방비다
- 거기서 난 예외는 그 블록이 못 잡는다
- 미선언 프로퍼티 접근이
ErrorException으로 승격된다 - 수정이 새 문제를 만들므로 고친 계획서를 다시 검증한다