-
[iOS][Spring] Google login 붙이기기타개발지식/풀스택개발 2026. 6. 19. 15:26
개요
Google login은 앱과 백엔드를 같이 붙일 때 ID token 중심으로 생각하면 편하다.
iOS 앱은 Google Sign-In SDK로 사용자를 로그인시키고,
idToken을 받는다.Spring 백엔드는 그 id token을 Google 공개키/라이브러리로 검증한 뒤, 우리 서비스 회원을 찾거나 만들고, 우리 서비스 토큰을 발급한다.
중요한 건 앱에서 받은 Google user id를 서버가 그냥 믿으면 안된다는 것이다.
Google 공식 문서도 백엔드에서는 plain user ID를 믿지 말고, 검증 가능한 ID token을 사용하라고 설명한다.
전체 흐름
iOS 앱 -> GoogleSignIn SDK 로그인 -> idToken 획득 -> POST /api/auth/google-login Spring 백엔드 -> Google ID token 검증 -> aud, iss, exp 확인 -> provider = GOOGLE, providerUserId = sub 로 회원 조회 -> 없으면 회원 생성 -> 우리 서비스 JWT access token / refresh token 발급 iOS 앱 -> 우리 서비스 토큰 저장 -> 이후 API 요청에 Authorization: Bearer {accessToken} 붙임Google access token은 Google API 호출 권한에 가깝고, 로그인 식별에는 id token이 더 맞다.
앱-백엔드 로그인에서는 백엔드에 id token을 보내고, 백엔드가 검증하는 흐름을 기본으로 잡자.
Google Cloud Console 설정
먼저 Google Cloud Console에서 프로젝트를 만든다.
Google Cloud Console > APIs & Services > OAuth consent screen앱 이름, 지원 이메일, 개발자 연락처를 채우고 저장한다.
기본 로그인만 할 거면 scope는 보통
openid,email,profile정도로 시작한다.그 다음 OAuth client를 만든다.
APIs & Services > Credentials > Create Credentials > OAuth client IDiOS 앱용 client를 하나 만든다.
Application type: iOS Bundle ID: com.example.myapp이 client id는 iOS 앱이 Google SDK를 초기화할 때 쓴다.
그리고 백엔드 인증용으로 Web application 타입 client도 하나 만든다.
Application type: Web applicationGoogle 문서 기준으로, 백엔드에 사용자를 안전하게 식별하려면 server client ID가 필요하다.
iOS 쪽
GIDServerClientID에 이 Web client ID를 넣으면, 앱에서 백엔드 검증용 id token을 받을 수 있다.Google Auth Platform에서 실제로 누르는 순서
위 설명만 보면 좀 추상적이다.
실제로는 Google Cloud Console에서 아래 흐름으로 들어간다.
Google Cloud Console > 프로젝트 선택 > Google Auth Platform > Branding > Audience > Clients예전 문서나 블로그에서는
APIs & Services > OAuth consent screen이라고 많이 나오는데, 요즘 콘솔에서는 Google Auth Platform으로 보이는 경우가 있다.결국 하는 일은 같다.
동의 화면을 만들고, OAuth client를 추가하는 것이다.
처음 OAuth client를 만들려고 하면 consent screen 설정이 먼저 필요하다고 나온다.

Branding 화면에서
Get started를 누른다.
여기서 앱 이름, 유저 지원 이메일, Audience, 개발자 연락처를 넣는다.
개인 앱이면 처음에는 Audience를 External로 두는 경우가 많다.

Branding 설정이 끝나면 왼쪽 메뉴의 Clients로 간다.
그리고
Create client를 누른다.
iOS 앱용 client를 만들 때는 Application type을 iOS로 고른다.
그리고 Xcode Bundle Identifier를 넣는다.
Team ID는 당장 필수는 아니지만, App Check까지 붙일 생각이면 넣어두는 게 좋다.

