모바일 앱 푸시 토큰 상태를 보려고 device_tokens 에서 ORDER BY RAND() LIMIT 100 으로 백 건을 뽑아 유효 여부를 셌다. 절반 넘게 무효라고 나왔는데 실제 푸시 발송은 멀쩡했다.
Table of contents
Open Table of contents
증상 — 무작위 표본이 낸 값
표본의 값과 실제 관측이 크게 어긋나면 표본 쪽을 먼저 의심하는 것이 순서다. 모집단에 무엇이 들어 있는지를 deleted_at 유무별로 세어 봤다.
삭제 표시가 붙은 행이 살아 있는 행의 두 배였다. 지운 토큰은 당연히 무효이므로 무작위로 뽑으면 그쪽이 대부분 잡힌다.
검증 — 지운 것을 뺀 뒤의 값
deleted_at IS NULL 을 붙인 모집단에서 다시 백 건을 뽑으니 대부분 유효로 나왔다. 실제 발송 상태와 맞는 값이었다.
같은 쿼리에 조건 하나가 있고 없고가 결론을 뒤집었다. 표본의 문제는 뽑는 방법이 아니라 무엇에서 뽑느냐에 있었다.
원인 — 화면을 통하면 안 보이는 조건
DeviceTokenRepository 를 보니 기본 조회에 deleted_at IS NULL 이 들어가 있었다. 화면을 통해 보면 그 조건이 항상 붙으므로 지운 행이 있다는 사실 자체가 드러나지 않는다.
내가 손으로 친 쿼리에만 그 조건이 없었다. 직접 조회할 때 빠지는 조건은 화면에서 늘 붙어 있던 것이라 빠뜨렸다는 자각도 없다.
표본이 이상해서 본 것이 다른 것을 알려 줬다
지운 행이 두 배가 넘는 것이 이상해서 deleted_at 날짜로 묶어 봤다. 특정 하루에 사천 건이 한꺼번에 지워져 있었고 리브랜딩 앱으로 갈아타면서 옛 앱 토큰을 정리한 정상 작업이었다.
그 사천 건 중 대부분이 구십 일을 넘긴 상태라 device_tokens_archive 로 옮기고 본 테이블에서 뺐다. 표본이 이상해서 들여다본 것이 데이터 정리 대상까지 알려 준 셈이다.
조치 — 조건이 빠질 자리를 없애기
두 가지 방법이 있었다. 손 조회 절차서에 deleted_at IS NULL 을 붙이라고 적는 것과, 살아 있는 행만 보는 뷰 v_device_tokens_active 를 만들어 손 조회는 그것을 보게 하는 것이다. 뒤쪽을 골랐다. 이유는 절차서는 읽는 사람만 지키고 뷰는 이름을 고르는 순간 조건이 따라오기 때문이다. 대신 지운 것까지 봐야 할 때는 본 테이블을 직접 봐야 하는 것을 감수했고 그 경우 의도가 드러난다.
코드에서도 findAll() 에 조건이 붙게 하고 전체를 보는 것은 findAllIncludingDeleted() 로 이름으로 구분되게 했다. 표본을 뽑기 전에 전체 건수와 섞인 것과 대표성을 확인하는 목록도 적었는데 시험용 기기가 test_ 이름 규칙으로만 구분되고 있어서 그것도 is_test 값으로 바꿔 조회에서 걸러지게 했다.
정리
- 표본에 지운 것이 섞이면 결과가 실제와 어긋난다
- 표본과 관측이 다르면 표본 쪽을 먼저 의심한다
- 문제는 뽑는 방법이 아니라 무엇에서 뽑느냐에 있다
- 화면을 통하면
deleted_at IS NULL이 늘 붙어 있어 빠진 줄 모른다 - 표본이 이상하면 왜 그런지 보면 다른 것이 나온다
- 지운 것이 쌓이면
device_tokens_archive로 옮긴다 - 뷰와 기본 메서드로 조건이 빠질 자리를 없앤다
- 이름 규칙에 기대는 구분은
is_test값으로 바꿔야 조회에서 걸린다