/ ADMOB, MONETIZATION, ANDROID

애드몹 앱 오프닝 광고 - 앱 시작/복귀시 자동으로 광고 표시하기

지난 글 (앱 시작과 함께 광고를 보여주려면? - 애드몹 앱 오프닝 광고)에서는 애드몹 앱 오프닝 광고에 대한 간략한 소개 및 광고를 수동으로 노출하는 방법을 알아보았습니다.

이번 글에서는 앱 오프닝 광고를 수동으로 노출하는 대신, 앱이 활성 상태로 전환될 때 (앱 최초 실행 및 다른 앱을 사용하다 복귀하는 경우) 자동으로 광고를 노출하는 방법을 살펴보겠습니다.

앱의 활성 상태 여부는 어떻게 확인해야 하나요?

앱이 활성화 되는 시점에 앱 오프닝 광고를 표시하려면, 액티비티 단위가 아닌 애플리케이션 자체, 즉 애플리케이션 프로세스의 활성화/비활성화 이벤트를 감지해야 합니다.

Android Jetpack 구성요소 중 하나인 Lifecycle 라이브러리는 생먕주기와 관련된 다양한 기능을 제공하는데요, 이를 사용하면 애플리케이션 프로세스의 생명주기를 간편하게 추적함으로써 앱의 활성화/비활성화 여부를 알아낼 수 있습니다. 생명주기와 관련된 자세한 내용은 개발자 문서를 참고하세요.

앱에 Jetpack Lifecycle 라이브러리 추가하기

Jetpack Lifecycle 라이브러리는 다양한 서브 라이브러리로 구성되어 있습니다. 애플리케이션 라이프사이클을 추적하려면 다음 라이브러리를 추가해야 합니다.

  • androidx.lifecycle:lifecycle-runtime: 라이프사이클 추적에 필요한 기본 메서드를 제공하는 라이브러리
  • androidx.lifecycle:lifecycle-process: 애플리케이션 프로세스 라이프사이클 추적 기능을 제공하는 라이브러리
  • androidx.lifecycle:lifecycle-compiler: 라이프사이클 추적과 관련된 어노테이션을 사용하기 위해 필요한 어노테이션 프로세서

새 프로젝트를 생성한 후, app/build.gradledependencies 섹션을 다음과 같이 변경합니다. 예시에서는 2.2.0버전을 사용하고 있으며, 라이브러리의 최신 버전을 확인하려면 개발자 문서를 참조하세요.

android {
    ...
}

dependencies {
    ...

    // 라이프사이클 추적에 필요한 기본 라이브러리를 추가합니다.
    implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0'

    // 애플리케이션 프로세스 라이프사이클 추적용 라이브러리르 추가합니다.
    implementation 'androidx.lifecycle:lifecycle-process:2.2.0'
    
    // 라이프사이클 추적용 어노테이션을 처리하는 어노테이션 프로세서를 추가합니다.
    annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.2.0'
}

애드몹 API를 사용해야 하므로 GMA Android SDK를 추가하는 것도 잊지 말아주세요. GMA Android SDK의 최신 버전은 애드몹 개발자 문서에서 확인할 수 있습니다.

android {
    ...
}

dependencies {
    ...

    // Google Mobile Ads SDK를 추가합니다.
    implementation 'com.google.android.gms:play-services-ads:19.5.0'
}

애드몹 앱 ID 설정

GMA SDK를 사용하려면 앱 매니페스트(AndroidManifest.xml)에 자신의 앱의 애드몹 앱 ID (ca-app-pub-xxxx/yyyy 형태)를 추가해야 합니다. 아래는 애드몹 테스트 앱 ID를 매니페스트에 추가한 예를 보여줍니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest>
    <application>
        ...

        <!-- 애드몹 앱 ID 추가-->
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-3940256099942544~3347511713" />
    </application>

</manifest>

AppOpenAdManager 클래스 수정

지난 글에서 사용했던 앱 오프닝 광고 처리를 담당하는 클래스인 AppOpenAdManager를 여기에서도 사용합니다. 아래 코드를 사용하여 AppOpenAdManager 클래스를 새로 만듭니다.

package com.androidhuman.ads.appopenads;

