레이블이 spring인 게시물을 표시합니다. 모든 게시물 표시
레이블이 spring인 게시물을 표시합니다. 모든 게시물 표시

2014년 2월 4일 화요일

Spring What is the difference between Class.getResource() and ClassLoader.getResource()?

If there's an SSCCE here, I don't understand why it doesn't 1) Show the directory organization in the .jar, and... 2) Take package into consideration Q: What (if anything) hasn't already been answered by What is the difference between Class.getResource() and ClassLoader.getResource()? (and the links it cites)? ========================================================================= I'm still not sure what isn't clear, but this example might help:
/*
  SAMPLE OUTPUT:
  ClassLoader.getResource(/subdir/readme.txt): NULL
  Class.getResource(/subdir/readme.txt): SUCCESS

  ClassLoader.getResource(subdir/readme.txt): SUCCESS
  Class.getResource(subdir/readme.txt): NULL
 */
package com.so.resourcetest;

import java.net.URL;

public class ResourceTest {

    public static void main(String[] args) {
        ResourceTest app = new ResourceTest ();
    }

    public ResourceTest () {
        doClassLoaderGetResource ("/subdir/readme.txt");
        doClassGetResource ("/subdir/readme.txt");
        doClassLoaderGetResource ("subdir/readme.txt");
        doClassGetResource ("subdir/readme.txt");
    }

    private void doClassLoaderGetResource (String sPath) {
        URL url  = getClass().getClassLoader().getResource(sPath);
        if (url == null)
            System.out.println("ClassLoader.getResource(" + sPath + "): NULL");
        else
            System.out.println("ClassLoader.getResource(" + sPath + "): SUCCESS");
    }

    private void doClassGetResource (String sPath) {
        URL url  = getClass().getResource(sPath);
        if (url == null)
            System.out.println("Class.getResource(" + sPath + "): NULL");
        else
            System.out.println("Class.getResource(" + sPath + "): SUCCESS");
    }
}


Spring Object/XML Mapping Example

The Spring’s Object/XML Mapping, is converting Object to XML or vice verse. This process is also known as
  1. XML Marshalling – Convert Object to XML.
  2. XML UnMarshalling – Convert XML to Object.
In this tutorial, we show you how to use Spring’s oxm to do the conversion, Object <--- Spring oxm ---> XML.
Note
No nonsense, for why and what benefits of using Spring’s oxm, read this official Spring Object/XML mapping article.

1. Project Dependency

Dependencies in this example.
Note
Spring’s oxm itself doesn’t handle the XML marshalling or UnMarshalling, it depends developer to inject their prefer XML binding framework. In this case, you will use Castor binding framework.

<properties>
  <spring.version>3.0.5.RELEASE</spring.version>
 </properties>
 
 <dependencies>
 
  <!-- Spring 3 dependencies -->
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-core</artifactId>
   <version>${spring.version}</version>
  </dependency>
 
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
   <version>${spring.version}</version>
  </dependency>
 
  <!-- spring oxm -->
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-oxm</artifactId>
   <version>${spring.version}</version>
  </dependency>
 
  <!-- Uses Castor for XML -->
  <dependency>
   <groupId>org.codehaus.castor</groupId>
   <artifactId>castor</artifactId>
   <version>1.2</version>
  </dependency>
 
  <!-- Castor need this -->
  <dependency>
   <groupId>xerces</groupId>
   <artifactId>xercesImpl</artifactId>
   <version>2.8.1</version>
  </dependency>
 
 </dependencies>

2. Simple Object

A simple object, later convert it into XML file.
package com.mkyong.core.model;
 
public class Customer {
 
 String name;
 int age;
 boolean flag;
 String address;
 
 //standard getter, setter and toString() methods.
}

3. Marshaller and Unmarshaller

This class will handle the conversion via Spring’s oxm interfaces : Marshaller and Unmarshaller.
package com.mkyong.core;
 
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.Unmarshaller;
 
public class XMLConverter {
 
 private Marshaller marshaller;
 private Unmarshaller unmarshaller;
 
 public Marshaller getMarshaller() {
  return marshaller;
 }
 
 public void setMarshaller(Marshaller marshaller) {
  this.marshaller = marshaller;
 }
 
 public Unmarshaller getUnmarshaller() {
  return unmarshaller;
 }
 
 public void setUnmarshaller(Unmarshaller unmarshaller) {
  this.unmarshaller = unmarshaller;
 }
 
 public void convertFromObjectToXML(Object object, String filepath)
  throws IOException {
 
  FileOutputStream os = null;
  try {
   os = new FileOutputStream(filepath);
   getMarshaller().marshal(object, new StreamResult(os));
  } finally {
   if (os != null) {
    os.close();
   }
  }
 }
 
 public Object convertFromXMLToObject(String xmlfile) throws IOException {
 
  FileInputStream is = null;
  try {
   is = new FileInputStream(xmlfile);
   return getUnmarshaller().unmarshal(new StreamSource(is));
  } finally {
   if (is != null) {
    is.close();
   }
  }
 }
 
}

4. Spring Configuration

In Spring’s bean configuration file, inject CastorMarshaller as the XML binding framework.
<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-3.0.xsd">
 
 <bean id="XMLConverter" class="com.mkyong.core.XMLConverter">
  <property name="marshaller" ref="castorMarshaller" />
  <property name="unmarshaller" ref="castorMarshaller" />
 </bean>
 <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller" />
 
</beans>

5. Test

Run it.
package com.mkyong.core;
 
import java.io.IOException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.mkyong.core.model.Customer;
 
public class App {
 private static final String XML_FILE_NAME = "customer.xml";
 
 public static void main(String[] args) throws IOException {
  ApplicationContext appContext = new ClassPathXmlApplicationContext("App.xml");
  XMLConverter converter = (XMLConverter) appContext.getBean("XMLConverter");
 
  Customer customer = new Customer();
  customer.setName("mkyong");
  customer.setAge(30);
  customer.setFlag(true);
  customer.setAddress("This is address");
 
  System.out.println("Convert Object to XML!");
  //from object to XML file
  converter.convertFromObjectToXML(customer, XML_FILE_NAME);
  System.out.println("Done \n");
 
  System.out.println("Convert XML back to Object!");
  //from XML to object
  Customer customer2 = (Customer)converter.convertFromXMLToObject(XML_FILE_NAME);
  System.out.println(customer2);
  System.out.println("Done");
 
 }
}
Output
Convert Object to XML!
Done 
 
Convert XML back to Object!
Customer [name=mkyong, age=30, flag=true, address=This is address]
Done
The following XML file “customer.xml” is generated in your project root folder.
File : customer.xml
<?xml version="1.0" encoding="UTF-8"?>
<customer flag="true" age="30">
 <address>This is address</address>
 <name>mkyong</name>
</customer>

Castor XML Mapping

Wait, why flag and age are converted as attribute? Is that a way to control which field should use as attribute or element? Of course, you can use Castor XML mapping to define the relationship between Object and XML.
Create following mapping file, and put it into your project classpath.
File : mapping.xml
<mapping>
 <class name="com.mkyong.core.model.Customer">
 
  <map-to xml="customer" />
 
  <field name="age" type="integer">
   <bind-xml name="age" node="attribute" />
  </field>
 
  <field name="flag" type="boolean">
   <bind-xml name="flag" node="element" />
  </field>
 
  <field name="name" type="string">
   <bind-xml name="name" node="element" />
  </field>
 
  <field name="address" type="string">
   <bind-xml name="address" node="element" />
  </field>
 </class>
</mapping>
In Spring bean configuration file, inject above mapping.xml into CastorMarshaller via “mappingLocation“.
<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-3.0.xsd">
 
 <bean id="XMLConverter" class="com.mkyong.core.XMLConverter">
  <property name="marshaller" ref="castorMarshaller" />
  <property name="unmarshaller" ref="castorMarshaller" />
 </bean>
 <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller" >
  <property name="mappingLocation" value="classpath:mapping.xml" />
 </bean>
 
</beans>
Test it again, the XML file “customer.xml” will be updated.
File : customer.xml
<?xml version="1.0" encoding="UTF-8"?>
<customer age="30">
 <flag>true</flag>
 <name>mkyong</name>
 <address>This is address</address>
</customer>

Spring SQL 로그 네멋대로 보기

SQL Log 직관적으로 볼수 있는 방법이 존재하지 않을까 해서 구글링 해보니 역시나..ㅋㅋ og4jdbc-remix 입니다. 설치전에 집고 넘어가야 하는 부분이 있는데, maven repository와 jdk 1.6에서만 지원 가능합니다. 이점 유의하시고 진행해야 될거 같네요. 설치법은 우선 간단합니다.

1. maven인 경우 설치 방법


  org.lazyluke
  log4jdbc-remix
  0.2.4


  org.slf4j
  slf4j-log4j12
  1.6.1

2. classpath로 설정 하는 경우 설치 방법

log4j, slf4j, log4jdbc, log4jdbc-remix를 다운 받아서 클래스 패스 설정을 하시면 됩니다. log4jdbc, slf4j, log4jdbc : http://code.google.com/p/log4jdbc/ log4jdbc-remix : http://code.google.com/p/log4jdbc-remix/ 이렇게 lib파일을 등록 했으면 이제 log4j.xml 설정을 합니다.



 
  
   
  
  
   
   
  
 

 
  
  
 
 
  
  
 


 
  
  
 

 
  
  
 

 
  
  
 

좀 다른 부분이 logger name이 기존하고는 좀 다른것을 확인하실 수 있을겁니다. 다음으로는 applicationContext.xml에서 dataSource를 변경합니다.

    
    
    
    
    ...

[/code]

기존에 이런식으로 설정이 되어있을 것이며 이부분을 밑에 처럼 변경합니다.

[code language="xml"]


 
 
 
 


 
 
  
   
   
   
  
 


원문 보기 : http://code.google.com/p/log4jdbc-remix/

Spring @Controller 리턴타입의 종류

자동추가 모델 오브젝트

@ModelAttribute 모델 오브젝트(커맨드 오브젝트)

메소드 파라미터 중에서 @ModelAttribute 를 붙인 모델 오브젝트나 @ModelAttribute 는 생략했지만 단순 타입이 아니라서 커맨드 오브젝트로 처리되는 오브젝트라면 자동으로 컨트롤러가 리턴하는 모델에 추가된다. 기본적으로 모델 오브젝트의 이름은 파라미터 타입 이름을 따른다. 이름을 직접 지정하고 싶다면 @ModelAttribute("모델이름") 으로 지정해 주면 된다. 따라서 코드에서 @ModelAttribute 모델 오브젝트를 모델맵에 직접 추가해줄 필요는 없다. 다음의 세 가지 메소드 선언은 모두 'user' 라는 이름으로 User 파라미터 오브젝트가 모델에 추가되게 해준다. [code language="java"] public void add(@ModelAttribute("user") User user) public void add(@ModelAttribute User user) public void add(User user) [/code]

Map/Model/ModelMap 파라미터

컨트롤러 메소드에 Map, Model, ModelMap 타입의 파라미터를 사용하면 미리 생성된 모델 맵 오브젝트를 전달받아서 오브젝트를 추가할 수 있다. 이런 파라미터에 추가한 오브젝트는 DispatcherServlet 을 거쳐 뷰에 전달되는 모델에 자동으로 추가된다. 컨트롤러에서 ModelAndView 를 별도로 만들어 리턴하는 경우에도 맵에 추가한 오브젝트는 빠짐없이 모델에 추가된다.

