구글 API로 자동화 스크립트를 짜다 보면 인증 단계에서 막힌다. 서비스 계정과 OAuth 클라이언트를 헷갈려 권한 오류를 겪거나, 일주일마다 브라우저 인증창이 다시 뜨는 경우가 흔하다. 실제로 부딪히는 함정 위주로 짚었다.
인증과 인가는 다르다, 그리고 왜 비밀번호를 직접 넘기지 않는가
인증(Authentication)은 “네가 누구인지 확인하는 것”이고, 인가(Authorization)는 “너에게 무엇을 허락할지 정하는 것”이다. 로그인은 인증, 그 다음 “이 앱이 내 캘린더를 읽어도 됨”이라고 허락하는 절차가 인가다. OAuth는 이 인가를 다루는 표준이다.
자동화 스크립트가 구글 계정 비밀번호를 직접 들고 있다면, 그 파일 하나가 유출되는 순간 계정 전체가 뚫린다. 비밀번호를 바꾸기 전까지는 회수할 방법도 없다.
OAuth는 비밀번호 대신 토큰을 발급한다. 호텔이 방 열쇠 원본 대신 기간이 지나면 저절로 무효가 되는 카드키를 주는 것과 같다. 카드키는 분실해도 프런트에서 바로 정지시킬 수 있고, 애초에 그 방 하나 말고는 못 연다.
서비스 계정과 OAuth 클라이언트는 다른 인증이다
구글 클라우드 콘솔에서 “사용자 인증 정보”를 만들려고 하면 갈림길이 나온다. ▲서비스 계정 ▲OAuth 클라이언트 ID, 이름이 비슷해 보여도 성격이 완전히 다르다.
서비스 계정은 프로그램 그 자체가 주체다. JSON 키 파일 하나로 인증이 끝나고, 사람이 매번 로그인할 필요가 없다. 구글 색인 API처럼 “이 프로젝트 소유의 작업”을 처리하는 API가 이 방식을 쓴다.
OAuth 클라이언트는 다르다. 특정 사용자 계정을 대신해 행동하겠다는 위임 구조라서, 그 사람이 로그인하고 승인 버튼을 눌러야만 토큰이 나온다. 서비스 계정 키를 아무리 정확히 넣어도 이 절차를 건너뛸 수 없다.
유튜브 Data API가 대표적이다. 영상을 어느 채널에 올릴지는 결국 “누구 계정이냐”의 문제라서, 서비스 계정으로는 원천적으로 접근이 막힌다. 채널을 여러 개 관리하는 콘텐츠 소유자를 위한 특수 예외를 빼면 반드시 사람의 동의가 필요하다. 이 구분을 모르고 서비스 계정 키부터 넣었다가 채널 연결 오류로 헤매는 경우가 많다.
API 키 하나로 끝나는 인증도 있다. LLM API를 처음 붙여보는 과정에서는 발급받은 키를 요청 헤더에 넣는 정도로 충분했다. OAuth는 그보다 한 단계 위 – “누구의 데이터에 접근하는가”를 다루는 인증이라 절차가 하나 더 붙는다.
| 구분 | 서비스 계정 | OAuth 클라이언트 |
|---|---|---|
| 인증 주체 | 프로그램 자체 | 특정 사용자 계정 |
| 사람 동의 | 불필요 | 필수, 로그인 후 승인 |
| 발급 형태 | JSON 키 파일 | 클라이언트 ID와 발급 토큰 |
| 대표 API | 색인 API, 클라우드 스토리지 | 유튜브 Data API, 지메일, 캘린더 |
| 인증 유지 | 키가 살아있는 한 계속 | 토큰 상태에 따라 만료 가능 |
클라이언트 유형은 데스크톱 앱으로 만든다
OAuth 클라이언트 ID를 생성할 때 콘솔은 유형을 고르라고 한다. 웹 애플리케이션, 데스크톱 앱, 안드로이드, iOS 등이 뜨는데, 서버에 배포된 서비스가 아니라 내 컴퓨터에서 돌리는 개인 스크립트라면 데스크톱 앱을 선택해야 한다.
웹 애플리케이션 유형은 인증이 끝난 뒤 사용자를 돌려보낼 리디렉션 URI(인증 완료 후 다시 열릴 웹 주소)를 미리 등록해야 한다. 로컬에서만 도는 스크립트에는 그런 주소가 없어서 이 유형을 고르면 설정 단계부터 막힌다.
데스크톱 앱 유형은 라이브러리가 컴퓨터 안(루프백 주소, 임시로 여는 로컬 포트)에서 인증을 자동으로 마무리한다. 스크립트를 실행하면 브라우저 창이 뜨고, 로그인과 승인만 누르면 창이 저절로 닫히며 인증이 끝난다.
예전에는 인증 코드를 화면에 띄워 직접 복사해 붙여넣는 방식(OOB)도 쓰였는데, 원격 피싱에 악용될 위험 때문에 구글이 2023년 전면 막았다. 지금은 이 루프백 방식이 표준이고, 국제 표준 문서(RFC 8252)도 네이티브 앱은 시스템 브라우저를 거치는 이 흐름을 쓰도록 못 박고 있다.
테스트 상태로 두면 일주일마다 다시 로그인해야 한다
동의 화면을 만들면 기본 상태는 “테스트”다. 개인용이니 굳이 구글 심사(검증)까지 받을 필요는 없다고 넘기기 쉬운데, 이 상태를 그대로 두면 발급되는 리프레시 토큰이 7일 뒤 자동으로 무효화된다.
결과는 뻔하다. 완전히 자동으로 돌아가야 할 스크립트가 일주일마다 갑자기 인증 오류를 뱉으면서 멈춘다. 원인을 모르면 코드 어딘가가 고장 났다고 오해하기 쉽지만, 사실은 동의 화면 상태 하나 때문이다.
해결은 동의 화면을 “게시”해서 상태를 프로덕션으로 바꾸는 것이다. 민감한 권한 범위를 요구하지 않는 개인 자동화라면 별도 심사 없이 버튼 하나로 바로 게시된다. 그 순간부터 7일 만료 제한이 사라진다.
다만 프로덕션으로 바꾸면 인증 화면에 “확인되지 않은 앱” 경고가 뜬다. 이건 구글의 정식 검증을 거치지 않았다는 뜻일 뿐이라, 본인 계정으로만 쓰는 개인 자동화에서는 무해하다. 경고 화면 아래쪽 “고급”을 눌러 이동하면 그대로 인증이 진행된다.
토큰 두 장의 역할, 그리고 토큰 파일은 비밀번호급으로
인증이 끝나면 토큰이 두 장 나온다. 액세스 토큰은 수명이 짧아서(보통 한 시간) 실제 API 호출마다 실려 나가고, 리프레시 토큰은 그 액세스 토큰을 새로 발급받는 데 쓰는 열쇠다.
스크립트는 이 흐름을 그대로 코드로 옮기면 된다. 저장된 토큰 파일을 읽어서, 액세스 토큰이 만료됐으면 리프레시 토큰으로 자동 갱신하고, 아예 파일이 없으면 그때만 브라우저 인증을 새로 연다.
creds = Credentials.from_authorized_user_file(token_path, SCOPES)
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request()) # 액세스 토큰만 조용히 새로 발급
save_token(token_path, creds)
elif not creds or not creds.valid:
creds = run_local_server_flow() # 최초 1회만 브라우저 인증
이렇게 짜두면 최초 1회만 브라우저가 뜨고, 이후 실행부터는 파일에 저장된 토큰만으로 조용히 갱신된다. 아래는 실무에서 자주 놓치는 부분이다.
- 필요한 권한 범위(스코프)만 정확히 요청한다. 유튜브 업로드만 하면 되는데 계정 전체 관리 권한까지 묶어서 요청하지 않는다.
- 토큰이 저장된 파일은 코드 저장소에 커밋하지 않는다. .gitignore에 등록해 실수로라도 올라가지 않게 한다.
- 토큰 파일 접근 권한은 본인 계정만 읽을 수 있도록 최소화한다.
- 유출이 의심되면 구글 계정의 “타사 앱 및 서비스 액세스” 페이지에서 해당 앱 권한을 바로 철회한다.
스코프를 좁게 잡을수록 동의 화면도 단순해진다. 요청하는 권한이 적으면 사용자(본인)가 승인 화면에서 확인할 항목도 줄고, 혹시 토큰이 새어나갔을 때 피해 범위도 그만큼 좁아진다. 구글 공식 문서도 데스크톱 앱은 필요한 최소 범위만 요청하도록 권장한다.
자주 묻는 질문 FAQ
Q1) 이 API가 서비스 계정으로 되는지는 어떻게 확인하나
해당 API 공식 문서의 인증 섹션에 “서비스 계정”이 언급돼 있는지 먼저 확인한다. 애매하면 실제로 서비스 계정 키로 호출해보면 된다. 사람 계정이 필요한 API는 권한 오류나 계정 연결 오류를 명확히 돌려준다.
Q2) 서버에서만 돌리는 스크립트라 브라우저 자체가 없는데 어떻게 하나
최초 1회 사람이 브라우저로 승인해야 하는 구조 자체는 피할 수 없다. 로컬 컴퓨터에서 최초 인증만 마친 뒤, 그 결과로 나온 토큰 파일을 서버로 옮겨 재사용하는 방식이 일반적이다. 이 파일이 곧 로그인 열쇠이므로 전송과 보관 모두 암호화된 경로로 다뤄야 한다.
Q3) 프로덕션으로 바꾸면 리프레시 토큰은 영원히 유효한가
정해진 만료 기한은 없어지지만 무조건 영구는 아니다. 6개월 넘게 스크립트를 안 돌리거나, 본인이 직접 앱 권한을 철회하거나, 구글 계정 비밀번호를 바꾸는 경우 등에는 토큰이 무효화될 수 있다. 스크립트에 인증 오류가 나면 자동으로 다시 브라우저를 여는 예외 처리를 같이 넣어두는 게 안전하다.