사용자 리뷰, 댓글, SNS 데이터를 수집했는데 이걸 어떻게 자동으로 분류할지 막막한 상황을 경험해봤을까? 수백 개 이상의 텍스트를 수작업으로 감정 분석하는 건 현실적으로 불가능하다. 다만 대부분의 개발자들은 자연어 처리(NLP) 감정 분석이 복잡한 머신러닝 모델이 필요하다고 착각해서 시도조차 하지 않는다. 이번에는 Naver Clova Sentiment API가 정확히 뭔지, 왜 필요한지, 어떻게 PHP에서 연동하는지 완벽하게 정리해서 소개하겠다.

 

Naver Clova Sentiment API란 무엇인가

Naver Clova Sentiment API는 네이버의 자연어 처리 기술을 기반으로 제공되는 감정 분석 서비스다. 주어진 텍스트의 감정을 긍정, 부정, 중립으로 분류하고, 각 감정의 신뢰도 점수를 반환한다. 별도 모델 학습 없이 API 호출만으로 정확한 감정 분석이 가능하다.

간단한 예를 들자면, "이 상품 정말 좋아요"라는 리뷰를 API에 보내면 긍정(positive) 분류와 함께 0.99 같은 신뢰도 점수를 받는 식이다. "배송이 너무 늦었어요"는 부정(negative)으로 판별된다. 이 데이터를 수집해 대시보드에 표시하거나 자동 알림 시스템을 구축할 수 있다.

 

API 클라이언트 등록 및 키 발급

먼저 Naver Developers 사이트에서 애플리케이션을 등록해야 한다. https://developers.naver.com/apps에 접속해 "애플리케이션 등록" 버튼을 클릭한다. 애플리케이션 이름, 사용 API(Clova Sentiment) 선택 후 등록하면 Client ID와 Client Secret을 발급받는다. 이 두 값이 API 인증에 필수다.

 

PHP에서 감정 분석 기본 구현

다음은 cURL로 API를 호출하는 기본 코드다.

<?php
// ✗ 잘못된 코드: 에러 처리 없이 응답 파싱
$text = "이 상품 정말 마음에 들어요!";
$client_id = "YOUR_CLIENT_ID";
$client_secret = "YOUR_CLIENT_SECRET";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://naveropenapi.apigw.naver.com/sentiment-analysis/v1/analyze");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["content" => $text]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Content-Type: application/json",
    "X-Naver-Client-Id: " . $client_id,
    "X-Naver-Client-Secret: " . $client_secret
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
$data = json_decode($response, true);
echo $data["sentiment"]; // NULL 오류 발생 가능
?>

✓ 올바른 코드: HTTP 상태 코드와 응답 검증

<?php
function analyzeSentiment($text, $client_id, $client_secret) {
    $url = "https://naveropenapi.apigw.naver.com/sentiment-analysis/v1/analyze";
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["content" => $text]));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Content-Type: application/json",
        "X-Naver-Client-Id: " . $client_id,
        "X-Naver-Client-Secret: " . $client_secret
    ]);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 5);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    // HTTP 상태 코드 확인
    if ($http_code !== 200) {
        throw new Exception("API 오류: HTTP " . $http_code . " - " . $response);
    }
    
    $data = json_decode($response, true);
    if (!isset($data["document"]["sentiment"])) {
        throw new Exception("응답 형식 오류");
    }
    
    return $data["document"];
}

// 사용 예
try {
    $result = analyzeSentiment("배송이 너무 늦었어요", "CLIENT_ID", "CLIENT_SECRET");
    echo "감정: " . $result["sentiment"] . "\n";
    echo "점수: " . $result["confidence"]["negative"] . "\n";
} catch (Exception $e) {
    echo "오류: " . $e->getMessage();
}
?>

API 응답은 다음과 같은 구조다. document 키 내부에 sentiment(감정 분류), confidence(각 감정별 신뢰도)가 포함된다. confidence는 긍정(positive), 부정(negative), 중립(neutral)의 확률을 모두 제공한다.

 

실전 예제: 리뷰 자동 분류 시스템

이제 실제 데이터베이스에 저장된 리뷰들을 배치 처리로 분석하는 예제를 살펴보자.

<?php
class SentimentAnalyzer {
    private $client_id;
    private $client_secret;
    private $pdo;
    
    public function __construct($client_id, $client_secret, $pdo) {
        $this->client_id = $client_id;
        $this->client_secret = $client_secret;
        $this->pdo = $pdo;
    }
    
