NHN Business Platform 웹플랫폼개발랩 김재홍

vert.x는 비동기 애플리케이션 개발을 위한 프레임워크로 node.js처럼 개발이 쉽고 확장이 편리합니다. vert.x 임베디드는 vert.x를 사용하는 방법 중 하나로 Java 또는 Groovy 애플리케이션에 HTTP, HTTPS, TCP, SSL, WebSockets 등의 기능을 추가할 때 사용합니다.

이 글에서는 vert.x 임베디드를 사용하면서 경험한 장단점과 vert.x 임베디드 사용 방법, 그리고 주의 사항을 설명하겠습니다. 이 글은 vert.x 1.3.1-final 버전을 기준으로 합니다.

vert.x와 vert.x 임베디드

vert.x의 가장 큰 장점은 사용하기 쉽다는 것이다. 예를 들어, 비동기 처리가 가능한 HTTP 서버를 구현한 코드는 다음과 같다.

public class Server extends Verticle {
public void start() {
vertx.createHttpServer().requestHandler(new Handler<HttpServerRequest>() {
public void handle(HttpServerRequest req) {
String file = req.path.equals("/") ? "index.html" : req.path;
req.response.sendFile("webroot/" + file);
}
}).listen(8080);
}
}

위 코드를 실행하는 명령어는 다음과 같다.

vertx run Server.java 

명령줄에서 명령어로 실행하면 이처럼 매우 간단하게 사용할 수 있지만, 그러려면 우선 시스템에 vert.x를 설치해야 한다. 그리고 실행 명령어와 특성을 학습해야 하기 때문에 vert.x를 처음 접하는 사용자는 어렵고 번거롭다고 느낄 수 있다.

vert.x는 명령줄 실행 외에, Java 또는 Groovy 애플리케이션에서 vert.x의 코어 라이브러리만 참조하여 임베디드 형태로 사용할 수도 있다. 이 경우 vert.x를 설치할 필요가 없고, 기존 애플리케이션에도 적용할 수 있으므로 vert.x 설치나 명령어가 어렵다고 느끼는 사람에게 권장한다.

vert.x 임베디드의 장점

vert.x 임베디드의 장점에는 크게 다음 세 가지가 있다.

  • 기존 애플리케이션에 vert.x의 기능을 적용할 수 있다.
  • vert.x 배포에 관련된 제약에서 벗어날 수 있다.
  • vert.x의 기능을 확장할 수 있다.

여기에서는 이 세 가지 장점에 대해 자세히 알아본다.

기존 애플리케이션에 vert.x 기능 적용

vert.x 임베디드는 기존 애플리케이션에 vert.x의 전체 기능을 적용하지 않고 HTTP, HTTPS, TCP, SSL, WebSocket 등의 핵심 기능만 적용하려고 할 때 유용하다. 그리고 Spring과 같은 기존 프레임워크에 적용할 수 있기 때문에 개발하고 테스트하기 쉽다.

예를 들면, 사용 중인 웹 애플리케이션(Tomcat + Spring MVC)에 WebSocket 기능을 추가하고 싶다면 다음 두 단계만 진행하면 된다.

  1. 기존 프로젝트에 vert.x 코어 라이브러리 파일(vertx-core-<version>.jar)을 추가한다.
  2. Spring에 관련 빈(bean)을 만들고, 빈이 초기화될 때 다음 코드가 실행되게 한다.
Vertx vertx = Vertx.newVertx();
vertx.createHttpServer().websocketHandler(new Handler<ServerWebSocket>() {
public void handle(final ServerWebSocket ws) {
if (ws.path.equals("/myapp")) {
ws.dataHandler(new Handler<Buffer>() {
public void handle(Buffer data) {
ws.writeTextFrame(data.toString()); // Echo it back
}
});
} else {
ws.reject();
}
}
}).requestHandler(new Handler<HttpServerRequest>() {
public void handle(HttpServerRequest req) {
if (req.path.equals("/")) req.response.sendFile("websockets/ws.html"); // Serve the html
}
}).listen(8080);

실제 프로젝트에서는 애플리케이션이 종료될 때 vertx.stop() 메서드를 사용해서 vert.x를 종료하는 등 추가로 고려해야 할 사항이 있다. 하지만 기존 애플리케이션에 비교적 간단하게 기능을 추가할 수 있다는 점은 vert.x 임베디드의 가장 큰 장점이다.

vert.x 배포에 관련된 제약 문제 해결

vert.x 임베디드는 vert.x 배포와 상관없이 직접 라이브러리를 참조해서 실행되기 때문에 vert.x의 배포 단위인 verticle을 사용하지 않는다.

