ZLink Framework Node.js — 사용 가이드¶
다른 언어로 보기 — C++ · C#/.NET · Java · Kotlin · Node/TypeScript
실시간 메시징이 중요한 서버 시스템을 여러 프로세스로 나눠 만드는 Node.js 애플리케이션 프레임워크다. NestJS 위에 그대로 들어가므로 별도 런타임으로 옮겨갈 필요가 없다.
// Both sides must name the mesh identically, or they never see each other
// as peers. The routing id names this node; without it the Framework assigns
// a generated one, which a caller cannot type into a URL. This local
// example binds and advertises a loopback endpoint.
const mesh = builder
.addRouteMesh(TutorialNames.mesh)
.listen('tcp://127.0.0.1:7701')
.setAdvertiseHost('127.0.0.1')
.routingId(TutorialNames.serverRoutingId);
handler 하나를 등록하면 메시지 디코딩·routing·인코딩은 framework가 처리한다.
만드는 시스템¶
여러 서버 프로세스가 역할을 나눠 협력하고, 상태 변화를 실시간으로 client에 전달해야 하는 시스템에 맞게 설계됐다.
| 도메인 | 핵심 시나리오 |
|---|---|
| 실시간 게임 | room 생성 → player 입장 → 게임 상태 갱신 → client push |
| 고객 지원 채팅 | 대화 개설 → 상담원 배정 → 메시지 중계 → 대화 상태 push |
| 주문 워크플로 | 주문 접수 → 단계별 처리 → 상태 변경 → client 알림 |
| 배송·배차 | 배차 요청 → 수행자 배정·수락 → 상태 추적 → 실시간 push |
공통 구조는 역할별 서버 프로세스가 typed 메시지로 통신하고, client는 실시간 연결로 상태 변화를 받는 것이다.
핵심 기능¶
Channel 메시징 — 서버 간 typed 요청-응답¶
channel은 서버 사이의 통신 경로에 붙인 이름이다. 한쪽이 channel 이름으로 요청을 보내면 반대편이 처리해 응답한다. 직렬화(JSON · MessagePack · Protobuf)는 framework가 처리한다.
보내는 쪽이다.
map('GET', /^\/players\/([^/]+)\/profile$/, async ([playerId]) => {
// The target is a channel name. Which node answers is decided at call time.
const profile = await route
.requestToChannel(TutorialNames.profileChannel, new GetPlayerProfile(playerId))
.submit<PlayerProfile>();
return ok(profile);
});
받는 쪽이다.
// Answers a request addressed to the "profile" channel. Any node that exposes
// this channel may receive it; the caller does not pick one.
@Injectable()
export class GetPlayerProfileHandler implements ZLinkRequestHandler<
GetPlayerProfile,
PlayerProfile
> {
async handle(request: GetPlayerProfile, context: ZLinkMessageContext): Promise<PlayerProfile> {
void context;
return {
playerId: request.playerId,
nickname: 'rookie',
level: 1
};
}
}
request-reply 외에 fanout(pub/sub)과 route mesh(주소 라우팅) 패턴도 제공한다. Channel 메시징 →
Spot — 상태 단위를 lock 없이 관리¶
Spot은 게임 room, 지원 대화, 주문 처리 단위처럼 상태 영역 하나와 그 참가자를 묶는 실행 단위다. 한 Spot 안에서 일어나는 일 — 참가자 packet, timer, 입장과 퇴장 — 은 모두 직렬로 처리된다. lock 없이 상태에 접근하며, 비동기 처리를 사용해도 같은 Spot에 두 요청이 겹치지 않는다.
// A room owns its own state and is addressed by a global SpotId. Messages sent
// to one room run one at a time, so the fields below need no synchronization.
//
// Every Node user Spot names the actor type it can admit. This room admits
// none, so it takes the default type and rejects every join. Actors are a later
// chapter. The scope is TRANSIENT because the Framework builds one instance per
// room.
@Injectable({ scope: Scope.TRANSIENT })
class GameRoom implements ZLinkSpot {
readonly context!: ZLinkSpotContext;
private title = 'untitled';
private readonly chat: string[] = [];
// Runs before the room accepts any message. Rejecting here means the create
// call fails and no room exists. Omit this method to accept every request.
async onCreate(request: ZLinkMessage): Promise<ZLinkSpotCreateResponse> {
const body = request.decode<OpenRoom>();
this.title = body.title;
return { accepted: true };
}
append(line: string): void {
this.chat.push(line);
}
state(): RoomState {
return {
title: this.title,
chat: [...this.chat]
};
}
// No actor ever joins this room, so the three membership callbacks below say
// so and do nothing else.
async onActorJoin(): Promise<ZLinkSpotActorJoinResult> {
return { accepted: false };
}
async onJoinedActor(): Promise<void> {}
async onLeaveActor(): Promise<void> {}
}
배정을 담당하는 entry spot(node마다 하나)과 상태 본체인 room spot(단위마다 하나)으로 나뉜다. 주기 작업은 timer로 등록한다. Spot →
STREAM과 Actor — client 실시간 연결¶
client의 실시간 양방향 연결을 STREAM이라 하고, 연결 하나를 대표하는 서버 쪽 객체가 Actor다. client가 접속하면 session이 Actor를 만들고, Actor는 Spot에 입장해 상태 처리에 참가한다.
// A packet with an Actor slot selects its binding. A packet without one
// can use the connection only when exactly one Actor is bound.
const bound = this.context.actors.bound;
const actor = dispatch.actor ?? (bound.length === 1 ? bound[0] : undefined);
if (!actor) {
throw new Error(
bound.length === 0
? 'Authenticate an Actor before sending player packets.'
: 'Select an Actor handle when more than one Actor is bound.'
);
}
await actor.relay(dispatch, payload);
client 쪽 접속은 별도 산출물인 stream connector가 담당한다. STREAM → · Session과 Actor 연결 →
Location — endpoint를 코드에 적지 않는다¶
같은 역할의 서버 여러 대가 떠 있을 때 어느 쪽으로 연결할지 주소를 코드에 적지 않는다. 공용 location store가 주소를 관리하고, 각 서버는 id로 지금 있는 node를 조회한다.
// Rooms are addressed by id, not by host, so their current location is
// kept here. Every node reads and writes the same store under the same
// prefix.
builder.addLocationStore(
new ZLinkRedisLocationStore({
url: 'redis://127.0.0.1:6379',
keyPrefix: 'zlink-tutorial-node:location'
})
);
NestJS가 제공하는 것¶
DI container, configuration, HTTP endpoint, logging은 NestJS의 것을 그대로 사용한다. ZLink Framework는 그 위에 handler와 mesh를 등록하므로, 같은 프로세스 안에서 REST endpoint와 실시간 연결이 함께 동작한다.
목차¶
| 순서 | 문서 | 내용 |
|---|---|---|
| 1 | 개요 | 핵심 표면, 계층 구조, topology를 빠르게 파악하기 |
| 2 | 둘러보기 | MORPG 하나로 기능별 동작을 직접 눌러 보며 확인하기 |
| 3 | 퀵스타트 | 설치, 두 process가 서로 호출하는 최소 project, 첫 실행 점검 |
| 4 | 핵심 개념 | channel·Spot·Actor·session이 각각 무엇인가 |
| 5 | Channel 메시징 | 이름으로 부르는 경로 — 등록과 호출 |
| 6 | Spot | 여럿이 함께 사용하는 자리를 id로 만들고 부르기 |
| 7 | Actor | 개체 하나를 id로 만들고 부르기 |
| 8 | STREAM | mesh 밖의 client가 연결 하나로 붙기 |
| 9 | Session과 Actor 연결 | 연결 하나를 Actor 하나에 묶기 |
| 10 | Location | id로 지금 있는 node를 조회하기 |
| 11 | 모니터링 | 기능별 가이드 자리 표시 — 본문은 아직 없다 |
| 12 | 실행 모델 | 두 queue, 직렬화 범위, 실행권 |
| 13 | Backpressure | 처리보다 도착이 빠를 때와 영향을 주는 옵션 |
| 14 | 활성화와 수명 | 종류별 생성 시점, lifecycle callback, 주입 수명 |
| 15 | Actor membership | Spot 사이 이동, 예약과 상한 |
| 16 | Timer와 worker | 주기 실행, 줄 밖 실행, 실행권 반납 |
| 17 | Relocation | 옮겨도 남는 것, adapter, 이동 단위 |
| 18 | Channel 동작 원리 | 패턴 차이, 대상 선택, pub/sub, 연결과 discovery |
| 19 | Handler와 메시지 처리 | 등록 변형, filter, codec, handler 종류 |
| 20 | STREAM의 동작 원리 | 등록 검증, 오류 귀속, 응답 token, 실행 방식 |
| 21 | Session 묶음의 동작 원리 | 묶는 개수, 경로 갱신, 끊김 통지, 실패 |
| 22 | 운영과 lifecycle | 런타임 메트릭, relocate, drain, readiness 연결 |
| 23 | Options | 옵션 목록, 기본값과 바꾸는 시점 |
| 24 | 샘플 고르기 | 어떤 샘플을 먼저 볼지 고르고 실행하는 방법 |
| 25 | Bingo 따라 읽기 | 인증·매칭·room·timer·multicast·cleanup을 코드 순서로 |
| 26 | TicTacToe 따라 읽기 | 수동 연결·수동 등록, 다른 node로의 Actor join |
| 27 | SupportChat 따라 읽기 | session 하나에 Actor 여럿, metadata relay, idle timer |
| 28 | DeliveryDispatch 따라 읽기 | one-way send, deadline 기록과 재배정, 고객 push |
| 29 | ShoppingMall 따라 읽기 | owner Instance Spot, replay·다음 단계·expected version |
| 30 | GameQuest 따라 읽기 | player별 owner, best-effort push와 보정 |
| 31 | ZoneWorld 따라 읽기 | capacity placement, 경계 join과 relocation, fanout·관찰 |
| 32 | E2E 테스트 | client library로 시스템 전체를 검증하기 |
| 33 | 주요 타입 사용 색인 | 계약 인터페이스를 검증 코드로 색인 |
| 34 | 모니터링 | 재작성 대기 — 상태 snapshot과 진단 |
파일 번호는 언어에 상관없이 같은 장을 가리키는 식별자다. 읽는 순서는 이 표가 정한다.
01 · 11 · 13 · 16장은 설치 방법과 표면 이름이 언어마다 달라 Node.js 전용으로 따로 작성되어 있다. 위 표의 링크에서 각 장을 바로 연다.
다이어그램 읽는 법¶
이 가이드의 다이어그램은 같은 시각 언어를 사용한다 — 색이 곧 개념이다.
여러 장이 같은 토폴로지를 그리며, 장마다 확대하는 위치만 바뀐다.
관련 문서¶
- 공개 계약: Node.js 공개 계약
- 언어 중립 의미: 공통 스펙
- client library: HTTP client · Stream connector