import com.google.android.gms.ads.AdError;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.FullScreenContentCallback;
import com.google.android.gms.ads.LoadAdError;
import com.google.android.gms.ads.appopen.AppOpenAd;

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import android.util.Log;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class AppOpenAdManager extends AppOpenAd.AppOpenAdLoadCallback
        implements Application.ActivityLifecycleCallbacks {

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT,
            AppOpenAd.APP_OPEN_AD_ORIENTATION_LANDSCAPE})
    public @interface AdOrientation {

    }

    @Retention(RetentionPolicy.SOURCE)
    @IntRange(from = 0L, to = MAX_AD_EXPIRY_DURATION)
    public @interface AdExpiryDuration {

    }

    public static class Builder {

        private final Application application;

        private final String adUnitId;

        @AdOrientation
        private int orientation = AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT;

        @AdExpiryDuration
        private long adExpiryDuration = MAX_AD_EXPIRY_DURATION;

        private AdRequest adRequest = new AdRequest.Builder().build();

        public Builder(@NonNull Application application, @NonNull String adUnitId) {
            this.application = application;
            this.adUnitId = adUnitId;
        }

        public Builder setOrientation(@AdOrientation int orientation) {
            this.orientation = orientation;
            return this;
        }

        public Builder setAdExpiryDuration(@AdExpiryDuration long duration) {
            this.adExpiryDuration = duration;
            return this;
        }

        public Builder setAdRequest(@NonNull AdRequest request) {
            this.adRequest = request;
            return this;
        }

        public AppOpenAdManager build() {
            return new AppOpenAdManager(this);
        }
    }

    public static final String TEST_AD_UNIT_ID = "ca-app-pub-3940256099942544/1033173712";

    public static final long MAX_AD_EXPIRY_DURATION = 3600000 * 4;

    private static final String TAG = "AppOpenManager";

    private final Application application;

    private final String adUnitId;

    private final int orientation;

    private final long adExpiryDuration;

    private final AdRequest adRequest;

    private Activity mostCurrentActivity;

    private AppOpenAd ad;

    private boolean isShowingAd = false;

    private long lastAdFetchTime = 0L;

    private AppOpenAdManager(Builder builder) {
        this.application = builder.application;
        this.adUnitId = builder.adUnitId;
        this.orientation = builder.orientation;
        this.adExpiryDuration = builder.adExpiryDuration;
        this.adRequest = builder.adRequest;

        // Used to keep track of most recent activity.
        this.application.registerActivityLifecycleCallbacks(this);
    }

    public void showAdIfAvailable() {
        showAdIfAvailable(null);
    }

    public void showAdIfAvailable(@Nullable final FullScreenContentCallback listener) {
        if (this.isShowingAd) {
            Log.e(TAG, "Can't show the ad: Already showing the ad");
            return;
        }

        if (!isAdAvailable()) {
            Log.d(TAG, "Can't show the ad: Ad not available");
            fetchAd();
            return;
        }

        FullScreenContentCallback callback = new FullScreenContentCallback() {
            @Override
            public void onAdFailedToShowFullScreenContent(AdError error) {
                if (listener != null) {
                    listener.onAdFailedToShowFullScreenContent(error);
                }
            }

            @Override
            public void onAdShowedFullScreenContent() {
                if (listener != null) {
                    listener.onAdShowedFullScreenContent();
                }
                AppOpenAdManager.this.isShowingAd = true;
            }

            @Override
            public void onAdDismissedFullScreenContent() {
                if (listener != null) {
                    listener.onAdDismissedFullScreenContent();
                }
                isShowingAd = false;
                AppOpenAdManager.this.ad = null;
                fetchAd();
            }
        };

        ad.show(mostCurrentActivity, callback);
    }

    private void fetchAd() {
        if (isAdAvailable()) {
            return;
        }

        AppOpenAd.load(application, adUnitId, adRequest, orientation, this);
    }

    private boolean isAdAvailable() {
        return this.ad != null && !isAdExpired();
    }

    private boolean isAdExpired() {
        return System.currentTimeMillis() - lastAdFetchTime > adExpiryDuration;
    }

    // AppOpenAd.AppOpenAdLoadCallback implementations

    @Override
    public void onAppOpenAdLoaded(AppOpenAd ad) {
        Log.d(TAG, "Ad loaded");
        this.lastAdFetchTime = System.currentTimeMillis();
        this.ad = ad;
    }

    @Override
    public void onAppOpenAdFailedToLoad(LoadAdError error) {
        Log.d(TAG, "Failed to load an ad: " + error.getMessage());
    }

    // Application.ActivityLifecycleCallbacks implementations

    @Override
    public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
        // Do nothing
    }

    @Override
    public void onActivityStarted(@NonNull Activity activity) {
        // Do nothing
    }

    @Override
    public void onActivityResumed(@NonNull Activity activity) {
        this.mostCurrentActivity = activity;
    }

    @Override
    public void onActivityPaused(@NonNull Activity activity) {
        // Do nothing
    }

    @Override
    public void onActivityStopped(@NonNull Activity activity) {
        // Do nothing
    }

    @Override
    public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
        // Do nothing
    }

    @Override
    public void onActivityDestroyed(@NonNull Activity activity) {
        // Do nothing
    }
}

애플리케이션 프로세스의 라이프사이클을 추적하려면 LifecycleObserver 인터페이스를 구현해야 합니다. 따라서, 다음과 같이 AppOpenAdManager 클래스가 LifecycleObserver 인터페이스를 상속하도록 변경합니다.

...
public class AppOpenAdManager extends AppOpenAd.AppOpenAdLoadCallback
        implements Application.ActivityLifecycleCallbacks, LifecycleObserver {
            
    ...
}

다음, 자동으로 광고를 표시할지 말지 여부를 결정하는 필드인 showAdAutomaticallyAppOpenAdManagerAppOpenAdManager.Builder 클래스에 추가합니다.

