Spring 3 MVC HttpMessageConverter 기능으로 RESTful 웹 서비스 빌드

Yi Ming Huang, Software Engineer, IBM

요약:  Java EE(Java™ Platform Enterprise Edition) 애플리케이션을 빌드하기 위해 잘 알려진 프레임워크인 Spring은 이제 모델-뷰-컨트롤러(MVC) 계층에서 REST(Representational State Transfer)를 지원합니다. RESTful 웹 서비스가 클라이언트 요청을 기반으로 하는 여러 표현을 제작하는 것은 중요합니다. 이 기사에서는 HttpMessageConverter로 여러 표현을 제작하는 것을 배웁니다. 코드 샘플은 HttpMessageConverter로 서비스와 통신하기 위해 RestTemplate를 사용하는 방법을 보여줍니다. 또한, ATOM 피드, XML 및 JSON(JavaScript Object Notation)과 같이 대중적인 표현을 작성하는 RESTful 웹 서비스를 빌드하기 위해 Spring API와 어노테이션을 사용하는 방법에 대해서도 학습합니다.

소개

자매 기사인 "Build RESTful web services using Spring 3"(참고자료 참조)에서는 RESTful 웹 서비스를 빌드하는 "Spring 방법"을 소개하였다. 이는 RESTful 웹 서비스의 중요한 기능인 여러 표현을 제작하기 위해 ContentNegotiatingViewResolver를 사용하는 방법도 설명했다. 이 기사에서는 HttpMessageConverter를 사용하여 여러 표현을 제작하는 또다른 방법과 HttpMessageConverter로 서비스와 통신하기 위해 RestTemplate를 사용하는 방법을 보여주는 기사의 예제를 설명한다.


Spring MVC에서 REST 지원

이 섹션에서는 주요 Spring 기능의 개요 또는 RESTful 웹 서비스를 지원하는 어노테이션을 제공한다.

@Controller
@Controller 어노테이션을 사용하여 MVC에서 컨트롤러가 되고 HTTP 요청을 처리할 클래스의 어노테이션을 작성한다.
@RequestMapping
@RequestMapping 어노테이션을 사용하여 특정 HTTP 메소드, URI 또는 HTTP 헤더를 처리해야 하는 함수의 어노테이션을 작성한다. 이 어노테이션은 Spring REST 지원의 키이다. 사용자는 method 매개변수를 변경하여 다른 HTTP 메소드를 처리한다.

예를 들면, 다음과 같다.

@RequestMapping(method=RequestMethod.GET, value="/emps", 
headers="Accept=application/xml, application/json")
                    

@PathVariable
URI에서 경로 변수는 @PathVariable 어노테이션을 사용하는 매개변수로 삽입될 수 있다.

예를 들면, 다음과 같다.

@RequestMapping(method=RequestMethod.GET, value="/emp/{id}")
public ModelAndView getEmployee(@PathVariable String id) { … }
                    

다른 유용한 어노테이션
@RequestParam을 사용하여 URL 매개변수를 메소드로 삽입한다.

@RequestHeader를 사용하여 특정 HTTP 헤더를 메소드로 삽입한다.

@RequestBody를 사용하여 HTTP 요청 본문을 메소드로 삽입한다.

@ResponseBody를 사용하여 HTTP 응답 본문으로 내용이나 오브젝트를 리턴한다.

HttpEntity<T>가 매개변수로 제공되는 경우 이를 사용하여 자동으로 메소드로 삽입한다.

ResponseEntity<T>를 사용하여 사용자 정의 상태나 헤더로 HTTP 응답을 리턴한다.

예를 들면, 다음과 같다.

public @ResponseBody Employee getEmployeeBy(@RequestParam("name") 
String name, @RequestHeader("Accept") String accept, @RequestBody String body) {…} 
public ResponseEntity<String> method(HttpEntity<String> entity) {…}
                    

메소드에 삽입 가능한 지원되는 어노테이션 또는 오브젝트의 전체 목록은 Spring 문서를 참조한다(참고자료 참조).

여러 표현 지원

서로 다른 MIME 유형으로 동일한 자원을 표현하는 것은 RESTful 웹 서비스의 중요한 부분이다. 일반적으로 서로 다른 "accept" HTTP 헤더로 동일한 URI를 사용하여 다른 표현을 사용하는 자원을 페치할 것이다. 또한 다른 URI나 다른 요청 매개변수를 사용하는 URI를 사용할 수 있다.

"Build RESTful web services using Spring 3"(참고자료 참조)에서는 동일한 URI를 처리하기 위해 다른 뷰 리졸버를 취할 수 있는ContentNegotiatingViewResolver를 소개하였다(accept 헤더의 차이 이용). 따라서 여러 표현을 제작하기 위해ContentNegotiatingViewResolver를 사용할 수 있다.

또한 여러 표현을 제작하는 또다른 방법이 있다 — HttpMessageConverter c@ResponseBody 어노테이션의 결합 이용. 이 방법으로는 뷰 기술을 사용하지 않아도 된다.

HttpMessageConverter

HTTP 요청과 응답은 텍스트 기반인데, 이는 브라우저와 서버가 원시 텍스트를 교환하여 통신한다는 의미이다. 하지만 Spring을 사용하면 컨트롤러 클래스의 메소드는 순수 'String' 유형과 도메인 모델(또는 기타 Java 내장 오브젝트)을 리턴한다. 어떻게 Spring이 오브젝트를 원시 텍스트로 직렬하거나 직렬 취소할 수 있는가? 이는 HttpMessageConverter이 처리한다. Spring은 사용자의 일반적인 필요에 맞출 수 있는 구현을 번들했다. 표 1에 몇 가지 예제가 있다.


