(스프링) 16장 스프링 MVC로 REST API 사용하기

SOAP는 전형적인 동작과 프로세싱에 집중하고 있으며, REST의 관심은 데이터 처리에 있다.


1. 휴식(REST)를 취하다


1.1 REST의 개념

REST는 SOAP의 수 많은 XML네임스페이스를 이용하지 않고 평범한 HTTP URL을 통해 호출한다.
REST는 RPC와 거의 관련이 없다. RPC가 서비스 지향적이고 액션과 동사에 초점을 맞추는데 반해, REST는 리소스 지향적이고 애플리케이션을 표현하는 객체와 명사를 강조한다.


*REST의 약어
  • 표현(Representational)- REST리소스는 XML, JSON, 심지어 HTML을 포함하여 리소스 사용자에게 가장 적합한, 사실상 거의 모든 형식으로 표현한다.
  • 상태(State)- REST와 작업할 경우 리소스에 대해 취할 수 있는 액션보다 리소스의 상태에 대해 더 많은 관심을 둔다.
  • 전달(Transfer)- REST는 한 애플리케이션에서 다른 애플리케이션으로 어떤 표현 형식으로 리소스 데이터 전달을 포함한다.

간단히 말하면, REST는 가장 적합한 형식이 무엇이든지 간에 서버에서 클라이언트(또는 그 반대로)리소스의 상태를 전달하는 것이다.

1.2 스프링이 REST를 지원하는 방법

  • 컨트롤러는 REST의 네 가지 주요 메소드인 GET,PUT, DELETE 그리고 POST를 포함하여 모든 HTTP메소드에 대한 요청을 처리한다. 스프링 3.2 및 상위 버전은 PATCH메소드도 지원한다.
  • 새로운 @PathVariable 애너테이션은 컨트롤러가 파라미터화된 URL(경로의 일부분에 변수 입력이 있는 URL)에 대한 요청을 처리할 수 있도록 한다.
  • 리소스는 XML, JSON, Atom 그리고 RSS같은 데이터 모델 렌더링을 위한 새로운 뷰 구현을 포함하여 스프링의 뷰와 뷰 리졸버를 이용해 다양한 방식으로 표현한다.
  • 클라이언트에 대한 가장 적합한 표현은 새로운 ContentNegotiatingViewResolver를 이용해 선택한다.
  • 뷰 기반의 렌더링은 새로운 @ResponseBody 애너테이션과 다양한 HttpMethodConverter구현체를 이용해 모두 무시한다.
  • 마찬가지로 새로운 @RequestBody 애너테이션은 HttpMethodConverter 구현체와 함께 인바인드 HTTP데이터를 컨트롤러의 핸들러 메소드에 전달하는 자바 객체로 변환한다. RestTemplate은 클라이언트 측의 REST리소스 사용을 간소화한다.




2. 첫번째 REST 엔드포인트 만들기


REST를 위한 스프링의 훌륭한 지원 중 하나는 RESTful 컨트롤러 생성이다.


<RESTful 스프링 MVC 컨트롤러>

@Controller
@RequestMaping("/spittles")
public class SpittleController{

    private static final String MAX_LONG_AS_STRING="9223372036854775807";
    
    private SpittleRepository spittleRepository;


   @Autowired
   public SpittleController(SpittleRepository spittleRepository){
     this.spittleRepository=spittleRepository;
   }


