ZK OSS는 Direct RIA 프레임워크다.
Direct RIA가 몬지. ZK가 몬지는 담에 기회에 있으면 포스팅하기로 하고,
이번 포스트는 zkoss에서 스프링을 사용하기 위한 설정만 살펴보기로 하자.
스프링 설정하는 건 다들 아는 것일테고, 간단히 zkoss에서 스프링 빈을 사용하는 방법에 대해서만 살펴보기로 하자.
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<description><![CDATA[springsample]]></description>
<display-name>springsample</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
WEB-INF/zkoss-config.xml
</param-value>
</context-param>
<listener>
<listener-class>org.zkoss.spring.web.context.CoreContextListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<description>ZK loader for ZUML pages</description>
<servlet-name>zkLoader</servlet-name>
<servlet-class>org.zkoss.zk.ui.http.DHtmlLayoutServlet</servlet-class>
<init-param>
<param-name>update-uri</param-name>
<param-value>/zkau</param-value>
</init-param>
<load-on-startup>1</load-on-startup><!-- Must -->
</servlet>
<servlet-mapping>
<servlet-name>zkLoader</servlet-name>
<url-pattern>*.zul</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>zkLoader</servlet-name>
<url-pattern>*.zhtml</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>zkLoader</servlet-name>
<url-pattern>/zk/*</url-pattern>
</servlet-mapping>
<servlet>
<description>The asynchronous update engine for ZK</description>
<servlet-name>auEngine</servlet-name>
<servlet-class>org.zkoss.zk.au.http.DHtmlUpdateServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>auEngine</servlet-name>
<url-pattern>/zkau/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>dspLoader</servlet-name>
<servlet-class>org.zkoss.web.servlet.dsp.InterpreterServlet</servlet-class>
<init-param>
<param-name>class-resource</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dspLoader</servlet-name>
<url-pattern>*.dsp</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.zul</welcome-file>
<welcome-file>index.zhtml</welcome-file>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
</welcome-file-list>
</web-app>
web.xml 파일내에서 가장 중요한 것은 리스너 두개의 순서다. !!!
이거 무시하면 띠바 겁내 헤롱거릴거다!
반드시
org.zkoss.spring.web.context.CoreContextListener 먼저 설정 해주고
org.springframework.web.context.ContextLoaderListener 는 나중에 설정해 줘야한다.
나머지는 가이드 문서에 나와있는데로 설정하면 별 문제 없다.
zkoss-config.xml (web.xml 파일에서 지정한 파일)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:zksp="http://www.zkoss.org/2008/zkspring/core"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.zkoss.org/2008/zkspring/core
http://www.zkoss.org/2008/zkspring/core/zkspring-core.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:annotation-config />
<context:component-scan base-package="org.zkoss.zkspringessentials.controller,org.zkoss.spring.beans.zkcomponents,com.archnal.zkoss"></context:component-scan>
<zksp:zk-config/>
</beans>
GreetingController.java
package com.archnal.zkoss.springsample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.zkoss.spring.context.annotation.EventHandler;
import org.zkoss.spring.util.GenericSpringComposer;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zul.Button;
import org.zkoss.zul.Messagebox;
import org.zkoss.zul.Textbox;
@Component("greetingController")
@Scope("desktop")
public class GreetingController extends GenericSpringComposer {
@Autowired
private Textbox name;
@Autowired
private Button greetBtn;
@Override
public void doAfterCompose(org.zkoss.zk.ui.Component comp) throws Exception {
super.doAfterCompose(comp);
}
@EventHandler("greetBtn.onClick")
public void showGreeting(Event event) throws WrongValueException, InterruptedException {
System.out.println("showGreeting");
Messagebox.show("Hello " + name.getValue() + "!");
}
}
greeting.zul
<?page title="Greeting " contentType="text/html;charset=UTF-8"?>
<?variable-resolver class="org.zkoss.zkplus.spring.DelegatingVariableResolver"?>
<window title="Autowire ZK Components Example" border="normal"
height="100px" width="400px" apply="${greetingController }">
<label value="Name:"></label>
<textbox id="name"/>
<button id="greetBtn" label="Greet!" />
</window>
위와 같이 설정하면 zkoss에서 스프링 프레임워크를 마음 껏 사용할 수 있다.~~
2011년 11월 7일 월요일
2011년 10월 18일 화요일
private 접근자를 가진 객체의 필드에 접근하기.
가끔 오픈 소스나 라이브러리를 가지고 개발해야 하는 경우에 특정 객체의 필드에 접근해야 할 필요가 있다.
아래의 샘플 코드를 살펴 보자.
위의 클래스는 프로퍼티를 추가하는 insert 메서드와 설정된 정보를 출력하는 display 메서드만 제공하는 간단한 클래스다.
그런데 insert 메서드에서는 이미 키가 존재하는 경우에 IllegalStateException을 발생시키기 때문에 기존에 설정된 프로퍼티값을 변경할 수가 없다.
InsertableContainer를 설계할 당시에는 평생 25살로 살거라 생각했었는데, 어느덧 35살이 되었기 때문에 age 속성을 변경해야할 필요가 생겼다.
public 접근자를 가진 메서드만 가지고는 age를 수정시킬 방법이 없다.
아래의 테스트 코드는 IllegalArgumentException을 발생시킨다.
아래의 소스 코드를 살펴보자.
리플렉션을 이용하여 InsertableContainer의 private 접근자를 가진 'map' 필드를 읽어 내고 있다.
accessible을 false 로 지정하거나, 아예 지정하지 않으면 private Field에 접근할 수 없기 때문에 아래와 같이 오류가 발생한다.
"java.lang.IllegalAccessException: Class test.java.FieldTest can not access a member of class test.java.InsertableContainer with modifiers "private"".
테스트 소스 코드를 아래와 같이 수정하면 이제 오류가 발생하지 않고 정상적으로 동작한다.
{lastName=Kim, age=25, firstName=Younggyu}
{lastName=Kim, age=35, firstName=Younggyu}
아래의 샘플 코드를 살펴 보자.
package test.java; import java.util.HashMap; import java.util.Map; public class InsertableContainer { private Mapmap = new HashMap (); public InsertableContainer() { map.put("firstName", "Younggyu"); map.put("lastName", "Kim"); map.put("age", "25"); } public void insert(String key, String value) { if(map.containsKey(key)) { throw new IllegalArgumentException("key already exists: " + key); } map.put(key, value); } public void display() { System.out.println(map); } }
그런데 insert 메서드에서는 이미 키가 존재하는 경우에 IllegalStateException을 발생시키기 때문에 기존에 설정된 프로퍼티값을 변경할 수가 없다.
InsertableContainer를 설계할 당시에는 평생 25살로 살거라 생각했었는데, 어느덧 35살이 되었기 때문에 age 속성을 변경해야할 필요가 생겼다.
public 접근자를 가진 메서드만 가지고는 age를 수정시킬 방법이 없다.
아래의 테스트 코드는 IllegalArgumentException을 발생시킨다.
@Test(expected=IllegalArgumentException.class) public void testField() { InsertableContainer container = new InsertableContainer(); container.display(); container.insert("age", "35"); }이런 경우에 Reflection을 이용하여 InsertableContainer의 map 필드를 읽어 내서 수정하는 방법을 고려해 보자.
아래의 소스 코드를 살펴보자.
리플렉션을 이용하여 InsertableContainer의 private 접근자를 가진 'map' 필드를 읽어 내고 있다.
@SuppressWarnings("unchecked") private Map위의 소스 코드에서 field.setAccessible(true); 소스 코드가 private 필드에 접근이 가능하게 한다.getContainerMap(InsertableContainer container) { Map map = null; try { Field field = InsertableContainer.class.getDeclaredField("map"); field.setAccessible(true); map = (Map )field.get(container); } catch(Exception ex) { ex.printStackTrace(); } return map; }
accessible을 false 로 지정하거나, 아예 지정하지 않으면 private Field에 접근할 수 없기 때문에 아래와 같이 오류가 발생한다.
"java.lang.IllegalAccessException: Class test.java.FieldTest can not access a member of class test.java.InsertableContainer with modifiers "private"".
테스트 소스 코드를 아래와 같이 수정하면 이제 오류가 발생하지 않고 정상적으로 동작한다.
@Test() public void testField2() { InsertableContainer container = new InsertableContainer(); container.display(); Mapmap = getContainerMap(container); map.put("age", "35"); container.display(); }
화면 출력은 아래와 같다.
{lastName=Kim, age=25, firstName=Younggyu}
{lastName=Kim, age=35, firstName=Younggyu}
2011년 10월 13일 목요일
How to use mybatis mapper
mybatis를 이용하면 XML 매핑 파일을 작성하는 대신 annotation 이용하여 개발이 가능하다.
SQL 코드를 클래스로부터 분리하여 별도의 리소스(sqlmap xml)파일로 관리할 수 있도록 하는 방식에서
인터페이스 안에 매핑정보를 넣어야 하는 게 올바른 방향인지는 궁금하다.
쿼리가 변경되면 인터페이스를 재 컴파일해서 올려야 할 듯.
아무튼 간단히 개발하는 곳에서는 적용해볼만 한듯해서 관련 소스를 올려둔다.
- 스프링 설정 파일
스프링 설정 파일에서는 dataSource, sqlSessionFactory 및 매퍼 인터페이스를 지정해야 한다.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <context:component-scan base-package="com.archnal.samples.mybatis"/> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> <context:property-placeholder location="spring/jdbc.properties" /> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> </bean> <bean id="accountMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="com.archnal.samples.mybatis.mapper.AccountMapper" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean> </beans>
- Mapper 인터페이스
Mapper 인터페이스에서는 mybatis에서 제공하는 @Select, @Insert 등의 annotation을 이용하여 쿼리를 작성할 수 있다.
package com.archnal.samples.mybatis.mapper; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import com.archnal.samples.mybatis.dto.Account; public interface AccountMapper { String INSERT = "INSERT INTO tbl_account (name, email) values (#{name}, #{email})"; @Select("SELECT * FROM tbl_account WHERE email = #{email}") Account getByEmail(@Param("email") String email); @Insert(INSERT) void insert(Account account); }
- test 클래스
아래의 소스 코드에서 처럼 실행하면 실제로 데이터베이스에서 값을 읽어 오거나 추가시킬 수 있다.
package com.archnal.samples.mybatis.mapper; import javax.annotation.Resource; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.transaction.TransactionConfiguration; import org.springframework.transaction.annotation.Transactional; import com.archnal.samples.mybatis.dto.Account; @ContextConfiguration(locations={"classpath*:spring/*-config.xml"}) @RunWith(SpringJUnit4ClassRunner.class) @TransactionConfiguration(transactionManager="transactionManager", defaultRollback=true) @Transactional(readOnly=true) public class AccountMapperTest { @Resource(name="accountMapper") AccountMapper accountMapper; @Test public void testGetByEmail() throws Exception { String email = "crexxx@gmail.com"; Account account = accountMapper.getByEmail(email); System.out.println("account: " + account); } @Test @Rollback(true) @Transactional(readOnly=false) public void testInsert() throws Exception { String name = "ko"; String email = "koxxx@gmail.com"; Account account = new Account(); account.setName(name); account.setEmail(email); accountMapper.insert(account); } }
Constants Utiltity class in spring core
스프링 프레임워크에 org.springframework.core.Constants 라는 클래스를 우연히 보게 되었다.
다른 클래스의 상수 값을 읽어내는 유틸리티 클래스다.
package spring.core; public class MyConstants { public static final int SIZE = 10; public static final String HOSTS_PATH = "/etc/hosts"; public static final String DB_TABLE_USER = "tbl_user"; public static final String DB_TABLE_MENU = "tbl_menu"; public static final String DB_TABLE_AUTH = "tbl_auth"; }
위와 같이 상수를 정의한 클래스가 있다고 하자.
org.springframework.core.Constants 클래스는 생성자로 상수가 정의되어 있는 클래스를 인자로 받는다.
숫자와 문자열로 정의된 상수값을 읽어 올 수 있다.
또한 prefix를 기준으로 상수를 그루핑할 수 있다. MyConstants 클래스에서는 'DB_TABLE_' 문자열을 prefix로 사용할 수 있다.
잘못된 상수명을 사용한다면 리턴 값이 null이 아니라 org.springframework.core.ConstantException 이 throw 된다는 점은 유의해야 한다.
Constants constants = new Constants(MyConstants.class);
숫자 값을 읽을 때는 아래와 같이 사용한다.
Number number = constants.asNumber("SIZE");
문자열 값을 읽을 때는 아래와 같이 사용한다.
String hostsPath = constants.asString("HOSTS_PATH");
상수명을 그룹핑 할 때는 아래와 같이 사용한다.
Set
package spring.core; import java.util.Set; import org.junit.Assert; import org.junit.Test; import org.springframework.core.Constants; public class ConstantsTest { @Test public void testConstants() throws Exception { Constants constants = new Constants(MyConstants.class); String hostsPath = constants.asString("HOSTS_PATH"); System.out.println(hostsPath); Assert.assertEquals(MyConstants.HOSTS_PATH, hostsPath); Number number = constants.asNumber("SIZE"); System.out.println("number: " + number); Assert.assertEquals(MyConstants.SIZE, number.intValue()); Setnames = null; names = constants.getNames("DB_TABLE_"); System.out.println(names); Assert.assertEquals(3, names.size()); names = constants.getNamesForProperty("dbTable"); System.out.println(names); Assert.assertEquals(3, names.size()); } }
2011년 9월 15일 목요일
PDE 개발 시 다른 PDE 프로젝트의 클래스 import
com.archnal.affogato.ui 라는 Eclipse plugin 프로젝트를 개발하면서,
공통적으로 자주 사용할 만한 PDE관련 클래스를 따로 묶어서 새로운 프로젝트를 만들고 싶었다.
com.archnal.pde라는 프로젝트에 com.archnal.pde.util 이라는 패키지를 만들어서
유틸리티 클래스를 몇개 구현한 후
com.archnal.affogato.ui 프로젝트에서 위의 패키지(com.archnal.pde.util)의 클래스를 import 하려는 데, 아래와 같은 컴파일 에러가 뜬다.
"Access restriction: The type EventUtil is not accessible due to restriction on required project com.archnal.pde"
com.archnal.affogato.ui 프로젝트의 Properties 메뉴에서 Project References 에서 com.archnal.pde 프로젝트를 선택해도 에러가 사라지지 않는다.
에러가 발생 한 import 문에 마우스 커서를 위치하면 아래와 같은 Quick Fix 구문이 나타난다.
"Export the 'com.archnal.pde.util' package from the 'com.archnal.pde' plugin"
위의 라벨을 클릭하면 com.archnal.pde 프로젝트의 plugin.xml 파일의 Runtime 탭의 Exported Packages 섹션에
com.archnal.pde.util 패키지가 자동으로 포함된다.
com.archnal.pde 프로젝트의 plugin.xml 파일을 열어서 Add 버튼을 클릭하여 수동으로 패키지를 추가해도 동일하다.
2011년 9월 5일 월요일
JDT 개발 시 plugin 소스 코드 보기
이클립스에서 JDT 플러그인 개발을 할 때 참조할 만한 가장 훌륭한 소스는
org.eclipse.jdt.ui 플러그인에 있는 소스다.
구글에서 여러 번 검색을 해 보고 따라 해 봤지만 번번히 실패를 했다.
아래의 링크에 있는 대로 실행해 봤다.
http://musingsofaprogrammingaddict.blogspot.com/2010/08/retrieving-jdt-sources-with-eclipse-36.html
위의 포스트의 댓글에도 있는 것처럼 방법은 간단한데 동작하지 않는단다.
나도 그랬다. 이것저것 설정을 바꿔 봤는데 잘 동작하지 않았다.
방법은 간단하다.
plug Ins 뷰 (뷰가 보이지 않거든 Window - Show View - Plug-in Development 에서 선택) 에서
참조하고자 하는 소스코드가 있는 플러그인(ex org.eclipse.jdt.ui) 을 선택한 후 오른쪽 마우스 버튼을 클릭하여
Import As - Project from a repository 메뉴를 실행시킨다.
워크스페이스에 org.eclipse.jdt.ui라는 프로젝트가 생성이 되었지만 여전히 자바 소스 코드를 내려 받지 않았다.
Import AS의 다른 메뉴를 실행하다 다시 위의 메뉴를 실행 보니 CVS 아이디와 비밀번호를 묻는 창이 띄워졌다.
아이디 입력란에 anonymous라고 입력하고 비밀번호는 비워둔 채로 OK 버튼을 클릭하니까
나의 로컬 워크스페이스에 org.eclipse.jdt.ui 라는 프로젝트가 생성되고 관련 자바 소스 코드가 내려 받아 졌다.
뒷걸음치다 쥐 잡은 꼴이지만 암튼 도움이 되시길..
참고로
Window - Preferences - Plug-in Developement 의 Source (Show Source plugins) 체크 박스도 설정해 보기도 하고 안 하기도 하고 그랬다. ^^
2011년 8월 29일 월요일
PDE launch 시 OutOfMemory 오류
원본: http://blog.outsider.ne.kr/497
플러그인 개발 하는 데 아래와 같은 에러가 날 때...
java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=256m VM 옵션 추가
2011년 8월 5일 금요일
jQuery ajax에서 dataType을 json으로 설정...
원본 소스: http://www.ibm.com/developerworks/kr/library/wa-aj-jsonp1/
0jQuery.getJSON( url, [data,] [success(data, textStatus, jqXHR)] ) 는 아래의 코드와 동일하다.
$.ajax({
url: url,
dataType: 'json',
data: data,
success: callback
});
dataType을 'json'으로 설정하고 스프링 컨트롤러에서 객체를 JSON 스트링으로 변환하여 서블릿의 outputStream에 쓰면 그걸로 되는 줄 알았다.
근데 자꾸 success 콜백이 호출되는 것이 아니구 error 콜백이 호출되는 것이다.
이래저래 문서를 찾아 보니 JSONP(JSON with Padding)과 관련이 있다는 걸 우연히 알고 문서를 좀 찾아 봤다.
JSONP에 대해 따로 포스트를 작성할 계획이 있는 건 아니라서 간단히 살펴보기로 한다.
브라우저의 보안 정책 중에 same-origin policy라는 게 있다. XMLHttpRequest는 같은 도메인으로만 요청을 할 수 있다는 제약이다. 이걸 해결하기 위해서 아래의 3가지 방법을 사용한다.
1. Proxy - third-party server로 요청을 보내기 위해서 같은 도메인에 프록시 서버를 개발한다.
2. IFrame - 요청을 보이지 않는 iframe을 이용해서 호출한다. 실행은 그럭저럭 하더라도 값을 가져오는 것은 역시 보안에 위배된다.
3. 동적 스크립트 삽입 - 자바스크립트는 same-origin policy 정책이 적용되지 않는다.
JSONP는 동적 스크립트 삽입 기능을 이용한다.
jQuery.ajax에서 dataType을 'json'으로 설정하면 jquery는 callback이라는 파라미터를 Timestamp를 이용하여 중복되지 않은 함수명으로 만들어 request의 파라미터로 전달한다.
서버쪽에서는 아래의 두가지를 작업 해 주어야한다.
1. contentType을 text/javascript 로 설정한다.
2. request의 파라미터로 전달된 callback 함수를 호출한다. 함수의 파라미터로 json 객체를 전달한다. 아마도 하나만 전달할 수 있을 것 같다(이건 확인 해야 하는데... success의 콜백에서 첫번째 파라미터가 서버로부터 리턴되는 데이터 객체이니깐 아마도 그럴거라고 믿고 있다.)
서버 측 소스 코드는 아래와 같다.
response.setContentType("text/javascript");
String result = callback + "(" + sb.toString() + ")"; // sb에는 json을 표현하는 문자열
PrintWriter pw = response.getWriter();
pw.print(result);
클라이언트 소스는 아래와 같다.
success 콜백으로 전달되는 data는 자바스크립트 객체다.
$.ajax({
type: 'post',
url: "${ctx}/myController.sh?method=getAdminJson",
data: "adminId=" + f.adminId.value,
dataType : "json",
error:function(){alert("Error");},
success:function(data){
var ospData = data;
if( ospData.returnVal == "Y"){
alert("Success");
}
}
});
만약에 서버측에서 JSONP 형식으로 javascript callback 메서드를 호출하는 방식이 아니라
JSON 문자열을 서블릿 응답 바디에 쓰는 방식으로 호출한다면 클라이언트 소스를 아래와 같이 호출하면 된다.
dataType을 'text'로 변경하고 success 메서드의 파라미터로 전달되는 data는 단순 문자열이기 때문에
javascript 개체로 변경하기 위해서는 eval( '(' + jsonString + ')' ) 메서드로 변환해야 한다.
$.ajax({
type: 'post',
url: "${ctx}/myController.sh?method=getAdminJson",
data: "adminId=" + f.adminId.value,
dataType : "text",
error:function(){alert("Error");},
success:function(data){
var ospData = eval( '(' + data + ')' );
if( ospData.returnVal == "Y"){
alert("Success");
}
}
});
Have a nice day.
0jQuery.getJSON( url, [data,] [success(data, textStatus, jqXHR)] ) 는 아래의 코드와 동일하다.
$.ajax({
url: url,
dataType: 'json',
data: data,
success: callback
});
dataType을 'json'으로 설정하고 스프링 컨트롤러에서 객체를 JSON 스트링으로 변환하여 서블릿의 outputStream에 쓰면 그걸로 되는 줄 알았다.
근데 자꾸 success 콜백이 호출되는 것이 아니구 error 콜백이 호출되는 것이다.
이래저래 문서를 찾아 보니 JSONP(JSON with Padding)과 관련이 있다는 걸 우연히 알고 문서를 좀 찾아 봤다.
JSONP에 대해 따로 포스트를 작성할 계획이 있는 건 아니라서 간단히 살펴보기로 한다.
브라우저의 보안 정책 중에 same-origin policy라는 게 있다. XMLHttpRequest는 같은 도메인으로만 요청을 할 수 있다는 제약이다. 이걸 해결하기 위해서 아래의 3가지 방법을 사용한다.
1. Proxy - third-party server로 요청을 보내기 위해서 같은 도메인에 프록시 서버를 개발한다.
2. IFrame - 요청을 보이지 않는 iframe을 이용해서 호출한다. 실행은 그럭저럭 하더라도 값을 가져오는 것은 역시 보안에 위배된다.
3. 동적 스크립트 삽입 - 자바스크립트는 same-origin policy 정책이 적용되지 않는다.
JSONP는 동적 스크립트 삽입 기능을 이용한다.
jQuery.ajax에서 dataType을 'json'으로 설정하면 jquery는 callback이라는 파라미터를 Timestamp를 이용하여 중복되지 않은 함수명으로 만들어 request의 파라미터로 전달한다.
서버쪽에서는 아래의 두가지를 작업 해 주어야한다.
1. contentType을 text/javascript 로 설정한다.
2. request의 파라미터로 전달된 callback 함수를 호출한다. 함수의 파라미터로 json 객체를 전달한다. 아마도 하나만 전달할 수 있을 것 같다(이건 확인 해야 하는데... success의 콜백에서 첫번째 파라미터가 서버로부터 리턴되는 데이터 객체이니깐 아마도 그럴거라고 믿고 있다.)
서버 측 소스 코드는 아래와 같다.
response.setContentType("text/javascript");
String result = callback + "(" + sb.toString() + ")"; // sb에는 json을 표현하는 문자열
PrintWriter pw = response.getWriter();
pw.print(result);
클라이언트 소스는 아래와 같다.
success 콜백으로 전달되는 data는 자바스크립트 객체다.
$.ajax({
type: 'post',
url: "${ctx}/myController.sh?method=getAdminJson",
data: "adminId=" + f.adminId.value,
dataType : "json",
error:function(){alert("Error");},
success:function(data){
var ospData = data;
if( ospData.returnVal == "Y"){
alert("Success");
}
}
});
만약에 서버측에서 JSONP 형식으로 javascript callback 메서드를 호출하는 방식이 아니라
JSON 문자열을 서블릿 응답 바디에 쓰는 방식으로 호출한다면 클라이언트 소스를 아래와 같이 호출하면 된다.
dataType을 'text'로 변경하고 success 메서드의 파라미터로 전달되는 data는 단순 문자열이기 때문에
javascript 개체로 변경하기 위해서는 eval( '(' + jsonString + ')' ) 메서드로 변환해야 한다.
$.ajax({
type: 'post',
url: "${ctx}/myController.sh?method=getAdminJson",
data: "adminId=" + f.adminId.value,
dataType : "text",
error:function(){alert("Error");},
success:function(data){
var ospData = eval( '(' + data + ')' );
if( ospData.returnVal == "Y"){
alert("Success");
}
}
});
Have a nice day.
2011년 8월 2일 화요일
SSL(HTTPS) 연동을 위한 설정.
꽤나 복잡하네... 한참 작성해야 할듯
"No trusted certificate found" 이런 에러가 뜰 때
이런 메시지가 뜰 때 아래와 같이 TrustManager를 새로 만들어서 등록함.
SSLContext ctx = SSLContext.getInstance("TLSv1");
X509TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
};
// ctx.init(null, tmf.getTrustManagers(), null);
ctx.init(null, new TrustManager[] { tm }, null);
SSLSocketFactory sslsocketfactory = ctx.getSocketFactory();
2. 아... CA를 확인할 수 없다고 할때 메시지는 모라고 뜨는지 기억이 안난다.
암튼 그런 인증서 (사설인증서는) CA가 없기 때문에 JRE_HOME/lib/security/cacerts 파일에 인증서를 등록해주면 JKS에서 인증서의 CA를 별도로 해주지 않아도 된다능..
아래와 같이 한다.
keytool -keystore D:\Programming\Java\jdk1.6.0_26\jre\lib\security\cacerts -import -file D:\TEMP\spp\mycert.cer -alias myalias
3. 이건 키 스토어에 인증서 추가하는 명령어
keytool -import -trustcacerts -keystore mykeystore.jks -storepass mypasswd -alias myalias -file mycert.cer -noprompt
"No trusted certificate found" 이런 에러가 뜰 때
이런 메시지가 뜰 때 아래와 같이 TrustManager를 새로 만들어서 등록함.
SSLContext ctx = SSLContext.getInstance("TLSv1");
X509TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
};
// ctx.init(null, tmf.getTrustManagers(), null);
ctx.init(null, new TrustManager[] { tm }, null);
SSLSocketFactory sslsocketfactory = ctx.getSocketFactory();
2. 아... CA를 확인할 수 없다고 할때 메시지는 모라고 뜨는지 기억이 안난다.
암튼 그런 인증서 (사설인증서는) CA가 없기 때문에 JRE_HOME/lib/security/cacerts 파일에 인증서를 등록해주면 JKS에서 인증서의 CA를 별도로 해주지 않아도 된다능..
아래와 같이 한다.
keytool -keystore D:\Programming\Java\jdk1.6.0_26\jre\lib\security\cacerts -import -file D:\TEMP\spp\mycert.cer -alias myalias
3. 이건 키 스토어에 인증서 추가하는 명령어
keytool -import -trustcacerts -keystore mykeystore.jks -storepass mypasswd -alias myalias -file mycert.cer -noprompt
2011년 8월 1일 월요일
tomcat6.0 + apache2.2 연동
mod_jk binary 내려 받기
- http://archive.apache.org/dist/tomcat/tomcat-connectors/jk/binaries/linux/
httpd conf 폴더
vi conf/workers.properties
------------------------------------------------------
worker.list=worker1
worker.worker1.port=8080
worker.worker1.host=70.7.105.162
worker.worker1.type=ajp13
worker.worker1.lbfactor=1
vi conf/uriworkermap.properties
------------------------------------------------------
/mycontextroot/dwr/*=worker1
/mycontextroot/ws*=worker1
/mycontextroot/*.do=worker1
/mycontextroot/*.jsp=worker1
httpd conf.d 폴더
mod_jk.conf
------------------------------------------------------
LoadModule jk_module modules/mod_jk.so
<IfModule jk_module>
JkWorkersFile conf/workers.properties
JkLogFile logs/mod_jk.log
JkLogLevel info
JkMountFile conf/uriworkermap.properties
</IfModule>
~
- http://archive.apache.org/dist/tomcat/tomcat-connectors/jk/binaries/linux/
httpd conf 폴더
vi conf/workers.properties
------------------------------------------------------
worker.list=worker1
worker.worker1.port=8080
worker.worker1.host=70.7.105.162
worker.worker1.type=ajp13
worker.worker1.lbfactor=1
vi conf/uriworkermap.properties
------------------------------------------------------
/mycontextroot/dwr/*=worker1
/mycontextroot/ws*=worker1
/mycontextroot/*.do=worker1
/mycontextroot/*.jsp=worker1
httpd conf.d 폴더
mod_jk.conf
------------------------------------------------------
LoadModule jk_module modules/mod_jk.so
<IfModule jk_module>
JkWorkersFile conf/workers.properties
JkLogFile logs/mod_jk.log
JkLogLevel info
JkMountFile conf/uriworkermap.properties
</IfModule>
~
2011년 7월 4일 월요일
@Transactional 과 @Aspect annotation의 순서지정.
org.springframework.core.annotation.Order 순서와 tx:annotation-driven order attribute 값으로 순서를 결정할 수 있다.
아래와 같이 설정하면 Transaction의 외부에서 아래의 Aspect가 실행되기 때문에 Exception 이 발생했을 경우 rollback이 우선 처리되고 응답 메시지가 생성된다.
아래와 같이 설정하면 Transaction의 외부에서 아래의 Aspect가 실행되기 때문에 Exception 이 발생했을 경우 rollback이 우선 처리되고 응답 메시지가 생성된다.
aspect 파일: @Aspect() @Order(1) @Service("myAspect") public class MyAspect { @Around("execution(public com.my.IServiceResult com.my.*ServiceImpl.*(..))") public IServiceResult handleDwrMethod(ProceedingJoinPoint joinpoint) { try { Object[] args = joinpoint.getArgs(); return (IServiceResult) joinpoint.proceed(args); } catch(Throwable ex) { return createErrorResult(ex); } } } spring 설정 파일: <tx:annotation-driven transaction-manager="queryTxManager" order="2" />
2011년 5월 18일 수요일
CXF stringListProvider
cxf config file
---
<bean id="stringListProvider" class="com.archnal.util.StringListBodyWriter"/>
---
Java 구현 클래스
---
---
---
<bean id="stringListProvider" class="com.archnal.util.StringListBodyWriter"/>
---
Java 구현 클래스
---
package com.archnal.util; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyWriter; @Produces( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public class StringListBodyWriter implements MessageBodyWriter<List<String>> { public long getSize(List<String> t, java.lang.Class<?> type, java.lang.reflect.Type genericType, java.lang.annotation.Annotation[] annotations, javax.ws.rs.core.MediaType mediaType) { Iterator<String> i = t.iterator(); long size = 0; while (i.hasNext()) { size += i.next().length(); } return size; }; @Override public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return type.equals(ArrayList.class) && (mediaType.equals(MediaType.TEXT_PLAIN_TYPE) || mediaType.equals(MediaType.TEXT_XML_TYPE) || mediaType .equals(MediaType.APPLICATION_JSON_TYPE)); } @Override public void writeTo(List<String> t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { BufferedWriter bw = new BufferedWriter(new OutputStreamWriter( entityStream)); String ts = null; for (String aT : t) { ts += aT; } bw.write(ts); bw.flush(); } }
2011년 3월 29일 화요일
HttpClient 4.x 샘플
HttpClient 3.x 위주로 사용하다가 HttpClient 4.x 버전으로 통신하는 클라이언트를 소스를 하나 만들었다. 고급스러운 건 별루 없다. 그래도 필요한 기능은 거의 포함되지 않았을까? 설명하기 귀찮다. 대충 알아서 보시길
아래의 소스 코드에는 다음과 같은 기능이 포함되어 있다.
- get / post 방식 통신
- JSESSIONID 쿠키 사용 (응답메시지, 요청메시지에서 사용)
- XML 응답 받아서 마샬링
- POST 방식의 UrlEncodedFormEntity 만들어서 요청보내기
아래의 소스 코드에는 다음과 같은 기능이 포함되어 있다.
- get / post 방식 통신
- JSESSIONID 쿠키 사용 (응답메시지, 요청메시지에서 사용)
- XML 응답 받아서 마샬링
- POST 방식의 UrlEncodedFormEntity 만들어서 요청보내기
package jaxrs; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.StringWriter; import java.net.URI; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.annotation.Resource; import javax.xml.transform.stream.StreamSource; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.CookieStore; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.protocol.ClientContext; import org.apache.http.client.utils.URIUtils; import org.apache.http.cookie.Cookie; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.cookie.BasicClientCookie; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.HttpParams; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HttpContext; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.oxm.jaxb.Jaxb2Marshaller; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import cxfbook.chapter4.domain.Book; import cxfbook.chapter4.domain.BookCollection; @ContextConfiguration(locations={"classpath*:spring/*-config.xml"}) @RunWith(SpringJUnit4ClassRunner.class) public class CallJaxrsTest { @Resource(name="jaxb2Marshaller") private Jaxb2Marshaller marshaller; @Test public void testCallJaxrs() throws Exception { URI uri = URIUtils.createURI("http", "localhost", 8080, "/chapter4/rest/user/book/tom/list", null, null); HttpGet get = new HttpGet(uri); get.addHeader("Accept", "application/xml"); System.out.println("uri: " + uri); HttpClient httpclient = new DefaultHttpClient(); HttpResponse response = httpclient.execute(get); HttpEntity entity = response.getEntity(); BookCollection books = (BookCollection) marshaller.unmarshal(new StreamSource(entity.getContent())); for(Book book : books.getBooks()) { System.out.println("book: " + book); } } @Test public void testCallSecureService() throws Exception { URI uri = URIUtils.createURI("http", "localhost", 8080, "/securityweb/login.do", null, null); Listparameters = new ArrayList (); parameters.add(new BasicNameValuePair("loginId", "tom")); parameters.add(new BasicNameValuePair("password", "tomspasswd")); UrlEncodedFormEntity reqEntity = new UrlEncodedFormEntity(parameters); HttpPost post = new HttpPost(uri); post.setEntity(reqEntity); DefaultHttpClient httpclient = new DefaultHttpClient(); HttpResponse response = httpclient.execute(post); System.out.println("params: " + post.getParams()); System.out.println("loginId: " + post.getParams().getParameter("loginId")); System.out.println("password: " + post.getParams().getParameter("password")); String jsessionId = null; Cookie jsessionIdCookie = null; for(Cookie cookie : httpclient.getCookieStore().getCookies()) { System.out.println(cookie.getName() + " : " + cookie.getValue() + " - " + cookie.isExpired(new Date())); if("JSESSIONID".equals(cookie.getName())) { if(cookie.isExpired(new Date()) == false) { jsessionId = cookie.getValue(); jsessionIdCookie = cookie; } } } System.out.println("JSESSIONID: " + jsessionId); HttpEntity entity = response.getEntity(); ByteArrayOutputStream out = new ByteArrayOutputStream(); entity.writeTo(out); System.out.println(new String(out.toByteArray())); System.out.println("uri: " + uri); if(jsessionId == null) { return; } System.out.println("jsessionId cookie: " + jsessionIdCookie.getClass().getName()); System.out.println("jsessionId cookie: " + jsessionIdCookie); ObjectOutputStream cookieOutputStream = new ObjectOutputStream(new FileOutputStream("cookie.bin")); cookieOutputStream.writeObject(jsessionIdCookie); HttpGet get = new HttpGet("http://localhost:8080/securityweb/ws/rest/secure/user/getUser/1"); ObjectInputStream cookieInputStream = new ObjectInputStream(new FileInputStream("cookie.bin")); jsessionIdCookie = (Cookie) cookieInputStream.readObject(); CookieStore cookieStore = new BasicCookieStore(); cookieStore.addCookie(jsessionIdCookie); HttpContext localContext = new BasicHttpContext(); localContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore); response = httpclient.execute(get, localContext); entity = response.getEntity(); out = new ByteArrayOutputStream(); entity.writeTo(out); System.out.println(new String(out.toByteArray())); } }
Filter에서 Spring DI 사용하기
필터는 일반적으로 서블릿 컨테이너에서 제어하기 때문에 스프링 어플리케이션에서 관리하는 빈을 DI 받을 수 없다.
아래의 필터 클래스를 살펴보자.
doFilter 메서드 안에서 DI 된 messageBuilder를 사용하고 있다.
스프링 DI를 사용하지 않는 일반 필터처럼 web.xml 파일에 아래와 같이 지정하면 messageBuilder.getErrorMessage("AUTH_ERROR")를 실행할 때 NullPointerException 이 발생한다.
스프링 어플리케이션 컨텍스트에 의해서 DI가 실행되지 않았기 때문에 여전히 messageBuilder가 null이기 때문이다.
이런 경우 스프링컨텍스트의 DI를 이용해서 필터를 구현하고자 하는 경우에는 아래와 같이 web.xml 파일을 설정해야 한다.
주의해야 할 점은 filter-name과 필터 클래스의 빈 이름이 동일해야 한다.
위의 필터의 타입레벨 annotation으로 @Service("restfulAuthFilter") 설정된 restfulAuthFilter가 filter-name으로 사용되어야 한다.
위와 같이 web.xml 파일을 설정하면 스프링의 DI를 이용하여 필터를 작성할 수 있다.
아래의 필터 클래스를 살펴보자.
package com.archnal.securityweb.web; import java.io.IOException; import javax.annotation.Resource; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; // import 생략 @Service("restfulAuthFilter") public class RestfulAuthFilter implements Filter { @Resource(name="statusMessageBuilder") StatusMessageBuilder messageBuilder; @Override public void destroy() { // TODO Auto-generated method stub } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { .. 생략 String message = messageBuilder.getErrorMessage("AUTH_ERROR"); filterChain.doFilter(request, response); } @Override public void init(FilterConfig arg0) throws ServletException { // TODO Auto-generated method stub } }
doFilter 메서드 안에서 DI 된 messageBuilder를 사용하고 있다.
스프링 DI를 사용하지 않는 일반 필터처럼 web.xml 파일에 아래와 같이 지정하면 messageBuilder.getErrorMessage("AUTH_ERROR")를 실행할 때 NullPointerException 이 발생한다.
스프링 어플리케이션 컨텍스트에 의해서 DI가 실행되지 않았기 때문에 여전히 messageBuilder가 null이기 때문이다.
<filter> <filter-name>restfulAuthFilter</filter-name> <filter-class> com.archnal.securityweb.web.RestfulAuthFilter </filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>restfulAuthFilter</filter-name> <url-pattern>/ws/rest/secure/*</url-pattern> </filter-mapping>
이런 경우 스프링컨텍스트의 DI를 이용해서 필터를 구현하고자 하는 경우에는 아래와 같이 web.xml 파일을 설정해야 한다.
<filter> <filter-name>restfulAuthFilter</filter-name> <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>restfulAuthFilter</filter-name> <url-pattern>/ws/rest/secure/*</url-pattern> </filter-mapping>
주의해야 할 점은 filter-name과 필터 클래스의 빈 이름이 동일해야 한다.
위의 필터의 타입레벨 annotation으로 @Service("restfulAuthFilter") 설정된 restfulAuthFilter가 filter-name으로 사용되어야 한다.
위와 같이 web.xml 파일을 설정하면 스프링의 DI를 이용하여 필터를 작성할 수 있다.
세션 스코프 빈 사용
스프링의 빈은 별다른 설정이 없으면 singleton 스코프로 생성이 된다.
특정 타입의 빈을 하나만 만들어 두고 공유해서 사용하다. 따라서 빈에 상태를 저장하는 코드를 작성하는 것은 매우 위험한 상황을 초래할 수 있으니 주의 해야 한다.
스프링의 스코프는 singleton 이외에도 prototype이 있다.
prototype은 getBean() 하는 시점마다 새로운 빈 객체를 생성해 준다.
스코프 빈으로는 웹어플리케이션에서 사용할 수 있는 request, session, global session 스코프를 가진 빈들도 있다.
request 스코프는 웹에서 사용자 등록 등의 기능에서 위저드 화면처럼 다중 페이지를 처리할 경우에 유용하다.
session 스코프는 세션범위 내에서 유일한 빈을 생성해 준다.
global session은 포틀릿을 지원하는 컨텍스트에서만 적용이 가능하다.
이번 포스트는 session 스코프에 대해서 정리하고자 작성되었다.
기존의 HttpSession 과 Session 스코프의 빈을 사용하는 것은 무슨 차이가 있을까?
저장하고자 하는 데이터가 세션 단위로 저장된다는 기본 개념은 동일하다.
하지만 HttpSession 을 이용하는 경우는 HttpServletRequest 객체가 필요하다. 서비스 레이어에 Presentation 레이어 객체인 HttpServletRequest를 파라미터로 넘기는 것은 적절해 보이지 않는다.
그렇지 않으려면 컨트롤러에서 세션에 저장된 객체를 읽어 낸 후 서비스빈으로 넘길 수 도 있다. 이것은 그나마 좀 괜찮다.
이번 포스트는 스프링에서 세션 스코프로 빈을 생성하는 방법을 살펴 볼 것이다.
HttpSession 을 사용하는 경우와 비교해 보면 이해하는데 도움이 될 것이다.
스프링에서 세션 스코프를 사용하는 빈을 생성한다는 것은 스코프가 서로 다른 빈들끼리의 관계임을 먼저 이해해야 한다.
대부부의 스프링 빈들이 싱글톤이기 때문에 세션 스코프 빈을 사용하는 빈(클라이언트 빈)이 싱글톤이라고 가정해보자.
싱글톤 빈에서 세션 스코프 빈을 사용하려면 어떻게 할까?
다른 싱글톤 빈을 사용할 때 처럼 Dependency Injection (이후 DI)을 사용할 수 없다. 객체가 하나만 생성되는 싱글톤 객체에 DI를 하게되면 DI 된 객체도 한 번 밖에 세팅되지 않기 때문에 결국 싱글톤이 되어 버리기 때문이다. 따라서 이러한 경우에는 Dependency Lookup(이후 DL)을 사용해야 한다. DL이란 빈 객체를 생성하는 ObjectFactory (Proxy 역할을 함)를 DI 해 두고, 객체가 필요한 시점에 getObject 메서드를 호출하는 방식이다. "필요할 때 빈을 찾는다"라는 의미로 이해해도 좋을 것 같다.
정리하자면 일반적인 스프링의 (싱글톤) 빈에서 세션 스코프 빈을 사용하려면 DL을 해야 하고, 따라서 세션 스코프 빈을 생성하는 ObjectFactory를 정의해야 한다.
먼저 세션 스코프 빈부터 정의해 보자.
세션에 저장해야 할 객체를 모두 담고 있는 SessionContext(이건 개발자 맘대로)라는 클래스를 아래와 같이 정의한다.
스프링에서 관리할 수 있도록 @Component annotation을 사용하였고, @Scope는 "session"으로 설정하였다.
스프링 설정 파일에는 아래와 같이 설정한다.
ObjectFactory를 설정했으며, @Component 를 스캔할 수 있도록 설정하였다.
일반적으로 세션에 저장되는 데이터는 로그인 되는 시점에 초기화된다. 세션에 저장된 데이터는 다양한 곳에서 다양하게 사용된다.
이 포스트에서는 RESTful 서비스에서 권한을 체크할 목적으로 사용된다고 가정한다.
먼저 로그인 컨트롤러를 살펴 보자
로그인 메서드에서는 HttpSession과 세션 스코프 빈에 데이터를 동일하게 저장하고 있다.
두 개를 모두 저장할 필요는 없으며, 두개의 저장소를 비교할 목적으로 작성된 소스코드로 이해하면 된다.
sessionContextFactory라는 ObjectFactory를 이용해서 세션 스코프 빈이 필요한 로그인 메서드 안에서 getObject를 호출하고 있음을 주의 깊게 살펴보자.
로그아웃 메서드에서는 session.invalidate() 메서드만 호출하고 있다.
다음은 세션 스코프를 사용하는 빈을 살펴 보자.
아래의 소스 코드는 CXF의 RESTful 서비스를 호출하기 전에 사용되는 ServiceInvoker 소스 코드 중의 일부다.
특정 URI로 서비스 요청이 들어오면 세션에 저장된 인증정보를 이용하여 서비스를 정상적으로 호출할 지 인증에러를 리턴할 지 결정하는 로직이 구현되어 있다.
아래의 소스코드 역시 HttpSession과 세션 스코프 빈에 사용하는 코드를 모두 작성한 소스다.
아래의 invoke메서드에서는 HttpServletRequest를 가져 올 수 있는 환경이지만, 세션 스코프 빈을 사용하기 위해서 HttpServletRequest가 필요하지는 않다.
마지막으로 스프링에서 세션 스코프 빈을 사용하기 위해서는 web.xml 파일에 아래와 같이 리스너를 등록해 주어야 한다.
아래와 같이 자바 소스를 이용해서 ObjectFactory를 등록할 수도 있다.
특정 타입의 빈을 하나만 만들어 두고 공유해서 사용하다. 따라서 빈에 상태를 저장하는 코드를 작성하는 것은 매우 위험한 상황을 초래할 수 있으니 주의 해야 한다.
스프링의 스코프는 singleton 이외에도 prototype이 있다.
prototype은 getBean() 하는 시점마다 새로운 빈 객체를 생성해 준다.
스코프 빈으로는 웹어플리케이션에서 사용할 수 있는 request, session, global session 스코프를 가진 빈들도 있다.
request 스코프는 웹에서 사용자 등록 등의 기능에서 위저드 화면처럼 다중 페이지를 처리할 경우에 유용하다.
session 스코프는 세션범위 내에서 유일한 빈을 생성해 준다.
global session은 포틀릿을 지원하는 컨텍스트에서만 적용이 가능하다.
이번 포스트는 session 스코프에 대해서 정리하고자 작성되었다.
기존의 HttpSession 과 Session 스코프의 빈을 사용하는 것은 무슨 차이가 있을까?
저장하고자 하는 데이터가 세션 단위로 저장된다는 기본 개념은 동일하다.
하지만 HttpSession 을 이용하는 경우는 HttpServletRequest 객체가 필요하다. 서비스 레이어에 Presentation 레이어 객체인 HttpServletRequest를 파라미터로 넘기는 것은 적절해 보이지 않는다.
그렇지 않으려면 컨트롤러에서 세션에 저장된 객체를 읽어 낸 후 서비스빈으로 넘길 수 도 있다. 이것은 그나마 좀 괜찮다.
이번 포스트는 스프링에서 세션 스코프로 빈을 생성하는 방법을 살펴 볼 것이다.
HttpSession 을 사용하는 경우와 비교해 보면 이해하는데 도움이 될 것이다.
스프링에서 세션 스코프를 사용하는 빈을 생성한다는 것은 스코프가 서로 다른 빈들끼리의 관계임을 먼저 이해해야 한다.
대부부의 스프링 빈들이 싱글톤이기 때문에 세션 스코프 빈을 사용하는 빈(클라이언트 빈)이 싱글톤이라고 가정해보자.
싱글톤 빈에서 세션 스코프 빈을 사용하려면 어떻게 할까?
다른 싱글톤 빈을 사용할 때 처럼 Dependency Injection (이후 DI)을 사용할 수 없다. 객체가 하나만 생성되는 싱글톤 객체에 DI를 하게되면 DI 된 객체도 한 번 밖에 세팅되지 않기 때문에 결국 싱글톤이 되어 버리기 때문이다. 따라서 이러한 경우에는 Dependency Lookup(이후 DL)을 사용해야 한다. DL이란 빈 객체를 생성하는 ObjectFactory (Proxy 역할을 함)를 DI 해 두고, 객체가 필요한 시점에 getObject 메서드를 호출하는 방식이다. "필요할 때 빈을 찾는다"라는 의미로 이해해도 좋을 것 같다.
정리하자면 일반적인 스프링의 (싱글톤) 빈에서 세션 스코프 빈을 사용하려면 DL을 해야 하고, 따라서 세션 스코프 빈을 생성하는 ObjectFactory를 정의해야 한다.
먼저 세션 스코프 빈부터 정의해 보자.
세션에 저장해야 할 객체를 모두 담고 있는 SessionContext(이건 개발자 맘대로)라는 클래스를 아래와 같이 정의한다.
스프링에서 관리할 수 있도록 @Component annotation을 사용하였고, @Scope는 "session"으로 설정하였다.
package com.archnal.securityweb.web; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import com.archnal.securityweb.domain.User; @Scope(value="session") @Component("sessionContext") public class SessionContext { private boolean authenticated; private User user; public User getUser() { return user; } public void setUser(User user) { this.user = user; } public boolean isAuthenticated() { return authenticated; } public void setAuthenticated(boolean authenticated) { this.authenticated = authenticated; } }
스프링 설정 파일에는 아래와 같이 설정한다.
ObjectFactory를 설정했으며, @Component 를 스캔할 수 있도록 설정하였다.
<bean id="sessionContextFactory" class="org.springframework.beans.factory.config.ObjectFactoryCreatingFactoryBean"> <property name="targetBeanName" value="sessionContext" /> </bean> <context:component-scan base-package="com.archnal.securityweb" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/> <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/> <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/> </context:component-scan>
일반적으로 세션에 저장되는 데이터는 로그인 되는 시점에 초기화된다. 세션에 저장된 데이터는 다양한 곳에서 다양하게 사용된다.
이 포스트에서는 RESTful 서비스에서 권한을 체크할 목적으로 사용된다고 가정한다.
먼저 로그인 컨트롤러를 살펴 보자
로그인 메서드에서는 HttpSession과 세션 스코프 빈에 데이터를 동일하게 저장하고 있다.
두 개를 모두 저장할 필요는 없으며, 두개의 저장소를 비교할 목적으로 작성된 소스코드로 이해하면 된다.
sessionContextFactory라는 ObjectFactory를 이용해서 세션 스코프 빈이 필요한 로그인 메서드 안에서 getObject를 호출하고 있음을 주의 깊게 살펴보자.
로그아웃 메서드에서는 session.invalidate() 메서드만 호출하고 있다.
package com.archnal.securityweb.controller; import javax.annotation.Resource; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.ObjectFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; // import 생략 @Controller public class LoginLogoutController { @Resource(name="userService") UserService userService; @Resource(name="sessionContextFactory") ObjectFactorysessionContextFactory; @RequestMapping(method=RequestMethod.GET,value="/login.do") public void loginForm() { } @RequestMapping(method=RequestMethod.POST,value="/login.do") public String loginSubmit( @RequestParam(value="loginId", required=true) String loginId, @RequestParam(value="password", required=true) String password, HttpSession session, ModelMap modelMap) { User user = userService.login(loginId, password); if(user != null) { SessionContext sessionContext = sessionContextFactory.getObject(); sessionContext.setAuthenticated(true); sessionContext.setUser(user); session.setAttribute("authenticated", true); session.setAttribute("user", user); } return "redirect:home.do"; } @RequestMapping(method={RequestMethod.POST, RequestMethod.GET}, value="logout.do") public String logout(HttpSession session) { session.invalidate(); return "redirect:home.do"; } }
다음은 세션 스코프를 사용하는 빈을 살펴 보자.
아래의 소스 코드는 CXF의 RESTful 서비스를 호출하기 전에 사용되는 ServiceInvoker 소스 코드 중의 일부다.
특정 URI로 서비스 요청이 들어오면 세션에 저장된 인증정보를 이용하여 서비스를 정상적으로 호출할 지 인증에러를 리턴할 지 결정하는 로직이 구현되어 있다.
아래의 소스코드 역시 HttpSession과 세션 스코프 빈에 사용하는 코드를 모두 작성한 소스다.
아래의 invoke메서드에서는 HttpServletRequest를 가져 올 수 있는 환경이지만, 세션 스코프 빈을 사용하기 위해서 HttpServletRequest가 필요하지는 않다.
package com.archnal.sample.cxf; import java.util.Locale; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.jaxrs.JAXRSInvoker; import org.apache.cxf.message.Exchange; import org.apache.cxf.message.MessageContentsList; import org.springframework.beans.factory.ObjectFactory; import org.springframework.stereotype.Service; // import 생략 @Service("baseInvoker") public class BaseInvoker extends JAXRSInvoker { @Resource(name="statusMessageBuilder") private StatusMessageBuilder statusMessageBuilder; @Resource(name="sessionContextFactory") ObjectFactorysessionContextFactory; @Override public Object invoke(Exchange exchange, Object request) { Response response = null; HttpServletRequest servletRequest = (HttpServletRequest)exchange.getInMessage().get("HTTP.REQUEST"); String servletPath = servletRequest.getServletPath(); String requestURI = servletRequest.getRequestURI(); if(requestURI.contains("/ws/rest/secure")) { SessionContext sessionContext = sessionContextFactory.getObject(); HttpSession session = servletRequest.getSession(); //boolean authenticated = Boolean.TRUE.equals(session.getAttribute("authenticated")); boolean authenticated = sessionContext.isAuthenticated(); if(! authenticated) { return new MessageContentsList(Response.status(Status.UNAUTHORIZED).build()); } } ... 생략 } }
마지막으로 스프링에서 세션 스코프 빈을 사용하기 위해서는 web.xml 파일에 아래와 같이 리스너를 등록해 주어야 한다.
<listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener>
아래와 같이 자바 소스를 이용해서 ObjectFactory를 등록할 수도 있다.
자바 코드를 이용한 ObjectFactory 빈 등록 @Configuration public class ObjectFactoryConfig { @Bean public ObjectFactoryCreatingFactoryBean serviceRequestFactory() { ObjectFactoryCreatingFactoryBean factoryBean = new ObjectFactoryCreatingFactoryBean(); factoryBean.setTargetBeanName("serviceRequest"); return factoryBean; } }
2011년 3월 16일 수요일
Spring DAO에서 DataAccessException 사용.
예외 처리는 분명 좀 복잡한 주제이기는 하다.
아래의 소스코드를 보자.
토비의 스프링3 의 저자는 이런 예외처리를 무의미하고 무책임하다라고 이야기하고 있다.
예를 들어 보자.
사용자 추가 시 보통 데이터베이스 테이블의 사용자 정보에는 중복되어 들어 갈 수 없는 사용자 로그인 아이디 같은 것이 있다.
이러한 컬럼들은 중복해서 테이블에 넣을 수가 없기 때문에 보통 Unique Constraints 에러를 발생시킨다. 하지만 DBMS 마다 어떤 SQLException을 던지는 지, 또 중복에러를 확인하기 위해서 에러코드로 체크해야 하는지, Exception의 타입으로 구분해야 하는지 확인해야 할 것이 너무나 많다.
이러한 상황을 피하기 위해서 테이블에 삽입하기 전에 데이터가 존재하는지 체크해 볼 수도 있다. 운이 아주 나쁘다면 체크하고 삽입하는 순간에 누군가 번개보다 빠르게 데이터를 삽입할 경우 역시나 예외가 발생할 것이고, 이런 상황이 현실화 될 가능성이 아주 희박할테니 이런 것쯤은 무시해도 된다해도 삽입할때마다 매번 데이터를 확인하는 것도 그렇게 우아한 것처럼 보이지는 않는다.
오래전부터 하이버네이트에는 이런 처리를 위해 예외 전환이라는 방식으로 개발자에게 도움을 주었다.
특정 DBMS에서 던지는 SQLException을 똑똑한 하이버네이트는 모두 알고 있기 때문에 이것을 하이버네이트에서 정의한 예외로 변경해서 던진다. 하이버네이트를 이용하여 DAO를 구현하는 개발자는 이제 DBMS에서 중복에러가 발생했을 때 어떤 에러인지를 확인할 필요가 없다. 언제나 org.hibernate.exception.ConstraintViolationException 이 던져질 테니 말이다.
이제 스프링 입장에서 생각해 보자. 하이버네이트가 저런 저급작업을 대신 해주니까 니들도 하이버네이트만 사용해! 라고 할 수는 없다. 각종 JPA,하이버네이트, iBatis 뿐만 아니라 스프링에서 제공하는 JdbcTemplate 까지 DAO를 구현할 만한 것들이 너무 많다.
스프링도 똑똑한 걸로 치자면 하이버네이트에 절대 뒤지지 않을 테니, 각종 DAO 관련한 것들에서 던지는 Exception 쯤은 모두 다 알고 있다. 하이버네이트가 org.hibernate.exception.ConstraintViolationException를 던진다면 스프링은 이걸 받아서 org.springframework.dao.DataIntegrityViolationException 이런 것 쯤은 던져 줄 수 있는 것이다.
결국 스프링은 이용해서 DAO를 개발하는 개발자 입장에서는 사용자 추가로직을 구현할 때 DataIntegrityViolationException 을 try {} catch{} 해서 UserDuplicationException 정도는 던질 수 있는 상황이 된 것이다.
이제 스프링에서 이런 것들을 가능하게 하는 설정을 알아보자.
스프링 설정 파일에 아래와 같이 추가한다.
PersistenceExceptionTranslationPostProcessor 는 DAO 클래스에 붙히는 스테레오타입인 @Repository 애너테이션이 설정된 클래스의 메서드에서 던지는 Exception을 스프링에서 정의한 Exception으로 전환하여 던지는 역할을 한다. 생각했던 것보다 간단히지 않은가?
위의 소스 코드들은 그렇다면 이렇게 수정해도 되지 않을까?
조금 무의미하고 무책임한 에러 처리 로직을 개선한 것 같은지 모르겠다.
아래의 소스코드를 보자.
public interface UserService { public void addUser(User user) throws Exception; } public class UserServiceImpl implements UserService { public void addUser(User user) throws Exception { userDao.create(user); } } public interface UserDao { public void create(User user) throws Exception; } public class UserDaoImpl implements UserDao { public void create(User user) throws Exception { // 구현로직 } }
토비의 스프링3 의 저자는 이런 예외처리를 무의미하고 무책임하다라고 이야기하고 있다.
예를 들어 보자.
사용자 추가 시 보통 데이터베이스 테이블의 사용자 정보에는 중복되어 들어 갈 수 없는 사용자 로그인 아이디 같은 것이 있다.
이러한 컬럼들은 중복해서 테이블에 넣을 수가 없기 때문에 보통 Unique Constraints 에러를 발생시킨다. 하지만 DBMS 마다 어떤 SQLException을 던지는 지, 또 중복에러를 확인하기 위해서 에러코드로 체크해야 하는지, Exception의 타입으로 구분해야 하는지 확인해야 할 것이 너무나 많다.
이러한 상황을 피하기 위해서 테이블에 삽입하기 전에 데이터가 존재하는지 체크해 볼 수도 있다. 운이 아주 나쁘다면 체크하고 삽입하는 순간에 누군가 번개보다 빠르게 데이터를 삽입할 경우 역시나 예외가 발생할 것이고, 이런 상황이 현실화 될 가능성이 아주 희박할테니 이런 것쯤은 무시해도 된다해도 삽입할때마다 매번 데이터를 확인하는 것도 그렇게 우아한 것처럼 보이지는 않는다.
오래전부터 하이버네이트에는 이런 처리를 위해 예외 전환이라는 방식으로 개발자에게 도움을 주었다.
특정 DBMS에서 던지는 SQLException을 똑똑한 하이버네이트는 모두 알고 있기 때문에 이것을 하이버네이트에서 정의한 예외로 변경해서 던진다. 하이버네이트를 이용하여 DAO를 구현하는 개발자는 이제 DBMS에서 중복에러가 발생했을 때 어떤 에러인지를 확인할 필요가 없다. 언제나 org.hibernate.exception.ConstraintViolationException 이 던져질 테니 말이다.
이제 스프링 입장에서 생각해 보자. 하이버네이트가 저런 저급작업을 대신 해주니까 니들도 하이버네이트만 사용해! 라고 할 수는 없다. 각종 JPA,하이버네이트, iBatis 뿐만 아니라 스프링에서 제공하는 JdbcTemplate 까지 DAO를 구현할 만한 것들이 너무 많다.
스프링도 똑똑한 걸로 치자면 하이버네이트에 절대 뒤지지 않을 테니, 각종 DAO 관련한 것들에서 던지는 Exception 쯤은 모두 다 알고 있다. 하이버네이트가 org.hibernate.exception.ConstraintViolationException를 던진다면 스프링은 이걸 받아서 org.springframework.dao.DataIntegrityViolationException 이런 것 쯤은 던져 줄 수 있는 것이다.
결국 스프링은 이용해서 DAO를 개발하는 개발자 입장에서는 사용자 추가로직을 구현할 때 DataIntegrityViolationException 을 try {} catch{} 해서 UserDuplicationException 정도는 던질 수 있는 상황이 된 것이다.
이제 스프링에서 이런 것들을 가능하게 하는 설정을 알아보자.
스프링 설정 파일에 아래와 같이 추가한다.
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
PersistenceExceptionTranslationPostProcessor 는 DAO 클래스에 붙히는 스테레오타입인 @Repository 애너테이션이 설정된 클래스의 메서드에서 던지는 Exception을 스프링에서 정의한 Exception으로 전환하여 던지는 역할을 한다. 생각했던 것보다 간단히지 않은가?
위의 소스 코드들은 그렇다면 이렇게 수정해도 되지 않을까?
public interface UserService { public void addUser(User user) throws UserDuplicationException; } public class UserServiceImpl implements UserDuplicationException { public void addUser(User user) throws Exception { try { userDao.create(user); } catch (DataIntegrityViolationException ex) { throw new UserDuplicationException(); } } } public interface UserDao { public void create(User user); } public class UserDaoImpl implements UserDao { public void create(User user) { // 구현로직 } }
조금 무의미하고 무책임한 에러 처리 로직을 개선한 것 같은지 모르겠다.
2011년 2월 21일 월요일
CXF 서비스 호출 시 발생하는 예외 처리 - Spring AOP Annotation
Spring에서 Annotation기반 AOP를 이용하려면 스프링 설정을 아래와 같이 지정해야 한다.
아래의 소스 코드는 CXF의 서비스 호출 시 에러 처리에 관련된 Aspect 소스 코드다.
아래의 소스 코드에서 @Service나 @Component 스테레오 타입이 지정되도록 해야한다.
위에 메서드를 호출하면 에러가 발생하는 경우에 @AfterThrowing 으로 처리가 가능할까 싶어서 테스트를 해 보았으나,
예외가 발생하는 경우에 @AfterThrowing이 지정된 메서드를 호출하지만 메서드가 호출 된 이후에 Aspect는 여전히 원래 발생한 Throwable을 잡아서 제거하지 않고 던져 버린다. 따라서 서비스 Invoker에서는 여전히 에러가 발생한 것으로 인식하여 장황한 stack trace를 화면에 출력한다.
위의 예제에서는 에러가 발생하는 경우 잡아채서 클라이언트에게 serverError(500) 의 상태코드로 전달하면서 에러상태 코드를 전달하려는 의도로 작성된 코드다.
이런 요구사항에 @AterThrowing은 에러를 여전히 던지기 때문에 적합하지 않다. 실제로 @AfterThrowing에서 리턴하는 Response는 아무런 역할을 하지 못한다. 위의 소스 코드에서 처럼 @Around 에서 원래 메서드를 호출하고 에러가 발생하는 경우에 Response를 생성하여 리턴하도록 해야한다.
하지만 이런 방식에서도 문제가 하나 있다. Restful서비스의 원래 메서드가 Response를 리턴하도록 되어 있어야 한다. 일반 Pojo 클래스를 리턴하는 경우에는 ClassCastException이 발생한다. 이처럼 에러를 일괄적으로 처리하기 위해서는 꼭 Response가 아니더라도 ResponseMessage 같은 일관된 메시지 형태의 객체를 리턴해야만 ClassCastException이 발생하지 않는다.
위의 Aspect가 적용된 상태에서 서비스를 호출하였을 때 정상적인 경우에 아래와 같이 화면에 출력된다.
에러가 발생하는 경우에는 아래와 같이 화면에 출력된다.
뭔 얘기를 하는건지 나만 알 수 있는 말을 적은 거 같다. 아는 사람은 알겠지 뭐~~
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"> <aop:aspectj-autoproxy /> </beans>
아래의 소스 코드는 CXF의 서비스 호출 시 에러 처리에 관련된 Aspect 소스 코드다.
아래의 소스 코드에서 @Service나 @Component 스테레오 타입이 지정되도록 해야한다.
package com.archnal.sample.cxf; import javax.ws.rs.core.Response; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Service; import com.archnal.sample.exception.BaseException; import com.archnal.sample.exception.StatusCode; @Aspect() @Service("restfulServiceAspect") public class RestfulServiceAspect { @Around("execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))") // @Pointcut("execution(public * com.archnal.sample.jaxrs.*RestfulServiceImpl.*(..))") public Object handleRestfulService(ProceedingJoinPoint joinpoint) { System.out.println("around: " + joinpoint.getSignature().getName()); Response response = null; try { Object[] args = joinpoint.getArgs(); Object entity = joinpoint.proceed(args); return entity; // response = Response.ok(entity).build(); // response = (Response)joinpoint.proceed(args); } catch(BaseException ex) { System.out.println("baseException: " + ex); response = Response.serverError().entity(create(ex)).build(); } catch(Throwable ex) { System.out.println("throwable: " + ex); response = Response.serverError().entity(create(ex)).build(); } return response; } @AfterThrowing(pointcut="execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))", throwing="e") public Object handleException(JoinPoint joinPoint, Throwable e) { System.out.println("afterThrowing: " + joinPoint.getSignature().getName()); System.out.println("e: " + e); return Response.serverError().entity(create(e)); } @Before("execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))") public void before(JoinPoint joinPoint) { System.out.println("before: " + joinPoint.getSignature().getName()); } @After("execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))") public void after(JoinPoint joinPoint) { System.out.println("after: " + joinPoint.getSignature().getName()); } @AfterReturning(pointcut="execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))", returning="result") public void afterReturning(JoinPoint joinPoint, Object result) { System.out.println("afterReturning: " + joinPoint.getSignature().getName()); System.out.println("result: " + result); } private ResponseResult create(BaseException ex) { ResponseResult header = new ResponseResult(); header.setSuccess(false); header.setStatusCode(ex.getStatusCode().getCode()); header.setMessage(ex.getMessage()); return header; } private ResponseResult create(Throwable th) { if(th.getClass().isAssignableFrom(BaseException.class)) { return create((BaseException) th); } ResponseResult header = new ResponseResult(); header.setSuccess(false); header.setStatusCode(StatusCode.UNKNOWN.getCode()); header.setMessage(th.getMessage()); return header; } }
위에 메서드를 호출하면 에러가 발생하는 경우에 @AfterThrowing 으로 처리가 가능할까 싶어서 테스트를 해 보았으나,
예외가 발생하는 경우에 @AfterThrowing이 지정된 메서드를 호출하지만 메서드가 호출 된 이후에 Aspect는 여전히 원래 발생한 Throwable을 잡아서 제거하지 않고 던져 버린다. 따라서 서비스 Invoker에서는 여전히 에러가 발생한 것으로 인식하여 장황한 stack trace를 화면에 출력한다.
위의 예제에서는 에러가 발생하는 경우 잡아채서 클라이언트에게 serverError(500) 의 상태코드로 전달하면서 에러상태 코드를 전달하려는 의도로 작성된 코드다.
이런 요구사항에 @AterThrowing은 에러를 여전히 던지기 때문에 적합하지 않다. 실제로 @AfterThrowing에서 리턴하는 Response는 아무런 역할을 하지 못한다. 위의 소스 코드에서 처럼 @Around 에서 원래 메서드를 호출하고 에러가 발생하는 경우에 Response를 생성하여 리턴하도록 해야한다.
하지만 이런 방식에서도 문제가 하나 있다. Restful서비스의 원래 메서드가 Response를 리턴하도록 되어 있어야 한다. 일반 Pojo 클래스를 리턴하는 경우에는 ClassCastException이 발생한다. 이처럼 에러를 일괄적으로 처리하기 위해서는 꼭 Response가 아니더라도 ResponseMessage 같은 일관된 메시지 형태의 객체를 리턴해야만 ClassCastException이 발생하지 않는다.
위의 Aspect가 적용된 상태에서 서비스를 호출하였을 때 정상적인 경우에 아래와 같이 화면에 출력된다.
before: get around: get after: get afterReturning: get result: org.apache.cxf.jaxrs.impl.ResponseImpl@170a5a3
에러가 발생하는 경우에는 아래와 같이 화면에 출력된다.
before: get around: get after: get afterThrowing: get e: com.archnal.sample.exception.BaseException: category: BIZ_LOGIC_ERROR, status: USER_NOT_FOUND, code: E_102 afterReturning: get result: org.apache.cxf.jaxrs.impl.ResponseImpl@15f8a0a
뭔 얘기를 하는건지 나만 알 수 있는 말을 적은 거 같다. 아는 사람은 알겠지 뭐~~
2011년 2월 18일 금요일
CXF의 Custom Invoker 개발
CXF에서 요청을 받아서 서비스를 실행하고 응답을 리턴하는 프로세스를 살펴 보면 필요에 따라 각각을 커스터마이즈 할 수 있다.
요청 프로세스는 inbound chain 이라 하여 아래의 단계를 거친다.
RECEIVE
(PRE/USER/POST)_STREAM
READ
(PRE/USER/POST)_PROTOCOL
UNMARSHAL
(PRE/USER/POST)_LOGICAL
PRE_INVOKE
INVOKE
POST_INVOKE
응답 프로세스는 outbound chain 이라 하여 아래의 단계를 거친다.
SETUP
(PRE/USER/POST)_LOGICAL
PREPARE_SEND
PRE_STREAM
PRE_PROTOCOL
WRITE
MARSHAL
(USER/POST)_PROTOCOL
(USER/POST)_STREAM
SEND
각 단계의 구현을 InInterceptor나 OutInterceptor를 이용하여 구현할 수 있다.
하지만 이번 포스트에서는 Invoker에 대해서만 설명하도록 한다.
Invoker란 inbound chain의 INVOKE 단계에서 호출되는 것으로 실제 서비스를 호출하는 기능을 담당한다. CXF는 기본적인 Invoker를 제공하지만 서비스 호출 전,후에 특별한 처리를 해야 한다면 별도의 Invoker를 개발하여 사용할 수 있다.
Invoker 설정
cxf 설정 파일에서 아래와 같이 지정한다.
Customized Invoker 개발
아래의 소스는 JAXRS에서 기본적으로 사용하는 JAXRSInvoker를 상속받아서 구현한 것으로 에러 처리 기능이 추가된 Invoker라 할 수 있다.
JAX-RS 서비스에서 정상처리된 경우에는 서비스가 리턴한 객체를 리턴하지만 서비스 처리도중 에러가 발생하는 경우에는 에러 코드와 에러 메시지를 엔티티로 포함하고 있는 Response 객체를 리턴한다.
요청 프로세스는 inbound chain 이라 하여 아래의 단계를 거친다.
RECEIVE
(PRE/USER/POST)_STREAM
READ
(PRE/USER/POST)_PROTOCOL
UNMARSHAL
(PRE/USER/POST)_LOGICAL
PRE_INVOKE
INVOKE
POST_INVOKE
응답 프로세스는 outbound chain 이라 하여 아래의 단계를 거친다.
SETUP
(PRE/USER/POST)_LOGICAL
PREPARE_SEND
PRE_STREAM
PRE_PROTOCOL
WRITE
MARSHAL
(USER/POST)_PROTOCOL
(USER/POST)_STREAM
SEND
각 단계의 구현을 InInterceptor나 OutInterceptor를 이용하여 구현할 수 있다.
하지만 이번 포스트에서는 Invoker에 대해서만 설명하도록 한다.
Invoker란 inbound chain의 INVOKE 단계에서 호출되는 것으로 실제 서비스를 호출하는 기능을 담당한다. CXF는 기본적인 Invoker를 제공하지만 서비스 호출 전,후에 특별한 처리를 해야 한다면 별도의 Invoker를 개발하여 사용할 수 있다.
Invoker 설정
cxf 설정 파일에서 아래와 같이 지정한다.
<jaxrs:server id="restfulServer" address="/rest"> <jaxrs:features> <cxf:logging/> </jaxrs:features> <jaxrs:invoker> <ref bean="baseInvoker"/> </jaxrs:invoker> <jaxrs:serviceBeans> <ref bean="userRestfulService"/> </jaxrs:serviceBeans> </jaxrs:server>
Customized Invoker 개발
아래의 소스는 JAXRS에서 기본적으로 사용하는 JAXRSInvoker를 상속받아서 구현한 것으로 에러 처리 기능이 추가된 Invoker라 할 수 있다.
JAX-RS 서비스에서 정상처리된 경우에는 서비스가 리턴한 객체를 리턴하지만 서비스 처리도중 에러가 발생하는 경우에는 에러 코드와 에러 메시지를 엔티티로 포함하고 있는 Response 객체를 리턴한다.
package com.archnal.sample.cxf; import java.util.Locale; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Response; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.jaxrs.JAXRSInvoker; import org.apache.cxf.message.Exchange; import org.apache.cxf.message.MessageContentsList; import org.springframework.stereotype.Service; import com.archnal.sample.exception.BaseException; import com.archnal.sample.exception.StatusCode; import com.archnal.sample.exception.StatusMessageBuilder; @Service("baseInvoker") public class BaseInvoker extends JAXRSInvoker { @Resource(name="statusMessageBuilder") private StatusMessageBuilder statusMessageBuilder; @Override public Object invoke(Exchange exchange, Object request) { Response response = null; HttpServletRequest servletRequest = (HttpServletRequest)exchange.getInMessage().get("HTTP.REQUEST"); Locale clientLocale = null; if(servletRequest != null) { clientLocale = servletRequest.getLocale(); } try { Object object = super.invoke(exchange, request); return object; } catch(Fault fault) { Throwable cause = fault.getCause(); if(cause.getClass().isAssignableFrom(BaseException.class)) { response = Response.serverError().entity(create((BaseException) cause, clientLocale)).build(); } else { response = Response.serverError().entity(create(cause, clientLocale)).build(); } } return new MessageContentsList(response); } private ResponseResult create(BaseException ex, Locale locale) { ResponseResult header = new ResponseResult(); header.setSuccess(false); header.setStatusCode(ex.getStatusCode().getCode()); header.setMessage(statusMessageBuilder.getErrorMessage(ex, locale)); return header; } private ResponseResult create(Throwable th, Locale locale) { ResponseResult header = new ResponseResult(); header.setSuccess(false); header.setStatusCode(StatusCode.UNKNOWN.getCode()); header.setMessage(statusMessageBuilder.getErrorMessage(StatusCode.UNKNOWN, locale)); return header; } }
2011년 2월 17일 목요일
CXF의 @FormParam("") 사용 시 WADL 생성 에러
사용자 등록 서비스를 가정해 보자.
loginId, firstName, lastName, email, password를 FormParam으로 입력받아야 할 경우
5개의 파라미터를 갖는 메서드를 만들어야 한다.
public void addUser(
@FormParam("loginId") String loginId,
@FormParam("firstName") String firstName,
@FormParam("lastName") String lastName,
@FormParam("email") String email,
@FormParam("password" String password) throws Exception;
이럴 경우에 User라는 VO 클래스가 존재한다면 아래와 같이 인터페이스를 정의할 수 있다.
public void addUser(@FormParam("") User user) throws Exception;
각각 input 의 name과 User 클래스의 필드 명을 비교하여 User 객체의 필드 값을 설정해 주기 때문에 메서드도 깔끔하고 개발하기도 용이하다.
public class User {
private String loginId;
private String firstName;
private String lastName;
private String email;
private String password;
// getter / setter methods.
}
하지만 이렇게 리소스 메서드를 정의할 경우 CXF가 WADL 을 생성할 때 아래와 같은 에러가 발생한다.
심각: Servlet.service() for servlet CXFServlet threw exception
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2882)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:100)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:390)
at java.lang.StringBuilder.append(StringBuilder.java:119)
at org.apache.cxf.jaxrs.model.wadl.WadlGenerator.doWriteParam(WadlGenerator.java:465)
at org.apache.cxf.jaxrs.model.wadl.WadlGenerator.doWriteBeanParam(WadlGenerator.java:454)
이러한 문제점은 CXF 내부의 문제로 CXF 버전 2.3.0 에서부터 발생하지 않는다.
따로 해결책을 찾으려 애쓰지 말고 CXF 버전을 올려 보시길...
loginId, firstName, lastName, email, password를 FormParam으로 입력받아야 할 경우
5개의 파라미터를 갖는 메서드를 만들어야 한다.
public void addUser(
@FormParam("loginId") String loginId,
@FormParam("firstName") String firstName,
@FormParam("lastName") String lastName,
@FormParam("email") String email,
@FormParam("password" String password) throws Exception;
이럴 경우에 User라는 VO 클래스가 존재한다면 아래와 같이 인터페이스를 정의할 수 있다.
public void addUser(@FormParam("") User user) throws Exception;
각각 input 의 name과 User 클래스의 필드 명을 비교하여 User 객체의 필드 값을 설정해 주기 때문에 메서드도 깔끔하고 개발하기도 용이하다.
public class User {
private String loginId;
private String firstName;
private String lastName;
private String email;
private String password;
// getter / setter methods.
}
하지만 이렇게 리소스 메서드를 정의할 경우 CXF가 WADL 을 생성할 때 아래와 같은 에러가 발생한다.
심각: Servlet.service() for servlet CXFServlet threw exception
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2882)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:100)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:390)
at java.lang.StringBuilder.append(StringBuilder.java:119)
at org.apache.cxf.jaxrs.model.wadl.WadlGenerator.doWriteParam(WadlGenerator.java:465)
at org.apache.cxf.jaxrs.model.wadl.WadlGenerator.doWriteBeanParam(WadlGenerator.java:454)
이러한 문제점은 CXF 내부의 문제로 CXF 버전 2.3.0 에서부터 발생하지 않는다.
따로 해결책을 찾으려 애쓰지 말고 CXF 버전을 올려 보시길...
2011년 2월 14일 월요일
JAXB 개발 환경에서 XSD 생성 (maven pom 파일)
Jaxb 관련 annotation을 이용하여 웹서비스를 개발하다보면
가끔씩 최종 XSD를 확인해 보고 싶은 경우가 생긴다.
물론 CXF와 같은 프레임워크를 이용하여 웹서비스를 개발하면 웹서비스 URI에 ?wsdl을 붙히면 WSDL을 살펴 볼 수도 있지만
개발 환경에서 @XmlRootElement, @XmlElement로 작업할 경우 매핑되는 XSD를 보고자 한다면 아래의 pom.xml 파일을 참고하기 바란다.
jdk1.6 (mustang)의 bin 폴더에 있는 xjc (xsd를 이용하여 java class 생성) 와 schemagen을 이용해도 동일한 결과를 얻을 수 있으나 maven plugin을 이용하는 편이 훨씬 수월하다.
가끔씩 최종 XSD를 확인해 보고 싶은 경우가 생긴다.
물론 CXF와 같은 프레임워크를 이용하여 웹서비스를 개발하면 웹서비스 URI에 ?wsdl을 붙히면 WSDL을 살펴 볼 수도 있지만
개발 환경에서 @XmlRootElement, @XmlElement로 작업할 경우 매핑되는 XSD를 보고자 한다면 아래의 pom.xml 파일을 참고하기 바란다.
jdk1.6 (mustang)의 bin 폴더에 있는 xjc (xsd를 이용하여 java class 생성) 와 schemagen을 이용해도 동일한 결과를 얻을 수 있으나 maven plugin을 이용하는 편이 훨씬 수월하다.
<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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.archnal.maven</groupId> <artifactId>jaxb-project</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>jaxb-project</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.5</version> <scope>test</scope> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.0.1</version> <type>jar</type> <scope>test</scope> </dependency> </dependencies> <repositories> <repository> <id>java.net</id> <name>java.net Maven Repository</name> <url>https://maven-repository.dev.java.net/nonav/repository</url> <layout>legacy</layout> </repository> </repositories> <build> <plugins> <plugin> <groupId>com.sun.tools.jxc.maven2</groupId> <artifactId>maven-jaxb-schemagen-plugin</artifactId> <version>1.2</version> <executions> <execution> <phase>generate-resources</phase> <goals> <goal>generate</goal> </goals> </execution> </executions> <configuration> <includes> <include>**/com/archnal/sample/jaxb/**</include> </includes> <verbose>true</verbose> <source>1.6</source> </configuration> </plugin> <plugin> <groupId>com.sun.tools.xjc.maven2</groupId> <artifactId>maven-jaxb-plugin</artifactId> <executions> <execution> <goals> <goal>generate</goal> </goals> </execution> </executions> <configuration> <includeSchemas> <includeSchema>**/*.xsd</includeSchema> <includeSchema>**/*.dtd</includeSchema> </includeSchemas> <verbose>true</verbose> <source>1.6</source> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> </plugins> </build> <pluginRepositories> <pluginRepository> <id>maven-repository.dev.java.net</id> <name>Java.net Maven 2 Repository</name> <url>http://download.java.net/maven/2</url> </pluginRepository> </pluginRepositories> </project>
2011년 1월 25일 화요일
Servlet3.0 - Annotation 및 Nonblocking 서블릿 구현
서블릿 스펙 3.0이 발표된지도 꽤 시간이 지났는데, 한참동안을 살펴보지 못했다.
요즘은 스프링 프레임워크를 이용하여 개발하다보니 Controller 가 웹 어플리케이션의 전부인것처럼 느껴질때도 있다.
그동안 관심을 가지고 있었던 Nonblocking 서블릿과 함께 어노테이션 기반의 서블릿 개발을 함께 살펴 보도록 하자.
서블릿 요청은 서블릿 스펙 2.5 까지만해도 thread for request 모델이었다. 모든 요청당 하나의 쓰레드가 할당되어 처리되도록 구현된 것이다.
Nonblocking 서블릿이란 처리 시간이 많이 소요되는 요청이라든지 메시지 큐를 이용하여 응답을 전송해야 하는 경우에 비동기적으로 요청을 처리하는 방식을 의미한다. HTTP1.1에서 연결을 지속적으로 유지할 수 있는 특징을 이용하여 서블릿 요청과 응답을 큐에 저장해두고 쓰레드는 반환 시킨 후 응답을 처리할 수 있을 때 다시 처리하는 것이다. 이렇게 하면 처리시간이 많이 소요되는 서블릿이라 하더라도 서블릿 요청이 쓰레드를 독점하지 않기 때문에 쓰레드를 고갈시키지 않는다.
Jetty나 Tomcat6.x 에서 자체적으로 Nonblocking 서블릿을 구현하기 위해 컨테이너에 의존적인 방법들을 소개하였지만 이는 이식성을 결여된 코드를 작성하는 것이기 때문에 추천할 만한 방법은 결코 아니다.
서블릿 3.0에서 Asyncronous processing 이라 하여 요청을 비동기적으로 처리할 수 있는 방법을 스펙에 포함시켰다.
Tomcat7.x에서 서블릿3.0을 지원하기 때문에 Tomcat7.x 에서 테스트 하도록 한다.
JDK는 1.6 이상의 버전을 사용한다.
web.xml
web.xml 파일은 web-app_3_0.xsd 스키마를 사용하도록 한다.
web-app 속성중에서 metadata-complete 를 false로 지정해야 서블릿 컨테이너가 로드되는 시점에 클래스 패스를 검사하여 Annotation이 설정된 클래스들을 서블릿으로 등록시킨다.
javax.servlet.annotation 패키지에 서블릿 개발 시 사용할 수 있는 annotation 클래스들이 있으면, WebServlet, WebFilter, WebListener등이 자주 사용된다.
살펴 볼 소스 코드는 glassfish 3.0x 버전의 샘플코드로 제공되는 소스다.
@WebServlet annotation의 asyncSupported 값을 true로 지정해야 한다.
doGet() 메서드를 살펴보면 AsyncContext를 생성한 후 queue에 추가시키고 요청을 끝낸다.
이 시점에 해당 서블릿에 할당된 쓰레드는 쓰레드 풀에 반환된다.
doPost() 메서드에는 doGet에서 생성된 asyncContext를 이용하여 각각의 명령에 따라서 결과를 클라이언트에게 전송해 준다.
아래의 소스코드에서는 Comet에서 즐겨 사용하는 방식으로 보이지 않는 iframe과 연결을 시켜두고 javascript로 iframe이 포함되어 있는 페이지의 javascript function을 호출하는 방식을 사용하였다.
chat.html
원래 소스코드는 prototype을 이용하여 작성되었으나 jquery로 변경하여 테스트 하였다.
브라우저에서 위의 소스코드를 테스트하고자 한다면 http://localhost:8080/async-request-war/chat 라고 URL을 입력하면 테스트 할 수 있다.
CSS파일과 jquery.js 파일은 생략하도록 한다.
요즘은 스프링 프레임워크를 이용하여 개발하다보니 Controller 가 웹 어플리케이션의 전부인것처럼 느껴질때도 있다.
그동안 관심을 가지고 있었던 Nonblocking 서블릿과 함께 어노테이션 기반의 서블릿 개발을 함께 살펴 보도록 하자.
서블릿 요청은 서블릿 스펙 2.5 까지만해도 thread for request 모델이었다. 모든 요청당 하나의 쓰레드가 할당되어 처리되도록 구현된 것이다.
Nonblocking 서블릿이란 처리 시간이 많이 소요되는 요청이라든지 메시지 큐를 이용하여 응답을 전송해야 하는 경우에 비동기적으로 요청을 처리하는 방식을 의미한다. HTTP1.1에서 연결을 지속적으로 유지할 수 있는 특징을 이용하여 서블릿 요청과 응답을 큐에 저장해두고 쓰레드는 반환 시킨 후 응답을 처리할 수 있을 때 다시 처리하는 것이다. 이렇게 하면 처리시간이 많이 소요되는 서블릿이라 하더라도 서블릿 요청이 쓰레드를 독점하지 않기 때문에 쓰레드를 고갈시키지 않는다.
Jetty나 Tomcat6.x 에서 자체적으로 Nonblocking 서블릿을 구현하기 위해 컨테이너에 의존적인 방법들을 소개하였지만 이는 이식성을 결여된 코드를 작성하는 것이기 때문에 추천할 만한 방법은 결코 아니다.
서블릿 3.0에서 Asyncronous processing 이라 하여 요청을 비동기적으로 처리할 수 있는 방법을 스펙에 포함시켰다.
Tomcat7.x에서 서블릿3.0을 지원하기 때문에 Tomcat7.x 에서 테스트 하도록 한다.
JDK는 1.6 이상의 버전을 사용한다.
Annotation을 이용한 웹 어플리케이션 개발
web.xml
<?xml version="1.0" encoding="utf-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0" metadata-complete="false"> <display-name>Servlet3.0 Web Application</display-name> </web-app>
web.xml 파일은 web-app_3_0.xsd 스키마를 사용하도록 한다.
web-app 속성중에서 metadata-complete 를 false로 지정해야 서블릿 컨테이너가 로드되는 시점에 클래스 패스를 검사하여 Annotation이 설정된 클래스들을 서블릿으로 등록시킨다.
javax.servlet.annotation 패키지에 서블릿 개발 시 사용할 수 있는 annotation 클래스들이 있으면, WebServlet, WebFilter, WebListener등이 자주 사용된다.
살펴 볼 소스 코드는 glassfish 3.0x 버전의 샘플코드로 제공되는 소스다.
@WebServlet annotation의 asyncSupported 값을 true로 지정해야 한다.
doGet() 메서드를 살펴보면 AsyncContext를 생성한 후 queue에 추가시키고 요청을 끝낸다.
이 시점에 해당 서블릿에 할당된 쓰레드는 쓰레드 풀에 반환된다.
doPost() 메서드에는 doGet에서 생성된 asyncContext를 이용하여 각각의 명령에 따라서 결과를 클라이언트에게 전송해 준다.
아래의 소스코드에서는 Comet에서 즐겨 사용하는 방식으로 보이지 않는 iframe과 연결을 시켜두고 javascript로 iframe이 포함되어 있는 페이지의 javascript function을 호출하는 방식을 사용하였다.
package web.servlet.async_request_war; import java.io.IOException; import java.io.PrintWriter; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.ServletConfig; 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(urlPatterns = {"/chat"}, asyncSupported = true) public class AjaxCometServlet extends HttpServlet { private static final Queue<AsyncContext> queue = new ConcurrentLinkedQueue<AsyncContext>(); private static final BlockingQueue<String> messageQueue = new LinkedBlockingQueue<String>(); private static final String BEGIN_SCRIPT_TAG = "<script type='text/javascript'>\n"; private static final String END_SCRIPT_TAG = "</script>\n"; private static final long serialVersionUID = -2919167206889576860L; private Thread notifierThread = null; @Override public void init(ServletConfig config) throws ServletException { Runnable notifierRunnable = new Runnable() { public void run() { boolean done = false; while (!done) { String cMessage = null; try { cMessage = messageQueue.take(); for (AsyncContext ac : queue) { try { PrintWriter acWriter = ac.getResponse().getWriter(); acWriter.println(cMessage); acWriter.flush(); } catch(IOException ex) { System.out.println(ex); queue.remove(ac); } } } catch(InterruptedException iex) { done = true; System.out.println(iex); } } } }; notifierThread = new Thread(notifierRunnable); notifierThread.start(); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); res.setHeader("Cache-Control", "private"); res.setHeader("Pragma", "no-cache"); PrintWriter writer = res.getWriter(); // for IE writer.println("<!-- Comet is a programming technique that enables web servers to send data to the client without having any need for the client to request it. -->\n"); writer.flush(); final AsyncContext ac = req.startAsync(); // ac.setTimeout(10 * 60 * 1000); ac.setTimeout(20 * 1000); ac.addListener(new AsyncListener() { public void onComplete(AsyncEvent event) throws IOException { System.out.println("## onComplete"); queue.remove(ac); } public void onTimeout(AsyncEvent event) throws IOException { System.out.println("## onTimeout"); queue.remove(ac); } public void onError(AsyncEvent event) throws IOException { System.out.println("## onError"); queue.remove(ac); } public void onStartAsync(AsyncEvent event) throws IOException { System.out.println("## onStartAsync"); } }); queue.add(ac); } @Override @SuppressWarnings("unchecked") protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); res.setHeader("Cache-Control", "private"); res.setHeader("Pragma", "no-cache"); req.setCharacterEncoding("UTF-8"); String action = req.getParameter("action"); String name = req.getParameter("name"); if ("login".equals(action)) { String cMessage = BEGIN_SCRIPT_TAG + toJsonp("System Message", name + " has joined.") + END_SCRIPT_TAG; notify(cMessage); res.getWriter().println("success"); } else if ("post".equals(action)) { String message = req.getParameter("message"); String cMessage = BEGIN_SCRIPT_TAG + toJsonp(name, message) + END_SCRIPT_TAG; notify(cMessage); res.getWriter().println("success"); } else { res.sendError(422, "Unprocessable Entity"); } } @Override public void destroy() { queue.clear(); notifierThread.interrupt(); } private void notify(String cMessage) throws IOException { try { messageQueue.put(cMessage); } catch(Exception ex) { IOException t = new IOException(); t.initCause(ex); throw t; } } private String escape(String orig) { StringBuffer buffer = new StringBuffer(orig.length()); for (int i = 0; i < orig.length(); i++) { char c = orig.charAt(i); switch (c) { case '\b': buffer.append("\\b"); break; case '\f': buffer.append("\\f"); break; case '\n': buffer.append("<br />"); break; case '\r': // ignore break; case '\t': buffer.append("\\t"); break; case '\'': buffer.append("\\'"); break; case '\"': buffer.append("\\\""); break; case '\\': buffer.append("\\\\"); break; case '<': buffer.append("<"); break; case '>': buffer.append(">"); break; case '&': buffer.append("&"); break; default: buffer.append(c); } } return buffer.toString(); } private String toJsonp(String name, String message) { return "window.parent.app.update({ name: \"" + escape(name) + "\", message: \"" + escape(message) + "\" });\n"; } }
chat.html
원래 소스코드는 prototype을 이용하여 작성되었으나 jquery로 변경하여 테스트 하였다.
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Glassfish Chat</title> <link rel="stylesheet" href="stylesheets/default.css" type="text/css" /> <script type="text/javascript" src="javascripts/jquery.js"></script> <script type="text/javascript"> var count = 0; var app = { url: '/async-request-war/chat', initialize: function() { $('#login-name').focus(); app.listen(); }, listen: function() { $('#comet-frame').attr('src', app.url + '?' + count); count++; }, login: function() { alert("login"); var name = $('#login-name').val(); if(! name.length > 0) { $('#system-message').css('color', 'red'); $('#login-name').focus(); return; } $('#system-message').css('color','#2d2b2d'); $('#system-message').html(name + ":"); $('#login-button').attr('disabled', 'true'); $('#login-form').hide(); $('#message-form').show(); var query = 'action=login' + '&name=' + encodeURI(name); app.callAjaxPost(query, function(data){ $('#message').focus(); }, function() { }); }, callAjaxPost: function(query, successFunc, completeFunc) { jQuery.ajax({ type: "post", async: false, url: app.url, data: query, error:?function(){ alert('Occur Error'); }, success:?successFunc, complete: completeFunc }); }, post: function() { var name = $('#login-name').val(); var message = $('#message').val(); if(!message.length > 0) { return; } $('#message').attr('disabled', 'true'); $('post-button').attr('disabled', 'true'); var query = 'action=post' + '&name=' + encodeURI(name) + '&message=' + encodeURI(message); app.callAjaxPost(query, function() {}, function() { $('#message').removeAttr('disabled'); $('#post-button').removeAttr('disabled'); $('#message').focus(); $('#message').val(''); }); }, update: function(data) { var name = data.name; var message = data.message; var p = $('<p>' + name + ':<br/>' + message + '</p>'); $('#display').append(p); $('#display').attr('scrollTop', $('#display').attr('scrollHeight')); } }; $(document).ready(function() { $('#login-name').keydown(function(event) { if(event.keyCode == 13) { $('#login-button').focus(); } }); $('#message').keydown(function(event) { if(event.shiftKey && event.keyCode == 13) { $('#post-button').focus(); } }); $('#login-button').click(app.login); $('#post-button').click(app.post); app.initialize(); }); </script> </head> <body> <div id="container"> <div id="container-inner"> <div id="header"> <h1>Glassfish Chat</h1> </div> <div id="main"> <div id="display"> </div> <div id="form"> <div id="system-message">Please input your name:</div> <div id="login-form"> <input id="login-name" type="text" /> <br /> <input id="login-button" type="button" value="Login" /> </div> <div id="message-form" style="display: none;"> <div> <textarea id="message" name="message" rows="2" cols="40"></textarea> <br /> <input id="post-button" type="button" value="Post Message" /> </div> </div> </div> </div> </div> </div> <iframe id="comet-frame" name='cometFrame' style="display: none;"></iframe> </body> </html>
브라우저에서 위의 소스코드를 테스트하고자 한다면 http://localhost:8080/async-request-war/chat 라고 URL을 입력하면 테스트 할 수 있다.
CSS파일과 jquery.js 파일은 생략하도록 한다.
2011년 1월 17일 월요일
Spring에서 JUnit 테스트 시 Transaction 처리
http://viralpatel.net/blogs/2010/11/spring3-mvc-hibernate-maven-tutorial-eclipse-example.html
위의 블로그에 방문하면 spring3 기반에서 hibernate를 이용해서 dao를 개발하는 방법을 비교적 친절하고 상세하게 잘 정리해 두었다. 참고하기 바란다.
이번 포스트에서 이야기하고자 하는 내용은 DAO 컴포넌트를 JUnit으로 테스트 할 때 Transaction 처리에 관한 내용이다.
위의 블로그는 WEB-INF 폴더에 있는 spring-servlet.xml 파일과 jdbc.properties 파일을 조금 수정해 주어야 한다.
위의 두 파일을 src/main/resources 소스 폴더에 spring 이란 패키지를 만들어서 복사한다.
spring-servlet.xml 파일에서 jdbc.properties 파일을 참조하는 부분을 아래와 같이 수정한다.
이제서야 JUnit TestCase를 작성할 사전 준비가 완료되었다.
위의 소스를 살펴보자.
@RunWith(SpringJUnit4ClassRunner.class)
@RunWith Annotation을 사용해야 JUnit에서 Spring을 설정을 그대로 사용할 수 있다. 테스트케이스에서 스프링에서 설정한 Bean을 Injection 받아서 사용할 수 있다는 의미다.
그런데 SpringJUnit4ClassRunner.class 사용시 주의해야 할 사항이 있다.
Spring 2.5.x 버전과 Spring 3.0.x 에서의 상속받는 상위 클래스가 다르다.
2.5.x 버전의 SpringJUnit4ClassRunner는 org.junit.internal.runners.JUnit4ClassRunner 클래스를 상속받아 구현되었는데 이는 JUnit4.3 이상에서 제공하는 클래스다.
3.0.x 버전의 SpringJUnit4ClassRunner는 org.junit.runners.BlockJUnit4ClassRunner 클래스를 상속받아 구현되었는데 이는 JUnit4.5 이상에서 제공하는 클래스다.
따라서 spring 버전에 따라서 사용해야하는 junit 버전이 달라짐을 유의해야 한다.
@ContextConfiguration(locations={"classpath*:spring/spring-servlet.xml"})
WEB-INF/spring-servlet.xml 파일을 리소스 폴더로 옮기고 그 클래스패스를 설정해 주었다.
먼저 DAO 구현 클래스를 살펴보자.
소스코드를 살펴보면 "sessionFactory.getCurrentSession()" 부분이 있다.
sessionFactory는 Hibernate의 세션을 관리하는 팩토리로
getCurrentSession()은 트랜잭션을 가지고 있는 현재 세션을 리턴한다.
따라서 트랜잭션에 없는 상황에서는 "No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here"라는 에러가 발생한다.
위의 ContactDAOTest에서 @Transactional annotation을 제거하고 실행하면 위와 같은 에러가 발생하는 것을 확인할 수 있다.
일반적인 경우라면 DAO에 Transactional annotation을 붙이지는 않는다.
보통 DAO를 사용하고 있는 서비스에 붙히는데, 전에는 서비스 인터페이스에 설정하는 걸 즐겨했다. 하지만 인터페이스에 @Transactional을 설정하는 것보다 구현 클래스에 설정하는게 더 적합하다. 인터페이스는 Transaction 설정과 같은 세부사항에 관여하면 안 되기 때문이다.
어쨋거나 이 글에서 이야기하고자 하는 내용은 지금부터다.
JUnit의 @Test annotation을 설정하여 DAO에 대한 테스트를 실행하는 경우 Insert, Update, Delete를 테스트할때마다 매번 DB에 반영되는게 번거롭고 문제도 가끔씩 발생시킨다.
테스트 자동화시에는 대부분의 경우 이렇게 데이터에 영향을 미치는 연산을 제외시키기 위해서 @Ignore annotation을 테스트 메서드에 달아 두곤 했다.
하지만 스프링에서 @Test annotation과 함께 설정된 @Transactional은 항상 rollback된다.
따라서 create, update, delete의 로직이 정상적으로 수행됨은 확인하면서도 실제로 DB에 반영되지는 않는다. 롤백이 되는 것과 에러가 발생하는것은 분명한 차이가 있다. PK가 중복된다면 rollback이 아니라 에러가 발생한 경우다. 데이터베이스에 데이터가 없다고 정상처리되지 않았다고 판단하면 안된다. JUnit이 초록색 불을 밝히는 것과 붉은색 불을 밝히는 것은 엄청난 차이가 있다. JUnit이 초록불이라면 데이터베이스에 값이 반영되지 않는다 하더라도 대부분의 경우에는 믿어도 된다.
매번 실행한다고 해도 데이터에 영향이 미치지 않는다.
하지만 눈으로 직접 추가, 변경, 삭제를 확인해보고 싶다면 어떻게 해야 할까. JUnit의 @Test 메서드를 실행하면서도 트랜잭션이 롤백되지 않도록 설정하면 된다.
첫번째 방법은 org.springframework.test.annotation.Rollback annotation을 사용하면 된다.
위와 같이 @Rollback(false)로 설정하면 테스트 메서드라 하더라도 롤백되지 않는다.
다른 방법으로는 테스트 클래스에 지정하는 방식이다.
org.springframework.test.context.transaction.TransactionConfiguration annotation을 사용한다.
위와 같이 설정하면 모든 테스트 메서드의 롤백이 실행되지 않고 데이터베이스에 반영된다.
만약 위와 같이 설정하고 특정 메서드에서 롤백이 되기를 원한다면 @Rollback(true)로 지정하면 된다.
스프링의 테스팅을 상세히 살펴보고자 한다면 http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/testing.html 사이트에 방문하기를 권한다.
위의 블로그에 방문하면 spring3 기반에서 hibernate를 이용해서 dao를 개발하는 방법을 비교적 친절하고 상세하게 잘 정리해 두었다. 참고하기 바란다.
이번 포스트에서 이야기하고자 하는 내용은 DAO 컴포넌트를 JUnit으로 테스트 할 때 Transaction 처리에 관한 내용이다.
위의 블로그는 WEB-INF 폴더에 있는 spring-servlet.xml 파일과 jdbc.properties 파일을 조금 수정해 주어야 한다.
위의 두 파일을 src/main/resources 소스 폴더에 spring 이란 패키지를 만들어서 복사한다.
spring-servlet.xml 파일에서 jdbc.properties 파일을 참조하는 부분을 아래와 같이 수정한다.
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer" p:location="spring/jdbc.properties"> </bean>
이제서야 JUnit TestCase를 작성할 사전 준비가 완료되었다.
package net.viralpatel.contact.dao; import java.util.List; import junit.framework.Assert; import net.viralpatel.contact.form.Contact; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations={"classpath*:spring/spring-servlet.xml"}) public class ContactDAOTest { @Autowired private ContactDAO contactDAO; @Test @Transactional public void testListContact() throws Exception { ListcontactList = contactDAO.listContact(); Assert.assertNotNull(contactList); } }
위의 소스를 살펴보자.
@RunWith(SpringJUnit4ClassRunner.class)
@RunWith Annotation을 사용해야 JUnit에서 Spring을 설정을 그대로 사용할 수 있다. 테스트케이스에서 스프링에서 설정한 Bean을 Injection 받아서 사용할 수 있다는 의미다.
그런데 SpringJUnit4ClassRunner.class 사용시 주의해야 할 사항이 있다.
Spring 2.5.x 버전과 Spring 3.0.x 에서의 상속받는 상위 클래스가 다르다.
2.5.x 버전의 SpringJUnit4ClassRunner는 org.junit.internal.runners.JUnit4ClassRunner 클래스를 상속받아 구현되었는데 이는 JUnit4.3 이상에서 제공하는 클래스다.
3.0.x 버전의 SpringJUnit4ClassRunner는 org.junit.runners.BlockJUnit4ClassRunner 클래스를 상속받아 구현되었는데 이는 JUnit4.5 이상에서 제공하는 클래스다.
따라서 spring 버전에 따라서 사용해야하는 junit 버전이 달라짐을 유의해야 한다.
@ContextConfiguration(locations={"classpath*:spring/spring-servlet.xml"})
WEB-INF/spring-servlet.xml 파일을 리소스 폴더로 옮기고 그 클래스패스를 설정해 주었다.
먼저 DAO 구현 클래스를 살펴보자.
package net.viralpatel.contact.dao; import java.util.List; import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import net.viralpatel.contact.form.Contact; @Repository() public class ContactDAOImpl implements ContactDAO { @Autowired private SessionFactory sessionFactory; public void addContact(Contact contact) { sessionFactory.getCurrentSession().save(contact); } @SuppressWarnings("unchecked") public ListlistContact() { return sessionFactory.getCurrentSession().createQuery("from Contact").list(); } public void removeContact(Integer id) { Contact contact = (Contact) sessionFactory.getCurrentSession().load(Contact.class, id); if(contact != null) { sessionFactory.getCurrentSession().delete(contact); } } }
소스코드를 살펴보면 "sessionFactory.getCurrentSession()" 부분이 있다.
sessionFactory는 Hibernate의 세션을 관리하는 팩토리로
getCurrentSession()은 트랜잭션을 가지고 있는 현재 세션을 리턴한다.
따라서 트랜잭션에 없는 상황에서는 "No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here"라는 에러가 발생한다.
위의 ContactDAOTest에서 @Transactional annotation을 제거하고 실행하면 위와 같은 에러가 발생하는 것을 확인할 수 있다.
일반적인 경우라면 DAO에 Transactional annotation을 붙이지는 않는다.
보통 DAO를 사용하고 있는 서비스에 붙히는데, 전에는 서비스 인터페이스에 설정하는 걸 즐겨했다. 하지만 인터페이스에 @Transactional을 설정하는 것보다 구현 클래스에 설정하는게 더 적합하다. 인터페이스는 Transaction 설정과 같은 세부사항에 관여하면 안 되기 때문이다.
어쨋거나 이 글에서 이야기하고자 하는 내용은 지금부터다.
JUnit의 @Test annotation을 설정하여 DAO에 대한 테스트를 실행하는 경우 Insert, Update, Delete를 테스트할때마다 매번 DB에 반영되는게 번거롭고 문제도 가끔씩 발생시킨다.
테스트 자동화시에는 대부분의 경우 이렇게 데이터에 영향을 미치는 연산을 제외시키기 위해서 @Ignore annotation을 테스트 메서드에 달아 두곤 했다.
하지만 스프링에서 @Test annotation과 함께 설정된 @Transactional은 항상 rollback된다.
따라서 create, update, delete의 로직이 정상적으로 수행됨은 확인하면서도 실제로 DB에 반영되지는 않는다. 롤백이 되는 것과 에러가 발생하는것은 분명한 차이가 있다. PK가 중복된다면 rollback이 아니라 에러가 발생한 경우다. 데이터베이스에 데이터가 없다고 정상처리되지 않았다고 판단하면 안된다. JUnit이 초록색 불을 밝히는 것과 붉은색 불을 밝히는 것은 엄청난 차이가 있다. JUnit이 초록불이라면 데이터베이스에 값이 반영되지 않는다 하더라도 대부분의 경우에는 믿어도 된다.
매번 실행한다고 해도 데이터에 영향이 미치지 않는다.
하지만 눈으로 직접 추가, 변경, 삭제를 확인해보고 싶다면 어떻게 해야 할까. JUnit의 @Test 메서드를 실행하면서도 트랜잭션이 롤백되지 않도록 설정하면 된다.
첫번째 방법은 org.springframework.test.annotation.Rollback annotation을 사용하면 된다.
@Test @Transactional @Rollback(false) public void testAddContact() throws Exception { Contact contact = new Contact(); contact.setFirstname("YG"); contact.setLastname("Kim"); contactDAO.addContact(contact); Assert.assertNotSame(0, contact.getId()); }
위와 같이 @Rollback(false)로 설정하면 테스트 메서드라 하더라도 롤백되지 않는다.
다른 방법으로는 테스트 클래스에 지정하는 방식이다.
org.springframework.test.context.transaction.TransactionConfiguration annotation을 사용한다.
@TransactionConfiguration(transactionManager="transactionManager", defaultRollback=false) public class ContactDAOTest { ... }
위와 같이 설정하면 모든 테스트 메서드의 롤백이 실행되지 않고 데이터베이스에 반영된다.
만약 위와 같이 설정하고 특정 메서드에서 롤백이 되기를 원한다면 @Rollback(true)로 지정하면 된다.
스프링의 테스팅을 상세히 살펴보고자 한다면 http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/testing.html 사이트에 방문하기를 권한다.
피드 구독하기:
글 (Atom)