표 1. HttpMessageConverter 예제
사용 항목가능한 작업
StringHttpMessageConverter요청 및 응답에서부터 문자열 읽기/쓰기. 기본값으로 이는 text/* 매체 유형을 지원하고 text/plain의 내용 유형(Content-Type)으로 쓴다.
FormHttpMessageConverter요청 및 응답에서부터 양식 데이터 읽기/쓰기. 기본값으로 이는 application/x-www-form-urlencoded 매체 유형을 읽고 데이터를 MultiValueMap<String,String>으로 쓴다.
MarshallingHttpMessageConverterSpring의 마샬러/언마샬러를 사용하여 XML 데이터 읽기/쓰기. 이는 application/xml 매체 유형의 데이터를 변환한다.
MappingJacksonHttpMessageConverterJackson의 ObjectMapper를 사용하여 JSON 데이터 읽기/쓰기. 이는 application/json 매체 유형의 데이터를 변환한다.
AtomFeedHttpMessageConverterROME의 피드 API를 사용하여 ATOM 피드 읽기/쓰기. 이는 application/atom+xml 매체 유형의 데이터를 변환한다.
RssChannelHttpMessageConverterROME의 피드 API를 사용하여 RSS 피드 읽기/쓰기. 이는 application/rss+xml 매체 유형의 데이터를 변환한다.

RESTful 웹 서비스 빌드

이 섹션에서는 여러 표현을 제작할 수 있는 간단한 RESTful 웹 서비스를 빌드하는 것을 학습한다. 샘플에 사용된 일부 자원은 "Build RESTful web services using Spring 3"(참고자료 참조)에서 빌드되었다. 또한 샘플 코드도 다운로드할 수 있다.

먼저 HttpMessageConverter를 구성해야 한다. 여러 표현을 제작하려면 몇 가지 HttpMessageConverter 인스턴스를 사용자 정의하여 오브젝트를 서로 다른 매체 유형으로 변환한다. 이 섹션에서는 JSON, ATOM 및 XML 매체 유형을 다룬다.

JSON

가장 간단한 예제로 시작하자. JSON은 경량 데이터 상호 교환 형식으로 사람이 읽고 쓰기에 쉽다. 목록 1에서는 JSON 변환기를 구성하는 코드를 보여준다.


목록 1. rest-servlet.xml에서 HttpMessageConverter 구성
<bean class="org.springframework.web.servlet.mvc.annotation
.AnnotationMethodHandlerAdapter">
   <property name="messageConverters">
       <list>
           <ref bean="jsonConverter" />
   <ref bean="marshallingConverter" />
   <ref bean="atomConverter" />
       </list>
   </property>
</bean>

<bean id="jsonConverter" 
            class="org.springframework.http.converter.json
.MappingJacksonHttpMessageConverter">
   <property name="supportedMediaTypes" value="application/json" />
</bean>

구성에서 세 가지 변환기가 등록된다. MappingJacksonHttpMessageConverter는 오브젝트를 JSON으로 변환하고 그 반대의 경우에도 사용된다. 이 내장 변환기는 Jackson의 ObjectMapper를 사용하여 JSON을 JavaBean으로 맵핑한다. 따라서 다음 Jackson JAR 파일을 클래스경로에 추가해야 한다.

  • org.codehaus.jackson.jar
  • org.codehaus.jackson.mapper.jar

다음 단계는 JSON 표현을 요구하는 요청을 처리하는 메소드를 쓰는 것이다. 목록 2에서 세부사항을 보여준다.


목록 2. EmployeeController에서 정의된 JSON 요청 처리
                
@RequestMapping(method=RequestMethod.GET, value="/emp/{id}", 
		headers="Accept=application/json")
public @ResponseBody Employee getEmp(@PathVariable String id) {
Employee e = employeeDS.get(Long.parseLong(id));
return e;
}
	
@RequestMapping(method=RequestMethod.GET, value="/emps", 
		headers="Accept=application/json")
public @ResponseBody EmployeeListinggetAllEmp() {
List<Employee> employees = employeeDS.getAll();
EmployeeListinglist = new EmployeeList(employees);
return list;
}
            

@ResponseBody 어노테이션은 리턴 오브젝트(Employee 또는 EmployeeList)를 응답 본문 내용으로 만드는 데 사용되고, 이로 인해MappingJacksonHttpMessageConverter 가 JSON으로 맵핑될 것이다.

HttpMessageConverter @ResponseBody를 사용하여 Spring의 뷰 기술을 포함시키지 않고, 여러 표현을 구현할 수 있다 --ContentNegotiatingViewResolver의 사용을 능가하는 장점.

이제 CURL 또는 REST 클라이언트 Firefox 플러그인을 사용하여 요청을 호출할 수 있다. HTTP 헤더인 Accept=application/json를 추가시키는 것을 유의하자. 목록 3에는 JSON 형식에서 원하는 응답을 보여준다.


목록 3. getEmp() 및 getAllEmp()에 대한 JSON 결과

Response for /rest/service/emp/1
{"id":1,"name":"Huang Yi Ming","email":"huangyim@cn.ibm.com"}

Response for /rest/service/emps
{"count":2,
"employees":[
{"id":1,"name":"Huang Yi Ming","email":"huangyim@cn.ibm.com"},
{"id":2,"name":"Wu Dong Fei","email":"wudongf@cn.ibm.com"}
]}
            

XML

Spring의 내장 변환기인 MarshallingHttpMessageConverter는 오브젝트와 XML(OXM) 사이에 맵핑하는 데 사용된다. 예제는 변환기에 대해 마샬러/언마샬러로 JAXB 2를 사용한다. 목록 4는 구성을 보여준다.


목록 4. MarshallingHttpMessageConverter 구성
                
<bean id="marshallingConverter" 
class="org.springframework.http.converter.xml
		.MarshallingHttpMessageConverter">
<constructor-arg ref="jaxbMarshaller" />
    <property name="supportedMediaTypes" value="application/xml"/>
      </bean>

      <bean id="jaxbMarshaller" 
      class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
      
    <property name="classesToBeBound">
	  <list>
	    <value>dw.spring3.rest.bean.Employee</value>
	    <value>dw.spring3.rest.bean.EmployeeList</value>
	  </list>
    </property>
    
</bean>
            

JAXB 2에는 java.util.List<T>를 XML로 맵핑하는 훌륭한 지원이 없다는 것을 이해하는 것이 중요하다. 일반적인 실제 사례는 오브젝트의 콜렉션에 대해 랩퍼 클래스를 추가하는 것이다. "Build RESTful web services using Spring 3"(참고자료 참조)을 참조하거나 이 JAXB 어노테이트된 클래스의 세부사항에 대한 소스 코드를 다운로드하자.

요청을 처리하는 컨트롤러에서 메소드는 어떠한가? 목록 2의 코드를 살펴보자. 당연히 여기에 코드를 추가하지 않아도 된다는 사실을 알게 된다. 다음과 같이 Accept 헤더에 지원되는 또다른 매체 유형을 추가하기만 하면 된다.

headers=”Accept=application/json, application/xml”
            

변환기는 요청한 유형(JSON 또는 XML)에 적합하게 오브젝트를 맵핑할 것이다. 목록 5에는 application/xml 표현을 요청할 때에 원하는 결과를 보여준다.


목록 5. getEmp() 및 getAllEmp()에 대한 XML 결과
                     
Response for /rest/service/emp/1
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <employee>
   <email>huangyim@cn.ibm.com</email>
   <id>1</id>
   <name>Huang Yi Ming</name>
 </employee>
Response for /rest/service/emps
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  <employees>
  <count>2</count>
    <employee>
      <email>huangyim@cn.ibm.com</email>
      <id>1</id>
      <name>Huang Yi Ming</name>
    </employee>
    <employee>
      <email>wudongf@cn.ibm.com</email>
      <id>2</id><name>Wu Dong Fei</name>
    </employee>
 </employees>
            

ATOM 피드

ATOM 피드는 RESTful 웹 서비스에서 데이터를 교환하기 위한 또다른 대중적인 형식이다. Atom 피드 문서는 피드에 대한 메타데이터와 이와 연관된 일부 또는 모든 항목이 포함된 Atom 피드의 표현이다. 그 루트는 atom:feed 요소이다. 또한 교환 형식 및 작동을 정의하는 ATOM Publish Protocol(APP)도 있다. (ATOM 및 APP 형식을 정의하는 것은 이 기사의 범위를 벗어난다. 자세한 정보는 참고자료를 참조한다.)

예제는 AtomFeedHttpMessageConverter를 사용하여 ROME ATOM API를 활용하는 ATOM 피드를 변환한다. 따라서 JAR 파일인 sun.syndication.jar을 클래스경로에 포함시켜야 한다. 목록 6에는 이 변환기의 구성을 보여준다.


목록 6. AtomFeedHttpMessageConverter 구성
                
<bean id="atomConverter" 
class="org.springframework.http.converter.feed
		.AtomFeedHttpMessageConverter">
<property name="supportedMediaTypes" value="application/atom+xml" />
</bean>
            

목록 7에는 ATOM 요청과 피드 생성을 처리하는 코드를 보여준다.


목록 7. EmployeeController & AtomUtil 클래스에서 getEmpFeed()
                
@RequestMapping(method=RequestMethod.GET, value="/emps", 
		headers="Accept=application/atom+xml")
public @ResponseBody Feed getEmpFeed() {
	List<Employee> employees = employeeDS.getAll();
	return AtomUtil.employeeFeed(employees, jaxb2Mashaller);
}

public static Feed employeeFeed(
	List<Employee> employees, Jaxb2Marshaller marshaller) {
Feed feed = new Feed();
feed.setFeedType("atom_1.0");
feed.setTitle("Employee Atom Feed");
		
List<Entry> entries = new ArrayList<Entry>();
for(Employee e : employees) {
	StreamResult result = new StreamResult(
	new ByteArrayOutputStream());
	marshaller.marshal(e, result);
	String xml = result.getOutputStream().toString();
			
	Entry entry = new Entry();
	entry.setId(Long.valueOf(e.getId()).toString());
	entry.setTitle(e.getName());
	Content content = new Content();
	content.setType(Content.XML);
	content.setValue(xml);
	
	List<Content> contents = new ArrayList<Content>();
	contents.add(content);
	entry.setContents(contents);
	entries.add(entry);
}
feed.setEntries(entries);
return feed;
}
            

상기 코드에서는 다음에 주목한다.

  • getEmpFeed() 메소드는 getAllEmp()와 동일한 URI를 처리하지만, Accept 헤더가 다르다.
  • employeeFeed() 메소드를 통해 Employee 오브젝트를 XML로 마샬한 다음에 이를 피드 항목의 <content> 요소로 추가한다.

목록 8에는 URI /rest/service/emps에 대한 application/atom+xml 표현을 요청할 때에 결과물을 보여준다.


목록 8. application/atom+xml을 요청할 때에 /rest/service/emps에 대한 결과물
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Employee Atom Feed</title>

 <entry>
    <title>Huang Yi Ming</title>
    <id>1</id>
  <content type="xml">
    <employee>
            <email>huangyim@cn.ibm.com</email>
            <id>1</id>
            <name>Huang Yi Ming</name>
    </employee>
  </content>
</entry>

  <entry>
    <title>Wu Dong Fei</title>
    <id>2</id>
  <content type="xml">
    <employee>
            <email>wudongf@cn.ibm.com</email>
            <id>2</id>
            <name>Wu Dong Fei</name>
     </employee>
   </content>
 </entry>
 
</feed>
 

POST, PUT 및 DELETE 구현

지금까지 예제에서 HTTP GET 메소드를 처리하기 위한 몇 가지 메소드를 구현했다. 목록 9에는 POST, PUT  DELETE 메소드의 구현을 보여준다.


목록 9. EmployeeController에서 POST, PUT 및 DELETE 메소드
@RequestMapping(method=RequestMethod.POST, value="/emp")
public @ResponseBody Employee addEmp(@RequestBody Employee e) {
employeeDS.add(e);
return e;
}
	
@RequestMapping(method=RequestMethod.PUT, value="/emp/{id}")
public @ResponseBody Employee updateEmp(
	@RequestBody Employee e, @PathVariable String id) {
employeeDS.update(e);
return e;
}
	
@RequestMapping(method=RequestMethod.DELETE, value="/emp/{id}")
public @ResponseBody void removeEmp(@PathVariable String id) {
employeeDS.remove(Long.parseLong(id));
}
            

@RequestBody 어노테이션은 addEmp()  updateEmp() 메소드에 사용된다. 이는 HTTP 요청 본문을 취하고 등록된HttpMessageConverter를 사용하여 오브젝트 클래스로 변환하려고 시도한다. 다음 섹션에서 이러한 서비스와 통신하는 RestTemplate를 사용할 것이다.

RestTemplate를 사용하여 REST 서비스와 통신

"Build RESTful web services using Spring 3"(참고자료 참조)은 REST 서비스를 테스트하기 위해 CURL 및 REST 클라이언트를 사용하는 방법에 대해 소개했다. 프로그래밍 레벨에서 Jakarta Commons의 HttpClient는 이를 수행하기 위해 일반적으로 사용된다(하지만 이는 이 기사의 범위를 벗어난다). RestTemplate라는 Spring REST 클라이언트도 사용할 수 있다. 이는 JdbcTemplate  JmsTemplate와 같이 Spring에서 다른 템플리트 클래스와 개념적으로 유사하다.

RestTemplate HttpMessageConverter도 사용한다. 요청에서 오브젝트 클래스를 전달할 수 있고 변환기가 맵핑을 처리하도록 할 수 있다.

RestTemplate 구성하기

목록 10에는 RestTemplate의 구성을 보여준다. 이는 이전에 소개된 세 가지 변환기도 사용한다.


목록 10. RestTemplate 구성
<bean id="restTemplate" 
class="org.springframework.web.client.RestTemplate">
<property name="messageConverters">
	<list>
	<ref bean="marshallingConverter" />
	<ref bean="atomConverter"  />
	<ref bean="jsonConverter" />
	</list>
</property>
</bean>
            

이 기사의 예제는 서버 사이에 통신을 간소화할 수 있는 일부 메소드만 사용했다. RestTemplate는 다음을 비롯한 다른 메소드를 지원한다.

  • exchange: 요청 본문으로 특정 HTTP 메소드를 실행하고 응답을 받는다.
  • getForObject: HTTP GET 메소드를 실행하고 오브젝트로서 응답을 받는다.
  • postForObject:특정 요청 본문으로 HTTP POST 메소드를 실행한다.
  • put: 특정 요청 본문으로 HTTP PUT 메소드를 실행한다.
  • delete: 특정 URI를 위한 HTTP DELETE 메소드를 실행한다.

코드 샘플

다음 코드 샘플은 RestTemplate를 사용하는 방법을 설명하는 데 유용하다. 사용된 API의 자세한 설명은 RestTemplate API(참고자료 참조)를 참조한다.

목록 11에는 요청에 헤더를 추가한 다음에 그 요청을 호출하는 방법을 보여준다. MarshallingHttpMessageConverter를 사용하면 응답을 받고 이를 유형화된 클래스로 변환할 수 있다. 다른 매체 유형을 사용하여 다른 표현을 테스트할 수 있다.


목록 11. XML 표현에 대한 요청
                
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
HttpEntity<String> entity = new HttpEntity<String>(headers);
ResponseEntity<EmployeeList> response = restTemplate.exchange(
"http://localhost:8080/rest/service/emps", 
HttpMethod.GET, entity, EmployeeList.class);
EmployeeListingemployees = response.getBody();
// handle the employees
            

목록 12에는 새 직원을 서버로 POST하는 방법을 보여준다. 서버 측 서비스인 addEmp()는 application/xml과 application/json의 매체 유형으로 데이터를 수락할 수 있다.


목록 12. 새 직원 POST
                
Employee newEmp = new Employee(99, "guest", "guest@ibm.com");
HttpEntity<Employee> entity = new HttpEntity<Employee>(newEmp);
ResponseEntity<Employee> response = restTemplate.postForEntity(
"http://localhost:8080/rest/service/emp", entity, Employee.class);
Employee e = response.getBody();
// handle the employee
            

목록 13에는 원래 직원을 업데이트하기 위해 수정된 직원을 PUT하는 방법을 보여준다. 이는 또한 요청 URI에서 플레이스홀더({id})로 사용할 수 있는 기능을 보여준다.


목록 13. 직원을 업데이트하기 위한 PUT
                
Employee newEmp = new Employee(99, "guest99", "guest99@ibm.com");
HttpEntity<Employee> entity = new HttpEntity<Employee>(newEmp);
restTemplate.put(
	"http://localhost:8080/rest/service/emp/{id}", entity, "99");

목록 14에는 기존 직원을 DELETE하는 방법을 보여준다.


목록 14. 기존 직원 DELETE
restTemplate.delete(
	"http://localhost:8080/rest/service/emp/{id}", "99");

요약

이 기사에서는 Spring 3에서 소개된 HttpMessageConverter에 대해 학습했다. 이는 여러 표현에 대해 클라이언트와 서버측 지원 모두를 제공한다. 제공된 소스 코드를 사용하면, 이 기사에서 HttpMessageConverter의 구현과 "Build RESTful web services using Spring 3"에서 ContentNegotiatingViewResolver를 사용하는 구현 사이에 차이점을 살펴볼 수 있다.


다운로드 하십시오

설명이름크기다운로드 방식
Article source codesrc_code.zip11KBHTTP

다운로드 방식에 대한 정보


참고자료

교육

  • 자매 기사인 "Build RESTful web services using Spring 3"(developerWorks, 2010년 7월)에서는 RESTful 웹 서비스를 빌드하는 "Spring 방법"을 소개하였다. 

  • 위키피디아에서 REST와 관련 링크에 대한 소개를 확인하자. 

  • Spring MVC showcase를 살펴보고 이 기술로 가능한 작업에 대한 아이디어를 얻자. 여기에는 샘플 프로젝트와 함께 지원하는 슬라이드 프리젠테이션과 스크린캐스트가 들어있다. 

  • Spring 3의 모든 것에 대해 알아보자. 

  • RestTemplate APIs에 대해 자세히 알아보자. 

  • JAXB Reference Implementation Project에 대해 모두 읽어보자. 

  • 대부분 신디케이션 형식으로 Java에서 쉽게 작업할 수 있는 Atom/RSS Java 유틸리티 세트인 ROME에 대해 살펴보자. 

  • ATOM에 대해 자세히 읽어보자. 

  • 프로토콜과 그 기본 조작 및 기능에 대한 높은 수준의 개요는 developerWorks 시리즈인 "Getting to know the Atom Publishing Protocol"(2006년 10월)을 읽어보자. 

  • 빠른 오픈 소스 JSON 프로세서인 Jackson에 대해 알아보자.

제품 및 기술 얻기

토론

필자소개

Yi Ming Huang은 소프트웨어 엔지니어로 China Development Lab에서 Lotus ActiveInsight를 담당하고 있다. 그는 Portlet/Widget 관련 웹 개발에 참여했으며 REST, OSGi 및 Spring 기술에 관심을 갖고 있다.


출처 - http://www.ibm.com/developerworks/kr/library/wa-restful/index.html






45.Spring REST Supports

Spring 3에서는 RESTful 웹 서비스와 웹 어플리케이션 개발을 지원하기 위해서 새로운 기능들을 선보이고 있다.

45.1.URI Template

REST 아키텍처에서 가장 기본적인 개념은 바로 모든 리소스에 ID를 부여한다는 것이다. 웹 기반의 어플리케이션에서 리소스는 어플리케이션에서 제공하는 서비스가 될 것이고, 서버에 존재하는 수많은 서비스들을 식별하기 위한 ID는 URI가 될 것이므로 RESTful URI는 다음과 같은 사항들을 고려한 설계가 필요하다.

  • URI path가 계층 구조를 이루도록 설계

  • 상위 path는 하위 path의 collection을 의미하도록 설계

예를 들면, '/movies/MV-00001'의 경우 'movies'와 movieId 값인 'MV-00001'은 계층 구조를 가지고, 상위의 'movies'는 하위 movieId의 collection을 의미하는 형태로 이루어진 RESTful URI이다.

URI Template은 '/movies/{movieId}'와 같이 하나 이상의 변수를 포함하고 있는 URI 형식의 문자열로, RESTful URI를 쉽게 만들고 관리할 수 있게 해준다.

DispatcherServlet URL 매핑

기존에 Spring MVC를 기반으로 개발된 웹 어플리케이션에서는 'xxx.do'라는 형태의 URL을 사용했지만, 위에서 설명했듯이 REST 스타일의 URL은 '/movies', '/movies/MV-00001/edit' 처럼 계층 구조로 사용가능하도록 설계되었다. 따라서 web.xml에 DispatcherServlet을 정의하고 매핑할 URL 패턴을 '/'로 지정해야한다. 이럴 경우 css 나 이미지 등 static 리소스 URL도 DispatcherServlet을 통하게 되어 화면이 정상적으로 동작하지 않는 문제가 있다. 그래서 Spring에서는 URLRewriteFilter라는 것을 이용하여 DispatcherServlet이 처리해야할 URL을 분리하고 있다.

그러나 Restweb Plugin의 경우 Foundation Plugin 등 다른 Plugine들과 함께 섞여서 동작해야하기 때문에 URLRewriteFilter를 사용하지 않고, 기존에 정의된 DispatcherServlet에 아래와 같이 매핑만 추가하도록 설정했다.

<servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>/restweb/*</url-pattern>
</servlet-mapping>

URLRewriteFilter는 Apache 웹서버의 mod_rewrite와 같은 기능을 하는 것으로, Resin, Orion, Tomcat 등 어떤 J2EE 호환 웹 애플리케이션 서버에서도 사용할 수 있는 자바 웹 필터이다. 자세한 내용은URLRewriteFilter 웹사이트를 참조하기 바란다.

45.1.1.@PathVariable

Spring 3부터 RESTful URI처리를 위해 @RequestMapping이 URI Template을 지원하도록 기능을 추가하였고, URI Template에 포함된 변수 값을 추출할 수 있도록 @PathVariable이라는 새로운 Annotation을 추가했다.

다음은 @PathVariable을 사용한 예이다.

@RequestMapping(value = "/movies/{movieId}/edit", method = RequestMethod.GET)
public String get(@PathVariable String movieId, Model model)
      throws Exception {
    Movie movie = this.movieService.get(movieId);
    // 중략
    return "restwebViewMovie";
}
'/movies/MV-00001'와 같은 URI로 요청이 들어왔을 때, 위의 get 메소드가 처리하게 되고 'MV-00001' 값은 'movieId' 입력 Argument에 바인딩된다.

아래와 같이 변수명을 지정하여 사용하거나 여러개의 변수를 사용할 수도 있다.

@RequestMapping(value = "/movies/{movie}/posters/{poster}", method = RequestMethod.GET)
public String get(@PathVariable("movie") String movieId, @PathVariable("poster") String posterId, Model model)
      throws Exception {
    Movie movie = this.movieService.get(movieId);
    // 중략
    return "restwebViewMovie";
}

'/movies/*/posters/{posterId}'와 같이 Ant-style의 경로에도 사용할 수 있고, URI Template의 변수를 String이 아닌 다른 타입의 입력 Argument로도 바인딩 가능하다.

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