  @RequestMapping(method=RequestMethod.GET)
   public List<Spittle> spittles(
                  @RequestParam(value="max",
                                       defaultValue=MAX_LONG_AS_STRING) long max,
                  @RequestParam(value="count",defaultValue="20") int count){
         
          return spittleRepository.findSpittles(max,count);
   }




---> 지금 이 컨트롤러는 전혀 RESTful 하지 않고, 리소스를 제공하는 컨트롤러도 아니다.
GET요청이 /spittles을 받을 때, spittles()메소드가 호출된다. 이것은 조회하고 주입된 SpittleRepository에서 추출된 Spittle리스트를 반환한다. 이 리스트는 렌더링 뷰 모델에 배치된다. 브라우저 웹 기반 애플리케이션의 경우, 아마도 이 모델 데이터가 HTML페이지에 렌더링 되는 것을 의미한다.

표현은 REST의 중요한 부분이다. 표현은 서버와 클라이언트가 리소스에 대해 통신하는 방법이다. 주어진 모든 리소스는 사실상 어떤 형식으로든 표현된다. 사용자의 취향에 따라 표현되는 방법(JSON, XML 등)이 변경될 뿐 리소스는 변하지 않는다.


컨트롤러는 일반적으로 리소스가 어떻게 표현되는지 관심이 없다는 사실을 아는 것이 중요하다. 컨트롤러는 리소스를 정의하는 자바 객체의 관점에서 리소스를 다룬다. 하지만 리소스가 클라이언트에 가장 적합한 형태로 변환되는 작업을 컨트롤러가 마칠 때까지는 아니다.
스프링은 리소스의 자바 표현을 클라이언트에 전달될 표현으로 변환하는 두 가지 방법을 제공한다.
  • 콘테츠 협상 - 모델이 클라이언트 제공되는 표현으로 렌더링 될 수 있도록 뷰는 선택된다.
  • 메시지 변환 - 메시지 변환기는 컨트롤러에서 반환된 객체를 클라이언트에 제공되는 표현으로 변경한다.


2.1 리소스 표현 협상

리소스 표현을 생성할 수 있는 뷰에 뷰 이름을 리졸빙할 때 고려해야 할 추가적인 사항이 존재한다. 뷰는 뷰의 이름과 일치해야 할 뿐만아니라 클라이언트와 어울리게 선정해야 한다. 만일 클라이언트가 JSON을 원하면, 뷰 이름이 일치하더라도 HTML 렌더링 뷰는 수행할 수 없다.

스프링의 ContentNegotiationViewResolver는 클라이언트가 고려하려는 콘텐츠 타입을 선택하는 특별한 뷰 리졸버이다. 간단히 다음과 같이 설정된다.
@Bean
public ViewResolver cnViewResolver(){
   return new ContentNegotiationgViewResolver();
}

ContentNegotiatingViewResolver의 동작을 이해하려면 콘텐츠 협상의 두 단계를 알아야 한다.
  1. 요청 미디어 타입 결정
  2. 요청 미디어 타입에 대해 최적의 뷰 검색

요청 미디어 타입 결정

첫 번째 단계는 클라이언트가 원하는 리소스 표현의 종류를 결정한다. 요청의 Accept헤더가 클라이언트에게 전송되어야하는 표현이 무엇인지 명확히 표시를 제공해 줄까?
아쉽게도 Accept헤더를 항상 신뢰할 수 없다. 
만일 사용자가 웹 브라우저를 통해 요청을 보낸다면, 사용자가 원하는 바를 브라우저가 Accept 헤더에 보낸것이라고 보장할 수 없다. 웹 브라우저는 일반적으로 text/html과 같이 사람에게 친숙한 콘텐츠 타입만 받으며, 다른 콘텐츠 타입을 지정하는 방법은 존재하지 않는다.

ContentNegotiatingViewResolver는 먼저 URL의 파일 확장자를 살펴본 후에 Accept헤더를 살펴보고 요청하는 모든 미디어 타입을 사용한다.  만일 URL의 끝에 확장자가 있으면 
ContentNegotiatingViewResolver는 확장자에 기반한 원하는 타입을 만들어 낸다. 만약 확장자가 .json이면 그때 원하는 콘텐츠 타입은 application/json이다.

파일 확장자가 미디어 타입을 위한 사용가능한 단서를 제공하지 못하면 요청된 Accept헤더가 고려된다. 그 경우에 Accept헤더의 값은 클라이언트가 원하는 MIME타입을 나타낸다. 검색할 필요는 없다.

결론적으로, 만약에 Accept헤더가 없고 확장자가 별 도움이 안된다면 ContentNegotiatingViewResolver는 기본 콘텐츠 타입으로 되돌아가고, 클라이언트는 서버가 전달하는 표현이 무엇이든지 사용한다.

스프링의 다른 뷰 리졸버와 달리 ContentNegotiatingViewResolver는 자기 자신의 뷰에 대해서는 리졸브를 하지 못한다. 대신 다른 뷰 리졸버에게 위임하고 요청한다.

미디어 타입 선택방식이 미치는 영향

ContentNegotiationManager을 이용하여 어떻게 행동할지 변경한다. ContentNegotiationManager를 통해 할 수 있는 몇가지 사항들은 다음과 같다.
  • 콘텐츠 타입을 요청으로부터 얻지 못할 경우의 기본 콘텐츠 타입을 지정한다.
  • 요청 파라미터를 통해서 콘텐츠을 지정한다.
  • 요청의 Accept헤더를 무시한다.
  • 특정 미디어 타입에 대한 요청 확장자를 매핑한다.
  • 확장자로부터 미디어 타입을 검색하기 위한 fallback옵션으로 JAF(Java Activation Framework)를 사용한다.
ContentNegotiationManager를 설정하기 위해서는 세 가지 방법이 존재한다.
  • 타입이 ContentNegotiationManager인 빈을 직접 선언한다.
  • ContentNegotiationManagerFactoryBean을 통해서 간접적으로 빈을 생성한다.
  • WebMvcConfigurerAdapter의 configureContentNegotiation()메소드를 오버라이드한다.
첫번쨰 방법은 사용할 수는 있지만, 특정한 이유가 없는 한 사용할 필요가 없다.

일반적으로 ContentNegotiationManagerFactoryBean는 XML에서 ContentNegotiationManager를 설정할때 매우 유용하다. 예를들면, 다음과 같이 XML에서 application/json의 기본콘텐츠 타입을 사용하여 ContentNegotiationManager을 설정한다.

<bean id="contentNegotiationManager"
    class="org.springframework.http.ContentNegotiationManagerFactoryBean"
    p:defaultContentType="application/json">


ContentNegotiationManagerFactoryBean은 FactoryBean의 구현체이므로 ContentNegotiationManager이 생성된다. ContentNegotiationManager는 ContentNegotiatingViewResolver의 ContentNegotiationManager프로퍼티로 주입된다.


자바 설정에 있어서 ContentNegotiationManager를 얻기 위한 가장 쉬운 방법은 WebMvcConfigureAdapter를 확장하고, configureContentNegotiation()메소드를 오버라이드 하는 것이다.
다음은 기본 콘텐츠 타입을 설정하기 위한 configureContentNegotiation() 구현체다.

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer){
  
  configurer.defaultContentType(MediaType.APPLICATION_JSON);
}