@ModelAttribute 메소드가 생성하는 오브젝트

@ModelAttribute 는 컨트롤러 클래스의 메소드 레벨에도 부여할 수 있다. 뷰에서 참고 정보로 사용되는 모델 오브젝트를 생성하는 메소드를 지정하기 위해 사용된다. 이를 이용하면 모델 오브젝트 생성을 전담하는 메소드를 만들 수 있다. @ModelAttribute 가 붙은 메소드는 컨트롤러 클래스 안에 정의하지만 컨트롤러 기능을 담당하지 않는다. 따라서 @RequestMapping 을 함께 붙이지 않아야 한다. @ModelAttribute 메소드가 생성하는 오브젝트는 클래스 내의 다른 컨트롤러 메소드의 모델에 자동으로 추가된다. 다음과 같이 메소드를 정의했다고 해보자. [code language="java"] @ModelAttribute("codes") public List codes() { return codeService.getAllCodes(); } [/code] codes() 메소드는 서비스 계층 오브젝트를 이용해 코드정보의 리스트를 받아서 리턴한다. 리턴되는 오브젝트는 @ModelAttribute에 지정한 "codes" 라는 이름으로 다른 컨트롤러가 실행될 때 모델에 자동 추가된다. 이렇게 @ModelAttribute 메소드가 필요한 이유는 무엇일까? 보통 폼이나 검색조건 페이지 등에서는 참조정보가 필요한 경우가 있다. 폼이나 검색조건 창에 select 태그를 써서 선택 가능한 목록을 보여주는 경우가 가장 대표적이다. 이때는 폼 데이터외에 참조정보의 목록을 모델에 넣어서 뷰로 보내줘야 한다. 물론 개별 컨트롤러에서 직접 모델에 추가해 줄 수도 있지만, 같은 클래스 내의 모든 컨트롤러 메소드에서 공통적으로 활용하는 정보라면 @ModelAttribute 메소드를 사용하는 것이 편리하다. 참조 정보가 많다면 @ModelAttribute 메소드를 하나 이상의 메소드에 적용할 수 있다.

BindingResult

@ModelAttribute 파라미터와 함께 사용하는 BindingResult 타입의 오브젝트도 모델에 자동으로 추가된다. 모델 맵에 추가될 때의 키는 "org.springframework.validation.BindingResult.모델이름" 이다. 모델이름이 user 라면 이에 대한 바인딩 결과를 담은 오브젝트는 org.springframework.validation.BindingResult.user 라는 이름의 키로 모델에 추가될 것이다. BindingResult 오브젝트가 모델에 자동으로 추가되는 이유는 스프링의 JSP 나 프리마커, 벨로시티 등의 뷰에서 사용되는 커스텀 태그나 매크로에서 사용되기 때문이다. 주로 잘못 입력된 폼 필드의 잘못 입력된 값을 가져오거나 바인딩 오류 메시지를 생성성할 때 사용된다. 일반적으로는 BindingResult 모델 오브젝트를 뷰에서 직접 사용할 필요는 없다. 핸들러 인터셉터를 이용해 바인딩 결과를 로깅하거나 분석할 때 사용할 수 있다.

ModelAndView

ModelAndView 는 컨트롤러가 리턴해야 하는 정보를 담고 있는 가장 대표적인 타입이다. 하지만 @Controller 에서는 @ModelAndView 를 이용하는 것보다 편리한 방법이 많아서 자주 사용되지는 않는다. 혹시 기존의 Controller 타입으로 작성한 컨트롤러 코드를 @MVC 방식으로 포팅할 경우라면 일단 ModelAndView 를 리턴하는 코드를 그대로 가져와서 적용한 뒤에 나중에 다듬는 방법을 쓸 수 있다. 예를 들어 스프링 2.5나 그 이전에서 자주 사용되던 Controller 타입의 컨트롤러 클래스가 다음과 같이 만들어졌다고 해보자. name 파라미터를 받아서 모델에 넣어주고 뷰 이름과 함께 ModelAndView 타입으로 리턴해주는 간단한 코드다. 모델은 맵을 먼저 만들어서 ModeAndView 생성자에 넣어도 되지만 아래 예처럼 ModelAndView 오브젝트를 만든 뒤에 addObject() 메소드로 추가해줘도 된다. [code language="java"] public class HelloController implements Controller { @Override public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { String name = request.getParameter("hello"); return new ModelAndView("hello.jsp").addObject("name", name); } } [/code] 이 컨트롤러 코드를 @MVC 스타일의 컨트롤러로 변경하는 건 아주 간단하다. 다음처럼 Controller 인터페이스 구현 선언을 없애고, @Controller 와 @RequestMapping 을 붙여주면 된다. [code language="java"] @Controller public class HelloController { @RequestMapping("/hello") public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { String name = request.getParameter("hello"); return new ModelAndView("hello.jsp").addObject("name", name); } } [/code] 물론 @Controller 답게 만들려면 파라미터로 HttpServletRequest 를 사용하는 대신 @RequestParam 으로 파라미터 값을 가져오는 편이 훨씬 깔끔하다. 이왕이면 메소드 이름도 의미 있는 것으로 바꾸자, throws Exception 도 선언이 꼭 필요한 경우가 아니라면 없애자, 마지막으로 Model 파라미터를 이용하도록 해보자. 다음과 같이 훨씬 간결한 코드로 작성할 수 있다. [code language="java"] @RequestMapping("/hello") public ModelAndView hello(@RequestParam String name, Model model) { model.addAttribute("name", name); return new ModelAndView("hello"); } [/code]

String

메소드의 리턴타입이 스트링이면 리턴 값은 뷰 이름으로 사용된다. 모델정보는 모델 맵 파라미터로 전달받아 추가해주는 방법을 사용해야 한다. Model 이나 ModelMap 파라미터를 전달받아서 여기에 모델 정보를 넣어주는 것이다.위의 컨트롤러 코드는 ModelAndView 오브젝트를 리턴하지만 필요한 것은 뷰 이름뿐이다. 따라서 다음과 같이 바꾸는 편이 더 깔끔하다. [code language="java"] @RequestMapping("/hello") public String hello(@RequestParam String name, Model model) { model.addAttribute("name", name); return "hello"; } [/code] 모델은 파라미터로 전달받은 Map, Model, ModelMap 에 넣어주고 리턴값은 뷰 이름을 스트링 타입으로 선언하는 방식은 흔히 사용되는 @Controller 메소드 작성 방법이다. 컨트롤러 코드에서 모델을 추가해주고 뷰 이름을 지정해 주는 방법 중에서 가장 깔끔하기 때문이다.

void

메소드의 리턴 타입을 아예 void 로 할 수도 있다. 이때는 RequestToViewNameResolver 전략을 통해 자동생성되는 뷰 이름이 사용된다. URL 과 뷰 이름을 일관되게 통일할 수만 있다면 void 형의 사용도 적극 고려해볼 만하다. 위의 메소드는 다시 다음과 같이 더 단순하게 바꿀 수 있다. [code language="java"] @RequestMapping("/hello") public void hello(@RequestParam String name, Model model) { model.addAttribute("name", name); } [/code] void 형 리턴타입이기 때문에 뷰 이름은 RequestToViewNameResolver 를 통해 자동생성된다. 디폴트로 등록된 RequestToViewNameResolver 는 URL 을 따라서 뷰 이름을 hello 로 만들어줄 것이다. 여기에 뷰 리졸버가 prefix, suffix 를 붙여주도록 하면 /WEB-INF/view/hello.jsp 같은 식으로 완성할 수 있다.

모델 오브젝트

뷰 이름은 RequestToVewNameResolver 로 자동생성하는 것을 사용하고 코드를 이용해 모델에 추가할 오브젝트가 하나뿐이라면, Model 파라미터를 받아서 저장하는 대신 모델 오브젝트를 바로 리턴해도 된다.스프링은 리턴타입이 미리 지정된 타입이나 void 가 아닌 단순 오브젝트라면 이를 모델 오브젝트로 인식해서 모델에 자동으로 추가해준다. 이때 모델 이름은 리턴값의 타입 이름을 따른다. 다음 메소드를 살펴보자. view() 메소드는 파라미터로 전달된 id 값을 이용해 User 오브젝트를 가져와서 바로 리턴해준다.메소드 리턴 타입은 User 타입으로 선언해뒀다. 이때는 리턴한 User 타입 오브젝트가 user 라는 이름으로 모델에 추가될 것이다. 뷰 이름은 제공할 기회가 없었으니 디폴트 RequestToViewNameResolver 를 통해 "view" 로 결정될 것이다. [code language="java"] @RequestMapping("/view") public User view(@RequestParam int id) { return userService.getUser(id); } [/code] 클래스 이름과 다른 모델 이름을 사용하고 싶다면, 메소드 레벨의 @ModelAttribute 를 사용해 모델 이름을 직접 지정해줄 수 있다.

Map/Model/ModelMap

메소드의 코드에서 Map 이나 Model, ModelMap 타입의 오브젝트를 직접 만들어서 리턴해주면 이 오브젝트는 모델로 사용된다. 컨트롤러 코드에서 필요한 모델 맵은 파라미터로 받아서 사용하면 편리하다. 직접 Map/Model/ModelMap 타입의 오브젝트를 코드에서 생성하는 건 비효율적이다. 따라서 맵을 리턴 타입으로 사용하는 일은 많지 않을 것이다. 이 리턴 타입의 특징은 분명하게 기억해둬야 한다. 단일 모델 오브젝트를 직접 리턴하는 방식을 사용하다가 실수할 수 있기 때문이다. 예를 들어 서비스 계층의 메소드에서 맵 타입으로 결과를 돌려주는 경우가 있다. SpringJDBC 의 API 를 보면 Map 타입의 결과를 돌려주는 메소드가 제법된다. 그런데 단일 오브젝트를 리턴하면 모델로 자동등록된다고 해서 다음과 같이 코드를 작성하면 안된다. [code language="java"] @RequestMapping("/view") public Map view(@RequestParam int id) { Map userMap = userService.getUserMap(id); return userMap; } [/code] 즉, 이코드에 의해 map 이라는 이름을 가진 맵 오브젝트가 모델에 추가될 것이라고 기대하면 안된다. Map 타입의 리턴 값은 그 자체로 모델맵으로 인식해서 그 안의 엔트리 하나하나를 개별적인 모델로 다시 등록해 버리기 때문이다. 따라서 Map 타입의 단일 오브젝트 리턴은 근본적으로 피하는 게 좋다. 위와 같은 코드는 원래 의도대로 하려면 다음과 같이 Model 오브젝트를 파라미터로 직접 받아서 코드에 추가해 줘야 한다. [code language="java"] @RequestMapping("/view") public void view(@RequestParam int id, Model model) { model.addAttribute("userMap", userService.getUserMap(id)); } [/code]

View

스트링 리턴 타입은 뷰 이름으로 인식한다고 했다. 그런데 뷰 이름 대신 뷰 오브젝트를 사용하고 싶다면 다음과 같이 리턴 타입을 View 로 선언하고 뷰 오브젝트를 리턴하면 된다.
public class UserController {

    @Autowired
    MarshallingView userXmlView;

    @RequestMapping("/user/xml")
    public View view(@RequestParam int id) {
        ....
        return this.userXmlView;
    }

}

@ResponseBody