@RequestMapping("/plans/{date}")
public void get(@PathVariable Date date) {
    // 중략
}
예로 '/plans/2010-09-05' URI로 들어온 요청은 위의 메소드가 처리할 것이고, '2010-09-05'는 date 입력 Argument에 Date 타입으로 바인딩 될 것이다.

45.2.Content Negotiation

RESTful 아키텍처에서 하나의 리소스는 여러 개의 Represenation을 가질 수 있다. 즉 서버에서 제공하는 하나의 서비스는 여러가지의 View로 보여질 수 있다는 것이다. 클라이언트가 요청을 전달할 때 HTTP Header 중 Accept라는 Header 값을 이용해서 원하는 응답의 형태를 명시하면, 서버에서는 요청을 처리한 후 클라이언트가 원한 Representation으로 결과를 전달한다. 이러한 처리 과정을 Content Negotiation이라고 한다.

여기서 한 가지 문제점은 HTML의 경우 Accept Header 값을 웹 브라우저가 고정하여 전송하기 때문에, Accept Header 값을 기반으로 한 Content Negotiation이 불가능하다는 것이다. 그래서 이에 대한 대안으로 URL path에 확장자를 붙여, 확장자를 통해 클라이언트가 원하는 Representation을 표시하는 방법을 사용한다. 예를 들어, 'http://localhost:8080/myapp/movies.pdf' 라는 요청이 들어오면 서버는 영화목록을 찾아서 PDF View로 클라이언트에게 전달하는 것이다.

지금까지 설명한 기능을 지원하기 위해서 Spring 3에서 추가한 것이 바로 ContentNegotiatingViewResolver이다.

45.2.1.ContentNegotiatingViewResolver

ContentNegotiatingViewResolver는 자기가 직접 View를 구성하는 것이 아니라, 등록된 다른 모든 View Resolver에게로 View를 찾는 것을 위임한다. 다른 View Resolver들이 리턴한 View의 Content-Type과 HTTP Request의 Accept 헤더 값 또는 파일 확장자로 기술된 미디어 타입(Content-Type값)을 비교하여 클라이언트가 요청한 Content-Type에 가장 적합한 View를 선택하여 응답을 돌려준다.

이렇듯 ContentNegotiatingViewResolver는 다른 View Resolver들과 반드시 함께 사용되어야 하므로 View Resolver 설정 시 반드시 order를 정의해야 한다. 당연히 ContentNegotiatingViewResolver가 가장 높은 우선순위(가장 작은숫자)를 가져야 한다.

파일 확장자 기반의 Content Negotiation을 처리하기 위해서는 ContentNegotiatingViewResolver의 mediaTypes 속성에 파일 확장자와 미디어 타입을 매핑시켜 정의한다.

다음은 ContentNegotiatingViewResolver를 사용하기 위한 설정 예이다.

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <property name="mediaTypes">
        <map>
            <entry key="html" value="text/html"/>
            <entry key="xml" value="application/xml" />
        </map>
    </property>
    <property name="order" value="0"/>
</bean>

ContentNegotiatingViewResolver는 기본적으로 WebApplicationContext에 등록된 View Resolver들을 자동으로 찾아서 처리하지만, 아래와 같이 viewResolvers 속성을 이용해서 다른 View Resolver들을 명시적으로 지정할 수도 있다. 또한, defaultViews 속성에 View를 명시해두면, View Resolver 체인에서 클라이언트가 요청한 Content-Type을 지원하는 View를 찾지 못한 경우에 디폴트 View로 사용된다.

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <property name="mediaTypes">
        <map>
            <entry key="html" value="text/html"/>
            <entry key="atom" value="application/atom+xml"/>
            <entry key="json" value="application/json"/>
        </map>
    </property>
  
     <property name="viewResolvers">
        <list>
            <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
            <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                <property name="prefix" value="/WEB-INF/jsp/"/>
                <property name="suffix" value=".jsp"/>
            </bean>
        </list>
    </property>
    
    <property name="defaultViews">
        <list>
            <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView"/>
        </list>
    </property>
</bean>

요청 처리후 돌려줄 적절한 View를 선택하기 위해서, ContentNegotiatingViewResolver는 클라이언트로부터 요청된 미디어 타입을 가지고 매칭시키는데, 이 미디어 타입을 알아내는 작업은 다음과 같은 과정으로 이루어진다.

  1. favorPathExtension 속성 값이 true(디폴트값이 true이다)이고, Request path에 파일 확장자가 포함되어 있다면, ContentNegotiatingViewResolver의 mediaTypes 속성에 정의된 매핑 정보를 사용한다. 적절한 미디어 타입을 찾지 못했을 때, 만약 Java Activation Framework가 classpath에 존재한다면, FileTypeMap.getContentType(String filename) 메소드의 리턴 값을 미디어타입으로 사용한다.

  2. favorParameter 속성 값이 true(디폴트값은 false이다)이고, Request에 미디어 타입을 정의하는 파라미터가 포함되어 있다면, ContentNegotiatingViewResolver의 mediaTypes 속성에 정의된 매핑 정보를 사용한다. 디폴트 파라미터 명은 'format'이고 이것은 parameterName이라는 속성으로 변경가능하다.

  3. 위의 과정으로도 미디어 타입을 찾지 못했을 때, ContentNegotiatingViewResolver의 ignoreAcceptHeader가 false로 지정되어 있으면 Request의 Accept 헤더 값을 사용한다.

  4. 위의 모든 과정을 거치고도 미디어 타입을 찾지 못한 경우, 최종적으로 ContentNegotiatingViewResolver의 defaultContentType이 정의되어 있다면 그 값을 클라이언트에서 요청한 미디어 타입으로 간주한다.

일단 클라이언트가 요청한 미디어 타입을 찾아내면 다른 View Resolver들에게 View를 요청하고, View Resolver들이 리턴한 View의 Content-Type과 요청들어온 미디어 타입의 매칭여부를 확인해서 가장 적합한 View를 찾아서 클라이언트로 응답한다.

45.3.Views

Spring 3에서는 REST Style의 웹 어플리케이션에서 하나의 리소스, 즉 하나의 서비스에 대한 여러 형태의 응답을 지원하기 위해 다음과 같은 새로운 View들을 추가했다.

  • AbstractAtomFeedView / AbstractRssFeedView : Atom이나 RSS 피드를 보여줄 수 있는 View

    AbstractAtomFeedView와 AbstractRssFeedView는 AbstractFeedView의 하위클래스로 java.net의 ROME 프로젝트를 기반으로 만들어져있다. Feed View를 구성하려면 AbstractAtomFeedView나 AbstractRssFeedView를 상속받은 클래스에서 각각에서 오버라이드 요구하는 메소드를 구현하여 사용한다.

    public class SampleContentAtomView extends AbstractAtomFeedView {
        @Override
        protected List<Entry> buildFeedEntries(Map<String, Object> model, 
                                                   HttpServletRequest request, HttpServletResponse response) throws Exception {
            // 중략
        }
    }
    public class SampleContentRssView extends AbstractRssFeedView {
        @Override
        protected List<Item> buildFeedItems(Map<String, Object> model, 
                                                HttpServletRequest request, HttpServletResponse response) throws Exception {
            // 중략
        }
    }

    구현한 Feed View를 사용하기 위해서 Bean 정의 파일을 작성해야한다.

    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="mediaTypes">
            <map>
                <entry key="atom" value="application/atom+xml"/>
                <entry key="html" value="text/html"/>
            </map>
        </property>
        <property name="viewResolvers">
            <list>
                <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
                <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                    <property name="prefix" value="/WEB-INF/jsp/"/>
                    <property name="suffix" value=".jsp"/>
                </bean>
            </list>
        </property>
    </bean>
    
    <bean id="movies" class="anyframe.sample.moviefinder.feed.MoviesAtomView"/>

  • MarshallingView : XML로 응답을 전달할 수 있는 View

    MarshallingView는 클라이언트에게 XML 응답을 돌려주기 위해서 Spring OXM의 Marshaller를 사용한다. 기본적으로 컨트롤러가 리턴한 모든 Model을 XML로 변환하지만, modelKey라는 속성에 Model의 이름을 지정함으로써 Marshalling되어 클라이언트로 전달될 Model을 필터링 할 수 있다.

    다음은 Restweb Plugin 설치로 추가된 restweb-servlet.xml의 일부이다. 먼저 설치된 Foundation Plugin에서 정의한 View Resolver들이 있으므로 MarshallingView를 위해서 BeanNameViewResolver만 추가하였다.

    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
            <property name="mediaTypes">
                <map>
                    <entry key="html" value="text/html" />
                    <entry key="xml" value="application/xml" />
                </map>
            </property>
            <property name="order" value="0" />
        </bean>
    
        <bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
            <property name="order" value="1" />
        </bean>
    
        <bean id="restwebViewMovie" class="org.springframework.web.servlet.view.xml.MarshallingView">
            <property name="marshaller" ref="marshaller" />
        </bean>
    
        <oxm:jaxb2-marshaller id="marshaller">
            <oxm:class-to-be-bound name="myapp.restweb.domain.Movie" />
        </oxm:jaxb2-marshaller>

  • MappingJacksonJsonView : JSON으로 응답을 전달할 수 있는 View

    MappingJacksonJsonView는 클라이언트에게 JSON 응답을 돌려주기 위해서 Jackson 라이브러리의 ObjectMapper를 사용한다. 디폴트로 Model 객체 모두의 내용을 JSON으로 보내도록 되어있지만, renderedAttributes 속성을 이용해서 JSON으로 변환할 Model을 필터링 할 수 있다. ObjectMapper 확장이 필요한 경우 objectMapper 속성을 이용해서 확장한 ObjectMapper를 정의해 준다.

    MappingJacksonJsonView는 Anyframe의 simpleweb-json Plugin에 적용되어 있으므로, 사용 예는 본 매뉴얼 Simpleweb Plugin의 JSON View 설정을 참조하기 바란다.

45.4.OXM (Object/XML Mapping)

OXM은 Spring에서 Object와 XML간의 변환을 위해서 JAXB, Castor, JiBX 같은 XML Marshalling 기술을 추상화한 기능으로 원래는 Spring Web Service 프로젝트에 포함되어 있던 모듈이 분리되어 Spring 3에서 Core 영역에 포함되었다. REST Feature 범위는 아니지만 MarshallingView 및 MarshallingHttpMessageConverter와 연관지어 이 장에서 설명하도록 하겠다.

Spring OXM은 다음과 같은 특징을 가진다.

  • 간편한 설정

    Marshaller를 일반 빈과 동일하게 정의한다. 또한 'oxm' 네임스페이스를 제공하여 JAXB2, XmlBeans, JiBX 등을 사용한 Marshaller를 손쉽게 정의할 수 있게 해준다.

  • 일관된 인터페이스

    Marshaller/Unmarshaller라는 두가지 인터페이스로 동작하기 때문에 OX Mapping Framework를 설정만으로 쉽게 변경할 수 있다. 또한 OX Mapping Framework을 섞어서(mix and match) 사용할 수도 있다.

  • 일관된 예외 계층

    Mapping(Serialization)하다 발생한 Exception 처리를 위해서 XmlMappingException이라는 Root Exception을 제공한다.

Spring OXM에서 Marshaller와 Unmarshaller 인터페이스는 구분되어 있지만 Spring에서 제공하고 있는 실제 구현체들은 하나의 클래스에서 두 개의 인터페이스 모두를 구현해서 제공하고 있다. 그래서 구현클래스 하나만 Bean으로 등록하면 Marshaller로 사용할 수도 있고 Unmarshaller로 사용할 수도 있다.

45.4.1.Programmatic Using

XML 변환을 위한 Marshaller는 아래와 같이 Bean으로 정의한 다음 클래스에서 Injection 받아서 사용할 수 있다. 예제에서는 Castor를 사용하고 있지만, JAXB, XMLBeans, JiBX, XStream 등도 Marshaller로 사용할 수 있다. 앞서 언급했듯이 CastorMarshaller는 Marshaller와 Unmarshaller 인터페이스를 모두 구현하였기 때문에 두 가지 용도로 참조할 수 있다.

<beans>
    <bean id="sample" class="SampleClass">
        <property name="marshaller" ref="castorMarshaller" />
        <property name="unmarshaller" ref="castorMarshaller" />
    </bean>
    
    <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller"/>
</beans>

다음은 클래스에서 Marshaller를 사용하는 예제이다.

public class SampleClass {
    @Inject
    private Marshaller marshaller;
    @Inject
    private Unmarshaller unmarshaller;
    
    // 중략
    public void save() throws IOException {
        FileOutputStream os = null;
        try {
            os = new FileOutputStream(FILE_NAME);
            this.marshaller.marshal(movies, new StreamResult(os));
        } finally {
            if (os != null) {
                os.close();
            }
        }
    }
    // 중략
}

45.4.2.Declarative Using

