한 앱의 출하 브랜치를 점검했습니다. 태스크 단위 리뷰 13건이 전부 통과했고, 제가 직접 돌린 "출하해도 되는가" 감사도 "코드는 문제없음"으로 결론 냈습니다.
그 뒤 브랜치 전체를 다시 훑는 리뷰를 한 번 더 돌렸습니다. blocker 2건이 나왔습니다.
둘 다 코드가 아니라 제가 쓴 스토어 문구였습니다.
당신의 스토어 설명에 적힌 문장 중, 마지막으로 코드와 대조해본 게 언제입니까?
첫 번째: "아무 데이터도 어디로도 보내지 않습니다"
15개 로케일 스토어 설명에 그렇게 적었습니다.
같은 세션에서 제가 채운 데이터 안전 신고서에는, 결제 SDK가 구매 내역과 기기 식별자를 서버로 보낸다고 명시돼 있었습니다. 실제 배포판을 실행해서 확인해보니, 이 통신은 구매 여부와 무관하게 앱을 켤 때마다 일어났습니다.
더 아픈 건 이겁니다. 저는 그 세션 정답지의 첫 줄에 **"이 넷은 같은 답을 해야 한다"**고 직접 적어뒀습니다. 그중 한 곳만 다른 말을 하고 있었습니다.
두 번째: "길게 누르면 음량만 바뀝니다"
실제 코드는 반복 입력 카운트가 1 이상일 때만 그 동작을 켭니다. 그런데 긴 누름의 첫 입력(down 이벤트)은 언제나 카운트 0입니다.
문서화된 동작이 실제로는 한 번도 일어난 적 없는 상태로 배포될 뻔했습니다.
세 번째: "권한은 정확히 셋입니다"
실제 배포 패키지를 열어 세어보니 다섯이었고, 그중 하나(결제 권한)는 스토어가 사용자에게 직접 노출하는 권한이었습니다.
손으로 센 매니페스트가 아니라 실제 패키지를 분석 도구로 읽어야 정답이 나옵니다.
왜 리뷰 13건이 이걸 못 잡았나
태스크별 리뷰는 자기 diff만 봅니다. "문구를 쓰는 태스크"와 "그 동작을 구현한 태스크"가 다르면, 아무도 둘을 나란히 놓고 대조하지 않습니다.
같은 모양의 실수가 이번이 세 번째였습니다.
당신이라면 리뷰를 한 겹 더 늘리겠습니까, 리뷰가 던지는 질문을 바꾸겠습니까?
저는 질문을 바꿨습니다. "잘 봐라"로는 안 걸립니다. 브랜치 전체 리뷰에 **"문구가 실제 코드 동작과 일치하는가"**를 명시적인 질문으로 써 넣어야 걸립니다.
규칙으로 바꾼 것
- 숫자를 적지 말고 출처를 적습니다. "권한 셋"은 라이브러리가 하나 늘면 그 순간 거짓이 됩니다. "진동·인터넷·결제 확인"처럼 항목을 나열하면 늙지 않습니다.
- 부정문을 의심합니다. "아무것도 안 보낸다"·"~하지 않습니다" 류는 검증 비용이 가장 크고, 틀렸을 때 허위 표시로 이어지는 값이 가장 큽니다.
- 토큰 규칙을 스크립트에 넣었습니다. 결제 SDK를 쓰면서 본문에 결제 출처가 언급되지 않으면 자동으로 경고가 뜹니다. 15개 로케일을 매번 사람이 다시 읽는 것보다 쌉니다. 그 관문을 다국어로 돌렸더니 정규식이 영어에서만 동작하던 문제가 따로 튀어나왔습니다.
자가진단
- 스토어 설명에 숫자가 적혀 있습니까? 그 숫자의 출처가 코드입니까, 기억입니까?
- 설명에 부정문이 있습니까? 그걸 반증할 수 있는 자리(SDK·네트워크 호출)를 확인했습니까?
- 같은 사실을 말하는 자리가 몇 곳입니까? 스토어 설명·데이터 안전 신고서·개인정보처리방침·앱 내 문구 — 넷이 같은 답을 합니까?
솔직한 부분
제 감사가 "코드 GO"를 낸 이유는 유닛 테스트·정적 분석·번들 크기만 봤기 때문입니다. 이 앱에는 데이터베이스 마이그레이션·복구 로직·결제 게이트를 검증하는 계측 테스트 스위트가 있었는데, 감사는 그걸 한 번도 실행하지 않았습니다.
더 나쁜 건, 감사 과정에서 제가 건드린 테스트 기기 상태가 그 스위트를 실제로 빨갛게 만들어 놓고 있었다는 겁니다. 기기 데이터베이스를 초기화하고 나서야 전부 통과했습니다. "테스트가 초록불"이라는 확인 자체가 오염된 상태에서 나온 판단이었습니다.
당신 앱 스토어 설명에서 부정문 한 줄만 골라, 그게 거짓이 되려면 무엇이 참이어야 하는지 적어보세요. 그다음 그걸 확인하면 됩니다.