제 안드로이드 앱은 평생 딱 1회 무료 작명을 줍니다. 그 한 번을 쓰고 나면 유료고요. 그래서 “앱을 지웠다 다시 깔면 무료 1회가 리셋된다”가 제가 정한 설계 규칙이었어요. 그런데 실기기에서 앱을 삭제하고 새로 설치한 직후, 첫 무료 작명을 눌렀더니 대뜸 페이월(유료 안내창)이 떴습니다. “무료 미리보기를 모두 사용했어요.” 방금 깐 앱인데요. 등골이 서늘했죠.
이게 저를 한참 헤매게 한 이유는, 코드를 아무리 봐도 멀쩡했기 때문이에요. 범인은 제 코드가 아니라 안드로이드가 뒤에서 조용히 해주던 일이었거든요.
그래서 이 글이 던지는 질문은 하나입니다. “내가 안 짠 코드가 내 앱의 동작을 바꾸고 있다면, 그걸 어떻게 알아채나?” 결제가 실제로 새는 문제나 로그에 키가 찍히는 문제는 내 코드 안에 원인이 있어서 읽으면 보입니다. 이번 건은 반대예요 — 내 코드에는 아무 잘못이 없고, 플랫폼이 기본값으로 켜둔 기능이 원인이라 코드를 백 번 읽어도 안 나옵니다. 초보 바이브코더가 “분명 로직은 맞는데 왜 안 되지?”에 가장 오래 빠지는 지점이 정확히 여기예요.
증상 — “방금 깐 앱인데 왜 이미 다 썼대?”
무료 쿼터는 “평생 1회”라 2회차부터 막혀야 정상인데, 완전히 삭제하고 재설치한 뒤 1회차부터 막혔습니다. 깨끗하게 새로 깐 설치(fresh install)인데도 앱이 “이 사용자는 이미 무료를 썼다”고 알고 있었다는 뜻이죠.
더 이상했던 건 재현이 들쭉날쭉했다는 점이에요. 예전에 테스트했을 땐 잘 통과했는데, 이번엔 막혔습니다. 될 때도 있고 안 될 때도 있는 버그 — 이런 비결정적인 증상이 원인 찾기를 제일 어렵게 만들죠.
게이트 코드는 멀쩡했다
제일 먼저 의심한 건 당연히 무료/유료를 가르는 게이트 코드였어요. 확인해보니 이랬습니다.
// NamingViewModel.kt (게이트 판단)
if (used >= FREE_GENERATION_LIMIT) {
// 페이월 표시
}
used(지금까지 쓴 무료 횟수)가 한도(FREE_GENERATION_LIMIT, 여기선 1) 이상이면 페이월을 띄우는, 지극히 정상적인 코드예요. 쿼터를 저장·조회하는 QuotaRepository도 멀쩡했고요. 로직엔 잘못이 없었습니다.
그러니 문제는 로직이 아니라 값이었어요. 재설치 직후인데 used가 이미 1로 들어와 있던 겁니다. 새로 깐 앱이 “이미 한 번 썼다”는 값을 어디선가 물려받고 있었다는 뜻이죠. 채점으로 치면, 풀이 과정은 완벽한데 문제지에 이미 남이 답을 적어둔 상태로 시작한 셈이에요. (오늘의 수학쌤 비유는 여기까지.)
진짜 범인 — 안드로이드 Auto Backup
그 “어디선가”가 바로 Auto Backup이었어요.
Auto Backup: 안드로이드가 앱 데이터(설정·작은 파일 등)를 사용자 구글 계정 클라우드에 자동으로 백업해두는 기능. 앱을 다시 깔면 그 데이터를 자동 복원해준다.
원래는 편의 기능이에요. 폰을 바꾸거나 앱을 다시 깔아도 설정이 유지되니까요. 문제는 이게 켜져 있는지도 몰랐다는 거예요. 안드로이드 앱은 백업 대상을 정하는 규칙 파일 두 개(backup_rules.xml, data_extraction_rules.xml)를 갖는데, 프로젝트를 처음 만들면 이게 전부 주석 처리된 템플릿으로 들어옵니다. 아무것도 지정 안 하면 어떻게 될까요? allowBackup(백업 허용 여부)이 기본값 true라, 앱 데이터가 통째로 백업 대상이 돼요.
allowBackup: 앱이 Auto Backup의 대상이 될지 정하는 스위치. 따로 끄지 않으면 켜진 것으로 동작한다.
그래서 무료 횟수를 저장하던 파일(quota DataStore, 안에 free_generations_used 값이 들어있음)까지 클라우드에 백업됐고, 재설치할 때 친절하게 복원된 거예요. used=1이 부활한 거죠. “fresh install인데 왜 이미 다 썼대?”의 답이 이거였습니다 — 사실 fresh가 아니었어요.
DataStore: 안드로이드에서 작은 설정값(여기선 무료 사용 횟수)을 저장하는 요즘 방식의 저장소.
왜 어떨 땐 되고 어떨 땐 안 됐나
아까 재현이 들쭉날쭉했다고 했죠. 그 이유도 여기 있었어요. Auto Backup은 아무 때나 도는 게 아니라 하루 한 번, 그것도 기기가 충전 중 + 와이파이 연결 + 유휴 상태일 때만 돌아갑니다. 그래서 백업이 아직 안 올라간 타이밍에 테스트하면 복원할 게 없어 통과하고, 백업이 한 번 올라간 뒤에 재설치하면 복원돼서 막혔던 거예요. “될 때도 있고 안 될 때도 있는” 버그의 정체가 바로 이 비결정적 타이밍이었습니다.
어떻게 고쳤나
방향은 간단했어요. 무료 쿼터 파일만 백업에서 빼는 것. 규칙 파일에 “이건 백업하지 마라”를 한 줄씩 넣어줬습니다.
<!-- backup_rules.xml (API 30 이하) -->
<exclude domain="file" path="datastore/quota.preferences_pb"/>
<!-- data_extraction_rules.xml (API 31 이상) -->
<cloud-backup>
<exclude domain="file" path="datastore/quota.preferences_pb"/>
</cloud-backup>
<device-transfer>
<exclude domain="file" path="datastore/quota.preferences_pb"/>
</device-transfer>
여기서 중요한 건 전부 끄지 않았다는 점이에요. 무료 쿼터 파일 하나만 제외하고, Room에 저장한 작명 세션 기록 같은 나머지 사용자 데이터는 백업을 그대로 유지했습니다. 그건 폰을 바꿔도 남아 있어야 좋은 데이터니까요. 필요한 것만 정확히 빼는 게 핵심이었어요. (참고로 최신 안드로이드는 규칙 파일이 둘로 갈려서, 옛 버전용·새 버전용 양쪽 다 넣어야 안 샙니다.)
한 번은 손으로 지워야 한다: 이 exclude는 “앞으로 백업하지 마라”일 뿐, 이미 기기에 복원돼 있는 used=1 값을 자동으로 지워주진 않아요. 그래서 새 빌드를 깐 뒤 한 번은 앱 설정 → 저장공간 삭제(또는 adb shell pm clear <패키지>)로 낡은 값을 밀어낸 다음 다시 검증해야 합니다.
그래서 뭘 하면 되나
“재설치했는데 왜 예전 상태가 남아 있지?” 싶은 버그를 만나면, 이 순서로 의심해보세요.
| 신호 | 판단 | 액션 |
|---|---|---|
| 로직은 맞는데 값이 이미 채워져 시작 | 🟡 코드 아닌 데이터 출처 문제 | 저장값이 어디서 오는지(복원 경로) 추적 |
| fresh install인데 예전 설정이 남음 | 🔴 Auto Backup 복원 의심 | 백업 규칙 XML이 주석 템플릿인지 확인 |
| 재현이 들쭉날쭉(될 때·안 될 때) | 🟡 비결정적 타이밍 | 백업 조건(충전·와이파이·유휴·하루1회) 의심 |
| 리셋돼야 할 값이 살아남음 | 🔴 그 파일만 exclude | 나머지 데이터는 백업 유지(선택적 제외) |
한 문장 요약: 재설치인데 옛 상태가 남는다면 십중팔구 Auto Backup 복원이다 — 리셋돼야 할 파일만 백업 규칙에서 exclude하고, 이미 복원된 값은 한 번 손으로 지운 뒤 재검증하라.
이 버그가 알려준 건, 내 앱의 상태가 내 코드만으로 정해지지 않는다는 사실이었어요. 안드로이드가 뒤에서 백업·복원을 대신 해주는데, 그 기본 동작을 모르면 “분명 새로 깔았는데 왜?”에서 한참을 헤맵니다. 편의 기능이 내 설계 규칙(“재설치=리셋”)과 부딪힌 거죠. 플랫폼이 기본으로 뭘 해주는지 한 번쯤 들여다보는 게, 이런 유령 같은 버그를 더 빨리 잡는 길이더라고요.