Spring에서 제공하는 'oxm' namespace를 이용하면 Marshaller 설정을 간편하게 추가할 수 있다.이를 위해서는 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"
    xmlns:oxm="http://www.springframework.org/schema/oxm"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/oxm
    http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd">

현재 제공하고 있는 태그들은 다음과 같다.

상세한 설정 방법은 각각의 Marshaller 설명에서 더 자세히 살펴보도록 하겠다.

45.4.3.JAXB

JAXB는 W3C XML 스키마를 지원하는 Object/XML 매핑 프레임워크로 Spring에서는 JAXB 2.0 API를 사용한 Jaxb2Marshaller를 제공하고 있다.

Jaxb2Marshaller를 사용하기 위한 설정은 다음과 같다.

<beans>
    <bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        <property name="classesToBeBound">
            <list>
                <value>myapp.restweb.domain.Movie</value>                
            </list>
        </property>
    </bean>
</beans>
스키마 Validation이 필요한 경우 'schema' 속성을 추가하여 스키마 파일을 지정해 줄 수 있다.

'oxm' namespace를 이용해서 아래와 같이 간편하게 설정할 수도 있다.

<oxm:jaxb2-marshaller id="marshaller" contextPath="myapp.restweb.domain"/>
다음은 Restweb Plugin의 src/test/resources/context-restclient.xml파일의 일부이다. <oxm:class-to-be-bound>를 이용하여 변환할 클래스 목록을 정의하였다.
<oxm:jaxb2-marshaller id="marshaller">
    <oxm:class-to-be-bound name="myapp.restweb.domain.Movie"/>
</oxm:jaxb2-marshaller>

45.4.4.Castor

Castor는 오픈 소스 XML 바인딩 프레임워크로, Java 객체와 XML간의 변환에 대해서 Castor에서 사용하는 디폴트 규칙을 그대로 따른다면 Spring에서는 제공하는 CastorMarshaller를 추가 설정 없이 간단하게 Bean으로 정의할 수 있다.

CastorMarshaller를 사용하기 위한 설정은 다음과 같다.

<beans>
    <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller" />
</beans>
Castor의 디폴트 변환 양식을 변경하고자 하는 경우 Castor 매핑 파일을 작성하여 아래 예와 같이 mappingLocation 속성으로 정의해준다. Castor 매핑 파일을 작성방법에 대해서는 Castor XML Mapping을 참조한다.
<beans>
    <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller">
        <property name="mappingLocation" value="classpath:mapping.xml" />
    </bean>
</beans>

45.4.5.XMLBeans

XMLBeans는 Full XML 스키마를 지원하는 XML 바인딩 프레임워크로, 자세한 내용은 XMLBeans 웹사이트를 참조하기 바란다. Spring에서 제공하는 Marshaller/Unmarshaller 구현체는 XmlBeansMarshaller이다.

XmlBeansMarshaller를 사용하기 위한 설정은 다음과 같다.

<beans>
    <bean id="xmlBeansMarshaller" class="org.springframework.oxm.xmlbeans.XmlBeansMarshaller" />
</beans>
단, XmlBeansMarshaller는 모든 java.lang.Object가 아닌 XmlObject 타입의 객체만 변환할 수 있다는 것을 주의해야한다.

'oxm' namespace를 이용해서 아래와 같이 간편하게 설정할 수도 있다.

<oxm:xmlbeans-marshaller id="marshaller"/>

45.4.6.JiBX

JiBX는 XML 데이터를 Java 오브젝트에 바인딩하는 데 사용되는 도구로, 자세한 내용은 JiBX 웹사이트를 참조하기 바란다. Spring에서 제공하는 Marshaller/Unmarshaller 구현체는 JibxMarshaller이다.

JibxMarshaller를 사용하기 위한 설정은 다음과 같다.

<beans>
    <bean id="jibxFlightsMarshaller" class="org.springframework.oxm.jibx.JibxMarshaller">
        <property name="targetClass">anyframe.sample.domain.Movie</property>
    </bean>
</beans>
위의 예에서는 하나의 JibxMarshaller만 정의하였지만, 여러 클래스를 변환하는 경우 targetClass 속성을 다르게 정의한 여러 개의 JibxMarshaller가 정의되어야 한다.

'oxm' namespace를 이용해서 아래와 같이 간편하게 설정할 수도 있다.

<oxm:jibx-marshaller id="marshaller" target-class="anyframe.sample.domain.Movie"/>

45.5.HTTP Method Conversion

REST에서 또 한가지 중요한 개념은 HTTP의 4가지 Method인 GET, POST, PUT, DELETE를 사용해서 모든 리소스 즉 URL을 상태를 조작한다는 것이다.

HTTP에서 위와 같이 4가지 Method를 정의하고 있지만, HTML은 이 중 단 2가지, GET과 POST만을 지원한다. JavaScript를 이용해서 PUT과 DELETE를 사용할 수도 있겠지만 번거로운 코딩 작업이 추가되어야 하기 때문에, 일반적으로 HTML에는 POST를 사용하고 실제 HTTP Method를 지정하는 hidden 타입의 입력 필드를 추가해서 사용하는 경우가 많다.

Spring 3에서는 HiddenHttpMethodFilter를 제공하여 실제 HTTP Method를 지정하는 hidden 타입의 입력 파라미터를 찾아내서 HTTP Method를 변환하는 작업을 지원해준다. web.xml에 HiddenHttpMethodFilter 설정을 추가하면, HTTP Method가 POST이고 _method라는 파라미터가 존재하는 경우 HTTP의 Method를 _method 값으로 바꾼다. '_method'가 아닌 다른 파라미터명을 사용하려면 methodParam 속성을 이용해서 지정해준다.

또한 Spring에서는 <form:form>에서 실제 HTTP Method를 지정하는 hidden 타입의 입력 필드를 자동으로 추가해주기 때문에 훨씬 더 편리하게 사용할 수 있다.

<form:form method="delete">
    <input type="submit" value="Delete Movie"/>
</form:form>
JSP에 위와 같이 작성하면, 내부적으로는 POST 방식으로 "_method=delete"가 전달되는 것이다.

HiddenHttpMethodFilter 사용 시 유의 사항

HiddenHttpMethodFilter를 사용할 때 한가지 주의할 점은 기존에 파일 업로드를 위해서 사용했던 MultipartResolver 설정 방식을 변경해야 한다는 것이다. web.xml에 MultipartFilter를 HiddenHttpMethodFilter 앞에 정의하고, MultipartResolver를 Spring의 root Application Context에 'filterMultipartResolver'라는 Bean 이름으로 설정해 주어야 파일 업로드 기능을 정상적으로 사용할 수 있다.

다음은 web.xml에 MultipartFilter와 HiddenHttpMethodFilter를 정의한 모습이다.

<filter>
    <filter-name>multipartFilter</filter-name>
    <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>multipartFilter</filter-name>
    <url-pattern>/restweb/*</url-pattern>
</filter-mapping>
<filter>
    <filter-name>httpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>httpMethodFilter</filter-name>
    <url-pattern>/restweb/*</url-pattern>
</filter-mapping>

다음은 context-restweb-multipart.xml에 정의한 MultipartResolver 설정이다.

<bean id="filterMultipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="maxUploadSize">
        <value>10000000</value>
    </property>
</bean>
MultipartResolver Bean을 'filterMultipartResolver'가 아닌 다른 이름으로 정의할 경우, web.xml에서 MultipartFilter 정의 시에 <init-param>을 이용해서 'multipartResolverBeanName'을 설정해준다.

45.6.Rest Client

지금까지 위에서 설명한 내용들을 서버측 구현과 관련된 내용이었다. RestTemplate은 REST 스타일 아키텍처에서 클라이언트 구현과 관련된 내용이다.

RestTemplate은 Spring에서 제공하고 있는 JdbcTemplate이나, JmsTemplate과 같은 맥락의 Template으로, RESTful Service 호출과 관련된 여러 메소드를 제공하여 REST 클라이언트를 쉽게 개발할 수 있도록 도와주는 것이다.RestTemplate에서 Java 객체를 HTTP Request로 변환하거나 서버로 부터 전달된 HTTP Response를 다시 Java 객체로 변환할 때 HttpMessageConverter가 사용된다. Spring에서 제공하는 주요 타입에 대한 HttpMessageConverter들은 RestTemplate에 디폴트로 등록된다. 그 외에 추가가 필요한 경우, RestTemplate을 정의할 때 messageConverters라는 속성을 이용한다.

45.6.1.Configuration

RestTemplate 역시 Spring 컨테이너에 Bean으로 정의하고, 참조할 클래스에서 Injection 받아 사용한다. 다음은 Restweb Plugin의 src/test/resources/context-restclient.xml파일의 일부이다.

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate" />

디폴트로 등록된 MessageConverter를 변경하거나 새로운 Message Converter를 추가하려면 messageConverters 속성을 사용한다.

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <property name="messageConverters">
        <list>
            <bean class="my.custom.MarshallingHttpMessageConverter">
                <property name="unmarshaller" ref="marshaller" />
                <property name="marshaller" ref="marshaller"/>
            </bean>
        </list>
    </property>
</bean>

45.6.2.RestTemplate

RestTemplate에서는 6개의 HTTP Method 각각에 맞게 RESTful 서비스를 쉽고 간편하게 호출할 수 있도록 다음과 같은 메소드들을 제공하고 있다.

RestTemplate에서 제공하는 getForObject() 메소드를 사용하면 서버로부터 어떤 리소스를 조회하는 기능을 구현할 수 있고, postForLocation() 메소드를 사용하면 서버 측에 리소스를 생성하거나 수정하는 기능을 구현할 수 있다.

다음은 Restweb Plugin의 TestCase에서 RestTemplate을 사용한 예이다. 코드에서 볼 수 있듯이 RESTful 서비스를 호출하여 결과를 받아오는 것이 한 줄의 코드로 처리가 가능하다.

@Inject
@Named("restTemplate")
private RestTemplate restTemplate;

@Test
public void findMovie() {
    String movieId = "MV-00005";
    String movieSearchUrl = "http://localhost:8080/testrest/restweb/movies/{movieId}/edit.xml";

    movie = restTemplate.getForObject(movieSearchUrl, Movie.class, movieId);
    
    assertThat(movie.getMovieId(), is(movieId));
}

RestTemplate의 메소드들은 모두 URI Template을 사용하여 요청 URI를 명시할 수 있다.

String result = restTemplate.getForObject("http://localhost:8080/testrest/restweb/movies/{movieId}/edit.xml", Movie.class, "MV-00005");

아래와 같이 URI Template의 변수를 Map으로 처리할 수도 있다.

Map<String, String> vars = new HashMap<String, String>();
vars.put("movieId", "MV-00005");
String result = restTemplate.getForObject("http://localhost:8080/testrest/restweb/movies/{movieId}/edit.xml", Movie.class, vars);

45.7.HTTP Message Conversion

RestTemplate이나 @Controller에서 Java 객체를 HTTP Request로 변환하거나 서버로 부터 전달된 HTTP Response를 다시 Java 객체로 변환할 때 HttpMessageConverter가 사용된다. Spring에서 제공하는 주요 타입에 대한 HttpMessageConverter들은 RestTemplate(클라이언트측)과 AnnotationMethodHandlerAdapter(서버측)에 디폴트로 등록되어 내부적으로 변환에 사용된다.

HttpMessageConverter 인터페이스는 아래와 같은 모습이다. 정의된 메소드들을 보면 제공하는 기능을 알 수 있다.

public interface HttpMessageConverter<T> {
    // 입력된 클래스와 미디어 타입이 이 HttpMessageConverter에서 Read 가능한지 여부를 확인.
    boolean canRead(Class<?> clazz, MediaType mediaType);
    
    // 입력된 클래스와 미디어 타입이 이 HttpMessageConverter에서 Write 가능한지 여부를 확인.
    boolean canWrite(Class<?> clazz, MediaType mediaType);
    
    // 이 HttpMessageConverter에서 지원하는 미디어 타입 목록을 리턴.
    List<MediaType> getSupportedMediaTypes();
    
    // 입력된 Message를 읽어 입력된 타입 형태로 변환하여 리턴
    T read(Class<T> clazz, HttpInputMessage inputMessage) throws IOException,
                                              HttpMessageNotReadableException;

    // 입력된 객체를 입력된 OutputMessage로 전송
    void write(T t, HttpOutputMessage outputMessage) throws IOException,
                                              HttpMessageNotWritableException;
}

Spring에서 제공하는 HttpMessageConverter 인터페이스 구현체들을 하나씩 살펴보자.

  • StringHttpMessageConverter

    HTTP Request나 Response와 String간의 변환을 수행한다. 디폴트로 모든 text 미디어 타입('text/*')을 지원한다.

  • FormHttpMessageConverter

    HTTP Request나 Response와 Form 데이터(MultiValueMap<String, String>) 간의 변환을 수행한다. 디폴트로 'application/x-www-form-urlencoded' 미디어 타입을 지원한다.

  • ByteArrayMessageConverter

    HTTP Request나 Response와 byte 배열 간의 변환을 수행한다. 디폴트로 모든 미디어 타입('*/*')을 지원한다.

  • MarshallingHttpMessageConverter

    HTTP Request나 Response를 Spring OXM의 Marshaller/Unmarshaller를 사용하여 XML로 변환한다. 디폴트로 'text/xml', 'application/xml' 미디어 타입을 지원한다.

  • MappingJacksonHttpMessageConverter

    HTTP Request나 Response를 Jackson 라이브러리의 ObjectMapper를 사용하여 XML로 변환한다. 디폴트로 'application/json' 미디어 타입을 지원한다.

  • SourceHttpMessageConverter

    HTTP Request나 Response와 javax.xml.transform.Source(DOMSource, SAXSource, StreamSource만 지원) 간의 변환을 수행한다. 디폴트로 지원하는 미디어 타입은 'text/xml', 'application/xml'이다.

  • BufferedImageHttpMessageConverter

    HTTP Request나 Response와 java.awt.image.BufferedImage 간의 변환을 수행한다. Java I/O API에서 지원하는 모든 미디어 타입에 대해서 변환을 지원한다.


