MUBLO · SYSTEM NOTE

ABOUT

SYSTEM NOTE

프레임워크 - 요청 하나가 지나가는 길

wwiz 2026-07-28 13:55 8분 읽기 조회 110 수정됨

사이트가 이상해졌을 때 진짜 문제는 화면이 깨졌다는 사실이 아닙니다. 어디를 봐야 하는지 모른다는 것입니다.

좋은 구조는 각자가 무엇을 책임지는지 분명한 구조입니다. 책임이 분명하면 문제가 났을 때 볼 곳도 정해집니다.

요청이 들어와 응답이 나가기까지의 흐름은 새롭지 않습니다. 진입점과 라우터, 컨트롤러와 응답은 대부분의 웹 프레임워크에 있습니다. 머블로가 중요하게 보는 것은 구성 요소의 목록이 아니라 순서와 경계입니다. 무엇을 먼저 결정하고, 어디까지를 누구의 일로 둘 것인가에 관한 이야기입니다.

길 전체

public/index.php            요청 진입 · 설치 여부 확인
bootstrap.php               경로와 설정을 갖춘다
Application::boot()         에러 핸들러 · 코어 서비스 등록
Application::run()
  ├ Request                 입력을 구성하고 잘못된 요청을 먼저 거절한다
  ├ ContextBuilder          관리자 요청인지, 어느 도메인인지 판단한다
  ├ validateDomain()        사용할 수 있는 도메인인지 확인한다
  ├ ExtensionManager        켜둔 기능만 불러오고, 그 결과를 잠근다
  ├ MiddlewarePipeline      보안 헤더 · 세션 · 위조 방지
  ├ Router                  켜져 있는 기능의 주소록에서만 찾는다
  ├ Dispatcher              컨트롤러를 실행한다
  └ Response 처리           종류에 따라 렌더링하거나 바로 내보낸다
ErrorHandler                처리 중 예외가 모이는 마지막 경계

한 요청은 이 길을 따라갑니다. 각 구간은 해야 할 일뿐 아니라 하지 않을 일도 정해져 있습니다.

도메인이 먼저, 그다음 확장

요청을 만들고 나면 그것이 머블로 안에서 무엇을 뜻하는지 해석합니다. 관리자 영역인지, 어느 도메인으로 들어왔는지, 사용할 스킨과 설정은 무엇인지 결정합니다. 여기서 도메인은 하나의 설치본 안에서 독립적으로 운영되는 사이트 단위입니다. 도메인이 정해지면 등록 여부와 상태를 확인하고, 사용할 수 없는 도메인이라면 요청은 그 자리에서 끝납니다.

코어는 상태만 봅니다. 계약이 만료됐는지 같은 판단은 그 정책을 가진 패키지가 하고, 결과를 상태에 반영합니다. 정책은 패키지가, 상태라는 장치는 코어가 — 경계는 여기서도 같은 방식으로 그어져 있습니다.

도메인은 반드시 확장보다 먼저 정해져야 합니다. 머블로는 설치본 하나로 여러 사이트를 운영하고, 사이트마다 켜둔 기능이 다릅니다. 도메인을 확정하기 전에 확장을 불러오면 A 사이트의 기능이 B 사이트에서도 살아날 수 있습니다. 격리가 기본값이 되려면 격리의 기준부터 먼저 정해야 합니다. 도메인 이야기는 다음 글에서 이어집니다.

도메인이 확정되면 그 사이트에서 켜둔 확장만 불러옵니다. 확장은 이때 서비스와 이벤트를 등록하고 요청 정보에 필요한 값을 더할 수 있습니다. 로드가 끝나면 그 정보는 잠기고, 이후에는 읽을 수만 있습니다. 요청 처리 중간에도 값이 계속 바뀐다면 같은 요청 안에서 코드의 위치에 따라 결과가 달라지기 때문입니다. 잠금은 “언제까지 쓸 수 있는가”를 문서가 아니라 코드로 강제합니다.

관문과 주소록

안쪽으로 들어온 요청은 공통 관문을 지납니다. 보안 헤더, 세션, 위조 방지 순으로 셋뿐입니다. 보안 헤더가 가장 바깥인 이유는 세션이나 위조 검사가 요청을 중간에 거절하더라도 관리자 HTML 응답에 필요한 헤더를 붙이기 위해서입니다. 안쪽에 두면 거절당한 관리자 HTML 응답에만 보안 헤더가 빠집니다. 목록이 짧은 것도 의도입니다. 로그인이나 권한 확인처럼 특정 주소에만 필요한 검사를 전역에 쌓기 시작하면, 어떤 요청에 무엇이 적용되는지 알 수 없게 됩니다.

