기초Context

Context

고전적 PHP는 평생을 하나의 단순한 규칙으로 살았어요. 하나의 프로세스, 하나의 요청이죠. 그 규칙에서 하나의 온전한 문화가 자라났습니다. 전역 변수, 정적 속성, 싱글턴이요. 현재 사용자? 정적 속성 Auth::$user죠. 로깅을 위한 요청 ID? 전역 변수고요. 그것은 흠 없이 동작했어요. "현재의 것"이 정말로 프로세스 전체에서 단 하나였으니까요.

TrueAsync는 그 규칙을 폐지했어요. 이제 수백 개의 코루틴이 한 프로세스 안에 뒤섞여 살며 서로 다른 사용자를 섬깁니다. 한 코루틴에서 Auth::$user를 대입하면, 다음에 깨어나는 코루틴이 그것을 도로 읽어요. 그게 같은 요청이기만 해도 운이 좋은 거예요. 익숙한 이야기죠. 아홉 번째 장에서 열 개의 워커가 하나의 PDO를 공유했을 때 똑같은 일이 일어났어요. 스레드 없는 경쟁인데, 이번에는 전역 상태에서 일어나는 것뿐입니다.

대신 모든 것을 매개변수로 전달할까요? 정직하지만 무자비해요. 실제로는 그중 하나만 필요한데도 인증 토큰을 스무 개의 함수 시그니처에 꿰어야 할 테니까요. 여러분이 진짜로 원하는 것은 프로세스가 아니라 논리적 실행 흐름에 묶인 저장소예요. 그리고 우리에게는 그 일에 알맞은 구조가 이미 있어요. 코루틴과 스코프가 이미 트리를 이루고 있으니까요.

트리 위의 저장소

Async\Context는 스코프나 코루틴에 붙는 키-값 저장소입니다. 스코프의 컨텍스트는 그 스코프의 모든 코루틴이 이용할 수 있어요.

php
use function Async\current_context;

// 미들웨어, 요청 처리 시작
current_context()
    ->set('request_id', bin2hex(random_bytes(8)))
    ->set('user_id', $userId);

그리고 거기서부터, 어디서든, 어떤 호출 깊이에서든, 추가 매개변수 하나 없이요.

php
function logInfo(string $message): void
{
    $requestId = current_context()->find('request_id');
    error_log("[$requestId] $message");
}

find에 주목하세요. 현재 컨텍스트에서 키를 찾고, 없으면 스코프 트리를 위로 올라갑니다. 자식 스코프는 부모의 데이터를 자동으로 볼 수 있으므로, 핸들러 깊숙이에서 시작된 코루틴은 요청의 맨 처음에 설정된 request_id를 찾아낼 거예요. Go의 context.Context와 같은 메커니즘인데, 다만 그것을 손수 찾아 여기저기 전달할 필요가 없어요. 트리가 이미 거기에 있으니까요.

요청들은 더 이상 서로 방해하지 않아요. 각 요청은 자기만의 스코프를 가지고, 따라서 자기만의 컨텍스트를 가져요. 천 개의 병행 요청, 천 개의 독립적인 request_id, 그리고 전역 변수는 단 하나도 없습니다.

세 가지 수준

컨텍스트는 넓은 것에서 좁은 것까지 세 가지 수준으로 존재해요.

php
use function Async\root_context;
use function Async\current_context;
use function Async\coroutine_context;

root_context();      // 프로세스 전체: 설정, 모두가 공유
current_context();   // 현재 스코프: 요청, 사용자, 로케일
coroutine_context(); // 오직 이 코루틴만: 비공개 데이터

root_context는 예전에 마땅히 전역 변수여야 했던 것을 위한 정당한 보금자리예요. 애플리케이션 이름, 설정 같은 것이죠. 여러분은 그것을 명시적으로 주소 지정합니다. root_context()->find('app_name')처럼요. coroutine_context는 반대편 극이에요. 그 데이터는 코루틴 자신 말고는, 같은 스코프의 이웃에게조차 보이지 않아요. 그 둘 사이에서 find가 스코프 수준들을 꿰어 줍니다. 로컬에서 못 찾으면 부모에게 물어보는 것이죠.

그리고 한 가지 더 반가운 세부 사항이 있어요.

php
current_context()->set('user_id', 42);
current_context()->set('user_id', 7); // AsyncException: 키가 이미 존재함

명시적으로 요청하지 않는 한(replace: true) 덮어쓰기는 금지됩니다. 컨텍스트는 이 장이 시작할 때 나온 바로 그 전역 변수의 병, 즉 누군가가 어딘가에서 조용히 값을 덮어쓰는 것으로부터 스스로를 지켜요.

여정의 끝

열다섯 장 전에 우리는 두 함수를 "한꺼번에" 실행하고서 그 출력이 서로 엇갈리는 것을 보고 놀랐어요. 그 이후로 하나의 온전한 시스템이 모양을 갖췄고, 그 모든 수준이 저마다의 관심사를 가지고 있어요.

  • 코루틴, spawn, await — 동시성의 단위이자 결과에 대한 약속입니다.
  • 취소, 타임아웃, 예외 — 중단 계약입니다. 아무것도 영원히 실행되지 않고, 아무것도 조용히 죽지 않아요.
  • Future와 채널 — 연결입니다. 단일 결과, 그리고 동기화를 갖춘 값의 스트림이죠.
  • Scope, TaskGroup, TaskSet, iterate — 구조입니다. 모든 코루틴에는 주인이 있고, 모든 그룹에는 대기 전략이 있어요.
  • — 리소스 규율입니다. 적은 연결, 많은 코루틴이죠.
  • 스레드 — 공유 메모리 없는, 계산을 위한 병렬성입니다.
  • Context — 프로세스가 아니라 실행에 묶인 데이터입니다.

우리가 결코 배울 필요가 없었던 것에 주목하세요. 뮤텍스, 세마포어, 데이터 경쟁, 콜백, 또는 함수를 비동기 대 동기로 물들이는 일 말이에요. 코드는 그저 놀고 있기를 멈춘 평범한 순차 PHP로 남았습니다.

여기서부터 두 갈래 길이 있어요. 직접 손으로 익히려면 문서로 가세요. 거기서는 모든 구성 요소가 마지막 나사 하나까지 분해되어 있어요. 그 모든 것이 밑에서 어떻게 동작하는지 이해하려면 아키텍처로 가세요. 아니면 무엇보다 좋은 방법은, 그저 여러분의 가장 느린 스크립트를 가져다가 단 하나의 spawn이 거기에 무슨 일을 하는지 보는 거예요.