자바 웹 애플리케이션 개발자로서, 여러분은 요청 (HttpServletRequest)과 세션 (HttpSession) 범위를 바로 배우셨을 겁니다. 이들 범위와 데이터, 객체 다루기를 이해하는 것은자바에서 웹 애플리케이션을 디자인하고 빌드하는데 중요한 요소입니다. [리퀘스트와 세션의 범위에 대해서는 스택 오버플로우 게시물을 참조하면 빠르게 배울 수 있습니다]

Spring MVC 범위

Spring MVC에서 웹 애플리케이션을 시작했을 때, Spring 모델과 세션 애트리뷰트는 이해하기 어려웠습니다. 특히, HTTP 리퀘스트와 세션 범위, 거기서 애트리뷰트에 관해서 말입니다. Spring 모델 요소는 세션이나 요청에서 똑같이 나타날까요? 그렇다면, 어떻게 제어할까요? 이 글에서, Spring의 MVC 모델과 세션이 동작하는 것을 파헤치도록 하겠습니다.

Spring의 @ModelAttribute

Spring의 모델에 데이터나 객체를 추가하는 몇 가지 방법이 있습니다. 데이터나 객체는 흔히 컨트롤러(controller)에 쓰인 어노테이션(annotation, 주석)을 통해 추가됩니다. 아래 예시에서 @ModelAttribute는 MyCommandBean의 인스턴스를 “myRequestObject” 키의 값으로 모델에 집어넣기 위해 쓰였습니다.

@Controller
public class MyController {

	@ModelAttribute("myRequestObject")
	public MyCommandBean addStuffToRequestScope() {
		System.out.println("Inside of addStuffToRequestScope");
		MyCommandBean bean = new MyCommandBean("Hello World",42);
		return bean;
	}

	@RequestMapping("/dosomething")
	public String requestHandlingMethod(Model model, HttpServletRequest request) {
		System.out.println("Inside of dosomething handler method");

		System.out.println("--- Model data ---");
		Map modelMap = model.asMap();
		for (Object modelKey : modelMap.keySet()) {
			Object modelValue = modelMap.get(modelKey);
			System.out.println(modelKey + " -- " + modelValue);
		}

		System.out.println("=== Request data ===");
		java.util.Enumeration reqEnum = request.getAttributeNames();
		while (reqEnum.hasMoreElements()) {
			String s = reqEnum.nextElement();
			System.out.println(s);
			System.out.println("==" + request.getAttribute(s));
		}

		return "nextpage";
	}

         //  ... the rest of the controller
}

들어오는 요청에서 @ModelAttribute로 주석 처리 된 모든 메서드는 모든 컨트롤러 처리기 메서드 (예 : 위 예제의 requestHandlingMethod)보다 먼저 호출됩니다. 이 메서드는 처리기 메서드 실행 전에 Spring 모델에 java.util.Map으로 데이터를 추가합니다. 간단한 시험으로 검증할 수 있습니다. 저는 두 개의 JSP 페이지를 만들었습니다. index.jsp와 nextpage.jsp입니다. index.jsp는 MyController의 requestHandlingMethod()를 트리거하는 애플리케이션에 요청을 보내는데 쓰입니다. 위 코드에 대해, requestHandlingMethod()는 여기서 nextpage.jsp로 연결되는 다음 뷰의 논리적 이름인 “nextpage”를 반환합니다.

조그만 웹 사이트가 이렇게 실행될 때, 컨트롤러의 System.out.println는 @ModelAttribute 메서드가 핸들러 메서드 전에 어떻게 실행되었는지 나타냅니다. 또한 MyCommandBean이 생성되고 Spring의 모델에 추가되어서 핸들러 메서드에서 유효한 것도 나타냅니다.

Inside of addStuffToRequestScope
Inside of dosomething handler method
--- Model data ---
myRequestObject -- MyCommandBean [someString=Hello World, someNumber=42]
=== Request data ===
org.springframework.web.servlet.DispatcherServlet.THEME_SOURCE
==WebApplicationContext for namespace 'dispatcher-servlet': startup date [Sun Oct 13 21:40:56 CDT 2013]; root of context hierarchy
org.springframework.web.servlet.DispatcherServlet.THEME_RESOLVER
==org.springframework.web.servlet.theme.FixedThemeResolver@204af48c
org.springframework.web.servlet.DispatcherServlet.CONTEXT
==WebApplicationContext for namespace 'dispatcher-servlet': startup date [Sun Oct 13 21:40:56 CDT 2013]; root of context hierarchy
org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping
==dosomething.request
org.springframework.web.servlet.HandlerMapping.bestMatchingPattern
==/dosomething.*
org.springframework.web.servlet.DispatcherServlet.LOCALE_RESOLVER
==org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver@18fd23e4

