PHP 외부 API 호출 시 타임아웃 문제와 해결책

프리터코더·2025년 5월 25일
0

php 문제 해결

목록 보기
19/79

문제 상황

외부 API를 호출할 때 타임아웃은 매우 흔한 문제입니다. 타임아웃이 제대로 설정되지 않으면 다음과 같은 문제들이 발생할 수 있습니다:

  • 응답이 느린 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);
}
?>

해결책

1. cURL을 이용한 타임아웃 설정

<?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();
}
?>

2. file_get_contents()를 이용한 타임아웃 설정

<?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();
}
?>

3. Guzzle HTTP 클라이언트 사용

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();
}
?>

4. 재시도 로직이 포함된 API 클라이언트

<?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;
    }
}

// 사용
profile
일용직 개발자. freetercoder@gmail.com

0개의 댓글