커뮤니티 게시글

자유게시판

AI 분석 2번째

nike 2026-08-06 14:58 조회 59 댓글 1 수정됨

코덱스 사용입니다.
부팅부터 출력까지 단계만 검토해 달라고 했습니다.

## 결론


Mublo의 요청 처리는 **작은 프레임워크가 반드시 가져야 할 경계를 직접 명시한 구조**다.

`public/index.php`는 설치 여부와 실행만 결정하고, `Application`은 요청 전체를 조율하며,

실제 정책은 Context·Middleware·Router·Dispatcher·Renderer로 넘긴다. 특히 멀티도메인

Context가 확장 로딩과 라우트 캐시보다 먼저 확정되는 점, 확장 로딩이 끝난 뒤 Context를

잠그는 점, 컨트롤러가 반드시 Response 객체를 반환하게 한 점이 좋다.


Laravel처럼 이미 검증된 범용 커널을 가져온 구조는 아니지만, 단순히 라우터와 include를

이어 붙인 수준도 아니다. 멀티도메인 CMS/커머스 플랫폼이라는 목적에 맞춰 요청 수명주기를

통제하고 있다.


## 전체 흐름


```text

Web server

  -> public/index.php

  -> bootstrap.php

       경로 상수 / Composer / 환경 / 오류 설정 / Application 생성

  -> 설치 여부 게이트

  -> Application::boot()

       Core 서비스 등록 / Core 이벤트 구독자 등록

  -> Application::run()

       Request 생성 및 입력 경로 검증

       -> ContextBuilder (도메인·영역·스킨·현재 메뉴)

       -> 도메인 유효성 검사

       -> 도메인별 활성 확장 register + boot

       -> 확장 이후 구독자 등록, Context attribute 잠금

       -> SecurityHeaders -> Session -> CSRF 미들웨어

       -> SiteContextReadyEvent

       -> RequestInterceptEvent

       -> Router

       -> route middleware

       -> Dispatcher / Controller action

       -> AbstractResponse

  -> Application::handleResponse()

       View / JSON / Redirect / HTML / File별 출력

  -> FrontViewRenderer 또는 AdminViewRenderer

  -> 최종 바이트 출력

```


## 1. 실제 진입점


### `public/index.php`


진입 파일의 책임은 좁다.


1. 시작 시간과 메모리를 기록한다.

2. `MUBLO_PUBLIC_PATH`를 현재 문서 루트로 정의한다.

3. `bootstrap.php`에서 `Application`을 받는다.

4. 설치 전에는 `/install`로, 설치 후 설치 경로 접근은 `/`로 보낸다.

5. 설치된 경우 `boot()`와 `run()`을 호출한다.

6. 디버그 모드의 HTML 응답에만 실행 시간, SQL 수, 메모리 정보를 HTML 주석으로 덧붙인다.


설치 판정은 `storage/installed.lock`과 `config/database.php` 존재 여부로 한다. 이 단계에서

DB 연결을 요구하지 않아, DB 장애를 “미설치”로 오판하지 않는다. 진입 파일에서 세션,

라우팅, 인증, 플러그인 로딩을 하지 않는 것도 적절하다.


### `bootstrap.php`


부트스트랩은 실행 환경만 만든다.


- 루트·소스·설정·스토리지·확장·에셋·뷰 경로 상수 정의

- 별도 document root를 위한 public 경로 재정의 지원

- Composer autoload와 전역 helper 로드

- `.env.{환경}.local`부터 `.env`까지 우선순위가 높은 파일을 먼저 읽는 환경 구성

- PHP 오류를 예외/로그 체계가 받을 수 있도록 기본 오류 출력과 로그 파일 설정

- timezone 설정

- `new Application()` 반환


여기서 DB나 세션을 미리 열지 않는다. 즉 CLI 도구, 설치기, 테스트가 같은 부트스트랩을

공유해도 웹 요청의 부수효과를 강제로 떠안지 않는다.


## 2. Core 부팅


`Application::boot()`의 순서는 다음 의미를 가진다.


1. 환경값을 `Env`에 적재한다.

2. `ErrorHandler`를 먼저 초기화한다.

3. `ServiceProvider`가 Core 인프라·Repository·Service·Controller를 컨테이너에 등록한다.

4. `EventDispatcher`를 확보한다.

