'소프트웨어개발/Jersey'에 해당되는 글 4건

  1. Jersey 2, FreeMarker 템플릿 엔진 적용하기
  2. Jersey 2, JAX-RS + MVC 적용하기
  3. Server-Sent Events(SSE)를 이용한 Java 채팅 웹 애플리케이션 구현 v0.2 (3)
  4. Maven, Servlet 3, Jersey 2 기반의 Hello, World! 웹 프로젝트 작성하기 (2)

Jersey 2, FreeMarker 템플릿 엔진 적용하기

개요

MVC 구조의 Java 웹 애플리케이션 개발시 View를 구현하는 가장 일반적인 방법은 JSP(JavaServer Pages)를 기반으로 EL(Expression Language), JSTL(JavaServer Pages Standard Tag Libaray)를 사용하는 것이다. 이 것 만으로도 충분히 직관적이고 강력한 View를 구현할 수 있지만 Java EE에 종속적이라는 단점이 존재한다.(예를 들어 이메일 템플릿 같은 경우 서블릿 컨테이너를 벗어난 애플리케이션에서 JSP로 처리하려면 별도의 프리 프로세서가 필요하다.) 이에 MVC 구조에 극단적으로 최적화되어 있으면서 Java EE에 얽매이지 않아 어디서나 사용될 수 있는 FreeMarker 템플릿 엔진을 적용하는 방법을 소개하고자 한다. Jersey 2의 기본 프로젝트 생성은 이 글을, MVCJSP 적용은 이 글을 참고한다.

/pom.xml

<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-mvc-freemarker</artifactId>
    <version>2.21</version>
</dependency>
  • jersey-mvc-freemarkerMVC + FreeMarker 통합 기능을 제공하는 Jersey 2의 확장 모듈이다.

/src/main/java/.../AppConfig.java

@ApplicationPath("/")
public class AppConfig extends ResourceConfig {

    public AppConfig() {

        packages(this.getClass().getPackage().getName());
        register(FreemarkerMvcFeature.class);
    }
}

/src/main/webapp/WEB-INF/web.xml

    <filter>
        <filter-name>jersey-mvc-filter</filter-name>
        <filter-class>org.glassfish.jersey.servlet.ServletContainer</filter-class>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>com.jsonobject.sample.AppConfig</param-value>
        </init-param>
        <init-param>
            <param-name>jersey.config.servlet.filter.contextPath</param-name>
            <param-value></param-value>
        </init-param>
        <init-param>
            <param-name>jersey.config.server.mvc.templateBasePath.freemarker</param-name>
            <param-value>/WEB-INF/views</param-value>
        </init-param>
        <init-param>
            <param-name>jersey.config.servlet.filter.staticContentRegex</param-name>
            <param-value>/assets/.*</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>jersey-mvc-filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
  • 이전 글과 설정이 대부분 동일하다. 유일하게 다른 점은 jersey.config.server.mvc.templateBasePath.freemarker에 대한 값으로 /WEB-INF/views 경로를 명시하였다. MVC 패턴의 View로서 기능하는 FTL 파일은 모두 /src/main/webapp/WEB-INF/views 폴더에 위치한다는 의미이다.

/src/main/java/.../HelloResource.java

@Path("/")
public class HelloResource {

    @GET
    @Path("helloworld")
    @Produces(MediaType.TEXT_HTML)
    public Viewable helloWorld() {

        Map<String, Object> model = new HashMap<String, Object>();
        model.put("title", "Jersey FreeMarker Hello World Example");
        model.put("body", "<Hello, World!>");
        model.put("footer", "This is footer message!");
        return new Viewable("/helloworld.ftl", model);
    }
}
  • 이전 글의 JSP 설정과 동일하다. 차이점은 View 기능을 하는 템플릿 파일의 확장자가 FTL이라는 것 뿐이다.

/src/main/webapp/WEB-INF/views/footer.ftl

<h3>${footer}</h3>

/src/main/webapp/WEB-INF/views/helloworld.ftl

<!DOCTYPE html>
<html>
<head>
  <title>${title}</title>
</head>
<body>
  <h2><#if body??>${body?upper_case?html}</#if></h2>
  <#include "footer.ftl">
