Kotlin Quickstart — 설치부터 첫 요청까지¶
이 장을 읽고 나면
패키지를 설치하고, 두 process가 서로 호출하는 최소 project를 실행할 수 있다.
저장소의 framework/languages/java/quickstart/kotlin/
프로젝트다. 아래 코드 블록은 사이트를 빌드할 때 그 파일에서 읽는다. Java판과 Kotlin판이 한
Gradle 빌드 안에 있다. location store 없이 process 둘이 서로의 endpoint를 직접 지정해
request/reply 한 번을 주고받는다.
0. 예제 저장소 clone¶
이 장의 project는 zlink-kotlin-examples 저장소의 quickstart/이며, 기능 가이드가 읽는
program인 tutorial/과 samples/도 같은 저장소에 있다.
git clone https://github.com/zlink-systems/zlink-kotlin-examples.git
cd zlink-kotlin-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")
// coroutine idiom
implementation("systems.zlink:zlink-framework-kotlin")
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.
data class Hello(val name: String)
data class Greeting(val text: String)
3. 처리하는 쪽¶
addHandlersFromPackageOf는 handler 타입을 찾기만 한다. 어느 channel에 노출할지는
channelName(...).server()에 따로 등록한다.
HTTP를 열지 않는 process라 SpringApplication의 isKeepAlive를 true로 두어야 한다. 없으면
context refresh 직후 JVM이 종료된다.
package systems.zlink.quickstart.server
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
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.kotlin.addHandlersFromPackageOf
import systems.zlink.framework.kotlin.addRequestHandler
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
class ServerApplication {
@Bean
fun zlink(): ZLinkFrameworkConfigurer = ZLinkFrameworkConfigurer { options ->
// Discovers handler types.
options.addHandlersFromPackageOf<ServerApplication>()
// Names the mesh.
val 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, Hello, Greeting>()
}
}
fun main(args: Array<String>) {
// This process serves no HTTP; without setKeepAlive the JVM exits as soon
// as the context finishes refreshing.
val app = SpringApplication(ServerApplication::class.java)
app.isKeepAlive = true
app.run(*args)
}
// Handles one request on the "greeting" channel. ZLinkRequestHandler returns a
// CompletionStage; a Kotlin suspend fun cannot override it. See ../../../README.md.
class HelloHandler : ZLinkRequestHandler<Hello, Greeting> {
override fun handle(request: Hello, context: ZLinkMessageContext): CompletionStage<Greeting> =
CompletableFuture.completedFuture(Greeting("hello, ${request.name}"))
}
4. 호출하는 쪽¶
package systems.zlink.quickstart.client
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
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.kotlin.kotlin
import systems.zlink.framework.kotlin.requestToChannel
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
class ClientApplication {
@Bean
fun zlink(): ZLinkFrameworkConfigurer = ZLinkFrameworkConfigurer { options ->
// This process also needs its own endpoint.
val 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")
}
}
fun main(args: Array<String>) {
runApplication<ClientApplication>(*args)
}
@RestController
class HelloController(routeClient: ZLinkRouteClient) {
private val route = routeClient.kotlin()
@GetMapping("/hello/{name}")
suspend fun hello(@PathVariable name: String): String =
// The target is a single ChannelName; which node handles it is not specified.
route.requestToChannel<Greeting>("greeting", Hello(name)).await().text
}
5. 실행¶
cd zlink-kotlin-examples/quickstart
./gradlew :kotlin:Server:installDist :kotlin:Client:installDist
# 터미널 두 개. server를 먼저 실행한다.
./kotlin/Server/build/install/Server/bin/Server
./kotlin/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가 바로 종료된다 | isKeepAlive를 true로 두었는지 확인한다 |
| 호출이 대상 없음으로 끝난다 | 받는 쪽이 그 channel 이름을 server 역할로 등록했는지, 두 process가 peer로 연결됐는지 확인한다 |
| 응답이 오지 않는다 | 보낸 쪽이 request를 썼는지 확인한다. send는 응답을 받지 않는다 |
7. 옮겨 갈 것¶
| 파일 | 내용 |
|---|---|
gradle/libs.versions.toml |
zlinkFramework 한 항목이 세 artifact를 고정한다. binding은 전이 의존으로 둔다 |
| Shared | 두 process가 공유하는 메시지 타입 |
| Server | addRouteMesh → listen → channelName(...).server().addRequestHandler(...), 그리고 isKeepAlive = true |
| Client | channelName(...).client(), peerConnections().connect(...), requestToChannel(...) |
빌드 설정에서 더 필요한 항목은 프로젝트의 README.md가 정리한다.
8. 다음으로 읽을 것¶
이 두 process는 endpoint를 서로 직접 적어 연결한다. 서버를 늘리거나 다른 주소로 다시 시작해도 호출 코드를 그대로 두려면 자동 연결이 필요하고, 그것은 Location이 다룬다.
- 개념을 먼저 확인할 때 — 핵심 개념
- 이름으로 호출하는 경로 — Channel 메시징
- id로 호출하는 상태 객체 — Spot · Actor
- 완결된 업무 흐름을 볼 때 — 샘플 고르기