ContentNegotiationManager빈을 보유할 때 필요한 것은 ContentNegotiatingViewResolver의 ContentNegotiationManager프로퍼티로 주입하는 동작이다. 이 동작을 위해서 ContentNegotiatingViewResolver를 선언하는 @Bean메소드 대상으로 약간의 변경이 필요하다.

@Bean
public ViewResolver cnViewResolver(ContentNegotiationManager cnm){
   ContentNegotiatingViewResolver cnvr= new ContentNegotiatingViewResolver();
   cnvr.setContentNegotiationManager(cnm);
   return cnvr;
}

ContentNegotiationManager를 사용하여 주입되며 setContentNegotiationManager()를 호출한다. 따라서 ContentNegotiatingViewResolver는 ContentNegotiationManager에서 정의된 동작이 적용된다.

다음코드는  ContentNegotiatingViewResolver를 사용할 때 더 선호하는 간단한 설정 예를 보여준다. 어떤 뷰 이름에 대한 JSON출력을 렌더링하는 HTML뷰에 대해 기본적으로 사용된다.

<ContentNegotiationManager 설정하기>

@Bean
public ViewResolver cnViewResolver(ContentNegotiationManager cnm){
   ContentNegotiatingViewResolver cnvr= new ContentNegotiatingViewResolver();
   cnvr.setContentNegotiationManager(cnm);
   return cnvr;
}

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer){
  
  configurer.defaultContentType(MediaType.TEXT_HTML);
}

