라이프 사이클 - 요청, 응답, 미들웨어

hyHA·2023년 11월 29일
0
post-custom-banner

라라벨 애플리케이션으로 들어오는 모든 요청은

일루미네이트 요청 객체로 변환되고,
요청 객체는 모든 미들웨어를 거친 후,
애플리케이션의 주요 로직에 전달되어 처리되고,
컨트롤러 등에서 응답 객체를 생성 후,
응답 객체는 모든 미들웨어를 거꾸로 거슬러 올라가 통과한 후 최종 사용자에게 반환된다.

요청응답 객체는,
애플리케이션으로 유입되는 사용자의 요청과 서버에서 나가는 응답과 관련된 모든 정보를 캡슐화하고 표현하는 역할을 한다.

서비스 프로바이더는,
애플리케이션에서 사용할 클래스를 바인딩하고 등록하기 위한 관련 동작을 한데 모은다.

미들웨어는,
애플리케이션의 주요 로직을 둘러싸고 요청 객체와 응답 객체를 조회하고 변경하거나 필요한 경우 처리를 거부하는 응답을 반환할 수 있다.

애플리케이션 부트스트랩하기

부트스트랩이란, 컴퓨터의 전원을 켜면 부팅이 되듯이, 애플리케이션이 동작하기 위해 부팅되는 것을 부트스트랩이라고 한다. 요청을 처리하기 전에 필요한 작업을 준비하는 과정이라고 생각하면 된다.

일반적으로는 서비스 컨테이너에 바인딩을 등록하는 것을 포함해서 이벤트 리스너, 미들웨어 그리고 라우트등을 등록 하는 것을 의미한다 (서비스 프로바이더는 애플리케이션 구성의 핵심이다)

index.php 파일은 웹 서버에서 클라이언트의 HTTP 요청을 받는 진입점이다.
클라이언트의 요청이 들어오면, 웹 서버는 이 index.php 파일을 실행하여 Laravel 애플리케이션의 실행을 시작한다.

참고로, 라라벨이 작동할 때 첫 번째 동작은 서비스 컨테이너(App) 인스턴스를 생성하는 것이다.

index.php

index.php 파일에는 아래 2가지 파일을 포함한다.

index.php>>

# /vendor/autoload.php
require __DIR__.'/vendor/autoload.php'; // 모든 의존성 등록

# /bootstrap/app.php
$app = require_once __DIR__.'/bootstrap/app.php'; // 애플리케이션 초기화
  1. compser가 관리하는 모든 의존성을 등록한 뒤,
  2. 라라벨 애플리케이션의 부트스트랩 코드가 실행되어 환경을 설정한다.

이후 요청에 대한 라우팅 및 컨트롤러 처리 등이 진행될 것이다.

먼저 index.php에서 로드하는 두 가지 파일의 기능을 살펴보자

/vendor/autoload.php

composer는 vender/autoload.php 파일을 생성한다.
이 파일은 composer가 설치한 패키지(클래스, 라이브러리, 프레임워크 등)들을 자동으로 로드하기 위한 오토로더 파일이다.

로드한다는 것은 해당 패키지들이 실행된다는 것을 의미한다.
예를 들면, autoload.php 파일에는 composer가 생성한 클래스 로더가 포함되어있다. 이 클래스 로더는 필요한 클래스를 동적으로 로드하여 코드를 실행할 때 해당 클래스가 필요한 경우에만 메모리에 로드한다.

즉, autoload.php 파일은 composer가 관리하는 모든 의존성을 등록하고 클래스가 호출될 때 동적으로 메모리에 자동으로 로드하기 위한 오토롤더 파일이다.

  • Composer

Composer : PHP 패키지를 관리하기 위한 도구
패키지 : 코드, 라이브러리, 프레임워크 등을 포함

  • composer.json 파일

Composer는 composer.json 파일을 사용하여 프로젝트의 의존성을 정의하고, 이를 기반으로 패키지를 설치한다. composer.json 파일은 프로젝트 루트 디렉토리에 위치하며, 여기에 패키지 정보와 버전 제약 조건 등이 기록된다

/bootstrap/app.php

Laravel 애플리케이션의 부트스트랩 프로세스를 시작하는 파일이다.