verticle에 대해 알아보기 위해 먼저 vert.x 명령어를 잠시 살펴보자. vert.x를 명령줄에서 실행하는 예는 다음과 같다.

vertx run com.acme.MyVerticle 

위 명령어에서 com.acme.MyVerticle은 vert.x의 배포 단위인 verticle로, Verticle 클래스를 상속받아서 구현한다.

verticle은 실행될 때 스레드(event loop)를 하나 할당받아, 할당받은 스레드에서만 verticle 내에서 발생하는 이벤트를 수행한다. 이러한 동작은 vert.x의 동시성(concurrency)과 관련된 특징으로, 사용자가 멀티 스레딩 환경에서 동시성과 관련된 번거로운 처리를 신경 쓰지 않아도 되게 한다. 또한 verticle은 하나의 스레드에서 실행되므로, vert.x 인스턴스당 여러 개의 verticle 인스턴스를 실행하여 성능을 최적화할 수 있다. 다음은 여러 개의 verticle을 실행하는 예이다.

vertx run com.acme.MyVerticle -instances 10 

그런데 verticle은 각각 독립된 클래스로더(class loader)를 사용하기 때문에 서로 객체를 공유할 수 없다는 제약이 있다. vert.x는 verticle 간에 데이터를 공유할 수 있도록 공유 데이터(shared data) 기능을 제공하지만, Java의 기본 타입(String, Integer 등)과 같은 불변(immutable) 데이터만 지원하므로 객체 자체를 공유할 수는 없다.(공유하려는 객체에 Shareable 인터페이스를 구현해서 verticle 자체를 vert.x 라이브러리 폴더에 넣으면, 실행될 때 verticle이 vert.x 시스템 라이브러리에 포함되기 때문에 객체를 공유할 수 있지만 모든 소스가 vert.x 내에 포함되어야 하기 때문에 권장하지 않는다.)

따라서 캐시 데이터를 공유하려면 데이터를 JSON으로 공유 데이터에 저장하고 각 verticle에서 다시 객체로 전환해야 한다. 또한 vert.x 인스턴스내의 verticle 간에 공유되는 데이터에 트랜잭션 처리가 필요한 경우에, 객체가 공유된다면 해당 객체에 동기화 처리(synchronized)를 적용해 간단하게 해결할 수 있는데, 객체를 공유할 수 없기 때문에 트랜잭션이 지원되는 외부 저장소 사용을 고려해야 한다는 부담이 있다.

vert.x 임베디드를 사용하면 클래스로더를 별도로 사용하지 않기 때문에 객체를 공유할 수 있다.

vert.x 기능 확장

vert.x 임베디드는 vert.x의 핵심 인터페이스인 Vertx 객체의 생성 시점을 직접 결정할 수 있기 때문에 Vertx 객체를 감싸는 방식(wrapper) 등으로 기능을 확장할 수 있다.

