2011년 3월 29일 화요일

세션 스코프 빈 사용

스프링의 빈은 별다른 설정이 없으면 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;
 }
}

댓글 없음:

댓글 쓰기