2016년 5월 25일 수요일

Vert.x 3.0.0 시작해보기...(4)

제가 요즘 요긴하게 써먹고 있는 Vert.x 을 소개하고자 합니다. 폴리글랏이긴 한데, 저는 Java 만을 사용해서 개발하고 있습니다. 그래서 Java 코드만 올리겠습니다.

장문의 글을 올리기 보다는, 바로 어떤 결과가 나오는 짧은 팁만 올리고, 댓글로 보충 내용을 올리는 쪽으로 해보겠습니다. 방송 형태가 적합하겠지만, 할 줄을 몰라서...(컴맹입니다)

------------------------------------------------------------------------------------------------------

저번 글에 그래도 읽어주신다고 댓글 달아주신 분이 몇 분 계셔서...마음의 상처가 약간은 치유가 됐습니다. T_T

그래서 이번에는...또 저만의 팁이 약간 들어간 Vert.x 의 TCP Server 기능에 대해서 소개를 해보고자 합니다.

역시나 백문이 불여일타...코드를 보면서 설명을 드리겠습니다.

vertx.createNetServer().connectHandler(socket -> {

 String handlerID = socket.writeHandlerID();

 socket.handler(buffer -> {

  String message = buffer.toString().trim();

  System.out.println(socket.remoteAddress() + " (" + handlerID + ") => " + message + "\r\n");

  socket.write("Receive => " + message + "\r\n");

 });

}).listen(9999);

HTTP Server 와 비슷하게 TCP Server 을 선언하고 처리하는 코드입니다. TCP Server 는 NetServer 라고도 불립니다. createHTTPServer() 대신에 createNetServer() 라는 Method 로 옵션을 줄 수도 있고, 주지 않을 수도 있습니다. 그리고 listen() 으로 포트를 지정해줄 수 있구요.
중요한 것은 connectHandler() 을 통해 클라이언트의 접속을 감지하고 이에 대한 처리를 할 수 있다는 것입니다. connectHandler() 는 Handler<NetSocket> 개체를 매개변수로 가지는데, 람다를 통해 NetSocket 개체를 바로 socket 이라는 이름으로 받아와 처리가 가능합니다. 물론 비동기로요...
이전에도 몇 번 언급했지만, 클라이언트가 Vert.x 의 TCP Server 로 접속을 하게 되면 고유한 ID 을 하나 부여받아서 이를 통해 사용자를 구분할 수 있게 됩니다. 그걸 받아오는 것이 socket.writeHandlerID() 입니다. 이걸 저장해두면, 저 람다 안에서는 이를 이용해서 여러가지 처리가 가능합니다.

기본적으로 가장 많이 사용하게 되는 것은 socket.handler() 입니다. 사용자가 메세지를 전송하면 Vert.x 는 handler() 이벤트를 발생시켜서 정보를 처리하게 됩니다. Handler<Buffer> 개체를 매개변수로 가지는데, 버퍼 형태로 데이터를 받아서 사용자에게 제공합니다. 보통 String 문자열로 받아올 경우 toString() 을 통해 문자를 저장할 수 있고, byte[] 형태 역시 getBytes() 로 쉽게 받아올 수 있습니다. 또한, substring 을 할 필요없이 getString() 이나 getBytes() 에 위치 정보를 매개변수로 전달해 특정 영역의 값만 가져올 수도 있고, int 나 long 같은 형태로 바로 받아올 수도 있습니다.

이렇게 받아온 정보를 콘솔에 출력한 뒤 socket.write() 로 다시 사용자에게 메세지를 보내는 것이 이 소스의 역활입니다. 일종의 Echo Server 인 셈이죠. 참 쉽죠???
기존 Java 의 TCP 처럼 Thread 을 쓸 것 없이 이렇게 간단한 문장으로 비동기로 처리되는 TCP Server 가 완성이 되었습니다.

소스를 실행한 뒤 명령 프롬프트(프로그램에서 텔넷 클라이언트를 설치해야 합니다)나 putty, SecureCRT 등의 툴로 접속을 한 뒤 타이핑을 하시면 바로바로 응답이 오는 것을 확인하실 수 있을 겁니다.



하지만, 실제 실행을 해보면 조금 테스트하기 불편한 것을 아실 수 있을 겁니다. 한 글자만 타이핑을 해도 바로 응답이 옵니다. 테스트 클라이언트를 만들어서 한 번에 문장을 보내면 괜찮을지 몰라도 손으로 테스트하기에는 여간 불편하지 않습니다. 이 문제를 어떻게 해결해야 할까요?

