외부 API를 연동할 때 PHP cURL을 많이 쓰는데, 개발 환경에선 잘 되다가 운영 환경에서만 갑자기 타임아웃이 나거나 SSL 인증서 오류가 뜨는 경험을 해봤을까? 다만 대부분의 개발자들은 이런 오류가 왜 발생하는지, 정확히 어떤 설정으로 해결해야 하는지 모르고 일단 오류를 무시하는 코드를 짜 버린다. 이번에는 cURL의 타임아웃 메커니즘, SSL 인증서 검증 원리, 그리고 실전에서 안정적인 외부 API 통신을 하는 방법을 완벽하게 정리해서 소개하겠다.

 

1단계: cURL 타임아웃과 SSL 검증의 기초 개념

cURL은 기본적으로 외부 서버와 통신할 때 세 가지 타임아웃을 관리한다. CONNECTTIMEOUT은 서버에 처음 연결하는 데 걸리는 시간, TIMEOUT은 전체 작업이 완료되는 데 걸리는 시간, 그리고 DNS 타임아웃은 도메인 이름을 IP로 변환하는 데 걸리는 시간이다. SSL 인증서 검증은 HTTPS 통신 시 서버의 인증서가 유효한지, 신뢰할 수 있는 인증기관에서 발급받았는지 확인하는 보안 메커니즘이다.

문제는 개발 환경에선 localhost나 자체 테스트 인증서를 쓰다가, 운영 환경으로 가면서 타임아웃 설정이나 SSL 검증 레벨이 달라지기 때문에 발생한다. 특히 공유 호스팅 환경에서는 DNS 요청이 느리거나, 외부 방화벽이 특정 포트를 제한할 수도 있다.

 

2단계: cURL 타임아웃 설정 방법

 

연결 타임아웃과 총 실행 타임아웃 구분하기

잘못된 코드: CONNECTTIMEOUT 없이 TIMEOUT만 설정

$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
curl_close($ch);

이 코드의 문제는 서버에 연결하는 데만 30초가 걸려도, CONNECTTIMEOUT이 없으면 계속 기다린다. 결국 전체 요청이 지연되거나 사용자 요청이 쌓인다.

올바른 코드: CONNECTTIMEOUT과 TIMEOUT 함께 설정

$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);  // 연결 타임아웃 5초
curl_setopt($ch, CURLOPT_TIMEOUT, 30);        // 전체 타임아웃 30초
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
if (curl_errno($ch)) {
    echo 'cURL Error: ' . curl_error($ch);
}
curl_close($ch);

이렇게 하면 서버 연결은 5초 내에 되어야 하고, 응답 받기까지는 총 30초를 기다린다. 만약 5초 안에 연결이 안 되면 즉시 오류를 반환한다.

 

DNS 타임아웃도 함께 고려하기

특히 공유 호스팅 환경에서는 DNS 쿼리가 느릴 수 있다. DNSTIMEOUT 설정을 추가하면 더 안정적이다.

$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_DNS_CACHE_TIMEOUT, 3600); // DNS 캐시 1시간
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

DNS 캐시를 설정하면 같은 도메인으로의 반복 요청 시 DNS 조회를 다시 하지 않아서 성능이 향상된다.

 

3단계: SSL 인증서 검증 문제 해결

 

SSL 검증 실패의 원인과 올바른 해결법

위험한 코드: SSL 검증을 완전히 비활성화

$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
curl_close($ch);

이 방법은 즉각적으로 SSL 오류를 없애지만, 중간자 공격(Man-in-the-Middle) 위험에 노출된다. 운영 환경에서 이렇게 하면 보안 취약점이 된다.

올바른 코드: 인증서 번들을 명시적으로 지정

$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_CAINFO, '/etc/ssl/certs/ca-certificates.crt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
if (curl_errno($ch)) {
    echo 'SSL Error: ' . curl_error($ch);
}
curl_close($ch);

CURLOPT_CAINFO는 CA 인증서 번들이 저장된 경로를 지정한다. Linux 시스템의 경우 보통 /etc/ssl/certs/ca-certificates.crt에 위치한다. PHP가 제공하는 내장 인증서 번들을 사용할 수도 있다.

 

PHP 내장 CA 번들 활용하기
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);

// PHP 내장 CA 번들 경로 자동 감지
if (defined('CURL_CA_BUNDLE_PATH')) {
    curl_setopt($ch, CURLOPT_CAINFO, CURL_CA_BUNDLE_PATH);
} else if (file_exists('/etc/ssl/certs/ca-certificates.crt')) {
    curl_setopt($ch, CURLOPT_CAINFO, '/etc/ssl/certs/ca-certificates.crt');
} else if (file_exists('/usr/local/share/ca-certificates/ca-certificates.crt')) {
    curl_setopt($ch, CURLOPT_CAINFO, '/usr/local/share/ca-certificates/ca-certificates.crt');
}

curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
if (curl_errno($ch)) {
    $error = curl_error($ch);
    error_log('API Request Failed: ' . $error);
    $response = null;
}
curl_close($ch);
return $response;

이렇게 하면 환경에 따라 적절한 CA 인증서를 자동으로 찾아서 사용한다.

 

4단계: 실전 예제 - 안정적인 API 호출 래퍼 함수

타임아웃과 SSL 검증을 모두 적용한 재사용 가능한 함수를 만들어 보자.

function safe_curl_request($url, $method = 'GET', $data = null, $headers = []) {
    $ch = curl_init($url);
    
    // 기본 타임아웃 설정
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_DNS_CACHE_TIMEOUT, 3600);
    
    // SSL 검증 설정
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
    
    // CA 인증서 경로 설정
    $ca_path = ini_get('curl.cainfo');
    if (!$ca_path || !file_exists($ca_path)) {
        // 시스템 CA 번들 찾기
        $possible_paths = [
            '/etc/ssl/certs/ca-certificates.crt',
            '/etc/ssl/certs/ca-bundle.crt',
            '/usr/local/share/ca-certificates/ca-certificates.crt'
        ];
        foreach ($possible_paths as $path) {
            if (file_exists($path)) {
                $ca_path = $path;
                break;
            }
        }
    }
    if ($ca_path && file_exists($ca_path)) {
        curl_setopt($ch, CURLOPT_CAINFO, $ca_path);
    }
    
    // HTTP 메서드 설정
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    
    // 요청 데이터 설정
    if ($data !== null) {
        if (is_array($data)) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
            $headers['Content-Type'] = 'application/json';
        } else {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        }
    }
    
    // 헤더 설정
    if (!empty($headers)) {
        $header_array = [];
        foreach ($headers as $key => $value) {
            $header_array[] = "$key: $value";
        }
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header_array);
    }
    
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_errno($ch) ? curl_error($ch) : null;
    
    curl_close($ch);
    
    return [
        'status' => $http_code,
        'body' => $response,
        'error' => $error,
        'success' => $error === null && $http_code >= 200 && $http_code < 300
    ];
}

// 사용 예제
$result = safe_curl_request(
    'https://api.example.com/users',
    'POST',
    ['name' => 'John', 'email' => 'john@example.com'],
    ['Authorization' => 'Bearer token123']
);

if ($result['success']) {
    $data = json_decode($result['body'], true);
    echo 'Success: ' . print_r($data, true);
} else {
    error_log('API Error: ' . ($result['error'] ?? 'HTTP ' . $result['status']));
}

 

5단계: 흔한 실수와 주의사항

 

✗ 타임아웃을 과도하게 크게 설정하기

타임아웃을 60초, 120초 같이 매우 크게 설정하면, 느린 외부 서버로 인한 병목이 계속 쌓인다. 결국 PHP-FPM이 응답할 수 없는 상태에 빠진다. 적절한 타임아웃은 보통 5~10초의 연결 타임아웃과 20~30초의 전체 타임아웃이다.

 

✗ SSL 오류를 무시하고 검증을 비활성화하기

운영 환경에서 SSL 검증을 끄는 것은 보안 취약점이다. 인증서가 만료되었거나 자체 서명된 인증서라면, CA 번들을 업데이트하거나 서버 인증서를 갱신해야 한다. 임시로 필요하면 테스트 환경에만 제한해야 한다.

 

✓ 타임아웃과 에러 처리를 함께 구현하기
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
$error_code = curl_errno($ch);
$error_msg = curl_error($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

// 구체적인 에러 메시지 처리
if ($error_code === CURLE_OPERATION_TIMEDOUT) {
    error_log('API Request Timeout');
} elseif ($error_code === CURLE_COULDNT_RESOLVE_HOST) {
    error_log('DNS Resolution Failed');
} elseif ($error_code === CURLE_SSL_CERTPROBLEM) {
    error_log('SSL Certificate Problem');
} elseif ($error_code !== 0) {
    error_log('cURL Error: ' . $error_msg);
} elseif ($http_code >= 400) {
    error_log('HTTP Error: ' . $http_code);
}

return $response;

 

마무리: 안정적인 외부 API 통신의 필수 요소

cURL 타임아웃과 SSL 검증은 외부 API를 연동할 때 반드시 신경 써야 하는 부분이다. 개발 환경에서 잘 작동하는 코드가 운영 환경에서 갑자기 느려지거나 오류가 나는 일을 방지하려면, 처음부터 적절한 타임아웃을 설정하고 SSL 검증을 활성화해야 한다. 위 예제의 safe_curl_request() 함수처럼 재사용 가능한 래퍼를 만들어 두면, 모든 API 호출이 일관되게 안정적으로 처리된다. 작은 설정 차이가 전체 시스템의 응답성과 보안을 크게 좌우한다는 점을 잊지 말자.