@ResponseBody 는 @RequestBody 와 비슷한 방식으로 동작한다. @ResponseBody 가 메소드 레벨에 부여되면, 메소드가 리턴하는 오브젝트는 뷰를 통해 결과를 만들어내는 모델로 사용되는 대신, 메시지 컨버터를 통해 바로 HTTP 응답의 메시지 본문으로 전환된다. 아래 hello() 메소드에 @ResponseBody 가 없다면 스트링 타입의 리턴 값은 뷰 이름으로 인식될 것이다. 하지만 @ResponseBody 가 붙었으므로 스트링 타입을 지원하는 메시지 컨버터가 이를 변환해서 HttpServletResponse 의 출력스트림에 넣어 버린다.
@RequestMapping("/hello")
@ResponseBody
public String hello() {
    return "Hello Spring";
}
@ResponseBody 가 적용된 컨트롤러는 리턴 값이 단일 모델이고 메시지컨버터가 뷰와 같은 식으로 동작한다고 이해할 수 있다. 근본적으로 @RequestBody, @ResponseBody 는 XML 이나 JSON 과 같은 메시지 기반의 커뮤니케이션을 위해 사용된다

Spring @Controller 파라미터의 종류

지금부터 소개할 타입의 오브젝트와 애노테이션은 @Controller 의 메소드 파라미터로 자유롭게 사용할 수 있다. 단, 몇 가지 파라미터 타입은 순서와 사용 방법상의 제약이 있으니 주의해야 한다.

HttpServletRequest/HttpServletResponse/ServletRequest/ServletResponse

대개는 좀 더 상세한 정보를 담은 파라미터 타입을 활용하면 되기 때문에 필요 없겠지만, 그래도 원한다면 HttpServletRequest, HttpServletResponse, ServletRequest, ServletResponse 오브젝트를 파라미터로 사용할 수 있다.

HttpSession

HttpSession 오브젝트는 HttpServletRequest 를 통해 가져올 수도 있지만, HTTP 세션만 필요한 경우라면 HttpSession 타입 파라미터를 선언해서 직접 받는 편이 낫다. HttpSession 은 서버에 따라서 멀티스레드 환경에서 안전성이 보장되지 않는다. 서버에 상관없이 HttpSession 을 안전하게 사용하려면 핸들어 어댑터의 synchronizeOnSession 프로퍼티를 true 로 설정해 줘야 한다.

WebRequest/NativeWebRequest

WebRequest 는 HttpServletRequest 의 요청 정보를 대부분 그대로 갖고 있는, 서블릿 API 에 종속적이지 않은 오브젝트 타입니다. WebRequest 는 원래 서블릿과 포틀릿 환경 양쪽에서 모두 적용 가능한 범용적인 핸들러 인터셉터를 만들 때 활용하기 위해 만들어 졌다. 따라서 스프링 서블릿/MVC 의 컨트롤러에서라면 꼭 필요한 건 아니다. NativeWebRequest 에는 WebRequest 내부에 감춰진 HttpServletRequest 와 같은 환경 종속적인 오브젝트를 가져올 수 있는 메소드가 추가되어 있다.

Locale

java.util.Locale 타입으로 DispatcherServlet 의 지역정보 리졸버(Locale Resolver) 가 결정한 Locale 오브젝트를 제공 받을 수 있다.

InputStream/Reader

HttpServletRequest 의 getInputStream() 을 통해서 받을 수 있는 콘텐트 스트림 또는 Reader 타입 오브젝트를 제공받을 수 있다.

OutputStream/Writer

HttpServletResponse 의 getOutputStream() 으로 가져올 수 있는 출력용 컨텐트 스트림 또는 Writer 타입 오브젝트를 받을 수 있다.

@PathVariable

@RequestMapping 의 URL 정의 부의 중괄호({}) 에 명시된 패스 변수를 받는다. @Controller 는 URL 에서 파라미터에 해당하는 부분에 {}을 넣는 URI 템플릿을 사용할 수 있다. 컨트롤러 메소드 파라미터에는 @PathVariable 애노테이션을 이용해 URI 템플릿 중에서 어떤 파라미터를 가져올지를 결정할 수 있다. 예를 들면 다음과 같다.
@RequestMapping("/user/view/{id}")
public String view(@PathVariable("id") int id) {
    ...
}
URL의 중괄호({}) 에는 패스 변수를 넣는다. 이 이름을 @PathVariable 애노테이션의 값으로 넣어서 메소드 파라미터에 부여해 주면 된다. /user/view/10 이라는 URL 이라면, id 파라미터에 10이라는 값이 들어올 것이다. 파라미터의 타입은 URL 의 내용이 적절히 변환될 수 있는 것을 사용해야 한다. int 타입을 썻을 경우에는 반드시 해당 패스 변수 자리에 숫자 값이 들어 있어야 한다. 타입이 일치하지 않는 값이 들어 올 경우 별 다른 예외처리를 해주지 않는 다면 HTTP 400 - Bad Request 응답코드가 전달될 것이다.

@RequestParam

HTTP 요청 파라미터를 메소드 파라미터에 넣어 주는 애노테이션이다. 가져올 오쳥 파라미터의 이름을 @RequestParam 애노테이션의 기본 값으로 지정해주면 된다. 요청 파라미터의 값은 메소드 파라미터의 타입에 따라 적절하게 변환된다. 몇가지 예를 살펴보자
public String method1(@RequestParam("id") int id) { ... }

public String method2(@RequestParam("id") int id,
        @RequestParam("name") String name,
        @RequestParam("file") MultipartFile file) { ... }

public String method3(@RequestParam Map params) { ... }

public String method4(
        @RequestParam(value="id", required=false, defaultValue="-1") int id) { ... }

public String method5(@RequestParam int id) { ... }
○ method1() 의 선언은 id 요청 파라미터를 int 타입으로 변환해서 메소드의 id 파라미터에 넣어준다. ○ @RequestParam 은 method2() 와 같이 하나 이상의 파라미터에 적용할 수 있다. 스프링의 내장 변환기가 다룰 수 있는 모든 타입을 지원한다. ○ method3() 과 같이 @RequestParam 에 파라미터 이름을 지정하지 않고 Map<String, String> 타입으로 선언하면 모든 요청 파라미터를 담은 맵으로 받을 수 있다. 파라미터 이름은 맵의 키에, 값은 맵의 값에 담겨 전달된다. ○ @RequestParam 을 사용했다면 해당 파라미터가 반드시 있어야 한다. 없다면 HTTP 400 - Bad Request 를 받게 될 것이다. 파라미터가 필수가 아니라 선택적으로 제공하게 하려면, required 엘리먼트를 false 로 설정해 주면 된다. 요청 파라미터가 존재하지 않을 때 사용할 디폴트 값도 지정할 수 있다. method4() 는 required 와 defaultValue 엘리먼트를 설정한 예이다. ○ method5() 는 메소드 파라미터의 이름과 요청 파라미터의 이름이 일치하기 때문에 @RequestParam 의 이름 엘리먼트를 생략한 예이다. ○ String, int 와 같은 단순 타입인 경우는 @RequestParam 을 아예 생략할 수도 있다. 이때는 메소드 파라미터와 같은 이름의 요청 파라미터 값을 받는다. 하지만 파라미터의 개수가 많고 종류가 다양해지면 코드를 이해하는 데 불편할 수도 있다. 단순한 메소드가 아니라면 명시적으로 @RequestParam 을 부여해 주는 것을 권장한다.

@CookieValue

HTTP 요청과 함께 전달된 쿠키 값을 메소드 파라미터에 넣어 주도록 @CookieValue 를 사용할 수 있다. 애노테이션의 기본 값에 쿠키 이름을 지정해주면 된다. 몇 가지 예를 살펴보자
public String method1(@CookieValue("auth") String auth) { ... }

public String method2(
        @CookieValue(value="auth", required=false, defaultValue="NONE") String auth) {...}
○ method1() 은 auth 라는 이름의 쿠키 값을 메소드 파라미터 auth 에 넣어주는 메소드 선언이다. 메소드 파라미터 이름과 쿠키 값이 동일하다면 쿠키 이름은 생략할 수 있다. ○ @CookieValue 도 @RequestParam 과 마찬가지로 지정된 쿠키 값이 반드시 존재해야만 한다. 지정한 쿠키 값이 없을 경우에도 예외가 발생하지 않게 하려면, @CookieValue 의 required 엘리먼트를 false 로 선언해줘야 한다. 또한 디폴트 값을 선언해서 쿠키 값이 없을 때 디폴트 값으로 대신하게 할 수 있다. method2() 는 required 와 defaultValue 엘리먼트를 적용한 예이다.

@RequestHeader

요청 헤더를 메소드 파라미터에 넣어 주는 애노테이션이다. 애노테이션의 기본 값으로 가져올 HTTP 헤더의 이름을 지정한다. 다음은 Host 와 Keep-Alive 헤더 값을 메소드 파라미터로 받도록 선언한 메소드다. [code language="java"] public void header(@RequestHeader("Host") String host, @RequestHeader("Keep-Alive") long keepAlive) { ... } [/code] @CookieValue 와 마찬가지로 @RequestHeader 를 사용했다면 헤더 값이 반드시 존재해야 한다. 헤더 값이 존재하지 않아도 상관없도록 설정하거나 디폴트 값을 주려면, @CookieValue 와 마찬가지로 required 와 defaultValue 엘리먼트를 이용하면 된다.

Map/Model/ModelMap

다른 애노테이션이 붙어 있지 않다면 java.util.Map 그리고 스프링의 org.springframework.ui.Model 과 org.springframework.ui.ModelMap 타입의 파라미터는 모두 모델 정보를 담는 데 사용할 수 있는 오브젝트가 전달된다. 모델을 담을 맵은 메소드 내에서 직접 생성할 수도 있지만 그보다는 파라미터로 정의해서 핸들러 어댑터에서 미리 만들어 제공해주는 것을 사용하면 편리하다. Model 과 ModelMap은 모두 addAttribute() 메소드를 제공해준다. 일반적인 맵의 put() 처럼 이름을 지정해서 오브젝트 값을 넣을 수도 있고, 자동 이름 생성 기능을 이용한다면 오브젝트만 넣을 수도 있다. 예를 들어 다음과 같이 ModelMap 에 User 타입 오브젝트를 넣는다면 타입정보를 참고해서 "user" 라는 모델 이름이 자동으로 부여된다.
@RequestMapping(...)
public void hello(ModelMap model) {
    User user = new User(1, "Spring");
    model.addAttribute(user);
}
ModelMap 과 Model 의 addAllAttribute() 메소드를 사용하면 Collection 에 담긴 모든 오브젝트를 자동 이름 생성 방식을 적용해서 모두 모델로 추가해 준다.

@ModelAttribute