주소록도 사이트마다 다릅니다. 코어의 주소에 그 사이트에서 켜둔 확장의 주소만 더합니다. 꺼둔 기능은 접근이 막히는 것이 아니라 주소 자체가 존재하지 않습니다. 확장의 주소가 기존 주소와 충돌하면 그 확장의 주소 전체를 취소하고, 코어 관리자 주소는 확장이 덮을 수 없습니다. 일부 화면만 살아 있거나, 확장 하나 때문에 관리자 화면이 조용히 가려지는 상태를 막기 위해서입니다.

조용히 어긋나는 것보다 시끄럽게 실패하는 편이 낫습니다.

컨트롤러는 화면을 출력하지 않는다

컨트롤러가 화면을 직접 출력하면 출력 책임이 코드 곳곳에 흩어집니다. 이미 본문을 내보낸 뒤에는 헤더를 바꾸기 어렵고, 오류가 나도 절반쯤 출력된 화면을 되돌릴 수 없습니다. 그래서 머블로의 컨트롤러는 무엇을 응답할지만 정해서 돌려줍니다. 실제 출력은 컨트롤러 밖에서 일어납니다.

응답은 레이아웃을 입힌 화면, 원본 HTML, JSON 데이터, 리다이렉트, 파일, XML 문서로 나뉩니다. 컨트롤러가 응답 객체가 아닌 값을 반환하면 즉시 오류가 납니다. 종류가 정해지면 출력 방식도 따라오므로, 애플리케이션은 누구에게 넘길지만 결정하고 화면 조합에는 관여하지 않습니다. 컨트롤러의 마지막 줄만 봐도 그 기능이 무엇을 반환하는지 알 수 있습니다.

마지막 경계

흐름 어디에서 예외가 발생하든 마지막에는 한 곳으로 모입니다. 여기서도 판단은 조심스럽습니다. 오류 메시지에 “찾을 수 없다”는 문장이 있다고 해서 404로 처리하지는 않습니다. 내부 부품을 찾지 못한 서버 오류가 평범한 “페이지가 없습니다” 화면으로 둔갑할 수 있기 때문입니다. 오류가 직접 가진 상태와 타입을 먼저 보고 응답을 결정합니다.

데이터를 기대한 요청에는 데이터 형식으로 오류를 돌려줍니다. 화면 뒤에서 도는 호출이 사람이 읽을 오류 페이지를 받아오면 곤란하니까요. 이미 출력이 시작됐다면 무리하게 되돌리려 하지 않습니다. 늦게 도착한 오류 처리기가 문제를 더 크게 만들지 않는 것도 마지막 경계의 책임입니다.

문제를 좌표로 말하기

이 흐름을 알면 문제를 위치로 말할 수 있습니다.

  • 특정 도메인만 열리지 않는다 → validateDomain()
  • 한 사이트에서만 특정 주소가 404다 → ExtensionManager(꺼졌는가) 또는 Router(등록됐는가)
  • 로그인이 유지되지 않는다 → MiddlewarePipeline
  • 특정 화면에서만 권한이 이상하다 → 그 라우트에 붙은 미들웨어
  • 내려받은 파일이 깨진다 → Response 종류와 출력 경계
  • 오류 응답의 형태가 이상하다 → ErrorHandler

용의자를 여러 곳에서 한 곳으로 줄이는 것. 프레임워크가 해줄 수 있는 가장 실질적인 도움입니다.

물론 공짜는 아닙니다. 요청마다 도메인을 확인하고, 켜둔 확장을 조회해 불러와야 합니다. 캐시로 비용을 줄일 수는 있지만 완전히 없앨 수는 없습니다. 그럼에도 이 순서를 택한 이유는 단순합니다. 격리와 예측 가능성을 내주고 얻은 속도는, 문제가 생겼을 때 더 큰 비용으로 돌아오기 때문입니다.

길이 정해져 있으면, 문제가 생겼을 때 찾을 곳도 정해집니다.

글쓰기

댓글 0

등록된 댓글이 없습니다.