2011년 3월 29일 화요일

HttpClient 4.x 샘플

HttpClient 3.x 위주로 사용하다가 HttpClient 4.x 버전으로 통신하는 클라이언트를 소스를 하나 만들었다. 고급스러운 건 별루 없다. 그래도 필요한 기능은 거의 포함되지 않았을까? 설명하기 귀찮다. 대충 알아서 보시길

아래의 소스 코드에는 다음과 같은 기능이 포함되어 있다.
- 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);
    
    List parameters = 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 받을 수 없다.

아래의 필터 클래스를 살펴보자.



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"으로 설정하였다.

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")
  ObjectFactory sessionContextFactory;

  @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")
  ObjectFactory sessionContextFactory;
  
  @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 사용.

예외 처리는 분명 좀 복잡한 주제이기는 하다.
아래의 소스코드를 보자.

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) {
        // 구현로직
    }
}


조금 무의미하고 무책임한 에러 처리 로직을 개선한 것 같은지 모르겠다.