콘텐츠로 이동

Java Quickstart — 설치부터 첫 요청까지

이 장을 읽고 나면

패키지를 설치하고, 두 process가 서로 호출하는 최소 project를 실행할 수 있다.

저장소의 framework/languages/java/quickstart/java/ 프로젝트다. 아래 코드 블록은 사이트를 빌드할 때 그 파일에서 읽는다. Java판과 Kotlin판이 한 Gradle 빌드 안에 있다. location store 없이 process 둘이 서로의 endpoint를 직접 지정해 request/reply 한 번을 주고받는다.

0. 예제 저장소 clone

이 장의 project는 zlink-java-examples 저장소의 quickstart/이며, 기능 가이드가 읽는 program인 tutorial/과 samples/도 같은 저장소에 있다.

git clone https://github.com/zlink-systems/zlink-java-examples.git
cd zlink-java-examples/quickstart

main은 최신 릴리스에 그 뒤의 수정을 더한 것이고, 패키지 버전은 그 릴리스에 맞춰져 있다. 이전 릴리스는 tag vA.B.C로 받는다(git checkout vA.B.C). 이슈와 PR은 zlink-systems/zlink로 보낸다.

1. 설치

  • JDK 25 이상. 게시된 zlink-framework-core 0.16.0의 class file major는 69(Java 25)다
  • Maven Central 접근

Maven Central에서 받는다. 서버 하나를 만들 때 필요한 최소 조합은 다음과 같다.

// 계약과 runtime
implementation("systems.zlink:zlink-framework-core")
// DI·수명주기 등록
implementation("systems.zlink:zlink-framework-spring-boot-starter")

systems.zlink:zlink(binding)는 적지 않는다. zlink-framework-core가 의존 버전을 선언한다.

gradle/libs.versions.toml
[versions]
# 세 zlink-framework-* artifact를 한 번에 고정한다.
zlinkFramework = "0.30.0"
# 아래 셋은 zlink-framework POM이 선언한 값과 맞춘 것이다.
# 충돌 해석에 맡기지 않는다.
springBoot = "3.5.14"
kotlin = "2.3.21"
jackson = "2.17.2"
# systems.zlink:zlink(binding)는 적지 않는다. zlink-framework-core의 POM이 정한다.

[libraries]
zlink-framework-core = { module = "systems.zlink:zlink-framework-core", version.ref = "zlinkFramework" }
zlink-framework-spring-boot-starter = { module = "systems.zlink:zlink-framework-spring-boot-starter", version.ref = "zlinkFramework" }
zlink-framework-kotlin = { module = "systems.zlink:zlink-framework-kotlin", version.ref = "zlinkFramework" }
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springBoot" }
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "springBoot" }
# Kotlin data class를 기본 JSON codec이 다루려면 필요하다. Kotlin Server는 web starter가
# 없어 전이로 들어오지 않는다.
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
# Spring이 @Configuration·@RestController를 CGLIB로 감싸는데 Kotlin class는 기본이 final이다.
# 이 플러그인이 해당 class를 열어 준다.
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }

필요할 때 추가하는 artifact는 다음과 같다.

artifact 언제 추가하나
zlink-framework-locations-redis Redis location store로 자동 연결을 사용할 때(Location)
zlink-framework-codec-protobuf · -codec-msgpack 기본 JSON codec 대신 사용할 때(Handler와 메시지 처리)
zlink-stream-connector 외부 client(게임 client·모바일)를 만들 때(STREAM)
zlink-http-client 서버에서 HTTP를 호출할 때(HTTP Client 가이드)

라이선스는 계층마다 다르다 — core·binding은 MPL-2.0, framework는 FSL-1.1-ALv2, zlink-http-client는 Apache-2.0이다. 서비스를 만들어 파는 데 드는 비용은 없다.

2. 공유 계약

Shared
package systems.zlink.quickstart.shared;

// Request/reply contract shared by the server and client processes.
public record Hello(String name) {}

3. 처리하는 쪽

addHandlersFromPackageOf는 handler 타입을 찾기만 한다. 어느 channel에 노출할지는 channelName(...).server()에 따로 등록한다.

HTTP를 열지 않는 process라 SpringApplication.setKeepAlive(true)가 필요하다. 없으면 context refresh 직후 JVM이 종료된다.

Server
package systems.zlink.quickstart.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import systems.zlink.framework.ZLinkMessageContext;
import systems.zlink.framework.channels.ZLinkRequestHandler;
import systems.zlink.framework.configuration.ZLinkMeshNodeBuilder;
import systems.zlink.framework.spring.EnableZLinkFramework;
import systems.zlink.framework.spring.ZLinkFrameworkConfigurer;
import systems.zlink.quickstart.shared.Greeting;
import systems.zlink.quickstart.shared.Hello;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