</body>
</html>
  • FreeMarker 템플릿은 JSP + EL + JSTL 조합과 상당히 유사하면서 훨씬 간결한 느낌을 준다. 무엇보다 Jave EE를 벗어난 어떠한 Java 애플리케이션에서도 사용할 수 있다는 장점이 있다.
  • 앞서 Resource 클래스에서 전달 받은 Model 오브젝트에 담긴 데이터들은 위와 같이 바로 사용될 수 있다.
  • 변수명 뒤에 붙은 ?upper_case는 대문자로 출력하겠다는 의미이다. 뒤 이어 붙은 ?htmlHTML Escaping을 의미한다. HTML 태그와 충돌을 유발하는 모든 문자는 특수 기호로 대체되어 출력된다.
  • <#if {OBJECT_NAME}??>...</#if>는 해당 오브젝트가 Null이 아닐 경우에만 안의 내용을 출력한다는 의미이다.
  • <#include {FILE_NAME}>은 다른 템플릿 파일을 포함한다는 의미이다.

보너스, Eclipse 플러그인 설치하기

기본적인 Eclipse EE 상태에서는 FreeMarker 템플릿을 담은 FTL 파일을 인식하지 못한다. 아래와 같은 순서로 FreeMarker 플러그인을 설치할 수 있다.(Eclipse EE Mars 버전 기준으로 URL은 추후 변경될 수 있다.)

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Jersey 2, JAX-RS + MVC 적용하기

개요

최근 회사에서 N 포탈과의 제휴 서비스를 Jersey 2로 개발하면서 생산성 측면에서 쏠쏠한 재미를 보았다. 이번 글은 Jersey 2 프레임워크에서 MVC 패턴을 적용하는 방법을 간단히 설명하고자 한다.

Jersey란 무엇인가?

Jersey 2Oracle이 직접 JAX-RS 표준을 구현한 공식 레퍼런스 구현체로 Java 플랫폼에서 RESTful Web Services를 높은 생산성으로 구현하고 소비할 수 있게 해주는 프레임워크이다.(JAX-RS 표준이 서버만을 고려하여 작성된 데 반해 Jersey 2는 클라이언트 입장에서도 서비스를 편리하게 소비할 수 있도록 구현되었다.)

MVC 패턴을 적용해보자

Jersey 2는 기본적으로 RESTful Web Services에 초점을 맞추고 있지만 공식적으로 제공되는 jerser-mvc-jsp 확장 모듈을 사용하여 국내 Java 웹 개발의 표준이나 다름 없는 Spring MVC 만큼이나 직관적이고 편리한 MVC 기능을 사용할 수 있다. Maven 기반의 프로젝트임을 가정하고 MVC 패턴 적용 방법을 차례로 설명한다. Jersey 2 기본 프로젝트 생성은 이 글을 참고한다.

/pom.xml

<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-mvc-jsp</artifactId>
    <version>2.19</version>
</dependency>
<dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
    <version>1.1.2</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>
  • jersey-mvc-jsp 라이브러리는 MVC 기능을 제공하는 Jersey 2의 확장 모듈이다.
  • JSP 파일 작성시 JSTL 표준 템플릿 태그를 사용하기 위해 standard, jstl 라이브러리를 추가하였다.

/src/main/java/.../AppConfig.java

@ApplicationPath("/")
public class AppConfig extends ResourceConfig {

    public AppConfig() {

        packages(this.getClass().getPackage().getName());
        register(JspMvcFeature.class);
    }
}
  • 웹 애플리케이션 기동시 org.glassfish.jersey.servlet.ServletContainer에 읽혀질 환경 설정 파일이다. JspMvcFeature.class를 등록하여 MVC 기능을 활성화한다.
  • @ApplicationPath("/") 명시는 / 경로(ContextPath)에 대한 모든 요청시 이 환경설정이 적용된다는 것을 의미한다.

/src/main/webapp/WEB-INF/web.xml