/bootstrap/app.php파일은 아래처럼 구성되어 있다.

  1. $app(Laravel 애플리케이션의 인스턴스, 서비스 컨테이너)을 생성
  2. 서비스 컨테이너($app)를 통해 클래스를 싱글톤으로 등록.

라라벨은 서비스 프로바이더서비스 컨테이너를 사용해 클래스 의존성을 관리하며, 이를 통해 애플리케이션을 구성하고 확장한다.

// 애플리케이션의 인스턴스를 생성
$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

// HTTP 커널의 인스턴스가 필요한 경우, 아래 클래스의 인스턴스를 제공
//싱글톤 메서드를 통해 클래스와 그 인터페이스를 서비스 컨테이너에 등록
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

return $app;

컴포저와 라라벨

라라벨의 핵심기능은 일루미네이트 네임스페이스 하위의 여러 컴포넌트로 구분되어있고, 컴포저를 이용해 각 라라벨 애플리케이션에 설치된다. 라라벨은 일루미네이트 패키지 외에 몇몇 심포니의 패키지와 다수의 외부 패키지도 설치한다. 즉, 라라벨이라는 프레임워크는 잘 조율된 컴포넌트들의 모음이다.

Kernel.php

커널은 라라벨 애프리케이션의 가장 핵심적인 로직이 동작하는 곳이다.
커널은 요청을 처리하기 전에 필요한 작업을 준비하는 부트스트랩 과정을 거친다.

  1. 커널에서는 사용자의 요청을미들웨어에 전달하고 예외가 발생하면 알맞은 응답을 처리한다.
    • 미들웨어는 요청을 가로채서 중간에서 작업을 수행하고,
  2. 그 뒤에 라우터로 전달해 등록된 라우트와 연결된 로직을 처리한다.
    • 이후$next($request)를 호출하여 다음 단계로 요청을 전달한다.

그러고 나서 클로저컨트롤러에서 반환되는 최종 응답을 클라이언트에 전달한다.

정리하면, 커널은 index.php가 최종 사용자에게 반환할 일루미네이트 응답 객체를 클라이언트에 반환하고 페이지 요청을 종료한다.

일반적으로
Http 커널은 라우터를 사용해 web 요청을 다루고,
콘솔 커널은 나머지 크론과 아티즌 같은 명령줄(커맨드) 요청을 다룬다.

커널은 handle() 메서드를 가진다.
handle() 메서드는 요청 객체를 받고 응답 객체를 반환한다.

서비스 프로바이더

config/app.php 파일에 정의된 서비스 프로바이더들이 등록된다.
등록된 서비스 프로바이더들은 애플리케이션에서 사용되는 서비스들을 초기화한다.
커널에서 진행되는 부트스트랩 코드는 몇몇을 제외하고는, 대부분 서비스 프로바이더라는 것으로 나뉘어 있다. 서비스 프로바이더는 애플리케이션의 핵심 기능을 다양한 부분으로 구분하여 부트스트랩하는데 필요한 로직을 캡슐화한 클래스다.

ex)
AuthServiceProvider : 라라벨 인증 시스템이 필요한 부트스트래핑 작업을 처리
RouteServiceProvider : 라우팅 시스템에 필요한 부트스트래핑 작업을 담당

애플리케이션이 구동할때 준비될 필요가 있는 여러 컴포넌트가 각각의 부트스트랩 코드를 나눠서 가지고 있다고 생각하자. 서비스 프로바이더는 부트스트랩 코드를 서로 연관된 단위로 모으는 도구다. 애플리케이션 코드를 잘 작동시키기 위해 사전에 실행하는 코드가 있다면 서비스 프로바이더에 넣는 것이 적합하다.

예를 들어 개발자가 작성하는 기능이 컨테이너에 등록된 어떤 클래스를 필요로 하는 걸 알게 되면 해당 기능만을 위한 서비스 프로바이더를 만들게 된다. (GithubServiceProvider나 MailServiceProvider와 같은 클래스를 만들 수 있다)

boot(), register(), 서비스 프로바이더 등록 지연하기

서비스 프로바이더는 두 가지 주요 메서드를 갖는다.
boot()와 register()이다. 그리고 DeferrableProvider 인터페이스, $defer 속성 또한 사용할 수 있다. 각각의 용도를 알아보자

  • register()