예를 들어 vert.x의 이벤트 버스(event bus)는 클러스터링을 위해서 Hazelcast(http://www.hazelcast.com)를 사용하여 이벤트의 주소를 저장하는데, Hazelcast는 Java 기반의 IMDG(In Memory Data Grid)이므로 Java GC(Garbage Collection)에 영향을 받을 수 있다. 그런데 Hazelcast는 더블 라이선스 정책을 취하고 있는 제품으로, ElasticMemory와 같이 GC에 영향을 받지 않는 기능을 사용하려면 상용 라이선스를 구입해야 한다. 그러나 ElasticMemory 기능이 없는 Hazelcast를 대규모 서비스에 적용하면 안정성을 보장할 수 없다. 그래서 이벤트 버스에서 Hazelcast를 사용하지 않으려고 해도 배포 단계에서 이미 Hazelcast가 동작하는 구조이기 때문에 임베디드 형태가 아니면 확장하기 어렵다.

vert.x 임베디드의 단점

vert.x 임베디드의 단점은 크게 다음 두 가지가 있다.

  • 임베디드 방식이 지원하는 프로그래밍 언어는 Java와 Groovy뿐이다.
  • vert.x 모듈을 사용할 수 없다.

여기에서는 위 두 가지 단점에 대해 자세히 알아본다.

Java와 Groovy만 지원

vert.x 임베디드가 지원하는 언어는 Java와 Groovy(http://groovy.codehaus.org/)뿐이기 때문에 vert.x 임베디드를 사용하려면 반드시 Java를 이해해야 한다.

그러나 vert.x를 설치해 사용할 때도 Java를 이해해야 하는 것은 마찬가지이다. vert.x는 Java 기반의 코어 라이브러리를 JavaScript, JRuby, Jython, Groovy와 같은 Java 호환 프로그래밍 언어로 감싸서, 각 언어가 단순히 코어 라이브러리를 참조하는 형태가 아니라 언어 본래의 스타일대로 개발할 수 있게 지원한다.

다음은 JavaScript로 웹 서버를 구현한 예이다.

load('vertx.js')

vertx.createHttpServer().requestHandler(function(req) {
var file = req.path === '/' ? 'index.html' : req.path;
req.response.sendFile('webroot/' + file);
}).listen(8080)

위 코드를 실행하는 실행하는 명령어는 다음과 같다.

vertx run server.js 

vert.x를 설치하여 사용하면 위 예처럼 JavaScript만 사용하여 개발할 수 있다. 그러나 vert.x 매뉴얼이나 예제로 알 수 없는 부분은 코어 라이브러리의 코드에서 직접 확인하는 경우가 자주 있으므로 기본적으로는 Java를 이해해야만 한다.

vert.x 모듈 사용 불가능

vert.x는 자주 사용되는 기능들을 모듈로 패키지화해서 저장소에 업로드해 두어, 사용자가 필요할 때 바로 다운로드해서 사용할 수 있게 지원한다. 그리고 이 모듈은 명령줄에서 명령어로 실행하기 때문에 vert.x 임베디드는 모듈을 사용할 수 없다.

참고
제공되는 vert.x 모듈은 아직은 개수가 적어 좀 더 활성화가 필요하다. 현재 제공하는 vert.x 모듈은 저장소(https://github.com/vert-x/vertx-mods/tree/gh-pages/mods)에서 확인할 수 있다.

사용 시 고려할 점

여기에서는 vert.x 임베디드를 사용할 때 고려해야 할 점 세 가지를 설명한다. 예시 코드에서 개발할 애플리케이션은 Tomcat + Spring 구조이다.

Vertx 객체의 사용 형태 결정

Vertx 객체는 vert.x의 핵심 클래스로, vert.x 사용에 필요한 대부분의 기능을 제공한다. 일반적으로는 Vertx 객체를 FactoryBean 형태로 만들고 필요할 때 Vertx 객체를 사용하는 방법을 많이 사용한다. 기능을 확장할 필요가 있다면 Vertx 객체를 감싸는 형태로 빈을 구성하는 방법 등을 사용할 수 있다.

여기에서는 두 번째 방법인 Vertx 객체를 감싸는 방법으로 기능을 확장한다.

@Component
public class VertxInternalHolder extends VertxInternal {
private VertxInternal vertx;
private DirectEventBus eventBus;
...

@PostConstruct
public void init() {
vertx = (VertxInternal)Vertx.newVertx();
eventBus = new DirectEventBus(vertx, getEventbusPort(),getHost());
}

@PreDestroy
public void destroy() {
if (vertx != null) {
vertx.stop();
}
}

...

@Override
public EventBus eventBus() {
return eventBus;
}
}

위 코드에서 VertxInternalHolder 빈은 VertxInternal 클래스를 상속받아 애플리케이션 내에서 Vertx 객체처럼 사용한다. VertxInternal 클래스는 Vertx 클래스를 상속받고 몇 가지 내부적인 기능을 추가로 구현하는 클래스로, Vertx.newVertx() 메서드를 실행하면 반환되는 DefaultVertx 객체가 이를 상속받고 있기 때문에 vert.x 임베디드 내부에서는 VertxInternal 클래스를 사용한다.

VertxInternalHolder 빈이 초기화될 때 init() 메서드에서 Vertx 객체를 생성한다. 그리고 이벤트 버스에서 Hazelcast를 사용하지 않기 위해서 vert.x의 DefaultEventBus가 아닌 DirectEventBus를 생성한 후 eventBus() 메서드를 오버라이드(override)해서 기능을 확장하고 있다.

Runnable 객체 구성 및 반복 실행

vert.x의 기능을 사용하기 위해 구현한 로직을 실행하기 위해서 다음과 같이 ServerBootstrap 빈을 구성한다.

@Component
public class ServerBootstrap {
@Autowired
private VertxInternalHolder vertxHolder;
...

@PostConstruct
public void init() {
final Runnable runnable = new Runnable() {
public void run() {
final HttpServer httpServer = vertxHolder.createHttpServer();
...
httpServer.listen(getHttpPort());
}
};

for (int i = 0; i < getInstanceCount(); i++) {
vertxHolder.startOnEventLoop(runnable);
}
}
...
}

ServerBootstrap 빈은 앞에서 구성한 VertxInternalHolder 빈과 자동으로 의존관계가 설정된다(Dependency Injection). ServerBootstrap 빈이 초기화될 때 실행되는 init() 메서드에서는 Runnable 인터페이스를 구현한 객체를 생성한다.

Runnable 객체의 run() 메서드 부분은 vert.x의 기능을 사용하기 위해 구현된 부분으로, HTTP 서버(HttpServer)를 생성해서 RESTful API를 처리한다고 가정한다.

마지막으로, 생성된 Runnable 객체를 인스턴스 개수만큼 반복해서 실행한다. 이는 HttpServer 객체가 verticle처럼, 실행 시에 스레드를 할당받아 그 스레드에서만 동작하기 때문이다. 따라서 성능을 최적화하려면 시스템에 적합한 개수만큼 실행되어야 한다.

동기화 방식 호출은 백그라운드 풀에서 실행

DB 쿼리와 같이 동기화 방식으로 처리되는 호출은 모두 백그라운드 풀에서 실행할 수 있도록 구현해야 한다. vert.x는 비동기 방식으로 실행되기 때문에, 이벤트를 처리할 때 동기화 방식으로 실행되는 부분이 있으면 성능에 나쁜 영향을 준다. 따라서 동기화 방식으로 처리되는 호출은 vert.x의 BlockingAction 기능을 사용하여 백그라운드에서 작업하게 해야 한다.

서버 정보를 데이터베이스에서 호출할 때 BlockingAction을 사용하는 예는 다음과 같다.

public void getAppServerIndex(final String appId,
final AsyncResultHandler<List<String>> resultHandler) {
new BlockingAction<List<String>>(vertx, resultHandler) {
public List<String> action() throws Exception {
return repository.getAssignedServerIndex(appId);
}
}.run();
}

주의 사항

vert.x는 종료될 때, 사용 중인 리소스를 정리하기 위해서 스레드 콘텍스트(Context)의 closeHooks 맵에 사용 중인 리소스를 저장한다.

vert.x에서 closeHooks 맵에 의해서 관리되는 리소스는 다음과 같다.

  • 이벤트 버스의 이벤트 처리 핸들러
  • HTTP/Net 서버
  • HTTP/Net 클라이언트

그런데 이벤트 버스의 이벤트 처리 핸들러를 해지할 때 이벤트를 등록한 콘텍스트와 해지하는 콘텍스트가 다르면 리소스가 삭제되지 않고 계속 남아 있는 현상이 발생할 수 있다. 예를 들어, HTTP 요청으로 이벤트 버스에 이벤트를 등록하고 해지하는 기능을 구현했다고 가정하자. 이때 HTTP 요청을 처리하는 스레드가 요청마다 다를 수 있는데, 이벤트를 등록한 콘텍스트와 해지하는 콘텍스트가 다르면 이벤트 핸들러는 삭제가 되지 않고 그냥 남게 되어 메모리 누수(leak)를 유발하게 된다. 이는 이벤트를 등록할 때 콘텍스트를 따로 저장해 두고 이벤트를 해지할 때 미리 저장해 놓은 콘텍스트를 스레드에 임시로 설정하여 해결할 수 있다.

다음은 이벤트 버스에서 이벤트를 해지할 때 콘텍스트를 설정하는 예이다.

Context.setContext(context);
vertx.eventBus().unregisterHandler(writeHandlerID, writeHandler);

마치며

vert.x는 2012년 5월에 1.0 버전이 릴리스된 후 빠르게 성장해서 현재 2.0 버전이 개발되는 중이다.

최근 성능에 대한 요구사항이 높아지면서 비동기 클라이언트/서버 애플리케이션에 대한 필요성이 점점 증가하고 있다. 비동기 애플리케이션으로 HTTP, HTTPS, TCP, SSL, WebSocket 등의 기능이 필요한 개발자라면 vert.x 사용을 고려해 볼 것을 권장한다.

프로젝트 초기에는 배포를 위해서 vert.x 모듈로 개발했지만, 프로젝트를 진행하면서 임베디드 형태가 더 적합하다는 사실을 깨닫고 중간에 구조를 변경하며 많은 우여곡절을 겪었다. 이 글이 이러한 시행착오를 줄이고 vert.x를 사용하는 데에 도움이 되길 바란다.

참고 자료

16d3526cd40a1028f6cd9e6a8de30253.jpg
NBP 웹플랫폼개발랩 김재홍
에러의 신이 있다면 제가 에러의 신에게 할 수 있는 말은 오직 하나 입니다. “Not today.”