디웹시 Blog 검색

공공데이터포털 401 오류부터 구글 OAuth 차단까지, API 인증 삽질 모음

공공데이터포털 401 오류, 구글 서비스 계정 키 생성 차단, OAuth 테스트 모드, 네이버 검색 API 이관까지. 사이트 공장을 만들며 하루에 연달아 막힌 API 인증 문제와 실제로 통한 해결법을 정리했어요.

수정

공유

“401 유효하지 않은 인증키 입니다.” 공공데이터포털 401 오류를 처음 봤을 때 제 반응은 이랬어요. 내 계정 인증키 넣으면 되는 거 아냐? 키는 분명히 넣었거든요. 그런데 이게 시작에 불과했어요. 같은 주에 구글은 서비스 계정 키를 못 만들게 막았고, OAuth 로그인은 세 번 연속으로 다른 이유로 차단됐고, 네이버 검색 API는 어느새 다른 플랫폼으로 이사 가 있었어요.

이 글은 사이트 공장 시리즈 3편이에요. 2편에서 공장 서버를 세웠다면, 이번엔 그 서버가 바깥 API들과 말을 트게 만드는 과정이에요. 서버 세팅보다 이쪽이 훨씬 오래 걸렸어요. 같은 오류 메시지를 검색해서 들어오신 분이라면 해당 섹션만 보셔도 돼요. 오류마다 원인과 실제로 통한 해결법을 따로 정리해뒀어요.

먼저 한눈에 보면 이래요.

막힌 곳실제 원인해결
공공데이터포털 401Encoding 키 이중 인코딩, API별 활용신청 미완료Decoding 키 사용, 활용신청 확인
구글 서비스 계정 키 생성조직 기본 보안 정책내 계정 OAuth 인증으로 전환
OAuth 로그인 403 org_internal앱 유형 내부, 로그인은 개인 Gmail개인 계정 프로젝트에서 외부로 생성
OAuth 로그인 403 access_denied앱이 테스트 상태브랜딩 정보 채우고 앱 게시
네이버 검색 APINAVER API HUB로 이관새 Client ID 발급, 헤더 변경
브이월드 경계 데이터해외 서버에서 호출 불가카카오 좌표→행정구역 변환으로 대체

공공데이터포털 401 오류, 키를 넣었는데 왜?

공장 리서처가 공공데이터를 조회하다가 401을 뱉었어요. 버림값을 만들 때 쓰던 제 계정 인증키를 그대로 넣었는데도요. AI 코딩 도구가 원인을 짚어줬는데, 키를 어떤 형태로 넣었는지가 문제였어요.

공공데이터포털은 인증키를 두 가지로 보여줘요. 일반 인증키(Encoding)와 일반 인증키(Decoding)요. Encoding 키는 이미 %2B, %3D 같은 문자로 인코딩된 상태예요. 이걸 파이썬 requests의 params에 그대로 넣으면 라이브러리가 한 번 더 인코딩해요. 그러면 포털 입장에서는 엉뚱한 키가 들어온 거라 401이 나요. 제가 .env에 넣은 게 바로 그 Encoding 키였어요. Decoding 키로 바꾸고 큰따옴표로 감싸서 넣었더니 해결됐어요. Decoding 키에는 +, /, = 같은 문자가 섞여 있어서 따옴표 없이 넣으면 .env 파서가 다르게 읽을 수 있거든요.

공공데이터포털 401 오류가 나면 키를 의심하기 전에 키의 형태부터 보세요. 이건 저만 겪는 문제가 아니라서, 공공데이터 API를 감싼 오픈소스 라이브러리들도 아예 대비를 해둬요. 한 의약품 OpenAPI 파이썬 클라이언트 문서는 키에 %가 있으면 Encoding 키로 판별해서 그대로 쓰고, 없으면 Decoding 키로 보고 직접 인코딩해서 이중 인코딩을 막는다고 설명해요. 같은 문서에 API마다 각각 활용신청이 필요하다는 안내도 있고요.