register() 메서드는 서비스 프로바이더가 부트스트래핑되는 과정에서 제일 먼저 호출된다. 이 메서드는 프로바이더가 제공하고자 하는 클래스의 인스턴스 생성 방법을 컨테이너에 등록하는 역할을 한다. 이를 '바인딩을 등록한다'고 한다. 이때 대상 클래스 인스턴스는 클래스 자체의 이름 또는 별칭(alias)으로 호출하도록 키워드가 등록된다. register()에서는 클래스의 인스턴스 생성 방법이 등록되므로 여기에서는 부트스트래핑이 완료된 클래스 인스턴스를 가지고 무엇인가 작업을 처리하는 로직을 추가하지 않는 것이 좋다.

  • boot()

boot() 메서드는 모든 서비스 프로바이더가 컨테이너에 바인딩 등록을 마친 뒤에 호출되는 메서드다. 여기에서는 이벤트 리스너를 바인딩하거나 라우트를 정의하는 등 부트스트래핑이 완료된 애플리케이션에서 추가적으로 필요한 작업들을 boot() 메서드에 담아서 처리할 수 있다.

만약 서비스 프로바이더가 컨테이너에 바인딩을 등록하기만 하고 (컨테이너에게 주어진 클래스나 인터페이스를 어떻게 인스턴스로 만드는지 알려주는) 부트스트래핑 과정을 수행하지 않는다면, 즉 register() 메서드는 존재하지만, boot() 메서드는 필요하지 않는 경우라면 서비스 프로바이더의 컨테이너 바인딩 등록 자체를 지연시킬 수 있다. 즉 register() 메서드 호출 자체를 나중에 할 수 있게 한다. 이렇게하면 애플리케이션의 부트스트래핑 시간을 줄일 수 있다.

  • 서비스 프로바이더 등록 지연하기

추가 공부 예정

요청 객체

지금까지 애플리케이션 부트스트랩 과정을 알아보았으니 이제 애플리케이션이 부트스트랩된 이후에 생성되는 Request 객체에 대해 알아보자.

라라벨에서 사용자의 요청을 나타내는 Illuminate\Http\Request 클래스는 심포니의 HttpFoundation\Request 클래스를 상속받아 라라벨용으로 만든것이다.

[심포니 HttpFoundation]

현존하는 대부분의 PHP 프레임워크에서 널리 활용된다. HTTP 요청, 응답, 헤더, 쿠키 등을 표현하기 위해 PHP에서 가장 널리 사용되는 추상화 도구다.

요청객체는 사용자의 HTTP 요청을 아주 작은 정보까지 세부적으로 모두 담을 수 있게 만들어진 객체다. 이 요청 객체를 사용하지 않고 순수하게 PHP로만 코드를 작성하는 경우에 로직에서 현재 사용자 요청에 관한 정보를 얻기 위해서는 $_SERVER, $_GET, $_POST 변수와 같은 글로벌 변수를 처리해야 하고, 관련된 추가 정보를 얻기 위해 복잡한 내장함수를 직접 호출해야 한다. 사용자가 업로드한 파일은 어디에 있는지, 접속자의 IP 주소는 무엇인지, 어떤 항목을 전송했는지, 이 모든게 분리되어 있어 이해하기 어렵고 테스트하기 어려운 방식으로 여기저기 흩어져있다.

심포니 HttpFoundation의 요청객체는 하나의 HTTP 요청에 관련된 모든 정보를 모아 하나의 객체로 표현하고, 이 객체에서 유용한 정보를 손쉽게 조회하는 편리한 메서드를 제공한다.

라라벨에서 사용하는 일루미네이트 요청 객체는 심포니 요청 객체에 더해 라라벨 애플리케이션에서 사용하기 편리한 메서드를 추가로 제공한다.

라라벨에서 요청 객체 얻기

라라벨은 애플리케이션에서 요청이 있을 때마다 내부적으로 요청 객체를 생성한다. 요청 객체에 접근하려면 3가지 방법을 사용할 수 있다.

  1. 컨테이너가 처리하는 생성자나 메서드에 요청클래스를 타입힌트하기
    ex) 컨트롤러의 메서드에 요청 클래스를 타입힌트하면 라라벨 컨테이너가 자동으로 요청 객체의 인스턴스를 주입해준다.
class PersonController extends Controller{
	public function index(Request $request){
    	$allInput = $request->all();
    }
}
  1. request() 글로벌 헬퍼 사용하기
    • request() 글로벌 헬퍼로 $request의 인스턴스를 얻은 다음 바로 객체의 메서드를 호출할 수도 있다.
