Zettelkasten

Google Play 개발자 API 권한 모델은 GCP IAM과 Play Console 두 레이어로 분리된다

·수정 3

요약

  • Google Play 개발자 API(androidpublisher)는 GCP 프로젝트의 SA로 인증받고, Play Console에서 부여된 권한으로 인가받는 2단계 모델이다.
  • 둘은 별개 권한 시스템이고, "자동 링크 프로젝트(api-숫자-숫자 형식)"라는 자동 생성된 GCP 프로젝트가 두 세계를 연결한다.
  • SA만 만들어도, Play Console에서 invite + 권한 부여 + 앱 권한/계정 권한 모두 체크해야 비로소 API가 동작한다.

본문

두 세계와 한 개의 다리

┌────────────────────────────────────────┐    ┌────────────────────────────────────────┐
│  GCP (cloud.google.com)                │    │  Play Console (play.google.com/console)│
│                                        │    │                                        │
│  Project: api-XXXX-XXXXXX              │    │  Developer Account: XXXX...XXXXXX      │
│   └── IAM                              │    │   ├── Apps: com.example.app            │
│        └── Service Accounts            │←──→│   └── Users and permissions            │
│             └── sa-name@...            │    │        └── sa-name@... (invited)       │
│                  └── Key (JSON)        │    │             ├── App permissions        │
│                                        │    │             └── Account permissions    │
│  API: Google Play Android Developer    │    │                                        │
│       (활성화 필요)                      │    │                                        │
└────────────────────────────────────────┘    └────────────────────────────────────────┘
        ↑                                              ↑
        │ 인증 (SA JSON → OAuth2 token)                │ 인가 (어떤 endpoint 호출 가능?)
        └──────────────────┬───────────────────────────┘
                           │
              androidpublisher.googleapis.com
              호출 시 둘 다 통과해야 200

자동 링크 프로젝트 (api-XXXX-NNNNNN)

  • Play Console에서 새 개발자 계정 생성 시 GCP에 api-<숫자>-<숫자> 형식의 프로젝트가 자동 생성된다.
  • 이 프로젝트는 Play Console 전용으로 androidpublisher API 활성화가 기본으로 돼 있다.
  • 자유롭게 다른 GCP 프로젝트를 직접 link할 수도 있지만, 자동 링크 프로젝트가 가장 무난하다.
  • 자동 링크 프로젝트의 GCP IAM에 SA를 만들고, 그 SA email을 Play Console에서 invite하는 게 표준 흐름이다.

인증과 인가의 분리

인증 (Authentication) — "너 누구야?"

  • SA JSON에서 OAuth2 access token 발급
  • scope: https://www.googleapis.com/auth/androidpublisher
  • google.oauth2.service_account.Credentials → token endpoint
  • 키가 valid하면 token 발급 성공 (이 단계에서 401 안 남)

인가 (Authorization) — "넌 뭘 할 수 있어?"

  • Play Console에서 해당 SA에 부여된 권한으로 결정
  • endpoint마다 요구 권한이 다르다 (subscriptions.getvoidedpurchases.list)
  • 인가 실패 시 401 permissionDenied 또는 403 Forbidden

Play Console 권한의 두 축

Play Console "Users and permissions"에서 SA 클릭 시 두 탭이 분리돼 있다:

앱 권한 (App permissions) — 특정 앱에 대한 권한

  • 특정 앱에 대해 재무 데이터 보기 / 주문 및 구독 관리 등 개별 체크
  • 앱별로 다르게 설정 가능

계정 권한 (Account permissions) — 개발자 계정 전체에 대한 권한

  • 모든 앱에 공통으로 적용되는 권한
  • 일부 endpoint(예: purchases.subscriptions.get)는 계정 권한 레벨의 동일 항목까지 체크되어야 동작

같은 "재무 데이터 보기"라도 App 레벨에서 체크한 것과 Account 레벨에서 체크한 것이 Google 내부적으로 별개 스코프로 평가된다. (자세한 사례는 Play Console SA는 앱 권한만으로는 purchases.subscriptions.get이 401을 반환한다)

표준 등록 흐름

  1. GCP — Play Console과 연결된 자동 링크 프로젝트 또는 직접 link한 프로젝트에 SA 생성
  2. GCP IAM — SA에는 별도 IAM role 불필요 (API 호출은 SA identity 자체로 충분)
  3. GCP — Service Account Key 생성 (JSON 다운로드)
  4. Play Console → Users and permissions — SA email 입력해서 invite
  5. 앱 권한 탭 — 대상 앱 추가 + 필요한 권한 체크
  6. 계정 권한 탭 — 동일 권한 체크 (특히 재무/주문 관련)
  7. 변경사항 저장
  8. propagation 대기 (수 분 ~ 수 시간, intermittent 가능)

흔히 놓치는 함정

  • GCP IAM 권한 ≠ Play Console 권한: GCP IAM에서 SA에 roles/owner를 줘도 Play API는 못 부른다. Play 권한은 Play Console에서만 부여된다.
  • API 활성화 별도: 자동 링크 프로젝트가 아닌 다른 GCP 프로젝트를 쓸 경우 "Google Play Android Developer API" 활성화를 수동으로 해줘야 한다.
  • SA 키 회전 시 권한: 같은 SA의 키만 회전하면 권한은 그대로 유지된다. 그러나 새 SA로 교체하면 새 SA를 다시 invite 해야 한다.
  • "Setup → API access" deprecated: 일부 계정에서는 이 메뉴가 사라졌다. 현대 UI는 Users and permissions로 통합되어 있고, 메뉴가 없다고 잘못된 게 아니다.
  • Play Console과 다른 SA 사용처 혼동: Firebase Admin, GCS, Cloud Tasks 등 다른 GCP 서비스용 SA와는 별개로 관리하는 게 안전하다.

인증 흐름 코드 (Python 예시)

from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build

SCOPES = ["https://www.googleapis.com/auth/androidpublisher"]

creds = Credentials.from_service_account_info(sa_json, scopes=SCOPES)
publisher = build("androidpublisher", "v3", credentials=creds, cache_discovery=False)

result = (
    publisher.purchases().subscriptions()
    .get(packageName="com.example.app", subscriptionId="sub_1", token=purchase_token)
    .execute()
)

creds 생성 시 network call 없음 (인증서 파싱만). .execute() 호출 시점에 token 발급 + API 호출이 일어남.

관련 노트

참고