애플리케이션 아키텍처
Wippy 애플리케이션은 소스 파일의 트리가 아니라 레지스트리 엔트리의 그래프입니다. 코드는 function.lua와 process.lua 엔트리에 살고, 이들을 연결하는 모든 것 — 어떤 함수가 HTTP 라우트에 응답하는지, 서비스가 어떤 프로세스를 감독하는지, 어떤 라이브러리가 어떤 라이브러리를 임포트하는지 — 은 _index.yaml에 선언됩니다. 앱을 구조화한다는 것은 그래프가 성장하면서도 합성 가능하고, 테스트 가능하며, 부팅 가능하게 유지되도록 그 그래프를 네임스페이스로 나누는 방법을 결정하는 일입니다.
이 페이지는 레이아웃 뒤에 있는 논리를 다룹니다. 기계적인 규칙(파일 형식, 명명, _index.yaml의 위치)은 YAML 및 프로젝트 구조를, 엔트리 종류 자체는 엔트리 종류 가이드를 참조하세요.
단위는 슬라이스다
파일 타입이 아니라 기능으로 조직하세요. 슬라이스는 하나의 능력을 끝에서 끝까지 소유합니다 — 데이터베이스 접근, 장기 실행 프로세스, HTTP 표면, 그리고 이들이 공유하는 어휘 — 그리고 하나의 네임스페이스 접두사 아래에 삽니다:
src/app/jobs/ namespace: app.jobs
src/app/auth/ namespace: app.auth
src/app/billing/ namespace: app.billing
그 대안 — 최상위 handlers/, models/, services/ 분할 — 은 모든 기능을 트리 전체에 흩어 놓고 근접성으로 서로 결합시킵니다. 슬라이스는 기능의 영향 범위를 하나의 폴더 안에 가둡니다: 프로젝트 전체에서 참조를 추적하지 않고도 읽고, 테스트하고, 삭제할 수 있습니다.
슬라이스 안의 레이어
슬라이스 내부는 무엇이 바깥 세계에 닿는가라는 축을 따라 나눕니다. 이것이 하위 네임스페이스로 표현된 포트와 어댑터(헥사고날) 아키텍처입니다:
src/app/jobs/ namespace: app.jobs ← shared vocabulary
consts.lua config.lua types.lua
persist/ namespace: app.jobs.persist ← database adapters (sql)
service/ namespace: app.jobs.service ← processes, workers
api/ namespace: app.jobs.api ← http.endpoints
임포트는 가장 바깥에서 가장 안쪽으로, 한 방향으로만 흐릅니다:
api → service → persist → { consts, config, types }
슬라이스 루트(공유 어휘)는 자신의 자식으로부터 아무것도 임포트하지 않습니다. 자식이 루트를 임포트합니다. 어떤 레이어도 위로 거슬러 올라가지 않으며, 어떤 슬라이스도 다른 슬라이스를 직접 임포트하지 않습니다 — 슬라이스 간 공유는 공통 부모 네임스페이스(예: app.core:types)를 거치며, 절대 옆으로 가지 않습니다.
더 작은 슬라이스는 형식을 줄입니다 — 라이브러리와 엔드포인트 하나가 담긴 단일 _index.yaml이면 충분합니다. 어떤 크기에서도 살아남는 규칙은 폴더 개수가 아니라 임포트 방향입니다.
공유 어휘
잘 구조화된 슬라이스의 루트에는 세 파일이 반복해서 나타납니다. 모든 레이어가 읽지만 그 어느 레이어도 아닌 것을 담습니다:
| 파일 | 담는 것 | 능력 |
|---|---|---|
consts.lua |
상태 머신, 열거형, 큐 티어, 프로세스의 레지스트리 ID. 데이터베이스 CHECK 제약을 반영하는 값들. |
없음 |
config.lua |
코드 기본값 폴백을 가진 env 조정 가능한 노브(env.get(KEY) or DEFAULT) — 값이 선택 사항이 되기 위해 env.variable 엔트리가 필요 없도록. |
env |
types.lua |
엔티티 형태(type Job = { ... }) — 영속 레이어가 반환하는 행들. |
없음 |
consts와 types는 호스트 능력을 선언하지 않습니다 — 테이블을 반환하는 순수한 library.lua입니다. 이는 의도적입니다: 도메인 어휘는 I/O를 수행할 수 없으므로 비즈니스 로직으로 흘러갈 수 없고, 데이터베이스도 프로세스 호스트도 없이 단위 테스트가 가능합니다.
이 어휘는 슬라이스 전용으로 유지하세요. 슬라이스 간에 공유되는 상수와 타입은 공통 부모에 살고 그곳의 임포트를 통해 참조됩니다 — 각 슬라이스로 복사되지 않습니다.
능력은 레이어별로 정렬된다
각 엔트리는 필요한 호스트 능력을 modules:에 선언합니다. 레이어드 슬라이스에서 이들은 깔끔하게 정렬됩니다:
persist/*는sql을 선언합니다 — 그 외 어느 것도 데이터베이스에 접근하지 못합니다.service/*는channel과 프로세스 호스트 능력을 선언합니다 — 그 외 어느 것도 스폰하거나 감독하지 못합니다.api/*는 엔드포인트가 요청을 마샬링하는 데 필요한 것을 선언합니다.- 루트 어휘는 아무것도 선언하지 않습니다.
보상은 어떤 능력의 영향 범위든 정확히 하나의 레이어가 된다는 것입니다. 데이터베이스에 쓸 수 있는 모든 것을 알고 싶다면 persist/를 읽으면 됩니다. 의존성 역전이 추상적 원칙이기를 멈추고 grep으로 확인할 수 있는 속성이 됩니다.
애플리케이션과 컴포넌트
같은 형태가 누가 구멍을 채우는가만 바꾸어 단일 앱에서 게시된 라이브러리까지 확장됩니다.
애플리케이션은 최상위의, 배포 가능한 그래프입니다. 구체적인 인프라 — http.service, process.host, 데이터베이스 연결 — 를 루트 네임스페이스(관례상 app) 아래에 소유하고, 모든 것을 스스로 배선합니다.
컴포넌트는 호스트 안으로 마운트되는 게시 가능한 모듈입니다. 호스트의 데이터베이스나 라우터를 알지 못하므로 그 이름을 부를 수 없습니다. 대신 구멍의 인터페이스 — ns.requirement 엔트리 — 를 선언하고, 호스트가 컴포넌트에 의존할 때 이를 채웁니다. 내부적으로 컴포넌트는 애플리케이션 슬라이스와 정확히 같게 구조화됩니다: 같은 레이어, 같은 어휘, 같은 임포트 방향. 유일한 추가는 그 가장자리의 요구사항 인터페이스입니다.
이것은 두 범주가 아니라 스펙트럼입니다:
- 단일 앱, 내부 슬라이스 — 슬라이스가
src/app/아래에 살고,app:db,app:processes를 참조하여 앱의 인프라를 직접 공유합니다. 요구사항 인터페이스가 필요 없습니다; 외부의 어떤 것도 이들을 마운트하지 않습니다. (집중된 서비스는 이렇게 만듭니다.) - 다중 컴포넌트 합성 — 각 컴포넌트는
ns.definition과ns.requirement인터페이스를 가진 자체 게시 가능한 모듈이며, 호스트가ns.dependency를 통해 합성합니다. 호스트는 각 요구사항(데이터베이스, 프로세스 호스트, 라우터)을 한 번씩 채웁니다. (재사용 가능한 부품의 플랫폼은 이렇게 만듭니다.)
슬라이스가 당신이 통제하지 않는 무언가에 의해 소비될 것인지로 선택하세요. 그렇다면 요구사항 인터페이스를 주고 게시하세요. 아니라면 앱의 인프라를 직접 참조하게 하고 형식을 생략하세요. 레이어링은 양 끝에서 불변이고, 패키징이 재사용에 따라 확장되는 부분입니다.
요구사항/의존성 메커니즘은 컴포넌트 구축을, 잠금 파일 측면은 의존성 관리를 참조하세요.
왜 이 형태인가 {#why-this-shape}
위의 규율은 스타일이 아닙니다. 각 규칙은 런타임이 그래프를 합성하고 부팅하는 방식을 지탱합니다:
네임스페이스 경계는 주입 이음새입니다. 레이어가 명시적 imports:를 통해서만 연결되고 별개의 네임스페이스에 살기 때문에, ns.requirement 메커니즘에는 주입할 구체적인 대상이 있습니다 — 호스트는 자신의 데이터베이스를 persist 레이어의 엔트리로, 자신의 프로세스 호스트를 service 레이어의 엔트리로 향하게 합니다. persist가 app:db를 직접 붙잡았다면 컴포넌트는 다른 호스트에 절대 마운트될 수 없습니다: 채울 구멍이 없을 것이기 때문입니다. 레이어링이 컴포넌트를 재배치 가능하게 만드는 것입니다.
단방향 임포트는 부트 순서의 존재를 보장합니다. 런타임은 부트 시 엔트리 그래프를 해결하고 위상 순서를 찾아야 합니다. api → service → persist → root, 절대 옆으로도 위로도 가지 않는 것은 그래프가 구성상 비순환임을 뜻합니다. 공유 부모를 거치는 슬라이스 간 결합은 슬라이스를 로더가 정렬할 수 없는 순환으로 묶는 대신 독립적으로 마운트 가능하게 유지합니다.
레이어별로 스코프된 능력은 영향 범위를 한정합니다. 호스트 능력은 엔트리별로 부여됩니다. persist만 sql을 선언하면, 데이터베이스에 도달할 수 있는 코드의 집합은 앱 전체의 창발적 속성이 아니라 한눈에 감사할 수 있는 하나의 디렉토리입니다.
레이어링은 테스트 가능성의 그라디언트를 만듭니다. 순수 어휘는 아무 세계 없이 테스트됩니다. persist 테스트는 데이터베이스는 건드리지만 워커는 건드리지 않습니다. 그런 다음 모듈 전체의 마운트 테스트가 단위 테스트가 의도적으로 볼 수 없는 이음새를 감사합니다 — 모든 감독 서비스가 실제 프로세스를 가리키는지, 스폰되는 모든 ID가 해석되는지, 모든 요구사항이 채워지는지. 레이어가 실제로 분리 가능할 때만 이 그라디언트를 얻습니다.
짧게 말하면: 여기서의 헥사고날 레이어링은 요구사항 주입, 레이어별 능력 스코핑, 비순환 부트 해결이 동시에 성립하는 유일한 형태입니다. 런타임의 합성 모델은 동작하기 위해 포트와 어댑터 분할을 요구합니다 — 이 규율이 부팅되는 그래프와 다른 누군가가 마운트할 수 있는 컴포넌트를 사주는 것입니다.
참고
- YAML 및 프로젝트 구조 — 파일 형식, 명명, 네임스페이스
- 컴포넌트 구축 —
ns.definition,ns.requirement, 마운트 - 의존성 관리 — 잠금 파일, 모듈 소비
- 레지스트리 — 엔트리가 저장되고 해석되는 방식
- 엔트리 종류 가이드 — 모든 엔트리 종류
- 프로세스 모델 — 서비스, 슈퍼비전, 호스트