EEltaCrew
홈 › 블로그

광고 SDK 초기화가 끝나기 전에 광고를 요청하면 꺼진다: 막는 장치를 만들고도 세 번 다시 나온 이유

게시 2026-10-15실측 기준 2026-10-14읽는 시간 약 6분elta · EltaCrew

운영 중인 앱에서 「MobileAds.initialize must be called」 크래시를 처음 고친 뒤에도 같은 크래시가 다른 모양으로 세 번 더 나왔습니다. 초기화 완료를 기다리는 장치는 있었지만, 시작 시점에 켜지는 값, 동의만 보고 켜지는 값, 그 장치를 거치지 않는 새 광고 자리가 빈틈이었습니다. 각 경우의 원인과 23개 앱 전수 점검 결과를 정리합니다.

처음 본 크래시

9월 22일, 진드기 촬영 앱 1.1.22 의 Crashlytics 에 이런 크래시가 올라왔습니다. Play 사전 출시 보고서의 테스트 로봇이 낸 것이었습니다.

java.lang.IllegalStateException: MobileAds.initialize must be called
before using the Google Mobile Ads SDK

우리 앱들은 Google Mobile Ads Next-Gen SDK 를 씁니다. 초기화는 메인 스레드를 막지 않으려고 백그라운드 스레드에서 돌리는데, 그 초기화가 끝나기 전에 화면이 먼저 떠서 네이티브 광고를 요청한 겁니다. 이 SDK 는 초기화가 끝나지 않은 상태에서 광고를 요청하면 위 예외를 던지고, 앱이 꺼집니다.

처음 만든 장치: 끝날 때까지 줄 세우기

광고 요청을 바로 하지 않고, 초기화가 끝났을 때 한꺼번에 실행하는 작은 클래스를 만들었습니다.

public final class AdInit {
    private static volatile boolean ready = false;
    private static final List<Runnable> pending = new ArrayList<>();
    private static final Handler main = new Handler(Looper.getMainLooper());

    /** 초기화 완료 콜백에서 부른다. */
    public static void markReady() {
        List<Runnable> run;
        synchronized (pending) {
            ready = true;
            run = new ArrayList<>(pending);
            pending.clear();
        }
        for (Runnable r : run) main.post(r);
    }

    /** 준비됐으면 바로, 아니면 준비된 뒤에 실행. */
    public static void ensureThen(Runnable r) {
        synchronized (pending) {
            if (!ready) { pending.add(r); return; }
        }
        if (Looper.myLooper() == Looper.getMainLooper()) r.run(); else main.post(r);
    }
}

MobileAds.initialize 의 완료 콜백에서 markReady() 를 부르고, 배너와 네이티브 광고를 요청하는 함수의 첫 줄을 AdInit.ensureThen(...) 으로 감쌌습니다. 진드기 촬영 앱은 같은 날 1.1.23 으로 고쳤고, 9월 22일부터 25일 사이에 View 로 화면을 만드는 다른 앱들에도 같은 장치를 넣었습니다. Compose 로 만든 앱들은 「동의와 초기화가 끝났다」는 상태값을 화면에 내려 주고, 광고 컴포저블이 그 값을 보고 요청하는 구조였습니다.

여기서 끝난 줄 알았습니다.

두 번째: 「시작」에 켜지는 값

10월 11일, 반응속도 게임 앱 1.0.20 의 Crashlytics 에서 같은 예외를 확인했습니다. 결과 화면의 네이티브 광고를 요청하는 곳이었고, 10월 14일 기준 30일 동안 2건, 사용자 1명이었습니다.

결과 화면 광고 요청은 초기화 여부를 아예 보지 않았습니다. 다른 광고 요청이 확인하던 adsInitialized 도 막는 역할을 하지 못했습니다. 이 값은 초기화를 한 번만 시작하려고 둔 값이라, 초기화가 끝날 때가 아니라 초기화를 시작할 때 켜졌기 때문입니다. 이름은 「초기화됨」이지만 실제 뜻은 「초기화 시작함」이었습니다. 또 사용자가 광고 동의를 거부하면 초기화를 아예 시작하지 않는데, 결과 화면 광고는 그때도 요청을 했습니다.

진드기 촬영 앱과 같은 방식(완료 콜백에서 켜지는 값 + 대기열)으로 바꾸고, 결과 화면 광고·보상형 광고·종료 광고를 모두 그 뒤로 옮겼습니다. 동의를 거부해 초기화가 없으면 대기열의 요청은 실행되지 않고 남습니다. 이 수정은 다음 버전에 들어가 있고, 아직 출시 전입니다.

