RSS

App Store Connect와 Google Play API 비교: 인증·권한·한계

앱 하나를 두 스토어에 등록하면서 양쪽 API를 같은 날 연달아 만졌습니다. 목적이 똑같았기 때문에 차이가 잘 보였습니다. 인증 방식부터 다르고, 권한을 다루는 사고방식이 다르고, 심지어 "안 되는 걸 알려주는 방식"까지 달랐습니다.

자격증명의 모양

Apple은 세 조각이 필요합니다.

  • Key ID (10자리 식별자)
  • Issuer ID (계정당 하나, UUID)
  • .p8 개인키 파일

Google은 파일 하나입니다. 서비스 계정 JSON 안에 이메일과 개인키가 다 들어있습니다. 그래서 Apple 쪽은 세 값을 각각 어디서 가져올지 신경 써야 하는데, Google은 파일 경로 하나만 알면 끝납니다.

대신 이 구조 때문에 Google 쪽 JSON은 그 자체로 완전한 열쇠입니다. Apple은 Key ID와 Issuer ID가 유출돼도 .p8 이 없으면 아무것도 못 하지만, 서비스 계정 JSON은 파일 하나가 곧 계정입니다.

서명한 토큰을 바로 쓰느냐, 한 번 바꾸느냐

여기가 제일 헷갈렸던 부분입니다. 둘 다 "JWT를 직접 서명한다"까지는 같은데, 그 다음이 갈립니다.

Apple은 서명한 JWT를 그대로 Bearer 토큰으로 씁니다.

// ES256. Apple은 raw R||S 형식을 기대하므로 dsaEncoding 을 반드시 지정합니다.
const signature = crypto.sign('sha256', Buffer.from(signingInput), {
  key: p8Pem,
  dsaEncoding: 'ieee-p1363',
});
// 이 JWT 자체가 Authorization: Bearer 값

Google은 서명한 JWT를 토큰 엔드포인트에서 액세스 토큰으로 교환한 뒤, 그 액세스 토큰을 씁니다. 단계가 하나 더 있습니다.

// RS256으로 서명한 뒤
POST https://oauth2.googleapis.com/token
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<JWT>
// 응답의 access_token 을 Authorization: Bearer 로 사용

알고리즘도 다릅니다. Apple은 ES256(타원곡선), Google은 RS256(RSA)입니다. Apple 쪽에서 dsaEncoding: 'ieee-p1363' 을 빼먹으면 Node가 기본값인 DER로 서명해버려서 401이 나는데, 에러 메시지만 봐서는 서명 형식 문제인지 키 문제인지 구분이 안 됩니다. 저는 이걸 처음에 키가 잘못된 줄 알고 한참 봤습니다.

목록을 볼 수 있느냐

이건 API 설계 철학 자체가 다른 지점입니다.

Apple은 GET /v1/apps 로 앱 컬렉션을 통째로 조회할 수 있습니다. 내가 접근 가능한 앱이 뭔지 목록으로 확인됩니다.

Google Play Android Publisher API에는 앱 목록 엔드포인트가 없습니다. 모든 경로가 /applications/{packageName}/... 형태라 패키지명을 이미 알고 있어야 합니다. 그래서 "이 서비스 계정이 어떤 앱에 접근되는지" 확인하려면 패키지명을 하나씩 찔러보는 수밖에 없습니다.

저는 편집 세션 생성으로 확인했습니다.

POST /androidpublisher/v3/applications/{packageName}/edits
// 200이면 접근 가능, 404면 접근 불가

패키지 세 개를 넣어서 하나씩 돌려봤습니다. Apple에서는 목록 한 번 부르면 끝날 일이었습니다.

404가 두 가지를 뜻합니다

접근 확인을 하다가 알게 된 차이입니다.

Apple은 못 하는 일을 알려줄 때 아주 구체적입니다.

{
  "code": "FORBIDDEN_ERROR",
  "detail": "The resource 'apps' does not allow 'CREATE'. Allowed operations are: GET_COLLECTION, GET_INSTANCE, UPDATE"
}

무엇이 막혔고 대신 무엇이 되는지까지 적혀 있습니다. 이 메시지 덕분에 "권한을 올리면 되겠지"라는 제 가정이 틀렸다는 걸 비교적 빨리 알았습니다. 정확히는 키를 세 개나 만들어보고 나서였지만, 어쨌든 답은 응답 안에 있었습니다.