5. Core 구독자를 먼저 등록한다.


여기까지는 아직 특정 도메인의 Plugin/Package를 로드하지 않는다. 확장의 활성 상태가

도메인마다 다르므로, 애플리케이션 생성 시점이 아니라 **요청의 도메인이 결정된 뒤**

로드해야 한다는 판단이다.


`ServiceProvider`는 등록 항목이 많지만 composition root라는 단일 책임 안에 있다. 파일이

길다는 사실 자체는 결함이 아니다. 오히려 의존성 조립을 여러 기능 클래스에 흩뿌리지 않고

한 곳에서 검색할 수 있다는 이점이 있다. 다만 등록 누락을 컴파일러가 잡아주지는 않으므로

DI 검사와 부팅 테스트가 계속 중요하다.


## 3. Request와 Context 생성


`Application::run()`은 먼저 `Request`를 만들고 다음 입력을 조기에 거부한다.


- 잘못된 JSON body: 400

- NUL 문자나 인코딩된 경로 구분자를 포함한 비정상 path: 400


그 뒤 `ContextBuilder`가 요청 단위 상태를 구성한다.


- `/admin`과 API 영역 판별

- Host로 도메인 해석: 캐시 우선, DB 폴백

- 도메인 SEO/사이트 설정과 요청 scheme/host를 결합해 이미지 URL 구성

- API는 스킨 없이 조기 반환

- Admin은 admin skin만 결정

- Front는 frame skin, Core front skin, block skin 결정

- 요청 URL과 도메인 메뉴 URL map을 대조해 현재 메뉴 코드와 메뉴별 layout override 확정


이 Context가 컨테이너에 요청 범위 객체로 등록되고, Cache·ErrorHandler·EditorHelper에도 현재

도메인이 전달된다. 따라서 뒤 단계가 raw Host를 제각기 재해석하지 않고 같은 tenant 결정을

공유한다.


도메인을 찾지 못했거나 정지된 경우도 이 경계에서 처리한다. 등록되지 않은 Host가 라우터나

확장 스캐너까지 들어가는 일을 막는다.


## 4. 도메인별 확장 로딩


Context 확정 후 `ExtensionService`가 해당 도메인의 활성 Plugin과 Package 목록을 구한다.

`ExtensionManager`는 Package를 먼저 register하고, Plugin을 register한 뒤, 모든 Provider의

`boot()`를 호출한다. 이후 Core의 “확장 이후” 구독자를 등록하고 Context attribute를 잠근다.


여기서 순서는 중요하다.


- 확장은 도메인 정보를 사용할 수 있다.

- 모든 서비스 등록이 끝난 뒤 이벤트 구독이 활성화된다.

- Context의 정규 attribute는 확장 로딩 이후 임의 변경되지 않는다.

- 운영 모드에서 비핵심 확장 하나의 실패는 진단을 남기고 나머지 Core 요청을 계속할 수 있다.


설치기와 public API 일부는 확장 로딩을 건너뛰어 복구 경로를 유지한다.


## 5. 전역 미들웨어와 실제 액션 실행


전역 파이프라인은 다음 순서다.


```text

SecurityHeadersMiddleware

  -> SessionMiddleware

       -> CsrfMiddleware

            -> 요청 본체

```


요청 본체에서는 다음 일이 일어난다.


1. `SiteContextReadyEvent` 발행

2. `RequestInterceptEvent` 발행

3. 가로챈 Response가 있으면 라우팅 없이 조기 반환

4. `Router::dispatch()`로 route 결정

5. `Dispatcher::dispatch()`로 route middleware와 controller action 실행


CSRF는 세션이 시작된 뒤 검증되고, 보안 헤더는 가장 바깥에서 정상·오류 응답 모두에 적용될

수 있는 배치다. `RequestInterceptEvent`는 팝업성 우회가 아니라 Response 객체를 반환하는

명시적 단축 경로라서 결과 타입 규약을 유지한다.


## 6. Router


Router는 FastRoute를 사용한다.


- 개발 모드: 매 요청 route table 구성

- 운영 모드: `storage/cache/routes/{domain}.{signature}.cache.php`

- trailing slash 정규화

- 명시 route 우선

- Front 미매칭은 404

- Admin 미매칭만 제한된 auto-resolve 허용

- 허용하지 않은 HTTP method는 405 경로