<filter>
    <filter-name>jersey-mvc-filter</filter-name>
    <filter-class>org.glassfish.jersey.servlet.ServletContainer</filter-class>
    <init-param>
        <param-name>javax.ws.rs.Application</param-name>
        <param-value>com.jsonobject.sample.AppConfig</param-value>
    </init-param>
    <init-param>
        <param-name>jersey.config.servlet.filter.contextPath</param-name>
        <param-value></param-value>
    </init-param>
    <init-param>
        <param-name>jersey.config.server.mvc.templateBasePath.jsp</param-name>
        <param-value>/WEB-INF/jsp</param-value>
    </init-param>
    <init-param>
        <param-name>jersey.config.servlet.filter.staticContentRegex</param-name>
        <param-value>/assets/.*</param-value>
    </init-param>
    </filter>
<filter-mapping>
    <filter-name>jersey-mvc-filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
  • Servlet 3.x 기반의 프로젝트라면 web.xml에 별도로 Jersey 2ServletContainer 클래스를 등록하지 않아도 된다. 그럼에도 위와 같이 FilterServletContainer를 등록한 것은 이미지, JavaScript, CSS와 같은 정적인 파일(Static Assets)을 처리하기 위함이다.
  • jersey.config.server.mvc.templateBasePath.jsp에 대한 값으로 /WEB-INF/jsp 경로를 명시하였다. MVC 패턴의 View로서 기능하는 JSP 파일은 모두 /src/main/webapp/WEB-INF/jsp 폴더에 위치한다는 의미이다.
  • jersey.config.servlet.filter.staticContentRegex에 대한 값으로 /assets/.* 정규식을 명시하였다. /assets/css/some.css와 같은 정적 파일 요청시 특정 Resource 클래스로 분기하지 않고 바로 처리하겠다는 의미이다.

/src/main/java/.../HelloResource.java

@Path("/")
public class HelloResource {

    @GET
    @Path("helloworld")
    @Produces(MediaType.TEXT_HTML)
    public Viewable getSource() {
        Map<String, Object> model = new HashMap<String, Object>();
        model.put("title", "Hello!");
        model.put("message", "Hello, World!");
        return new Viewable("/helloworld.jsp", model);
    }
}
  • Viewable 클래스는 MVC 패턴의 View 기능을 하는, Jersey 2가 독자적으로 제공하는 클래스이다. Spring MVC에 익숙한 개발자라면 ModelAndView 클래스와 유사한 것을 느낄 것이다. 클라이언트의 요청에 대해 처리한 결과를 Model 오브젝트에 담아 Viewable 오브젝트에 전달하여 동적인 View를 생성한다.

/src/main/webapp/WEB-INF/jsp/helloworld.jsp

<%@ page language="java" contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>${it.title}</title>
</head>
<body>
${it.message}
</body>
</html>
  • 마지막 단계이다. 앞서 전달 받은 Model 오브젝트는 ${it.modelName}과 같은 형태로 View에 맵핑되어 클라이언트에 응답으로 반환된다.

결론

최근 해외에서는 Python 진영의 Flask 프레임워크가 웹 개발시 높은 생산성으로 많은 인기를 얻고 있다. Java 진영에도 Flask와 대적할만한 Dropwizard 프레임워크가 있다. DropwizardJava 진영의 여러 유용한 라이브러리를 엄선, 짜집기하여 만든 종합선물세트이자 맥가이버 칼과도 같은 프레임워크라고 볼 수 있다. 흥미로운 점은 JAX-RS 표준을 구현한 많은 프레임워크 중에 Jersey 2DropwizardREST 담당 모듈로 선정되었다는 것이다. 그만큼 성능을 인정 받은 것이라고 평가할 수 있다. 엄격한 표준 구현, 높은 생산성 측면에서 Spring MVC에만 익숙했던 국내 개발자들에게 Jersey 2MVC의 조합을 추천한다.

참고 문서

저작자 표시 비영리 동일 조건 변경 허락
신고

Server-Sent Events(SSE)를 이용한 Java 채팅 웹 애플리케이션 구현 v0.2

개요

앞서 지난 글에서 Server-Sent Events(이하 SSE)를 이용한 채팅 웹 애플리케이션 구현 예제를 소개했었다. 이번 글에서는 Jersey 프레임워크의 Server SSE API를 이용한 SSE 브로드캐스트 기능을 이용하여 완전한 Real-Time Server Push Notification을 구현해보고자 한다.