public class AppOpenAdManager extends AppOpenAd.AppOpenAdLoadCallback
        implements Application.ActivityLifecycleCallbacks, LifecycleObserver {

    ...

    public static class Builder {

        ...

        // showAdAutomatically 필드 추가
        private boolean showAdAutomatically = false;

        // setShowAdAutomatically() 메서드 추가
        public Builder setShowAdAutomatically(boolean showAutomatically) {
            this.showAdAutomatically = showAutomatically;
            return this;
        }
    }

    ...

    // showAdAutomatically 필드 추가
    private final boolean showAdAutomatically;

    private AppOpenAdManager(Builder builder) {
        ...

        // 빌더 객체에서 값을 가져옵니다.
        this.showAdAutomatically = builder.showAdAutomatically;

        ...
    }

다음으로, 애플리케이션이 활성 상태로 전환되었을 때 호출할 메서드인 onApplicationBecameActive() 메서드를 추가합니다. 메서드에 @OnLifecycleEvent(Lifecycle.Event.ON_START) 어노테이션을 붙여주면, 애플리케이션이 활성 상태로 전환되었을 때 이 메서드가 호출되게끔 구성할 수 있습니다.

@OnLifecycleEvent(Lifecycle.Event.ON_START)
    public void onApplicationBecameActive() {
        if (this.showAdAutomatically) {
            showAdIfAvailable();
        }
    }

애플리케이션 프로세스의 라이프사이클 변화를 감지하려면 Lifecycle.addObserver() 메서드를 사용하여 옵저버를 등록해야 합니다. 다음과 같이 AppOpenAdManager의 생성자를 수정합니다.

private AppOpenAdManager(Builder builder) {
    ...

    // 애플리케이션 라이프사이클을 감지합니다.
    ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
}

커스텀 Application 클래스 작성

지난 글과 마찬가지로 커스텀 애플리케이션 클래스를 작성합니다. setShowAdAutomatically(true)를 사용하여 앱 오프닝 광고를 자동으로 표시하게끔 설정합니다.

package com.androidhuman.ads.appopenads;

import com.google.android.gms.ads.MobileAds;

import android.app.Application;

public class MyApplication extends Application {

    private AppOpenAdManager appOpenAdManager;

    @Override
    public void onCreate() {
        super.onCreate();

        MobileAds.initialize(this);

        appOpenAdManager = new AppOpenAdManager
                .Builder(this, AppOpenAdManager.TEST_AD_UNIT_ID)
                .setShowAdAutomatically(true)
                .build();
    }

    public AppOpenAdManager getAppOpenAdManager() {
        return this.appOpenAdManager;
    }
}

커스텀 애플리케이션을 매니페스트에 등록합니다. AndroidManifest.xml 파일을 연 후, application 태그의 android:name항목에 앞에서 생성한 커스텀 애플리케이션 클래스를 지정합니다. (아래 예는 MyApplication 클래스가 com.androidhuman.ads.appopenads 패키지에 있는 경우의 예를 보여줍니다)

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>

    <application
        ...
        android:name=".MyApplication">

        ...
    </application>

</manifest>

앱 실행 및 테스트

이것으로 앱 구현이 모두 끝났습니다. 최초 실행시에는 광고가 로드되지 않은 상태이기에 광고가 표시되지 않지만, 이후에 앱을 다시 실행하면 앱 실행 하면에서 광고가 표시되는 것을 확인할 수 있습니다.

광고 게재 빈도 (Frequency capping) 설정

앱이 활성화 될 때 자동으로 광고를 노출하면 자칫 지나치게 많이 광고를 표시하게 될 가능성이 있습니다. 지나치게 많은 광고는 일반적으로 사용자 경험에 부정적인 영향을 주므로, 표시되는 광고의 수를 적절히 조절하는 것이 중요합니다.

앱 오프닝 광고의 게재 빈도는 다음과 같은 방법으로 설정할 수 있습니다.

애드콥 콘솔에서 설정하는 방법

애드몹 콘솔 내 광고 단위 설정에서 게재 빈도를 설정할 수 있습니다. 다음은 15분에 1회 까지만 광고를 노출하게 설정한 예시입니다.

코드 내에서 설정하는 방법

애드몹 콘솔 대신, 앱 자체에서 게재 빈도를 설정하게끔 구현할 수도 있습니다.

이 경우, 고정된 값을 사용하여 배포하기 보다는, 사용자의 반응에 따라 게재 빈도를 유연하게 조절할 수 있게끔 Firebase Remote Config를 사용하여 게재 빈도를 원격으로 조정할 수 있도록 개발하는 것을 권합니다.

이 글에서 작성한 예제 프로젝트 소스 코드는 Github 내 kunny/blog_samples 저장소에서 확인할 수 있습니다.

kunny

커니

안드로이드와 오픈소스, 코틀린(Kotlin)에 관심이 많습니다. 한국 GDG 안드로이드 운영자 및 GDE 안드로이드로 활동했으며, 현재 구글에서 애드몹 기술 지원을 담당하고 있습니다.

Read More