<블로그 원문은
여기에서 확인하실 수 있으며, 블로그 번역 리뷰는 양찬석(Google)님이 참여해 주셨습니다.>
Google Play Services SDK 11.6.0에서는 API 구조가 변경되었습니다. GoogleApiClient 클래스 대신, 각 기능별로 별도의 클라이언트가 제공됩니다. 이에 따라 Play Games Services API를 사용하는데 필요한 상용구 코드의 양이 줄어들었습니다.
API는 사용하기 더욱 쉽고, 스레드 안전(thread-safe)하고 메모리를 효율적으로 사용할 수 있도록 변경되었습니다. 새로운 API는
Task
모델을 활용하여 activity와 API의 비동기 결과 처리 사이에 발생하는 문제를 효과적으로 분리합니다. 이 방식은 Firebase에서 처음 등장했으며 외부 개발자 커뮤니티에서 좋은 평가를 받았습니다. Task 모델은 장점에 대해 더욱 자세히 알아보려면
Task에 대한 블로그 시리즈와
Task API 개발자 가이드를 확인해 보시기 바랍니다.
개발자 문서는 새로운 API는 물론, 개발자를 위한 그 외 모든 Google 리소스에 대한 정보를 얻을 수 있는 신뢰할 수 있는 출처입니다.
Play Games Services API 안드로이드 기본 샘플 프로젝트와
클라이언트 서버 골격 프로젝트 모두 새로운 API를 사용하도록 업데이트되었습니다. 샘플 프로젝트는 새로운 API를 사용하면서 발생하는 문제를 제보할 수 있는 최상의 장소이기도 합니다.
많은 변화가 이루어진 것 같지만, 두려워하지 마세요! Play Services API 클라이언트 사용법은 무척 간단하고 코드 상 불필요한 내용을 많이 줄일 수 있습니다. API 클라이언트 사용은 다음 세 부분으로 나뉩니다.
- 인증이 이제 Google 로그인 클라이언트를 사용하여 명시적으로 수행됩니다. 이로 인해 인증 프로세스 제어 방법과 Google 로그인 ID 및 Play Games Services ID 간의 차이가 더 명확해집니다.
- 모든 Games.category 정적 메서드 호출을 해당 API 클라이언트 메서드로 변환합니다. Task 클래스를 사용하도록 변환하는 것도 포함됩니다. Task 모델은 코드에 있는 문제의 분리에 큰 도움이 되며 작업 리스너가 UI 스레드에서 콜백되므로 멀티스레드 복잡성을 줄여줍니다.
- 멀티플레이어 invitation 처리는 턴 방식 및 실시간 멀티플레이어 API 클라이언트를 통해 명시적으로 이루어집니다. GoogleApiClient가 더 이상 사용되지 않으므로 멀티플레이어 invitation을 포함하는 '연결 힌트' 객체에 액세스할 수 없습니다. 이제는 명시적 메서드 호출을 통해 invitation에 액세스할 수 있습니다.
인증
인증 프로세스의 세부 정보는
Google Developers 웹사이트에서 확인할 수 있습니다.
가장 일반적인 인증 사용 사례는 DEFAULT_GAMES_SIGN_IN 옵션을 사용하는 것입니다. 이 옵션을 사용하면 게임에서 사용자의 게임 프로필을 사용할 수 있습니다. 사용자의 게임 프로필에는 이름, 이미지 아바타와 같이 게임이 표시할 수 있는 게이머 태그만 포함되므로 사용자의 실제 ID가 보호됩니다. 따라서 사용자가 추가적인 개인정보 공유에 동의할 필요가 없으므로 사용자와 게임 간의 마찰이 줄어듭니다.
참고: 게임 프로필만 사용하는 것으로 요청할 수 있는 Google 로그인 옵션은
requestServerAuthCode()뿐입니다.
requestIDToken()과 같은 다른 모든 옵션의 경우 사용자가 추가 정보를 공유하는 것에 대해 동의해야 합니다. 또한, 이 옵션은 사용자에게 탭이 없는 로그인 환경이 제공되지 않도록 하는 효과도 있습니다.
한 가지 더 참고할 사항은 Snapshots API를 사용하여 게임 데이터를 저장하는 경우
로그인 옵션을 생성할 때 Drive.SCOPE_APPFOLDER 범위를 추가해야 한다는 점입니다.
private GoogleSignInClient signInClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// other code here
GoogleSignInOptions signInOption = new GoogleSignInOptions.Builder(
GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
// If you are using Snapshots add the Drive scope.
.requestScopes(Drive.SCOPE_APPFOLDER)
// If you need a server side auth code, request it here.
.requestServerAuthCode(webClientId)
.build();
signInClient = GoogleSignIn.getClient(context, signInOption);
}
한 번에 하나의 사용자 계정으로만 로그인할 수 있으므로 activity가 재개될 때 자동 로그인을 시도하는 것이 좋습니다. 그렇게 해도 유효한 경우 사용자를 자동으로 로그인하는 효과가 있습니다. 또한, 사용자가 다른 activity에서 로그아웃하는 것처럼 변경 사항이 있는 경우 로그인된 계정이 업데이트되거나 무효화됩니다.
private void signInSilently() {
GoogleSignInOptions signInOption =
new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
.build();
GoogleSignInClient signInClient = GoogleSignIn.getClient(this, signInOption);
signInClient.silentSignIn().addOnCompleteListener(this,
new OnCompleteListener<GoogleSignInAccount>() {
@Override
public void onComplete(@NonNull Task<GoogleSignInAccount> task) {
// Handle UI updates based on being signed in or not.
enableUIButtons(task.isSuccessful());
// It is OK to cache the account for later use.
mSignInAccount = task.getResult();
}
});
}
@Override
protected void onResume() {
super.onResume();
signInSilently();
}
대화식 로그인은 별도의 인텐트를 실행하는 방법으로 이루어집니다. 대단하죠! 오류 해결책이 있는지 확인하고 올바른 API를 호출하여 오류를 해결하려고 시도할 필요가 더 이상 없습니다. 그저 간단히 activity를 시작하고 onActivityResult()에서 결과를 가져오기만 하면 됩니다.
Intent intent = signInClient.getSignInIntent();
startActivityForResult(intent, RC_SIGN_IN);
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
if (requestCode == RC_SIGN_IN) {
// The Task returned from this call is always completed, no need to attach
// a listener.
Task<GoogleSignInAccount> task =
GoogleSignIn.getSignedInAccountFromIntent(intent);
try {
GoogleSignInAccount account = task.getResult(ApiException.class);
// Signed in successfully, show authenticated UI.
enableUIButtons(true);
} catch (ApiException apiException) {
// The ApiException status code indicates the
// detailed failure reason.
// Please refer to the GoogleSignInStatusCodes class reference
// for more information.
Log.w(TAG, "signInResult:failed code= " +
apiException.getStatusCode());
new AlertDialog.Builder(MainActivity.this)
.setMessage("Signin Failed")
.setNeutralButton(android.R.string.ok, null)
.show();
}
}
}
GoogleSignIn.getLastSignedInAccount() 메서드를 호출하여 사용자가 로그인되었는지 여부를 확인할 수 있습니다. 이 메서드는 로그인된 사용자에 대한 GoogleSignInAccount를 반환하고, 로그인된 사용자가 없는 경우에는 null을 반환합니다.
if (GoogleSignIn.getLastSignedInAccount(/*context*/ this) != null) {
// There is a user signed in, handle updating the UI.
enableUIButtons(true);
} else {
// Not signed in; update the UI.
enableUIButtons(false);
}
로그아웃은
GoogleSignInClient.signOut()을 호출하는 방식으로 수행됩니다. 따라서 더 이상 게임을 위한 특정한 로그아웃이 없습니다.
signInClient.signOut().addOnCompleteListener(MainActivity.this,
new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
enableUIButtons(false);
}
});
);
Games Services API 클라이언트 사용
이전 버전의 Play Games Services에서는 API를 호출하는 일반적인 패턴이 다음과 같았습니다.
PendingResult<Stats.LoadPlayerStatsResult> result =
Games.Stats.loadPlayerStats(
mGoogleApiClient, false /* forceReload */);
result.setResultCallback(new
ResultCallback<Stats.LoadPlayerStatsResult>() {
public void onResult(Stats.LoadPlayerStatsResult result) {
Status status = result.getStatus();
if (status.isSuccess()) {
PlayerStats stats = result.getPlayerStats();
if (stats != null) {
Log.d(TAG, "Player stats loaded");
if (stats.getDaysSinceLastPlayed() > 7) {
Log.d(TAG, "It's been longer than a week");
}
if (stats.getNumberOfSessions() > 1000) {
Log.d(TAG, "Veteran player");
}
if (stats.getChurnProbability() == 1) {
Log.d(TAG, "Player is at high risk of churn");
}
}
} else {
Log.d(TAG, "Failed to fetch Stats Data status: "
+ status.getStatusMessage());
}
}
});
이 API는 Games 클래스의 정적 필드에서 액세스가 이루어지고 결과를 얻기 위해 개발자가 리스너에 추가한 PendingResult를 반환했습니다.
그런데 이제는 여기에 약간 변화가 생겼습니다. Games 클래스에서 API 클라이언트를 가져오는 정적 메서드가 있으며 PendingResult 클래스가 Task 클래스로 바뀌었습니다.
그 결과, 새로운 코드는 다음과 같습니다.
GoogleSignInAccount mSignInAccount = null;
Games.getPlayerStatsClient(this, mSignInAccount).loadPlayerStats(true)
.addOnCompleteListener(
new OnCompleteListener<AnnotatedData<PlayerStats>>() {
@Override
public void onComplete(Task<AnnotatedData<PlayerStats>> task) {
try {
AnnotatedData<PlayerStats> statsData =
task.getResult(ApiException.class);
if (statsData.isStale()) {
Log.d(TAG,"using cached data");
}
PlayerStats stats = statsData.get();
if (stats != null) {
Log.d(TAG, "Player stats loaded");
if (stats.getDaysSinceLastPlayed() > 7) {
Log.d(TAG, "It's been longer than a week");
}
if (stats.getNumberOfSessions() > 1000) {
Log.d(TAG, "Veteran player");
}
if (stats.getChurnProbability() == 1) {
Log.d(TAG, "Player is at high risk of churn");
}
}
} catch (ApiException apiException) {
int status = apiException.getStatusCode();
Log.d(TAG, "Failed to fetch Stats Data status: "
+ status + ": " + task.getException());
}
}
});
여기서 볼 수 있듯이, 크게 변한 것은 아니지만 Task API의 이점을 모두 활용할 수 있으므로 GoogleApiClient 수명 주기 관리에 대해 염려할 필요가 없습니다.
이러한 변경 패턴은 모든 API에서 동일합니다. 추가 정보가 필요한 경우
개발자 웹사이트에서 확인할 수 있습니다. 예를 들어, 예전에 Games.Achievements를 사용했다면 이제는
Games.getAchievementClient()를 사용해야 합니다.
Play Games Services API에 대한 마지막 주요 변경 사항은 새로운 API 클래스인 GamesClient가 추가되었다는 점입니다. 이 클래스는
setGravityForPopups(),
getSettingsIntent()와 같은 지원 메서드를 처리하며 게임이 알림에서 실행된 경우 멀티플레이어 invitation 객체에 대한 액세스도 제공합니다.
이전에는 연결 힌트와 함께 onConnected() 메서드가 호출되었습니다. 이 힌트는 시작 시 activity로 전달된 invitation을 포함할 수 있는 Bundle 객체입니다.
이제는 GamesClient API를 사용하는 경우 invitation이 있으면 게임에서 signInSilently()를 호출해야 합니다. 이 호출은 사용자가 invitation에서 확인되었기 때문에 성공할 것입니다. 그런 후
GamesClient.getActivationHint()를 호출하여 활성화 힌트를 가져오고 invitation이 있는 경우 이를 처리합니다.
Games.getGamesClient(MainActivity.this, mSignInAccount)
.getActivationHint().addOnCompleteListener(
new OnCompleteListener<Bundle>() {
@Override
public void onComplete(@NonNull Task<Bundle> task) {
try {
Bundle hint = task.getResult(ApiException.class);
if (hint != null) {
Invitation inv =
hint.getParcelable(Multiplayer.EXTRA_INVITATION);
if (inv != null && inv.getInvitationId() != null) {
// retrieve and cache the invitation ID
acceptInviteToRoom(inv.getInvitationId());
return;
}
}
} catch (ApiException apiException) {
Log.w(TAG, "getActivationHint failed: " +
apiException.getMessage());
}
}
});
오류 처리
메서드 호출이 실패하면
Task.isSuccessful()
이 false가 되고 이 오류에 대한 정보는
Task.getException()
을 호출하여 액세스할 수 있습니다. 경우에 따라서는 이러한 예외가 API 호출에서 성공 이외의 값이 반환되는 단순한 형태로 나타나기도 합니다. 다음과 같이
ApiException
로 캐스트하여 이를 확인할 수 있습니다.
if (task.getException() instanceof ApiException) {
ApiException apiException = (ApiException) task.getException();
status = apiException.getStatusCode();
}
다른 경우, MatchApiException이 반환될 수 있으며 여기에는 업데이트된 일치 데이터 구조가 포함됩니다. 이러한 예외도 비슷한 방법으로 검색할 수 있습니다.
if (task.getException() instanceof MatchApiException) {
MatchApiException matchApiException =
(MatchApiException) task.getException();
status = matchApiException.getStatusCode();
match = matchApiException.getMatch();
} else if (task.getException() instanceof ApiException) {
ApiException apiException = (ApiException) task.getException();
status = apiException.getStatusCode();
}
상태 코드가
SIGN_IN_REQUIRED이면 플레이어를 다시 인증해야 함을 나타냅니다. 이를 위해
GoogleSignInClient.getSignInIntent()를 호출하여 플레이어를 대화식으로 로그인합니다.
요약
기존 GoogleApiClient 방식 대신, 더욱 느슨하게 결합된 API 클라이언트 사용으로 변경됨에 따라 상용구 코드가 줄어들고 사용 패턴이 더욱 명확해지고 스레드 안전성이 확보되는 이점이 있습니다. 현재 게임을 API 클라이언트로 마이그레이션하는 경우 다음 자료를 참고하시기 바랍니다.
게임에 대한 로그인 모범 사례:
https://developers.google.com/games/services/checklist
Play Games Services 샘플:
Android 기본 샘플
클라이언트 서버 골격
StackOverflow:
https://stackoverflow.com/questions/tagged/google-play-games