공공데이터포털에서 오픈 API 신청을 마치고 발급받은 인증키로 PHP cURL 요청을 보냈는데, 분명 제대로 전달한 것 같은 키가 'SERVICE KEY IS NOT REGISTERED' 오류를 뱉어 당황한 경험이 있을 것이다. 포털 마이페이지에서는 분명 정상 승인 상태로 나오는데 API 서버는 지속해서 인증 실패를 반환한다.
다만 대부분의 개발자들은 공공데이터포털이 제공하는 두 종류의 인증키 특성을 모른 채 인터넷 예제 코드를 그냥 복사해 붙인다. 이번에는 이 에러가 발생하는 정확한 원인을 분석하고, PHP cURL 환경에서 서비스키 인코딩 문제를 깔끔하게 해결하는 방법을 완벽하게 정리해서 소개하겠다.
공공데이터포털의 마이페이지에 들어가 보면 인증키가 'Encoding'과 'Decoding' 두 가지 버전으로 제공된다. 서비스키 내부에는 플러스(+), 슬래시(/), 등호(=) 같은 특수문자가 다수 포함되어 있다.
HTTP GET 요청 시 이 특수문자들은 URL 규격에 맞게 변환되어야 한다. 문제의 핵심은 PHP에서 URL을 생성할 때 일어나는 이중 인코딩(Double Encoding) 현상이다.
| 구분 | Encoding 인증키 | Decoding 인증키 |
|---|---|---|
| 특성 | URL 인코딩이 이미 완료된 형태 | 특수문자가 원본 문자 그대로 포함된 형태 |
| http_build_query() 사용 시 | % 기호가 %25로 재인코딩되어 키 파손 | 자동 인코딩되어 올바른 값 전달 |
| 쿼리스트링 직접 결합 시 | 문자열 그대로 붙여서 사용 가능 | 특수문자 누락으로 인지되어 실패 |
백엔드 실무에서 외부 API를 호출할 때 배열을 만들어 쿼리 스트링을 생성하는 것이 일반적이다. 하지만 공공데이터포털 API는 이 과정에서 자주 문제를 일으킨다.
- 문제 1: Encoding 키를 http_build_query() 함수에 전달하여 %25 형태의 이중 인코딩이 발생하는 경우
- 해결 1: Decoding 키를 배열에 전달하거나, Encoding 키를 쿼리스트링 맨 뒤에 직접 결합하기
- 문제 2: API 신규 신청 직후 서버 동기화 지연(최대 1시간 소요)을 고려하지 않고 바로 호출하는 경우
- 해결 2: 포털 내부 시스템 동기화 시간을 감안하여 10분에서 30분 정도 대기 후 재시도하기
실제 백엔드 환경에서 문제없이 동작하는 PHP cURL 연동 코드 예시를 확인해보자.
<?php
$endpoint = 'http://apis.data.go.kr/1360000/VilageFcstInfoService202001/getUltraSrtNcst';
// 이미 인코딩된 키를 가져와서 사용
$encodingKey = 'aBc123%2Fxyz%3D%3D';
$params = [
'serviceKey' => $encodingKey, // http_build_query가 %를 %25로 변환함!
'pageNo' => 1,
'numOfRows' => 10,
'dataType' => 'JSON'
];
// 이 시점에서 serviceKey=aBc123%252Fxyz%253D%253D 형태로 오염됨
$url = $endpoint . '?' . http_build_query($params);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
<?php
$endpoint = 'http://apis.data.go.kr/1360000/VilageFcstInfoService202001/getUltraSrtNcst';
// 마이페이지에서 제공하는 'Decoding' 키를 설정에 저장해두고 사용한다.
$decodingKey = 'aBc123/xyz==';
$params = [
'pageNo' => 1,
'numOfRows' => 10,
'dataType' => 'JSON',
'base_date' => '20240520',
'base_time' => '0600',
'nx' => 55,
'ny' => 127
];
// serviceKey는 urlencode를 거치도록 직접 명시하고 나머지 파라미터를 결합한다.
$queryString = http_build_query($params);
$fullUrl = $endpoint . '?serviceKey=' . urlencode($decodingKey) . '&' . $queryString;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $fullUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
throw new Exception('cURL Error: ' . curl_error($ch));
}
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
print_r($data);
}
{
"response": {
"header": {
"resultCode": "00",
"resultMsg": "NORMAL SERVICE."
},
"body": {
"dataType": "JSON",
"items": {
"item": [
{
"baseDate": "20240520",
"baseTime": "0600",
"category": "PTY",
"obsrValue": "0"
}
]
}
}
}
}
✗ 잘못된 방식: 인증 에러가 난다고 해서 인터넷 블로그 구석에 나오는 `CURLOPT_SSL_VERIFYPEER = false` 설정을 무작위로 붙이거나, string replace로 %25를 강제로 %로 바꾸는 편법을 쓰는 행동. 이는 보안 취약점을 만들고 파라미터가 추가될 때 다시 깨진다.
✓ 올바른 방식: 환경 변수(.env)에는 무조건 **Decoding 키**를 원본 상태로 보관하고, HTTP 요청을 조립할 때 `urlencode($decodingKey)`를 거쳐 쿼리스트링에 연결하는 규칙을 프로젝트 전체 표준으로 세우는 것이 확실하다.
공공데이터포털 API 연동 시 발생하는 인증 에러는 대부분 서비스 키의 인코딩 처리 문제에서 비롯된다. Encoding 키와 Decoding 키의 차이를 정확히 구분하고 cURL 요청 시 URL 생성 방식을 바로 잡으면 원인 불명의 99 에러나 키 미등록 에러로 허비하는 시간을 완전히 줄일 수 있다.
공공데이터포털 API 연동은 공공 데이터 자원을 백엔드 서비스로 안전하게 가져오는 핵심 통로다. 정확한 인증키 인코딩 습관이 모여서 안정적인 외부 API 통신 환경을 만든다는 점을 잊지 말자. 이 글의 실전 cURL 코드 구조를 참고해 기존 호출 방식을 검토해 보면, 더 이상 인증 실패 없이 깨끗하게 API 응답을 수신할 수 있을 것이다.