개선된 점

이번 글에서 소개하는 v0.2 예제에서는 v0.1 예제 대비 아래와 같은 부분이 개선되었다.

  • 본격적으로 Jersey 프레임워크를 도입하여 클라이언트로부터 새 메시지 도착시 곧바로 모든 SSE 연결에 새 메시지를 브로드캐스트(Broadcast)하는 기능을 추가하였다. v0.1은 브로드캐스트 기능이 존재하지 않아 각각의 SSE 연결마다 2.5초 간격으로(Polling) 새 메시지를 확인하여 시간 지연이 있었기 때문에 진정한 의미의 Asynchronous Notification이 되지 못했다.
  • 클라이언트가 새 메시지 수신시 브라우저의 탭(또는 창)에 해당 화면이 열려 있지 않아도 새 메시지가 도착했음을 알려주는 팝업 기능을 추가하였다. 이를 위해 HTML5 Web Notifications API를 사용하였다.

사전지식

본격적인 소스 코드 작성에 앞서 아래와 같은 개발환경 구축 및 사전지식이 필요하다.

  • JDK 7, Eclispe IDE for Java EE Developers(이하 Eclipse EE)가 설치되어 있어야 한다. 설치 방법은 이 글을 참고한다.
  • Maven을 기반으로 Jersey 2 프레임워크를 적용하여 웹 애플리케이션을 작성할 수 있어야 한다. 이 글을 참고한다.

/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.jsonobject</groupId>
    <artifactId>minichat-jersey</artifactId>
    <packaging>war</packaging>
    <version>0.2-SNAPSHOT</version>
    <name>minichat-jersey Maven Webapp</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet-core</artifactId>
            <version>2.17</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet</artifactId>
            <version>2.17</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-sse</artifactId>
            <version>2.17</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
            <version>2.17</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.3</version>
        </dependency>
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.7</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.3.1</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>minichat-jersey</finalName>
    </build>
</project>

/src/main/webapp/WEB-INF/web.xml

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    metadata-complete="true" version="3.1">
    <display-name>MiniChat v0.2</display-name>
</web-app>

/src/main/java/com/jsonobject/minichatjersey/config/ApplicationConfig.java

package com.jsonobject.minichatjersey.config;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/rest")
public class ApplicationConfig extends ResourceConfig {
    public ApplicationConfig() {
        packages("com.jsonobject.minichatjersey.resource");
    }
}

/src/main/java/com/jsonobject/minichatjersey/model/Message.java

package com.jsonobject.minichatjersey.model;

import org.joda.time.DateTime;

public class Message {
    private Long messageId;
    private String userName;
    private String message;
    private DateTime dateCreated;

    public Message() {
        super();
    }

    public Long getMessageId() {
        return messageId;
    }

    public void setMessageId(Long messageId) {
        this.messageId = messageId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public DateTime getDateCreated() {
        return dateCreated;
    }

    public void setDateCreated(DateTime dateCreated) {
        this.dateCreated = dateCreated;
    }

    @Override
    public String toString() {
        return "Message [messageId=" + messageId + ", userName=" + userName + ", message=" + message + ", dateCreated=" + dateCreated + "]";
    }
}

/src/main/java/com/jsonobject/minichatjersey/resource/MessageResource.java

package com.jsonobject.minichatjersey.resource;

import javax.servlet.ServletContext;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.glassfish.jersey.media.sse.EventOutput;
import org.glassfish.jersey.media.sse.OutboundEvent;
import org.glassfish.jersey.media.sse.SseBroadcaster;
import org.glassfish.jersey.media.sse.SseFeature;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.jsonobject.minichatjersey.model.Message;

@Path("/message")
public class MessageResource {

    private static final Logger logger = LoggerFactory.getLogger(MessageResource.class);

    @Context
    private ServletContext context;

