코드로 배우는 스프링 웹 프로젝트-Part4.Rest 방식과 Ajax를 이용하는 댓글 처리/Chap16.Rest방식으로 전환
코드로 배우는 스프링 웹 프로젝트-16.Rest 방식으로 전환
코드로 배우는 스프링 웹 프로젝트 - 개정판
2019년 7월 10일 인쇄판
Part4. Rest 방식과 Ajax를 이용하는 댓글 처리
Chapter16. Rest 방식으로 전환
모바일 시대가 되며 WEB 분야의 가장 큰 변화는 서버 역할의 변화.
과거에는 서버 데이터를 소비하는 주체가 브라우저라는 특정한 앱으로 제한적이었다면 이제는 웹이나 앱이 서버에서 제공하는 데이터를 소비하게 된다.
과거의 서버는 브라우저라는 하나의 대상만을 상대로 데이터를 제공했으므로 아예 브라우저가 소화 가능한 모든 데이터를 HTML 형태로 전달하고 브라우저는 이를 화면에 보여주는 역할을 해 왔다.
스마트폰에서는 앱이라 불리는 고유 애플리케이션을 이용해 데이터를 소비하게 되고 보이는 화면 역시 자신만의 방식으로 서비스되게 된다.
앱에서 서버에 기대하는 것은 HTML이 아니라 그저 자신에게 필요한 순수 데이터가 되었다. 이처럼 서버의 역할은 점점 더 순수하게 데이터 처리를 목적으로 하는 형태로 진화하고 있다. 또한 브라우저와 앱은 서버에서 전달하는 데이터를 이용해 앱 혹은 브라우저 내부에서 별도의 방식으로 소비하는 형태로 전환하고 있다.
이러한 변화 속에서 웹의 URI 의미도 조금 다르게 변하기 시작했다. 예를 들어 과거의 웹페이지들은 페이지를 이동하더라고 브라우저 주소는 변화하지 않는 방식을 선호했다면 최근엔 주소 역시 같이 이동하는 방식을 사용한다.
REST는 Reoresentational State Transfer의 약어로 하나의 URI는 하나의 고유한 리소스를 대표하도록 설계된다는 개념에 전송방식을 결합해 원하는 작업을 지정한다.
예를 들어 /board/123은 게시물 중에서 123번이라는 고유 의미를 가지도록 설계하고 이에 대한 처리를 GET, POST 등으로 결정한다.
@RestController : Controller가 REST 방식을 처리하기 위한 것임을 명시
@ResponseBody : 일반적인 JSP와 같은 뷰로 전달되는 게 아니라 데이터 자체를 전달하기 위한 용도
@PathVariable : URL 경로에 있는 값을 파라미터로 추출할 때 사용
@CrossOrigin : Ajax의 크로스 도메인 문제를 해결해주는 어노테이션
@RequestBody : JSON 데이터를 원하는 타입으로 바인딩 처리
16.1 @RestController
REST 방식에서는 서버에서 전송하는 것이 순수한 데이터이다. 기존의 Controller에서 Model에 데이터를 담아 JSP 같은 뷰로 전달하는 방식이 아니라서 조금 다르게 동작한다.
스프링 4부터는 @Controller 외에 @RestController 어노테이션을 추가해 해당 Controller의 모든 메서드가 기존과 다르게 처리한다는 것을 명시한다. @RestController 이전에는 @Controller와 메서드 선언부에 @ResponseBody를 이용해 동일 결과를 만들 수 있었다.
16.1.1 예제 프로젝트 준비
SpringL Legacy Project로 ex03 생성
org.zerock.controller 지정.
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-xml -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.6</version>
</dependency>
PART3에서 추가했던 dependency 외에 jackson-databind 추가
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
위는 자바 인스턴스를 JSON 타입의 문자열로 변환하는 라이브러리.
16.2 @RestController의 반환 타입
org.zerock.controller에 SampleController 생성
package org.zerock.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.log4j.Log4j;
@RestController
@RequestMapping("/sample")
@Log4j
public class SampleController {
}
16.2.1 단순 문자열 반환
@RestCotroller는 JSP와 달리 순수 데이터를 반환하는 형태이므로 다양한 포맷 데이터를 전송할 수 잇다. 주로 많이 사용하는 건 문자열이나 JSON, XML이다.
package org.zerock.controller;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.log4j.Log4j;
@RestController
@RequestMapping("/sample")
@Log4j
public class SampleController {
@GetMapping(value="/getText", produces="text/plain; charset=UTF-8")
public String getText() {
log.info("MIME TYPE : " + MediaType.TEXT_PLAIN_VALUE);
return "안녕하세요";
}
}
기존의 @Controller는 문자열 반환하는 경우 JSP 파일 이름으로 처리하지만 @RestController는 순수 데이터가 된다. GetMapping에 사용된 produces 속성은 해당 메서드가 생산하는 MIME 타입을 의미한다.
produces 속성값으로 지정된 text/plain 결과가 나오는 것을 확인할 수 있다.
16.2.2 객체의 반환
객체 반환은 JSON이나 XML을 이용한다. 전달된 객체를 생산하기 위해 org.zerock.domain 패키지 작성, SampleVO 클래스
package org.zerock.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SampleVO {
private Integer mno;
private String firstName;
private String lastName;
}
비어 있는 생성자를 만들기 위한 @NoArgsCOnstructor와 모든 속성을 사용하는 생성자를 위한 @AllArgsConstructor 어노테이션
package org.zerock.controller;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zerock.domain.SampleVO;
import lombok.extern.log4j.Log4j;
@RestController
@RequestMapping("/sample")
@Log4j
public class SampleController {
@GetMapping(value="/getSample",
produces= {MediaType.APPLICATION_JSON_UTF8_VALUE,
MediaType.APPLICATION_XML_VALUE})
public SampleVO getSample() {
return new SampleVO(112, "스타", "로드");
}
}
SampleController의 메서드를 위와 같이 수정한다.
getSample은 XML과 JSON 방식 데이터를 생성할 수 있도록 작성되어 있다.
@GetMapping이나 @RequestMapping의 produces 속성은 반드시 지정해야 하는 건 아니므로 생략도 가능하다.
16.2.3 컬렉션 타입의 객체 반환
package org.zerock.controller;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zerock.domain.SampleVO;
import lombok.extern.log4j.Log4j;
@RestController
@RequestMapping("/sample")
@Log4j
public class SampleController {
@GetMapping(value="/getSample",
produces= {MediaType.APPLICATION_JSON_UTF8_VALUE,
MediaType.APPLICATION_XML_VALUE})
public SampleVO getSample() {
return new SampleVO(112, "스타", "로드");
}
@GetMapping(value="/getText", produces="text/plain; charset=UTF-8")
public String getText() {
log.info("MIME TYPE : " + MediaType.TEXT_PLAIN_VALUE);
return "안녕하세요";
}
//컬렉션 타입 객체 반환
@GetMapping(value="/getList")
public List<SampleVO> getList() {
return IntStream.range(1, 10).mapToObj(i->new SampleVO(i, i+"First", i+"Last"))
.collect(Collectors.toList());
}
}
URL을 호출하면 기본적으론 XML데이터를 전송하며 뒤에 확장자를 .json으로 하면 JSON 형태 배열 데이터를 볼 수 있다.
맵의 경우 키와 값을 가지는 하나의 객체로 간주된다.
@GetMapping(value="/getMap")
public Map<String, SampleVO> getMap() {
Map<String, SampleVO> map = new HashMap<>();
map.put("First", new SampleVO(111, "그루트", "주니어"));
return map;
}
}
Map을 이용하는 경우 키에 속하는 데이터는 XML로 변환되는 경우 태그의 이름이 되기 때문에 꼭 문자열로 지정
16.2.4 ResponseEntity 타입
REST 방식으로 호출하는 경우 데이터 자체를 전송하는 방식으로 처리되기 때문에 데이터를 요청한 쪽에서 정상적인 데이터인지 비정상적인 데이터인지 확연히 구분할 수 있는 방법을 제공해야 한다.
ResponseEntity는 데이터와 HTTP 헤더의 상태 메시지 등을 같이 전달하는 용도로 사용
HTTP의 상태 코드와 에러 메시지 등을 함께 데이터를 전달할 수 있어서 받는 입장에선 확실하게 결과를 알 수 있다.
@GetMapping(value="/check", params = {"height", "weight"})
public ResponseEntity<SampleVO> check(Double height, Double weight) {
SampleVO vo = new SampleVO(0, "" + height, ""+ weight);
ResponseEntity<SampleVO> result = null;
if(height<150) {
result = ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(vo);
} else {
result = ResponseEntity.status(HttpStatus.ACCEPTED.OK).body(vo);
}
return result;
}
}
만일 height 값이 150보다 작으면 502 상태 코드와 데이터를 전송하고 그렇지 않으면 200 코드와 데이터를 전송한다.
16.3 @RestController에서 파라미터
@RestController에선 기존의 @Controller에서 사용하던 일반적인 타입이나 사용자가 정의한 타입을 사용한다.
- @PathVariable : 일반 컨트롤러에서도 사용이 가능하지만 REST 방식에서 자주 사용. URL 경로 일부를 파라미터로 사용할 떄
- RequestBody : JSON 데이터를 원하는 타입 객체로 변환해야 하는 경우
16.3.1 @PathVAriable
REST 방식에선 URL에 최대한 많은 정보를 담으려 함. 예전부터 ? 뒤에 추가되는 쿼리 스트링이라는 형태로 파라미터를 이용해 전달하던 데이터들이 REST 방식에선 경로의 일부로 차요오디는 경우가 많다.
스프링 MVC에서 @PathVariable로 URL 상의 경로 일부를 파라미터로 사용할 수 있다.
http://localhost:8080/sample/{sno}
{}로 처리된 부분이 컨트롤러 메서드에서 변수 처리가 가능하다.
REST 방식에서 URL 자체에 데이터를 식별할 수 있는 정보들을 표현하는 경우가 많다.
SampleController 추가
@GetMapping("/product/{cat}/{pid}")
public String[] getPath(
@PathVariable("cat") String cat,
@PathVariable("pid") Integer pid
) {
return new String[] {"category : " + cat, "productid : " + pid};
}
}
16.3.2 @RequstBody
전달된 요청의 내용을 이용해 해당 파라미터 타입으로 변환을 요구. 내부적으로 HttpMessageConverter 타입 객체로 다양한 포맷의 입력 데이터를 변환할 수 있다. 대부분 JSON을 서버에 보내 원하는 타입의 객체로 변환하는 용도로 사용되지만 경우에 따라선 원하는 포맷의 데이터를 보내고 이를 해석해서 원하는 타입으로 사용하기도 한다.
org.zerock.domain에 Ticket 클래스 정의
package org.zerock.domain;
import lombok.Data;
@Data
public class Ticket {
private int tno;
private String owner;
private String grede;
}
SampleController 추가
@PostMapping("/ticket")
public Ticket convert(@RequestBody Ticket ticket) {
log.info("convert.........ticket" + ticket);
return ticket;
}
다른 메서드와 달리 @PostMapping이 적용된다.
@ResponseBody가 말 그대로 요청한 내용을 처리하기 떄문에 일반적인 파라미터 전달방식을 사용할 수 없기 때문이다.
16.4 REST 방식의 테스트
위와 같이 GET이 아닌 POST 방식으로 지정되어 있으면서 JSON 형태 데이터를 처리하는 것을 브라우저에서 개발하려면 많은 시간과 노력이 들어간다.
16.4.1 JUnit 기반 테스트
SampleControllerTests 클래스 작성
package org.zerock.controller;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.zerock.domain.Ticket;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import com.google.gson.Gson;
import lombok.Setter;
import lombok.extern.log4j.Log4j;
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration //Test for Controller
@ContextConfiguration({
"file:src/main/webapp/WEB-INF/spring/root-context.xml",
"file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml"
})
//Java Config
//@ContextConfiguration(classes={org.zerock.config.RootConfig.class, org.zerock.config.ServletConfig.class})
@Log4j
public class SampleControllerTests {
@Setter(onMethod_ = {@Autowired})
private WebApplicationContext ctx;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(ctx).build();
}
@Test
public void testConvert() throws Exception {
Ticket ticket = new Ticket();
ticket.setTno(123);
ticket.setOwner("Admin");
ticket.setGrade("AAA");
String jsonStr = new Gson().toJson(ticket);
log.info(jsonStr);
mockMvc.perform(post("/sampe/ticket")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonStr))
.andExpect(status().is(200));
}
}
16.4.2 기타 도구
JUnit 말고도 TOmcat을 구동한다면 REST 방식을 테스트할 수 있는 여러 도구들이 존재한다.
최근에는 브라우저에서 직접 REST를 테스트 가능.
크롬 브라우저 앱스토어에서 REST client로 검색하면 많은 확장 프로그램을 볼 수 있다.
Restlet Client 추천.
16.5 다양한 전송방식
Create : POST
Read : GET
Update : PUT
Delete : DELETE