공공데이터포털에서 오픈 API 신청을 마치고 발급받은 인증키로 PHP cURL 요청을 보냈는데, 분명 제대로 전달한 것 같은 키가 'SERVICE KEY IS NOT REGISTERED' 오류를 뱉어 당황한 경험이 있을 것이다. 포털 마이페이지에서는 분명 정상 승인 상태로 나오는데 API 서버는 지속해서 인증 실패를 반환한다.
다만 대부분의 개발자들은 공공데이터포털이 제공하는 두 종류의 인증키 특성을 모른 채 인터넷 예제 코드를 그냥 복사해 붙인다. 이번에는 이 에러가 발생하는 정확한 원인을 분석하고, PHP cURL 환경에서 서비스키 인코딩 문제를 깔끔하게 해결하는 방법을 완벽하게 정리해서 소개하겠다.

 

1. 공공데이터포털 인증키 구조와 에러 발생 원인

공공데이터포털의 마이페이지에 들어가 보면 인증키가 'Encoding'과 'Decoding' 두 가지 버전으로 제공된다. 서비스키 내부에는 플러스(+), 슬래시(/), 등호(=) 같은 특수문자가 다수 포함되어 있다.
HTTP GET 요청 시 이 특수문자들은 URL 규격에 맞게 변환되어야 한다. 문제의 핵심은 PHP에서 URL을 생성할 때 일어나는 이중 인코딩(Double Encoding) 현상이다.

구분Encoding 인증키Decoding 인증키
특성URL 인코딩이 이미 완료된 형태특수문자가 원본 문자 그대로 포함된 형태
http_build_query() 사용 시% 기호가 %25로 재인코딩되어 키 파손자동 인코딩되어 올바른 값 전달
쿼리스트링 직접 결합 시문자열 그대로 붙여서 사용 가능특수문자 누락으로 인지되어 실패

 

2. 흔히 범하는 cURL 요청 실수와 올바른 해결 방법

백엔드 실무에서 외부 API를 호출할 때 배열을 만들어 쿼리 스트링을 생성하는 것이 일반적이다. 하지만 공공데이터포털 API는 이 과정에서 자주 문제를 일으킨다.

  • 문제 1: Encoding 키를 http_build_query() 함수에 전달하여 %25 형태의 이중 인코딩이 발생하는 경우
  • 해결 1: Decoding 키를 배열에 전달하거나, Encoding 키를 쿼리스트링 맨 뒤에 직접 결합하기
  • 문제 2: API 신규 신청 직후 서버 동기화 지연(최대 1시간 소요)을 고려하지 않고 바로 호출하는 경우
  • 해결 2: 포털 내부 시스템 동기화 시간을 감안하여 10분에서 30분 정도 대기 후 재시도하기

 

3. 실전 PHP cURL 연동 코드 구현

실제 백엔드 환경에서 문제없이 동작하는 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);

 

✓ 올바른 코드 (Decoding 키 원본 활용 및 안전한 쿼리 결합)
<?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"
          }
        ]
      }
    }
  }
}

 

4. 주의사항 및 흔한 실수

✗ 잘못된 방식: 인증 에러가 난다고 해서 인터넷 블로그 구석에 나오는 `CURLOPT_SSL_VERIFYPEER = false` 설정을 무작위로 붙이거나, string replace로 %25를 강제로 %로 바꾸는 편법을 쓰는 행동. 이는 보안 취약점을 만들고 파라미터가 추가될 때 다시 깨진다.
✓ 올바른 방식: 환경 변수(.env)에는 무조건 **Decoding 키**를 원본 상태로 보관하고, HTTP 요청을 조립할 때 `urlencode($decodingKey)`를 거쳐 쿼리스트링에 연결하는 규칙을 프로젝트 전체 표준으로 세우는 것이 확실하다.

 

5. 요약 및 마무리

공공데이터포털 API 연동 시 발생하는 인증 에러는 대부분 서비스 키의 인코딩 처리 문제에서 비롯된다. Encoding 키와 Decoding 키의 차이를 정확히 구분하고 cURL 요청 시 URL 생성 방식을 바로 잡으면 원인 불명의 99 에러나 키 미등록 에러로 허비하는 시간을 완전히 줄일 수 있다.
공공데이터포털 API 연동은 공공 데이터 자원을 백엔드 서비스로 안전하게 가져오는 핵심 통로다. 정확한 인증키 인코딩 습관이 모여서 안정적인 외부 API 통신 환경을 만든다는 점을 잊지 말자. 이 글의 실전 cURL 코드 구조를 참고해 기존 호출 방식을 검토해 보면, 더 이상 인증 실패 없이 깨끗하게 API 응답을 수신할 수 있을 것이다.