본문 바로가기

Spring

Spinrg MVC step by step

Spring 입문자의 필수 길잡이인


Spring-MVC-step-by-step이 스프링 2.5 버젼에 맞춰서 업그레이드 되었네요.


저도 Spring 걸음마 단계라서 예전에 공부했던 내용을 되새겨 볼 겸


다시 한번 해봤습니다. 이클립스3.3, ant1.7, spring2.5, jdk1.5, tomcat 6.0 을 사용했네요


그리고 junit을 사용하여 테스트 클래스들을 만들고 있습니다.


저 같은 경우 일단 흐름을 파악해보고 싶어서


hello.jsp 페이지로 부터 request가 흘러가는 것을 trace 해보도록 하겠습니다.


그냥 자기 공부용이고 정리해놓은 것입니다.


나중에 저 페이지도 한번 번역해서 올려볼까 합니다. 어렵지 않은 영어니까요 :)


http://www.springframework.org/docs/Spring-MVC-step-by-step/index.html








일단 web.xml에


<<<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" >

  <servlet>
    <servlet-name>springapp</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>springapp</servlet-name>
    <url-pattern>*.htm</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>
      index.jsp
    </welcome-file>
  </welcome-file-list>

  <jsp-config>
    <taglib>
      <taglib-uri>/spring</taglib-uri>
      <taglib-location>/WEB-INF/tld/spring-form.tld</taglib-location>
    </taglib>
  </jsp-config>

</web-app>


위 정의에 의해서 springapp 서블릿은 스프링에서 제공하는 서블릿 디스패쳐에 의해

springapp-servlet.xml에 있는 정의를 따라가게 됩니다.


그리고 호출은 .htm으로 하게 되겠네요.


자 그럼 호출을 하게 되면 그 주소는


http://localhost:8080/springapp/hello.htm

 

이렇게 작성합니다.


springapp 서블릿이기 때문에 springapp-servlet.xml로 넘어갑니다.


<<<spirngapp-servlet.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <!-- the application context definition for the springapp DispatcherServlet -->

    <bean id="productManager" class="springapp.service.SimpleProductManager">
        <property name="products">
            <list>
                <ref bean="product1"/>
                <ref bean="product2"/>
                <ref bean="product3"/>
            </list>
        </property>
    </bean>

    <bean id="product1" class="springapp.domain.Product">
        <property name="description" value="Lamp"/>
        <property name="price" value="5.75"/>
    </bean>

    <bean id="product2" class="springapp.domain.Product">
        <property name="description" value="Table"/>
        <property name="price" value="75.25"/>
    </bean>

    <bean id="product3" class="springapp.domain.Product">
        <property name="description" value="Chair"/>
        <property name="price" value="22.79"/>
    </bean>

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="messages"/>
    </bean>

    <bean name="/hello.htm" class="springapp.web.InventoryController">
        <property name="productManager" ref="productManager"/>
    </bean>

    <bean name="/priceincrease.htm" class="springapp.web.PriceIncreaseFormController">
        <property name="sessionForm" value="true"/>
        <property name="commandName" value="priceIncrease"/>
        <property name="commandClass" value="springapp.service.PriceIncrease"/>
        <property name="validator">
            <bean class="springapp.service.PriceIncreaseValidator"/>
        </property>
        <property name="formView" value="priceincrease"/>
        <property name="successView" value="hello.htm"/>
        <property name="productManager" ref="productManager"/>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>



<bean name="/hello.htm" class="springapp.web.InventoryController">
  <property name="productManager" ref="productManager"/>
</bean>

 

이부분을 보면 hello.htm이 호출되면 springapp.web.InventoryController 이 POJO 빈을

사용하는군요. 스프링에서는 일단 컨트롤러 클래스가 호출이 됩니다.

그리고 저 InventoryController에서는 productManager란 이름의 인스턴스도 사용하고 있겠구나..

라는 것을 알 수 있습니다. 실제로 보면.


<<<InventoryController.java>>>

import java.io.IOException;
import java.util.Map;
import java.util.HashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.service.ProductManager;

public class InventoryController implements Controller {

    protected final Log logger = LogFactory.getLog(getClass());

    private ProductManager productManager;

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String now = (new java.util.Date()).toString();
        logger.info("returning hello view with " + now);

        Map<String, Object> myModel = new HashMap<String, Object>();
        myModel.put("now", now);
        myModel.put("products", this.productManager.getProducts());

        return new ModelAndView("hello", "model", myModel);
    }


    public void setProductManager(ProductManager productManager) {
        this.productManager = productManager;
    }

}


위 소스를 보면 private ProductManager productManager; 이렇게 선언은 했지만

실제로 인스턴스로 생성하는 부분은 없습니다. 그런데.. ModelAndView 메서드에서는 마치

생성이 된 것 처럼 사용을 하고 있습니다.


myModel.put("products", this.productManager.getProducts());


