HTTP 클라이언트란
Laravel의 HTTP 클라이언트는 Guzzle 을 래핑한 사용하기 쉬운 API입니다.
Http 파사드를 통해 외부의 웹 서비스나 API에의 HTTP 요청을 간결하게 기술할 수 있습니다.
use Illuminate\Support\Facades\ Http ;
$response = Http :: get ( 'https://api.example.com/users' );
Guzzle은 사전 설치되어 있기 때문에 추가 설정 없이 바로 사용하기 시작할 수 있습니다.
기본적인 요청
GET 요청
use Illuminate\Support\Facades\ Http ;
$response = Http :: get ( 'https://api.example.com/users' );
쿼리 파라미터는 배열로 전달할 수 있습니다.
$response = Http :: get ( 'https://api.example.com/users' , [
'page' => 1 ,
'per_page' => 20 ,
]);
POST 요청
데이터는 기본적으로 application/json으로 전송됩니다.
$response = Http :: post ( 'https://api.example.com/users' , [
'name' => '山田 太郎' ,
'email' => '[email protected] ' ,
]);
PUT / PATCH / DELETE
// PUT 요청
$response = Http :: put ( 'https://api.example.com/users/1' , [
'name' => '山田 花子' ,
]);
// PATCH 요청
$response = Http :: patch ( 'https://api.example.com/users/1' , [
'email' => '[email protected] ' ,
]);
// DELETE 요청
$response = Http :: delete ( 'https://api.example.com/users/1' );
응답의 처리
Http::get() 등의 메서드는 Illuminate\Http\Client\Response 인스턴스를 반환합니다.
이 객체에는 응답을 검사하기 위한 다수의 메서드가 준비되어 있습니다.
$response = Http :: get ( 'https://api.example.com/users/1' );
// 응답 바디
$response -> body (); // 문자열로 취득
$response -> json (); // 배열로 변환
$response -> json ( 'name' ); // JSON 의 특정 키를 취득
$response -> object (); // stdClass 객체로서 취득
$response -> collect (); // Collection 으로서 취득
// 스테이터스
$response -> status (); // 스테이터스 코드(예: 200)
$response -> successful (); // 200번대라면 true
$response -> failed (); // 400번대 이상이라면 true
$response -> clientError (); // 400번대라면 true
$response -> serverError (); // 500번대라면 true
// 자주 사용하는 스테이터스 코드의 판정
$response -> ok (); // 200
$response -> created (); // 201
$response -> noContent (); // 204
$response -> notFound (); // 404
$response -> unauthorized (); // 401
$response -> forbidden (); // 403
$response -> unprocessableEntity (); // 422
$response -> tooManyRequests (); // 429
JSON 응답은 배열 접근으로도 취득할 수 있습니다.
$name = Http :: get ( 'https://api.example.com/users/1' )[ 'name' ];
요청 옵션
헤더의 설정
$response = Http :: withHeaders ([
'X-Api-Version' => '2' ,
'Accept-Language' => 'ja' ,
]) -> get ( 'https://api.example.com/users' );
application/json을 받아들일 것을 나타내는 경우 acceptJson()이 편리합니다.
$response = Http :: acceptJson () -> get ( 'https://api.example.com/users' );
브라우저에서 Laravel로 보내는 AJAX 요청의 CSRF 헤더(X-CSRF-TOKEN / X-XSRF-TOKEN)에 대해서는 CSRF 보호 를 참조해 주십시오.
Bearer 토큰 인증 (가장 일반적):
$response = Http :: withToken ( $token ) -> get ( 'https://api.example.com/me' );
Basic 인증 :
$response = Http :: withBasicAuth ( '[email protected] ' , 'password' )
-> get ( 'https://api.example.com/private' );
베이스 URL 의 설정
같은 호스트로의 요청이 많은 경우 baseUrl()로 정리할 수 있습니다.
$response = Http :: baseUrl ( 'https://api.example.com' )
-> withToken ( $token )
-> get ( '/users/1' );
폼 데이터의 전송
application/x-www-form-urlencoded로 전송하고 싶은 경우 asForm()을 사용합니다.
$response = Http :: asForm () -> post ( 'https://api.example.com/login' , [
'username' => 'taro' ,
'password' => 'secret' ,
]);
타임아웃
// 응답 타임아웃(기본은 30초)
$response = Http :: timeout ( 10 ) -> get ( 'https://api.example.com/slow-endpoint' );
// 접속 타임아웃(기본은 10초)
$response = Http :: connectTimeout ( 5 ) -> get ( 'https://api.example.com/endpoint' );
타임아웃을 초과한 경우 Illuminate\Http\Client\ConnectionException이 스로우됩니다.
외부 API의 호출에는 반드시 타임아웃을 설정하는 것을 권장합니다.
리트라이
일시적인 네트워크 장애나 서버 에러에 대해 자동 리트라이를 설정할 수 있습니다.
// 최대 3회, 100밀리초 간격으로 리트라이
$response = Http :: retry ( 3 , 100 ) -> post ( 'https://api.example.com/orders' , $data );
조건부 리트라이(접속 에러일 때만 리트라이 등):
use Illuminate\Http\Client\ PendingRequest ;
use Throwable ;
use Illuminate\Http\Client\ ConnectionException ;
$response = Http :: retry ( 3 , 100 , function ( Throwable $exception , PendingRequest $request ) {
return $exception instanceof ConnectionException ;
}) -> post ( 'https://api.example.com/orders' , $data );
에러 핸들링
수동으로 에러를 확인
Laravel의 HTTP 클라이언트는 기본적으로는 400·500번대의 응답에 대해 예외를 스로우하지 않습니다.
failed()나 clientError() 등으로 명시적으로 확인합니다.
$response = Http :: get ( 'https://api.example.com/users/999' );
if ( $response -> notFound ()) {
// 404 의 처리
}
if ( $response -> failed ()) {
// 에러 로그를 기록하는 등
logger () -> error ( 'API request failed' , [ 'status' => $response -> status ()]);
}
예외를 스로우
throw()를 사용하면 에러 시에 Illuminate\Http\Client\RequestException을 스로우합니다.
// 에러 응답(4xx/5xx)이면 예외를 스로우
$response = Http :: post ( 'https://api.example.com/users' , $data ) -> throw ();
// throwIf(): 조건이 true 일 때 스로우(아래는 독립된 사용 예시)
$response = Http :: get ( 'https://api.example.com/users/1' );
$response -> throwIf ( $response -> status () === 422 );
// throwUnlessStatus(): 지정 스테이터스 코드 이외에서 스로우(아래도 독립된 사용 예시)
$response = Http :: post ( 'https://api.example.com/orders' , $data );
$response -> throwUnlessStatus ( 201 );
throw()는 응답 인스턴스를 반환하기 때문에 메서드 체인으로 쓸 수 있습니다.
$user = Http :: post ( 'https://api.example.com/users' , $data )
-> throw ()
-> json ();
예외를 캐치해 처리하는 경우:
use Illuminate\Http\Client\ RequestException ;
use Illuminate\Http\Client\ ConnectionException ;
try {
$response = Http :: timeout ( 5 )
-> post ( 'https://api.example.com/users' , $data )
-> throw ();
} catch ( ConnectionException $e ) {
// 타임아웃이나 접속 에러
logger () -> error ( 'Connection failed: ' . $e -> getMessage ());
} catch ( RequestException $e ) {
// 4xx / 5xx 에러
logger () -> error ( 'API error' , [ 'status' => $e -> response -> status ()]);
}
병행 요청
여러 API를 동시에 호출하고 싶은 경우, pool()로 병행 실행할 수 있습니다.
use Illuminate\Http\Client\ Pool ;
use Illuminate\Support\Facades\ Http ;
$responses = Http :: pool ( fn ( Pool $pool ) => [
$pool -> as ( 'users' ) -> get ( 'https://api.example.com/users' ),
$pool -> as ( 'posts' ) -> get ( 'https://api.example.com/posts' ),
$pool -> as ( 'comments' ) -> get ( 'https://api.example.com/comments' ),
]);
$users = $responses [ 'users' ] -> json ();
$posts = $responses [ 'posts' ] -> json ();
$comments = $responses [ 'comments' ] -> json ();
순서대로 실행하는 것보다 대폭 고속화할 수 있습니다. 외부 API를 여러 번 호출하는 대시보드 등에 유효합니다.
테스트
Http::fake() 에 의한 목
테스트에서는 Http::fake()를 사용해 실제 HTTP 요청을 보내지 않고 응답을 시뮬레이트합니다.
use Illuminate\Support\Facades\ Http ;
Http :: fake ();
// 모든 요청에 200을 반환
$response = Http :: get ( 'https://api.example.com/users' );
$response -> successful (); // true
특정 URL에 대해 응답을 설정하는 경우:
Http :: fake ([
'api.example.com/users/*' => Http :: response ([ 'id' => 1 , 'name' => '山田 太郎' ], 200 ),
'api.example.com/posts/*' => Http :: response ([ 'error' => 'Not Found' ], 404 ),
'*' => Http :: response ( 'OK' , 200 ), // 그 외의 모두
]);
응답의 시퀀스(여러 번 호출되었을 때 순서대로 응답을 반환):
Http :: fake ([
// 1회째: 200, 2회째: 200, 3회째: 429 를 반환
// 시퀀스가 소진되면 이후의 요청에서 예외가 스로우됨
'api.example.com/*' => Http :: sequence ()
-> push ([ 'id' => 1 ], 200 )
-> push ([ 'id' => 2 ], 200 )
-> pushStatus ( 429 ),
]);
요청의 검증
Http::assertSent()로 요청의 내용을 검증할 수 있습니다.
use Illuminate\Http\Client\ Request ;
use Illuminate\Support\Facades\ Http ;
Http :: fake ();
Http :: withToken ( 'my-token' ) -> post ( 'https://api.example.com/users' , [
'name' => '山田 太郎' ,
]);
Http :: assertSent ( function ( Request $request ) {
return $request -> url () === 'https://api.example.com/users'
&& $request -> hasHeader ( 'Authorization' , 'Bearer my-token' )
&& $request [ 'name' ] === '山田 太郎' ;
});
// 전송되지 않았음을 확인
Http :: assertNotSent ( function ( Request $request ) {
return $request -> url () === 'https://api.example.com/admin' ;
});
// 요청이 1건만 전송되었음을 확인
Http :: assertSentCount ( 1 );
테스트에서는 반드시 Http::fake()를 선두에서 호출합시다.
호출 잊음이 있으면 실제 외부 API로 요청이 날아가 버립니다.
Http::preventStrayRequests()를 사용하면 페이크하지 않은 URL로의 요청에서 예외를 스로우시킬 수 있습니다.
Stray 요청의 방지
Http :: preventStrayRequests ();
Http :: fake ([
'api.example.com/*' => Http :: response ([ 'ok' => true ]),
]);
// 페이크되지 않은 URL 로의 요청은 예외가 됨
Http :: get ( 'https://other.example.com/endpoint' ); // 예외 스로우
실전 예시: 외부 API 를 호출하는 서비스 클래스
실제 프로젝트에서는 HTTP 클라이언트의 로직을 서비스 클래스에 정리하는 것이 모범 사례입니다.
서비스 클래스 작성
<? php
namespace App\Services ;
use Illuminate\Http\Client\ RequestException ;
use Illuminate\Http\Client\ ConnectionException ;
use Illuminate\Support\Facades\ Http ;
class GitHubService
{
private string $baseUrl = 'https://api.github.com' ;
public function __construct (
private readonly string $token ,
) {}
/**
* 사용자 정보를 취득
*
* @throws ConnectionException
* @throws RequestException
*/
public function getUser ( string $username ) : array
{
return Http :: baseUrl ( $this -> baseUrl )
-> withToken ( $this -> token )
-> acceptJson ()
-> timeout ( 10 )
-> get ( "/users/{ $username }" )
-> throw ()
-> json ();
}
/**
* 리포지토리 목록을 취득
*
* @throws ConnectionException
* @throws RequestException
*/
public function getRepositories ( string $username , int $page = 1 ) : array
{
return Http :: baseUrl ( $this -> baseUrl )
-> withToken ( $this -> token )
-> acceptJson ()
-> timeout ( 10 )
-> retry ( 2 , 500 )
-> get ( "/users/{ $username }/repos" , [
'page' => $page ,
'per_page' => 30 ,
'sort' => 'updated' ,
])
-> throw ()
-> json ();
}
}
서비스 프로바이더에서 등록
// app/Providers/AppServiceProvider.php
use App\Services\ GitHubService ;
public function register () : void
{
$this -> app -> singleton ( GitHubService :: class , function () {
return new GitHubService (
token : config ( 'services.github.token' ),
);
});
}
// config/services.php
'github' => [
'token' => env ( 'GITHUB_TOKEN' ),
],
컨트롤러에서 이용
<? php
namespace App\Http\Controllers ;
use App\Services\ GitHubService ;
use Illuminate\Http\Client\ RequestException ;
use Illuminate\Http\Client\ ConnectionException ;
use Illuminate\Http\ JsonResponse ;
class GitHubController extends Controller
{
public function __construct (
private readonly GitHubService $github ,
) {}
public function show ( string $username ) : JsonResponse
{
try {
$user = $this -> github -> getUser ( $username );
return response () -> json ( $user );
} catch ( ConnectionException ) {
return response () -> json ([ 'error' => 'GitHub API 에 접속할 수 없었습니다.' ], 503 );
} catch ( RequestException $e ) {
$status = $e -> response -> status ();
if ( $status === 404 ) {
return response () -> json ([ 'error' => '사용자를 찾을 수 없습니다.' ], 404 );
}
return response () -> json ([ 'error' => 'GitHub API 에서 에러가 발생했습니다.' ], 502 );
}
}
}
테스트 작성
<? php
namespace Tests\Unit\Services ;
use App\Services\ GitHubService ;
use Illuminate\Http\Client\ ConnectionException ;
use Illuminate\Http\Client\ RequestException ;
use Illuminate\Support\Facades\ Http ;
use Tests\ TestCase ;
class GitHubServiceTest extends TestCase
{
private GitHubService $service ;
protected function setUp () : void
{
parent :: setUp ();
Http :: fake ([
'api.github.com/users/octocat' => Http :: response ([
'login' => 'octocat' ,
'name' => 'The Octocat' ,
'public_repos' => 8 ,
], 200 ),
'api.github.com/users/notfound' => Http :: response (
[ 'message' => 'Not Found' ],
404
),
]);
$this -> service = new GitHubService ( token : 'test-token' );
}
public function test_사용자_정보를_취득할_수_있다 () : void
{
$user = $this -> service -> getUser ( 'octocat' );
$this -> assertEquals ( 'octocat' , $user [ 'login' ]);
$this -> assertEquals ( 'The Octocat' , $user [ 'name' ]);
Http :: assertSent ( function ( $request ) {
return $request -> url () === 'https://api.github.com/users/octocat'
&& $request -> hasHeader ( 'Authorization' , 'Bearer test-token' );
});
}
public function test_존재하지_않는_사용자는_예외가_스로우된다 () : void
{
$this -> expectException ( RequestException :: class );
$this -> service -> getUser ( 'notfound' );
}
}
메서드 설명 successful()200번대 failed()400번대 이상 clientError()400번대 serverError()500번대 ok()200 created()201 notFound()404 unauthorized()401 forbidden()403 unprocessableEntity()422 tooManyRequests()429
HTTP 클라이언트의 로직은 서비스 클래스에 정리
반드시 타임아웃을 설정(timeout()과 connectTimeout())
일시적인 장애에 대해 리트라이를 설정(retry())
테스트에서는 Http::fake()를 반드시 사용해 외부 API를 실제로 두드리지 않음
Http::preventStrayRequests()를 테스트의 셋업에 추가하면 안심
API 의 토큰이나 인증 정보는 환경 변수와 config/services.php 로 관리