$request = request();
$allInput = $request->all();
혹은
$allInput = request()->all();
  1. 글로벌 메서드 app() 사용하기
    • 정규화된 클래스명과 단축 키워드 두 가지 모두 사용할 수 있다.
$request = app(Illuminate\Http\Request::class);
$request = app('request');

요청에 대한 기본 정보 얻기

이제 요청 객체를 얻는 방법을 알게 됐으니 요청 객체로 무엇을 할 수 있는지 알아보자. 요청 객체의 주된 목적은 현재 사용자의 요청을 객체로 표현하는 것이며, 주요 기능은 현재 사용자의 요청에 대한 자세한 정보를 쉽게 조회하는 것이다.

  • 요청 객체에서 사용할 수 있는 메서드

추가 공부 예정

응답 객체

요청 객체와 비슷하게 Illuminate 응답 객체는 애플리케이션이 최종 사용자에게 전달하는 응답을 객체로 표현한 것이다. 응답 객체는 헤더, 쿠키, 콘텐츠, 그 외 최종 사용자의 브라우저가 페이지를 만드는 데 사용하는 모든 정보를 포함한다.

요청 객체와 마찬가지로 Illuminate\Http\Response 클래스도 심포니 클래스를 상속받는다.

[심포니 HttpFoundation]

현존하는 대부분의 PHP 프레임워크에서 널리 활용된다. HTTP 요청, 응답, 헤더, 쿠키 등을 표현하기 위해 PHP에서 가장 널리 사용되는 추상화 도구다.

응답 심포니 클래스(Symfony\Component\HttpFoundation\Response)는 사용자에게 전달할 응답을 생성하고 필요한 작업들을 처리할 수 있는 속성값과 메서드를 가진 기반 클래스가 된다.

일루미네이트의 응답 클래스(Illuminate\Http\Response)는 이 클래스를 기반으로 라라벨에서 편의 기능 일부를 추가한 클래스다.

컨트롤러에서 응답 객체를 만들고 반환하기

응답 객체를 다루는 방법을 알아보기 전에 기본적인 응답 객체를 어떻게 사용하는지 보자

라우트에서 반환되는 모든 응답 객체는 결국 HTTP 응답으로 변환된다. 특정 헤더를 추가하고 콘텐츠의 내용을 정의하거나 쿠키를 설정하는 일 등 그 무엇이든 사용자의 브라우저가 해석할 수 있는 응답 그 자체로 변환된다.

  1. 가장 간단한 HTTP 응답
Route::get('route', function() {
	return new Illuminate\Http\Response('Hello!');
});

//글로벌 함수를 이용한 경우
Route::get('route',function() {
	return response('Hello!');
})
  1. HTTP 상태코드와 헤더를 변경한 간단한 HTTP 응답
Route::get('route', function(){
	return response('Error!', 400)
    	-> header('X-Header-Name', 'header-value') //헤더 설정
        ->cookie('cookie-naem', 'cookie-value'); //쿠키 추가
})
  • 헤더 설정 방법
  • 쿠키 추가 방법
    추가 공부 예정

맞춤형 응답 타입

뷰, 다운로드, 파일, JSON 응답을 처리하는 경우에 특화된 맞춤형 응답 타입이 있다. 각 타입은 헤더나 콘텐츠 구조를 위한 특정 템플릿을 쉽게 재사용할 수 있도록 미리 만들어둔 매크로라고 생각하면 이해하기 쉽다.

  • 뷰 응답
  • 다운로드 응답
  • 파일 응답
  • JSON 응답
  • 리다이렉트 응답
  • 커스텀 응답 매크로
  • 레스폰서블 인터페이스
    추가 공부 예정

미들웨어

미들웨어는 소프트웨어를 구성할 때 사용되는 아키텍처 패턴의 일종으로 다른 많은 프레임워크에서 이 패턴을 구현하고 있다. 이 절에서 라라벨이 사용하는 미들웨어가 무엇인지 알아보자.

미들웨어 소개

미들웨어라는 개념은 애플리케이션이 마치 여러 층을 가진 양파처럼 일련의 계층으로 감싸져 있다는 것을 의미한다. 모든 요청은 애플리케이션으로 가는 과정에서 모든 미들웨어 계층을 통과한다.