@EnableZLinkFramework
@SpringBootApplication
public class ServerApplication {

    public static void main(String[] args) {
        // This process serves no HTTP; without setKeepAlive the JVM exits as
        // soon as the context finishes refreshing.
        SpringApplication app = new SpringApplication(ServerApplication.class);
        app.setKeepAlive(true);
        app.run(args);
    }

    @Bean
    ZLinkFrameworkConfigurer zlink() {
        return options -> {
            // Discovers handler types.
            options.addHandlersFromPackageOf(ServerApplication.class);

            // Names the mesh.
            ZLinkMeshNodeBuilder mesh =
                    options.addRouteMesh("services")
                            // This process's own endpoint, for peers to connect to.
                            .listen("tcp://127.0.0.1:7101");
            // This process handles the "greeting" channel.
            mesh.channelName("greeting")
                    .server()
                    .addRequestHandler(HelloHandler.class, Hello.class, Greeting.class);
        };
    }
}

// Handles one request on the "greeting" channel.
final class HelloHandler implements ZLinkRequestHandler<Hello, Greeting> {

    @Override
    public CompletionStage<Greeting> handle(Hello request, ZLinkMessageContext context) {
        return CompletableFuture.completedFuture(new Greeting("hello, " + request.name()));
    }
}

4. 호출하는 쪽

Client
package systems.zlink.quickstart.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import systems.zlink.framework.channels.ZLinkRouteClient;
import systems.zlink.framework.configuration.ZLinkMeshNodeBuilder;
import systems.zlink.framework.spring.EnableZLinkFramework;
import systems.zlink.framework.spring.ZLinkFrameworkConfigurer;
import systems.zlink.quickstart.shared.Greeting;
import systems.zlink.quickstart.shared.Hello;

@EnableZLinkFramework
@SpringBootApplication
public class ClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }

    @Bean
    ZLinkFrameworkConfigurer zlink() {
        return options -> {
            // This process also needs its own endpoint.
            ZLinkMeshNodeBuilder mesh =
                    options.addRouteMesh("services").listen("tcp://127.0.0.1:7102");
            // This side only calls; it does not handle "greeting".
            mesh.channelName("greeting").client();
            // Manual connection -- the server's endpoint is given directly.
            mesh.peerConnections().connect("tcp://127.0.0.1:7101");
        };
    }
}

@RestController
class HelloController {
    private final ZLinkRouteClient route;

    HelloController(ZLinkRouteClient route) {
        this.route = route;
    }

    @GetMapping("/hello/{name}")
    String hello(@PathVariable String name) {
        // The target is a single ChannelName; which node handles it is not specified.
        // submit_sync blocks this application thread until the reply arrives. A request
        // handler runs on an application thread, not a runtime execution context, so
        // blocking here is allowed.
        return route.requestToChannel("greeting", new Hello(name))
                .submit_sync(Greeting.class)
                .text();
    }
}

5. 실행

cd zlink-java-examples/quickstart
./gradlew :java:Server:installDist :java:Client:installDist

# 터미널 두 개. server를 먼저 실행한다.
./java/Server/build/install/Server/bin/Server
./java/Client/build/install/Client/bin/Client

curl http://127.0.0.1:5080/hello/world

응답은 hello, world, 상태 코드 200이다.

6. 첫 실행이 안 될 때 확인할 항목

증상 확인할 항목
artifact를 찾지 못한다 §1의 artifact 이름을 그대로 적었는지 확인한다. binding 버전은 따로 고정하지 않는다
startup이 실패한다 두 process의 mesh 이름이 같은지, listen endpoint가 다른 process와 겹치지 않는지 확인한다
server가 바로 종료된다 SpringApplication.setKeepAlive(true)를 호출했는지 확인한다
호출이 대상 없음으로 끝난다 받는 쪽이 그 channel 이름을 server 역할로 등록했는지, 두 process가 peer로 연결됐는지 확인한다
응답이 오지 않는다 보낸 쪽이 request를 썼는지 확인한다. send는 응답을 받지 않는다

7. 옮겨 갈 것

파일 내용
gradle/libs.versions.toml zlinkFramework 한 항목이 세 artifact를 고정한다. binding은 전이 의존으로 둔다
Shared 두 process가 공유하는 메시지 타입
Server addRouteMesh → listen → channelName(...).server().addRequestHandler(...), 그리고 setKeepAlive(true)
Client channelName(...).client(), peerConnections().connect(...), requestToChannel(...)

빌드 설정에서 더 필요한 항목은 프로젝트의 README.md가 정리한다.

8. 다음으로 읽을 것

이 두 process는 endpoint를 서로 직접 적어 연결한다. 서버를 늘리거나 다른 주소로 다시 시작해도 호출 코드를 그대로 두려면 자동 연결이 필요하고, 그것은 Location이 다룬다.