외부 API를 호출할 때 타임아웃은 매우 흔한 문제입니다. 타임아웃이 제대로 설정되지 않으면 다음과 같은 문제들이 발생할 수 있습니다:
<?php
// 문제가 있는 코드 - 타임아웃 미설정
function badApiCall($url) {
// 기본 타임아웃(30초)으로 동작하여 너무 오래 기다림
$response = file_get_contents($url);
return json_decode($response, true);
}
// cURL 사용 시에도 타임아웃 미설정
function badCurlCall($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// CURLOPT_TIMEOUT 미설정으로 무한 대기 가능
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
?>
<?php
class ApiClient {
private $connectTimeout;
private $timeout;
public function __construct($connectTimeout = 5, $timeout = 10) {
$this->connectTimeout = $connectTimeout;
$this->timeout = $timeout;
}
public function get($url, $headers = []) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
// 타임아웃 설정
CURLOPT_CONNECTTIMEOUT => $this->connectTimeout, // 연결 타임아웃
CURLOPT_TIMEOUT => $this->timeout, // 전체 타임아웃
// SSL 설정
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
// 헤더 설정
CURLOPT_HTTPHEADER => $headers,
// User-Agent 설정
CURLOPT_USERAGENT => 'MyApp/1.0'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("cURL Error: " . $error);
}
if ($httpCode >= 400) {
throw new Exception("HTTP Error: " . $httpCode);
}
return $response;
}
public function post($url, $data, $headers = []) {
$ch = curl_init();
// JSON 데이터 전송을 위한 기본 헤더
$defaultHeaders = ['Content-Type: application/json'];
$headers = array_merge($defaultHeaders, $headers);
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
// 타임아웃 설정
CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
CURLOPT_TIMEOUT => $this->timeout,
// 헤더 설정
CURLOPT_HTTPHEADER => $headers,
// SSL 설정
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("cURL Error: " . $error);
}
if ($httpCode >= 400) {
throw new Exception("HTTP Error: " . $httpCode);
}
return $response;
}
}
// 사용 예시
try {
$client = new ApiClient(5, 10); // 연결 5초, 전체 10초 타임아웃
$response = $client->get('https://api.example.com/data');
$data = json_decode($response, true);
echo "API 응답: " . print_r($data, true);
} catch (Exception $e) {
echo "API 호출 실패: " . $e->getMessage();
}
?>
<?php
class SimpleApiClient {
public static function get($url, $timeout = 10, $headers = []) {
// 컨텍스트 옵션 설정
$context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => $timeout,
'header' => implode("\r\n", array_merge([
'User-Agent: MyApp/1.0',
'Accept: application/json'
], $headers)),
'ignore_errors' => true // HTTP 에러 코드도 응답으로 받기
]
]);
$response = @file_get_contents($url, false, $context);
if ($response === false) {
throw new Exception("API 호출 실패: " . error_get_last()['message']);
}
// HTTP 응답 코드 확인
$httpCode = self::getHttpResponseCode($http_response_header);
if ($httpCode >= 400) {
throw new Exception("HTTP Error: " . $httpCode);
}
return $response;
}
public static function post($url, $data, $timeout = 10, $headers = []) {
$postData = json_encode($data);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'timeout' => $timeout,
'header' => implode("\r\n", array_merge([
'Content-Type: application/json',
'Content-Length: ' . strlen($postData),
'User-Agent: MyApp/1.0'
], $headers)),
'content' => $postData,
'ignore_errors' => true
]
]);
$response = @file_get_contents($url, false, $context);
if ($response === false) {
throw new Exception("API 호출 실패: " . error_get_last()['message']);
}
$httpCode = self::getHttpResponseCode($http_response_header);
if ($httpCode >= 400) {
throw new Exception("HTTP Error: " . $httpCode);
}
return $response;
}
private static function getHttpResponseCode($headers) {
if (empty($headers[0])) {
return 0;
}
preg_match('/HTTP\/\d\.\d\s+(\d+)/', $headers[0], $matches);
return isset($matches[1]) ? (int)$matches[1] : 0;
}
}
// 사용 예시
try {
$response = SimpleApiClient::get('https://api.example.com/data', 5);
$data = json_decode($response, true);
echo "데이터: " . print_r($data, true);
} catch (Exception $e) {
echo "오류: " . $e->getMessage();
}
?>
composer require guzzlehttp/guzzle
<?php
require_once 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ConnectException;
class GuzzleApiClient {
private $client;
public function __construct($baseUri = '', $defaultTimeout = 10) {
$this->client = new Client([
'base_uri' => $baseUri,
'timeout' => $defaultTimeout,
'connect_timeout' => 5,
'headers' => [
'User-Agent' => 'MyApp/1.0',
'Accept' => 'application/json'
]
]);
}
public function get($endpoint, $options = []) {
try {
$response = $this->client->get($endpoint, $options);
return $response->getBody()->getContents();
} catch (ConnectException $e) {
throw new Exception("연결 타임아웃: " . $e->getMessage());
} catch (RequestException $e) {
if ($e->hasResponse()) {
$statusCode = $e->getResponse()->getStatusCode();
throw new Exception("HTTP Error {$statusCode}: " . $e->getMessage());
}
throw new Exception("요청 실패: " . $e->getMessage());
}
}
public function post($endpoint, $data, $options = []) {
try {
$defaultOptions = [
'json' => $data,
'headers' => [
'Content-Type' => 'application/json'
]
];
$options = array_merge_recursive($defaultOptions, $options);
$response = $this->client->post($endpoint, $options);
return $response->getBody()->getContents();
} catch (ConnectException $e) {
throw new Exception("연결 타임아웃: " . $e->getMessage());
} catch (RequestException $e) {
if ($e->hasResponse()) {
$statusCode = $e->getResponse()->getStatusCode();
throw new Exception("HTTP Error {$statusCode}: " . $e->getMessage());
}
throw new Exception("요청 실패: " . $e->getMessage());
}
}
public function getWithCustomTimeout($endpoint, $timeout) {
return $this->get($endpoint, ['timeout' => $timeout]);
}
}
// 사용 예시
try {
$client = new GuzzleApiClient('https://api.example.com/', 10);
// 기본 타임아웃으로 요청
$response = $client->get('/users');
// 커스텀 타임아웃으로 요청
$slowResponse = $client->getWithCustomTimeout('/slow-endpoint', 30);
echo "응답: " . $response;
} catch (Exception $e) {
echo "오류: " . $e->getMessage();
}
?>
<?php
class RetryApiClient {
private $maxRetries;
private $retryDelay;
private $timeout;
public function __construct($maxRetries = 3, $retryDelay = 1, $timeout = 10) {
$this->maxRetries = $maxRetries;
$this->retryDelay = $retryDelay;
$this->timeout = $timeout;
}
public function callWithRetry($url, $method = 'GET', $data = null) {
$attempt = 0;
$lastException = null;
while ($attempt < $this->maxRetries) {
try {
return $this->makeRequest($url, $method, $data);
} catch (Exception $e) {
$lastException = $e;
$attempt++;
// 마지막 시도가 아니면 대기 후 재시도
if ($attempt < $this->maxRetries) {
$delay = $this->retryDelay * pow(2, $attempt - 1); // 지수 백오프
sleep($delay);
error_log("API 호출 재시도 {$attempt}/{$this->maxRetries} - {$delay}초 대기 후");
}
}
}
throw new Exception("API 호출 최종 실패 ({$this->maxRetries}회 시도): " . $lastException->getMessage());
}
private function makeRequest($url, $method, $data) {
$ch = curl_init();
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_USERAGENT => 'RetryApiClient/1.0',
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Content-Type: application/json'
]
];
if ($method === 'POST' && $data !== null) {
$options[CURLOPT_POST] = true;
$options[CURLOPT_POSTFIELDS] = json_encode($data);
}
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("cURL Error: " . $error);
}
// 5xx 에러는 재시도, 4xx 에러는 재시도하지 않음
if ($httpCode >= 500) {
throw new Exception("Server Error: " . $httpCode);
} elseif ($httpCode >= 400) {
throw new Exception("Client Error: " . $httpCode . " (재시도 안함)");
}
return $response;
}
}
// 사용