REST 방식으로 전환
@RestController
REST 방식에서 가장 먼저 기억해야 하는 점은 서버에서 전송하는 것이 순수한 데이터라는 점이다.
기존 Controller에서 Model에 데이터를 담아서 JSP 등과 같은(View)로 전달하는 방식이 아니므로 기존의 Controller 와는 조금 다르게 동작한다.
예제프로젝트 준비
Spring Legacy Project를 이용해서 ex03 프로젝트를 생성한다.
pom.xml
// pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.codehows</groupId> <artifactId>controller</artifactId> <name>ex03</name> <packaging>war</packaging> <version>1.0.0-BUILD-SNAPSHOT</version> <properties> <java-version>11</java-version> <org.springframework-version>5.0.7.RELEASE</org.springframework-version> <org.aspectj-version>1.6.10</org.aspectj-version> <org.slf4j-version>1.6.6</org.slf4j-version> </properties> <dependencies> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${org.springframework-version}</version> <exclusions> <!-- Exclude Commons Logging in favor of SLF4j --> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${org.springframework-version}</version> </dependency> <!-- AspectJ --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>${org.aspectj-version}</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${org.slf4j-version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>${org.slf4j-version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${org.slf4j-version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> <exclusions> <exclusion> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> </exclusion> <exclusion> <groupId>javax.jms</groupId> <artifactId>jms</artifactId> </exclusion> <exclusion> <groupId>com.sun.jdmk</groupId> <artifactId>jmxtools</artifactId> </exclusion> <exclusion> <groupId>com.sun.jmx</groupId> <artifactId>jmxri</artifactId> </exclusion> </exclusions> </dependency> <!-- @Inject --> <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> </dependency> <!-- Servlet --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.0</version> <scope>provided</scope> </dependency> <!-- Test --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${org.springframework-version}</version> </dependency> <!-- 객체를 JSON/XML로 변환하기 위함 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.6</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.9.6</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-eclipse-plugin</artifactId> <version>2.9</version> <configuration> <additionalProjectnatures> <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature> </additionalProjectnatures> <additionalBuildcommands> <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand> </additionalBuildcommands> <downloadSources>true</downloadSources> <downloadJavadocs>true</downloadJavadocs> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.5.1</version> <configuration> <source>11</source> <target>11</target> <compilerArgument>-Xlint:all</compilerArgument> <showWarnings>true</showWarnings> <showDeprecation>true</showDeprecation> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.2.1</version> <configuration> <mainClass>org.test.int1.Main</mainClass> </configuration> </plugin> </plugins> </build> </project>
- 스프링 버전 수정, 자바 버전, Maven Compile 버전을 수정 하고 프로젝트 업데이트
- JSON 데이터를 처리하기 위한 jackson-databind라는 라이브러리 pom.xml에 추가.(jackson-databind 라이브러리는 나중에 브라우저에 객체를 JSON이라는 포맷의 문자열로 변환시켜 전송할 때 필요하다)
- 테스트 할 때는 직접 Java 인스턴스를 JSON 타입의 문자열로 변환해야하는 일들도 있으므로 gson라이브러리도 추가
- 작성된 프로젝트의 서블릿 버전을 수정하고, Lombok 관련 설정을 추가
- 테스트를 위해서 JUnit 버전을 변경하고, spring-test 관련 모듈을 추가
@RestController의 반환 타입
스프링의 @RestController는 특별히 기존의 @Controller와 다른점은 없다. org.codehows.controller 패키지에 SampleController를 생성한다.
단순 문자열 반환
@RestController는 JSP와 달리 순수한 데이터를 반환하는 형태이므로 다양한 포맷의 데이터를 전송할 수 있다. 주로 많이 사용하는 형태는 일반 문자열이나 JSON, XML등을 사용한다
SampleController에 문자열을 반환하려면 다음과 같은 형태로 작성한다.
// SampleController.java
package org.codehows.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 타입을 의미한다.
프로젝트의 실행은 http://localhost:8080/sample/getText 호출한다.
브라우저에 전송된 실제 데이터는 브라우저의 개발자 도구를 이용해서 확인할 수 있다.
결과를 보면 produces의 속성 값으로 지정된 text/plain 결과가 나오는 것을 확인할 수 있다.
객체의 반환
객체를 반환하는 작업은 JSON이나 XML을 이용한다. 전달된 객체를 생산하기 위해서 org.codehows.domain 패키지를 생성하고 SampleVO 클래스를 작성한다.
// SampleVO.java
package org.codehows.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;
}
SampleVO 클래스는 비어 있는 생성자를 만들기 위한 @NoArgsConstructor와 모든 속성을 사용하는 생성자를 위한 @AllArgsConstructor 어노테이션을 이용했다. 어노테이션을 통해서 생성 결과를 보면 생성자가 여러개 생성되는 것을 볼 수 있다.
SampleController에서는 SampleVO를 리턴하는 메서드를 아래와 같이 설계한다.
// SampleController.java
... 생략 ...
@GetMapping(value = "/getSample",
produces = { MediaType.APPLICATION_JSON_UTF8_VALUE,
MediaType.APPLICATION_XML_VALUE })
public SampleVO getSample() {
return new SampleVO(112, "스타", "로드");
}
}
getSample() 은 XML과 JSON 방식의 데이터를 생성할 수 있도록 작성되었는데, 브라우저에서 /sample/getSample을 호출하면 다음과 같은 화면을 볼 수 있다.
위의 결과는 브라우저가 받은 데이터가 XML이기 때문에 보이는 화면이다. 개발자 도구를 통해서 살펴보면 정상적인 XML데이터라는 것을 확인할 수 있다.
동일한 메서드를 /sample/getSample.json을 호출하게 되면 기존과 달리 JSON 타입의 데이터가 전달되는 것을 확인할 수 있다.
@GetMapping 이나 @RequestMapping의 produces 속성은 받느시 지정해야하는 것은 아니므로 생략하는 것도 가능하다.
컬렉션 타입의 객체 반환
경우에 따라서는 여러 데이터를 한 번에 전송하기 위해서 배열이나 리스트, 맵 타입의 객체들을 전송하는 경우도 발생한다.
SampleController의 일부
// SampleController.java
... 생략 ...
@GetMapping(value = "/getList")
public List<SampleVO> getList() {
return IntStream.range(1, 10).mapToObj(i -> new SampleVO(i, i+ "First", i + " Last"))
.collect(Collectors.toList());
}
}
getList() 는 내부적으로 1부터 10 미만까지의 루프를 처리하면서 SampleVO 객체를 만들어서 List<SampleVO>로 만들어 낸다.
http://localhost:8080/sample/getList / http://localhost:8080/sample/getList.json
맵의 경우에는 키와 값을 가지는 하나의 객체로 간주된다.
// SampleController.java
... 생략 ...
@GetMapping(value = "/getMap")
public Map<String, SampleVO> getMap() {
Map<String, SampleVO> map = new HashMap<>();
map.put("First", new SampleVO(111, "그루트", "주니어"));
return map;
}
http://localhost:8080/sample/getMap
http://localhost:8080/sample/getMap.json
ResponseEntity 타입
REST 방식으로 호출하는 경우는 화면 자체가 아니라 데이터 자체를 전송하는 방식으로 처리되기 때문에 데이터를 요청한 쪽에서는 정상적인 데이터인지 비정상적인 데이터인지를 구분할 수 있는 확실한 방법을 제공해야 한다,
ResponseEntity는 데이터와 함께 HTTP 헤더의 상태 메세지 등을 같이 전달하는 용도로 사용한다. HTTP의 상태 코드와 에러 메세지 등을 함께 데이터를 전달할 수 있기 때문에 받는 입장에서는 확실하게 결과를 알 수 있다.
// SampleController.java
... 생략 ...
@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.OK).body(vo);
}
return result;
}
check()는 반드시 height와 weight를 파라미터로 전달 받는다.
@RestController에서 파라미터
@RestController는 기존의 @Controller 에서 사용하던 일반적인 타입이나 사용자가 정의한 타입을 사용한다. 여기에 추가로 몇가지 어노테이션을 이용하는 경우가 있다.
- @PathVariable : 일반 컨트롤러에서도 사용이 가능하지만 REST 방식에서 자주 사용된다. URL 경로의 일부를 파라미터로 사용할 때 이용
- @RequestBody : JSON 데이터를 원하는 타입의 객체로 변환해야 하는 경우에 주로 사용
@PathVariable
스프링 MVC에서는 @PathVariable 어노테이션을 이용해서 URL 상에 경로의 일부를 파라미터로 사용할 수 있다.
위의 URL에서 { } 로 처리된 부분은 컨트롤러의 메서드에 변수로 처리가 가능하다. @PathVariable은 { } 의 이름을 처리할 때 사용한다
REST 방식에서는 URL 자체에 데이터를 식별 할 수 있는 정보들을 표현하는 경우가 많으므로 다양한 방식으로 @PathVariable이 사용된다
SampleController의 일부
// SampleController.java
... 생략 ...
@GetMapping("/product/{cat}/{pid}")
public String[] getPath(
@PathVariable("cat") String cat,
@PathVariable("pid") Integer pid) {
return new String[] { "category: " + cat, "productid: " + pid };
}
@PathVariable을 적용하고 싶은 경우에는 { } 를 이용해서 변수명을 지정하고, @PathVariable을 이용해서 지정된 이름의 변수값을 얻을 수 있다. 값을 얻을 때에는 int, double과 같은 기본 자료형은 사용할 수 없다.
브라우저에서 http://localhost:8080/sample/product/bags/1234 호출하면 cat과 pid 변수의 값으로 처리되는 것을 확인 할 수 있다.
@RequestBody
@RequestBody는 전달된 요청(Request)의 내용(body)을 이용해서 해당 파라미터의 타입으로 변환을 요구한다,
내부적으로 HttpMessageConverter 타입의 객체들을 이용해서 다양한 포맷의 입력 데이터를 변환할 수 있다. 대부분의 경우에는 JSON 데이터를 서버에 보내서 원하는 타입의 객체로 변환하는 용도로 사용됬지만, 경우에 따라서는 원하는 포맷의 데이터를 보내고, 이를 해석해서 원하는 타입으로 사용되기도 한다,
Ticket 클래스
// Ticket.java
package org.codehows.domain;
import lombok.Data;
@Data
public class Ticket {
private int tno;
private String owner;
private String geade;
}
Ticket 클래스는 번호(tno)와 소유주(owner), 등급(grade)을 지정한다.
Ticket을 사용하는 예제는 SampleController에 추가한다,
// SampleController.java
... 생략 ...
@PostMapping("/ticket")
public Ticket convert(@RequestBody Ticket ticket) {
log.info("convert .......ticket" + ticket);
return ticket;
}
SampleController의 다른 메서드와 달리 @PostMapping이 적용된 것을 볼수 있는데, 이것은 @RequestBody가 말 그대로 요청(request)한 내용(body)을 처리하기 때문에 일반적인 파라미터 전달방식을 사용할 수 없기 때문이다.
REST 방식의 테스트
위와 같이 GET 방식이 아니고, POST 등의 방식으로 지정되어 있으면서 JSON 형태의 데이터를 처리하는 것을 브라우저에서 개발하려면 많은 시간과 노력이 들어간다.
@RestController를 쉽게 테스트할 수 있는 방법은 주로 REST 방식의 데이터를 전송하는 툴을 이용하거나 JUnit과 spring-test를 이용해서 테스트 하는 방식을 고려할 수있다,
JUnit 기반의 테스트
JUnit을 이용하는 방식은 Part3 에서 진행했던 방식을 그대로 이용한다.
REST 방식을 이용하다 보면 JSON 테스트를 테스트해야 하므로 차이점 위주로 알아 두면 좋다.
src/test/java 폴더 아래 SampleControllerTests 클래스를 작성한다.
// SampleControllerTests.java
package org.codehows.controller;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.codehows.domain.Ticket;
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.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.google.gson.Gson;
import lombok.Setter;
import lombok.extern.log4j.Log4j;
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml",
"file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml"})
@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("/sample/ticket")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonStr))
.andExpect(status().is(200));
}
}
JUnit을 이용하는 방식의 테스트 장점은 역시 Tomcat을 구동하지 않고도 컨트롤러를 구동해 볼 수 있다는 점이다,
기타도구
JUnit을 이용하는 방식 외에도 Tomcat을 구동한다면 REST 방식을 테스트할 수 있는 여러 가지 도구들이 존재한다.
Chrome 브라우저 앱스토어(chrome://apps/)로 이동해서 REST client로 검색하면 꽤많은 크롬 확장 프로그램을 볼 수 있다.
Yet Another REST Client 추가 > 테스트 진행
http://localhost:8080/sample/ticket
{"tno":123, "owner":"user00", "grade":"AAA"}
결과
다양한 전송방식
REST 방식의 데이터 교환에서 가장 특이한 점은 기존의 GET/POST 외에 다양한 방식으로 데이터를 전달한다는 점이다.
HTTP의 전송방식은 아래와 같은 형태로 사용된다.
REST 방식은 URI와 같이 결합하므로 회원(member)이라는 자원을 대상으로 전송방식을 결합하면 다음과 같은 형태가 된다,
POST 방식도 그렇지만 PUT, DELETE 방식은 브라우저에서 테스트하기가 쉽지 않기 때문에 개발시 JUnit이나 Restlet Client 등과 같은 도구를 이용해서 테스트하고 개발해야 한다.
'Spring' 카테고리의 다른 글
스프링 - Part 4 - REST 방식과 Ajax를 이용하는 댓글 처리 02 (Ajax 댓글 처리 - 02) (0) | 2023.03.29 |
---|---|
스프링 - Part 4 - REST 방식과 Ajax를 이용하는 댓글 처리 02 (Ajax 댓글 처리 - 01) (0) | 2023.03.29 |
스프링 - Part 3 - 기본적인 웹 게시물 관리 09 (검색처리) (0) | 2023.03.28 |
스프링 - Part 3 - 기본적인 웹 게시물 관리 08 (페이징 화면 처리) (0) | 2023.03.27 |
스프링 - Part 3 - 기본적인 웹 게시물 관리 07 (MyBatis와 스프링에서 페이징 처리) (0) | 2023.03.27 |