이런식으로 말이죠.. 그리고 파랑색 메서드를 보면

productManager 이놈에게 뭔가 셋팅을 해주는 setter 메서드가 존재합니다.


즉 위 xml에 정의된

<bean id="productManager" class="springapp.service.SimpleProductManager">

이 선언에 의해 productManager는 springapp.service.SimpleProductManager의 인스턴스를

전달받게 됩니다.


이것이 스프링에서 제공하는 IoC입니다. 그리고 기본은 싱글턴 인스턴스입니다.


또한 xml에서

<bean id="product1" class="springapp.domain.Product">
        <property name="description" value="Lamp"/>
        <property name="price" value="5.75"/>
    </bean>

이런식으로 각 product 인스턴스를 생성해서 할당해주고 있습니다.


즉 hello.htm을 실행하면


1. 위 xml에 의해 productManager에 있는 product에 값들이 할당되고

2. InventoryController에 있는 ModelAndView 메서드가 실행되고

3. myModel이라는 Map 인스턴스에 값들을 셋팅 후 리턴시킵니다.


단순히 hello라는 값과 model이라는 키로 Map의 인스턴스를 넘기죠.


spirngapp-servlet.xml의

붉은색 부분에 있는 resolver 선언에 의해 hello라는 값이 리턴되면 prefix와 suffix가 자동으로 붙어서

/WEB-INF/jsp/hello.jsp라는 값이 넘어오게 됩니다.


hello.jsp에서는


<<<hello.jsp>>>

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<html>
  <head><title><fmt:message key="title"/></title></head>
  <body>
    <h1><fmt:message key="heading"/></h1>
    <p><fmt:message key="greeting"/> <c:out value="${model.now}"/></p>
    <h3>Products</h3>
    <c:forEach items="${model.products}" var="prod">
      <c:out value="${prod.description}"/> <i>$<c:out value="${prod.price}"/></i><br><br>
    </c:forEach>
    <br>
    <a href="<c:url value="priceincrease.htm"/>">Increase Prices</a>
    <br>
  </body>
</html>


이렇게 Map에 넣어둔 model이라는 키로 값을 전달받아 tld를 사용해 화면에 보여주고

아래 링크가 하나 있습니다. priceincrease.htm 이라는 링크네요...


눌러서 따라가 봅시다.


<<<spirngapp-servlet.xml>>>에 파란색부분에 보면

    <bean name="/priceincrease.htm" class="springapp.web.PriceIncreaseFormController">
        <property name="sessionForm" value="true"/>
        <property name="commandName" value="priceIncrease"/>
        <property name="commandClass" value="springapp.service.PriceIncrease"/>
        <property name="validator">
            <bean class="springapp.service.PriceIncreaseValidator"/>
        </property>
        <property name="formView" value="priceincrease"/>
        <property name="successView" value="hello.htm"/>
        <property name="productManager" ref="productManager"/>
    </bean>

이렇게 선언이 되어있습니다. 복잡합니다.


이 링크는 각 상품의 할인율을 입력하는 화면으로 할인율을 입력하면 할인된 가격을 보여주는

간단한 웹 어플리케이션입니다.


즉 입력화면으로 사용하기 위한 form역할을 하는 jsp가 있어야 하고

정상적으로 할인율이 들어왔는지 체크하기 위한 validation이 있어야 하며

성공했을때 이동하여야 할 (바뀐 값을 보여줄) 페이지가 있어야 하죠. 이러한 것들을 정의해놓았습니다. 얼핏보면 struts와 살짝 비슷한 느낌도 듭니다.


우선 위 링크를 클릭하면 매핑되어있는 컨트롤러에게 request가 넘어갑니다.

springapp.web.PriceIncreaseFormController

이놈이군요. 소스를 봅시다.

<<<PriceIncreaseFormController.java>>>

package springapp.web;

import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.service.ProductManager;
import springapp.service.PriceIncrease;

public class PriceIncreaseFormController extends SimpleFormController {

    /** Logger for this class and subclasses */
    protected final Log logger = LogFactory.getLog(getClass());

    private ProductManager productManager;

    public ModelAndView onSubmit(Object command)
            throws ServletException {

        int increase = ((PriceIncrease) command).getPercentage();
        logger.info("Increasing prices by " + increase + "%.");

        productManager.increasePrice(increase);

        logger.info("returning from PriceIncreaseForm view to " + getSuccessView());

        return new ModelAndView(new RedirectView(getSuccessView()));
    }

    //get 방식의 구현
    //hello.jsp에서 링크타고 넘어왔을때 호출된다.
    protected Object formBackingObject(HttpServletRequest request) throws ServletException {
        PriceIncrease priceIncrease = new PriceIncrease();
        priceIncrease.setPercentage(40);
        return priceIncrease;
    }

    public void setProductManager(ProductManager productManager) {
        this.productManager = productManager;
    }