@ModelAttribute 는 여기서 소개하는 것처럼 메소드 파라미터에도 부여할 수 있고 메소드 레벨에 적용할 수도 있다. 두 가지가 비슷한 개념이지만 사용 목적이 분명히 다르니 그 차이점에 주의해야 한다. @ModelAttribute 는 모델 맵에 담겨서 뷰에 전달되는 모델 오브젝트의 한가지라고도 볼 수 있다. 기본적으로 @ModelAttribute 가 붙은 파라미터는 별도의 설정 없이도 자동으로 뷰에 전달된다. 클라이언트로부터 컨트롤러가 받은 요청정보 중에서, 하나 이상의 값을 가진, 오브젝트 형태로 만들 수 있는 구조적인 정보를 @ModelAttribute 모델이라고 부른다. @ModelAttribute 는 이렇게 컨트롤러가 전달받은 오브젝트 형태의 정보를 가리키는 말이다. 그렇다면 사용자가 제공하는 정보 중에서 단순히 @RequestParam 이 아니라 @ModelAttribute 를 사용해서 모델로 받는 것은 어떤 게 있을까? 사실 정보의 종류가 다른 건 아니다. 단지 요청 파라미터를 메소드 파라미터에서 1:1로 받으면 @RequestParam 이고, 도메인 오브젝트나 DTO 의 프로퍼티에 요청 파라미터를 바인딩해서 한번에 받으면 @ModelAttribute 라고 볼 수 있다. 하나의 오브젝트에 클라이언트의 요청 정보를 담아서 한 번에 전달되는 것이기 때문에 이를 커맨드 패턴에서 말하는 커맨드 오브젝트라고 부르기도 한다. 요청 파라미터가 많은 경우 @RequestParam 으로 모두 1:1로 받으면 코드가 아무래도 지저분 해진다. 대신 모든 요청 파라미터를 바인딩할 프로퍼티들을 가지는 하나의 클래스를 정의해 사용하면 코드는 무척이나 깔끔해 질 것이다. 예를 들어 아래 add() 메소드는 사용자 등록폼에서 전송된 모든 사용자 정보 파라미터를 바인딩할 수 있는 User 오브젝트를 사용해서 깔끔하게 처리하고 있다.
@RequestMapping(value="/user/add", method=RequestMethod.POST)
public String add(@ModelAttribute User user) {
    userService.add(user);
    ...
}
@ModelAttribute 애노테이션도 생략 가능하다. @RequestParam, @ModelAttribute 애노테이션을 사용하면 메소드 선언이 길어지고 복잡해 보인다고 이를 무조건 생략하는 건 위험할 수 있다. 그래서 가능한 @ModelAttribute 나 @RequestParam 을 사용하는 것을 권장한다. @ModelAttribute 가 해주는 기능이 한 가지가 더 있데, 그것은 컨트롤러가 리턴하는 모델에 파라미터로 전달한 오브젝트를 자동으로 추가해 주는 것이다. 이때 모델의 이름은 기본적으로 파라미터의 이름을 따른다. User 클래스라면 user 라는 이름의 모델이 자동으로 등록된다는 것이다. 다른 이름을 사용하고 싶다면 다음과 같이 @ModelAttribute 에 모델 이름을 지정해 줄 수도 있다.
public String update(@ModelAttribute("currentUser") User user) {
    ...
}
위와 같이 정의하면 update() 컨트롤러가 DispatcherServlet 에게 돌려주는 모델 맵에는 "currentUser" 라는 키로 User 오브젝트가 저장되어 있을 것이다.

Errors/BindingResult

@ModelAttribute 는 단지 오브젝트에 여러 개의 요청 파라미터 값을 넣어서 넘겨주는 게 전부가 아니다. @ModelAttribute 가 붙은 파라미터를 처리할 때는 @RequestParam 과 달리 Validation 작업이 추가적으로 진행된다. 변환이 불가능한 타입의 요청 파라미터가 들어 왔을 때 어떤 일이 일어 날까? @RequestParam 의 경우 먼저 살펴보자. @RequestParam은 스프링의 기본 타입 변환 기능을 이용해서 요청 파라미터 값을 메소드 파라미터 타입으로 변환한다. 타입 변환이 성공한다면 메소드의 파라미터로 전달되지만, 변환 작업을 실패한다면 특별한 예외처리를 해 놓지 않은 이상 디폴트 예외 리졸버를 통해 HTTP 400 - Bad Request 응답 상태로 전환돼서 클라이언트로 전달된다. 이런 경우 사용자에게 친절한 메시지를 보여주고 싶다면 org.springframework.beans.TypeMismatchException 예외를 처리하는 핸들러 예외 리졸버를 추가해 주면 된다. 이제 @ModelAttribute 의 경우를 살펴보자. 다음과 같이 메소드 선언이 있고 UserSearch 의 id 프로퍼티가 int 타입이라고 하자. 이때 /search/?id=abcd 라는 URL 이 들어 왓다면 어떻게 될까?
public String search(@ModelAttribute userSearch, BindingResult result) { ... }
UserSearch 의 setId() 를 이용해 id 값을 넣으려고 시도하다가 예외를 만나게 될 것이다. 하지만 이때는 작업이 중단되고 HTTP 400 응답 상태코드가 클라이언트로 전달되지 않는다. 타입 변환에 실패하더라도 작업은 계속 진행된다. 단지 타입 변환 중에 발생한 예외가 BindException 타입의 오브젝트에 담겨서 컨트롤러로 전달될 뿐이다. 그렇다면 @ModelAttribute 에서는 왜 요청 파라미터의 타입 변환 문제를 바로 에러로 처리하지 않는 것일까? 그 이유는 @ModelAttribute 는 요청 파라미터의 타입이 모델 오브젝트의 프로퍼티 타입과 일치하는 지를 포함한 다양한 방식의 검증 기능을 수행하기 때문이다. @ModelAttribute 입장에서는 파라미터 타입이 일치하지 않는다는 건 검증 작업의 한 가지 결과일 뿐이지, 예상치 못한 예외상황이 아니라는 뜻이다. 별도의 검증과정 없이 무조건 프로퍼티 타입으로 변환해서 값을 넣으려고 시도하는 @RequestParam 과는 그런 면에서 차이가 있다. 사용자가 직접 입력하는 폼에서 들어오는 정보라면 반드시 검증이 필요하다. 버튼이나 링크에 미리 할당된 URL에 담겨 있는 파라미터와 달리 사용자가 입력하는 값에는 다양한 오류가 있을 수 있기 때문이다. 사용자가 입력한 폼의 데이터를 검증하는 작업에는 타입 확인뿐 아니라 필수정보의 입력여부, 길이제한, 포멧, 값의 허용범위 등 다양한 검증 기준이 적용될 수 있다. 이렇게 검증 과정을 거친 뒤 오류가 발견됐다고 하더라도 HTTP 400 과 같은 예외 응답상태를 전달하면서 작업을 종료하면 안된다. 어느 웹사이트에 가서 회원가입을 하는 중에 필수 항목을 하나 빼먹었다고 호출스택 정보와 함께 HTTP 400 에러 메시지가 나타난다면 얼마나 황당하겠는가? 그래서 사용자의 입력 값에 오류가 있을 때 이에 대한 처리를 컨트롤러에게 맡겨야 한다. 그러려면 메소드 파라미터에 맞게 요청정보를 추출해서 제공해주는 책임을 가진 어댑터 핸들러는 실패한 변환 작업에 대한 정보를 컨트롤러에게 제공해 줄 필요가 있다. 컨트롤러는 이런 정보를 참고해서 적절한 에러 페이지를 출력하거나 친절한 에러 메시지를 보여주면서 사용자가 폼을 다시 수정할 기회를 줘야 한다. 바로 이 때문에 @ModelAttribute 를 통해 폼의 정보를 전달받을 때는 org.springframework.validation.Errors 또는 org.springframework.validation.BindingResult 타입의 파라미터를 같이 사용해야 한다. Erros 나 BindingResult 파라미터를 함께 사용하지 않으면 스프링은 요청 파라미터의 타입이나 값에 문제가 없도록 애플리케이션이 보장해준다고 생각한다. 단지 파라미터의 개수가 여러 개라 커맨드 오브젝트 형태로 전달받을 뿐이라고 보는 것이다. 따라서 이때는 타입이 일치하지 않으면 BindingException 예외가 던져진다. 이 예외는 @RequestParam 처럼 친절하게 HTTP 400 응답 상태코드로 변환되지도 않으니 절절하게 예외처리를 해주지 않으면 사용자는 지저분한 에러메시지를 만나게 될 것이다. 폼에서 사용자 정보를 등록받는 add() 와 같은 메소드라면 반드시 아래와 같이 정의해야 한다.
@RequestMapping(value="add", method=RequestMethod.POST)
public String add(@ModelAttribute User user, BindingResult bindingResult) { ... }
BindingResult 대신 Errors 타입으로 선언해도 좋다. 이 두가지 오브젝트에는 User 오브젝트에 파라미터를 바인딩하다가 발생한 변환 오류와 모델 검증기를 통해 검증하는 중에 발견한 오류가 저장된다. 파라미터로 전달받은 bindingResult 를 확인해서 오류가 없다고 나오면, 모든 검증을 통과한 것이므로 안심하고 user 오브젝트 내용을 DB에 등록하고 성공 페이지로 넘어가면 된다. 반대로 bindingResult 오브젝트에 오류가 담겨 있다면 다시 등록폼을 출력해서 사용자가 잘못된 정보를 수정하도록 해야 한다. 스프링의 폼을 처리하는 커스텀 태그를 활용하면 BindingResult 에 담긴 오류 정보를 적절한 메시지로 변환해서 화면에 출력해줄 수 있다. BindingResult 나 Errors 를 사용할 때 주의할 점은 파라미터의 위치다. 이 두 가지 타입의 파라미터는 반드시 @ModelAttribute 파라미터 뒤에 나와야 한다. 자신의 바로 앞에 있는 @ModelAttribute 파라미터의 검증 작업에서 발생한 오류만을 전달해 주기 때문이다.

SessionStatus

컨트롤러가 제공하는 기능 중에 모델 오브젝트를 세션에 저장했다가 다음 페이지에서 다시 활용하게 해주는 기능이 있다. 이 기능을 사용하다가 더 이상 세션 내에 모델 오브젝트를 저장할 필요가 없을 경우에는 코드에서 직접 작업 완료 메소드를 호출해서 세션안에 저장된 오브젝트를 제거해 줘야 한다. 이때 필요한 것이 스프링의 org.springframework.web.bind.support.SessionStatus 오브젝트다. 파라미터로 선언해 두면 현재 세션을 다룰 수 있는 SessionStatus 오브젝트를 제공해 준다. 세션안에 불필요한 오브젝트를 방치하는 것은 일종의 메모리 누수이므로 필요 없어지면 확실하게 제거해 줘야 한다.

@RequestBody

이 애노테이션이 붙은 파라미터는 HTTP 요청의 body 부분이 그대로 전달된다. 일반적인 GET/POST 의 요청 파라미터라면 @RequestBody 를 사용할 일이 없을 것이다. 반면에 XML 이나 JSON 기반의 메시지를 사용하는 요청의 경우에는 이 방법이 매우 유용하다. AnnotationMethodHandlerAdapter 에는 HttpMessageConverter 타입의 메시지 변환기가 여러 개 등록되어 있다. @RequestBody 가 붙은 파라미터가 있으면 HTTP 요청의 미디어 타입과 파라미터의 타입을 먼저 확인한다. 메시지 변환기 중에서 해당 미디어 타입과 파라미터 타입을 처리할 수 있는 것이 있다면, HTTP 요청의 본문 부분을 통째로 변환해서 지정된 메소드 파라미터로 전달해 준다. String HttpMessageConverter 타입 변환기는 스트링 타입의 파라미터와 모든 종류의 미디어 타입을 처리해 준다. 따라서 다음과 같이 정의한다면 메시지 본문 부분이 모두 스트링으로 변환돼서 전달될 것이다.
public void message(@RequestBody String body) { ... }
XML 본문을 가지고 들어오는 요청은 MarshallingHttpMessageConverter 등을 이용해서 XML 이 변환된 오브젝트로 전달 받을 수 있다. JSON 타입의 메시지라면 MappingJacksonHttpMessageConverter 를 사용할 수 있다. @RequestBody 는 보통 @ResponseBody 와 함께 사용된다.