    public function analyzeReviews() {
        // 분석되지 않은 리뷰 조회
        $stmt = $this->pdo->prepare("SELECT id, content FROM reviews WHERE sentiment IS NULL LIMIT 10");
        $stmt->execute();
        $reviews = $stmt->fetchAll(PDO::FETCH_ASSOC);
        
        foreach ($reviews as $review) {
            try {
                $result = $this->callAPI($review["content"]);
                $sentiment = $result["sentiment"];
                $confidence = $result["confidence"];
                
                // 신뢰도가 0.8 이상인 경우만 저장
                if ($confidence[$sentiment] >= 0.8) {
                    $update_stmt = $this->pdo->prepare(
                        "UPDATE reviews SET sentiment = :sentiment, confidence = :confidence, analyzed_at = NOW() WHERE id = :id"
                    );
                    $update_stmt->execute([
                        ":sentiment" => $sentiment,
                        ":confidence" => $confidence[$sentiment],
                        ":id" => $review["id"]
                    ]);
                } else {
                    // 신뢰도 낮은 경우 중립으로 표시
                    $update_stmt = $this->pdo->prepare(
                        "UPDATE reviews SET sentiment = 'neutral', confidence = :confidence, analyzed_at = NOW() WHERE id = :id"
                    );
                    $update_stmt->execute([
                        ":confidence" => $confidence[$sentiment],
                        ":id" => $review["id"]
                    ]);
                }
            } catch (Exception $e) {
                // API 오류 로깅
                error_log("리뷰 ID {$review['id']} 분석 실패: " . $e->getMessage());
            }
        }
    }
    
    private function callAPI($text) {
        $url = "https://naveropenapi.apigw.naver.com/sentiment-analysis/v1/analyze";
        
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["content" => $text]));
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            "Content-Type: application/json",
            "X-Naver-Client-Id: " . $this->client_id,
            "X-Naver-Client-Secret: " . $this->client_secret
        ]);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);
        
        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($http_code !== 200) {
            throw new Exception("API HTTP " . $http_code);
        }
        
        return json_decode($response, true)["document"];
    }
}

// 사용
$pdo = new PDO("mysql:host=localhost;dbname=myapp", "user", "pass");
$analyzer = new SentimentAnalyzer("CLIENT_ID", "CLIENT_SECRET", $pdo);
$analyzer->analyzeReviews();
?>

이 코드는 분석 결과를 데이터베이스에 저장하고, 신뢰도가 낮은 경우를 필터링한다. 배치 작업으로 정기적으로 실행하면 대량의 리뷰를 자동으로 분류할 수 있다.

 

주의사항 및 흔한 실수
항목 ✗ 잘못된 것 ✓ 올바른 것
인증 헤더 Authorization: Bearer token X-Naver-Client-Id, X-Naver-Client-Secret 헤더 사용
텍스트 길이 1000자 이상의 긴 문장 그대로 전송 최대 1000자로 제한, 필요시 분할 처리
요청 형식 {"text": "..."}로 요청 {"content": "..."}로 요청 (필드명 주의)
에러 처리 응답만 파싱하고 HTTP 코드 무시 HTTP 200 확인 후 JSON 파싱
API 호출 빈도 무한정 호출 Rate Limit 고려해 배치 처리 또는 캐싱 활용

 

감정 분석 결과 활용 방법

감정 분석 결과를 수집한 후 어떻게 활용할까? 가장 직관적인 방법은 대시보드에 통계를 표시하는 것이다. 예를 들어 "이번 달 리뷰 중 긍정 75%, 부정 15%, 중립 10%" 같은 차트를 보여줄 수 있다. 또 다른 방법은 자동 알림이다. 부정 리뷰가 들어오자마자 이를 감지해 고객 서비스 팀에 알림을 보내는 식이다. 신뢰도 점수가 높은 부정 리뷰만 선별해서 우선 처리할 수도 있다.

<?php
// 부정 리뷰 자동 알림
$stmt = $pdo->prepare("SELECT * FROM reviews WHERE sentiment = 'negative' AND confidence > 0.9 AND notified = 0");
$stmt->execute();
$negative_reviews = $stmt->fetchAll(PDO::FETCH_ASSOC);

foreach ($negative_reviews as $review) {
    // Slack 또는 이메일로 알림 전송
    sendAlert("새로운 부정 리뷰: " . $review["content"]);
    
    $update = $pdo->prepare("UPDATE reviews SET notified = 1 WHERE id = :id");
    $update->execute([":id" => $review["id"]]);
}
?>

 

성능 최적화와 비용 절감

Naver Clova API는 호출 횟수에 따라 비용이 발생한다. 따라서 불필요한 호출을 줄이는 게 중요하다. 가장 효과적인 방법은 캐싱이다. 동일한 텍스트가 여러 번 입력되는 경우가 많으니 분석 결과를 Redis나 Memcached에 저장해두고 재사용하는 식이다. 또 다른 방법은 배치 처리로 한 번에 많은 데이터를 처리하되 시간대를 제한하는 것이다. 새벽 시간에 일괄 처리하면 API 호출을 일정하게 유지할 수 있다.

 

정리 및 다음 단계

Naver Clova Sentiment API는 복잡한 머신러닝 없이도 정확한 감정 분석을 가능하게 해준다. 인증 헤더, 요청 형식, 응답 파싱 같은 기본을 제대로 이해하면 리뷰 분류부터 고객 감정 모니터링까지 다양한 실무 활용이 가능하다. 이 글의 코드 예제와 주의사항을 참고해 먼저 작은 규모로 테스트해보고, 그 다음 자신의 프로젝트에 맞게 확장하면 된다. 감정 분석이 일상화되면 데이터 기반 의사결정의 정확도가 크게 높아질 것이다.