이 두 번째 내용이 공공데이터포털 401 오류의 또 다른 원인이에요. 인증키는 계정에 하나지만, 호출하려는 API마다 활용신청을 따로 해야 해요. 버림값 때 신청한 API는 잘 되는데, 리서처가 새로 찾은 API는 신청을 안 했으니 같은 키로도 막히는 거예요. 그래서 원인을 가를 때 버림값 API로 같은 키를 먼저 호출해봤어요. 버림값 API가 되면 키는 정상이고 활용신청 문제, 버림값 API도 안 되면 키 형태 문제예요. 키 앞뒤에 공백이나 줄바꿈이 섞여 들어가도 똑같이 401이 나니까, 버림값 API까지 막히면 복사한 값 자체도 한 번 의심해보는 게 좋아요. 공공데이터포털 401 오류는 결국 이 세 갈래 안에서 끝났어요.

공공데이터포털 마이페이지에서 Encoding과 Decoding 인증키가 나란히 보이는 화면

여기서 설계도 하나 바꿨어요. 리서처가 새로 찾은 API를 무작정 호출하게 두면 활용신청이 안 된 곳에서 계속 401이 날 테니까요. 그래서 리서치 단계에서는 실제 호출 대신 데이터셋 정보로 판단하게 했어요. 다행히 표준데이터는 파일로 내려받는 방식이라 활용신청이 필요 없었고, 주차장 데이터도 그쪽이었어요.

공공데이터포털 키에는 나중에 알게 된 함정이 하나 더 있어요. 이 키는 주소 뒤에 serviceKey라는 이름으로 붙여서 보내요. 헤더로 보내는 카카오나 네이버와 달리, 요청 주소에 키가 그대로 들어가는 거죠. 그런데 HTTP 라이브러리는 요청 주소를 통째로 로그에 남기는 경우가 있어요. 공장 로그를 점검해보니 리서치가 돌 때마다 키가 찍혔을 가능성이 있었어요. 서버 안의 로그였고 바깥으로 나간 흔적은 없어서 재발급까지는 하지 않았지만, 그 뒤로 로그에서 serviceKey, token 같은 값은 전부 가리도록 바꿨어요. 주소에 키를 싣는 API는 로그부터 의심해야 해요.

구글 서비스 계정 키 생성이 막혔다

서치콘솔에 사이트를 자동으로 등록하려면 구글 API 인증이 필요해요. 처음엔 서버용으로 흔히 쓰는 서비스 계정 키를 받으려 했어요. 그런데 키 만들기 버튼을 누르자 “서비스 계정 키 생성 사용 중지됨” 창이 떴어요. 적용된 정책 ID는 iam.disableServiceAccountKeyCreation이었고요. 엥? 소리가 절로 나왔어요.

원인은 제가 뭘 잘못한 게 아니라 구글의 기본 보안 정책이었어요. 구글 클라우드 공식 문서에 따르면 2024년 5월 3일 이후에 만들어진 조직에는 기본 보안 정책 묶음이 자동으로 적용되고, 그 안에 서비스 계정에 영구 키를 만들지 못하게 막는 항목이 들어 있어요. 구글 워크스페이스 같은 조직에 속한 계정으로 프로젝트를 만들었다면 이 창을 볼 가능성이 높아요.

정책을 프로젝트 단위로 풀 수도 있어요. 다만 조직 정책 관리자 역할을 직접 부여해야 하고, 구글이 권장하는 길도 아니에요. 그래서 다른 방법을 택했어요. 서비스 계정 대신 제 구글 계정으로 한 번 로그인해서 받은 리프레시 토큰을 서버가 쓰는 방식이요. 이게 오히려 나았던 이유가 있어요. 공장이 제 계정 명의로 사이트를 등록하니까, 만든 사이트가 처음부터 제 서치콘솔 목록에 그대로 떠요. 서비스 계정 명의였다면 공장이 만든 사이트가 제 화면에는 안 보였을 거예요.

OAuth 로그인, 세 번 연속 다른 이유로 막혔다

여기서부터가 진짜 삽질이었어요. 맥에서 인증 스크립트를 돌리고 로그인했더니 “액세스 차단됨: site-factory은(는) 조직 내에서만 사용할 수 있습니다”가 떴어요. 403 오류, org_internal이었어요. AI가 제 계정이 조직 계정일 거라 보고 앱 사용자 유형을 내부로 안내했는데, 실제로 로그인한 건 개인 Gmail이었던 거예요. 내부 앱은 조직 소속 계정만 쓸 수 있어요.