@Value

빈의 값 주입에서 사용하던 @Value 애노테이션도 컨트롤러 메소드 파라미터에 부여할 수 있다. 사용 방법은 DI 에서 프로퍼티나 필드, 초기화 메소드 파라미터에 @Value 를 사용하는 것과 동일하다. 주로 시스템 프로퍼티나 다른 빈의 프로퍼티 값, 또는 좀 더 복잡한 SpEL 을 이용해 클래스의 상수를 읽어 오거나 특정 메소드를 호출한 결과 값, 조건식 등을 넣을 수 있다. 다음은 시스템 프로퍼티에서 OS 이름을 가져와 osName 파라미터에 넣어주는 메소드 선언이다. 스프링이 컨텍스트에 자동으로 등록해주는 시스템 프로퍼티 빈인 systemProperties 로부터 os.name 을 키로 가진 값을 가져와 osName 프로퍼티 변수에 넣어 준다.
@RequestMapping(...)
public String hello(@Value("#{systemProperties['os.name']}") String osName) { ... }
컨트롤러도 일반적인 스프링 빈이기 때문에 @Value를 메소드 파라미터 대신 컨트롤러 필드에 아래와 같이 DI 해주는 것이 가능하다.
public class UserController {

    @Value("#{systemProperties['os.name']}")
    private String osName;

    @RequestMapping(...)
    public String add(...) {
        String osName = this.osName;
        ...
    }

}
@Value 로 정의하는 상수 값이 컨트롤러의 여러 메소드에서 필요로 한다면, 필드에 DI 해두는 편이 낫다.

@Valid

@Valid 는 JSR-303 의 빈 검증기를 이용해서 모델 오브젝트를 검증하도록 지시하는 지시자다. 모델 오브젝트의 검증 방법을 지정하는데 사용하는 애노테이션이다. 보통 @ModelAttribute 와 함께 사용한다.

Spring @SessionAttributes 와 SessionStatus

HTTP 요청에 의해 동작하는 서블릿은 기본적으로 상태를 유지하지 않는다. 따라서 매 요청이 독립적으로 처리된다. 하나의 HTTP 요청을 처리한 후에는 사용했던 모든 리소스를 정리해 버린다. 하지만 애플리케이션은 기본적으로 상태를 유지할 필요가 있다. 사용자가 로그인하면 그 로그인 정보는 계속 유지돼야할 것이고, 여러 페이지에 걸쳐 단계적으로 정보를 입력하는 위저드 폼 같은 경우에도 폼 정보가 하나의 요청을 넘어서 유지돼야 한다. 간단하게는 단순한 폼 처리 중에도 유지해줘야 하는 정보가 있다.

도메인 중심 프로그래밍 모델과 상태 유지를 위한 세션 도입의 필요성

사용자 정보의 수정 기능을 생각해보자. 수정 기능을 위해서는 최소한 두 번의 요청이 서버로 전달돼야 한다. 첫 번째는 수정 폼을 띄워달라는 요청이다. 수정할 사용자 ID를 요청에 함께 전달한다. 서버는 주어진 ID에 해당하는 사용자 정보를 읽어서 수정 가능한 폼을 출력해 준다. 사용자가 폼의 정보를 수정하고 수정 완료 버튼을 누르면 두 번째 요청이 서버로 전달된다. 이때는 수정한 폼의 내용이 서버로 전달된다. 서버는 사용자가 수정한 정보를 받아서 DB에 저장하고 수정 완료 메시지가 담긴 페이지를 보여준다.. 어떻게 보면 수정 작업은 두 번의 요청과 두 개의 뷰 페이지가 있으면 되는 간단한 기능이다. 하지만 좀 더 생각해보면 수정 작업은 생각보다 복잡해질 수 있다. 사용자가 수정한 폼의 필드 값에 오류가 있는 경우에 에러 메시지와 함께 수정 화면을 다시 보여줘야 하기 때문이다. 또, 상태를 유지하지 않고 폼 수정 기능을 만들려면 도메인 오브젝트 중심 방법보다는 계층 간의 결함도가 높은 데이터 중심 방식을 사용하기 쉬워진다. 폼의 검증과 에러 메시지 문제는 이후에 다룰 것이고, 일단 여기서는 폼 처리시 상태유지 문제만 살펴보자. 서버에서 하나의 요청 범위를 넘어서 오브젝트를 유지시키지 않으면 왜 데이터 중심의 코드가 만들어지고 계층간의 결함도가 올라가는지 생각해보자. 사용자 정보의 수정 폼을 띄우는 컨트롤러는 아마도 다음과 같이 만들어질 것이다.
@RequestMapping(value="/user/edit", method=RequestMethod.GET)
public String form(@RequestParam int id, Model model) {
    model.addAttribute("user", userService.getUser(id));
    return "user/edit";
}
사용자 수정 폼에는 사용자 테이블에 담긴 대부분의 정보를 출려해줄 필요가 있다. 그래서 도메인 오브젝트 중심 방식의 DAO 를 사용해서 주어진 id에 대한 사용자 정보를 User 오브젝트에 담아 모두 가져오는 것이 자연스럽다. 서비스 계층에서 사용자 정보를 가져오면서 일부 정보를 활용하거나 가공하는 등의 작업을 해도 상관없다. User 라는 의미 있는 도메인 오브젝트를 사용하기 때문이다. 그런데 문제는 여기서부터다. 사용자 정보 수정 화면이라고 모든 정보를 다 수정하는 것이 아니다. 사용자 스스로 자신의 정보를 수정하는 경우라면 수정할 수 있는 필드는 제한되어 있다. 로그인 아이디나 중요한 가입정보는 특별한 권한이 없으면 수정할 수 없게 해야 한다. 사용자 정보에 포함된 레벨이나 포인트 등도 수정할 수 없다. 마지막 로그인 시간이나, 기타 사용자의 활동정보에 해당하는 내용도 수정할 수 없다. 이런 정보는 아예 수정 폼에 나타나지 않거나, 나타난다고 하더라도 읽기 전용으로 출력만 될 뿐이다. 좀 더 권한이 많은 관리자 모드의 사용자 수정 화면이라고 하더라도 여전히 폼에서 모든 정보를 다 수정할 필요는 없다. 따라서 User 도메인 오브젝트에 담겨서 전달된 내용 중에서 수정 가능한 일부 정보만 폼의 이나 등을 이용한 수정용 필드로 출력된다. 그렇기 때문에 폼을 수정하고 저장 버튼을 눌렀을 때는 원래 User에 담겨 있던 내용 중에서 수정 가능한 필드에 출력했던 일부만 서버로 전송된다는 문제가 발생한다. 폼의 서브밋을 받는 컨트롤러 메소드에서 만약 다음과 같이 User 오브젝트로 폼의 내용을 바인딩하게 했다면 어떻게 될까?
@RequestMapping(value="/user/edit", method=RequestMethod.POST)
public String submit(@ModelAttribute User user) {
    userService.updateUser(User);
    return "user/editsuccess";
}
@ModelAttribute 가 붙은 User 타입 파라미터를 선언했으니 폼에서 POST 를 통해 전달되는 정보는 User 오브젝트의 프로퍼티에 바인딩돼서 들어갈 것이다. 문제는 폼에서 이나 로 정의한 필드의 정보만 들어간다는 점이다. 단순히 화면에 읽기 전용으로 출력했던 loginId 나 가입일자라든가, 아예 출력도 안 했던 포인트나 내부 관리 정보 등은 폼에서 전달되지 않으므로 submit() 메소드의 파라미터로 전달되는 user 오브젝트에는 이런 프로퍼티 정보가 모두 비어 있을 것이다. 일부 필드의 정보가 빠진 이 반쪽짜리 user 오브젝트를 User 도메인 오브젝트를 사용해서 비즈니스 로직을 처리하는 서비스 계층의 UserService 빈을 사용해서 User 도메인 오브젝트를 사용해서 DB에 결과를 업데이트해주는 UserDao 빈에 전달해준다고 해보자. 어떤 일이 일어날까? UserService 에 사용자 정보를 수정할 때마다 사용자 정보를 일부 참조하거나 변경해주는 로직이 포함되어 있거나, 수정이 일어날때 이를 로깅하거나 관리자에게 통보하거나 통계를 내기 위해 다른 서비스 오브젝트를 호출하도록 만들어질 수 있다. 폼에 얼마나 많은 정보를 띄우고 이를 다시 돌려받는지에 따라 다르겠지만, 아마도 서비스 계층의 로직을 처리하는 중에 치명적인 오류가 발생할지도 모른다. 도메인 오브젝트를 이용해 비즈니스 로직을 처리하도록 만든 서비스 계층의 코드는 폼에서 어떤 내용만 다시 돌려지는지 알지 못하기 때문에, User 오브젝트를 받았다면 모든 프로퍼티 값을 다 사용하려고 할 것이다. 따라서 일부 비즈니스 로직에선 잘못된 값이 사용될 수도 있고, 아예 예외가 발생할 수도 있다. DAO 경우는 더 심각하다. 도메인 오브젝트를 사용하도록 만든 DAO 의 업데이트 메소드는 도메인 오브젝트의 수정 가능한 모든 필드를 항상 업데이트한다. 따라서 일부 프로퍼티 값이 null 이거나 0인 상태로 전달돼도 그대로 DB에 반영해버린다. 결국 중요한 데이터를 날려버리는 심각한 결과를 초래할 것이다. 사용자가 자기 정보를 수정하고나면 0이 되는 버그 같은게 만들어 질 수 있다는 뜻이다. 도메인 오브젝트를 사용해 수정 폼을 처리하는 방식에는 이런 심각한 문제점이 있다. 그렇다면 이 문제를 해결할 수 있는 방법을 한번 생각해 보자.

히든필드

수정을 위한 폼에 User 의 모든 프로퍼티가 다 들어가지 않기 때문에 이런 문제가 발생했으니 모든 User 오브젝트의 프로퍼티를 폼에 다 넣어주는 방법을 생각해 볼 수 있다. 물론 사용자가 수정하면 안되는 정보가 있으니 이런 정보는 히든 필드에 넣어줘야 한다. 히든 필드를 사용하면 화면에서는 보이지 않지만 폼을 서브밋하면 다시 서버로 전송된다. 결국 컨트롤러가 받는 User 타입의 오브젝트에는 모든 프로퍼티의 값이 채워져 있을 것이다. 사용자가 수정하도록 노출한 필드는 바뀔 수 있지만, 히든 필드로 감춰둔 것은 원래 상태 그대로 돌아올 것이다. 이 방법은 간단히 문제를 해결한 듯 보이지만 사실 두 가지 심각한 문제가 있다. 첫째, 데이터 보안에 심각한 문제를 일으킨다. 폼의 히든 필드는 브라우저 화면에는 보이지 않지만 HTML 소스를 열어보면 그 내용과 필드 이름까지 쉽게 알아낼 수 있다. 폼을 통해 다시 서버로 전송되는 정보는 간단히 조작될 수 있다. 즉, 사용자가 나쁜 마음을 먹으면 HTML 을 통해 히든 필드 정보를 확인하고 그 값을 임의로 조작해서 서버로 보내버릴 수 있다. 이렇게 히든 필드에 중요 정보를 노출하고, 이를 다시 서버에서 받아서 업데이트 하는 방법은 보안에 매우 취약하다는 단점이 있다. 두 번째 문제는 사용자 정보에 새로운 필드가 추가됐다고 해보자. 그런데 깜빡하고 폼에 이 정보에 대한 히든 필드를 추가해주지 않으면, 추가된 필드의 값은 수정을 거치고 난 후 null 로 바뀌는 현상이 발생할 것이다. 이런 것은 테스트에서도 쉽게 발견되지 않으므로 실수하기 쉽다. 이런 버그 때문에 많은 사용자의 중요한 정보를 다 날린 뒤에 문제를 발견하면 큰일이다. 따라서 히든 필드 방식은 매우 유치한 해결 방법이고 공개된 서비스에서는 사용을 권장할 수 없다.