    @POST
    @Path("/sendMessage")
    @Consumes(MediaType.APPLICATION_JSON)
    public void sendMessage(Message message) {
        message.setDateCreated(new DateTime());
        OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder();
        OutboundEvent event = eventBuilder.name("message").mediaType(MediaType.TEXT_PLAIN_TYPE).data(String.class, new Gson().toJson(message)).build();
        getChannelBroadcaster().broadcast(event);
        logger.info("sendMessage(); broadcast, messsage={}", message.toString());
    }

    @GET
    @Path("/getNewMessages")
    @Produces(SseFeature.SERVER_SENT_EVENTS)
    public EventOutput listenToBroadcast() {
        logger.info("listenToBroadcast(): start");
        final EventOutput eventOutput = new EventOutput();
        getChannelBroadcaster().add(eventOutput);

        return eventOutput;
    }

    private SseBroadcaster getChannelBroadcaster() {
        SseBroadcaster broadcaster = (SseBroadcaster) context.getAttribute("CHANNEL");
        if (broadcaster == null) {
            broadcaster = new SseBroadcaster();
            context.setAttribute("CHANNEL", broadcaster);
        }
        return broadcaster;
    }
}
  • 순수 서블릿으로 작성되었던 v0.1 예제에 비해 Jersey 프레임워크의 힘을 빌어 소스 코드가 상당히 간결해진 것을 확인할 수 있다.
  • Resource 클래스는 Jersey 프레임워크에서 각 HTTP 요청에 대한 첫 관문 역할을 한다. Spring MVC와 비교하면 Controller 클래스와 동일한 기능을 수행한다. 차이점은 Controller 클래스는 단 한 번만 생성되어 싱글턴 오브젝트로 취급되지만 Resource 클래스는 매 요청시마다 오브젝트가 생성된다.
  • SSE 연결에 대한 브로드캐스트를 담당하는 SseBroadcaster 오브젝트의 사용을 확인할 수 있다. 모든 SSE 연결에 대해 브로드캐스트를 하기 위해서는 각 연결이 모두 한 채널에 묶여있어야 하며 이는 모두 동일한 SseBroadcaster 오브젝트에 Subscriber로 등록됨으로서 가능해진다. 모든 요청시 SseBroadcaster 오브젝트에 접근하기 위해 ServletContext에 해당 오브젝트를 저장하고 불러온다.
  • 클라이언트로부터 /getNewMessages 요청시 클라이언트와의 SSE 연결이 처음 수립되어 OPEN 상태가 된다. 서버에서는 해당 연결을 SseBroadcaster 오브젝트에 Subscriber로 등록하는 것 외에는 다른 작업을 하지 않는다.
  • 클라이언트로부터 /sendMessage 요청시 전송받은 메시지를 브로드캐스트한다. 매우 간단하다. 현재의 채팅 개념을 메신저 개념으로 바꾼다면 별도의 저장소에 해당 메시지를 등록하는 로직을 추가할 수 있을 것이다.

/src/main/webapp/index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta content="width=device-width, initial-scale=1" name="viewport">
  <title>미니 채팅방</title>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet">
  <link href="http://getbootstrap.com/assets/css/docs.min.css" rel="stylesheet">
  <style>
  * {
      font-family: 'Malgun Gothic' !important;
  }
  </style>
</head>
<body>
  <div class="bs-callout" id="messages">
    <h2>미니 채팅방</h2>
    <h5>CreatedBy: <strong>Yi Tae-Hyeong</strong>, Version: <strong>0.2</strong></h5>
    <h5>Tech: <strong>Server-Sent Events(SSE)</strong></h5>
    <h5>Client: <strong>HTML5 SSE</strong></h5>
    <h5>Server: <strong>Jersey Server SSE</strong></h5>
    <p>&nbsp;</p>
  </div>
  <div class="bs-callout">
    <form>
      <div class="form-group">
        <input class="form-control" id="userName" placeholder="이름" type="text">
      </div>
      <div class="form-group">
        <textarea class="form-control" id="message" placeholder="메시지" rows="3"></textarea>
      </div>
      <div class="form-group">
        <button class="btn btn-primary btn-lg btn-block" id="sendMessage" type="button">메시지 전송</button>
      </div>
    </form>
  </div>
  <script src="${pageContext.request.contextPath}/resources/libs/EventSource.js" type="text/javascript"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/sugar/1.4.1/sugar.min.js"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min.js"></script>
  <script src="http://getbootstrap.com/assets/js/docs.min.js"></script>
  <script>
        var contextPath = '${pageContext.request.contextPath}';