Google은 404 Package not found 하나로 두 상황을 덮습니다. 앱이 존재하지 않는 경우와, 존재하지만 이 서비스 계정에 권한이 없는 경우가 같은 응답입니다. 보안상 의도된 설계지만, 디버깅할 때는 "아직 안 만든 건가, 권한을 안 준 건가"를 응답만 보고는 판단할 수 없습니다.

권한을 붙이는 위치

Apple은 축이 둘입니다.

  1. 키 자체의 역할 (관리자, 앱 관리 등)
  2. 앱 쪽의 접근 범위 설정 (제한된 액세스 / 전체 액세스)

이 둘이 동시에 맞아야 키가 그 앱을 봅니다. 키에 "전체 앱" 권한이 있어도 앱이 "제한된 액세스"로 만들어졌으면 목록에서 조용히 빠집니다. 에러가 나는 게 아니라 그냥 안 보이기 때문에, 처음에는 반영이 늦는 줄 알고 몇 번 더 조회했습니다.

Google은 축이 하나입니다. 서비스 계정이 Play Console의 "사용자 및 권한" 목록에 사람 계정과 나란히 사용자처럼 등록됩니다. 여기서 계정 전체 권한을 주면 이후 만드는 앱은 자동으로 포함됩니다. 실제로 이번에 새 앱을 만들자마자 아무 설정 없이 바로 200이 떨어졌습니다.

개인적으로는 Google 쪽 모델이 이해하기 쉬웠습니다. 서비스 계정을 "팀원 한 명"으로 취급하니 권한을 어디서 확인해야 하는지 헷갈릴 일이 없습니다.

둘 다 못 하는 일

앱 생성입니다. 이건 양쪽이 똑같습니다.

Apple은 POST /v1/apps 가 403으로 막혀 있고, 역할을 관리자까지 올려도 마찬가지입니다. Google은 아예 해당 엔드포인트가 존재하지 않습니다. 결국 두 스토어 모두 앱 레코드 자체는 웹 콘솔에서 손으로 만들어야 합니다.

다만 그 앞 단계는 갈립니다. Apple은 Bundle ID 등록(POST /v1/bundleIds)이 API로 되기 때문에, 식별자를 미리 만들어두고 앱 생성만 웹에서 하는 식으로 나눌 수 있습니다. Google은 패키지명이 첫 AAB 업로드 시점에 결정되므로 미리 등록할 대상 자체가 없습니다.

파일 이름에 속았습니다

마지막은 API 차이가 아니라 제 착각입니다.

서비스 계정 키 파일 이름에 다른 프로젝트 이름이 들어 있어서, 저는 그게 그 프로젝트 전용 키인 줄 알았습니다. 그래서 "이 키를 새 앱에 재사용할 수 있나"를 한참 고민했습니다.

찔러보니 반대였습니다. 파일명에 적힌 그 프로젝트의 패키지는 404가 나고, 정작 이름과 무관한 쪽 패키지가 200이 나왔습니다. 키가 만들어진 Google Cloud 프로젝트와, 그 키가 연결된 Play 개발자 계정은 별개였던 겁니다. 파일 이름은 만들 때 붙인 라벨일 뿐 소속을 뜻하지 않았습니다.

이번 작업에서 계정을 착각한 게 이번이 두 번째였습니다. 앞서 Expo 쪽에서도 저장소 계정과 Expo 계정을 같은 것으로 착각했었습니다. 이름이 비슷하면 같은 소속이라고 넘겨짚는 버릇이 있는 것 같습니다.

정리

Apple ASC API Google Play API
자격증명 Key ID + Issuer ID + .p8 서비스 계정 JSON 하나
서명 알고리즘 ES256 (ieee-p1363 필수) RS256
토큰 서명한 JWT를 그대로 사용 JWT를 액세스 토큰으로 교환
앱 목록 조회 GET /v1/apps 로 가능 엔드포인트 없음, 패키지명 필요
권한 축 키 역할 + 앱 접근 범위 (둘 다 필요) 서비스 계정을 사용자로 등록
접근 불가 응답 403 + 허용 동작 명시 404 (없음/권한없음 구분 불가)
앱 생성 불가 (403) 불가 (엔드포인트 없음)
식별자 사전 등록 Bundle ID API로 가능 첫 업로드 때 결정

자동화 가능한 범위는 결국 비슷합니다. 앱을 만드는 일은 사람이 웹에서 하고, 그 뒤의 릴리스와 제출은 기계가 합니다. 그 경계가 양쪽 다 같은 자리에 그어져 있다는 게 조금 의외였습니다.