이제 남은 질문은 “Spring 모델 데이터는 어디에 수납되는데?”죠. Java의 기본 요청 범위에 저장되는 것일까요? 정답은 , 결국은요. 위 출력에서 알 수 있듯이, MyCommandBean은 모델이지만, 처리기 메서드가 실행될 때는 요청 객체는 아닙니다. 대신, Spring은 처리기 메서드의 실행과 다음 뷰(여기서는 nextpage.jsp) 표시 전까지 애트리뷰트로서 요청에 모델 데이터를 추가하지 않습니다.

이는 index.jsp와 nextpage.jsp 양쪽에서 HttpServletRequest에 저장된 애트리뷰트 데이터를 출력하면서 알 수 있습니다. HttpServletRequest 애트리뷰트를 표시하기 위한 JSP 스크립틀릿(아래 설명)를 사용하는 이런 두 페이지를 모아봤습니다.

애플리케이션이 실행되고 index.jsp가 표시될 때, 요청 범위에는 애트리뷰트가 없는 것을 알 수 있습니다.

Request Scope (key==values)
<% java.util.Enumeration reqEnum = request.getAttributeNames(); while (reqEnum.hasMoreElements()) { String s = reqEnum.nextElement(); out.print(s); out.println("==" + request.getAttribute(s)); %>
<% } %>

이 경우, “do something” 링크를 누르면 MyController의 처리기 메서드가 실행되게 만듭니다. 그리고 이게 nextpage.jsp를 표시되게 만듭니다. 주어진 같은 JSP 스크립틀릿이 nextpage.jsp에 있는데, 이 또한 요청 범위를 렌더합니다.

역시! nextpage.jsp가 렌더될 때, HttpServletRequest 범위에 추가되어서 생성된 모델 MyCommandBean을 보여줍니다! “myRequestObject”의 Spring 모델 애트리뷰트 키는 복사되었고, 요청의 애트리뷰트 키로서 사용되었습니다.

따라서 처리기 메서드 실행 이전(또는 도중)에 만들어진 Spring 모델 데이터는 다음 뷰가 렌더되기 전에 HttpServletRequest에 복사되는 것입니다.

Spring 모델 vs 요청 선택의 이유

Spring이 모델 애트리뷰트를 사용하는 이유가 궁금하실 겁니다. 왜 직접 데이터를 요청 객체에 추가하지 않는 거죠? 저는 이 해답은 Rod Johnson에게서 얻었습니다. Professional Java Development with the Spring Framework라는 책입니다. 이 책은 Spring API에 대해선 좀 낡았는데(Spring 2.0을 다룸), Spring 엔진에 대해 다룬 설명이 살짝 적힌 구절을 발견했습니다. 여기 모델 요소에 대한 내용을 인용해봤습니다.

“(요청 애트리뷰트로서) HttpServletRequest에 바로 요소를 추가하는 것은 같은 목적으로 이뤄진다. 이렇게 하는 이유는 명백한데, MVC 프레임워크에 정한 요구 사항 중 하나를 충족하기 위함이다: 되도록뷰에 의존하지 말아야 한다. 즉, HttpServletRequest에 바인딩되지 않은 뷰 기술을 통합하길 바란다는 것이다.(페이지 429-430)

Spring의 @SESSIONATTRIBuTES

그럼 이제 Spring 모델 데이터가 어떻게 관리되는지, 일반적인 Http 요청 애트리뷰트 데이터랑 어떻게 연관되어 있는지도 알았습니다. Spring의 세션 데이터는 어떨까요?

Spring의 @SessionAttributes는 어떤 모델 애트리뷰트가 세션에 저장되어야 하는지를 컨트롤러에게 지시하는데 쓰입니다. 실제로, 솔직히 말해서, Spring 문서는 @SessionAttributes 어노테이션이 세션과 일부 대화식 저장소에 투명하게 저장되어야 하는 모델 애트리뷰트의 이름을 나열한다고 되어 있습니다. 다시 말해, “일부 대화식 저장소”는 Spring MVC가 디자인에 구애받지 않는 기술을 유지하력 노력하는 방법을 제시합니다.

실제로, @SessionAttributes는 어떤 모델 애트리뷰트가 뷰를 렌더링하기 전에 복사되어야 하는지 Spring에게 알려줄 수 있게 합니다. 다시 말해, 이는 간단한 코드로 검증 가능합니다.

