본문 바로가기

Java/Vert.x

버텍스 코어(Vert.x Core) 시작하기

버텍스 코어(Vert.x Core) API

버텍스 프로그램 개발을 위한 핵심 Java API는 버텍스 코어(Vert.x Core:Repository)입니다.

  • TCP 클라이언트 및 서버
  • 웹 소켓을 포함한 HTTP 클라이언트 및 서버
  • 이벤트 버스
  • 공유 데이터(Local maps 및 clustered distributed maps
  • 타이머
  • Verticle 배포 및 종료
  • 데이터그램(UDP) 소켓
  • DNS 클라이언트
  • File system 접근
  • 고가용성
  • Native transforts
  • 클러스터링

코어 API에서 제공하능 기능 대부분은 Low-level에서 동작합니다.

데이터 베이스 엑세스, 권한 부여 및 High-level의 웹 기능 등은 코어에서 다루지 않습니다(Vert.x ext-extensions 참고).

코어 API 사용을 위해서 Maven 또는 Gradle에서 다음 의존성을 추가합니다.

  • Maven(pom.xml):
<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-core</artifactId>
 <version>3.9.16</version>
</dependency>
  • Gradle(build.gradle):
dependencies {
 compile 'io.vertx:vertx-core:3.9.16'
}

버텍스 객체 생성

버텍스의 모든 기능은 Vertx(버텍스 객체)로부터 실행됩니다(HTTP 클라이언트 및 서버를 생성하거나, 이벤트 버스에 대한 객체 참조를 얻거나, 타이머를 실행 하는 등).

새로운 버텍스 객체를 생성하려면 다음과 같이 작성합니다.

Vertx vertx = Vertx.vertx();
더보기

버텍스 애플리케이션 대부분은 하나의 버텍스 객체만을 필요로합니다.

여러 개의 Vertx 객체를 사용하여 프로그램을 개발하는 경우는, 이벤트 버스와 같은 코어 API의 기능을 서로 다른 환경으로 분리하는 등의 목적이 있을 수 있습니다-이와 같은 방법은 일반적이지 않으므로, 우선 하나의 Vertx 객체만을 사용하면 됩니다.

버텍스 객체를 생성하면서 애플리케이션에 필요로 하는 중요 옵션을 전달 할 수 있습니다.

별도 옵션을 전달하지 않으면 버텍스 객체는 API가 제공하는 디폴트 옵션을 사용합니다.

VertxOptions 객체를 사용하여 클러스터링, 고가용성, 풀 사이즈 등의 다양한 프로퍼티를 조절하게 됩니다.

Vertx vertx = Vertx.vertx(new VertxOptions().setWorkerPoolSize(40));

버텍스 애플리케이션에서 클러스터링을 실행하려면 버텍스 객체를 사용합니다(자세한 내용은 Eclipse: Vertx.io Core Manual의 이벤트 버스 섹션을 참고).

기존 버텍스 객체 생성과 다른 점은 비동기 함수를 호출한다는 점입니다.

클러스터 내에서 여러 개의 버텍스 객체들이 그룹화 되는데 필요한 시간(아마 몇 초 내외로, 이때 대기 시간동안 스레드 차단을 원하지 않는다면) 때문입니다.

com.hazelcast.config.Config hazelcastConfig = ConfigUtil.loadConfig();
HazelcastClusterManager hazelcastClusterManager = new HazelcastClusterManager();
hazelcastClusterManager.setConfig(hazelcastConfig);
...
VertxOptions options = new VertxOptions();
options.setClusterManager(hazelcastClusterManager)
options.setClustered(true);
Vertx.clusteredVertx(options, clusteredVertx -> {
	if (clusteredVertx.succeeded()) {
		this.vertx = clusteredVertx.result();
    }
});

버텍스는 기본적으로 메소드 체이닝 패턴을 지향합니다(fluent API 기반의).

예를 들어: 메소드 체이닝 패턴을 사용하여 간략화 될 수 있으며, 코드 가독성과 개발 속도를 높여줍니다.

request.response().putHeader("Content-Type", "text/plain").write("some text").end();

반응형-리액터(Reactor)와 멀티 리액터(Multi-Reactor) 패턴

버텍스 애플리케이션은 이벤트 드리븐(Event driven) 방식으로 동작합니다.

버텍스 애플리케이션에 어떤 작업이 실행되고자 하면, 우리 코드를 버텍스가 직접 호출(콜백-Callback)하게 됩니다.

  • 타이머의 실행
  • 소캣으로부터 데이터 수신
  • 디스크로부터 파일 읽기 완료
  • 예외(Exception) 발생
  • HTTP 서버가 요청을 수신 등

버텍스 API의 handler를 통해서 수신하게 됩니다.

예를 들어 1초 간격으로 반복되는 타이머는 버텍스로부터 다음과 같이 콜백 받습니다.

vertx.setPeriodic(1000, id -> {
	// This handler will get called every second
	System.out.println("timer fired!");
});

또는 HTTP 서버가 요청을 수신하는 경우, 버텍스가 handler를 통해서 이벤트가 실행되어야 하는 코드를 비동기 콜백-Asyncronously callback합니다.

이같은 이벤트 드리븐 방식은 버텍스에서 가장 중요한 개념입니다.

server.requestHandler(request -> {
	// This handler will be called every time an HTTP request is received at the server
	request.response().end("hello world!");
});

예외적인 경우를 제외하면, 버텍스가 제공하는 어떤 API도 스레드를 차단하지 않습니다.

  • 코드 실행이 즉각적인 결과를 나타낸다면, 즉시 리턴하게 됩니다.
  • 코드 실행을 위해서 대기해야 된다면, 이벤트를 수신하기 위해 handler를 제공해야 합니다.
더보기

버텍스 API가 스레드를 차단하지 않기 때문에, 적은 수의 스레드를 사용하더라도 높은 병렬성을 가질 수 있습니다.

따라서 전통적인 스레드를 차단하는 코드는 지양해야 합니다(전통적인 코드를 사용하여 소캣으로부터 데이터 수신 대기 등).

스레드가 차단되면 그 시간동안에 다른 작업을 수행하지 못합니다.

차단 코드가 실행되는 동안 더 많은 작업을 처리하려면(전통적인 방식에서는) 더 많은 스레드를 실행해야 합니다.

더 많은 스레드가 실행됨은 Stack 메모리 사용량이 높아지고 컨텍스트 스위칭(Context switcihg)이 더 잦은 간격으로 발생할 수 있다는 오버헤드가 발생하게 됩니다.

버텍스는 이벤트가 실행 가능할 때 handler에 이벤트를 전달합니다.

이 역할을 수행하는 것을 이벤트 루프(Event loop) 스레드라고 부릅니다.

차단 코드가 실행되지 않는한 이벤트 루프 스레드는 handler가 도착할 때마다 이벤트를 빠르게 전달합니다.

이를 리액터 패턴(Reactor pattern)이라고 부릅니다.

표준적인 리액터 패턴(Node.js와 같은)에서는 하나의 이벤트 루프 스레드를 사용하며, 모든 이벤트가 하나의 고정된 스레드에서 다른 스레드로 전달됩니다.

이벤트 루프 스레드의 병목을 해결하기 위해서는 표준적인 리액터 패턴에서는 여러 개의 프로세스를 실행하는 방법이 있습니다.

반면 버텍스는 다중 리액터 패턴(Multi-Reactor pattern)으로 동작합니다.

버텍스 객체 하나는 복수 개의 이벤트 루프 스레드를 사용하여 이벤트를 handler에 전달합니다.

https://vertx.io/introduction-to-vertx-and-reactive/

Default 프로퍼티는 버텍스 어플리케이션이 실행되는 머신의 코어 개수이며, 이 값은 io.vertx.core에서 조절 가능합니다.

더보기

버텍스 객체 하나가 여러 개의 이벤트 루프 스레드를 사용하더라도, 각 handler는 정해진 이벤트 루프 스레드에서만 실행됩니다(최초 콜백된 이벤트 루프 스레드에 의해서 점유됨).

따라서 다중 리액터 패턴이면서 하나의 이벤트는 싱글 스레드에서처럼 동작합니다(워커 버티클-Worker verticle로 지정하면 예외적으로 여러 이벤트 루프에서 실행되도록 할 수 있습니다). 

이벤트 루프 스레드 차단

버텍스 API는 이벤트 루프 스레드를 차단하지 않지만, 우리가 작성하는 handler가 스레드를 차단하지 않도록 주의해야합니다.

그렇게 되면 이벤트 루프 스레드가 차단되는 동안에는 다른 이벤트가 실행되지 못한채 대기하기 때문입니다.

  • Thread.sleep() 실행
  • Lock을 위해 대기
  • Mutex 또는 Monitor를 위해 대기
  • 데이터베이스 동작에 대한 결과 대기-수명이 긴
  • 복잡한 연산을 위한 상당한 시간 소요
  • 오랜 시간이 걸리는 반복문 실행 등

차단 시간이라는 것은 추상적이며, 애플리케이션이 서비스되는 환경에 따라서 차이가 있을 수 있습니다.

1초에 10000개의 HTTP 요청을 실행해야 하는 애플리케이션은 요청 하나를 0.1ms 안에 응답해야 하므로 그 이상 차단되서는 안됩니다.

만약 애플리케이션이 응답하고 있지 않다면 어딘가에서 이벤트 루프 스레드가 차단되고 있음을 의미합니다.

이 같은 문제를 진단하기 위해 버텍스는 이벤트 루프가 한동안 리턴되지 않으면 자동으로 경고 로그(Stack traces와 함께)를 출력합니다.

Thread vertx-eventloop-thread-3 has been blocked for 20458 ms
더보기

이벤트 루프 스레드 차단 경고를 끄거나 경고 로그의 표시 기준 시간, 그리고 표시되는 시간 간격 등을 조절하기 위해서면 io.vertx.core.VertxOptions을 사용합니다.

이벤트 루프 스레드의 차단은 버텍스 애플리케이션의 치명적인 오류로 볼 수 있으므로 가급적 차단 경고는 활성화 상태를 유지합니다.

/**
* The default number of threads in the internal blocking  pool (used by some internal operations) = 20
*/
public static final int DEFAULT_INTERNAL_BLOCKING_POOL_SIZE = 20;

/**
* The default value of blocked thread check interval = 1000 ms.
*/
public static final long DEFAULT_BLOCKED_THREAD_CHECK_INTERVAL = 1000;

/**
* The default value of blocked thread check interval unit = TimeUnit.NANOSECONDS
*/
public static final TimeUnit DEFAULT_BLOCKED_THREAD_CHECK_INTERVAL_UNIT = TimeUnit.MILLISECONDS;

이벤트 루프 스레드를 차단하지 않는 것은 버텍스 프로그래밍의 황금룰이면서도, JVM 기반의 프로그래밍의 많은 메소드가 여전히 동기화 API(Synchronous API)를 사용하고 차단 코드를 실행합니다(대표적인 Synchronous API로는 JDBC API가 있습니다).

버텍스 API가 모든 차단 코드를 비동기 API로 제공하기는 어려우므로, 예외적인 상황에서 이벤트 루프 스레드를 차단하지 않을 수 있는 방법을 제공합니다.

executeBlocking를 사용하면 차단 코드를 비동기처럼 실행하고, 실행 결과를 handler로 리턴 받을 수 있습니다.

vertx.executeBlocking(promise -> {
	// Call some blocking API that takes a significant amount of time to return
	String result = someAPI.blockingMethod("hello");
	promise.complete(result);
}, res -> {
	System.out.println("The result is: " + res.result());
});
더보기

executeBlocking을 사용하더라도 차단 코드는 합리적인 시간(즉, 수 초 내에) 안에 종료되어야 합니다.

긴 차단 작업 또는 루프 폴링(Loop polling)을 사용하는 작업 등은 배제되어야 합니다.

차단 작업이 10초 이상 걸리게 되면 Blocked thread checker에 의해서 차단 경고 로그가 표시됩니다.

긴 차단 작업은 이벤트 버스 또는 runOrContext를 사용하여 verticle과 상호 작용할 수 있는 다른 애플리케이션에서 처리하도록 유도해야 합니다.

기본적으로 executeBlocking은 동일한 컨텍스트(Context, 예를 들면 Verticle 인스턴스)에서 실행되는 경우 여러 번 호출되었을 때 직렬(순차적으로) 실행됩니다.

실행 순서를 제어하지 않고 병렬로 처리하고자 한다면 인자로 boolean ordered = false를 전달합니다.

차단 코드 사용의 다른 대안은 워커 버티클(Worker verticle) 사용입니다.

워커 버티클은 항상 워커 풀의 스레드에서 실행되며(이벤트 루프 스레드와 독립된) 풀 사이즈는 setWorkerPoolSize로 지정됩니다.

풀은 다음과 같이 동적으로 목적에 따라 생성 될 수 있습니다.

WorkerExecutor executor = vertx.createSharedWorkerExecutor("my-worker-pool");
executor.executeBlocking(promise -> {
	// Call some blocking API that takes a significant amount of time to return
	String result = someAPI.blockingMethod("hello");
	promise.complete(result);
}, res -> {
	System.out.println("The result is: " + res.result());
});