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가 되겠네요 ^^