@Bean
public ViewResolver beanNameViewResolver(){  <----빈으로서 뷰를 검색
   return new BeanNameViewResolver();
}

@Bean
public View spittles(){
   return new MappingJackson2JsonView();  <--"spittles" JSON 뷰
}



---->대부분의 환경에서는  ContentNegotiatingViewResolver는 클라이언트가 ContentNegotiationManager에서 설정된 것처럼 HTML이 필요함을 가정한다. 
그렇지만 클라이언트가 JSON이 필요함을 지정하면(요청 경로 또는 Accept헤더에서의 .json확장자가 해당) ContentNegotiatingViewResolver는 JSON뷰를 지원하는 뷰 리졸버를 찾는다.
논리 뷰 이름이 "spittle"이며, 설정된 BeanNameViewResolver는 spittles()메소드에서 선언된 View를 리졸브한다. 빈 명칭이 논리 뷰 이름과 매칭되므로 다른 매칭 뷰가 존재하지 않는다면, ContentNegotiatingViewResolver는 HTML을 제공하는 기본 뷰 대상으로 되돌아간다.

ContentNegotiatingViewResolver의 장점과 한계

ContentNegotiatingViewResolver를 사용함의 가장 큰 장점은 컨트롤러 코드를 변경하지 않고 스프링MVC의 최상단에서 REST리소스 표현을 제공한다는 점이다.

ContentNegotiatingViewResolver는 또한 매우 심각한 한계점이 있다. ViewResolver구현체와 마찬가지로 리소스가 클라이언트에 어떻게 렌더링 될지를 결정하는 기회를 가진다

즉, 클라이언트가  무엇을 예상하는지를 나타내지 못한다. 이런 한계점 때문에 ContentNegotiatingViewResolver 사용을 더 선호하지 않는다.
대신, 리소스 표현을 제공하기 위한 스프링의 메시지 변환기를 사용하는 것에 더 의존한다.


2.2 HTTP메시지 변환기 사용

메시지 변환은 컨트롤러에 의해서 만들어지는 데이터를 클라이언트에 제공되는 표현으로 변환시키기 위한 보다 직접적인 방법을 제공한다. 메시지 변한을 사용할 떄, DispatcherServlet은 뷰에 대해 데이터모델을 전달하는 동작을 신경쓰지 않는다. 