미들웨어는 대부분 애플리케이션 핵심 로직과는 구분되며, 대개 이론적으로 현재 작업중인 애플리케이션뿐만 아니라 어떤 애플리케이션에도 적용할 수 있는 방식으로 구성된다.

미들웨어는 자신이 확인할 수 있는 정보로 요청을 조회해서 추가적인 작업을 처리하거나 거부할 수 있다.

ex) 미들웨어가 시간당 접속 제한 같은 기능을 구현하기에 알맞다는 것을 의미. IP 주소를 검사하고 지난 1분간 특정 URL에 얼마나 자주 접근했는지 확인한 후, 기준을 초과하면 429 상태 응답(너무 많은 요청 HTTP 상태코드)을 되돌려줄 수 있다.

미들웨어는 애플리케이션에서 내보내는 응답에도 접근할 수 있으므로, 응답에 추가적인 작업을 처리하기에도 좋다.

ex) 최종 사용자에게 응답을 보내기 직전에 미들웨어를 이용해서 해당 요청/응답 사이클에서 확인된 모든 쿠키를 응답 헤더에 추가한다

미들웨어의 가장 강력한 기능은 미들웨어가 요청/응답 사이클의 거의 처음과 마지막에 필요한 작업을 처리할 수 있다는 점이다. 이는 세션의 활성화 같은 작업을 처리하기에 딱 알맞은 경우다. PHP에서는 요청/응답 사이클의처음 부분에서 세션을 시작하고, 마지막 부분에서 세션을 닫아야 하는데 미들웨어가 이런 작업을 처리하기에 최적의 장소다.

커스텀 미들웨어 만들기

예를 들어 DLETE HTTP 메서드를 사용하는 모든 요청을 거부하고, 쿠키를 되돌려주는 미들웨어가 필요하다고 가정해보자. 이런 경우에는 커스텀 미들웨어를 생성할 수 있다.

//아티즌 명령어로 커스텀 미들웨어 생성
php artisan make:middleware BanDeleteMethod

위 명령어를 실행하면 아래와 같이 app/Http/Middleware/BanDeleteMethod.php 파일이 생성된다.

class BanDeleteMethod{
	public function handle($request, Closure $next){
    	return $next($request);
    }
}

handle() 메서드가 유입되는 요청과 반환되는 응답을 어떻게 처리하는지가 미들웨어를 이해하는 데 가장 어려운 부분이다.

미들웨어의 handle()메서드

먼저 미들웨어는 다른 미들웨어 위에 하나씩 겹쳐지는 계층 구조를 갖고, 최종적으로 애플리케이션의 주요 비즈니스 로직을 둘러싸고 있다는 것을 기억하자.

요청이 들어오면 첫번째 미들웨어에서 처리된 후 다른 미들웨어로 차례로 넘긴 다음에 주요 비즈니스 로직에 전달된다. 그 후 결과로 반환되는 응답은 요청이 처리될 때의 미들웨어의 반대 방향으로 통과하여 전달되고, 최종적으로 첫번째 미들웨어가 마지막으로 응답을 처리한다.

  • $next()

요청을 $next()로 넘긴다는 것은 요청을 남아있는 다른 미들웨어로 건네는 것을 의미한다. $next() 클로저는 $request를 다음 미들웨어의 handle() 메서드에 넘겨준다. 더는 넘겨줄 미들웨어가 없을 때까지 반복하고 주요 비즈니스 로직에 다다르면 종료된다.

컨트롤러에서 반환된 응답은 미들웨어를 거꾸로 타고 올라간다. 각각의 미들웨어가 결국 응답을 반환하기 때문이다. 그래서 같은 handle()메서드 내에서 미들웨어가 $request를 기반으로 작업을 처리하고 $next() 클로저를 전달할 수도 있고, 최종 사용자에게 반환되기 전에 전달받은 출력으로 무엇인가 추가적인 작업을 할 수도 있는 것이다.

미들웨어 등록

작성한 미들웨어 클래스는 등록을 해야 사용할 수 있다. 작성한 미들웨어를 전체에 적용되는 글로벌 미들웨어로 등록하거나 특정 라우트에만 적용되는 라우트 미들웨어로 등록해야 한다.

  • 글로벌 미들웨어 등록

