네이버 클라우드 플랫폼(NCP)의 SENS(Simple & Easy Notification Service) API를 활용해 회원가입 인증문자나 알림톡을 발송하려다가 401 Unauthorized 에러에 막혀본 경험이 있는가? 공식 가이드 문서를 보면서 그대로 cURL 요청을 보냈는데도 인증에 계속 실패하는 경우가 비일비재하다. 대부분의 개발자들은 Access Key와 Secret Key만 헤더에 적절히 넣으면 작동할 것이라 착각하지만, NCP API는 별도의 HMAC-SHA256 기반 시그니처 서명을 요구한다.
이번 글에서는 NCP SENS API가 요구하는 인증 메커니즘을 정확히 이해하고, 401 에러를 일으키는 원인과 이를 해결하는 PHP cURL 기반의 HMAC-SHA256 시그니처 생성 알고리즘을 완벽히 정리해보겠다.
네이버 클라우드 플랫폼의 API 보안 체계는 단순히 API 키 하나를 전송하는 방식이 아니다. 외부 악의적 요청이나 요청 데이터 위변조를 막기 위해 **타임스탬프 기반 HMAC-SHA256 암호화 시그니처**를 매 요청마다 새로 생성해서 헤더에 실어 보내야 한다.
API 호출 시 필수적으로 포함되어야 하는 HTTP 헤더 항목은 아래와 같다.
| 헤더명 | 설명 | 예시 / 형식 |
|---|---|---|
| x-ncp-apigw-timestamp | 현재 시각의 Unix Timestamp (밀리초 단위) | 1680000000000 (13자리 숫자로 전달) |
| x-ncp-iam-access-key | NCP 마이페이지에서 발급받은 Access Key ID | ncp_iam_access_key_example |
| x-ncp-apigw-signature-v2 | HMAC-SHA256으로 암호화 후 Base64 인코딩한 문자열 | q8Z... (서명 문자열) |
이 중 가장 실패가 많이 발생하는 지점이 바로 x-ncp-apigw-signature-v2 헤더 값이다. 요청 메서드, URI, 타임스탬프, Access Key 조합을 줄바꿈 문자(\n)로 연결한 후, 발급받은 Secret Key로 암호화해야만 정상 처리된다.
시그니처를 만들 때 조합하는 원본 문자열(Message)은 아주 엄격한 규격을 따른다. 띄어쓰기 한 칸이나 줄바꿈 문자가 누락되면 암호화 해시값이 완전히 달라져 401 에러가 발생한다.
조합할 서명 원본 문자열의 구조는 다음과 같다.
[HTTP-Method] + " " + [Request-URI] + "\n" + [Timestamp] + "\n" + [AccessKey]
1. HTTP Method는 대문자(GET, POST 등)로 작성해야 한다.
2. Request-URI는 도메인(https://sens.apigw.ntruss.com)을 제외한 경로 전체(예: /sms/v2/services/ncp:sms:kr:123456789012:service_id/messages)를 포함해야 한다.
3. 타임스탬프는 10자리(초)가 아닌 13자리(밀리초) Unix Timestamp여야 하며, 헤더의 x-ncp-apigw-timestamp에 넣은 값과 토씨 하나 틀리지 않고 동일해야 한다.
이제 실제로 PHP 환경에서 SENS SMS 발송 API를 호출할 때 발생하는 오답 코드와 올바른 연동 코드를 비교해보자.
많은 개발자가 타임스탬프를 초 단위(10자리)로 전달하거나, Base64 바이너리 인코딩 옵션을 누락하여 401 Unauthorized 에러를 겪는다.
<?php
// 잘못된 예시: 타임스탬프 단위 오류 및 hash_hmac 출력 방식 오류
$accessKey = "YOUR_NCP_ACCESS_KEY";
$secretKey = "YOUR_NCP_SECRET_KEY";
$serviceId = "ncp:sms:kr:123456789012:service_id";
$timestamp = time(); // ✗ 10자리 초 단위 타임스탬프 사용
$url = "/sms/v2/services/{$serviceId}/messages";
// ✗ 줄바꿈 서식 및 raw_output 옵션 누락
$message = "POST " . $url . "\n" . $timestamp . "\n" . $accessKey;
$signature = base64_encode(hash_hmac('sha256', $message, $secretKey)); // 16진수 문자열을 다시 base64 변환하는 실수
?>
밀리초 단위 타임스탬프를 생성하고, hash_hmac() 함수의 네 번째 인자로 true(Raw Binary Output)를 전달하여 정상적인 Base64 시그니처를 만드는 올바른 코드다.
<?php
$accessKey = "YOUR_NCP_ACCESS_KEY";
$secretKey = "YOUR_NCP_SECRET_KEY";
$serviceId = "ncp:sms:kr:123456789012:service_id"; // SENS 서비스 ID
// 1. 13자리 밀리초 타임스탬프 생성
$timestamp = (string)round(microtime(true) * 1000);
// 2. HTTP 요청 경로 설정
$uri = "/sms/v2/services/" . rawurlencode($serviceId) . "/messages";
$fullUrl = "https://sens.apigw.ntruss.com" . $uri;
// 3. 서명 원본 문자열 조합
$method = "POST";
$space = " ";
$newLine = "\n";
$message = $method . $space . $uri . $newLine . $timestamp . $newLine . $accessKey;
// 4. HMAC-SHA256 암호화 및 Base64 인코딩 (raw_output = true 필수)
$signature = base64_encode(hash_hmac('sha256', $message, $secretKey, true));
// 5. 발송 요청 바디 데이터 준비
$data = [
'type' => 'SMS',
'contentType' => 'COMM',
'countryCode' => '82',
'from' => '01012345678', // 발신번호 (NCP에 사전 등록된 번호)
'content' => '[REDINFO] 인증번호는 [123456] 입니다.',
'messages' => [
[
'to' => '01098765432' // 수신번호
]
]
];
$postFields = json_encode($data);
// 6. cURL 요청 전송
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $fullUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json; charset=utf-8',
'x-ncp-apigw-timestamp: ' . $timestamp,
'x-ncp-iam-access-key: ' . $accessKey,
'x-ncp-apigw-signature-v2: ' . $signature
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 202) {
echo "SMS 발송 성공: " . $response;
} else {
echo "발송 실패 (HTTP Status " . $httpCode . "): " . $response;
}
?>
정상적으로 서명이 인증되어 202 Accepted 응답을 수신했을 때의 JSON 구조는 다음과 같다.
{
"requestId": "202603301234567890abcdef",
"requestTime": "2026-03-30T10:00:00.000",
"statusCode": "200",
"statusName": "success"
}
코드를 바르게 작성했음에도 여전히 401이나 400 에러가 난다면 아래 항목을 체크해봐야 한다.
- Service ID 포맷 오기입: 프로젝트 설정의 Service ID 전체(
ncp:sms:kr:...)를 URI 경로에 정확히 넣었는지 확인하자. - hash_hmac() 4번째 파라미터 누락: PHP의
hash_hmac()기본값은 Hex 문자열을 반환한다. NCP 서명은 Raw Binary 데이터를 필요로 하므로 반드시 네 번째 인자를true로 넘긴 후base64_encode()를 씌워야 한다. - 서버 시각 불일치: 백엔드 서버의 NTP 시각이 실제 표준시와 크게 틀어져 있으면 NCP API Gateway가 유효하지 않은 요청으로 판단해 거부한다.
- 발신번호 미등록: NCP 콘솔 내 [SENS] > [Calling Number] 메뉴에 사전 등록되지 않은 번호를
from파라미터에 입력하면 400 에러가 리턴된다.
NCP SENS API 연동의 핵심은 **정확한 시그니처 생성과 헤더 규격 준수**다. 작은 공백 하나나 타임스탬프의 오차가 인증 실패로 이어질 수 있다는 점을 잊지 말자. 이 글의 **PHP cURL 예제 코드와 시그니처 생성 로직**을 참고해 백엔드 발송 모듈을 구현하면, 에러 없는 안정적인 문자 발송 시스템을 완성할 수 있을 것이다.