안드로이드 앱에서 영상을 만들어 공유하는 기능을 붙였습니다. 캐시에 mp4를 쓰고, FileProvider URI로 감싸서 시스템 공유 시트에 넘깁니다.
임시 파일이 1~6MB라 정리 코드를 넣었습니다.
context.startActivity(Intent.createChooser(send, null))
shareVm.consume() // 상태 초기화 + videoFile.delete()공유했으니 지운다. 자연스럽습니다. 렌더러는 실기기에서 검증했고, mp4가 실제로 만들어지는 것까지 확인했고, 이 코드는 프로덕션에 나갔습니다.
무엇이 잘못됐나
startActivity(createChooser(...)) 는 선택 UI를 띄울 뿐입니다.
수신 앱은 사용자가 대상을 고른 뒤에 FileProvider URI를 엽니다. 그 사이는 짧으면 1초, 길면 사용자가 앱 목록을 훑는 몇 초입니다. 저는 그 창에서 파일을 지웠습니다.
수신 앱이 URI를 열면 FileNotFoundException 이 납니다. 그건 그쪽 프로세스에서 나고, 우리 앱에는 아무 신호도 오지 않습니다. 크래시도 없고, 로그도 없고, 사용자에게는 그냥 "영상이 안 붙은 공유"만 남습니다.
왜 검증을 통과했나
제 실기기 검증은 여기까지였습니다.
val file = TerritoryReelRenderer(ctx).render(payload) { … }
assertTrue(file!!.length() > 200_000)
assertTrue(String(head, 4, 4) == "ftyp")렌더러가 파일을 만드는 것을 확인했습니다. 그 파일이 인텐트를 타고 다른 앱까지 살아서 도착하는지는 확인하지 않았습니다. 검증이 끝난 지점이 정확히 버그가 시작되는 지점이었습니다.
여기서 잠깐 — 당신이라면 이 정리 코드를 어디로 옮기겠습니까?
정리할 자리가 없다
iOS에는 답이 있습니다. UIActivityViewController 의 onDismiss 에서 지우면 됩니다. 시트가 닫혔다는 건 파일 사용이 끝났다는 뜻입니다.
안드로이드에는 그 콜백이 없습니다. chooser는 사용자가 무엇을 골랐는지도, 언제 끝났는지도 알려주지 않습니다. startActivityForResult 를 써도 공유 대상이 결과를 돌려준다는 보장이 없습니다.
그래서 "공유가 끝난 시점"에 지우는 건 애초에 불가능합니다. 시점을 바꿔야 합니다.
/** 다음 렌더 시작 시점에 오래된 스크래치를 쓸어낸다.
* 진행 중인 공유가 잡고 있는 파일과 절대 경합하지 않는 유일한 지점이다. */
private fun sweepOldClips() {
val cutoff = System.currentTimeMillis() - 60 * 60 * 1000
File(context.cacheDir, "share").listFiles()
?.filter { it.name.startsWith("plotta-reel-") && it.lastModified() < cutoff }
?.forEach { it.delete() }
}이전 공유는 이미 끝났음이 보장되고, 지금 공유는 아직 시작 전입니다. 파일이 한 시간쯤 더 캐시에 남지만, 캐시는 원래 OS가 회수하는 공간입니다.
곁다리로 하나 더
같은 파일에서 다른 것도 물렸습니다. 렌더러가 만든 mp4를 context.cacheDir 루트에 썼는데, file_paths.xml 은 이렇게 돼 있었습니다.
<paths>
<cache-path name="share" path="share/" />
</paths>cache/share/ 만 노출합니다. 루트에 쓴 파일은 FileProvider 가 URI를 만들어주지 않습니다. 이것도 렌더까지는 멀쩡하고 공유 마지막 단계에서만 터지는 종류입니다.
두 버그가 같은 성질입니다. 파일을 만드는 데까지는 성공했고, 넘기는 데서 실패했습니다. 성공 신호가 결과를 보장하지 않는 이야기는 워크플로는 2주 내내 초록불이었고, 사이트는 2주 내내 낡아 있었습니다에도 썼습니다.
자가진단 3가지
startActivity를 "끝났다"로 읽고 있지 않습니까? 그건 "띄웠다"입니다. 그 뒤에 오는 정리 코드는 전부 의심하세요.FileProvider로 넘기는 파일이file_paths.xml에 선언된 경로 안에 있습니까? 루트에 쓰면 렌더는 되고 공유만 안 됩니다.- 당신의 실기기 검증은 어디서 끝납니까? 파일이 만들어지는 곳입니까, 아니면 다른 앱이 그 파일을 여는 곳입니까? 둘 사이가 버그가 사는 자리입니다.
솔직한 부분
이 버그는 프로덕션에 나갔습니다. 사용자가 공유를 눌렀을 때 영상이 붙지 않는 상태로요. 배포 후 코드 리뷰에서 잡혀 다음 버전에 고쳤습니다.
그리고 정리 코드를 넣은 이유 자체는 옳았습니다 — 1~6MB짜리 파일을 아무도 안 지우면 캐시가 계속 붑니다. 틀린 건 지우는 시점 하나였습니다. 필요한 코드를 잘못된 자리에 둔 것이고, 그게 가장 잡기 어려운 종류입니다. 코드만 봐서는 이상한 데가 없거든요.
당신 앱의 임시 파일은, 누가 다 쓰기 전에 지워지고 있지 않습니까?