세 번째: 장치를 거치지 않는 새 광고 자리

10월 14일 오후, 일출·일몰 앱 1.0.22 에서 같은 예외가 났습니다. Android 16 기기 한 대에서 1건이었습니다.

java.lang.IllegalStateException: MobileAds.initialize must be called ...
    at ...ui.components.StartAdSheetKt$StartAdSheet$3$1

이 앱에는 동의와 초기화가 끝나야 켜지는 LocalAdsConsentReady 가 있었고, 기존 네이티브 광고 컴포저블은 이 값을 보고 있었습니다. 그런데 9월 6일 광고 UI 를 개편하면서 새로 만든 시작 광고 시트와 배너 자리는 이 값을 보지 않고 바로 광고를 요청하고 있었습니다. 장치는 있었는데, 새 광고 자리가 그 장치를 지나가지 않은 겁니다.

배너 쪽은 요청을 try 로 감싸 두어서 크래시는 나지 않았지만, 예외가 나면 그 실행 동안 배너 자리가 비어 있었고, 동의를 받기 전에 요청이 나갈 수 있는 구조이기도 했습니다.

두 곳 모두 같은 값을 기다리게 했습니다.

@Composable
fun StartAdSheet() {
    val adsReady = LocalAdsConsentReady.current   // 동의 + SDK 초기화 완료
    val allowed = remember { StartAdGate.shouldShow(ctx) }
    if (!allowed || !adsReady) return
    // ... 여기서부터 NativeAdLoader.load
}

23개 앱 전수 점검

한 앱에서 다시 나왔으니 다른 앱에도 있을 수 있다고 보고, 운영 중인 23개 앱의 소스와 Crashlytics 30일 기록을 모두 확인했습니다.

점검 중에 한 번 틀렸습니다. 처음에는 검색한 이름이 빠져서 Compose 앱 5개도 같은 문제가 있다고 봤는데, 소스를 다시 열어 보니 다른 이름으로 같은 일을 하고 있어서 정정했습니다. 이름으로 찾는 점검은 이름이 하나라도 다르면 틀립니다.

두 앱은 갤럭시 노트10+ 에서 디버그 빌드와 R8 을 켠 출시 빌드 둘 다로 시작 광고가 뜨고 크래시가 없는 것을 확인한 뒤, 10월 14일 일출·일몰 앱 1.0.23, 대시보드 앱 1.0.54 로 내보냈습니다.

정리

이 글의 일출·일몰 앱은 선글로우 - 골든아워·일출일몰·달(Google Play) 입니다.

AndroidAdMobGoogle Mobile Ads SDKCrashlytics크래시

함께 읽기

이미지 줄여 읽기 한 줄이 만든 크래시: inSampleSize 는 0 에서 시작한다메모리를 아끼려고 QR 스캐너 앱 문서함의 썸네일을 줄여 읽게 바꿨더니, 다음 버전에서 문서 목록을 열 때마다 앱이 꺼졌습니다. 원인은 BitmapFactory.Options 의 inSampleSize 기본값이 0 이라는 것이었습니다. 바꾼 코드, 크래시 기록, 고친 코드, 같은 실수를 다른 곳에서 찾은 방법을 정리합니다.Crashlytics 에 안 잡힌 크래시: 넣은 버전부터만 보인다앱 21개에 Firebase Crashlytics 를 넣고 사흘 뒤, 사용자가 겪은 크래시를 Play Console 에서 먼저 발견했습니다. Crashlytics 는 SDK 를 넣은 빌드부터만 보고하고, 사용자 폰에 남아 있는 옛 버전의 크래시는 Play vitals 에만 쌓입니다. 21앱 전수 결과와 지금 쓰는 확인 순서를 정리합니다.동기화가 판매점 8,802곳을 지운 날: 공공데이터 필드 이름 하나와 100건짜리 목록로또 판매점 지도 앱에서 동기화 뒤 판매점 이름이 전부 「알수없음」으로 바뀌고, 앱에 넣어 둔 판매점 8,802곳이 사라졌습니다. 공공데이터 응답의 필드 이름 변경, 첫 100건만 받은 목록으로 삭제하던 동기화, 지오코더의 없는 키까지 원인 세 가지와 고친 방법을 정리합니다.