다음 예제를 통해 해결해 보도록 하겠습니다. 이후 예제는 아래 링크입니다. 몇 개의 예제가 하나의 소스에 있습니다. 주의해서 보세요.

socket.handler(RecordParser.newDelimited("\n", buffer -> {

 String message = buffer.toString().trim();

 System.out.println(socket.remoteAddress() + " (" + handlerID + ") => " + message + "\r\n");

 socket.write("Receive => " + message + "\r\n");

}));

첫번째 소스의 handler() 부분에 RecordParser.newDelimited() 가 추가되었습니다. 그 안에서 람다가 처리되네요. 이름에서 예상되시겠지만, 이 개체의 역할은 구분자(Delimiter)를 이용해서 내용을 모아서 처리해주는 것입니다. 아주 편리한 기능입니다. TCP 나 UDP 는 한 번에 모든 문장을 전송하지 않습니다. UDP 에 비해 훨씬 우아한 처리를 하는 TCP 조차 그 우아한 내부 처리 때문에 사용자가 보낸 문자열을 여러 개로 쪼개서 전송할 때가 있습니다. 그래서 앞서와 같이 그냥 Buffer 로 받을 경우 내가 한 번에 보낸 문장이 한 번에 오지 않고 여러 요청으로 나뉘어와서 비동기 처리가 어지럽게 될 수 있습니다. 물론 TCP 특성한 요청은 반드시 1 번만 수신되고, 순서까지 지켜서 오기 때문에 직접 Queue 을 이용해 메세지를 합치는 작업을 할 수 있습니다만, 이렇게 간단하게 메세지를 합쳐서 처리를 해줍니다. "\n" 이라는 문자를 만날 때까지요.
저 구분자가 줄바꿈인건 아시죠? 이제 이 두번째 예제를 실행하시면, 줄바꿈이 나오기 전까지는 사용자에게 응답이 가지 않습니다. 편리하죠?

그리고 예제 소스를 열어보시면 뭔가 더 많은 handler 가 처리되고 있음을 아실 겁니다. drainHandelr(), endHandler(), closeHandler(), exceptionHandler() 가 그것입니다. 사용자의 접속이 일어나자마자 일어나는 이벤트도 있고(모바일 등의 환경에서 자주 일어나는 순간적인 접속 끊김 현상에 대비해 재접속일 경우 이전 접속의 처리를 이어가고자 할 때 유용합니다) 접속이 끊겼을 때 일어나는 이벤트도 있습니다. 클라이언트의 접속 끊김 방식에 따라 exceptionHander() 호출될 때도 있고, endHandler() 와 closeHandler() 만 발생하는 경우도 있을 겁니다. 상황에 맞게 이벤트 처리를 해주시면 됩니다.
주의하실 점은, exceptionHandler() 만 Throwable 개체를 제공하고, 나머지 handler 들은 모두 Void 개체를 제공한다는 것입니다. 그래서 exceptionHandler() 는 에러 메세지 기반의 처리가 가능하지만, 나머지 handler 들은 NetSocket(위에서 socket 이라는 이름으로 받아온)을 이용한 정보를 가지고만 처리를 할 수 있습니다. HandlerID 등을 알 수 있기 때문에 Map 등에 사용자 정보를 저장해뒀다가 close 가 감지될 때 사용자를 제거해준다거나, drain 가 감지될 때 이전 HandlerID 대신에 갱신을 해준다거나 하는 작업도(실제로는 handler 에서 할 수 밖에 없긴 합니다만) 만들어봄 직 합니다.



마지막으로, 저만의 응용으로 넘어갑니다. 이 예제는 3 번째 팁에서 이용했던 Test3_2.java 파일을 이용해서 내용을 추가한 것입니다. 추가된 코드를 아래에 소개합니다.

Handler<NetSocket> handler = socket -> {

 String handlerID = socket.writeHandlerID();

 socket.handler(RecordParser.newDelimited("\n", buffer -> {

  String message = buffer.toString().trim();

  try {

   JsonObject param = new JsonObject(message);

   String method = param.getString("METHOD");

   eventBus.send("Test." + method, param, (AsyncResult<Message<String>> result) -> {

    if (result.succeeded()) {

     String returnValue = result.result().body();

     eventBus.send(handlerID, Buffer.buffer().appendString("{\"METHOD\": \"" + method + "\", \"RESULT\": \"" + returnValue + "\"}\r\n"));

    } else {

     eventBus.send(handlerID, Buffer.buffer().appendString("{\"METHOD\": \"" + method + "\", \"ERROR\": \"" + result.cause().getMessage() + "\"}\r\n"));
     result.cause().printStackTrace();

    }

   });

  } catch (Exception e) {

   socket.write(e.getMessage());

  }

 }));

};