DB 재조회

두 번째 해결책은 기능적으로 보자면 완벽하다. 폼으로부터 수정된 정보를 받아 User 오브젝트에 넣어줄 때 빈 User 오브젝트 대신 DB에서 다시 읽어온 User 오브젝트를 이용하는 것이다. 이 방식으로 만든 컨트롤러 메소드를 살펴보자.
@RequestMapping(value="/user/edit", method=RequestMethod.POST)
public String submit(@ModelAttribute User formUser, @RequestParam int id) {
    User user = userService.getUser(id);
     
    user.setName(formUser.getName());
    user.setPassword(formUser.getPassword());
    user.setEmail(formUser.getEmail());
     
    ...
     
    userService.updateUser(user);
    return "user/editsuccess";
}
이 방법은 업데이트를 위해 서비스 계층으로 전달할 User 오브젝트를 DB에서 새로 읽어온 것으로 사용한다. DB에서 새로 읽어왔으므로 User 오브젝트에는 모든 프로퍼티의 내용이 다 들어 있다. 그리고 폼에서 전달되는 정보를 담을 User 타입의 formUser 파라미터도 메소드에 추기해 준다. formUser 에는 폼에 사용자가 수정할 수 있도록 출력해둔 필드의 내용만 들어 있을 것이다. 그리고 DB에서 가져온 모든 필드 정보가 담겨 있는 User 오브젝트에, 폼에서 전달 받은 formUser 중 폼에서 전달된 필드 값을 복사해 준다. 그리고 복사가 끝난 User 오브젝트를 서비스 계층으로 전달하는 것이다. 이렇게 전달된 User 오브젝트에는 모든 필드가 다 채워져 있고 폼에서 수정한 내용도 모두 반영되어 있다. 따라서 서비스 계층에서 User 도메인 오브젝트를 어떻게 활용하든 아무런 문제가 없다. 또 DAO 에서 모든 필드를 다 업데이트해도 상관없다. 완벽해 보이긴 하지만 이 방법에는 몇 가지 단점이 있다. 일단 폼을 서브밋할 때마다 DB에서 사용자 정보를 다시 읽는 부담이 있다. 성능에 큰 영향을 줄 가능성이 높진 않더라도 분명 DB의 부담을 증가시키는 것은 사실이다. 성능은 차치하더라도 폼에서 전달되는 필드가 어떤 것인지 정확히 알고 이를 복제해줘야 한다. 복사할 필드를 잘 못 선택하거나 빼먹으면 문제가 발생한다. 얼핏 보면 간단히 문제를 해결한 듯 보이지만 여전히 불편하며 새로운 문제를 초래하는 방법이다.

계층 사이의 강한 결합

세 번째로 생각해볼수 있는 방법은 계층 사이에 강한 결합을 주는 것이다. 강한 결합이라는 의미는 각 계층의 코드가 다른 계층에서 어떤 일이 일어나고 어떤 작업을 하는지를 자세히 알고 그에 대응해서 동작하도록 만든다는 뜻이다. 이 방식은 앞에서 지적한 폼 수정 문제의 전제를 바꿔서 문제 자체를 제거한다. 기본 전제는 서비스 계층의 updateUser() 메소드가 User 라는 파라미터를 받으면 그 User 는 getUser() 로 가져오는 User와 동등하다고 본다는 것이다. User 라는 오브젝트는 한 사용자 정보를 완전히 담고 있고, 그것을 전달받으면 원하는 모든 필드를 참조하고 조작할 수 있다고 여긴다는 말이다. 이렇게 각 계층이 도메인 모델을 따라 만든 도메인 오브젝트에만 의존하도록 만들면 각 계층 사이에 의존성과 결합도를 대폭 줄일 수 있다. 결합도를 줄인다는 의미는 한 계층의 구현 코드를 수정해도 기본적인 전제인 도메인 오브젝트가 바뀌지 않으면 다른 계층의 코드에 영향을 주지 않는다는 뜻이다. UserService 의 updateUser() 메소드는 사용자 레벨을 업그레이드하는 로직에서 결과를 반영하기 위해 호출되든, 관리자의 수정화면을 처리하는 AdminUserController 에서 호출되든, 사용자가 스스로 정보를 수정하는 화면을 처리하는 UserController 에서 호출되든 상관없이 동일하게 만들 수 있다. UserDao 의 update() 메소드도 마찬가지다. 전달되는 User 오브젝트가 폼을 통해 수정된 User인지 상관하지 않고 자신의 기능에만 충실하게 만들 수 있다. 결국 모든 계층의 코드가 서로 영향을 주지 않고 독립적으로 확장하거나 변경할 수 있고, 여러 개의 로직에서 공유할 수 있다. 반면에 계층 사이에 강한 결합을 준다는 건 각 계층의 코드가 특정 작업을 중심으로 긴밀하게 연결되어 있고, 자신을 사용하는 다른 계층의 코드가 어떤 작업을 하는지 구체적으로 알고 있다는 뜻이다. 예를 들어 사용자 자신의 정보를 수정하는 폼을 통해 전달되는 User 오브젝트에는 name, password, email 세 개의 필드만 들어온다는 사실을 UserService 의 updateUserForm() 메소드가 알고 있게 해주면 문제는 간단해진다. 폼에서 세 개의 필드만 수정할 수 있음을 알고 있으니 그 세 개의 필드만 담긴 User 오브젝트가 컨트롤러로 전달되는 것을 알 수 있다. 그에 따라 세 개의 필드 외에는 참조하지 않고 무시하도록 코드를 만들면 된다. DAO 도 마찬가지다, User의 모든 필드를 업데이트 하는 대신 updateForm() 이라는 메소드를 하나 따로 만들어서 name, password, email 세 개의 필드만 수정하도록 SQL을 작성하게 해주면 된다. 관리자 메뉴의 사용자 정보 수정 기능이 있고 거기에는 더 많은 정보를 수정하도록 만들어진 폼이 있다면, 이 폼을 처리하는 컨트롤러는 관리자 사용자 정보 수정을 전담하는 UserService 의 updateAdminUserForm() 을 호출하게 한다. updateAdminUserForm() 은 폼에 어떤 필드가 수정 가능하도록 출력되는지를 알고 있는 메소드다. 따라서 해당 필드만 참조해서 비즈니스 로직을 처리하고, 역시 해당 필드만 DB에 반영해주는 UserDao 의 updateAdminForm() 메소드를 사용한다. 아예 컨트롤러에서 폼의 정보를 받는 모델 오브젝트를 폼에서 수정 가능한 파라미터만 가진 UserForm, UserAdminForm 등을 독립적으로 만들어서 사용하는 방법도 있다. 결국 개별 작업을 전담하는 코드를 각 계층마다 만들어야 한다. 이런식으로 계층 간에 강한 의존성을 주고 결합도를 높이면 처음에는 만들기 쉽다. 대부분의 코드는 화면을 기준으로 해서 각 계층별로 하나씩 독립적으로 만들어질 것이다. DAO 의 메소드와 서비스 계층의 메소드는 각각 하나의 화면을 위해서만 사용된다. 결국 화면을 중심으로 거기에 사용되는 SQL 을 정의하고 하나씩의 메소드를 새롭게 추가하는 것을 선호하는 개발자가 애용하는 방식이다. 이런식의 개발은 코드 생성기를 적용하기도 좋다. 하지만 이렇게 결합도가 높은 코드를 만들 경우, 애플리케이션이 복잡해지기 시작하면 단점이 드러난다. 일단 코드의 중복이 늘어난다. 코드를 재사용하기가 힘들기 때문이다. 수정할 필드가 조금 달라도 다른 메소드를 만들어야 한다. 코드는 자꾸 중복되고 그 때문에 기능을 변경할 때 수정해야 할 곳도 늘어난다. 또한 계층 사이에 강한 결합이 있기 때문에 한쪽을 수정하면 연관된 코드도 함께 수정해줘야 한다. 코드의 중복은 많아지고 결합이 강하므로 그만큼 테스트하기도 힘들다. 테스트 코드에도 중복이 일어난다. 하지만 여전히 이런 방식을 선호하는 경우가 많다. 이런 방식이라면 서비스 계층, DAO 계층을 구분하는 것도 별 의미가 없을 지 모른다. 차라리 2계층이나, 컨트롤러 - 뷰만의 1계층 구조로 가능게 나을 수 있다. 아무튼, 이렇게 계층 사이에 강한 결합을 만들고 수정할 필요가 있는 필드가 어떤 것인지 모든 계층의 메소드가 다 알고 있게 하면 문제는 해결할 수 있다. 이럴 땐 User 와 같은 도메인 오브젝트보다는 차라리 파라미터 맵을 쓰는 것이 편리할 수 있다. 다음과 같이 컨트롤러 메소드의 @RequestParam 이 붙는 Map 타입 파라미터를 사용하면 모든 요청 파라미터를 맵에 담은 것을 전달 받을 수 있다. 맵을 이용하면 필드의 정보를 문자로 된 키로 가져와야 하고, 값도 모두 스트링 타입이기 때문에 필드의 정보를 담을 클래스를 정의하는 수고는 덜지만 한편으로는 코드의 부담이 늘어날 수도 있다.
@RequestMapping(value="/user/edit", method=RequestMethod.POST)
public String submit(@RequestParam Map userMap) {
    userService.updateUserByUserMap(user);
    return "user/editsuccess";
}
어쩔 수 없이 이런 접근 방법을 사용해야 하는 경우가 있다. 폼에서 받은 정보가 특정 도메인 오브젝트에 매핑되지 않는 특별한 정보인 경우도 있다. 하지만 도메인 오브젝트에 연관된 정보라면 가능한 도메인 오브젝트 접근 방법을 사용하는 것을 권장한다. 그래야 스프링이 제공해주는 편리한 기능을 활용할 수 있으며, 좀 더 객체지향적인 코드를 만들 수 있기 때문이다. 마지막으로 이 문제에 대한 스프링의 해결책을 알아보자.

Spring Redirect - FlashMap, RedirectAttributes

Spring MVC의 Controller에서 특정페이지 또는 다른 Controller로 Redirect 할 때 파라메터를 전달하는 방법은 크게 다른게 없었다. 이동하고자 하는 URL 뒤에 파라메터를 붙여주는 방법이다.

@RequestMapping(value="/create", method=RequestMethod.POST) public String setLicense( HttpServletRequest request, HttpServletResponse response) {

return "redirect:/springmvc/test?param=1"

}

