조용히 실패하는 것들 11 — 있는 척하는 안전장치는 없는 것보다 나쁩니다
구독 BM과 광고 검사 스크립트를 정비하다가 '있는 척하지만 일은 하지 않던' 안전장치 세 개를 잡았습니다. 평문 'pro' 한 줄로 뚫리던 잠금장치, 아무도 모르는 지문 해시, 반쪽짜리 광고 ID 검사기. 안전하다는 착각이 진짜 위험을 부릅니다.
앱에 인앱 결제와 구독 비즈니스 모델(BM)을 세우던 날, 성격이 똑같은 문제를 세 개 잡았습니다.
셋 다 코드상에는 분명히 존재했고, 얼핏 보면 매우 꼼꼼한 안전장치처럼 보였습니다. 하지만 속을 까보니 아무 일도 하지 않거나 오히려 문을 활짝 열어두고 있었습니다.
1. 암호화 해시 옆에 열려 있던 뒷문
PRO 구독 해금을 위한 프로모션 코드 검증 로직이 있었습니다.
// 겉보기에는 안전한 SHA-256 검증 구조
fun isProCode(input: String): Boolean {
val inputHash = sha256(input.trim())
return input.equals("pro", ignoreCase = true)
|| input.equals("premium", ignoreCase = true)
|| inputHash == PROMO_SECRET_HASH
}
- 옳은 설계:
sha256(코드) == PROMO_SECRET_HASH검사. 코드를 평문으로 남기지 않고 해시 지문으로만 대조하는 정석적인 구조입니다. - 치명적인 현실: 바로 그 앞에 평문 문자열들이
||로 이어져 있었습니다. - 결과: 월 ₩1,900, 연 ₩14,900, 평생 ₩19,900짜리 PRO 기능이 사용자가 ‘pro’ 한 단어만 치면 프리패스로 열렸습니다. 테스트할 때 임시로 넣어둔 조건이 코드에 화석처럼 남아 있었던 것입니다.
더 우스운 것은, 그 뒤에 적혀 있던 PROMO_SECRET_HASH는 과거에 어떤 단어를 해시한 것인지 팀원 누구도 몰랐습니다. (모든 후보 단어를 해시해 봐도 일치하지 않는 죽은 해시였습니다.)
2. 반만 세고 있던 광고 ID 검사 스크립트
출시 전 AdMob 테스트 광고 ID가 프로덕션에 나가는 참사를 막기 위해 admob-ids.sh라는 검사 도구를 돌리고 있었습니다.
이 스크립트는 소스 코드 내의 광고 ID가 ‘Google 견본 테스트 ID인가’와 ‘비어 있는가’를 둘 다 검사합니다.
# admob-ids.sh
CHECK_TARGETS="ADMOB_BANNER ADMOB_INTERSTITIAL"
그런데 검사 대상 목록에 배너와 전면 광고 둘만 적혀 있었습니다. 그날 새로 추가된 보상형 광고(Rewarded Ad) 자리는 검사 대상에서 빠져 있어, 견본 ID가 그대로 들어가도 스크립트는 항상 0(성공)을 뱉으며 초록불을 띄우고 있었습니다.
3. 출시 다음 날 태어난 소스가 적힌 CHANGELOG
릴리스 노트를 점검하다가 v1.0.0 절에 당시에는 없었던 고급 기능이 적혀 있는 것을 발견했습니다.
Git 커밋 로그를 추적해 보니, 해당 소스 코드는 공식 출시 다음 날 밤에 처음 커밋된 것이었습니다.
문서 작성자가 ‘원래 v1.0.0에 넣으려고 기획했던 내용’을 머릿속 기억에 의존해 릴리스 문서에 미리 적어두었고, 실제 배포된 바이너리와 문서의 내용이 서로 어긋나 있었던 것입니다.
가짜 안전장치가 진짜 위험한 이유
안전장치가 아예 없으면 사람은 조심합니다.
- 프로모션 코드 입력창을 닫아두거나 수동으로 확인합니다.
- 광고 ID를 배포 직전에 눈으로 두 번 세 번 검수합니다.
- CHANGELOG를 쓸 때 커밋 로그를 일일이 대조합니다.
하지만 “여기에 검사 스크립트가 돌아가고 있다”, “SHA-256으로 암호화되어 있다”는 사실을 알고 나면 사람은 안심하고 경계를 풉니다.
그 안심의 틈새로 진짜 버그와 보안 구멍이 유유히 지나갑니다.
청산의 원칙
우리는 이 ‘가짜 안전장치’들을 다음과 같이 뜯어고쳤습니다.
- 테스트용 백도어 문자열 전면 제거: 로컬 디버그 빌드(
BuildConfig.DEBUG)에서만 동작하는 전용 인스펙터로 분리하고, 릴리스 빌드에서는 조건문 자체가 컴파일 타임에 증발하도록 격리했습니다. - 검사 대상의 동적 열거: 광고 ID 검사 스크립트가 하드코딩된 변수명을 훑는 대신,
res/values/strings.xml에 정의된admob_*프리픽스를 가진 모든 엔트리를 정규식으로 자동 수집하여 검사하게 바꿨습니다. - 릴리스 노트의 태그 기반 생성: CHANGELOG는 사람이 기억으로 쓰지 않고, Git의 릴리스 태그 간 커밋 메시지를 파싱하여 자동으로 뼈대를 세우도록 일치시켰습니다.
다시 같은 일을 한다면
- 있는 척하는 검사는 없는 검사보다 해롭습니다. 사람의 주의력을 마비시키기 때문입니다.
- 테스트 편의를 위해 열어둔 임시
||조건문은 언제나 제품의 가장 비싼 기능을 공짜로 엽니다. - 안전장치를 만들었다면, 그 안전장치가 실패하는 테스트 케이스를 먼저 작성해야 합니다.