하나의 Context, 하나의 Registry, 그리고 언제 멈춰야 할지 알기
Source: Dev.to
마지막 포스트, 첫 번째 런타임과 어댑터가 살아났습니다
타입 시스템은 선언 병합, 빌드‑툴 교체 등으로 다시 싸우고 있었지만 솔직히? 그 문제를 좀 잊어버렸어요. 아직 존재하지만 지금은 아무것도 막고 있지 않아요. 가끔 앞으로 나아가면 옛 전투가 기다리고 있죠. 😄
유닛은 존재했습니다. 등록할 수 있었습니다. 하지만 **“설정 파일에 선언된 유닛”**과 “실제로 실행 중인 유닛” 사이의 연결 고리는 아직 없었습니다.
그것이 바뀌었습니다. 이번 작업 라운드는 이전보다 조용하지만 모든 것을 건드립니다:
- 설정 해석
- 공유 베이스 컨텍스트
- 작동하는 expose‑and‑query 시스템
- 레지스트리를 정상적으로 유지하기 위한 몇 가지 작은 유틸리티
먼저 해야 할 일: 유닛 로딩
다른 어떤 작업도 수행되기 전에, 커널은 자신이 다루는 유닛이 무엇인지 알아야 합니다. 바로 config.resolve.units가 하는 일입니다 — 설정 파일에서 평평한 유닛 리스트를 받아 커널이 실제로 사용할 수 있는 형태로 해석합니다.
유닛은 참조(문자열, 팩토리 함수, 가져온 객체) 형태로 들어오고, 로드되고 검증된, 부팅 준비가 된 정의 형태로 나옵니다. 커널은 이를 어떻게 분류하고 초기화할지 알고 있습니다.
이것은 **“내 설정에 유닛을 선언했다”**와 “커널이 그것을 어떻게 처리해야 할지 안다” 사이의 다리입니다.
이 다리가 없으면 유닛 배열은 파일에 있는 데이터일 뿐입니다. 이 다리가 있으면 커널은:
- 유닛을 종류별로 분류하고,
- 의존성에 따라 위상 정렬을 수행하고,
- 올바른 순서로 라이프사이클 훅을 호출할 수 있습니다.
화려한 작업은 아니지만, 다른 모든 것이 가능하도록 하는 배관 역할을 합니다.
모든 것을 공유하는 하나의 컨텍스트
유닛이 해석되면 다음 질문은: 그들이 작업할 때 무엇을 얻게 될까? Velnora의 모든 유닛 종류는 컨텍스트 객체를 갖습니다 — configure()와 build() 같은 라이프사이클 훅에 전달되는 객체입니다. 컨텍스트는 유닛이 시스템의 나머지와 소통하는 방법이며, API를 노출하고, 다른 유닛을 조회하고, 설정을 읽는 역할을 합니다.
지금까지는 각 컨텍스트가 독립적으로 정의되었습니다:
IntegrationConfigureContext는 자체ctx.query()를 가졌습니다.IntegrationBuildContext도 자체를 가졌습니다.AdapterDevContext— 같은 방식으로 처음부터 다시 선언되었습니다.
그 결과 모든 곳에 중복된 표면이 생겼고, 공유 동작에 대한 변경은 모두에게 반영해야 했습니다. 세 종류의 유닛이라면 관리가 가능했지만, 열 개가 되면 관리가 어려워집니다.
해결책은 간단합니다 — 공유되는 모든 것을 담는 BaseContext를 만들고, 구체적인 컨텍스트는 이를 상속하도록 합니다.
- Integration 컨텍스트는 베이스를 상속하고 integration‑specific 기능을 추가합니다.
- Adapter 컨텍스트도 마찬가지입니다.
- Runtime 컨텍스트도 마찬가지입니다.
공유 부분은 한 곳에 모이고, 특화된 부분은 각자 자리에서 유지됩니다. 이것은 영리한 아이디어가 아니라, 실제로 처음으로 구현된 것입니다.
Expose, Query, 완료
BaseContext는 이제 모든 유닛이 종류와 관계없이 필요로 하는 두 가지 작업, 즉 노출과 조회 메커니즘을 완전히 구현합니다 — ctx.expose()와 ctx.query()가 실제로 동작하고, 단순히 타입만 정의된 것이 아니라 엔드‑투‑엔드로 연결되었습니다.
- 유닛은
configure()단계에서 키 아래에 API를 노출할 수 있습니다. - 해당 키에 대한 의존성을 선언한 다른 유닛은
query를 통해 실제 구현을 받아올 수 있습니다.
형변환도, any도, 수동 조회도 없습니다. 전체 Unit System이 설계된 기반이 이제 실현되었습니다.
BaseContext를 뒷받침하는 것은 GlobalRegistry — 런타임에 모든 노출된 API를 보관하는 중앙 레지스트리 객체입니다.
- 유닛이
ctx.expose("docker", dockerApi)를 호출하면GlobalRegistry에 기록됩니다. - 다른 유닛이
ctx.query("docker")를 호출하면 같은 곳에서 읽어옵니다.
각 레지스트리 항목은 키로 구분되며, 키는 적절히 타입이 지정되고 강제됩니다. 컴파일 타임에 TypeScript가 "docker"가 존재한다고 확인하면, 런타임 레지스트리에도 "docker" 슬롯이 존재합니다. 존재하지 않으면 슬롯도 없습니다.
Source: …
ist and the query fails loudly.
With these in place, the BaseContext is not just a shared interface anymore. It is the working layer that integrations, adapters, and runtimes all build on top of. The thing that was only types two posts ago is now running code.
A Helper for the Lazy
One small helper that came out of this work — makeRegistryObject. It is a convenience for lazy people. You give it a name and a shape, and it builds you a nested object where every level is also a usable string key.
makeRegistryObject("kernel", {
config: {
units: "units"
}
})
The result:
result→"kernel"result.config→"kernel:config"result.config.units→"kernel:config:units"
Each level works as a registry key. No manual string building, no typos, full autocomplete.
It is a tiny thing, but in a monorepo with a growing number of registry keys, it saves a lot of stupid mistakes.
The Pause
After wiring up the base context and the config resolution, I started pushing into the runtime implementation — the actual guts of how a runtime like Node or Bun boots its toolchain, resolves packages, and executes code. And then I stopped.
Not because something broke, but because I realized I was about to make decisions that would be very expensive to undo. The runtime layer touches everything:
- how units get their toolchains,
- how execution plans are built,
- how the package‑manager abstraction plugs in,
- how thread‑mode and process‑mode execution diverge.
One wrong abstraction here and every adapter, every integration, every future runtime inherits the mistake.
So I went back to research: reading through the earlier design notes, sketching alternatives, revisiting the Toolchain API and the Adapter Protocol to see if the boundaries still made sense after…
Everything that changed in the last few rounds. Sometimes the most productive thing you can do is stop coding and think. This is one of those times.
다음은 무엇인가
런타임 설계가 확정되면, 다음 단계는 이를 구현하는 것입니다: BaseContext를 통해 툴체인을 부팅하고, 레지스트리를 통해 패키지를 해결하며, 어댑터가 뒤에 어떤 런타임이 있는지 알 필요 없이 사용할 수 있는 실행 계획을 생성하는 실제 Node 런타임을 만드는 것이죠. 이것이 전체 스택이 엔드‑투‑엔드로 작동함을 증명하는 테스트가 됩니다.