배포와 인프라7 분 읽기

공유한 영상이 첨부되지 않았습니다 — 제가 방금 지웠거든요

안드로이드에서 공유 시트를 띄운 직후 임시 파일을 정리했습니다. 깔끔한 코드처럼 보였고, 프로덕션에 나갔고, 영상 공유는 전부 조용히 실패하고 있었습니다.

#android#gotchas#reality-check#shipping
startActivity(chooser)는 선택 UI를 띄울 뿐이고 수신 앱은 사용자가 대상을 고른 뒤에 FileProvider URI를 연다. 그 사이 파일을 지우면 FileNotFoundException이 나고 우리 앱에는 아무 신호도 오지 않는다.
startActivity 는 '공유가 끝났다'가 아니라 '선택 창을 띄웠다'입니다.

안드로이드 앱에서 영상을 만들어 공유하는 기능을 붙였습니다. 캐시에 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에는 답이 있습니다. UIActivityViewControlleronDismiss 에서 지우면 됩니다. 시트가 닫혔다는 건 파일 사용이 끝났다는 뜻입니다.

안드로이드에는 그 콜백이 없습니다. 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가지

  1. startActivity 를 "끝났다"로 읽고 있지 않습니까? 그건 "띄웠다"입니다. 그 뒤에 오는 정리 코드는 전부 의심하세요.
  2. FileProvider 로 넘기는 파일이 file_paths.xml 에 선언된 경로 안에 있습니까? 루트에 쓰면 렌더는 되고 공유만 안 됩니다.
  3. 당신의 실기기 검증은 어디서 끝납니까? 파일이 만들어지는 곳입니까, 아니면 다른 앱이 그 파일을 여는 곳입니까? 둘 사이가 버그가 사는 자리입니다.

솔직한 부분

이 버그는 프로덕션에 나갔습니다. 사용자가 공유를 눌렀을 때 영상이 붙지 않는 상태로요. 배포 후 코드 리뷰에서 잡혀 다음 버전에 고쳤습니다.

그리고 정리 코드를 넣은 이유 자체는 옳았습니다 — 1~6MB짜리 파일을 아무도 안 지우면 캐시가 계속 붑니다. 틀린 건 지우는 시점 하나였습니다. 필요한 코드를 잘못된 자리에 둔 것이고, 그게 가장 잡기 어려운 종류입니다. 코드만 봐서는 이상한 데가 없거든요.

당신 앱의 임시 파일은, 누가 다 쓰기 전에 지워지고 있지 않습니까?

관련 글