어느덧 이 컬럼을 시작한 지도 1년이 다 되어간다. 두 달에 한번 쓰는 것이라 부담이 없으리라 생각했는데, 그렇게 만만치만은 않은 것임을 새삼 느낀다.
지난 10월에 쓴 글에서 자바 EE 6 완결판을 소개하기를 고대한다고 했는데 실제로 스펙이 최종 승인됐다. 자바 EE 6 스펙 리드인 Roberto Chinnicij이 자기 블로그 포스트에서 이 기쁜(?) 소식을 타전했다. 급하게나마 자바 EE 6 API 문서 전체는http://javadoc.glassfish.org/javaee6/apidoc/에서 확인할 수 있다. 최종 스펙 문서와 참조 구현체인 글래스피시(GlassFish) V3 최종판(그리고 이를 지원하는 넷빈즈(NetBeans) 6.8 최종판)은 12월 10일 출시될 예정이다.
따라서 최종판 자바 EE 6를 완벽히 누리는 것은 다음 기회로 미루고, 이번에는 자바 EE 6의 일정에까지 영향을 준 의존성 주입(이하 DI) 기능에 대해 알아보자.
새 DI 맛보기
본격적으로 들어가기에 앞서, 넷빈즈 6.8(2009년 12월 3일 현재 RC1)을 http://netbeans.org/community/releases/68/에서 받아 설치해둔다(이때 함께 설치되는 글래스피시는 빌드 73이다).
애초 자바 EE 6의 DI는 JSR 299 Web Beans라는 이름으로 표준화가 진행되다가, Java Contexts and Dependency Injection for Java EE(이 이름이 너무 길어 대신 CDI라고 줄여 쓴다)라는 좀 더 구체적인 이름으로 바뀌었다. 하지만 JSR 299는 여전히 Web Beans라는 이름으로 자주 불린다(일종의 애칭처럼 되어버렸다). 해당 스펙 리드인 Gavin King(하이버네이트(Hibernate) 창시자로 유명하며, 최근 방한해 한국 개발자들에게도 더욱 친숙해졌다)이 JBoss에서 구현하는 참조 구현체 이름도 처음에는 Web Beans였다가 최근 스펙 이름이 바뀌면서 혼동을 피하기 위해 Weld로 바꾸었다(http://seamframework.org/Weld/WeldOverview참조).
이미 스프링 같은 DI 프레임워크가 많이 소개되어 DI 자체에 대한 설명은 차치하고, 대신 CDI를 어떻게 사용하는지 간단한 예제와 함께 살펴보려 한다. https://glassfish-samples.dev.java.net/에는 글래스피시를 기반으로 다양한 예제를 제공하는데, CDI에 대한 예제도 들어 있어서 http://weblogs.java.net/blog/rogerk/archive/2009/09/09/context-and-dependency-injection-jsr-299-and-servlets에 간략히 소개되기도 했다.
우리가 다룰 예제는 정확히 https://glassfish-samples.dev.java.net/source/browse/glassfish-samples/ws/javaee6/webbeans/webbeans-servlet/ 이하에서 볼 수 있지만, 코드를 편하게 보고 실제 실행하려면https://glassfish-samples.dev.java.net/source/browse/glassfish-samples/ 페이지에 나온 안내에 따라 CVS로 체크아웃해 받아보는 것이 편하다. 반드시 glassfish-samples라는 모듈 전체를 받기를 권하며, 그 밑으로 ws/javaee6/webbeans/webbeans-servlet 디렉터리를 넷빈즈에서 Open Project로 열면 간단히 실행까지 해볼 수 있다.

그림 1. webbeans-servlet 실행 결과 화면
위 화면에서 User Name과 Password는 아무것이나 입력해도 상관 없다. 단 입력하지 않으면 로그인이 안된다. 그러면 위와 같이 돌아가는 웹 애플리케이션을 바닥부터 만들어 보자.
우선, New Project -> Java Web -> Web Application을 선택하여 웹 애플리케이션 프로젝트를 생성한다. 프로젝트 이름(Project Name)은 webbeans-test로 하여 앞서 열어본 프로젝트의 이름을 피한다. Next 버튼을 누르면 Java EE Version을 선택하게 되는데 Java EE 6 Web임을 확인하고 넘어가면 된다.
프로젝트 생성을 마쳤으니 본격적으로 코드 작성에 돌입하자. 먼저 DI를 할 대상들로, 사용자 인증 정보를 나타내는 Credentials 클래스와 인증 처리를 하는 Login 클래스를 만드는데, 프로젝트 홈 밑의 Source Packages 밑으로 New > Java Class를 선택하여 아래와 같이 짠다(패키지를 webbeansservlet으로 하는 것도 잊지 말자).
Listing 1. Credentials.javapackage webbeansservlet;
import java.io.Serializable;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.inject.Default;
import javax.inject.Named;
@RequestScoped
public class Credentials implements Serializable {
private String username = null;
private String password = null;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
|
Credentials 클래스는 일반적인 자바 클래스(POJO) 같지만 @RequestScoped라는 CDI 어노테이션이 붙어 CDI의 범위를 한정한다. 또한 Serializable을 구현하여 보존성을 확보한다.
Listing 2. Login.javapackage webbeansservlet;
import java.io.Serializable;
import javax.enterprise.context.SessionScoped;
import javax.inject.Inject;
@SessionScoped
public class Login implements Serializable {
@Inject Credentials credentials;
private boolean loggedIn = false;
/**
* This is where you could potentially access a database.
*/
public void login() {
if ((credentials.getUsername() != null &&
credentials.getUsername().trim().length() > 0) &&
(credentials.getPassword() != null &&
credentials.getPassword().trim().length() > 0)) {
loggedIn = true;
}
}
public boolean isLoggedIn() {
return loggedIn;
}
}
|
Login 클래스는 Credentials와는 달리 @SessionScoped로 세션 범위임을 선언하는데, 그 안에 @Inject 어노테이션을 통해 credentials 필드에 DI를 하고 있다. 그러면 이제 Credentials와 Login을 웹 애플리케이션 수준에서 사용할 수 있도록 LoginServlet을 작성해보자.
프로젝트 홈 밑의 Source Packages 밑으로 New > Servlet을 선택하여 패키지 이름으로 webbeansservlet과 클래스 이름으로 LoginServlet을 입력한 다음, Servlet Name과 URL Pattern(s), Add information to deployment descriptor (web.xml) 설정은 주어진 대로 하고 넘어간다(이렇게 하면 web.xml이 생성되지 않고도 서블릿을 실행할 수 있다). 아래는 LoginServlet 코드다.
Listing 3. LoginServletpackage webbeansservlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name="LoginServlet", urlPatterns={"/LoginServlet"})
public class LoginServlet extends HttpServlet {
@Inject Credentials credentials;
@Inject Login login;
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8&quot ;
PrintWriter out = response.getWriter();
credentials.setUsername(request.getParameter("username&quot );
credentials.setPassword(request.getParameter("password&quot );
login.login();
try {
if (login.isLoggedIn()) {
out.println("Successfully Logged In As: " + credentials.getUsername());
} else {
out.println("Login Failed: Check username and/or password.&quot ;
}
} finally {
out.close();
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
}
|
이 서블릿의 credentials와 login 필드 모두 DI로 형성되는데, processRequest 안을 보면 요청으로 들어온 정보로 credentials를 채운다. 다음으로 login.login() 메서드로 로그인 처리를 호출하고 나서 login.isLoggedIn() 메서드로 로그인 상태에 따라 메시지를 반환한다.
여기서 기존 코딩 방식과 다른 점은, credentials를 채운 다음 login에 따로 넣어줄 필요가 없다는 점이다. 즉 credentials의 username과 password를 설정한 다음 login.login() 메서드를 부르는 시점에 이미 login의 credentials 필드도 같은 상태가 되어 있다. 특히 credentials가 요청 범위(Request Scope)이므로, processRequest 안에서는 똑같은 값으로 유지될 것이며, login은 세션 범위이므로 요청을 넘어 세션에 걸쳐 유지될 것이다. 이 서블릿을 부르는 HTML 페이지는 다음과 같다.
Listing 4. index.jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Web Beans Servlet Injection</title>
<script type="text/javascript" src="ajax.js">
</script>
</head>
<body>
<form name="myForm" method="POST" action="LoginServlet">
<table class="title-panel">
<tbody>
<tr>
<td><span class="title-panel-text">Login Servlet</span></td>
</tr>
<tr>
<td><span class="title-panel-subtext">Powered By Servlet 3.0 and Web Beans</span></td>
</tr>
</tbody>
</table>
<table height="30" style="font-size: 16px">
<tr>
<td>Enter any value for user name and password.</td>
</tr>
</table>
<table style="font-size: 16px">
<tr>
<td style="color:red">*</td>
<td>Denotes required entry.</td>
</tr>
</table>
<table height="30">
<table border="1" style="font-size: 18px">
<tr>
<td>User Name:</td>
<td><input type="text" name="username" id="username" /></td>
<td style="color:red">*</td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" name="password" id="password" /></td>
<td style="color:red">*</td>
</tr>
</table>
<table border="1">
<tr>
<td colspan="2"><input type="button" value="Submit" onclick="ajaxFunction();" /></td>
<td colspan="2"><input type="button" value="Reset" onclick="resetFunction();" /></td>
</tr>
</table>
</table>
<table height="20">
<tr>
<td><div id="message" style="color:red;font-size: 14px"></td>
</tr>
</table>
</form>
</body>
</html>
|
이 파일은 프로젝트 루트의 Web Pages 밀에 이미 생성되어 있으므로 위와 같이 바꾸기만 하면 된다. 위 페이지는 Ajax를 사용하는데 자바스크립트 코드는 ajax.js라는 파일로 따로 두었다.
Listing 5. ajax.js function getXMLObject() {
var xmlHttp = false;
try {
xmlHttp = new ActiveXObject("Msxml2.XMLHTTP&quot // For Old Microsoft Browsers
} catch (e) {
try {
xmlHttp = new ActiveXObject("Microsoft.XMLHTTP&quot // For Microsoft IE 6.0+
} catch (e2) {
xmlHttp = false // No Browser accepts the XMLHTTP Object then false
}
}
if (!xmlHttp && typeof XMLHttpRequest != 'undefined') {
xmlHttp = new XMLHttpRequest(); //For Mozilla, Opera Browsers
}
return xmlHttp; // Mandatory Statement returning the ajax object created
}
var xmlhttp = new getXMLObject();
function ajaxFunction() {
if(xmlhttp) {
var username = document.getElementById("username&quot ;
var password = document.getElementById("password&quot ;
xmlhttp.open("POST","LoginServlet",true); //getname will be the servlet name
xmlhttp.onreadystatechange = handleServerResponse;
xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xmlhttp.send("username=" + username.value + "&password=" + password.value);
}
}
function handleServerResponse() {
if (xmlhttp.readyState == 4) {
if(xmlhttp.status == 200) {
document.getElementById("message&quot .innerHTML=xmlhttp.responseText;
} else {
alert("Error during AJAX call. Please try again&quot ;
}
}
}
function resetFunction() {
document.getElementById("username&quot .value="";
document.getElementById("password&quot .value="";
document.getElementById("message&quot .innerHTML="";
}
|
이 파일을 프로젝트 루트의 Web Pages에서 New > Web > JavaScript File을 선택하여 생성하면 된다. 그런데 이와 같이 다 된 듯하여 실행해 보면 정작 DI가 되질 않는다. 결론부터 말하면, WEB-INF 프로젝트 루트의 Web Pages > WEB-INF 디렉터리 밑에 beans.xml이라는 파일을 다음과 같은 내용으로 작성해야 한다.
Listing 6. beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans/>
|
이렇게 하고 나서 빌드와 배포를 한 다음 http://localhost:8080/webbeans-test/를 브라우저로 열면 맨 앞에서 본 것과 비슷한(똑같지는 않다) 화면과 처리가 된다.
좀 더 범용적인 DI로 돌아오다