이전 글에서 HTTP 서비스 시작 시 발생하는 과정을 다루어 Hyperf 프레임워크와 HTTP 서비스 초기화 과정에 대해 더 깊이 이해할 수 있게 되었습니다. 오늘은 HTTP 서비스에 접속할 때 요청을 어떻게 처리하고 응답 콘텐츠를 반환하는지에 대해 계속해서 알아보겠습니다.
HTTP 서비스가 시작될 때, Hyperf는 Swoole에 요청 이벤트 처리 함수를 등록합니다. HTTP 요청을 받으면 Swoole이 해당 함수를 호출합니다.
HTTP 서비스가 시작된 후, HTTP 서비스로 요청을 보내면 HTTP 요청이 Swoole로 전송됩니다. Swoole은 HTTP 메시지를 HTTP 요청 객체로 파싱하고 HTTP 응답 객체를 생성합니다.
그런 다음 Swoole은 요청 이벤트 처리 함수를 호출하여 요청과 응답 객체를 매개변수로 전달합니다. 이 함수 내에서 요청 처리가 완료되면 응답 객체를 호출하여 응답 콘텐츠를 전송합니다.
이 글에서는 Hyperf에서 HTTP 서비스가 요청을 처리하는 과정을 세 부분으로 나누어 설명하겠습니다. 첫 번째 부분에서는 HTTP 서비스의 요청 이벤트 처리 함수를 등록하는 방법을, 두 번째 부분에서는 HTTP 요청을 처리하고 응답 콘텐츠를 전송하는 방법을 다루겠습니다.
요청 이벤트 처리 함수 등록
HTTP 서비스가 시작될 때, Hyperf는 Swoole에 요청 이벤트 처리 함수를 등록해야 합니다. 그렇다면 Hyperf에서 이 처리 함수는 무엇일까요?
HTTP 서비스 '서비스 구성 읽기' 단계에서 config/autoload/server.php 구성 파일에서 구성 정보를 가져오는데, 여기서 Event::ON_REQUEST는 요청 이벤트의 열거 값이며, Hyperf\HttpServer\Server::onRequest()가 요청 이벤트의 처리 함수입니다.
return [
...
'servers' => [
[
...
'callbacks' => [
Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'],
],
],
],
...
]
'HTTP 서비스 초기화' 단계에서 요청 이벤트 처리 함수를 Swoole에 등록하며, 이 작업의 주요 내용은 다음과 같습니다:
foreach ($events as $event => $callback) {
...
[$className, $method] = $callback;
$class = $this->container->get($className);
if (method_exists($class, 'setServerName')) {
// 서비스 이름 설정
$class->setServerName($serverName);
} // 핵심 미들웨어 초기화
if ($class instanceof MiddlewareInitializerInterface) {
$class->initCoreMiddleware($serverName);
}
...
// 이벤트 처리 함수 등록
$server->on($event, $callback);
}
initCoreMiddleware() 메서드에서 HTTP 서비스의 라우팅 정보를 초기화하고, 서비스 이름을 통해 해당 HTTP 서비스의 미들웨어 및 예외 처리기를 가져옵니다.
public function initCoreMiddleware(string $serverName): void {
$this->serverName = $serverName;
$this->coreMiddleware = $this->createCoreMiddleware();
$config = $this->container->get(ConfigInterface::class);
$this->middlewares = $config->get('middlewares.' . $serverName, []);
$this->exceptionHandlers = $config->get('exceptions.handler.' . $serverName, $this->getDefaultExceptionHandler());
}
HTTP 요청 처리
위 내용을 통해 HTTP 요청 처리 로직이 Hyperf\HttpServer\Server::onRequest() 메서드에서 완료된다는 것을 알 수 있습니다. 이 메서드의 원형은 다음과 같습니다:
public function onRequest($request, $response): void
Hyperf\HttpServer\Server::onRequest() 메서드의 실행 작업을 다음과 같은 단계로 나누었습니다.
요청 및 응답 객체 초기화
이 단계에서는 PSR-7 요청 및 응답 객체를 초기화해야 합니다. 이는 Hyperf의 표준 구성 요소가 PSR 표준을 기반으로 구현되어 있지만, 하위 프레임워크는 PSR 표준을 기반으로 구현되지 않았을 수 있으므로 먼저 호환성 적용이 필요하기 때문입니다.
[$psr7Request, $psr7Response] = $this->initRequestAndResponse($request, $response);
initRequestAndResponse() 메서드에서 객체가 PSR 표준을 기반으로 하는지 확인한 후, 그렇지 않으면 PSR-7 요청 및 응답 객체로 변환합니다.
요청 라우팅 정보 매칭
이 단계에서는 Hyperf\HttpServer\CoreMiddleware::dispatch() 메서드를 호출하여 위에서 초기화한 PSR-7 요청 객체로 라우팅 정보를 매칭합니다.
$psr7Request = $this->coreMiddleware->dispatch($psr7Request);
dispatch() 메서드에서는 요청 객체의 요청 방식과 요청 주소로 HTTP 서비스의 라우팅 정보를 매칭하고, 매칭 결과를 Hyperf\HttpServer\Router\Dispatched 객체로 변환하여 새 요청 객체의 속성에 저장하고 반환합니다.
public function dispatch(ServerRequestInterface $request): ServerRequestInterface
{
$routes = $this->dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());
$dispatched = new Dispatched($routes);
return Context::set(ServerRequestInterface::class, $request->withAttribute(Dispatched::class, $dispatched));
}
여기서 '왜 Dispatched를 원래 요청 객체의 속성에 직접 저장하지 않는가?'라는 의문이 생길 수 있습니다.
이는 PSR-7 표준에서 규정한 사항입니다. PSR-7 표준에서 요청은 변경 불가능(immutable)하다고 간주됩니다. 모든 상태 변경이 가능한 메서드를 구현하여 현재 요청의 내부 상태를 유지하고, 변경된 상태를 포함하는 인스턴스를 반환해야 합니다.
즉, 요청 객체의 정보를 수정하려면 현재 객체에서 복제하여 새 객체를 만들고, 새 객체에서 수정 후 새 객체를 반환해야 합니다. 아래는 withAttribute 메서드의 구현입니다:
public function withAttribute($name, $value)
{
$clone = clone $this;
$clone->attributes[$name] = $value;
return $clone;
}
이 점을 알게 되면 앞으로 '속성을 설정했는데 값을 가져올 수 없다'는 같은 혼란이 없을 것입니다.
전역 및 라우트 미들웨어 준비
HTTP 서비스에서 미들웨어는 적용 범위에 따라 두 가지로 나뉩니다: 전역 미들웨어와 라우트 미들웨어입니다.
전역 미들웨어는 모든 라우트에 적용되며, 라우트 미들웨어는 일부 라우트에만 적용됩니다. 라우트 미들웨어가 적용되는지 여부는 라우트 매칭 결과에 따라 결정됩니다. 라우트가 매칭되면 미들웨어 관리자에서 해당 라우트의 미들웨어를 가져옵니다. 그렇지 않으면 전역 미들웨어만 사용합니다.
$dispatched = $psr7Request->getAttribute(Dispatched::class);
// 전역 미들웨어 가져오기
$middlewares = $this->middlewares;
// 라우트 매칭 여부 확인
if ($dispatched->isFound()) {
// 서비스 이름, 요청 주소, 요청 방식으로 라우트 미들웨어 가져오기
$registeredMiddlewares = MiddlewareManager::get($this->serverName, $dispatched->handler->route, $psr7Request->getMethod());
$middlewares = array_merge($middlewares, $registeredMiddlewares);
}
모든 준비가 완료되면 Hyperf는 Hyperf\Dispatcher\HttpDispatcher::dispatch() 메서드를 호출하여 요청 객체를 각 미들웨어에 순서대로 처리한 후, 핵심 미들웨어의 Hyperf\HttpServer\CoreMiddleware::process() 메서드를 호출하여 최종 처리를 진행합니다.
$psr7Response = $this->dispatcher->dispatch($psr7Request, $middlewares, $this->coreMiddleware);
라우트 처리 함수 실행
Hyperf\HttpServer\CoreMiddleware::process() 메서드에서는 주로 Dispatcher 객체의 상태에 따라 해당 작업을 실행합니다.
- 라우트가 없으면
NotFoundHttpException예외를 발생시킵니다. - 요청 방식이 올바르지 않으면
MethodNotAllowedHttpException예외를 발생시킵니다. - 라우트를 찾으면 해당 라우트에 바인딩된 처리 함수를 해석하고 실행한 후 응답 객체를 반환합니다.
아래 코드는 이 부분의 실행 로직을 보여줍니다.
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
...
/** @var Dispatched $dispatched */
$dispatched = $request->getAttribute(Dispatched::class);
// `Dispatcher` 객체의 상태에 따라 작업 실행
$response = match ($dispatched->status) {
Dispatcher::NOT_FOUND => $this->handleNotFound($request),
Dispatcher::METHOD_NOT_ALLOWED => $this->handleMethodNotAllowed($dispatched->params, $request),
Dispatcher::FOUND => $this->handleFound($dispatched, $request),
default => null,
};
...
}
Hyperf는 클로저와 요청 처리기 두 가지 방식으로 라우트의 처리 함수를 설정할 수 있습니다. 다음은 이 두 가지 방식의 예시입니다.
// 클로저
Router::get('/hello-hyperf', function () {
return 'Hello Hyperf.';
});
// 요청 처리기, 다음 세 가지 방식 중 어느 것도 동일한 효과를 냅니다
Router::get('/hello-hyperf', 'App\Controller\IndexController::hello');
Router::get('/hello-hyperf', 'App\Controller\IndexController@hello');
Router::get('/hello-hyperf', [App\Controller\IndexController::class, 'hello']);
handleFound() 메서드에서 처리 함수가 클로저이면 parseClosureParameters() 메서드로 클로저의 매개변수를 해석한 후 실행합니다.
if ($dispatched->handler->callback instanceof Closure) {
$parameters = $this->parseClosureParameters($dispatched->handler->callback, $dispatched->params);
$callback = $dispatched->handler->callback;
$response = $callback(...$parameters);
}
처리 함수가 요청 처리기이면 prepareHandler() 메서드로 요청 처리기의 컨트롤러(Controller)와 동작(Action)을 해석하고, 컨테이너로 컨트롤러 객체를 인스턴스화한 후 parseMethodParameters() 메서드로 동작 메서드의 매개변수를 해석하고 마지막으로 해당 메서드를 실행합니다.
[$controller, $action] = $this->prepareHandler($dispatched->handler->callback);
$controllerInstance = $this->container->get($controller);
...
$parameters = $this->parseMethodParameters($controller, $action, $dispatched->params);
$response = $controllerInstance->{$action}(...$parameters);
처리 함수 실행이 완료된 후 반환된 결과가 ResponseInterface 인터페이스를 구현하지 않으면 transferToResponse() 메서드로 변환한 후 응답 객체를 반환합니다.
if (! $response instanceof ResponseInterface) {
$response = $this->transferToResponse($response, $request);
}
return $response->withAddedHeader('Server', 'Hyperf');
응답 콘텐츠 전송
응답 객체가 준비되면 응답 콘텐츠를 클라이언트에게 전송해야 합니다. 이 작업은 Hyperf\HttpServer\ResponseEmitter::emit() 메서드 호출을 통해 완료됩니다.
// 응답을 클라이언트로 전송
if (! isset($psr7Response) || ! $psr7Response instanceof ResponseInterface) {
return;
}
if (isset($psr7Request) && $psr7Request->getMethod() === 'HEAD') {
$this->responseEmitter->emit($psr7Response, $response, false);
} else {
$this->responseEmitter->emit($psr7Response, $response);
}
emit() 메서드에서 PSR-7 응답 객체의 응답 헤더, 쿠키 및 상태 코드 정보를 Swoole 응답 객체에 작성합니다. 그런 다음 응답 본문이 파일 객체인지 확인하여 파일이면 Swoole\Http\Response::sendfile() 메서드로 클라이언트에 파일을 전송하고, 그렇지 않으면 Swoole\Http\Response::end() 메서드로 응답 콘텐츠를 전송합니다.
public function emit(ResponseInterface $response, mixed $connection, bool $withContent = true): void
{
try {
if (strtolower($connection->header['Upgrade'] ?? '') === 'websocket') {
return;
}
// PSR-7 응답 객체의 정보를 Swoole 응답 객체에 작성
$this->buildSwooleResponse($connection, $response);
// 응답 콘텐츠가 파일인지 확인
$content = $response->getBody();
if ($content instanceof FileInterface) {
// 파일을 클라이언트로 전송
$connection->sendfile($content->getFilename());
return;
}
// 응답 콘텐츠 전송
if ($withContent) {
$connection->end((string) $content);
} else {
$connection->end();
}
} catch (Throwable $exception) {
$this->logger?->critical((string) $exception);
}
}
마지막으로 브라우저에서 응답 콘텐츠를 확인할 수 있습니다.