애드몹 앱 오프닝 광고 - 앱 시작/복귀시 자동으로 광고 표시하기
지난 글 (앱 시작과 함께 광고를 보여주려면? - 애드몹 앱 오프닝 광고)에서는 애드몹 앱 오프닝 광고에 대한 간략한 소개 및 광고를 수동으로 노출하는 방법을 알아보았습니다.
이번 글에서는 앱 오프닝 광고를 수동으로 노출하는 대신, 앱이 활성 상태로 전환될 때 (앱 최초 실행 및 다른 앱을 사용하다 복귀하는 경우) 자동으로 광고를 노출하는 방법을 살펴보겠습니다.
앱의 활성 상태 여부는 어떻게 확인해야 하나요?
앱이 활성화 되는 시점에 앱 오프닝 광고를 표시하려면, 액티비티 단위가 아닌 애플리케이션 자체, 즉 애플리케이션 프로세스의 활성화/비활성화 이벤트를 감지해야 합니다.
Android Jetpack 구성요소 중 하나인 Lifecycle 라이브러리는 생먕주기와 관련된 다양한 기능을 제공하는데요, 이를 사용하면 애플리케이션 프로세스의 생명주기를 간편하게 추적함으로써 앱의 활성화/비활성화 여부를 알아낼 수 있습니다. 생명주기와 관련된 자세한 내용은 개발자 문서를 참고하세요.
앱에 Jetpack Lifecycle 라이브러리 추가하기
Jetpack Lifecycle 라이브러리는 다양한 서브 라이브러리로 구성되어 있습니다. 애플리케이션 라이프사이클을 추적하려면 다음 라이브러리를 추가해야 합니다.
androidx.lifecycle:lifecycle-runtime
: 라이프사이클 추적에 필요한 기본 메서드를 제공하는 라이브러리androidx.lifecycle:lifecycle-process
: 애플리케이션 프로세스 라이프사이클 추적 기능을 제공하는 라이브러리androidx.lifecycle:lifecycle-compiler
: 라이프사이클 추적과 관련된 어노테이션을 사용하기 위해 필요한 어노테이션 프로세서
새 프로젝트를 생성한 후, app/build.gradle
내 dependencies
섹션을 다음과 같이 변경합니다. 예시에서는 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 {
...
}
다음, 자동으로 광고를 표시할지 말지 여부를 결정하는 필드인 showAdAutomatically
를 AppOpenAdManager
및 AppOpenAdManager.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 저장소에서 확인할 수 있습니다.