<스프링은 HTTP메시지 변환기를 제공하여 다양한 자바 타입과 리소스 표현간의 마샬링 작업을 처리한다.>

  • BufferedImageHttpMessageConverter : BufferedImages를 이미지 바이너리 데이터로(또는 반대방향으로) 변환한다.
  • ByteArrayHttpMessageConverter : 바이트 배열 읽기/쓰기, 모든 미디어 타입(*/*)에서 읽고 application/octet-stream으로 쓴다.
  • MappingJacksonHttpMessageConverter : 타입이 있는 객체 또는 타입이 없는 HashMap으로부터 JSON을(또는 반대방향으로) 읽고 쓴다. Jackson JSON 라이브러리가 클래스패스에 존재하는 경우 등록한다.
  • MarshallingHttpMessageConverter : 주입된 마샬러와 언마샬러를 이용해 XML을 읽고 쓴다. 지원되는 마샬러(언마샬러) Castor, JAXB2, JIBX, XMLBeans 그리고 XStream이 있다.
  • RssChannelHttpMessageConverter : Rome Channel 객체로 부터 RSS피드를 (또는 반대방향으로) 읽고 쓴다. Rome라이브러리가 클래스패스에 존재하는 경우 등록된다.

예를들어, 클라이언트가 요청의 Accept헤더를 통해 application/json 을 받을 수 있음을 나타낸다고 가정하자. Jackson JSON라이브러리가 클래스패스에 있다고 가정하면, 핸들러 메소드에서 반환된 객체는 클라이언트에 반환되는 JSON표현으로의 변환을 위해 MappingJacksonHttpMessageConverter에 할당된다. 반면에 요청 헤더가 text/xml을 선호한다고 나타내면, Jaxv2RootElementHttpMessageConverter가 클라이언트에 대한 XML응답을 생성하는 작업을 수행한다.


응답바디에서 리소스 상태 변환

일반적으로 핸들러 메소드는 String이외의 다른 자바 객체를 반환하면 이 객체는 결국 뷰에 렌더링할 모델이 된다. 하지만 메시지 변환을 적용하면 스프링을 통해 일반 모델/뷰를 스킵할 수 있고, 대신 메시지 변환기를 사용한다. 가장 간단한 방법은 @ResponseBody를 사용하여 컨트롤러 메소드를 애너테이션하는 것이다.

@RequestMapping(method=RequestMethod.GET, produces="application/json")
public @ResponseBody List<Spittle> spittles(
         @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
         @RequestParam(value="count", defaultValue="20") int count){

     return spittleRespository.findSpittles(max,count);
}


@ResponseBody 애너테이션은 반환된 객체를 리소스로서 클라이언트트에 보내고, 클라이언트가 사용가능한 표현형태로 변환한다. 특정하게 말하면 DispatcherServlet은 요청의 Accept헤더를 고려하고, 원하는 표현을 클라이언트에 제공하고, 메시지 변환기를 찾는다.

클라이언트의 Accept헤더는 클라이언트가 application/json을 사용하도록 지정할 경우, 그리고 Jackson JSON라이브러리가 application이 클래스 패스에 존재할 때 MappingJacksonHttpMessageConverter을 선택한다.
메시지 변환기는 컨트롤러로부터 반환된 Spittle리스트를 응답 보디로 작성되는 JSON도큐먼트 형태로 변경한다.

요청바디에서 리소스 상태받기

@ResponseBody는 스프링이 클라이언트에 데잍를 전송할 때 메시지 변환기를 사용할 수 있는 것처럼 @RequesBody는 클라이언트에서 객체로 보낸 리소스 표현을 변경하기 위해서 스프링이 메시지 변환기를 검색한다.
예를들면, 사용자는 클라이언트가 저장될 새 Spittle을 제공할 방법이 필요하다고 가정하자

@RequestMapping(method=RequestMethod.POST
consumes="application/json")
public @ResponseBody
Spittle saveSpittle(@RequestBody Spittle spittle){
 return spittleRespository.save(spittle);
}


@RequestMapping은 /spittles용 POST요청을 처리함을 나타낸다. POST요청 바디(request body)를 통해 Spittle을 위한 리소스 표현을 전달한다. Spittle파라미터는 @RequestBody로 애너테이션 되므로 스프링은 요청의 Content-Type헤더를 볼 수 있고, 요청 바디를  Spittle로 전환 할 수 있는 메시지 변환기를 찾는다.

예를들면, 클라이언트가 JSON표현으로 Spittle데이터를 전송한다면, 그다음에는 Content-Type헤더가 application/json으로 설정된다. 그 경우에는 DispatcherServlet은 JSON을 자박 객체로 변경할 수 있는 메시지 변환기를 사용한다. JSON표현을 saveSpittle()메소드로 전달되는 Spittle로 변환한다. 메소드는 @ResponseBody로 애너테이션 되며, 따라서 반환되는 Spittle은 클라이언트로 반환되는 리소스 표현으로 변환된다.

@RequestMapping은 application/json으로 설정되는 consumes애트리뷰트를 가진다. Consumes애트리뷰트는 produces애트리뷰트처럼 동작하며, 요청의 Content-Type헤더에 대해서만 관련된다. 요청의 Content-Type헤더가 application/json이라면 이 메소드는 /spittles에 대한 POST요청을 철한다. 

메시지 변환을 위한 기본 컨트롤러

@Controller대신에 @RestControlle를 사용하여 컨트롤러 클래스를 애너테이션한다면, 스프링은 메시지 변환을 컨트롤러의 모든 핸들러 메소드에 적용한다. @ResponseBody를 사용하여 각 메소드를 애너테이션할 필요는 없다.


<RestController 사용하기>

@RestController
@RequestMappping("/spittles")
public class SpittleController{

  private static final String MAX_LONG_AS_STRING="9223372036854775807";
    
    private SpittleRepository spittleRepository;


   @Autowired
   public SpittleController(SpittleRepository spittleRepository){
     this.spittleRepository=spittleRepository;
   }

@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(
         @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
         @RequestParam(value="count", defaultValue="20") int count){

     return spittleRespository.findSpittles(max,count);
}




@RequestMapping(method=RequestMethod.POST
consumes="application/json")
public Spittle saveSpittle(@RequestBody Spittle spittle){
 return spittleRespository.save(spittle);
}


}


--->핸들러 메소드 중 어느것도 @ResponseBody로 애너테이션 되지 않는다. 그러나 컨트롤러가 @RestController로 애너테이션 되므로 메소드에서 반환되는 객체들은 클라이언트에 대한 리소스 표현 생성을 우히느 메시지 변환을 수행한다.


3. 더 많은 리소스 사용하기


3.1 클라이언트와 에러 처리하기

클라이언트가 요청한 것을 찾을 수 없다는 것을 말하기 위해 404(Not Found)가 되어야 한다. 그리고 응답바디는 비어있는 대신에 에러 메시지를 가진다.

  • 스프링은 해당 시나리오를 수행하기 위해 몇개의 옵션을 가진다.
  • 상태 코드는 @ResponseStatus애너테이션으로 지정된다.
  • 컨트롤러 메소드는 응답에 관한 메타 데이터를 가지는 ResponseEntity를 반환한다.


**ResponseEntity 사용하기

@ResponseBody에 대한 대안으로서 컨트롤러 메소드는 ResponseEntity를 반환한다.
리소스로 표현되는 객체와 더불어 응답에 대한 메타데이터를(헤더와 상태코드와 같은)가지는 객체다.

다음은 ResponseEntity를 반환하는 spittleById()의 새버전을 보여준다.

@RequetMapping(value="\{id}", method=RequestMethod.GET)
public ResponseEntity<Spittle> spittleById(@PathVariable long id){
  
    Spittle spittle=spittleRepository.findOne(id);
    HttpStatus status=spittle!=null?HttpStatus.OK : HttpStatus.NOT_FOUND;

    return new ResponseEntity<Spittle>(spittle,status);
}


-->spittleById()는 @ResponseBody로 애너테이션되지 않음을 주목한다. 응답헤더, 상태코드, 페이로드를 전달하는 것과 더불어, ResponseEntity는 @ResponseBody의 의미(semantics)를 암시하므로 페이로드는 메소드가 @ResponseBody로 애너테이션된다면 응답 바디로 렌더링된다.
ResponseEntity가 반환되면 @ResponseBody로 메소드를 애너테이션 할 필요없다.


이 경우에 응답바디는 여전히 비어있는 상태다. 추가 에러 정보를 전달하기 위한 바디를 사용해보자.

먼저 에러정보를 전달하기 위해 Error객체를 정의한다.

public class Error{
   private int code;
   private String message;

  public Error(int code, String message){
   this.code=code;
   this.message=message;
  }

  public int getCode(){
     return code
  }
  public String getMessage(){
    return message;
}
}



@RequetMapping(value="\{id}", method=RequestMethod.GET)
public ResponseEntity<?> spittleById(@PathVariable long id){
  
    Spittle spittle=spittleRepository.findOne(id);
    if(spittle==null){
    Error error=new Error(4,"Spittle ["+id+"] not found");
    return new ResponseEntity<Error> (error, HttpStatus.NOT_FOUND);
    }
    return new ResponseEntity<Spittle>(spittle,HttpStatus.OK);
}


예외처리

spittleById()에서 if 블록은 에러를 처리한다. 에러핸들러를 이용하기위한 일부 코드를 리팩토링하자.
SpittleNotFoundException을 사용하는 에러 핸들러를 정의하자.

@ExceptionHandler(SpittleNotFoundException.class)
public ResponseEntity<Error> spittleNotFound(SpittleNotFoundException e){
  
    long spittle=e.getSpittleId();
    Error error=new Error(4,"Spittle ["+id+"] not found");
    return new ResponseEntity<Error> (error, HttpStatus.NOT_FOUND);

}


SpittleNotFoundException은 완전히 기본적인 예외 클래스다.

public class SpittleNotFoundException extends RuntimeException
{
   private long spittleId;
   public SpittleNotFoundException(long spittleId){
         this.spittleId=spittleId;
   }

   public long getSpittleId(){
     return spittleId;
   }
}


이제 spittleById()메소드에서 에러 처리 대부분을 제거한다.

@RequetMapping(value="\{id}", method=RequestMethod.GET)
public ResponseEntity<Spittle> spittleById(@PathVariable long id){
  
    Spittle spittle=spittleRepository.findOne(id);
    if(spittle==null){throw new SpittleNotFoundException(id);}
    return new ResponseEntity<Spittle>(spittle,HttpStatus.OK);
}


spittleNotFound()는 항상 오류를 반환하므로 주위에 ResponseEntity를 유지하는 유일한 이유는 상태 코드를 설정할 수 있기 때문이다. 그러나 @ResponseStatus(HttpStatus.NOT_FOUND)를 가지고 spittleNotFound()를 애너테이션 하여, 동일한 효과를 얻고 ResponseEntity를 제거한다.

컨트롤러 클래스가 @RestController로 애너테이션될 떄, @ResponseBody 애너테이션을 제거하고 코드를 좀 더 정리할 수 있다.
@ExceptionHandler(SpittleNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Error spittleNotFound(SpittleNotFoundException e){
   long spittleId=e.getSpittleId();
   return new Error(4,"Spittle ["+id+"] not found");
}

3.2 응답 시의 헤더 설정하기


다음코드는 새로운 리소스가 생성되고, 통신하기 위해 ResponseEntity를 반환하는 saveSpittle()의 새 버전을 나타낸다.

@RequetMapping(method=RequestMethod.POST
                        consumes="application/json")
public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle){
  
    Spittle spittle=spittleRepository.save(spittle);  <----spittle 저장

    HttpHeader headers=new HttpHeaders();   <----위치 헤더 설정
       URI locationUri=URI.create(
                              "http://localhost:8080/spittr/spittles/"+spittle.getId());
     
      ResponseEntity<Spittle> responseEntity=  <----ResponseEntity 생성
                   new ResponseEntity<Spittle>(spittle, headers, HttpStatus.CREATED)
      return responseEntity;

}


--->>> URI를 직접 구성하기보다는 스프링은 UriComponentsBuilder라는 형태의 몇가지 자원을 제공한다. 한 번에 조금씩 URI의 다양한 컴포넌트들을(호스트, 포트, 경로, 쿼리와 같은)지정하여 UriComponents 인스턴스를 만들 수 있는 Builder클래스를 지원한다. UriComponentsBuilder가 만드는 UriComponents 객체를 통해 Location 헤더 설정에 적합한 URI를 얻을 수 있다.


<위치 URI를 생성하기 위해 UriComponentsBuilder 사용하기>

@RequetMapping(method=RequestMethod.POST
                        consumes="application/json")
public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle,
                               UriComponentsBuilder ucb){<---UriComponentsBuilder 제공
  
    Spittle spittle=spittleRepository.save(spittle);

    HttpHeader headers=new HttpHeaders();   <----위치 URI 계산
      URI locationUri=ucb.path("/spittles/")
                               .path(String.valueOf(spittle.getId())
                               .build()
                               .toUri();
       headers.setLocation(locationUri);
     
      ResponseEntity<Spittle> responseEntity=  <----ResponseEntity 생성
                   new ResponseEntity<Spittle>(spittle, headers, HttpStatus.CREATED)
      return responseEntity;

}


---> 경로는 2단계로 만들어진다. 첫 번쨰 단계는 컨트롤러가 처리하는 기본 경로인 /spittles/로 설정하기 위해 path()를 호출한다. 저장된 Spittle ID는 두 번쨰 호출 시 path()로 전달된다. path()에 대한 각 호출은 이전 호출을 기반으로 하ㅏㄴ다.

경로가 완전히 설정된 이후에 build()메소드는 UriComponents 객체를 구성하기 위해 호출된다. toUri()에 대한 호출을 통해서 새로 생성된 Spittle flthtmdml URI를 얻을 수 있다.


4. REST 리소스 사용하기

RestTemplate이 RESTful 리소스 사용의 지루함을 막아준다.

4.1 RestTemplate 동작 살펴보기


*RestTemplate은 11개의 고유동작을 정의하고, 각각은 총 36개의 메소드에 오버로드 된다.

  • delete() : 특정 URL의 리소스에 HTTP DELETE를 수행한다.
  • exchange() : URL에 대한 특정 HTTP메소드를 실행하고, 응답 바디로부터 매핑된 객체가 포함된 ResponseEntity를 반환한다.
  • execute() : URL에 대한 특정 HTTP메소드를 실행하고, 응답 바디로 부터 매핑된 객체를 반환한다.
  • getForEntity() : HTTP GET요청을 보내고, 객체 대한 매핑으로 응답 바디가 포함된 ResponseEntity를 반환한다.
  • getForObjet() : HTTP GET 요청을 보내고, 응답 바디로 부터 매핑된 객체를 반환한다.
  • headForHeaders() : HTTP HEAD 요청을 보내고, 특정 리소스 URL에 대한 HTTP헤더를 반환한다.
  • optionsForAllow() : HTTP OPTIONS 요청을 보내고, 특정 URL에 대한 Allow헤더를 반환한다.
  • postForEntity() : 데이터를 POST하고, 응답 바디로부터 매핑된 객체가 포함된 ResponseEntity를 반환한다.
  • postForLocation() : URL에 대한 데이터를 POST하고, 새로운 리소스의 URL을 반환한다.
  • postForObject() : URL에 대한 데이터를 POST하고, 객체에 대한 매핑으로 응답바디를 반환한다.
  • put() : 특정 URL에 대한 리소스를 PUT한다.

4.2 리소스 GET하기

getForObject() 메소드의 시그니처는 다음과 같다.

<T> T getForObject(URI url, Class<T> responseType)throws RestClientException;

<T> T getForObject(URI url, Class<T> responseType, Object... uriVariables)throws RestClientException;


<T> T getForObject(URI url, Class<T> responseType, Map<String, ?> uriVariables)throws RestClientException;


마찬가지로 , getForEntity()메소드의 시그니처는 다음과 같다.

<T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType)throws RestClientException;

<T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType, Object... uriVariables)throws RestClientException;

<T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType, Map<String, ?> uriVariables)throws RestClientException;


4.5 리소스 PUT하기

4.6 리소스 DELETE하기

리소스를 서버에 더 이상 보관하고 싶지 않다면 RestTemplate의 delete()메소드를 호출하면된다.

4.7 리소스 데이터 POST하기


4.10 리소스 교환

응답에서 헤더를 읽을 수 있다는 점은 유용하다. 하지만 서버에 전송하는 요청에 헤더를 설정하고 싶다면 RestTemplate의 exchange()메소드가 유용하다.


댓글

이 블로그의 인기 게시물

(네트워크)폴링방식 vs 롱 폴링방식

(ElasticSearch) 결과에서 순서 정렬

(18장) WebSocekt과 STOMP를 사용하여 메시징하기