값을 읽었는데 예상과 다른 것이 나왔고 그런데도 오류는 나지 않았다. 항목이 분명히 다섯 개인데 $items->count 를 읽으면 0 이 나오고 있었다.
Table of contents
Open Table of contents
원인 — 값이 아닌 것을 읽은 이유
count() 는 메서드인데 그것을 속성처럼 접근하고 있었다. 그러면 존재하지 않는 속성이므로 null 이 되고 숫자로 쓰이는 자리에서 0 이 된다.
괄호를 붙여 $items->count() 로 하니 제대로 된 값이 나왔다. 문제는 없는 속성을 읽어도 예외가 안 나고 Undefined property 경고만 난다는 점이었다.
꺼져 있던 경고
error_reporting 을 확인하니 E_NOTICE 와 E_WARNING 을 둘 다 꺼 두고 있었다. E_ALL 로 다시 켜니 문제의 자리가 속성 이름과 함께 바로 출력됐다.
경고를 끄면 이런 것이 조용히 지나가고 오류 대신 틀린 값이 남는다. 개발 환경에서 전부 켜니 경고가 수백 개 나왔고 한 번에 다 고칠 수는 없었다.
판단 기준 — 위험도로 나눈 처리 순서
전부 한 번에 고치려니 위험한 것과 아닌 것이 섞여서 진도가 나가지 않았다. 그래서 성격에 따라 세 종류로 나눴다.
값이 틀리게 나오는 것과 동작에 영향이 없는 것과 E_DEPRECATED 문법 경고다. 없는 속성 접근은 값이 틀리게 나오므로 먼저 고쳤고 문법 경고는 판을 올릴 때 같이 하기로 했다. 분류하고 나서 첫 종류만 그 주에 끝냈고 나머지는 목록에 두고 차례로 했다.
운영에서의 설정
운영 환경에는 두 가지 방법이 있었는데 개발과 같이 display_errors 까지 켜는 것과 error_reporting 은 켜되 display_errors=Off 와 log_errors=On 으로 로그에만 남기는 것이다. 뒤쪽을 골랐다. 이유는 PHP 매뉴얼이 운영에서는 표시 대신 로깅을 쓰라고 강하게 권하고 php.ini-production 도 그렇게 돼 있어서다. 대신 경고가 났다는 것을 화면에서 바로 알 수 없어 로그를 주기적으로 세어야 하는 것을 감수했다.
끄면 문제를 알 수 없고 화면에 보이면 안쪽 사정이 밖으로 나가기 때문이다. 로그에서 종류별로 세어 보니 한 자리에서만 수천 건 나는 것이 있었고 그것부터 고쳤다.
해결 — 미리 잡는 쪽으로
로그에서 사후에 보는 것보다 PHPStan 으로 미리 잡는 쪽이 훨씬 빨랐다. 같은 것을 실행 전에 잡아 주므로 CI 에 붙여서 새로 들어오는 것까지 막았다.
반환 타입을 명시하니 분석 도구가 더 잘 잡았고 자주 쓰는 것은 메서드로 감싸서 호출하는 쪽에서 괄호를 빠뜨릴 자리를 없앴다. 괄호 없이 쓴 비슷한 실수를 grep -rn '->count[^(]' 로 찾으니 열 곳 남짓 더 나와서 전부 고쳤다.
정리
- 속성처럼 접근했는데 메서드인 경우가 있고 값이 아닌 것이 온다
- 경고를 끄면 조용히 지나가고 값이 틀리게 나온다
- 개발에서는
E_ALL을 켜고 운영에서는display_errors대신log_errors로 남긴다 - 켜서 쏟아진 경고는 위험도로 분류하고 순서를 정한다
- 로그에서 종류별로 세어 많은 것부터 고친다
- PHPStan 으로 실행 전에 잡고 CI 에 붙여 새것을 막는다
- 타입을 적으면 분석이 더 잘 잡는다
- 자주 쓰는 것은 메서드로 감싸 빠뜨릴 자리를 없앤다