    public ProductManager getProductManager() {
        return productManager;
    }

}

나중에 다시 나오겠지만 onSubmit 이 메서드는 폼 화면에서 submit을 눌렀을때 실행되는 (post방식)

메서드이며, get방식으로 request가 들어오게 되면 formBackingObjectrk 메서드가 호출됩니다.


단순히 링크를 클릭했기 때문에 이경우에는 formBackingObject 메서드가 호출이 되고, 특별히 하는

일 없이 PriceIncrease 이 클래스에 기본 값 (여기서는 기본 할인율이 되겠지요..)을 셋팅해주고 리턴해줍니다.


<<<PriceIncrease .java>>>

package springapp.service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class PriceIncrease {

    /** Logger for this class and subclasses */
    protected final Log logger = LogFactory.getLog(getClass());

    private int percentage;


    public void setPercentage(int i) {
        percentage = i;
        logger.info("Percentage set to " + i);
    }

    public int getPercentage() {
        return percentage;
    }

}


자 그럼 할인율이 기본으로 40으로 셋팅이 되고, 딱히 하는 일이 없기 때문에 폼으로 설정된 jsp로

넘어가게 됩니다.

<property name="formView" value="priceincrease"/> 이 설정에 의해

priceincrease.jsp가 되겠네요. 예상하셨겠지만 value의 값을 바꾸면 jsp의 이름도 똑같이

바뀌어야 합니다. 아니면 404 에러가 나겠지여~


<<<priceincrease.jsp>>>

<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>
<head>
  <title><fmt:message key="title"/></title>
  <style>
    .error { color: red; }
  </style>
</head>
<body>
<h1><fmt:message key="priceincrease.heading"/></h1>
<form:form method="post" commandName="priceIncrease">
  <table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
    <tr>
      <td align="right" width="20%">Increase (%):</td>
        <td width="20%">
          <form:input path="percentage"/>
        </td>
        <td width="60%">
          <form:errors path="percentage" cssClass="error"/>
        </td>
    </tr>
  </table>
  <br>
  <input type="submit" align="center" value="Execute">
</form:form>
<a href="<c:url value="hello.htm"/>">Home</a>
</body>
</html>


테스트를 더 해봐야겠지만 priceIncrease 클래스에 설정된 할인 퍼센트율이 일단 나오게 됩니다.

jsp의 <form:input path="percentage"/> percentage와 같은 이름으로 commandClass에

변수가 선언되어 있어야 합니다. getter/setter 메서드도 존재해야 하구요,


특정 값을 입력 후 submit을 하면

<property name="validator">
      <bean class="springapp.service.PriceIncreaseValidator"/>
</property>

이 설정에 의해 일단 validator에게 request가 넘어갑니다.

PriceIncreaseValidator 클래스에서는 public void validate(Object obj, Errors errors) {
        PriceIncrease pi = (PriceIncrease) obj;
        if (pi == null) {
            errors.rejectValue("percentage", "error.not-specified", null, "Value required.");
        }
        else {
            logger.info("Validating with " + pi + ": " + pi.getPercentage());

            //object[]배열은 message.properties에서 error.too-high=Don''t be greedy - you can''t raise prices by more than {0}
            if (pi.getPercentage() > maxPercentage) {
                errors.rejectValue("percentage", "error.too-high",
                    new Object[] {new Integer(maxPercentage)}, "Value too high.");
            }
            if (pi.getPercentage() <= minPercentage) {
                errors.rejectValue("percentage", "error.too-low",
                    new Object[] {new Integer(minPercentage)}, "Value too low.");
            }
        }
    }

이런식으로 넘어온 값을 체크하고

새로 입력한 값은

        <property name="commandName" value="priceIncrease"/>
        <property name="commandClass" value="springapp.service.PriceIncrease"/>

이 설정에 의해 커멘드 클래스에 전달되고 percentage라고 되어 있는 필드에 매핑이 됩니다.

(1:1 매핑)


만약 priceincrease.jsp에서

<form:input path="percentage2"/>


이렇게 필드가 추가되면 commandClass인 PriceIncrease에도

percentage2라는 필드와 이를 위한 getter, setter 메서드가 존재해야 합니다.


validator를 통과하면

springapp.web.PriceIncreaseFormController 이 클래스의 아까 설명한

public ModelAndView onSubmit(Object command) {...} 이 메서드가 실행이 되고

((PriceIncrease) command).getPercentage(); 이런식으로 command 클래스를 (새로 입력한 값이

셋팅되어 있는) 가져올 수 있게 됩니다.


그리고 해당 percentage로 새로운 값을 계산해 주고

return new ModelAndView(new RedirectView(getSuccessView()));


이렇게 return을 하죠.


getSuccessView()는

<property name="successView" value="hello.htm"/>

이 설정에 의하야... hello.jsp가 되겠네요 ^^