SView : S(cala|imple) View for Spring MVC = {
August 6th, 2008
SView : S(cala|imple) View for Spring MVC
들어가는 말
smalltalk부터 시작된 MVC 패턴은 SoC(Separation of Concerns, 관심 분리)이라는 관점에서 응용 프로그램의 복잡도를 관리 할수 있는 아키텍쳐 패턴으로 인기가 있습니다.
특히 웹에서는 MVC 패턴을 구현한 많은 프레임워크가 있는데, 다른 대부분의 웹 프레임워크와 마찬가지로 Spring WebMVC는 여러 뷰/프리젠테이션 기술을 통합하여 사용할수 있습니다.
Scala는 JavaVM이나 .NET CLR위에서 동작하는 객체지향 프로그래밍과 함수형 프로그래밍을 혼합한 multi-paradigm 언어입니다. 여러가지 흥미로운 특징이 있지만, 객체지향 기반의 패턴 매칭과 언어 신택스차원에서 지원하는 XML Processing은 XML 처리에 용이한 특징이 있습니다.
위의 이러한 특징들을 이용하여 SpringMVC의 View을 Scala의 XML Processing을 이용해서 구현해보았습니다.
관련 소스는 http://github.com/anarcher/sview/tree/master 에 있으며, git을 이용해서 받으실수 있습니다. ( git clone git://github.com/anarcher/sview.git )
SpringMVC의 View,ViewResolver
대개의 MVC 프레임워크가 그러하듯이 Spring WebMVC도 View을 선택하는 기능을 제공합니다. 이 부분에 대한 중요한 두개의 인터페이스가 있는데, org.springframework.web.servlet.View와 org.springframework.web.servlet.ViewResolver입니다.
ViewResolver은 말 그대로 viewName 과 실제 View을 결정해 주는 객체입니다. View는 넘어온 Request을 해당 View 구현체에게 위임하여 처리하는 객체입니다.
특히 ViewResolver는 대개 Ordered 인터페이스를 구현하여 여러 ViewResolver을 등록하여 사용 할 수 있게 구현합니다. (Chain of responsibility)
- <bean id="sviewBeanNameViewResolver" class="sview.SViewBeanNameViewResolver">
<property name="order" value="1" />
<property name="prettyPrint" value="true" />
<property name="sviewFactoryBeanName" value="sviewSpringBeanFactory" />
</bean>
<bean id="sviewSpringBeanFactory" class="sview.SViewSpringBeanFactory"></bean> -
<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"><value>org.springframework.web.servlet.view.JstlView</value></property>
<property name="prefix"><value>/WEB-INF/views/</value></property>
<property name="suffix"><value>.jsp</value></property>
</bean>
ViewResolver의 구현상속을 하려면 View resolveViewName(String viewName,Locale locale)이라는 메소드를 구현해야 합니다.
즉 전달받은 ViewName으로 View을 결정해 달라는 부분이라고 할수 있습니다.
- public class SViewBeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver,Ordered {
private final static String DELIMITER = "sview:";
private int order = Integer.MAX_VALUE;
private String sviewFactoryBeanName = "sviewSpringBeanFactory";
private boolean isPrettyPrint = true;
public void setOrder(int order) {
this.order = order;
}
public int getOrder() {
return order;
}
public void setSviewFactoryBeanName(String sviewFactoryBeanName) {
this.sviewFactoryBeanName = sviewFactoryBeanName;
}
public void setPrettyPrint(boolean isPrettyPrint) {
this.isPrettyPrint = isPrettyPrint;
}
public View resolveViewName(String viewName,Locale locale) throws Exception {
if(viewName.indexOf(DELIMITER) < 0) {
return null;
}
String sviewName = viewName.substring(viewName.indexOf(DELIMITER)+6);
WebApplicationContext ctx = super.getWebApplicationContext();
SViewFactory sviewFactory = (SViewFactory) ctx.getBean(this.sviewFactoryBeanName);
ViewAdapter view = new ViewAdapter();
view.setSViewName(sviewName);
view.setSViewFactory(sviewFactory);
view.setPrettyPrint(isPrettyPrint);
return view;
}
}
위의 코드는 ViewResolver의 구현클래스으로, viewName중 DELIMITER으로 시작되는 viewName인지를 검출하고, 스프링 컨텍스트에서 해당 이름으로 등록된 sviewFactory을 얻어 옵니다.
ViewAdapter 는 View 인터페이스의 구현클래스이고 전달받은 sviewFactory을 가지고 해당 sview을 얻어서, 랜더링하는(sviewRenderer)하는 책임이 있습니다.
Scala의 XML Processing
Scala의 XML Processing은 xml을 문법 신택스처럼 사용할수 있습니다. 특히 xml을 코드상에 작성하면 scala 컴파일러가 xml 신택스를 scala.xml에 있는 객체로 교체해줍니다. 정확한 비교는 아니지만, 우리가 문자열을 변수에 할당 할때, String s = "hello" 과 String s = new String("hello")와 같은 것 처럼, scala에서는 val xml =<xml>hello</xml>이 val xml = Elem(null, "xml", Null, TopScope,Text("hello"))와 같습니다.
특히 내장된 표현(embedded expresions)을 사용하면 별도의 템플릿 엔진을 사용하지 않아도 될 정도로 XQuery을 사용하는 듯한 xml 처리가 용이해 질수 있습니다.
- val date = <date>{ df.format(new java.util.Date()) }</date>
- Console.println(date)
Hello World
sview을 이용한 Hello World을 출력하는 간단한 프로그램을 만들어 보겠습니다.
Spring2.5 의 AnnotationBased Controller와 DI을 사용합니다. 우선 SpringMVC의 View,ViewResolver에 나오는 설정을 springmvc-context.xml에 합니다.
그리고 MVC의 Controller과 View을 작성합니다.
- @Controller
public class IndexController
{
@RequestMapping("/index.sview")
public String indexScala(ModelMap models) {
Model model = new Model();
model.setName("HELLO WORLD");
models.addAttribute("model",model);
return "sview:index:sampleLayout";
}
}
IndexController.java
- @Component
@Scope("prototype")
class Index(b:ViewBinding) extends SViewContent(b){
def head() = {
<title> this is index </title>
}
def content() = {
val m : Model = b.getModels.get("model").asInstanceOf[Model]
val df : java.text.DateFormat = java.text.DateFormat.getDateInstance()
<html>
<head>
{ head }
</head>
<body>
<div>
<div> this is index List ! </div>
<div> { m.getName } </div>
<div> today is { df.format(new java.util.Date()) } </div>
</div>
</body>
</html>
}
}
index.scala
IndexController의 indexScala에서 리턴하는 viewName이 "sview:index:sampleLayout"입니다. 앞의 prefix는 sviewResolver에서 검출하는 용도로 쓰이고, index:sampleLayout은 각각 SView의 이름입니다.
sampleLayout.scala는 SiteMesh처럼 index의 html의 head와 body을 자신의 head와 body으로 추가하여 출력하게 됩니다. 간단한 레이아웃 기능이라고 할수 있습니다.
- @Component
@Scope("prototype")
class SampleLayout(b:ViewBinding,v:SView) extends SViewComposite(b,v) {
def content() = {
<html> -
<head>
-
{ v.content \\ "head" \\ "_" }
-
</head>
-
<body>
-
{ v.content \\ "body" \\ "_" }
-
</body>
-
</html>
- }
- }
sampleLayout.scala
대략적인 구성 요소
SView
SView는 Scala의 trait입니다. 자바의 Interface에 해당합니다. SView에는 다음과 같은 메소드가 있습니다.
- trait SView
{
def doctype() : String
def contenttype() : String
def content() : Elem
def render(render : SViewRenderer) : Unit
}
SView은 View에서 필요한 행동을 정의합니다. 이중 기본적인 정보를 지원할수 있는 것은 구현 상속으로 제공하게 됩니다.
- abstract class AbstractSView extends SView {
def contenttype() : String = {"text/html"}
def doctype() : String = {""}
def render(render : SViewRenderer) : Unit = {
render.setContentType(contenttype())
render.setDocType(doctype())
render.setContent(content())
}
}
SView,SViewContent,ViewComposite : Composite , case classes
SView,AbstractSView을 상속받는 클래스에는 SViewContent와 SViewComposite가 있습니다. 이 두 클래스는 Scala의 케이스 클래스입니다.
- abstract case class SViewContent(binding : ViewBinding) extends AbstractSView {}
abstract case class SViewComposite(binding : ViewBinding , view : SView) extends AbstractSView {}
어떤 패턴매칭을 하기 위해서 case class으로 만든 건 아니었습니다.(처음에는 패턴 매칭으로 Render의 책임을 다르게 구성하려고 그랬습니다.)
케이스 클래스는 객체 타입을 패턴으로 분해(decomposition)하기 때문에 erlang의 애리티(arity)처럼 패턴 매칭에 매칭변수로 쓰입니다.
그래서 하위 구현 클래스들의 생성자에 대한 제한을 줄수 있는 장점을 이용했습니다.
SViewComposite는 위의 레이아웃처럼 또 다른 SView을 인자로 얻어서 자기 자신의 content에 추가 할수 있습니다. 즉 공통적인 SView을 사용하기때문에 전체/부분이라는 구조를 쉽게 구성할수 있습니다.(Compositie패턴) Client가 보기에 Decorator패턴처럼 하나의 인터페이스을 가지고 다른 기능을 하는 것 처럼, 포함받은 SView의 내용을 자기 자신의 content에 추가하기 때문에 Decorator패턴의 실체화라고도 생각합니다.
SViewFactory,SViewSpringBeanFactory : AbstractFactory , ConcreteFactory
SViewFactory는 SView을 생성하는 책임이 있습니다.
- public interface SViewFactory {
public SView getSView(String viewName,ViewBinding viewBinding);
}
여기서 ViewBinding은 구현패턴에서 이야기하는 파라미터 객체입니다.
원래 View 인터페이스는 HttpServletRequest,HttpServletResponse와 Model을 인자로 받는 메소드가 있습니다. ViewBinding은 이 인수를 파라미터객체로 합성합니다.
SViewSpringBeanFactory은 SViewFactory의 인터페이스 상속을 하여 SpringBeanContext에서 viewName으로 SView을 얻어옵니다.
- public class SViewSpringBeanFactory extends WebApplicationObjectSupport implements SViewFactory {
private final String DEFAULT_VIEW_DELIMITER= ":";
public SView getSView(String viewName,ViewBinding viewBinding) {
String[] str = viewName.split(DEFAULT_VIEW_DELIMITER);
String sviewName = null;
String scompositeViewName = null;
if(str.length >= 1 && StringUtils.hasText(str[0])) sviewName = str[0];
if(str.length >= 2 && StringUtils.hasText(str[1])) scompositeViewName = str[1];
WebApplicationContext ctx = super.getWebApplicationContext();
SView sview = (SView) ctx.getBean(sviewName,new Object[]{viewBinding});
if(StringUtils.hasText(scompositeViewName)) {
SView sviewcomposite = (SView) ctx.getBean(scompositeViewName, - new Object[]{viewBinding,sview});
return sviewcomposite;
}
return sview;
}
}
ViewAdapter : Adapter
SView을 상속한 객체들은 이미 상속했으므로 View 인터페이스를 상속하지 않는 한 ViewResolver에 추가할수 없습니다.
물론 SView가 View을 extends해도 되지만. ViewAdapter을 사용함으로써, 기존의 클래스(SView)에 대한 변경을 하지 않고, 전혀 다른 타입의 클래스(View)처럼 사용할수 있습니다.
특히 이부분은 스프링 프레임워크의 View Interface의 render의 Map model이 제너릭 타입 파라미터가 되어 않아서, Scala에서 인터페이스 상속을 하지 못해, 전혀 다른 타입인 View인척하는 객체입니다.
- public class ViewAdapter implements View {
private final static String DEFAULT_CONTENT_TYPE = "text/html";
private String sviewName;
private SViewFactory sviewFactory;
private boolean isPrettyPrint = true;
private String contentType;
public ViewAdapter() {
}
public void setSViewName(String sviewName) {
this.sviewName = sviewName;
}
public void setSViewFactory(SViewFactory sviewFactory) {
this.sviewFactory = sviewFactory;
}
public void setPrettyPrint(boolean isPrettyPrint) {
this.isPrettyPrint = isPrettyPrint;
}
public String getContentType() {
return contentType;
}
public void render(Map model, HttpServletRequest request,HttpServletResponse response) - throws Exception {
ViewBinding viewBinding = new ViewBinding((Map<String,Object>) model,request,response);
SView sview = sviewFactory.getSView(sviewName,viewBinding);
this.contentType = sview.contenttype();
SViewWriteRenderer renderer = new SViewWriteRenderer();
sview.render(renderer);
response.setContentType(this.contentType);
PrintWriter out = response.getWriter();
renderer.print(out,isPrettyPrint);
out.close();
}
}
SViewRenderer , SViewWriteRenderer : Abstract Builder, Concrete Builder
공통적인 연산을 정의한 AbstractSView의 build()의 파라미터로 전달되는 SViewRenderer는 출력에 필요한 정보에 대한 메소드만을 가지고 있습니다. 즉 Director인 SView는 Renderer가 어떻게 표현할지에 대하여 알지 못합니다. SViewRenderer의 구체 클래스인 SViewWriteRenderer는 HttpServletResponse에서 전달 받는 PrintWriter을 이용해서 표현합니다. 만일 ServletOutputStream이나 기타 다른 방식으로의 표현을 바꾸더라도 변화는 ViewAdpater에서 다른 방식의 표현을 하는 Renderer으로 교체로 국한됩니다.
- trait SViewRenderer {
def setDocType(docType : String)
def setContentType(contentType : String)
def setContent(content : Elem)
}
public class SViewWriteRenderer implements SViewRenderer {
private String docType = "";
private String contentType = "";
private Elem content = null;
public void setDocType(String docType) {
this.docType = docType;
}
public String getContentType() {
return this.contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public void setContent(Elem content) {
this.content = content;
}
public void print(PrintWriter writer,boolean isprettyprint) {
String result = null;
if(isprettyprint == true) {
PrettyPrinter pp = new PrettyPrinter(120,4);
result = pp.format(content);
}
else {
result = content.toString();
}
writer.print(docType);
writer.print(result);
}
}
맺는 말
작성하고 나서 혹시 이와 비슷한 생각을 한 사람이 있는지 알아보니까 Object-Oriented Pattern Matching 논문을 작성한 burak emir 의 scalaservlet 이 있었습니다.
http://burak.emir.googlepages.com http://burak.emir.googlepages.com/scalaservlet.html
그리고 scala의 웹 프레임워크인 lift의 SHtml에서도 다음과 같이 사용합니다. http://github.com/dpp/liftweb/tree/master/lift/src/main/scala/net/liftweb/http/SHtml.scala
XML/HTML의 각 노드마다 객체를 생성하기 때문에 일반적인 JSP보다 비용 발생이 많은 듯 합니다. 실제 필드에서 적용하려면, 캐쉬전략이라든가 Scala의 XML Processing말고 다른 DSL이나 Template 엔진을 구성할 필요도 있다고 생각합니다. SViewSpringBeanFactory에서 sviewName에 해당하는 SView 객체를 얻지 못했을 경우에 좀더 명확한 Exception(SViewNotFoundException 등등)을 발생한다던가 하는 부분과 성능에 대한 부분은 다시 한번 고려해볼만 하다고 생각합니다.
처음 생각은 자바에서의 동적 스크립트 언어의 효과적인 사용에 적합한 레이어가 View가 아닐까해서 View에 대한 가볍고 쉬운 언어를 찾다가 여기까지 왔습니다. :-) 웹 프로그래밍에서 특히 자바 웹 프로그래밍에서 Html과 데이터를 바인딩 하는 방법은 많지만. 효과적으로 관리 가능한 (특히 테스트 가능한) 뷰 기술은 많지 않아 보입니다.
지금까지 읽어 주셔서 감사합니다.
참고자료
http://static.springframework.org/spring/docs/2.5.x/api/org/springframework/web/servlet/View.html
http://www.scala-lang.org/intro/xml.html
http://burak.emir.googlepages.com/scalaxbook.docbk.html
}

September 9th, 2008 at 06:20 AM