        var getNewMessages = function() {
            var source = new EventSource(contextPath + '/rest/message/getNewMessages');
            source.onmessage = function(event) {
                var newMessage = JSON.parse(event.data);
                var messageTemplate = '<h4><strong>:userName</strong></h4><div class="alert :messageStyle"><strong>:message</strong>&nbsp;<span class="badge">:dateCreated</span></div>';
                var messageStyle = 'alert-info';
                if ($('#userName').val() !== newMessage.userName) {
                    messageStyle = 'alert-warning';
                }
                $('#messages').append(
                        messageTemplate.replace(':messageStyle', messageStyle).replace(':userName', newMessage.userName)
                                .replace(':message', newMessage.message).replace(':dateCreated',
                                        Date.create(newMessage.dateCreated.iMillis).format('{HH}:{mm}')));
                window.scrollTo(0, document.body.scrollHeight);
                notifyNewMessage(newMessage);
            }
            source.onerror = function(event) {
                source.close();
            }
        };

        var notifyNewMessage = function(message) {
            if (!('Notification' in window)) {
                return;
            }
            if (Notification.permission !== 'granted') {
                Notification.requestPermission();
            }
            notification = new Notification(message.userName, {
                body : message.message
            });
            notification.onshow = function() {
                setTimeout(function() {
                    notification.close()
                }, 5000);
            };
        };

        $('#sendMessage').click(function() {
            var message = {};
            message.userName = $('#userName').val();
            message.message = $('#message').val();
            if (message.userName === '' || message.message === '' || message.message.charCodeAt(0) === 10) {
                return;
            }
            $.ajax({
                type : 'post',
                url : contextPath + '/rest/message/sendMessage',
                data : JSON.stringify(message),
                contentType : 'application/json;charset=UTF-8'
            });
            $('#message').val('');
            $('#message').focus();
        });

        $("#message").keyup(function(event) {
            if (event.which == 13) {
                $('#sendMessage').click();
            }
        });

        getNewMessages();
  </script>
</body>
</html>
  • EventSource 오브젝트는 HTML5 표준에 명시된 오브젝트이지만 Internet Explorer에서는 지원되지 않는다. 그렇다고 실망할 필요는 없다. 고맙게도 EventSource 오브젝트를 에뮬레이션해주는 Polyfill 버전이 다수 존재한다. v0.2에서는 Yaffle이 제공하는 eventsource.min.js를 사용하여 크로스 브라우징 SSE를 구현하였다.
  • notifyNewMessage() 메써드는 새로 도착한 메시지에 대해 데스크탑에 팝업으로 알림 기능을 수행한다. 앞서 소개한 이를 위해 HTML5 Web Notifications API를 사용하였다. SSE와 마찬가지로 Internet Explorer에서는 지원되지 않는다.

실행 화면



개선할 점