미들웨어 등록은 모두 app/Http/Kernel.php에서 이뤄진다. 미들웨어를 글로벌로 등록하려면 $middleware 속성에 클래스명을 추가한다.

app/Http/Kernel.php>>

protected $middleware = [
	\App\Http\Middleware\TrustProxies::class,
    \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
    \App\Http\Middleware\BanDeleteMethod::class, //BanDeleteMethod 미들웨어 등록
];
  • 라우트 미들웨어 등록

특정 라우트에 대한 미들웨어는 라우트 미들웨어로 추가되거나 미들웨어 그룹의 일부로 추가될 수 있다.
라우트 미들웨어로 등록하려면 app/Http/Kernel.php에 있는 $routeMiddleware 배열에 추가한다.
라우트 미들웨어를 등록할 때는 사용할 키를 추가로 지정한다.

app/Http/Kernel.php>>

protected $routeMiddleware = [
	'auth' => \App\Http\Middleware\Authenticate::class,
    ...
    'ban-delete' => \App\Http|Middleware\BanDeleteMethod::class, //라우트 미들웨어를 등록할 때는'키'를 지정한다
];
  • 미들웨어를 라우트 정의에 사용하기
  • 미들웨어 그룹 사용하기
    추가 공부 예정

미들웨어에 파라미터 넘기기

추가 공부 예정

신뢰할 수 있는 프록시

프로젝트가 AWS 클라우드에서 애플리케이션 로드 밸런서(ALB)와 같이 리버스 프록시 역할을 하는 로드 밸런서 뒤에서 작동하도록 서비스를 구성했다고 생각해보자.

사용자는 HTTPS 프로토콜로 접속을 했는데, URL 헬퍼 함수를 사용해서 생성한 링크가 HTTP 기반으로 생성되는 일이 발생하거나 시간당 접속 제한 미들웨어를 활성화했을 때 모든 트래픽이 제한되는 경우가 발생하는 일이 생길 수 있다. 이런 문제들의 원인은 리버스 프록시로 동작하는 로드 밸런서에서 전달되는 X_FORWARDED_PROTO, X_FORWARDED_PORT, X_FORWARDED_FOR를 라라벨 애플리케이션에서 처리하지 못하기 때문이다.

따라서 애플리케이션의 앞에서 작동하는 프록시를 신뢰할 수 있다고 설정하는 작업이 필요하다. 라라벨에서는 이런 작업을 수행하는 신뢰할 수 있는 프록시(TrustedProxy)기능이 내장되어있다.

App\Http\Middleware\TrustProxies 미들웨어의 $proxies 배열에 신뢰할 수 있는 로드 밸런서나, 프록시의 IP를 추가하고 모든 X_FORWARDED 헤더를 적용할 것인지 아니면 일부만 적용할지 결정할 수 있다.

참고
처음부터 제대로 배우는 라라벨
https://dev.to/patelparixit07/laravel-request-lifecycle-195e
https://www.google.com/imgres?imgurl=https%3A%2F%2Fmedia.licdn.com%2Fdms%2Fimage%2FC4D12AQFh90A3LbtBcw%2Farticle-cover_image-shrink_600_2000%2F0%2F1647521574476%3Fe%3D2147483647%26v%3Dbeta%26t%3Dd5QHj3kri3nBTQ1O4rTUC8S45rnlKYYmPz5nsGl-xGc&tbnid=hb8AB_MUASB2jM&vet=12ahUKEwiQ89fhpemCAxX6dvUHHTOWCjMQMygDegQIARBS..i&imgrefurl=https%3A%2F%2Fwww.linkedin.com%2Fpulse%2Flaravel-request-lifecycle-muhammad-raees-riaz&docid=pVqU6b9Cgs0NPM&w=1137&h=437&q=%EB%9D%BC%EB%9D%BC%EB%B2%A8%20%EC%9A%94%EC%B2%AD%20%EB%9D%BC%EC%9D%B4%ED%94%84%EC%82%AC%EC%9D%B4%ED%81%B4&ved=2ahUKEwiQ89fhpemCAxX6dvUHHTOWCjMQMygDegQIARBS
https://viblo.asia/p/tap-5-vong-doi-request-laravel-request-lifecycle-laravel-LzD5dwqOljY
https://www.slideshare.net/ssuser800974/ss-55140258

profile
룰루랄라
post-custom-banner

0개의 댓글