출처 - http://dev.anyframejava.org/docs/anyframe/4.5.1/reference/html/ch45.html






Anyframe Spring REST Plugin

Version 1.0.2

본 문서의 저작권은 삼성SDS에 있으며 Anyframe 오픈소스 커뮤니티 활동의 목적하에서 자유로운 이용이 가능합니다. 본 문서를 복제, 배포할 경우에는 저작권자를 명시하여 주시기 바라며 본 문서를 변경하실 경우에는 원문과 변경된 내용을 표시하여 주시기 바랍니다. 원문과 변경된 문서에 대한 상업적 용도의 활용은 허용되지 않습니다. 본 문서에 오류가 있다고 판단될 경우 이슈로 등록해 주시면 적절한 조치를 취하도록 하겠습니다.


I.Installation

Spring 3 부터 Spring MVC에서는 RESTful 웹서비스 구현을 위한 기능을 제공한다. springrest plugin은 Spring MVC에서 제공하는 RESTful 웹서비스 구현을 위한 기능들을 활용하는 방법을 가이드하기 위한 샘플 코드와 이 오픈 소스를 활용하는 데 필요한 참조 라이브러리들로 구성되어 있다.

1.Install a Spring REST Plugin

본 장에서는 springrest plugin 설치로 생성된 샘플 코드를 중심으로 Spring MVC에서 지원하는 여러가지 기능들을 활용하여 RESTful 웹 서비스를 구현하는 방법에 대해 보다 상세히 다루게 될 것이다.

