자료를 조회할 때 company_id와 deleted_at과 is_active 세 조건을 항상 같이 걸어야 했다. 한 곳에서 하나가 빠진 것을 발견하고 전체를 확인하기로 했다.
Table of contents
Open Table of contents
검증 — 흩어진 조회의 실태
해당 테이블을 조회하는 자리를 grep으로 세니 수십 곳이었고 하나씩 확인했다. 셋 다 있는 곳이 대부분이고 둘만 있는 곳이 넷이며 하나만 있는 곳이 하나였다.
company_id 조건이 빠진 곳이 가장 위험했는데 다른 회사 자료가 그대로 보이기 때문이다. 같이 걸려야 하는 조건이 흩어져 있으면 시간이 지나면서 하나씩 빠진다.
조치 — 자동으로 붙이는 구조
두 가지 방법이 있었는데 빠진 자리마다 조건을 손으로 넣는 것과 모델의 addGlobalScope로 전역으로 붙게 하는 것이다. 뒤쪽을 골랐다. 이유는 손으로 넣으면 다음에 새로 생기는 조회에서 또 빠져서다. 대신 조건이 붙는 것이 코드에 안 보여 원시 쿼리를 따로 챙겨야 하는 것을 감수했다. 그러면 조회하는 쪽은 나머지 조건만 추가하면 된다.
빼야 하는 경우는 Laravel 공식 문서대로 withoutGlobalScope에 스코프 이름을 명시해 빼게 해서 의도적으로 뺀 것이 코드에 드러나게 했다. 전역 범위는 모델을 거칠 때만 붙으므로 DB::select 원시 쿼리를 쓰는 몇 곳도 찾았고 그중 셋에 조건이 빠져 있었다. 남기는 원시 쿼리에는 왜 모델을 안 쓰는지를 주석으로 적고 조건을 직접 넣었다.
검증 — 빠지면 걸리는 시험
주요 경로를 돌면서 DB::listen으로 잡은 쿼리에 company_id 조건이 있는지 확인하는 시험을 만들었다. 새 화면이 생기면 그 목록에 넣게 했다.
다른 회사 자료가 보이는지도 상세와 목록 양쪽으로 시험했다. 처음 돌렸을 때 상세에서 실패했는데 조건이 빠진 자리였다. 목록만 시험했으면 상세의 누락은 안 걸렸을 것이다.
주의 — 지난 것을 확인하는 조건
조건이 빠져 있던 동안 실제로 다른 회사 자료가 나갔는지도 보려 했다. 접근 로그에 company_id가 없어서 확인할 수 없었다.
로그에 그 값을 넣게 고쳤고 그 뒤로는 같은 확인이 가능해졌다. 지난 것을 확인하려면 그때 로그에 그 값이 있었어야 하므로 사고 뒤에 넣는 것은 다음 사고를 위한 준비다.
결과 — 조건이 늘 때 드러난 값
나중에 조건이 하나 더 필요해졌다. 한 자리에 모아 뒀으므로 거기만 고쳤고 수십 곳은 손대지 않았다.
흩어져 있었으면 수십 곳에 넣어야 했고 그중 몇 곳은 빠뜨렸을 것이다. 모아 두는 값은 처음이 아니라 조건이 늘 때 나왔다.
전역으로 붙게 하면서 빼는 방법도 생겼는데 그것을 아무 데서나 쓸 수 있으면 다시 같은 문제가 된다. withoutGlobalScope를 쓸 수 있는 자리를 AdminRepository 하나로 모으고 거기서 권한을 확인하게 했다. grep해 보면 그 한 곳만 남으므로 다른 데서 쓰면 검토에서 걸린다.
정리
- 같이 걸려야 하는 조건이 흩어져 있으면 하나가 빠진다
- 조회하는 자리를 세고 하나씩 확인한다
- 한 자리로 모으고
addGlobalScope로 자동으로 붙게 한다 - 빼야 하는 경우는
withoutGlobalScope로 명시적으로 빼서 드러나게 한다 - 모델을 안 거치는
DB::select원시 쿼리를 따로 찾는다 - 조건이 빠지면 걸리는 시험을 만든다
- 남의 자료가 보이는지 상세와 목록을 둘 다 시험한다
- 뺄 수 있는 자리를 한 곳으로 좁히고 권한을 확인한다