특히 route cache의 키를 raw Host가 아니라 Context에서 해석된 정식 도메인으로 만든다. 임의의

서브도메인을 대량 전송해 캐시 파일과 inode를 늘리는 문제를 피하려는 방어다.


Front에서 controller 자동 매핑을 금지한 것도 중요하다. 자동 매핑은 route별 인증 middleware를

붙일 수 없으므로 Front까지 열면 대소문자 변형이나 비정규 URL로 인증 경계를 우회할 수 있다.

Admin에서만 조회 action allowlist와 함께 제한적으로 사용한다.


확장 route는 Core/다른 확장과 충돌 검사를 거친다. 확장 설정을 저장하기 전에도 실제 조합으로

route table을 검증하기 때문에, 충돌을 활성화 후 첫 요청에서 발견하는 구조가 아니다.


## 7. Dispatcher


Dispatcher는 route 결과를 실제 액션으로 바꾼다.


1. controller class 존재 확인

2. 컨테이너 등록 인스턴스 우선, 없으면 생성자 reflection DI

3. action 존재 확인

4. public·non-static 메서드만 허용

5. route middleware 실행

6. action 인자에 Request, Context, route params를 시그니처에 맞춰 주입

7. 반환값이 `AbstractResponse`인지 검증


컨트롤러가 문자열이나 배열을 직접 echo하지 못하게 하고 Response 계층을 통과시키는 것이

핵심이다. 이 규칙 덕분에 상태 코드, 헤더, JSON, redirect, file download를 최종 출력 단계에서

일관되게 다룰 수 있다.


Reflection 기반 자동 DI는 소규모 자체 프레임워크에 실용적이다. 반면 런타임에야 잘못된

생성자나 action 서명을 알 수 있으므로 `check-di`, PHPStan, Router/Dispatcher 테스트가 사실상

컴파일 단계 역할을 한다.


## 8. Response에서 실제 출력까지


`Application::handleResponse()`는 Response 종류별로 처리한다.


- `ViewResponse`: `RendererResolveEvent`로 renderer 교체 기회를 준 뒤 Admin/Front renderer로 출력

- `JsonResponse`: 상태·헤더 설정 후 JSON 직렬화

- `RedirectResponse`: Location과 상태 코드 출력

- `HtmlResponse`: 준비된 HTML 출력

- `FileResponse`: 파일 또는 메모리 콘텐츠를 안전한 다운로드 헤더와 함께 전송


### Front 2-pass rendering


`FrontViewRenderer`는 콘텐츠를 먼저 buffer에 렌더한 뒤 전체 frame을 조립한다.


1. `ViewContext`와 helper 구성

2. `ViewContextCreatedEvent`로 확장 helper 등록

3. 공통 사이트·회원·메뉴 데이터 수집

4. 선택된 도메인 frame override와 editor runtime 설정

5. 콘텐츠를 먼저 렌더해 스킨의 header/footer/layout 힌트 수집

6. Head

7. topbar 블록

8. Header

9. subhead 블록

10. LayoutOpen, 좌측 블록, contenthead, 본문, contentfoot, 우측 블록

11. LayoutClose, subfoot, Footer

12. `FrontFootRenderEvent` 확장 HTML

13. Foot

14. `PageViewedEvent`


레이아웃 우선순위도 코드로 고정돼 있다.


```text

블록 페이지 설정 > 메뉴 override > 스킨 힌트 > header 없는 화면의 full 강제 > 도메인 기본값

```


선택 스킨에 특정 파일이 없으면 같은 그룹의 `basic` 파일로 폴백한다. Core가 새 뷰를 추가해도

부분 오버라이드 스킨이 바로 깨지지 않게 만든 호환 정책이다.


### 늦게 등록되는 CSS/JS


블록이나 확장은 렌더링 도중 필요한 asset을 `AssetManager`에 등록한다. 전체 HTML buffer가

완성된 뒤 CSS/JS placeholder를 치환하므로, 콘텐츠를 먼저 렌더하면서도 CSS는 head에,

JS는 body 끝에 놓을 수 있다. 슬롯 marker가 없는 asset은 기본 영역으로 폴백해 유실하지 않는다.


이 방식은 번들러 없는 서버 렌더링 시스템에 잘 맞는다. 다만 전체 페이지 출력 버퍼에

