콘텐츠로 이동

ZLink Framework Java — 사용 가이드

실시간 메시징이 중요한 서버 시스템을 여러 프로세스로 나눠 만드는 Java 애플리케이션 프레임워크다. Spring Boot 위에 그대로 들어가므로 별도 런타임으로 옮겨갈 필요가 없다.

// 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.
//
// A caller that names a routing id in connect(...) compares the advertised
// endpoint against the endpoint it dialed. Both use 127.0.0.1 here.
ZLinkMeshNodeBuilder mesh =
        options.addRouteMesh("game")
                .listen("tcp://127.0.0.1:7501")
                .setAdvertiseHost("127.0.0.1")
                .setRoutingId(RoutingId.from("game-server-1"));

handler 하나를 등록하면 메시지 디코딩·routing·인코딩은 framework가 처리한다.


만드는 시스템

여러 서버 프로세스가 역할을 나눠 협력하고, 상태 변화를 실시간으로 client에 전달해야 하는 시스템에 맞게 설계됐다.

도메인 핵심 시나리오
실시간 게임 room 생성 → player 입장 → 게임 상태 갱신 → client push
고객 지원 채팅 대화 개설 → 상담원 배정 → 메시지 중계 → 대화 상태 push
주문 워크플로 주문 접수 → 단계별 처리 → 상태 변경 → client 알림
배송·배차 배차 요청 → 수행자 배정·수락 → 상태 추적 → 실시간 push

공통 구조는 역할별 서버 프로세스가 typed 메시지로 통신하고, client는 실시간 연결로 상태 변화를 받는 것이다.

↗ 크게 보기


핵심 기능

Channel 메시징 — 서버 간 typed 요청-응답

channel은 서버 사이의 통신 경로에 붙인 이름이다. 한쪽이 channel 이름으로 요청을 보내면 반대편이 처리해 응답한다. 직렬화(JSON · MessagePack · Protobuf)는 framework가 처리한다.

보내는 쪽이다.

@GetMapping("/players/{playerId}/profile")
CompletionStage<Contracts.PlayerProfile> profile(@PathVariable String playerId) {
    // The target is a channel name. Which node answers is decided at call time.
    var profileReq = new Contracts.GetPlayerProfile(playerId);
    return route.requestToChannel("profile", profileReq).submit(Contracts.PlayerProfile.class);
}

받는 쪽이다.

// Answers a request addressed to the "profile" channel. Any node that exposes
// this channel may receive it; the caller does not pick one.
public final class GetPlayerProfileHandler
        implements ZLinkRequestHandler<Contracts.GetPlayerProfile, Contracts.PlayerProfile> {

    @Override
    public CompletionStage<Contracts.PlayerProfile> handle(
            Contracts.GetPlayerProfile request, ZLinkMessageContext context) {
        var profile = new Contracts.PlayerProfile(request.playerId(), "rookie", 1);
        return CompletableFuture.completedFuture(profile);
    }
}

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 Java user Spot names the actor type it can admit. This room admits
// none, so it names the base type and rejects every join. Actors are a later
// chapter.
public final class GameRoom implements ZLinkSpot<ZLinkActor> {

    private final ZLinkSpotContext context;
    private final List<String> chat = new ArrayList<>();

    private String title = "untitled";

    public GameRoom(ZLinkSpotContext context) {
        this.context = context;
        // Handler classes are named here rather than scanned, the same way the
        // channel registrations name theirs. Each takes the target room as its
        // first argument.
        context.handlers().addHandler(PostChatHandler.class);
        context.handlers().addHandler(GetRoomStateHandler.class);
    }

    @Override
    public ZLinkSpotContext context() {
        return context;
    }

    // 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.
    @Override
    public CompletionStage<ZLinkSpotCreateResponse> onCreate(ZLinkMessage request) {
        var body = request.decode(Contracts.OpenRoom.class);
        title = body.title();
        var accepted = ZLinkSpotCreateResponse.accept();
        return CompletableFuture.completedFuture(accepted);
    }

    // No actor ever joins this room, so the three membership callbacks below say
    // so and do nothing else.
    @Override
    public CompletionStage<ZLinkSpotActorJoinResult> onActorJoin(
            String actorId, ZLinkMessage request) {
        var rejected = ZLinkSpotActorJoinResult.reject();
        return CompletableFuture.completedFuture(rejected);
    }

    @Override
    public CompletionStage<Void> onJoinedActor(ZLinkActor actor) {
        return CompletableFuture.completedFuture(null);
    }

    @Override
    public CompletionStage<Void> onLeaveActor(ZLinkActor actor) {
        return CompletableFuture.completedFuture(null);
    }

    void append(String line) {
        chat.add(line);
    }

    Contracts.RoomState state() {
        return new Contracts.RoomState(title, List.copyOf(chat));
    }
}

배정을 담당하는 entry spot(node마다 하나)과 상태 본체인 room spot(단위마다 하나)으로 나뉜다. 주기 작업은 timer로 등록한다. Spot →


STREAM과 Actor — client 실시간 연결

client의 실시간 양방향 연결을 STREAM이라 하고, 연결 하나를 대표하는 서버 쪽 객체가 Actor다. client가 접속하면 session이 Actor를 만들고, Actor는 Spot에 입장해 상태 처리에 참가한다.

ZLinkSessionActor player = dispatch.actor();
if (player == null) {
    var bound = context.actors().bound();
    player =
            switch (bound.size()) {
                case 1 -> bound.getFirst();
                case 0 ->
                        throw new IllegalStateException(
                                "Authenticate an Actor before sending player packets.");
                default ->
                        throw new IllegalStateException(
                                "Select an Actor handle when more than one Actor is bound.");
            };
}
return player.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.
var locationOptions =
        new ZLinkRedisLocationOptions()
                .setConnectionString("127.0.0.1:6379")
                .setKeyPrefix("zlink-tutorial-java:location:");
options.addLocationStore(new ZLinkRedisLocationStore(locationOptions));

Location →


Spring Boot가 제공하는 것

DI container, configuration, HTTP endpoint, logging은 Spring Boot의 것을 그대로 사용한다. 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장은 설치 방법과 표면 이름이 언어마다 달라 Java 전용으로 따로 작성되어 있다. 위 표의 링크에서 각 장을 바로 연다.

다이어그램 읽는 법

이 가이드의 다이어그램은 같은 시각 언어를 사용한다 — 색이 곧 개념이다.

↗ 크게 보기

여러 장이 같은 토폴로지를 그리며, 장마다 확대하는 위치만 바뀐다.

관련 문서