그래서 설정을 외부로 바꾸러 콘솔에 들어갔더니 이번엔 “추가 액세스 권한 필요” 화면이 떴어요. 이제 안 들어가지잖아, 하고 당황했는데 원인은 단순했어요. OAuth 로그인을 하면서 브라우저 계정이 Gmail로 바뀌었고, 프로젝트는 조직 계정으로 만들었으니 권한이 없었던 거예요. 여기서 계정을 다시 바꾸는 대신, 아예 Gmail 계정으로 프로젝트를 새로 만들었어요. 개인 계정 프로젝트에는 조직이 없으니 키 생성을 막던 조직 정책도 안 걸리고, 콘솔 계정과 서치콘솔 소유자, OAuth 로그인 계정이 전부 하나로 통일돼요. 새 프로젝트에서는 API 라이브러리에서 Search Console API와 Site Verification API 두 개를 켜고, OAuth 클라이언트는 데스크톱 앱 유형으로 만들었어요. 사이트 추가와 사이트맵 제출은 앞의 API가, 도메인 소유권 확인은 뒤의 API가 맡아요. 데스크톱 앱 유형을 고른 건 맥에서 한 번만 브라우저로 로그인하면 되는 방식이라서예요.

새 프로젝트에서 외부 앱으로 만들고 다시 로그인하니 세 번째 차단이 떴어요. “앱은 현재 테스트 중이며 개발자가 승인한 테스터만 앱에 액세스할 수 있습니다.” 403 access_denied예요. 외부 앱은 만들면 기본이 테스트 상태라, 앱 게시를 따로 눌러야 해요. 그런데 대상 메뉴의 앱 게시 버튼이 회색으로 비활성화돼 있었어요. 마우스를 올려보니 앱 이름, 지원 이메일, 홈페이지 URL, 개인정보처리방침 URL이 필요하다는 안내가 떴고요. 브랜딩 메뉴에서 네 가지를 채우니 버튼이 살아났어요. 로고는 일부러 비워뒀어요. 로고를 올리면 브랜드 검수를 요구받을 수 있다고 해서요.

구글 OAuth 대상 메뉴에서 앱 게시 버튼이 비활성화되고 브랜딩 정보 안내가 뜬 화면

테스트 사용자에 제 Gmail만 추가하면 당장 로그인은 됐을 거예요. 그런데 그러면 안 되는 이유가 있어요. 구글 애즈 API 공식 문서에 따르면 외부 사용자 유형이면서 게시 상태가 테스트인 프로젝트는 7일 뒤에 만료되는 리프레시 토큰을 받고, 이를 피하려면 게시 상태를 프로덕션으로 바꾸라고 안내해요. 테스트 상태로 두면 공장이 일주일마다 조용히 멈춰요. 그래서 꼭 게시까지 갔어요. 게시 후 로그인하면 “Google에서 확인하지 않은 앱” 경고가 뜰 수 있는데, 제가 만든 앱이니 고급을 눌러 넘어가면 돼요. 저 혼자 쓰는 앱이라 검수 신청은 하지 않았어요. 인증 스크립트는 맥에서 딱 한 번 돌리면 끝이에요. 브라우저로 로그인해서 받은 리프레시 토큰을 서버에 두면, 그 뒤로는 서버가 알아서 접근 토큰을 갱신해요. 혹시 토큰이 만료되거나 철회되면 텔레그램으로 재인증하라는 알림이 오게 해뒀어요. 인증이 조용히 끊기는 게 제일 무서우니까요.

네이버 검색 API는 이사 가 있었다

리서처는 경쟁 사이트를 분석할 때 네이버 검색 결과를 봐요. 그래서 예전처럼 네이버 개발자센터에서 검색 API를 신청하려고 했는데, 신청 화면이 네이버 클라우드 플랫폼의 NAVER API HUB로 바뀌어 있었어요. 뉴스, 블로그, 웹문서, 검색어트렌드 같은 API가 카드로 쭉 나열된 화면이었죠.

NAVER API HUB 마이그레이션 가이드를 보면 기존 개발자센터에서 쓰던 Client ID와 Client Secret은 API HUB 호출에 쓸 수 없어서 새로 발급받은 값으로 교체해야 하고, 요청 헤더 이름도 X-Naver-Client-Id에서 X-NCP-APIGW-API-KEY-ID로 바뀌어요. 예전 코드를 그대로 쓰면 키를 새로 받아도 헤더 때문에 실패하는 거죠. 저는 웹문서, 블로그, 검색어트렌드 세 개만 신청했어요. 상위 결과가 지자체인지 블로그인지 판정하는 데 웹문서와 블로그를, 한철 반짝하는 주제를 걸러내는 데 검색어트렌드를 써요.