  • SSE의 크로스 브라우징 측면에서 Polyfill 스크립트의 사용은 소극적인 처방이라고 할 수 있다. 이를 지원하는 Atmosphere라는 강력한 프레임워크가 존재한다. 추후 이를 적용한 새로운 예제를 선보일 예정이다.


저작자 표시 비영리 동일 조건 변경 허락
신고

Maven, Servlet 3, Jersey 2 기반의 Hello, World! 웹 프로젝트 작성하기

개요

스마트폰이 등장한 이래 웹 서비스를 소비하는 클라이언트의 종류가 다양해지면서 단순히 렌더링된 페이지를 보여주는 웹 서비스에서 API를 통해 다양한 클라이언트에 대응할 수 있는 RESTful Web Services의 중요성이 날로 높아지고 있다. 이러한 요구에 대응하기 위해 JAX-RS(Java API for RESTful Web Servies)Java EE 6부터 표준으로 포함되었다. Jersey는 바로 이 JAX-RS 표준을 충실히 구현한 웹 프레임워크이다. 이번 글에서는 Maven, Servlet 3, Jersey 2 기반의 Hello, World! 웹 프로젝트를 작성해 보고자 한다. Eclipse IDE for Java EE Developers가 설치되어 있어야 한다. Eclispe EE의 설치 방법은 이 글을, Maven 프로젝트 생성은 이 글을 참고한다.

/pom.xml

본격적인 소스 코드 작성에 앞서 관련 라이브러리에 대한 의존성 정보를 추가해야 한다. pom.xml 파일을 열고 /project/dependencies 엘레먼트에 아래 내용을 추가해야 한다. 저장 후 프로젝트명 우클릭하고 Run As -> Maven install을 실행한다.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.jsonobject</groupId>
    <artifactId>helloworld</artifactId>
    <packaging>war</packaging>
    <version>0.0.1-SNAPSHOT</version>
    <name>HelloWorld Maven Webapp</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet-core</artifactId>
            <version>2.17</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet</artifactId>
            <version>2.17</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-sse</artifactId>
            <version>2.17</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
            <version>2.17</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.3</version>
        </dependency>
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.7</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.3.1</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>helloworld</finalName>
    </build>
</project>
  • javax.servlet-apiServlet 3.1을 사용하기 위한 필수 구성이다.
  • jersey-container-servlet-core, jersey-container-servletJersey 프레임워크를 사용하기 위한 필수 구성이다.
  • jersey-media-sseServer-Sent Event를 다룰 경우 필요하며 jersey-media-json-jackson은 요청 데이터와 결과 데이터로 JSON을 다룰 경우 필요하다.
  • junit은 테스트를 위해, logback-classic은 로깅을 위한 구성이다.
  • joda-timeJava 8 미만의 개발환경에서 날짜와 시간을 쉽게 다루기 위한 구성이다.

/src/main/webapp/WEB-INF/web.xml

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    metadata-complete="true" version="3.1">
    <display-name>HelloWorld</display-name>
</web-app>
  • Servelt 3의 장점을 살려 web.xml 설정은 display-name을 주는 것만으로 충분하다. SevletContainer 설정은 클래스로 대신 작성한다.

/src/main/java/com/jsonobject/helloworld/config/ApplicationConfig.java

package com.jsonobject.helloworld.config;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/rest")
public class ApplicationConfig extends ResourceConfig {
    public ApplicationConfig() {
        packages("com.jsonobject.helloworld.resource");
    }
}
  • web.xml 설정에서 생략한 SevletContainer 설정을 여기에 작성한다. URI=/rest로 들어오는 모든 요청을 com.jsonobject.helloworld.config 패키지의 Resource 역할을 담당하는 클래스로 분기한다.

/src/main/java/com/jsonobject/helloworld/resource/HelloWorldResource.java

package com.jsonobject.helloworld.resource;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/helloWorld")
public class HelloWorldResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String helloWrold() {
        return "Hello, World!";
    }
}
  • 한 눈에 봐도 서블릿 클래스만으로 RESTful Web Service를 작성할 때보다 훨씬 소스 코드가 간결하다는 것을 알 수 있을 것이다. 프레임워크가 눈에 보이지 않는 부분에서 대신 많은 번거로운 작업을 수행해준다.
  • 클래스, 메써드마다 @Path 어노테이션으로 특정 URI에 대한 요청을 처리할 수 있다. 위 예제는 클래스에서 /rest/helloWorld 요청을 받는다.
  • @Produces 어노테이션으로 요청에 대한 응답 데이터의 형식을 명시할 수 있다. 위 예제는 단순히 Hello, World! 문자열을 출력하므로 text/plain을 명시하였다. 한편, @Consumes 어노테이션으로 요청 데이터의 형식을 명시할 수 있다.
  • 작성이 완료되었으면 실행할 차례이다. 메뉴에서 Run -> Run as -> Run on Server를 실행한다. 브라우저에서 http://localhost:8080/helloworld/rest/helloworld 주소를 입력하면 화면에 Hello, World! 문자열이 출력되는 것을 확인할 수 있다.

참고 글


저작자 표시 비영리 동일 조건 변경 허락
신고