Spring 4.0 에서 제공되는 핵심 기능 변경사항중 가장 주목할 점이 있다면 WebSocket의 지원입니다.
일단 Java에서는 Java EE 7에서 WebSocket에 대한 기본 API는 모두 구성이 마쳐져 있습니다. 당장 WebSocket을 지원하는 Web Page를 구성하기 위해서는 다음과 같은 조건들이 필요합니다.
1. Java EE 7 이상 지원
2. 최신 WAS 지원 (tomcat 8 이상, jetty 9.0.4 이상)
Spring에서는 WebSocket을 다음과 같은 방법으로 지원하고 있습니다.
1. @ServerEndPoint를 이용한 Java 기본 API
2. WebSocketHandler를 이용한 구성 - Spring WebSocket API
Spring WebSocket API의 경우, Java 기본 API와는 구성이 다릅니다. 이와 같은 구성을 갖게 되는 이유는 SocketJS와 같은 WebSocket을 이용하는 다른 API들을 사용할 수 있도록 한번 Wrapping을 거친 구성을 가지게 하는 것이 목표이기 때문입니다.. Spring WebSocket API의 경우, WebSocketHandler가 가장 핵심이 되고, 이를 이용한 코드 구성에 대해서 알아보도록 하겠습니다.
먼저, pom.xml에 필요한 dependency를 설정합니다.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
websocket의 경우, servlet 3.0을 사용해야지 되고, websocket에 대한 dependency는 다음과 같습니다.
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.0</version>
</dependency>
간단한 EchoService를 지원하는 WebSocket 지원 WebPage를 구성합니다. 그에 따른 WebSocketHandler는 TextWebSocketHandlerAdapter를 이용합니다. 여담이지만, Spring에서 Adapter라는 접미사가 붙은 객체들은 대부분 상속을 통해서 좀더 편하게 설정들을 할 수 있도록 도와주는 Spring에서 제공하는 일종의 Helper Class 또는 Parent Class 들이라고 할 수 있습니다.
WebSocketHandlerAdapter에서는 다음 3개의 method를 주목할 필요가 있습니다.
1. afterConnectionEstablished(WebSocketSession session)
: WebSocket connection이 발생되었을 때, 호출되는 method입니다. connection open이 된 후기 때문에, 해줘야지 될 일들을 처리하면 됩니다.
2. afterConnectionClosed(WebSocketSession session, CloseStatus status)
: WebSocket connection이 끊겼을 때, 호출되는 method입니다. connection close가 된 후, 일을 처리하면 됩니다.
3. handleMessage(WebSocketSession session, WebSocketMessage<?> message)
: 핵심적인 method입니다. 실질적인 통신 method입니다. socket.accept() 후에 onMessage 에서 처리될 일들을 이곳에서 coding하면 된다고 생각하면 됩니다.
한번 코드를 확인해보도록 하겠습니다.
public class EchoWebSocketHandler extends TextWebSocketHandlerAdapter {
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
String payloadMessage = (String) message.getPayload();
session.sendMessage(new TextMessage("ECHO : " + payloadMessage));
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// Connection이 구성된 후, 호출되는 method
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// Connection이 종료된 후, 호출되는 method
super.afterConnectionClosed(session, status);
}
}
WebSocket을 지원하는 Handler의 구성이 모두 마쳐진 후, WebSocketHandler를 Spring @MVC에 통합하기 위해서는 Config를 다음과 같이 구성합니다.
@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// WebSocket을 /echo 에 연결합니다.
registry.addHandler(echoHandler(), "/echo");
// SocketJS 지원 url을 /socketjs/echo에 연결합니다.
registry.addHandler(echoHandler(), "/socketjs/echo").withSockJS();
}
@Bean
public WebSocketHandler echoHandler() {
return new EchoWebSocketHandler();
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
매우 단순한 구조로 Config를 구성할 수 있습니다. Config에서 주목할 것은 WebSocketConfigurer interface입니다. 이 interface를 통해서 WebSocket Handler를 아주 쉽게 구성할 수 있습니다.
마지막으로 web.xml을 대신할 DispatcherWebApplicationIntializer 입니다. 특별한 것은 없고, Servlet Configuration에서 만들어진 WebConfig.class를 반환합니다.
public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setInitParameter("dispatchOptionsRequest", "true");
}
}
이제 WebSocket을 테스트 하기 위해 간단한 HTML Page를 구성해보도록 하겠습니다.
echo.html은 다음과 같이 구성할 수 있습니다.
<!DOCTYPE html>
<html>
<head>
<title>WebSocket/SockJS Echo Sample (Adapted from Tomcat's echo sample)</title>
<style type="text/css">
#connect-container {
float: left;
width: 400px
}
#connect-container div {
padding: 5px;
}
#console-container {
float: left;
margin-left: 15px;
width: 400px;
}
#console {
border: 1px solid #CCCCCC;
border-right-color: #999999;
border-bottom-color: #999999;
height: 170px;
overflow-y: scroll;
padding: 5px;
width: 100%;
}
#console p {
padding: 0;
margin: 0;
}
</style>
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<script type="text/javascript">
var ws = null;
var url = null;
var transports = [];
function setConnected(connected) {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('echo').disabled = !connected;
}
function connect() {
if (!url) {
alert('Select whether to use W3C WebSocket or SockJS');
return;
}
ws = (url.indexOf('socketjs') != -1) ?
new SockJS(url, undefined, {protocols_whitelist: transports}) : new WebSocket(url);
ws.onopen = function () {
setConnected(true);
log('Info: connection opened.');
};
ws.onmessage = function (event) {
log('Received: ' + event.data);
};
ws.onclose = function (event) {
setConnected(false);
log('Info: connection closed.');
log(event);
};
}
function disconnect() {
if (ws != null) {
ws.close();
ws = null;
}
setConnected(false);
}
function echo() {
if (ws != null) {
var message = document.getElementById('message').value;
log('Sent: ' + message);
ws.send(message);
} else {
alert('connection not established, please connect.');
}
}
function updateUrl(urlPath) {
if (urlPath.indexOf('socketjs') != -1) {
url = urlPath;
document.getElementById('sockJsTransportSelect').style.visibility = 'visible';
}
else {
if (window.location.protocol == 'http:') {
url = 'ws://' + window.location.host + urlPath;
} else {
url = 'wss://' + window.location.host + urlPath;
}
document.getElementById('sockJsTransportSelect').style.visibility = 'hidden';
}
}
function updateTransport(transport) {
transports = (transport == 'all') ? [] : [transport];
}
function log(message) {
var console = document.getElementById('console');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(message));
console.appendChild(p);
while (console.childNodes.length > 25) {
console.removeChild(console.firstChild);
}
console.scrollTop = console.scrollHeight;
}
function clear() {
$('#message').html('');
}
</script>
</head>
<body>
<div>
<div id="connect-container">
<input id="radio1" type="radio" name="group1" onclick="updateUrl('/tutorial01/echo');">
<label for="radio1">W3C WebSocket</label>
<br>
<input id="radio2" type="radio" name="group1" onclick="updateUrl('/tutorial01/socketjs/echo');">
<label for="radio2">SockJS</label>
<div id="sockJsTransportSelect" style="visibility:hidden;">
<span>SockJS transport:</span>
<select onchange="updateTransport(this.value)">
<option value="all">all</option>
<option value="websocket">websocket</option>
<option value="xhr-polling">xhr-polling</option>
<option value="jsonp-polling">jsonp-polling</option>
<option value="xhr-streaming">xhr-streaming</option>
<option value="iframe-eventsource">iframe-eventsource</option>
<option value="iframe-htmlfile">iframe-htmlfile</option>
</select>
</div>
<div>
<button id="connect" onclick="connect();">Connect</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
</div>
<div>
<textarea id="message" style="width: 350px">Here is a message!</textarea>
</div>
<div>
<button id="echo" onclick="echo();" disabled="disabled">Echo message</button>
</div>
</div>
<div id="console-container">
<div id="console"></div>
</div>
</div>
</body>
</html>
이제 구성된 Page의 테스트를 위해 maven jetty를 build 항목에 추가합니다.
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.2</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.5</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.8</version>
<configuration>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
<wtpversion>2.0</wtpversion>
</configuration>
</plugin>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.0.4.v20130625</version>
<configuration>
<webApp>
<contextPath>/${project.artifactId}</contextPath>
</webApp>
</configuration>
</plugin>
</plugins>
</build>
구성후, mvn jetty:run 명령어를 통해서 실행이 가능합니다.