간단하게 해결할 수 있는 방법이긴 한데 이럴 경우 Redirect 되는 페이지 주소에 파라메터 정보도 같이 붙어 나오게 된다. 왠지 정보를 숨기고 싶어서 생각할 수 있는 방법이 Session에 넣어두는 방법이다. Session에 정보를 넣었다가 뺐다가 해야 하는데 이것도 왠지 완벽하지 않다. 이런 문제점을 해결하기 위해서 Spring 3.1 부터는 Redirect 할 때 파라메터를 쉽게 전달할 수 있도록 RedirectAttributes , FlashMap 클래스를 제공한다. 먼저 RedirectAttributes를 사용하여 Redirect시 파라메터를 저장할 수 있는데 두가지 방법으로 가능하다. 첫번째 방법은 addAttribute 메소드로 값을 저장한다. 이 경우에 URL에 파라메터가 붙어 전달되게 된다. 그렇기 때문에 전달된 페이지에서 파라메터가 노출되게 된다. 두번째 방법은 addFlashAttribute 메소드로 Redirect 동작이 수행되기 전에 Session에 값이 저장되고 전달 후 소멸된다. Session을 선언해서 집어넣고 사용후 지워주는 수고를 덜어주는 아주 유용한 기능이라고 볼 수 있다. 다음은 사용 예이다.

@RequestMapping(value="/set", method=RequestMethod.GET)
public String set( RedirectAttributes redirectAttributes ) {

 //addAttribute로 전달할 값을 저장한다.
 redirectAttributes.addAttribute("notifications", "true");
 redirectAttributes.addAttribute("message", "success");

 //or

 //addFlashAttribute로 전달할 값을 저장한다.
 redirectAttributes.addFlashAttribute("notifications", "true");
 redirectAttributes.addFlashAttribute("message", "success");

 return "redirect:list";
}

@RequestMapping(value="/list", method=RequestMethod.GET)
public String get() {
 return "pages/testpage";
}

FlashMap에 값을 추가하고 가져올 수 있는 API도 제공하는데 RequestContextUtils 클래스의 메소드를 이용하여 데이터를 저장하고 꺼내올 수 있다. 아래는 사용 예이다.

@RequestMapping(value="/set", method=RequestMethod.GET)
public String set( HttpServletRequest request ) {
 //FlashMap에 전달할 값을 저장한다.
 FlashMap fm = RequestContextUtils.getOutputFlashMap(request);
 fm.put("notifications", "true");
 fm.put("message", "success");

 return "redirect:list";
}

@RequestMapping(value="/list", method=RequestMethod.GET)
public String get() {

 //FlashMap에 저장되어 전달된 값을 가져온다.
 Map flashMap = RequestContextUtils.getInputFlashMap(request);
 if(flashMap !=null) {
 System.out.println( (String)flashMap.get("notifications") );
 System.out.println( (String)flashMap.get("message") );
 }
 return "pages/testpage";
}

Spring @ModelAttribute의 쓰임새

몇일전 @ModelAttribute와 @SessionAttributes의 이해와 한계라는 글을 읽다 내가 알고있는 @ModelAttribute의 역할과는 달라서 조금 길게 덧글을 써보고자 한다. 위 글에서는 @ModelAttribute의 역할을 다음과 같이 정의하고 있다.
@ModelAttribute는 다른 말로 커맨드 오브젝트라고도 불리는데 그 이유는 클라이언트가 전달하는 파라미터를 1:1로 전담 프로퍼티에 담아내는 방식이 커맨드 패턴 그 자체이기 때문이다. 위의 이미지와 같이 @ModelAttr ibute는 클라이언트의 파라미터를 완벽하게 이해하고 있는 자바빈 객체를 요구하며 이 객체에 @ModelAttribu te란 어노테이션를 붙이면 자동으로 바인딩해주고 중간 과정은 모두 자동으로 생략 해준다.
내가 아는 한 Spring @MVC는 @ModelAttribute와는 관계없이 데이터 바인딩과 모델에 담는 일을 처리해준다. 즉, 다음과 같이 코드를 작성하면 동일한 결과를 얻을 수 있다.
@Controller
public class HomeController {

    @RequestMapping("/command")
    public String command(CommandObject command) {
        return "command";
    }

    @RequestMapping("/modelCommand")
    public String modelCommand(@ModelAttribute CommandObject command) {
        return "command";
    }

}
 
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>



    이름: 
별명:
command() / modelCommand() 핸들러는 둘 다 데이터 바인딩 / 모델에 값 유지가 된다. 위 코드만 봐도 @ModelAttribute가 데이터 바인딩과는 관계가 없다는것을 알 수 있다. 그렇다면 @ModelAttribute는 무엇에 쓰는 녀석인가? 난 다음과 같이 총 4가지 유형으로 @ModelAttribute 쓰임새를 정리할 수 있다고 생각한다.

1. formBackingObject or referenceData 로 사용

formBackingObject/referenceData는 Spring 3.0 이전에 상속기반의 MVC에서 사용하던 용어로 View에 필요한 기본값을 내려보낼때 사용한다.
@Controller
public class HomeController {

    @ModelAttribute("commandTypes")
    public Set commandTypes() {
        Set commandTypes = new HashSet();
        commandTypes.add("commandType1");
        commandTypes.add("commandType2");
        commandTypes.add("commandType3");
        return commandTypes;
    }

    @RequestMapping("/command")
    public String command(@RequestParam(required=false) String commandType) {
        if(commandTypes().contains(commandType)) {
            // 명령 수행
        }
        return "command";
    }

}
View에서 ‘commandTypes’로 선택 가능한 명령을 보여주고, 입력받아서 처리하는 것이다.

2. 핸들러(@RequestMapping)에 넘어가는 인자의 이름 설정

첫번째 샘플코드에서 본것처럼 스프링은 핸들러가 받는 인자는 모델에 담아서 View로 보낸다. 여기서 정해진 이름이 없다면 스프링은 정해진 규칙(클래스 이름을 기준으로 첫자는 소문자로 이후는 유지)대로 이름을 만들어준다. CommandObject를 받을 때 이름을 설정하지 않았다면 ‘commandObject’라는 이름을 만들어주는 것이다. 개발자가 직접 이름을 지정할 필요가 있다면 다음과 같이 코드를 작성해서 설정해줄 수 있다.
@Controller
public class HomeController {

    @RequestMapping("/command")
    public String command(CommandObject command, Model model) {
        model.addAttribute("command", command);
        return "command";
    }

    @RequestMapping("/modelCommand")
    public String modelCommand(@ModelAttribute("command") CommandObject command) {
        return "command";
    }

}
3. @SessionAttributes로 유지된 객체를 받을 때
@Controller
@SessionAttributes("command")
public class HomeController {

    @ModelAttribute("command")
    public CommandObject commandObject() {
        return new CommandObject();
    }

    @RequestMapping("/command1")
    public String command1(@ModelAttribute("command") CommandObject command) {
        return "command";
    }

    @RequestMapping("/command2")
    public String command2(@ModelAttribute("command") CommandObject command) {
        return "command";
    }

}
command1()과 command2()에서 받는 command 객체는 commandObject()로 생성된 객체가 Session으로 유지되어 넘어온 객체이다.  

Spring MVC Form Handling Annotation Example

1. SimpleFormController vs @Controller

In XML-based Spring MVC web application, you create a form controller by extending the SimpleFormController class. In annotation-based, you can use @Controller instead. SimpleFormController
public class CustomerController extends SimpleFormController{
      //...
}
Annotation
@Controller
@RequestMapping("/customer.htm")
public class CustomerController{
      //...
}

2. formBackingObject() vs RequestMethod.GET

In SimpleFormController, you can initialize the command object for binding in the formBackingObject() method. In annotation-based, you can do the same by annotated the method name with @RequestMapping(method = RequestMethod.GET). SimpleFormController
@Override
 protected Object formBackingObject(HttpServletRequest request)
  throws Exception {
 
  Customer cust = new Customer();
  //Make "Spring MVC" as default checked value
  cust.setFavFramework(new String []{"Spring MVC"});
 
  return cust;
 }
Annotation
@RequestMapping(method = RequestMethod.GET)
 public String initForm(ModelMap model){
 
  Customer cust = new Customer();
  //Make "Spring MVC" as default checked value
  cust.setFavFramework(new String []{"Spring MVC"});
 
  //command object
  model.addAttribute("customer", cust);
 
  //return form view
  return "CustomerForm";
 }

3. onSubmit() vs RequestMethod.POST

In SimpleFormController, the form submission is handle by the onSubmit() method. In annotation-based, you can do the same by annotated the method name with @RequestMapping(method = RequestMethod.POST). SimpleFormController
 @Override
 protected ModelAndView onSubmit(HttpServletRequest request,
  HttpServletResponse response, Object command, BindException errors)
  throws Exception {
 
  Customer customer = (Customer)command;
  return new ModelAndView("CustomerSuccess");
 
 }
Annotation
@RequestMapping(method = RequestMethod.POST)
public String processSubmit(
 @ModelAttribute("customer") Customer customer,
 BindingResult result, SessionStatus status) {

 //clear the command object from the session
 status.setComplete(); 

 //return form success view
 return "CustomerSuccess";

}

4. referenceData() vs @ModelAttribute

In SimpleFormController, usually you put the reference data in model via referenceData() method, so that the form view can access it. In annotation-based, you can do the same by annotated the method name with @ModelAttribute. SimpleFormController
@Override
protected Map referenceData(HttpServletRequest request) throws Exception {

 Map referenceData = new HashMap();

 //Data referencing for web framework checkboxes
 List webFrameworkList = new ArrayList();
 webFrameworkList.add("Spring MVC");
 webFrameworkList.add("Struts 1");
 webFrameworkList.add("Struts 2");
 webFrameworkList.add("JSF");
 webFrameworkList.add("Apache Wicket");
 referenceData.put("webFrameworkList", webFrameworkList);

 return referenceData;
}
Spring’s form

Annotation
@ModelAttribute("webFrameworkList")
public List populateWebFrameworkList() {

 //Data referencing for web framework checkboxes
 List webFrameworkList = new ArrayList();
 webFrameworkList.add("Spring MVC");
 webFrameworkList.add("Struts 1");
 webFrameworkList.add("Struts 2");
 webFrameworkList.add("JSF");
 webFrameworkList.add("Apache Wicket");

 return webFrameworkList;
}
Spring’s form

5. initBinder() vs @InitBinder

In SimpleFormController, you define the binding or register the custom property editor via initBinder() method. In annotation-based, you can do the same by annotated the method name with @InitBinder. SimpleFormController
protected void initBinder(HttpServletRequest request,
 ServletRequestDataBinder binder) throws Exception {

 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
 binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
}
Annotation
@InitBinder
public void initBinder(WebDataBinder binder) {
 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

 binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
}

From Validation

In SimpleFormController, you have to register and map the validator class to the controller class via XML bean configuration file, and the validation checking and work flows will be executed automatically. In annotation-based, you have to explicitly execute the validator and define the validation flow in the @Controller class manually. See the different : SimpleFormController

    
 

 
 
  
 

Annotation
@Controller
@RequestMapping("/customer.htm")
public class CustomerController{
 
 CustomerValidator customerValidator;
 
 @Autowired
 public CustomerController(CustomerValidator customerValidator){
  this.customerValidator = customerValidator;
 }
 
 @RequestMapping(method = RequestMethod.POST)
 public String processSubmit(
  @ModelAttribute("customer") Customer customer,
  BindingResult result, SessionStatus status) {
 
  customerValidator.validate(customer, result);
 
  if (result.hasErrors()) {
   //if validator failed
   return "CustomerForm";
  } else {
   status.setComplete();
   //form success
   return "CustomerSuccess";
  }
 }
 //...

Full Example

See a complete @Controller example.
package com.mkyong.customer.controller;
 
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.support.SessionStatus;
 
import com.mkyong.customer.model.Customer;
import com.mkyong.customer.validator.CustomerValidator;
 
@Controller
@RequestMapping("/customer.htm")
public class CustomerController{
 