의존하므로 대형 스트리밍 응답에는 적합하지 않다. 파일 응답을 별도 Response로 분리한 이유도

여기에 있다.


## 9. 오류 경로


`ErrorHandler`는 PHP error를 예외로 바꾸고 uncaught exception과 shutdown fatal error를 받는다.


- typed application exception의 HTTP status 사용

- AJAX/API 요청은 JSON 오류, 일반 요청은 HTML 오류

- debug에서는 진단 정보, production에서는 노출을 제한한 메시지

- URL의 민감 query parameter 마스킹

- 도메인 logger 사용

- asset/probe 404 같은 소음성 오류 로그 억제


확장 로딩 오류도 별도 diagnostics에 남는다. 사용자에게 오류 세부정보를 노출하지 않으면서

운영자가 “왜 특정 확장 기능만 사라졌는지” 추적할 수 있는 경로가 있다.


## 설계 평가


### 잘된 점


- 멀티도메인 결정이 라우트·캐시·확장보다 앞선다.

- Core 부팅과 도메인별 확장 부팅이 분리돼 있다.

- Context를 요청의 단일 진실로 사용하고 확장 이후 잠근다.

- Front 자동 라우팅을 닫고 명시 route를 보안 경계로 삼는다.

- route middleware와 global middleware의 책임이 구분된다.

- 모든 controller 결과가 Response 계층으로 수렴한다.

- 2-pass rendering과 asset capture가 블록/스킨 확장을 현실적으로 지원한다.

- 특정 스킨 파일 누락, 비핵심 확장 실패, block render 실패에 국소 폴백이 있다.


### 남은 부담과 개선 가치


1. `Application`, `Router`, `FrontViewRenderer`는 정책의 중심점이다. 길어서가 아니라 변경 시

   영향 범위가 넓기 때문에 요청 수명주기 회귀 테스트가 중요하다. 현재 Router, Dispatcher,

   Renderer 단위 테스트는 이 부담을 상당 부분 받치고 있다.

2. 405가 전용 HTTP 예외보다 일반 `RuntimeException` 경로를 쓰는 부분은 오류 매핑을 더

   명시적으로 만들 여지가 있다.

3. Front 출력은 buffer와 `echo` 중심이다. 현재 제품 성격에는 맞지만 향후 streaming/edge

   rendering을 요구하면 Response body abstraction을 한 단계 더 올려야 한다.

4. Reflection DI는 편하지만 설정 오류가 런타임 오류다. `composer check-di`를 배포 게이트에서

   빼면 안 된다.

5. 운영 모드의 확장 장애 흡수는 가용성을 높이는 대신 기능 누락을 조용히 만들 수 있다.

   diagnostics와 로그 알림을 운영 화면에서 눈에 띄게 유지해야 한다.


## 최종 판단


부팅부터 출력까지의 설계는 **좋다**. 자체 프레임워크를 만들었다는 사실 때문이 아니라,

요청에서 변하면 안 되는 상태, 확장이 개입할 수 있는 시점, 보안 검사가 선행돼야 하는 지점,

출력 형식이 갈라지는 지점을 실제 코드 경계로 만들었기 때문이다.


이 부분은 “짧고 우아한 마이크로 커널”보다는 “제품의 정책을 한눈에 추적할 수 있는 명시적

커널”에 가깝다. 현재 Mublo에는 후자가 더 적합하다.


## 주요 근거 파일


- `public/index.php`

- `bootstrap.php`

- `src/Core/App/Application.php`

- `src/Core/Provider/ServiceProvider.php`

- `src/Core/Context/ContextBuilder.php`

- `src/Core/App/Router.php`

- `src/Core/App/Dispatcher.php`

- `src/Core/Middleware/MiddlewarePipeline.php`

- `src/Core/Rendering/FrontViewRenderer.php`

- `src/Core/Rendering/AdminViewRenderer.php`

- `src/Core/Error/ErrorHandler.php`

- `src/Core/Response/*Response.php`

- `tests/Unit/Core/App/RouterTest.php`

- `tests/Unit/Core/App/DispatcherTest.php`

- `tests/Unit/Rendering/FrontViewRenderer*Test.php`

글쓰기

댓글 1

우아한삽질 2026-08-07 10:48
오... 오늘도 열렬한 분석 감사합니다 !