본 장의 내용을 본격적으로 시작하기에 앞서 로컬 PC에 springrest plugin을 설치해 보도록 하자.

  1. springrest plugin을 설치하기 위해서는 모든 plugin의 기반이 되는 foundation plugin이 설치되어 있어야 한다. foundation plugin 설치가 필요한 경우에는 Foundation Plugin 설치하기를 참조하도록 한다.

  2. Command 창에서 샘플 프로젝트의 위치로 이동한 후, db/hsqldb/start.cmd (or start.sh)을 실행시킴으로써 샘플 DB를 시작시킨다. (기본적으로 제공되는 HsqlDB가 아닌 다른 DB를 활용하고자 하는 경우에는 본 문서 내의 DB 변경 을 참조하도록 한다.)

  3. Command 창에서 다음과 같이 명령어를 입력하여 springrest plugin을 설치한다.

    mvn anyframe:install -DpluginName=springrest
  4. Command 창에서 다음과 같이 명령어를 입력한 후, Jetty Server가 정상적으로 시작되었으면 브라우저를 통해 springrest plugin이 정상적으로 설치되었는지 확인한다. (생성된 샘플 프로젝트명이 myproject인 경우 브라우저 주소창에 http://localhost:8080/myproject를 입력한다.)

    mvn clean jetty:run

    위 그림에서 보이는 바와 같이 왼쪽 메뉴에 Foundation Sample 메뉴 외에 Spring REST Sample 메뉴가 추가된 것을 확인할 수 있을 것이다. Spring REST Sample 메뉴를 클릭하여 Spring에서 제공하는 REST 지원 기능들을 통해 목록을 조회할 수 있는지 확인하고, 영화 상세 정보에서 View as XML 버튼을 클릭하여 영화의 상세정보를 XML로 확인해 보도록 하자.

WAS(Web Application Server)별 유의사항

본 문서에서는 plugin 설치로 생성된 샘플 어플리케이션을 실행시키기 위한 WAS로써 Jetty, Tomcat를 채택하여 설명을 기술하고 있다. 그러나 plugin 설치로 생성된 샘플 어플리케이션은 특정 WAS에 종속되지 않으므로 mvn clean compile war:war와 같은 명령어 실행을 통해 패키징한 후 WebLogic, JEUS와 같은 다른 WAS에 deploy하여 실행시키는 것도 가능하다. 단, 샘플 어플리케이션이 참조하는 일부 라이브러리의 버전을 해당 WAS에서 지원하지 않는 경우가 있다. 따라서 Tomcat 또는 Jetty가 아닌 다른 WAS를 이용하여 샘플 어플리케이션을 테스트해보고자 하는 경우에는 기본적으로 Foundation 매뉴얼 내의 WAS(Web Application Server)별 유의사항 을 참고하도록 한다.

Foundation plugin 외에도 설치된 다른 plugin이 있는 경우에는, 각 plugin 매뉴얼 내의 Installation > "WAS(Web Application Server)별 유의사항"도 함께 참고하도록 한다.

II.What is REST?

설치한 springrest plugin의 샘플 코드를 이용해서 Spring에서 제공하는 REST 지원 기능들을 살펴보기 전에 먼저 REST라는 개념이 무엇인지에 대해서 간단히 살펴보도록 하자.

REST는 REpresentational State Transfer의 약자로, 통식 규약이나 표준 또는 스펙이 아니라 분산 하이퍼미디어 시스템을 위한 www같은 소프트웨어 아키텍처의 한 형식이다. REST라는 용어는 2000년 로이필딩(Roy Fielding)의 박사학위 논문에서 처음 소개된 것으로 네트워크 상에서 클라이언트와 서버 사이의 통신 방식에 대해서 서술하고 있다.

2.REST 아키텍처

REST 아키텍처는 다음과 같은 요소들로 구성된다.

  • Resource

    REST에서 가장 중요한 개념은 바로 유일한 ID를 가지는 Resource가 서버에 존재하고, 클라이언트는 각 Resource의 상태를 조작하기 위해 요청을 보낸다는 것이다. 일반적으로 Resource는 Movie, Student, Product 등과 같은 명사형의 단어이고, HTTP에서 이러한 Resource를 구별하기 위한 ID는 '/moviefinder/movies/MV-00001'와 같은 URI이다.

  • Method

    GET, DELETE 등과 같이 Resource를 조작할 수 있는 동사형의 단어를 Method라고 한다. 클라이언트는 URI를 이용해서 Resource를 지정하고 해당 Resource를 조작하기 위해서 Method를 사용한다. HTTP에서는 GET, POST, PUT, DELETE 등의 Method를 제공한다.

  • Representation of Resource

    클라이언트가 서버로 요청을 보냈을 때, 서버가 응답으로 보내주는 Resource의 상태를 Representation이라고 한다. REST에서 하나의 Resource는 여러 형태의 Representation으로 나타내어 질 수 있다. 이를 Content Negotiation이라고 하는데, 뒤에서 자세히 설명할 것이다.

위의 구성 요소들을 바탕으로 REST 아키텍처에서 클라이언트가 'http://example.com/movies/MV00004'라는 URI를 가진 Movie Resource를 조회하는 과정을 그림으로 표현하면 다음과 같다.

3.Key Principles of REST

REST는 네트워크 아키텍처 원칙의 모음이다. 여기서 네트워크 아키텍처 원칙이란 Resource를 정의하고 Resource에 대한 ID(URI)를 지정하는 방법에 대한 개괄을 말한다. 간단한 의미로는, 도메인 지향 데이터를 HTTP위에서 전송하기 위한 아주 간단한 인터페이스를 설명한 것이라고 할 수 있다. REST의 핵심 원칙는 아래와 같이 5가지 정도로 요약할 수 있다. (출처 : http://www.infoq.com/articles/rest-introduction)

3.1.Give every "thing" an ID

위에서 설명했듯이 모든 Resource에는 URI라고 하는 유일한 ID를 부여한다. 클라이언트는 URI를 이용해서 수많은 Resource를 식별하므로 이 URI 설계를 위한 다음과 같은 Design Rule이 RESTful Web Services라는 책에서 소개되고 있다. 이는 많은 사람들이 그동안 RESTful 아키텍처를 적용하면서 축적된 경험을 바탕으로 만들어진 URI 설계 가이드이다.

  • URI는 직관적으로 Resource를 인식할 수 있는 단어들로 구성할 것

    '/movies', '/products' 등과 같이 직관적으로 어떤 정보를 제공하는지 알 수 있도록 URI를 구성할 것을 가이드하고 있다.

  • URI는 계층구조로 구성할 것

    '/hotels/hayatt/bookings/20101128'와 같이 URI path가 계층적인 구조를 가지도록 구성하는 것이 좋다.

  • URI의 상위 path는 하위 path의 집합을 의미하는 단어로 구성할 것

    '/hotels/hayatt/bookings/20101128'와 같이 'hotels'는 'hayatt'의 집합이므로 '/hotels' 만으로도 호텔목록이라는 정보를 제공할 수 있는 유효한 URI가 된다.

이 외에도 여러가지 가이드들이 존재하지만 특징적인 것들만 나열하였다.

위와 같은 가이드에 맞춰 URI를 만들면 '/hotels/hilton', '/hotels/hayatt' 처럼 비슷한 패턴의 URI가 많이 생성된다. 이런 URI를 쉽게 관리할 수 있도록 URI를 추상화할 수 있도록 도와주는 것이 URI Template이다. URI Template은 '/movies/{movieId}'와 같이 하나 이상의 변수를 포함하고 있는 URI 형식의 문자열이다. URI Template에 대한 자세한 내용은 proposed RFC를 참조하기 바란다.

3.2.Link things together

하나의 Resource는 여러 개의 다른 Resource 정보를 포함할 수 있다. 아래 예에서 보는 것 처럼 Order는 Product와 Customer를 포함하고 있어서 Order정보 조회 요청에 대한 응답으로 전달된 Representation에 Product와 Customer에 대한 link가 포함되어있다. Representation이 다른 Resource에 대한 URI를 link로 포함하기 때문에 필요에 따라 클라이언트가 추가적인 정보를 조회할 수 있다. 이 개념은 'HATEOAS(Hypermedia As The Engine Of Application State)라는' 용어로도 많이 표현된다.

<order self='http://example.com/customers/1234' >
    <amount>23</amount>
    <product ref='http://example.com/products/4554' />
    <customer ref='http://example.com/customers/1234' />
</order>

클라이언트는 'Order'라는 Resource에 대한 Representation을 전달받았고, 필요에 따라 'Product'나 'Customer'의 정보를 다시 요청하면 된다. 즉, 서버에서는 또 다른 State로 전환할 수 있는 Resource의 link를 전달하기만 하고, 전환되어야 할 State의 순서를 지정하지는 않는다.

3.3.Use standard methods

Resource에 대한 CRUD 조작을 위해서 HTTP에서 제공하는 standard method를 사용할 것을 권장한다. 클라이언트가 서버의 Movie를 삭제하기 위해서 기존에는 '/movies.do?id=MV-00001&method=delete'와 같은 방식으로 요청했다면, REST에서는 '/movies/MV-00001'라는 URI와 HTTP의 DELETE method의 조합으로 요청할 수 있다.

일반적으로 대부분의 브라우저에서는 GET, POST만 지원하기 때문에 REST 구현을 위한 Spring이나 Apache CXF같은 프레임워크들에서는 모든 HTTP method를 지원하기 위한 방안을 제공하고 있다.

HTTP에서 제공하는 Method에 대한 자세한 내용은 HTTP/1.1 RFC에 정의되어 있다.

예를 들어, 상품 주문을 관리하는 어플리케이션에서 RESTful 웹 서비스를 제공한다고 할 때, URI와 HTTP method의 조합은 아래의 그림처럼 정리할 수 있다.

3.4.Resources with multiple representations

HTTP 기반의 REST에서 클라이언트는 자신이 처리할 수 있는 Format으로 Representation을 달라고 서버에게 요청할 수 있다. Request message의 Accept header에 클라이언트가 처리할 수 있는 Format을 명시하여 서버로 요청을 보내면 된다. 예를 들어, 아래의 HTTP Request는 "'MV-00005'라는 ID를 가진 영화의 상세 정보를 XML 형태로 줘"라는 의미가 된다.

위의 요청을 받은 서버는 응답으로 다음과 같은 Response Message를 전달할 것이다.

Accept header에 다른 Format을 명시하면 서버는 다른 형태의 응답을 전달할 것이다.

이와 같이 하나의 Resource는 여러개의 Representation을 가질 수 있다. 이를 Content Negotiation이라고 한다.

여기서 한 가지 문제점은 일반적인 브라우저에서는 Accept Header 값을 고정하여 전송하기 때문에, Accept Header 값을 기반으로 한 Content Negotiation이 불가능하다는 것이다. 그래서 이에 대한 대안으로 URL path에 확장자를 붙여, 확장자를 통해 클라이언트가 원하는 Representation을 표시하는 방법을 사용한다. 예를 들어, '/myapp/movies.pdf' 라는 요청이 들어오면 서버는 영화목록을 찾아서 PDF View로 클라이언트에게 전달하는 것이다.

3.5.Communicate statelessly

REST에서 서버는 클라이언트로 부터 들어오는 각 요청에 대한 상태를 저장하지 않도록 권장한다. 요청이 처리되기 위해서 필요한 모든 정보는 반드시 요청에 포함하도록 해야한다. 서버는 클라이언트 관련 정보를 저장할 필요가 없으므로 클라이언트의 수의 증가에도 시스템이 유연하게 대응할 수 있다.

III.Spring REST Supports

이제 위에서 설명한 REST 아키텍처를 적용한 서비스를 구현하기 위해서 Spring 3에서는 어떤 기능을 추가적으로 지원하는지 springrest plugin의 소스 코드와 함께 하나씩 자세히 살펴보도록 하자.

Spring의 REST를 위한 기능은 모두 Spring MVC를 기반으로 지원된다. 다양한 Annotation과 HTTP Request/Response Body 메세지 처리를 위한 HttpMessageConverter, Content Negotiation 지원을 위한 ViewResolver, 모든 HTTP method 사용을 위한 Filter, 그리고 REST 클라이언트 어플리케이션 개발에 도움을 주는 RestTemplate 등이 있다.

4.Request Mapping

위에서 언급했듯이, Spring에서 제공하는 REST 지원 기능들은 모두 Spring MVC 기반으로 되어 있다. REST 방식으로 노출되는 서비스는 곧 Controller의 메소드이기 때문에 기존에 웹 어플리케이션을 개발하던 방식과 크게 다르지 않다.

Resource의 ID인 URI를 Controller 클래스나 메소드에 매핑하기 위해서는 @RequestMapping을 사용한다. @RequestMapping이 URI Template을 지원하기 때문에 아래 샘플코드와 같이 사용할 수 있다.

@Controller
@RequestMapping("/movies")
public class MovieController {
    // ...
    @RequestMapping(value = "/{movieId}", method = RequestMethod.GET)
    public String get(@PathVariable String movieId, Model model) throws Exception {
        // ...
    }
}
또한 REST 아키텍처에서 가이드하고 있는 원칙 중 하나인, 모든 HTTP method 사용을 위해서 @RequestMapping에서 'method' 속성을 제공한다. 따라서, '/movies/MV-00001'이라는 URI가 GET으로 요청이 들어올 경우 위의 get() 메소드가 매핑될 것이다.

DispatcherServlet URL 매핑

기존에 Spring MVC를 기반으로 개발된 웹 어플리케이션에서는 'xxx.do'라는 형태의 URL을 사용했지만, 위에서 설명했듯이 REST 스타일의 URL은 '/movies', '/movies/MV-00001' 처럼 계층 구조로 사용가능하도록 설계되었다. 따라서 web.xml에 DispatcherServlet을 정의하고 매핑할 URL 패턴을 '/'로 지정해야한다.

이 경우 css 나 이미지 등의 static 리소스 URL도 DispatcherServlet을 통하게 되어 화면이 정상적으로 동작하지 않는 문제가 있다. 그래서 Spring에서는 <mvc:default-servlet-handler/>를 제공하고 있다. 이 태그의 역할은 내부적으로DefaultServletHttpRequestHandler를 등록해주는 것이다. 이 핸들러는 가장 낮은 우선순위를 가지고 있고, /**로 매핑되어 있다. 따라서 다른 handler mapping을 다 거친 후에 실패한 URL만 넘어오게 된다. DefaultServletHttpRequestHandler는 최종적으로 넘어온 요청을 처리하기 위해서 직접 static 리소스를 핸들링하는 것이 아니라 원래 서버가 제공하는 디폴트 서블릿으로 전달한다. 그래서 URLRewriteFilter 같은 것을 사용하지 않아도 간단하게 '/'를 DispatcherServlet에 매핑시킬 수 있게 된다.

그러나 springrest plugin의 경우 foundation plugin 등 다른 plugin들과 함께 섞여서 동작해야하기 때문에 <mvc:default-servlet-handler/>를 사용하지 않고, 기존에 정의된 DispatcherServlet에 아래와 같이 매핑만 추가하도록 설정했다.

<servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>/springrest/*</url-pattern>
</servlet-mapping>

또한, Spring에서는 URI Template에 포함된 변수 값을 추출할 수 있도록 @PathVariable이라는 새로운 Annotation을 추가했다.

다음은 @PathVariable을 사용한 예이다.

@RequestMapping(value = "/{movieId}", method = RequestMethod.GET)
public String get(@PathVariable String movieId, Model model)
      throws Exception {
    Movie movie = this.movieService.get(movieId);
    // 중략
    return "springrestViewMovie";
}
'/movies/MV-00001'와 같은 URI로 요청이 들어왔을 때, 위의 get 메소드가 처리하게 되고 'MV-00001' 값은 'movieId' 입력 인자로 바인딩된다.

아래와 같이 변수명을 지정하여 사용하거나 여러개의 변수를 사용할 수도 있다.

@RequestMapping(value = "/movies/{movie}/posters/{poster}", method = RequestMethod.GET)
public String get(@PathVariable("movie") String movieId, @PathVariable("poster") String posterId, Model model)
      throws Exception {
    // 중략
    return "springrestViewMovie";
}

'/movies/*/posters/{posterId}'와 같이 Ant-style의 경로에도 사용할 수 있고, URI Template의 변수를 String이 아닌 다른 타입의 입력 인자로도 바인딩 가능하다.

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

@RequestMapping("/plans/{date}")
public void get(@PathVariable Date date) {
    // 중략
}
예로 '/plans/2010-09-05' URI로 들어온 요청은 위의 메소드가 처리할 것이고, '2010-09-05'는 date 입력 인자에 Date 타입으로 바인딩 될 것이다.

@RequestMapping에서 사용할 수 있는 속성은 Foundation Plugin 매뉴얼의 컨트롤러 구현 내용을 참조하기 바란다.

5.Multiple Representation

앞 장에서 설명했듯이, RESTful 아키텍처에서 하나의 Resource는 여러 형태의 Represenation을 가질 수 있다. 즉, 클라이언트가 서버에 생성하거나 수정하기 위해 전달하는 데이터의 형태도 다양할 수 있고, 서버가 클라이언트의 요청을 처리하고 전달하는 응답도 다양한 형태를 가질 수 있다. 이러한 Content Negotiation을 지원하기 위해서 Spring에서 제공하는 기능에 대해서 살펴보도록 하자.

기존의 웹 어플리케이션은 웹 페이지에서 form submit을 통해 저장 또는 수정하고자 하는 데이터들이 전달되었다. 그렇게 submit된 데이터는 아래와 같은 모습의 HTTP Request message로 서버에 들어온다.

일반적인 웹 어플리케이션에서 Controller의 메소드는 위 HTTP message body 부분의 정보를 Command 객체에 바인딩해서 사용한다.

@RequestMapping(value = "/{movieId}", method = RequestMethod.POST)
public void update(Movie updateMovie) throws Exception {
	this.movieService.update(updateMovie);
}

그러나 RESTful 웹 서비스로 노출하는 메소드는 아래의 그림 처럼 xml, json 등 다양한 형식으로 요청 데이터가 들어올 수 있다.

그래서 Spring에서는 다양한 형태의 HTTP Request Message를 직접 처리할 수 있도록 @RequestBody를 제공한다. 또한, 클라이언트로 다양한 형태의 HTTP Response Message를 직접 리턴할 수 있도록 @ResponseBody를 제공한다.
@RequestMapping(value = "/{movieId}", method = RequestMethod.PUT)
@ResponseBody
public void update(@RequestBody Movie updateMovie) throws Exception {
	this.movieService.update(updateMovie);
}

@RequestBody와 @ResponseBody가 각각 Request/Reponse message를 처리할 때, message와 Java 객체간의 변환은 HttpMessageConverter가 담당한다. Spring에서는 미디어 타입(예: html, xml, json 등)에 따라 Jaxb2RootElementHttpMessageConverter, StringHttpMessageConverter, MappingJacksonHttpMessageConverter 등 여러가지 HttpMessageConverter 구현체를 제공하고 있다. 자세한 내용은 본 매뉴얼 HTTP Message Conversion을 참조하기 바란다. @RequestBody를 적용하여 Request message 처리시 Content-Type header 값에 따라 적절한 HttpMessageConverter가 사용된다. 마찬가지로, @ResponseBody를 사용하여 Response message 생성시 Request로 들어온 Accept header 값에 따라 적절한 HttpMessageConverter가 사용된다.

클라이언트로 전달할 Response를 좀 더 상세하게 구성하고자 하는 경우에는 ResponseEntity<?>를 사용할 수 있다.

@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<String> create(@RequestBody Movie movie) throws Exception {

    this.movieService.create(movie);

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("Location", "http://localhost:8080/mypjt2/movies/" + movie.getMovieId());
    
    // 201 CREATED, Location header
    return new ResponseEntity<String>("Created resource " + movie.getMovieId(), responseHeaders, HttpStatus.CREATED);
}

위 코드 예제에서는 @ResponseBody 대신 ResponseEntity를 사용해서 Location header와 '201 CREATED'라는 status code로 Response를 구성했다. 일반적으로 POST method의 경우 요청에 의해 Resource가 생성되었다면 '201 CREATED' status code와 새로 생성된 Resource를 조회할 수 있는 정보를 기술한 Location header를 리턴한다.

HTTP Response Status Code

자세한 내용은 HTTP Status Code Definitions와 Method Definitions를 참조하기 바란다.

@ResponseBody나 ResponseEntity를 사용하지 않고 기존의 웹 어플리케이션에서 처럼 View의 이름을 리턴하는 경우에도 Content Negotiation이 가능하도록 Spring에서는 ContentNegotiatingViewResolver를 제공한다.

ContentNegotiatingViewResolver는 자기가 직접 View를 구성하는 것이 아니라, 등록된 다른 모든 View Resolver에게로 View를 찾는 것을 위임한다. 다른 View Resolver들이 리턴한 View의 Content-Type과 HTTP Request의 Accept 헤더 값 또는 파일 확장자로 기술된 미디어 타입(Content-Type값)을 비교하여 클라이언트가 요청한 Content-Type에 가장 적합한 View를 선택하여 응답을 돌려준다.

이렇듯 ContentNegotiatingViewResolver는 다른 View Resolver들과 반드시 함께 사용되어야 하므로 View Resolver 설정 시 반드시 order를 정의해야 한다. 당연히 ContentNegotiatingViewResolver가 가장 높은 우선순위(가장 작은숫자)를 가져야 한다.

파일 확장자 기반의 Content Negotiation을 처리하기 위해서는 ContentNegotiatingViewResolver의 mediaTypes 속성에 파일 확장자와 미디어 타입을 매핑시켜 정의한다.

다음은 ContentNegotiatingViewResolver를 사용하기 위한 설정 예이다.

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <property name="mediaTypes">
        <map>
            <entry key="html" value="text/html"/>
            <entry key="xml" value="application/xml" />
        </map>
    </property>
    <property name="order" value="0"/>
</bean>

ContentNegotiatingViewResolver는 기본적으로 WebApplicationContext에 등록된 View Resolver들을 자동으로 찾아서 처리하지만, 아래와 같이 viewResolvers 속성을 이용해서 다른 View Resolver들을 명시적으로 지정할 수도 있다. 또한, defaultViews 속성에 View를 명시해두면, View Resolver 체인에서 클라이언트가 요청한 Content-Type을 지원하는 View를 찾지 못한 경우에 디폴트 View로 사용된다.

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <property name="mediaTypes">
        <map>
            <entry key="html" value="text/html"/>
            <entry key="atom" value="application/atom+xml"/>
            <entry key="json" value="application/json"/>
        </map>
    </property>
  
     <property name="viewResolvers">
        <list>
            <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
            <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                <property name="prefix" value="/WEB-INF/jsp/"/>
                <property name="suffix" value=".jsp"/>
            </bean>
        </list>
    </property>
    
    <property name="defaultViews">
        <list>
            <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView"/>
        </list>
    </property>
</bean>

요청 처리후 돌려줄 적절한 View를 선택하기 위해서, ContentNegotiatingViewResolver는 클라이언트로부터 요청된 미디어 타입을 가지고 매칭시키는데, 이 미디어 타입을 알아내는 작업은 다음과 같은 과정으로 이루어진다.

  1. favorPathExtension 속성 값이 true(디폴트값이 true이다)이고, Request path에 파일 확장자가 포함되어 있다면, ContentNegotiatingViewResolver의 mediaTypes 속성에 정의된 매핑 정보를 사용한다. 적절한 미디어 타입을 찾지 못했을 때, 만약 Java Activation Framework가 classpath에 존재한다면, FileTypeMap.getContentType(String filename) 메소드의 리턴 값을 미디어타입으로 사용한다.

  2. favorParameter 속성 값이 true(디폴트값은 false이다)이고, Request에 미디어 타입을 정의하는 파라미터가 포함되어 있다면, ContentNegotiatingViewResolver의 mediaTypes 속성에 정의된 매핑 정보를 사용한다. 디폴트 파라미터 명은 'format'이고 이것은 parameterName이라는 속성으로 변경가능하다.

  3. 위의 과정으로도 미디어 타입을 찾지 못했을 때, ContentNegotiatingViewResolver의 ignoreAcceptHeader가 false로 지정되어 있으면 Request의 Accept 헤더 값을 사용한다.

  4. 위의 모든 과정을 거치고도 미디어 타입을 찾지 못한 경우, 최종적으로 ContentNegotiatingViewResolver의 defaultContentType이 정의되어 있다면 그 값을 클라이언트에서 요청한 미디어 타입으로 간주한다.

일단 클라이언트가 요청한 미디어 타입을 찾아내면 다른 View Resolver들에게 View를 요청하고, View Resolver들이 리턴한 View의 Content-Type과 요청들어온 미디어 타입의 매칭여부를 확인해서 가장 적합한 View를 찾아서 클라이언트로 응답한다.

Response Status Code와 에러 페이지

Controller의 메소드에서 Exception이 발생했을 때, Response를 직접 리턴하는 경우에는 메소드 내부에서 Exception을 catch 하고, Response에 Error Status Code를 설정하여 리턴을 하면 된다. 그러나 Response를 직접 리턴하지 않는 경우에는 발생한 Exception을 exceptionResolver가 처리하도록 되어 있다.

현재 Anyframe의 Foundation Plugin에 의해 SimpleMappingExceptionResolver가 설정되고 defaultErrorView로 error.jsp가 렌더링되어 에러가 발생했음에도 불구하고 REST 클라이언트에게는 '200 OK'라는 Response Status와 함께 error 페이지 HTML 내용이 리턴되는 문제가 있다.

springrest plugin에서는 클라이언트(Accept: application/xml)에게 error status code를 전달하기 위해서 MarshallingView를 상속받은 MarshallingViewForError를 추가하였다. 내부적으로 페이지 랜더링 대신에 Exception별로 적절한 error status code를 보낸다. (Spring 내부적으로 발생하는 Exception에 대해서 처리하기 위해서 DefaultHandlerExceptionResolver의 코드를 사용했다.)

6.Views

Spring 3부터 Spring MVC에는 웹 어플리케이션에서 하나의 리소스, 즉 하나의 서비스에 대한 여러 형태의 응답을 지원하기 위해 다음과 같은 새로운 View들이 추가되었다..

  • AbstractAtomFeedView / AbstractRssFeedView : Atom이나 RSS 피드를 보여줄 수 있는 View

    AbstractAtomFeedView와 AbstractRssFeedView는 AbstractFeedView의 하위클래스로 java.net의 ROME 프로젝트를 기반으로 만들어져있다. Feed View를 구성하려면 AbstractAtomFeedView나 AbstractRssFeedView를 상속받은 클래스에서 각각에서 오버라이드 요구하는 메소드를 구현하여 사용한다.

    public class SampleContentAtomView extends AbstractAtomFeedView {
        @Override
        protected List<Entry> buildFeedEntries(Map<String, Object> model, 
                                                   HttpServletRequest request, HttpServletResponse response) throws Exception {
            // 중략
        }
    }
    public class SampleContentRssView extends AbstractRssFeedView {
        @Override
        protected List<Item> buildFeedItems(Map<String, Object> model, 
                                                HttpServletRequest request, HttpServletResponse response) throws Exception {
            // 중략
        }
    }

    구현한 Feed View를 사용하기 위해서 Bean 정의 파일을 작성해야한다.

    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="mediaTypes">
            <map>
                <entry key="atom" value="application/atom+xml"/>
                <entry key="html" value="text/html"/>
            </map>
        </property>
        <property name="viewResolvers">
            <list>
                <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
                <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                    <property name="prefix" value="/WEB-INF/jsp/"/>
                    <property name="suffix" value=".jsp"/>
                </bean>
            </list>
        </property>
    </bean>
    
    <bean id="movies" class="anyframe.sample.moviefinder.feed.MoviesAtomView"/>

  • MarshallingView : XML로 응답을 전달할 수 있는 View

    MarshallingView는 클라이언트에게 XML 응답을 돌려주기 위해서 Spring OXM의 Marshaller를 사용한다. 기본적으로 컨트롤러가 리턴한 모든 Model을 XML로 변환하지만, modelKey라는 속성에 Model의 이름을 지정함으로써 Marshalling되어 클라이언트로 전달될 Model을 필터링 할 수 있다.

    다음은 springrest plugin 설치로 추가된 springrest-servlet.xml의 일부이다. 먼저 설치된 foundation plugin에서 정의한 View Resolver들이 있으므로 MarshallingView를 위해서 BeanNameViewResolver만 추가하였다.

    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="mediaTypes">
            <map>
                <entry key="html" value="text/html" />
                <entry key="xml" value="application/xml" />
            </map>
        </property>
        <property name="order" value="0" />
    </bean>
    
    <bean id="springrestViewMovie" class="org.springframework.web.servlet.view.xml.MarshallingView">
        <property name="marshaller" ref="marshaller" />
    </bean>
      
    <bean id="springrestListMovie" class="org.springframework.web.servlet.view.xml.MarshallingView">
        <property name="marshaller" ref="marshaller" />
    </bean>
      
    <bean id="error" class="test003.springrest.moviefinder.web.view.MarshallingViewForError">
        <property name="marshaller" ref="marshaller" />
    </bean>
    
    <oxm:jaxb2-marshaller id="marshaller">
        <oxm:class-to-be-bound name="test003.springrest.domain.Movie" />
        <oxm:class-to-be-bound name="test003.springrest.domain.ResultPage" />
        <oxm:class-to-be-bound name="anyframe.common.Page" />
    </oxm:jaxb2-marshaller>

  • MappingJacksonJsonView : JSON으로 응답을 전달할 수 있는 View

    MappingJacksonJsonView는 클라이언트에게 JSON 응답을 돌려주기 위해서 Jackson 라이브러리의 ObjectMapper를 사용한다. 디폴트로 Model 객체 모두의 내용을 JSON으로 보내도록 되어있지만, renderedAttributes 속성을 이용해서 JSON으로 변환할 Model을 필터링 할 수 있다. ObjectMapper 확장이 필요한 경우 objectMapper 속성을 이용해서 확장한 ObjectMapper를 정의해 준다.

    MappingJacksonJsonView는 Anyframe의 simpleweb-json Plugin에 적용되어 있으므로, 사용 예는 본 매뉴얼 Simpleweb Plugin의 JSON View 설정을 참조하기 바란다.

7.Exception Handling

Spring MVC에서는 컨트롤러의 메소드에서 특정 Exception이 발생했을 경우에 해당 Exception을 처리할 수 있도록 HandlerExceptionResolver 인터페이스를 제공한다. HandlerExceptionResolver 인터페이스의 resolveException(Exception, Handler) 메소드를 구현하여 DispatcherServlet에 등록하면 해당 Exception이 발생했을 때 구현한 메소드가 호출된다. 기존에 web.xml에서 <error-page>를 이용해서 에러를 보여주는 페이지를 정의하는 방식과 비슷하지만 좀 더 유연한 기능들을 제공한다.

Spring MVC에서는 몇가지 HandlerExceptionResolver 구현체를 제공하고 있다. Foundation Plugin 샘플에서 볼 수 있는 SimpleMappingExceptionResolver가 그 중 하나이다. SimpleMappingExceptionResolver의 exceptionMappings속성에 Exception 클래스와 해당 Exception이 발생했을 때 보여줄 View를 매핑하면된다. Foundation Plugin에서는 모든 Exception에 대해서 error라는 이름의 View로 화면을 렌더링하도록 설정되어있다.

그 밖에 Exception 처리 방법에는 어떤 것들이 있는지 알아보자.

7.1.@ExceptionHandler

HandlerExceptionResolver 인터페이스를 직접 구현하지 않고 @ExceptionHandler를 이용할 수도 있다. 컨트롤러 메소드에 @ExceptionHandler를 붙이면 지정한 Exception이 발생했을 때 해당 예외를 처리하게 할 수 있다.

@Controller
@RequestMapping("/movies")
public class MovieController {
    // ...
    
    @ExceptionHandler(NotFoundException.class)
    public void handleNotFoundException(NotFoundException ex) {
        // ...
    }
}
위 컨트롤러에서 NotFoundException이 발생하면 handleNotFoundException() 메소드가 호출될 것이다.

다음과 같이 적절한 Response Status Code를 전달하기 위해서 @ResponseStatus와 함께 사용할 수도 있다.

@Controller
@RequestMapping("/movies")
public class MovieController {
    // ...
    
    @ExceptionHandler(NotFoundException.class)
    @ResponseStatus(value=HttpStatus.NOT_FOUND)
    public void handleNotFoundException(NotFoundException ex) {
        // ...
    }
}

7.2.@ResponseStatus

@ResponseStatus를 사용하면 컨트롤러 메소드나 Exception 클래스가 Status Code를 리턴하도록 정의할 수 있다.

@ResponseStatus(value=HttpStatus.NOT_FOUND)
public class NotFoundException extends BaseException {
        // ...
}

위의 같이 정의한 경우, NotFoundException이 발생하면 클라이언트로 '404 Not Found' Status Code가 전달된다.

7.3.DefaultHandlerExceptionResolver

DispatcherServlet이 디폴트로 등록하는 HandlerExceptionResolver로 DefaultHandlerExceptionResolver가 있다. DefaultHandlerExceptionResolver는 Spring에서 내부적으로 발생하는 주요 Exception들을 적절한 Response Status Code로 전환해 준다. 예를 들면, Request로 들어온 데이터를 처리하다가 타입이 맞지 않으면 TypeMismatchException이 발생하는데, 이것을 '400 Bad Request' Status Code로 리턴한다. DefaultHandlerExceptionResolver는 디폴트로 등록되지만 다른 HandlerExceptionResolver를 등록할 경우에는 명시적으로 등록하는 것이 좋다.

ExceptionHTTP Status Code
ConversionNotSupportedException500 (Internal Server Error)
HttpMediaTypeNotAcceptableException406 (Not Acceptable)
HttpMediaTypeNotSupportedException415 (Unsupported Media Type)
HttpMessageNotReadableException400 (Bad Request)
HttpMessageNotWritableException500 (Internal Server Error)
HttpRequestMethodNotSupportedException405 (Method Not Allowed)
MissingServletRequestParameterException400 (Bad Request)
NoSuchRequestHandlingMethodException404 (Not Found)
TypeMismatchException400 (Bad Request)

Response Status Code와 HandlerExceptionResolver

기존의 웹 어플리케이션에서는 Error가 발생했을 때 에러의 정보를 보여주는 페이지로 이동하였다. 이 경우 REST 클라이언트에서는 에러가 발생했음에도 불구하고 '200 OK'라는 Response Status와 함께 error 페이지 HTML 내용이 리턴되는 문제가 있다. 반대로 REST 클라이언트에게 Error Status Code를 리턴하도록 설정을 바꿀 경우 기존 웹 어플리케이션에서는 에러를 위한 페이지를 사용할 수 없다.

자세한 내용은 Response Status Code와 에러 페이지를 참조하기 바란다.

8.HTTP Method Conversion

앞서 설명했듯이, REST 아키텍처에서는 HTTP에서 정의하고 있는 모든 method를 사용할 것을 권장하고 있지만, 브라우저 기반의 HTML에서는 이 중 단 2가지, GET과 POST만을 지원한다. JavaScript를 이용해서 PUT과 DELETE를 사용할 수도 있겠지만 번거로운 코딩 작업이 추가되어야 하기 때문에, 일반적으로 HTML에는 POST를 사용하고 실제 HTTP Method를 지정하는 hidden 타입의 입력 필드를 추가해서 사용하는 경우가 많다.

Spring 3에서는 HiddenHttpMethodFilter를 제공하여 실제 HTTP Method를 지정하는 hidden 타입의 입력 파라미터를 찾아내서 HTTP Method를 변환하는 작업을 지원해준다. web.xml에 HiddenHttpMethodFilter 설정을 추가하면, HTTP Method가 POST이고 _method라는 파라미터가 존재하는 경우 HTTP의 Method를 _method 값으로 바꾼다. '_method'가 아닌 다른 파라미터명을 사용하려면 methodParam 속성을 이용해서 지정해준다.

또한 Spring에서는 <form:form>에서 실제 HTTP Method를 지정하는 hidden 타입의 입력 필드를 자동으로 추가해주기 때문에 훨씬 더 편리하게 사용할 수 있다.

<form:form method="delete">
    <input type="submit" value="Delete Movie"/>
</form:form>
JSP에 위와 같이 작성하면, 내부적으로는 POST 방식으로 "_method=delete"가 전달되는 것이다.

HiddenHttpMethodFilter 사용 시 유의 사항

HiddenHttpMethodFilter를 사용할 때 한가지 주의할 점은, 파일 업로드를 위해 form의 enctype 속성을 'multipart/form-data'로 지정하는 경우 HiddenHttpMethodFilter가 정상적으로 동작하기 않기 때문에 기존에 파일 업로드를 위해서 사용했던 MultipartResolver 설정 방식을 변경해야 한다는 것이다.

web.xml에다가 MultipartFilter를 HiddenHttpMethodFilter 앞에 정의하고, MultipartResolver를 Spring의 root Application Context에 'filterMultipartResolver'라는 Bean 이름으로 설정해 주어야 HiddenHttpMethodFilter가 정상적으로 동작할 수 있다.

다음은 web.xml에 MultipartFilter와 HiddenHttpMethodFilter를 정의한 모습이다.

<filter>
    <filter-name>multipartFilter</filter-name>
    <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>multipartFilter</filter-name>
    <url-pattern>/springrest/*</url-pattern>
</filter-mapping>
<filter>
    <filter-name>httpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>httpMethodFilter</filter-name>
    <url-pattern>/springrest/*</url-pattern>
</filter-mapping>

다음은 context-springrest-multipart.xml에 정의한 MultipartResolver 설정이다.

<bean id="filterMultipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="maxUploadSize">
        <value>10000000</value>
    </property>
</bean>
MultipartResolver Bean을 'filterMultipartResolver'가 아닌 다른 이름으로 정의할 경우, web.xml에서 MultipartFilter 정의 시에 <init-param>을 이용해서 'multipartResolverBeanName'을 설정해준다.

9.Implementing REST Client

지금까지 위에서 설명한 내용들은 모두 서버측 구현과 관련된 내용이었다. RestTemplate은 REST 아키텍처에서 클라이언트 구현과 관련된 내용이다.

RestTemplate은 Spring에서 제공하고 있는 JdbcTemplate이나, JmsTemplate과 같은 맥락의 Template으로, RESTful Service 호출과 관련된 여러 메소드를 제공하여 REST 클라이언트를 쉽게 개발할 수 있도록 도와주는 것이다. RestTemplate에서 Java 객체를 HTTP Request로 변환하거나 서버로 부터 전달된 HTTP Response를 다시 Java 객체로 변환할 때 HttpMessageConverter가 사용된다. Spring에서 제공하는 주요 타입에 대한 HttpMessageConverter들은 RestTemplate에 디폴트로 등록된다. 그 외에 추가가 필요한 경우, RestTemplate을 정의할 때messageConverters라는 속성을 이용한다.

9.1.Configuration

RestTemplate 역시 Spring 컨테이너에 Bean으로 정의하고, 참조할 클래스에서 Injection 받아 사용한다. 다음은 springrest plugin의 src/test/resources/context-restclient.xml파일의 일부이다.

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate" />

RestTemplate에서도 HTTP Request 메세지를 구성하거나, Response 메세지를 파싱할 때 HttpMessageConverter를 사용한다. 디폴트로 등록된 HttpMessageConverter를 변경하거나 새로운 HttpMessageConverter를 추가하려면 messageConverters 속성을 사용한다.

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <property name="messageConverters">
        <list>
            <bean class="my.custom.MarshallingHttpMessageConverter">
                <property name="unmarshaller" ref="marshaller" />
                <property name="marshaller" ref="marshaller"/>
            </bean>
        </list>
    </property>
</bean>

9.2.RestTemplate

RestTemplate은 GET, POST 등 6개의 HTTP Method를 사용하여 쉽고 간편하게 RESTful 웹 서비스를 호출할 수 있도록 다음과 같은 메소드를 제공하고 있다.

RestTemplate에서 제공하는 getForObject() 메소드를 사용하면 서버로부터 어떤 리소스를 조회하는 기능을 구현할 수 있고, postForLocation() 메소드를 사용하면 서버 측에 리소스를 생성하거나 수정하는 기능을 구현할 수 있다.

다음은 springrest plugin의 TestCase에서 RestTemplate을 사용한 예이다. 코드에서 볼 수 있듯이 RESTful 서비스를 호출하여 결과를 받아오는 것이 한 줄의 코드로 처리가 가능하다.

@Inject
@Named("restTemplate")
private RestTemplate restTemplate;

@Test
public void findMovie() {
    String movieId = "MV-00005";
    String movieSearchUrl = "http://localhost:8080/mypjt2/movies/{movieId}";

    Movie movie = restTemplate.getForObject(movieSearchUrl, Movie.class,
        movieId);

    assertThat(movie.getMovieId(), is(movieId));
}

RestTemplate의 메소드들은 모두 URI Template을 사용하여 요청 URI를 명시할 수 있다.

String result = restTemplate.getForObject("http://localhost:8080/testrest/springrest/movies/{movieId}/edit.xml", Movie.class, "MV-00005");

아래와 같이 URI Template의 변수를 Map으로 처리할 수도 있다.

Map<String, String> vars = new HashMap<String, String>();
vars.put("movieId", "MV-00005");
String result = restTemplate.getForObject("http://localhost:8080/testrest/springrest/movies/{movieId}/edit.xml", Movie.class, vars);

또한, exchange 메소드를 이용하여 HTTP Response의 header와 body 정보를 자유롭게 사용할 수도 있다.

@Test
public void createMovie() throws Exception {
    String movieCreateUrl = "http://localhost:8080/mypjt2/movies";

    Movie movie = makeMovie();

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_ATOM_XML);
    HttpEntity<Movie> requestEntity = new HttpEntity<Movie>(movie, headers);

    ResponseEntity<String> response = restTemplate.exchange(movieCreateUrl,
        HttpMethod.POST, requestEntity, String.class);
    assertThat(response.getStatusCode().toString(), is("201"));

    String movieSearchUrl = response.getHeaders().getLocation().toURL()
        .toString();
    movie = restTemplate.getForObject(movieSearchUrl, Movie.class);

    assertThat(movie, notNullValue());
    assertThat(movieSearchUrl,
        is("http://localhost:8080/mypjt2/movies/"
            + movie.getMovieId()));
    assertThat(movie.getTitle(), is("괴물"));

    System.out.println("New movie is registered.");
    System.out.println("1.MOVIE ID : " + movieSearchUrl);
    System.out.println("2.MOVIE Object : " + movie);
}

Error Status Code

RestTemplate으로 RESTful 웹 서비스를 호출했을 때, 서버로부터 404나 500 등의 Error Status Code를 리턴을 받게되면 Exception이 발생한다. Response Status Code에 대해서는 본 매뉴얼의 HTTP Response Status Code 를 참조하기 바란다.

10.HTTP Message Conversion

RestTemplate이나 @Controller에서 Java 객체를 HTTP Request로 변환하거나 서버로 부터 전달된 HTTP Response를 다시 Java 객체로 변환할 때 HttpMessageConverter가 사용된다. Spring에서 제공하는 주요 타입에 대한 HttpMessageConverter들은 RestTemplate(클라이언트측)과 AnnotationMethodHandlerAdapter(서버측)에 디폴트로 등록되어 내부적으로 변환에 사용된다.

HttpMessageConverter 인터페이스는 아래와 같은 모습이다. 정의된 메소드들을 보면 제공하는 기능을 알 수 있다.

public interface HttpMessageConverter<T> {
    // 입력된 클래스와 미디어 타입이 이 HttpMessageConverter에서 Read 가능한지 여부를 확인.
    boolean canRead(Class<?> clazz, MediaType mediaType);
    
    // 입력된 클래스와 미디어 타입이 이 HttpMessageConverter에서 Write 가능한지 여부를 확인.
    boolean canWrite(Class<?> clazz, MediaType mediaType);
    
    // 이 HttpMessageConverter에서 지원하는 미디어 타입 목록을 리턴.
    List<MediaType> getSupportedMediaTypes();
    
    // 입력된 Message를 읽어 입력된 타입 형태로 변환하여 리턴
    T read(Class<T> clazz, HttpInputMessage inputMessage) throws IOException,
                                              HttpMessageNotReadableException;

    // 입력된 객체를 입력된 OutputMessage로 전송
    void write(T t, HttpOutputMessage outputMessage) throws IOException,
                                              HttpMessageNotWritableException;
}

Spring에서 제공하는 HttpMessageConverter 인터페이스 구현체들을 하나씩 살펴보자.

  • StringHttpMessageConverter

    HTTP Request나 Response와 String간의 변환을 수행한다. 디폴트로 모든 text 미디어 타입('text/*')을 지원한다.

  • FormHttpMessageConverter

    HTTP Request나 Response와 Form 데이터(MultiValueMap<String, String>) 간의 변환을 수행한다. 디폴트로 'application/x-www-form-urlencoded' 미디어 타입을 지원한다.

  • ByteArrayMessageConverter

    HTTP Request나 Response와 byte 배열 간의 변환을 수행한다. 디폴트로 모든 미디어 타입('*/*')을 지원한다.

  • MarshallingHttpMessageConverter

    HTTP Request나 Response를 Spring OXM의 Marshaller/Unmarshaller를 사용하여 XML로 변환한다. 디폴트로 'text/xml', 'application/xml' 미디어 타입을 지원한다.

  • MappingJacksonHttpMessageConverter

    HTTP Request나 Response를 Jackson 라이브러리의 ObjectMapper를 사용하여 XML로 변환한다. 디폴트로 'application/json' 미디어 타입을 지원한다.

  • SourceHttpMessageConverter

    HTTP Request나 Response와 javax.xml.transform.Source(DOMSource, SAXSource, StreamSource만 지원) 간의 변환을 수행한다. 디폴트로 지원하는 미디어 타입은 'text/xml', 'application/xml'이다.

  • BufferedImageHttpMessageConverter

    HTTP Request나 Response와 java.awt.image.BufferedImage 간의 변환을 수행한다. Java I/O API에서 지원하는 모든 미디어 타입에 대해서 변환을 지원한다.

11.OXM (Object/XML Mapping)

OXM은 Spring에서 Object와 XML간의 변환을 위해서 JAXB, Castor, JiBX 같은 XML Marshalling 기술을 추상화한 기능으로 원래는 Spring Web Service 프로젝트에 포함되어 있던 모듈이 분리되어 Spring 3에서 Core 영역에 포함되었다. REST Feature 범위는 아니지만 MarshallingView 및 MarshallingHttpMessageConverter와 연관지어 이 장에서 설명하도록 하겠다.

Spring OXM은 다음과 같은 특징을 가진다.

  • 간편한 설정

    Marshaller를 일반 빈과 동일하게 정의한다. 또한 'oxm' 네임스페이스를 제공하여 JAXB2, XmlBeans, JiBX 등을 사용한 Marshaller를 손쉽게 정의할 수 있게 해준다.

  • 일관된 인터페이스

    Marshaller/Unmarshaller라는 두가지 인터페이스로 동작하기 때문에 OX Mapping Framework를 설정만으로 쉽게 변경할 수 있다. 또한 OX Mapping Framework을 섞어서(mix and match) 사용할 수도 있다.

  • 일관된 예외 계층

    Mapping(Serialization)하다 발생한 Exception 처리를 위해서 XmlMappingException이라는 Root Exception을 제공한다.

Spring OXM에서 Marshaller와 Unmarshaller 인터페이스는 구분되어 있지만 Spring에서 제공하고 있는 실제 구현체들은 하나의 클래스에서 두 개의 인터페이스 모두를 구현해서 제공하고 있다. 그래서 구현클래스 하나만 Bean으로 등록하면 Marshaller로 사용할 수도 있고 Unmarshaller로 사용할 수도 있다.

11.1.Programmatic Using

XML 변환을 위한 Marshaller는 아래와 같이 Bean으로 정의한 다음 클래스에서 Injection 받아서 사용할 수 있다. 예제에서는 Castor를 사용하고 있지만, JAXB, XMLBeans, JiBX, XStream 등도 Marshaller로 사용할 수 있다. 앞서 언급했듯이 CastorMarshaller는 Marshaller와 Unmarshaller 인터페이스를 모두 구현하였기 때문에 두 가지 용도로 참조할 수 있다.

<beans>
    <bean id="sample" class="SampleClass">
        <property name="marshaller" ref="castorMarshaller" />
        <property name="unmarshaller" ref="castorMarshaller" />
    </bean>
    
    <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller"/>
</beans>

다음은 클래스에서 Marshaller를 사용하는 예제이다.

public class SampleClass {
    @Inject
    private Marshaller marshaller;
    @Inject
    private Unmarshaller unmarshaller;
    
    // 중략
    public void save() throws IOException {
        FileOutputStream os = null;
        try {
            os = new FileOutputStream(FILE_NAME);
            this.marshaller.marshal(movies, new StreamResult(os));
        } finally {
            if (os != null) {
                os.close();
            }
        }
    }
    // 중략
}

11.2.Declarative Using

Spring에서 제공하는 'oxm' namespace를 이용하면 Marshaller 설정을 간편하게 추가할 수 있다.이를 위해서는 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"
    xmlns:oxm="http://www.springframework.org/schema/oxm"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/oxm
    http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd">

현재 제공하고 있는 태그들은 다음과 같다.

상세한 설정 방법은 각각의 Marshaller 설명에서 더 자세히 살펴보도록 하겠다.

11.3.JAXB

JAXB는 W3C XML 스키마를 지원하는 Object/XML 매핑 프레임워크로 Spring에서는 JAXB 2.0 API를 사용한 Jaxb2Marshaller를 제공하고 있다.

Jaxb2Marshaller를 사용하기 위한 설정은 다음과 같다.

<beans>
    <bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        <property name="classesToBeBound">
            <list>
                <value>myapp.springrest.domain.Movie</value>                
            </list>
        </property>
    </bean>
</beans>
스키마 Validation이 필요한 경우 'schema' 속성을 추가하여 스키마 파일을 지정해 줄 수 있다.

'oxm' namespace를 이용해서 아래와 같이 간편하게 설정할 수도 있다.

<oxm:jaxb2-marshaller id="marshaller" contextPath="myapp.springrest.domain"/>
다음은 springrest plugin의 src/test/resources/context-restclient.xml파일의 일부이다. <oxm:class-to-be-bound>를 이용하여 변환할 클래스 목록을 정의하였다.
<oxm:jaxb2-marshaller id="marshaller">
    <oxm:class-to-be-bound name="myapp.springrest.domain.Movie"/>
</oxm:jaxb2-marshaller>

11.4.Castor

Castor는 오픈 소스 XML 바인딩 프레임워크로, Java 객체와 XML간의 변환에 대해서 Castor에서 사용하는 디폴트 규칙을 그대로 따른다면 Spring에서는 제공하는 CastorMarshaller를 추가 설정 없이 간단하게 Bean으로 정의할 수 있다.

CastorMarshaller를 사용하기 위한 설정은 다음과 같다.

<beans>
    <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller" />
</beans>
Castor의 디폴트 변환 양식을 변경하고자 하는 경우 Castor 매핑 파일을 작성하여 아래 예와 같이 mappingLocation 속성으로 정의해준다. Castor 매핑 파일을 작성방법에 대해서는 Castor XML Mapping을 참조한다.
<beans>
    <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller">
        <property name="mappingLocation" value="classpath:mapping.xml" />
    </bean>
</beans>

11.5.XMLBeans

XMLBeans는 Full XML 스키마를 지원하는 XML 바인딩 프레임워크로, 자세한 내용은 XMLBeans 웹사이트를 참조하기 바란다. Spring에서 제공하는 Marshaller/Unmarshaller 구현체는 XmlBeansMarshaller이다.

XmlBeansMarshaller를 사용하기 위한 설정은 다음과 같다.

<beans>
    <bean id="xmlBeansMarshaller" class="org.springframework.oxm.xmlbeans.XmlBeansMarshaller" />
</beans>
단, XmlBeansMarshaller는 모든 java.lang.Object가 아닌 XmlObject 타입의 객체만 변환할 수 있다는 것을 주의해야한다.

'oxm' namespace를 이용해서 아래와 같이 간편하게 설정할 수도 있다.

<oxm:xmlbeans-marshaller id="marshaller"/>

11.6.JiBX

JiBX는 XML 데이터를 Java 오브젝트에 바인딩하는 데 사용되는 도구로, 자세한 내용은 JiBX 웹사이트를 참조하기 바란다. Spring에서 제공하는 Marshaller/Unmarshaller 구현체는 JibxMarshaller이다.

JibxMarshaller를 사용하기 위한 설정은 다음과 같다.

<beans>
    <bean id="jibxFlightsMarshaller" class="org.springframework.oxm.jibx.JibxMarshaller">
        <property name="targetClass">anyframe.sample.domain.Movie</property>
    </bean>
</beans>
위의 예에서는 하나의 JibxMarshaller만 정의하였지만, 여러 클래스를 변환하는 경우 targetClass 속성을 다르게 정의한 여러 개의 JibxMarshaller가 정의되어야 한다.

'oxm' namespace를 이용해서 아래와 같이 간편하게 설정할 수도 있다.

<oxm:jibx-marshaller id="marshaller" target-class="anyframe.sample.domain.Movie"/>


출처 - http://dev.anyframejava.org/docs/anyframe/plugin/springrest/1.0.2/reference/htmlsingle/springrest.html


Posted by linuxism
,