index.jsp와 nextpage.jsp에서 HttpSession 애트리뷰트를 보이기 위한 추가 JSP 스크립틀릿을 덧붙였습니다.

<% java.util.Enumeration sessEnum = request.getSession() .getAttributeNames(); while (sessEnum.hasMoreElements()) { String s = sessEnum.nextElement(); out.print(s); out.println("==" + request.getSession().getAttribute(s)); %>
<% } %>

Spring 세션에서 같은 모델 애트리뷰트(myRequestObject)를 넣기 위해 @SessionAttributes로 MyController를 어노테이션으로 추가했습니다.

@Controller
@SessionAttributes("myRequestObject")
public class MyController {
  ...
}

또한 컨트롤러의 처리기 메서드에 코드를 추가하여서, HttpSession에 어떤 애트리뷰트가 있는지 (HttpServletRequest에서 보이는 그대로를) 보이게 했습니다.

@SuppressWarnings("rawtypes")
@RequestMapping("/dosomething")
public String requestHandlingMethod(Model model, HttpServletRequest request, HttpSession session) {
  System.out.println("Inside of dosomething handler method");

  System.out.println("--- Model data ---");
  Map modelMap = model.asMap();
  for (Object modelKey : modelMap.keySet()) {
	Object modelValue = modelMap.get(modelKey);
	System.out.println(modelKey + " -- " + modelValue);
  }

  System.out.println("=== Request data ===");
  java.util.Enumeration reqEnum = request.getAttributeNames();
  while (reqEnum.hasMoreElements()) {
	String s = reqEnum.nextElement();
	System.out.println(s);
	System.out.println("==" + request.getAttribute(s));
  }

  System.out.println("*** Session data ***");
  Enumeration e = session.getAttributeNames();
  while (e.hasMoreElements()){
	String s = e.nextElement();
	System.out.println(s);
	System.out.println("**" + session.getAttribute(s));
  }

  return "nextpage";
}

이제, @SessionAttrubute로 주석처리되면 단일 HTTP 요청을 Spring MVC가 처리하기 전, 도중, 후에 세션 객체에 무엇이 있는지 볼 수 있습니다. 우선, index.jsp 페이지가 (요청이 전송되고 Spring MVC에 의해 처리되기 전에) 보이면서, 우리는 HttpServletRequest나 HttpSession 어디에도 애트리뷰트 데이터가 없다는 것을 알 수 있습니다.

처리기 메서드(requestHandlingMethod) 실행 도중에, MyCommandBean이 Spring 모델 애트리뷰트에 추가되었지만, 아직 HttpServletReuest나 HttpSession 영역에 없는 것을 알 수 있습니다.

그러나 처리기 메서드가 실행되고, nextpage.jsp가 렌더될 때, 모델 애트리뷰트 데이터(MyCommandBean)이 (같은 애트리뷰트 키로) HttpServletRequest와 HttpSession에 복사되는 것을 알 수 있습니다.

세션 애트리뷰트 제어

그럼 이제 여러분은 Spring 모델과 세션 애트리뷰트 데이터가 어떻게 HttpServletRequest와 HttpSession에 추가되는지를 알게 되어서 감탄하셨을 겁니다. 그러나 이제 Spring 세션에서 데이터를 관리하는 방법에 대해 신경쓰셔야 합니다. Spring은 Spring 세션 애트리뷰트를 제거하고, (HttpSession 전체를 없애지 않고) HttpSession으로부터 삭제하는 수단을 제공합니다. 단순히 Spring SessionStatus 객체를 매개변수로 컨트롤러 처리기 메서드에 추가만 하면 됩니다. 이 방법에선 SessionStatus 객체를 사용하여 Spring 세션을 끝냅니다.

@RequestMapping("/endsession")
public String nextHandlingMethod2(SessionStatus status){
  status.setComplete();
  return "lastpage";
}

숙달

부디 이 글을 통해 여러분이 Spring 모델과 세션 애트리뷰트를 이해하는데 도움이 되셨으면 좋겠습니다. 이는 마술이 아니고, 그저 HttpSession과 HttpServletRequest가 Spring의 모델과 세션 애트리뷰트를 저장하는데 어떻게 사용되는지 이해하는 것입니다. 저는 Intertech 웹 사이트에 코드를 첨부해놓았습니다. 찾아보시고 Spring 모델과 세션을 이해하기 위해 관심을 가지셨다면, 여기서 자유롭게 다운받으세요.

출처

Understanding Spring MVC Model and Session Attributes