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-core0.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가 의존 버전을
선언한다.
[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. 공유 계약¶
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이 종료된다.
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. 호출하는 쪽¶
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이 다룬다.
- 개념을 먼저 확인할 때 — 핵심 개념
- 이름으로 호출하는 경로 — Channel 메시징
- id로 호출하는 상태 객체 — Spot · Actor
- 완결된 업무 흐름을 볼 때 — 샘플 고르기