 CustomerValidator customerValidator;
 
 @Autowired
 public CustomerController(CustomerValidator customerValidator){
  this.customerValidator = customerValidator;
 }
 
 @RequestMapping(method = RequestMethod.POST)
 public String processSubmit(
  @ModelAttribute("customer") Customer customer,
  BindingResult result, SessionStatus status) {
 
  customerValidator.validate(customer, result);
 
  if (result.hasErrors()) {
   //if validator failed
   return "CustomerForm";
  } else {
   status.setComplete();
   //form success
   return "CustomerSuccess";
  }
 }
 
 @RequestMapping(method = RequestMethod.GET)
 public String initForm(ModelMap model){
 
  Customer cust = new Customer();
  //Make "Spring MVC" as default checked value
  cust.setFavFramework(new String []{"Spring MVC"});
 
  //Make "Make" as default radio button selected value
  cust.setSex("M");
 
  //make "Hibernate" as the default java skills selection
  cust.setJavaSkills("Hibernate");
 
  //initilize a hidden value
  cust.setSecretValue("I'm hidden value");
 
  //command object
  model.addAttribute("customer", cust);
 
  //return form view
  return "CustomerForm";
 }
 
 
 @ModelAttribute("webFrameworkList")
 public List populateWebFrameworkList() {
 
  //Data referencing for web framework checkboxes
  List webFrameworkList = new ArrayList();
  webFrameworkList.add("Spring MVC");
  webFrameworkList.add("Struts 1");
  webFrameworkList.add("Struts 2");
  webFrameworkList.add("JSF");
  webFrameworkList.add("Apache Wicket");
 
  return webFrameworkList;
 }
 
 @InitBinder
 public void initBinder(WebDataBinder binder) {
  SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
 
  binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
 
 }
 
 @ModelAttribute("numberList")
 public List populateNumberList() {
 
  //Data referencing for number radiobuttons
  List numberList = new ArrayList();
  numberList.add("Number 1");
  numberList.add("Number 2");
  numberList.add("Number 3");
  numberList.add("Number 4");
  numberList.add("Number 5");
 
  return numberList;
 }
 
 @ModelAttribute("javaSkillsList")
 public Map populateJavaSkillList() {
 
  //Data referencing for java skills list box
  Map javaSkill = new LinkedHashMap();
  javaSkill.put("Hibernate", "Hibernate");
  javaSkill.put("Spring", "Spring");
  javaSkill.put("Apache Wicket", "Apache Wicket");
  javaSkill.put("Struts", "Struts");
 
  return javaSkill;
 }
 
 @ModelAttribute("countryList")
 public Map populateCountryList() {
 
  //Data referencing for java skills list box
  Map country = new LinkedHashMap();
  country.put("US", "United Stated");
  country.put("CHINA", "China");
  country.put("SG", "Singapore");
  country.put("MY", "Malaysia");
 
  return country;
 }
}
To make annotation work, you have to enable the component auto scanning feature in Spring.

 
 
 
 
 
  
 
  
 
 
 
              
                 /WEB-INF/pages/
              
              
                 .jsp
              
        

Spring MVC And List Example

In this tutorial, we show you how to print the List values via JSTL c:forEach tag. P.S This web project is using Spring MVC frameworks v3.2 1. Project Structure Review the project directory structure, a standard Maven project. 이미지 2. Project Dependencies Add Spring and JSTL libraries. pom.xml

  3.2.2.RELEASE
  1.2
 

 

  
  
   jstl
   jstl
   ${jstl.version}
  

  
  
   org.springframework
   spring-core
   ${spring.version}
  

  
   org.springframework
   spring-web
   ${spring.version}
  

  
   org.springframework
   spring-webmvc
   ${spring.version}
  

 
3. Spring Controller A Spring controller to return a List.
/*MainController.java*/

package com.mkyong.web.controller;

import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MainController {

 @RequestMapping(value = "/", method = RequestMethod.GET)
 public ModelAndView getdata() {

  List list = getList();

  //return back to index.jsp
  ModelAndView model = new ModelAndView("index");
  model.addObject("lists", list);

  return model;

 }

 private List getList() {

  List list = new ArrayList();
  list.add("List A");
  list.add("List B");
  list.add("List C");
  list.add("List D");
  list.add("List 1");
  list.add("List 2");
  list.add("List 3");

  return list;

 }

}
4. JSP Page To print the returned List from controller, uses JSTL c:forEach tag.
  • index.jsp
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>


 

Spring MVC and List Example

  • ${listValue}

Spring 3 JavaConfig Example

Since Spring 3, JavaConfig features are included in core Spring module, it allow developer to move bean definition and Spring configuration out of XML file into Java class. But, you are still allow to use the classic XML way to define beans and configuration, the JavaConfig is just another alternative solution. See the different between classic XML definition and JavaConfig to define a bean in Spring container. Spring XML file :


 


Equivalent configuration in JavaConfig :
package com.mkyong.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.mkyong.hello.HelloWorld;
import com.mkyong.hello.impl.HelloWorldImpl;

@Configuration
public class AppConfig {

 @Bean(name="helloBean")
 public HelloWorld helloWorld() {
     return new HelloWorldImpl();
 }

}
Spring JavaConfig Hello World Now, see a full Spring JavaConfig example. 1. Directory Structure spring-javaconfig-folder 2. Dependency Library To see JavaConfig(@Configuration), you need to include CGLIB library. See dependencies:

 
  org.springframework
  spring-core
  ${spring.version}
 

 
  org.springframework
  spring-context
  ${spring.version}
 

 
 
  cglib
  cglib
  2.2.2
 
3. Spring Bean a simple bean.
package com.mkyong.hello;

public interface HelloWorld {

 void printHelloWorld(String msg);

}
package com.mkyong.hello.impl;

import com.mkyong.hello.HelloWorld;

public class HelloWorldImpl implements HelloWorld {

 @Override
 public void printHelloWorld(String msg) {

  System.out.println("Hello : " + msg);
 }

}
4. JavaConfig Annotation Annotate with @Configuration to tell Spring that this is the core Spring configuration file, and define bean via @Bean.
package com.mkyong.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.mkyong.hello.HelloWorld;
import com.mkyong.hello.impl.HelloWorldImpl;

@Configuration
public class AppConfig {

 @Bean(name="helloBean")
 public HelloWorld helloWorld() {
 return new HelloWorldImpl();
 }

}
5. Run it Load your JavaConfig class with AnnotationConfigApplicationContext
package com.mkyong.core;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.mkyong.config.AppConfig;
import com.mkyong.hello.HelloWorld;

public class App {
 public static void main(String[] args) {

            ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
     HelloWorld obj = (HelloWorld) context.getBean("helloBean");

     obj.printHelloWorld("Spring3 Java Config");

 }
}
Output Hello : Spring3 Java Config

Spring 스프링 구동방법 컨셉

IoC컨테이너를 통해 애플리케이션이 만들어지는 방식

image image

웹환경에서 스프링애플리케이션이 기동방식

image

부모 컨텍스트를  이용한 계층구조

image

스프링 웹애플리케이션의 다양한 구성방법 image

Spring - ApplicationContext 객체가져오기.

스프링으로 개발하다 보면 bean들의 ID값을 이용해 ApplicationContext 객체로부터 동적으로 객체 가져오기

String configLocation = "config/spring/app-context.xml"; ApplicationContext context = new ClassPathXmlApplicationContext(configLocation);

이 방법으로 ApplicationContext객체를 건네 받을 수 있으나 좋은 방법은 아니다.

스프링에서는 ApplicationContextAware인터페이스를 제공한다.


public interface ApplicationContextAware {

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;

}

ApplicationContextAware인터페이스에서는 구현해야되는 메소드 하나이다.
public abstract class BaseController implements ApplicationContextAware {

    /**

    * ApplicationContext

    */

    protected ApplicationContext applicationContext;

    @Override

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

        this.applicationContext = applicationContext;

    }

}

Spring annotation-driven 자동으로 등록되는 빈은 ??

<mvc:annotation-driven> 애노테이션 방식의 @MVC를 사용시 필요한 몇가지 빈들을 자동으로 등록해 줍니다. 자동으로 등록되는 빈이 어떤것인지 기억해두고 이를 다시 <bean>태그로 등록하지 않게 해야 합니다. AnnotationMethodHandlerAdapter와 DefaultAnnotationHandlerMapping등의 설정을 변경해야 할 때는 <mvc:annotation-driven>을 사용할 수는 없고 이때는 직접 필요한 빈을 등록하고 프로퍼티를 설정해줘야 합니다. 아래는 자동으로 등록되는 빈들의 내용이고 몰라도 일단 사용상의 문제는 없겠습니다.

1. DefaultAnnotationHandlerMapping 

@RequestMapping을 이용한 핸들러 매핑 전략 등록, 이것이 가장 우선이 되므로 다른 핸들러 매핑은 자동등록 되지 않는다.

2. AnnotationMethodHandlerAdapter

디폴트 핸들러 어댑터, 이것이 가장 우선이 되므로 다른 핸들로 어댑터는 자동등록 되지 않는다.

3. ConfigurableWebBindingInitializer

모든 컨트롤러 메소드에 자동으로 적용되는 WebDataBinder 초기화용 빈을 등록하고 AnnotationMethodhandlerAdapter의 프로퍼티로 연결해 준다. 기본 등록 빈은 FormattingConversionServiceFactoryBean, LocalValidatorFactoryBean 이고 LocalValidatorFactoryBean기능이 적용되려면 JSR-303지원 라이브러리가 클래스패스에 등록되어 있어야 한다.

4. 메시지 컨버터

네개의 디폴트 메시지 컨버터와 Jaxb2RootElementHttpMessageConverter, MappingJacksonHttpMessageConverter가 추가로 등록된다 (JAXB2와 Jackson 라이브러리를 클래스패스에 있어야만 등록됨)

5. <spring:eval>을 위한 컨버전 서비스 노출용 인터셉터

기본적으로 표준 컨버터를 이용해서 모델의 프로퍼티 값을 JSP에 출력할 문자열로 변환 하지만 <mvc:annotation-driven>을 등록해주면 ConfigurableWebBindingInitializer에 등록되는 것과 동일한 컨버전 서비스를 인터셉터를 이용해서 <spring:eval>태그에서 사용할 수 있게 해준다.

6. validator

자동등록되는 ConfigurableWebBindingInitializer의 validator 프로퍼티에 적용할 Validator 타입의 빈을 직접 지정할 수 있다. [code language="text"] // property 설정 [/code] 자동등록되는 ConfigurableWebBindingInitializer의 conversionService 프로퍼티에 설정될 빈을 지정할 수 있다. [code language="text"] .... [/code]
  • <mvc:annotation-driven> 암묵적으로 설정되는 빈
  • 소스코드는org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser.java
[code language="text"] [/code]

Annotations bean scans 설정방법

1. 서블릿 컨텍스트(dispatcher-servlet.xml) 설정




 
서블릿 컨텍스트에서는 @Conponent, @Service, @Repository와 같은 stereoAnnotationType을 제외하고 @Controller로 정의된 클래스빈만 등록 즉, use_default-filters="false" 의미는 stereoType을 제외한다는 의미.

2.루트 컨텍스트(applicationContext.xml) 설정




 
기본패키지에 등록된 stereoAnnotationType의 빈을 모두 등록하되 @Controller빈은 등록에서 제외