vertx.createNetServer().connectHandler(handler)
  .listen(9999);

위에서 설명한 코드들이 대부분이라 쉽게 이해가 되실 겁니다. 실제 handler() 내부의 구현만 좀 바뀌었습니다. 내용은 크게 어렵진 않습니다.
내용의 핵심은 "JSON 으로 메세지를 넘겨주면 이를 JsonObject 개체로 만든 뒤 HTTP Server 에서 사용하던 EventBus 을 재활용해서 처리한 뒤 JSON 으로 결과를 전송한다" 입니다. 쉽죠? (고 밥 로스 선생님 따라하기)
그래도 약간의 규칙이 필요합니다. 클라이언트에서 "METHOD" 라는 키로 기존의 HTTP Server 의 URL 의 주소값을 대체하는 것입니다. 기존에 Login 와 Sleep 을 구현했으니 이 두가지를 쓸 수 있겠죠. HTTP Server 의 QueryString, 즉 매개변수 부분은 각각 키로 변수 이름을 지정하고, 값에 원래의 값을 넣으면 완벽하게 동일하게 동작하게 됩니다. 즉, 역으로 생각해서 TCP Server 를 구현하는데 telnet 등으로 테스트하기 쉽지 않을 때, HTTP Server 을 생성해두고 Postman 와 같은 툴로 쉽게 테스트를 진행할 수도 있는 것입니다. 어떠신가요? 유용하지 않나요?

그리고 예제를 유심히 보신 분은 눈치 채셨겠지만, socket.write() 로 클라이언트에게 메세지를 전송하던 부분 중 일부가 바뀌어있는 곳이 있습니다. eventBus.send(handerID, Buffer.buffer().appendString()); 형태로요. 그렇습니다. EventBus.send() 는 서버에 구현한 EventBus 말고도 접속한 클라이언트에게 메세지를 전송할 때에도 사용합니다. 나 뿐만 아니라 다른 클라이언트에게 메세지를 전송할 때에도 그 사용자의 HanderID 만 안다면 전송이 가능합니다. send() 말고도 publish() 도 존재합니다. publish() 는 send 와 달리 HandlerID 을 매개변수로 받지 않는데, 현재 서버에 접속한 모든 클라이언트에게 메세지를 보내는 기능입니다. 아울러 send() 는 오류가 발생하면 여러 사용자에게 보낼 때 중간에 전송을 중단한다고 하던데(저는 한 번에 한 명에게만 보내는 형태로만 구현해서 실제로 어떻게 그렇게 되는지는...) publish() 는 에러가 난 애들은 건너뛰고 다음 사용자에게도 전송을 한다고 하더군요.
이를 이용해서 채팅방을 구현할 때 Map 으로 사용자 정보를 저장할 개체를 생성해두고 그 채팅방에 입장할 때 HandlerID 을 Map 에 추가해두면, 사용자가 메세지를 전송해오면 Map 에서 사용자 목록을 꺼내서 그 사용자들에게 send() 로 메세지를 전송해주면 쉽게 채팅방 기능도 구현할 수 있게 됩니다.

단, 클러스터 환경에선 send() 나 publish() 을 이용한 전송에 주의를 기울여야 합니다. 해당 인스턴스에 접속한 사용자에게만 메세지를 보낼 수 있습니다. 해당 내용에 대해서 예전에 제가 적은 글을 링크로 남깁니다.






지금까지 TCP Server 에 대해서 간단하게 알아봤는데, 사실 Vert.x 에는 HTTP 을 이용한 WebSocket 이나 SockJS 도 제공합니다. 이것 역시 TCP 와 거의 유사하게 동작하기 때문에 아주 유용하게 사용하실 수 있을 것이라 믿어 의심치 않습니다! (말이 길어지는 걸 보니...자신이 없어서 이런다는거 아시겠죠? T.T)

별 유용하지도 않으면서 쓸데없이 길기만 한 팁을 읽어주셔서 고맙습니다.

오늘은 여기까지...




이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;

위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.

댓글 없음:

댓글 쓰기