생성 후에는 Client ID와 plist 다운로드가 나온다.
여기서 Client ID는 iOS의
GIDClientID에 들어가고, plist 안의 Reversed Client ID는 URL Scheme에 들어간다.
백엔드 검증용 server client는 같은 Clients 화면에서 한 번 더 만든다.
Clients > Create client > Application type: Web application이 Web client ID를 iOS의
GIDServerClientID에 넣고, Spring의google.client-id에도 같은 값으로 넣는다.헷갈리면 아래처럼 정리해두면 된다.
iOS client ID - Application type: iOS - iOS Info.plist GIDClientID - Reversed Client ID는 URL Scheme Server client ID - Application type: Web application - iOS Info.plist GIDServerClientID - Spring google.client-id - 백엔드에서 idToken aud 검증 기준여기서 iOS client ID와 server client ID를 섞는 순간, 앱 로그인은 된 것 같은데 Spring 검증에서 계속 실패한다.
iOS 프로젝트 설정
Google Sign-In SDK를 추가한다.
https://github.com/google/GoogleSignIn-iOSGoogle 공식 문서 기준으로 Swift Package Manager에서는 보통 아래 product를 쓴다.
GoogleSignIn GoogleSignInSwift // SwiftUI 버튼이 필요하면 추가Info.plist에는 iOS client id와 URL scheme을 넣는다.
<key>GIDClientID</key> <string>YOUR_IOS_CLIENT_ID</string> <key>GIDServerClientID</key> <string>YOUR_WEB_SERVER_CLIENT_ID</string> <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>YOUR_REVERSED_IOS_CLIENT_ID</string> </array> </dict> </array>reversed client id는 Google Cloud Console의 iOS URL scheme에 보이는 값이다.
예를 들어 client id가 아래라면,
1234567890-abcdefg.apps.googleusercontent.comURL scheme은 이런 식이다.
com.googleusercontent.apps.1234567890-abcdefg이거 안 넣으면 로그인 후 앱으로 돌아오는 callback이 안 잡힌다.
iOS App에서 URL callback 처리
import SwiftUI import GoogleSignIn @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in GIDSignIn.sharedInstance.handle(url) } } } }카카오와 마찬가지로 callback 처리가 빠지면 로그인 화면까지 갔다가 앱 세션이 안 이어진다.
iOS 로그인 코드
Google 로그인은 presenting view controller가 필요하다.
SwiftUI 앱에서는 현재 root view controller를 찾아서 넘기는 헬퍼를 하나 두면 된다.
import GoogleSignIn import UIKit @MainActor final class GoogleLoginService { private let authClient: AuthClient init(authClient: AuthClient) { self.authClient = authClient } func login() async throws -> LoginResponse { guard let presentingViewController = UIApplication.shared.rootViewController else { throw AuthError.missingPresenter } let result = try await GIDSignIn.sharedInstance.signIn( withPresenting: presentingViewController ) guard let idToken = result.user.idToken?.tokenString else { throw AuthError.missingProviderToken } return try await authClient.googleLogin( idToken: idToken, accessToken: result.user.accessToken.tokenString, deviceId: DeviceID.current, timezone: TimeZone.current.identifier, locale: Locale.current.identifier ) } }accessToken은 fallback이나 Google API 호출이 필요할 때 같이 넘길 수 있다.
하지만 백엔드 로그인 검증의 기준은 idToken으로 잡는 게 낫다.
iOS 요청 형태
{ "idToken": "google-id-token", "accessToken": "google-access-token", "deviceId": "device-uuid", "timezone": "Asia/Seoul", "locale": "ko-KR" }로그인 성공 후 앱은 백엔드가 내려준 우리 서비스 토큰을 저장한다.
Google id token을 앱 내부 API 인증용으로 계속 쓰는 구조가 아니다.
Spring 요청 DTO
public record GoogleLoginRequest( String idToken, String accessToken, String deviceId, String timezone, String locale ) { }Spring 의존성
Google ID token 검증은 Google API Client를 쓰면 편하다.
implementation "com.google.api-client:google-api-client"버전은 프로젝트의 BOM이나 현재 Spring Boot 환경에 맞춰 잡으면 된다.
직접 JWT 라이브러리로 검증할 수도 있지만, 처음에는 공식 라이브러리를 쓰는 쪽이 덜 헷갈린다.
Spring에서 Google ID token 검증
@Component public class GoogleTokenVerifier { private final GoogleIdTokenVerifier verifier; public GoogleTokenVerifier( @Value("${google.client-id}") String googleClientId ) throws GeneralSecurityException, IOException { this.verifier = new GoogleIdTokenVerifier.Builder( GoogleNetHttpTransport.newTrustedTransport(), GsonFactory.getDefaultInstance() ) .setAudience(List.of(googleClientId)) .build(); } public GoogleUser verify(String idTokenValue) { try { GoogleIdToken idToken = verifier.verify(idTokenValue); if (idToken == null) { throw new AuthException("Invalid Google id token"); } GoogleIdToken.Payload payload = idToken.getPayload(); return new GoogleUser( payload.getSubject(), payload.getEmail(), Boolean.TRUE.equals(payload.getEmailVerified()), (String) payload.get("name"), (String) payload.get("picture") ); } catch (Exception e) { throw new AuthException("Google login failed", e); } } }google.client-id에는 iOS client id가 아니라 backend 인증용 Web client ID를 넣는 경우가 많다.iOS Info.plist의
GIDServerClientID와 Spring의google.client-id가 맞아야 audience 검증이 통과한다.여기가 틀리면 token은 있어 보이는데 서버 검증이 계속 실패한다.
회원 처리
Google token payload의
sub를 provider user id로 저장한다.@Service public class AuthService { private final GoogleTokenVerifier googleTokenVerifier; private final MemberRepository memberRepository; private final TokenService tokenService; public LoginResponse loginWithGoogle(GoogleLoginRequest request) { GoogleUser googleUser = googleTokenVerifier.verify(request.idToken()); Member member = memberRepository .findByProviderAndProviderUserId(Provider.GOOGLE, googleUser.id()) .orElseGet(() -> memberRepository.save( Member.social(Provider.GOOGLE, googleUser.id(), googleUser.email()) )); TokenPair tokenPair = tokenService.issue(member.getId(), request.deviceId()); return LoginResponse.from(member, tokenPair); } }DB 유니크 키는 아래처럼 잡는다.
provider + provider_user_id unique이메일은 바뀔 수 있고, provider마다 정책이 달라서 외부 로그인 연결 키로는
sub가 더 적합하다.컨트롤러
@RestController @RequestMapping("/api/auth") public class AuthController { private final AuthService authService; public AuthController(AuthService authService) { this.authService = authService; } @PostMapping("/google-login") public LoginResponse googleLogin(@RequestBody GoogleLoginRequest request) { return authService.loginWithGoogle(request); } }실제 프로젝트에서는 응답을 공통 포맷으로 감싸면 된다.
{ "data": { "accessToken": "service-access-token", "refreshToken": "service-refresh-token", "memberId": 1, "isExistingMember": true } }앱 세션 저장
앱은 로그인 성공 후 우리 서비스 토큰만 저장한다.
final class AuthSession { func apply(_ response: LoginResponse) { Keychain.set(response.accessToken, forKey: "accessToken") Keychain.set(response.refreshToken, forKey: "refreshToken") UserDefaults.standard.set(response.memberId, forKey: "memberId") } }이후 API 요청은 아래처럼 보낸다.
Authorization: Bearer {service-access-token}401이 오면 refresh API를 한 번 호출하고, 성공하면 원래 요청을 한 번만 재시도한다.
무한 재시도는 금방 지옥이 된다.
자주 막히는 부분
- iOS OAuth client ID와 Web server client ID를 헷갈림
GIDServerClientID를 안 넣어서 id token audience가 서버와 안 맞음- reversed client id URL scheme을 안 넣어서 로그인 후 앱으로 안 돌아옴
- 백엔드에서 Google user id 문자열만 받고 검증을 안 함
- Spring의
google.client-id가 실제 id token의 aud와 다름 - Simulator에서는 되는데 실기기에서 Bundle ID나 signing 차이로 안 됨
Google login은 설정값 이름이 비슷해서 헷갈린다.
iOS client ID, reversed client ID, server client ID 이 세 개를 표로 적어두고 시작하면 삽질이 많이 줄어든다.
최종 정리
Google login은 iOS 앱에서 GoogleSignIn SDK로 id token을 받고, Spring 백엔드에서 그 id token을 검증하는 구조로 잡으면 된다.
백엔드는 Google token의
sub를 provider user id로 쓰고, 우리 서비스 토큰을 새로 발급한다.가장 많이 틀리는 건 client ID 종류다.
앱에는 iOS client ID와 reversed URL scheme, 백엔드 검증에는 Web server client ID를 맞춰 넣는다고 기억하면 된다.
참고 링크
'기타개발지식 > 풀스택개발' 카테고리의 다른 글
[GitHub][보안]Fine-grained PAT 만들 때 권한과 만료일을 어디까지 줄여야 하나 (0) 2026.06.20 [ChatGPT][업무자동화]Projects와 Custom GPT 차이를 실제 사용 기준으로 정리하기 (0) 2026.06.20 [OpenAI][Structured Outputs] JSON mode와 Structured Outputs 차이를 실무 기준으로 정리하기 (0) 2026.06.20 [iOS][Spring] Apple login 붙이기 (0) 2026.06.19 [iOS][Spring] Kakao login 붙이기 (0) 2026.06.19