NAVER API HUB 애플리케이션 등록 화면에 검색 API 카드가 나열된 모습

하나 헷갈리기 쉬운 게 있어요. 키워드 검색량을 주는 네이버 검색광고 API는 이 HUB와 별개예요. 그건 여전히 검색광고 쪽에서 따로 발급받아요. 저도 처음엔 둘이 한곳으로 합쳐진 줄 알았어요. 그리고 API 카드마다 요금 안내 버튼이 붙어 있을 만큼 사용량에 따라 비용이 생길 수 있는 구조라, 공장 쪽에는 한도 초과 응답이 오면 재시도하지 말고 그 실행을 멈춘 뒤 텔레그램으로 알리라고 지시해뒀어요. 리서처가 루프에 빠져서 호출을 퍼붓는 사고를 막으려고요.

해외 서버에서 막힌 API는 우회로를 찾았다

주차장 사이트를 만들다가 주소에 동 이름이 빠진 주차장 수백 곳을 좌표로 동에 배정해야 하는 일이 생겼어요. 처음 계획은 국토교통부 공간정보 플랫폼인 브이월드에서 읍면동 경계 데이터를 받는 거였어요. 그런데 공장 서버가 핀란드에 있잖아요. 브이월드는 해외 IP에서는 안 되는 걸로 알고 있어서, 키를 받아봐야 서버에서 쓸 수 없었어요.

대신 카카오를 썼어요. 카카오 로컬 API 문서에 따르면 좌표로 행정구역정보를 받는 API가 있고, 응답에 행정동(H)과 법정동(B)이 구분돼서 와요. 사이트의 동 페이지가 법정동 기준이라 B만 쓰면 이름이 그대로 맞았어요. 서버에서 호출도 정상이었고요. 대상 672행 중 654행이 동에 배정됐고, 좌표가 아예 없는 17행과 다른 구로 나온 1행만 남았어요. 결과는 좌표별로 캐시해둬서 매주 데이터를 다시 받을 때는 카카오를 다시 부르지 않아요. 두 번째 수집 때 639건이 전부 캐시에서 나와서 실제 호출은 0건이었어요. 호출이 실패하거나 한도에 걸리면 그 행만 기타로 두고 수집은 계속 진행하게 했고요. 외부 API 하나 때문에 매주 데이터 갱신 전체가 멈추면 안 되니까요.

판정 모델 Jev도 비슷한 선택을 했어요. 여러 게이트웨이를 거쳐 쓸 수 있었지만, 중간 단계 없이 TypeSafe 직접 API로만 붙였어요. 공장은 파이썬이라 REST 호출 하나면 충분했거든요.

인증 삽질에서 남은 체크리스트

하나씩 놓고 보면 전부 별것 아닌 원인이었어요. 키 형태, 활용신청, 계정 종류, 게시 상태, 이사 간 플랫폼, 서버 위치. 그런데 한 주에 몰아서 맞으니 시간이 꽤 갔어요. 그래서 이제 새 API를 붙일 때는 순서를 정해두고 봐요. 키가 어떤 형태인지, API별 신청이 따로 필요한지, 지금 로그인한 계정이 프로젝트를 만든 계정과 같은지, 테스트 모드면 토큰이 언제 만료되는지, 문서가 아직 그 주소에 있는지, 해외 서버에서 호출되는지요.

새 API를 붙일 때 제일 먼저 할 일은 이미 되는 API로 같은 키를 시험해보는 것이에요. 공공데이터포털 401 오류 때도 버림값 API 하나로 키 문제와 활용신청 문제를 바로 갈랐어요. 원인을 둘로 쪼개면 해결은 금방이에요.

그리고 키는 전부 서버 .env에만 넣고 AI 코딩 도구 채팅창에는 붙여넣지 않기로 규칙을 정했는데, 그런데도 텔레그램 봇 토큰은 한 번 대화창에 그대로 찍혔어요. 다음 4편은 이렇게 연결해서 올린 첫 사이트, 전국 주차장 요금 사이트에서 사이트맵 267개가 전부 거부당한 이야기예요.

이어서