Spring MVC/04.Rest

01.Rest

  • -

REST

REST란 Representational State Transfer의 약자로 하나의 URI는 하나의 고유한 리소스와 연결되며 이 리소스를 GET/POST/PUT/DELETE 등 HTTP 메서드로 제어하자는 개념이다.

Representational은 웹 상의 상태를 표현할 수 있는 어떤 자원을 의미하고 이 자원의 상태(State) 즉 데이터를 전송(Transfer)하는 것으로 해석해 볼 수 있다.

특히 서버에 접근하는 클라이언트의 종류가 단순히 브라우저를 넘어 스마트폰, 다른 서비스 등으로 다양해지면서 화면에 대한 관심은 없고 데이터, 비즈니스 로직에만 관심 있는 경우가 많은데 이때 사용되는 것이 REST이다.

 

자바나 C와 같은 언어에서 어떤 기능을 제공하는 것을 API라고 하듯이 REST 형태로 사용자가 원하는 기능을 제공하는 것REST API라고 한다. 최근에 많이 활용되는 공공 API들이 대표적인 예이다. 또한 REST 방식으로 서비스를 제공하는 것을 "Restful 하다"라고 말한다.

 

REST API의 URI 작성

 

일반적으로 URI는 다음의 규칙으로 만드는데 관습일 뿐 강제되지는 않는다.

  • 일반적인 URL의 형태를 가지므로 소문자를 사용하며 긴 문자는 언더바(_) 대신 하이픈(-)을 사용하며 마지막에 /를 사용하지 않는다. (어차피 URI 자체는 @Controller에서 경로문자열로 처리하므로 서버에서는 문제될게 없다.)
    • X: http://quietjun.com/rest/User_Profiles/
    • O: http://quietjun.com/rest/user-profiles
  • REST API를 만들면서 URI는 정보 자체를 표현하기 때문에 대부분 명사로 나타낸다. 어떤 행위를 의미하는 동사는 HTTP 메서드인 GET/POST/PUT/DELETE로 나타낸다.
    • X: GET: http://quietjun.com/rest/orders/show/1
    • O: GET: http://quietjun.com/rest/orders/1
  • 요소는 복수(collection)로 선언하고 단수를 표현할 때에는 하위 단계로 나타내며 하위 collection을 가질 수 있다.
    • GET: http://example.com/api/orders
    • GET: http://example.com/api/orders/1
    • GET: http://example.com/api/orders/1/items/3

 

다음 예는 http://example.com/api/orders라는 라는 URI를 여러 HTTP method로 접근했을 때의 의미이다.

Http Method 동작
GET 전체 리소스에 대한 정보 획득 http://example.com/api/orders
: 모든 주문 내역 조회
특정 조건에 맞는 모든 정보 획득 http://example.com/api/orders?from=100
 : 100번 주문부터 모든 주문 내역 조회
특정 리소스에 대한 정보 획득 http://example.com/api/orders/123
: 123번 주문에 대한 상세 내역 조회
POST 새로운 리소스 저장 http://example.com/api/orders
: 새로운 주문 생성. 파라미터는 request body로 전달
PUT 기존 리소스 업데이트 http://example.com/api/orders/123
: 123번 주문에 대한 수정. 파라미터는 request body로 전달
DELETE 기존 리소스 삭제 http://example.com/api/orders/123
: 123번 주문 삭제

 

자주 사용되는 HTTP method들의 주요 특성도 한번 점검하고 가면 좋다.

Hypertext Transfer Protocol - Wikipedia

 

Hypertext Transfer Protocol - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Application protocol for distributed, collaborative, hypermedia information systems Hypertext Transfer ProtocolInternational standardRFC 1945 HTTP/1.0 (1996) RFC 2068 HTTP/1.1 (1997)

en.wikipedia.org

 

PUT과 유사한 개념인 PATCH의 차이점에 대해서도 살펴보자.

https://goodteacher.tistory.com/538

프로젝트 구성

REST 처리를 위한 프로젝트를 구성해 보자.

기존의 MyBatis 관련 포스트에서 사용했던 src 폴더를 그대로 가져온 후 필요한 부분을 변경시켜 주자.

추가로 pom.xml에 com.github.pagehelper에 대한 maven dependency 가 필요하다.

<dependency>
	<groupId>com.github.pagehelper</groupId>
	<artifactId>pagehelper-spring-boot-starter</artifactId>
	<version>1.2.13</version>
</dependency>

 

이제 사용할 빈들의 위치가 BootCh06RestApplication과 다른 패키지에 있으므로 BootCh06RestApplication에 @ComponentScan을 추가해서 com.eshome.db 패키지를 스캔하도록 설정해 주자. @MapperScan도 역시 패키지가 달라졌기 때문에 필요하다. BootCh04MyBatisApplication.java는 불필요하므로 삭제한다.

 

@SpringBootApplication
@ComponentScan({"com.eshome.db.model", "com.eshome.rest"})
@MapperScan(basePackageClasses = CityRepo.class)
public class BootCh06RestApplication {

    public static void main(String[] args) {
        SpringApplication.run(BootCh06RestApplication.class, args);
    }
}

 

기존에 작성되었던 단위테스트를 실행해 보고 정상적으로 잘 작동하는지 확인해 보자.

 

Spring에서의 REST

스프링은 REST를 편리하게 사용할 수 있도록 다양한 애너테이션들을 제공한다.

@PathVariable

Rest는 URI template을 구성해서 리소스를 가리킨다. 즉 http://example.com/api/orders/123 에서 123에 해당하는 값은 매번 바뀌어야 다른 값을 조회할 수 있을 것이다. 이런 경우 @RequestMapping의 path는 /api/orders/{orderNo}와 같은 형태로 구성한다.

@PathVariable은 말 그대로 Path 즉 경로상에 변수(path variable)를 참조할 때 사용된다. @PathVariable은 handler method의 파리미터에 사용할 수 있는 애너테이션으로 path variable을 자바 변수로 할당한다.

controller를 추가하고 다음과 같이 handler method를 만들어보자.

package com.eshome.rest.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Controller
public class HtmlService {

    @GetMapping("/api/{aptNm}/{dong}")
    public String pathVariableTest(@PathVariable String aptNm, @PathVariable Integer dong) {
        return String.format("전달 받은 값은: %s %s", aptNm, dong);
    }
}

@GetMapping의 path에 aptNm, dong이 path variable로 선언되어 있고 handler method에서 @PathVariable을 이용해서 참조하는 것을 볼 수 있다. dong의 경우에서 알 수 있듯이 @PathVariable도 @RequestParam처럼 자동 형변환을 지원한다.

이 메서드를 요청하기 위해서는 다음과 같이 브라우저에서 호출한다.

http://localhost:8080/api/개나리아파트/1104

 

하지만 위와 같이 호출했을 때 제대로 된 결과 페이지가 출력될 거라는 희망을 가진 사람은 없을 것이다. "전달받은 ~~. html"과 같은 페이지를 만든 적은 없기 때문이다.

REST는 <html>이 아니라 데이터에만 관심 있다는 것을 기억해 주자!!

 

@ResponseBody

@ResponseBody는 응답 즉 response를 바로 <body>에 써버리는 용도이다. 즉 @ResponseBody가 사용되면 return 된 문자열이 view resolver를 거치지 않고 곧바로 클라이언트로 전달된다.

위 handler method에 @ResponseBody를 추가하고 브라우저에서 호출해 보자.

@GetMapping("/api/{aptNm}/{dong}")
@ResponseBody
public String pathVariableTest(@PathVariable String aptNm, @PathVariable Integer dong) {
    return String.format("전달 받은 값은: %s %s", aptNm, dong);
}

 

@ResponseBody는 handler method와 Controller 클래스에 선언할 수 있다.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {

}

Controller 클래스에 @ResponseBody를 선언하면 해당 Controller에 있는 모든 handler method들은 @RsponseBody로 간주된다.

 

@RestController

Rest 서비스가 필요할 때마다 메서드에 @ResponseBody를 선언하기는 귀찮다. 그래서 일반적으로 Rest 서비스를 위한 Controller와 일반 서비스를 위한 Controller로 나눠서 사용하는 경우가 많다. 이때 @Controller와 @ResponseBody를 두 개 쓰기 번거롭다면 @RestController를 사용하자. @RestController는 @Controller와 @ResponseBody를 포함한다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
	@AliasFor(annotation = Controller.class)
	String value() default "";
}

 

@RestController를 사용하면 보다 편리하게 Rest 서비스 구현이 가능하다.

package com.eshome.rest.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class RestServiceController {

    @GetMapping("/always")
    public String alwaysRestService() {
        return "언제나 rest";
    }
}

 

REST 서비스하는 handler method의 리턴 타입으로는 문자열은 물론 DTO나 Collection 등도 반환 가능하다. 이들은 MessageConverter에 의해 JSON 또는 XML 형태의 문자열로 변환해서 리턴된다.

다음은 DTO 형태의 객체가 반환되고 JSON 형태로 출력되는 예이다.

@GetMapping("/city")
public City city() {
    return new City("서울", "KO", "Asia", 50000000);
}

다음은 List가 반환돼서 출력되는 예이다.

@GetMapping("/list")
public List<String> list() {
    List<String> fruits = Arrays.asList("banana", "apple");
    return fruits;
}

 

@RequestBody

@ResponseBody가 응답을 처리하기 위한 애너테이션이었다면 @RequestBody는 request의 body를 통해 application/json 형태로 전달되는 데이터를 객체로 변환하기 위해서 사용된다.

application/x-www-form-urlencoded로 전달되는 경우는 AJAX라도 @RequestParam으로 처리한다.
@PutMapping("/city")
public Boolean insertCity(@RequestBody City city) {
    log.trace("도시 저장: {}", city);
    return true;
}

 

Rest 테스트를 위한 클라이언트

REST 서비스가 잘 동작하는지 브라우저를 통해서 테스트하기는 번거로움이 있다. 많은 경우 별도의 프로그램을 이용하는데 여기서는 talend api tester라는 chrome plugin을 사용해 보자.

https://chrome.google.com/webstore/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm

 

Talend API Tester - Free Edition

Visually interact with REST, SOAP and HTTP APIs.

chrome.google.com

설치는 어렵지 않으니 스스로 진행해 보고 사용 방법은 실제 API를 만들고 테스트해 보기로 한다.

 

REST API 개발

Country의 정보 관리를 위한 REST API를 개발해 보자.

 

전체 국가 조회

먼저 가장 간단한 예로 전체 국가 목록을 반환시켜 보자. Controller에서는 당연히 Service를 써야 하지만 만들어놓은 서비스가 없는 관계로 그냥 Repository를 사용한다. 나쁜 코드다.ㅜㅜ

전체 국가 정보를 한 번에 다 받아오면 부담스럽기 때문에 Paging 해서 가져온다. 이때 페이징 정보는 query string을 이용해서 파라미터로 전달받는다.

@Autowired
CountryRepo repo;

@GetMapping("/countries")
public Page<Country> allCountries(@RequestParam Integer per, @RequestParam Integer page) {
    PageHelper.startPage(page, per);
    return repo.selectAll();
}

 

이를 테스트하기 위해 Talend API Tester를 써보자.

 

  1. METHOD에서 사용할 METHOD를 선택한다. 여기서는 GET이다.
  2. 테스트하려는 URI를 작성한다. 여기서는 http://localhost:8080/api/countries이다.
  3. Query Parameters를 선택하고 파라미터들을 기입하면 자동으로 2번 항목의 URI에 추가된다.
  4. Send를 눌러 request를 전송한다.

요청 결과는 하단의 Response에 표시된다.

상태코드가 200으로 성공적으로 동작하였고 BODY 부분에 데이터가 잘 도달한 것을 알 수 있다.

 

ResponseEntity

앞선 예에서는 단순히 조회된 데이터를 클라이언트로 전송하도록만 처리되고 있다. 그런데 많은 경우 조회된 데이터와 함께 HTTP 상태 코드는 물론 요청의 성공 여부, 실패 시 오류 내용 등 다양한 내용을 전달할 필요가 있다.

이런 경우 ResponseEntity를 사용한다.

    @GetMapping("/countries")
    public ResponseEntity<Map<String, Object>> allCountries(@RequestParam Integer per, 
                                                            @RequestParam Integer page) {
        PageHelper.startPage(page, per);
        ResponseEntity<Map<String, Object>> entity = null;
        try {
            Page<Country> data = repo.selectAll();
            entity = handleSuccess(data);
        } catch (RuntimeException e) {
            entity = handleException(e);
        }
        return entity;
    }

    private ResponseEntity<Map<String, Object>> handleSuccess(Object data) {
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("success", true);
        resultMap.put("data", data);
        return new ResponseEntity<Map<String, Object>>(resultMap, HttpStatus.OK);
    }

    private ResponseEntity<Map<String, Object>> handleException(Exception e) {
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("success", false);
        resultMap.put("data", e.getMessage());
        return new ResponseEntity<Map<String, Object>>(resultMap, HttpStatus.INTERNAL_SERVER_ERROR);
    }

 

이제 요청 성공 시 Map을 통해 Country 정보(data) 뿐 아니라 성공 여부(success)가 HttpStatus.OK 즉 200 상태 코드와 함께 전달된다. 또한 실패 시에는 성공 여부와 오류 원인(data)과 500 상태 코드로 서버의 오류 상태를 전달한다.

 

특정 국가 조회

이번에는 Path variable을 사용할 수 있도록 특정 국가 정보를 조회해 보자.

@GetMapping("/countries/{code}")
public ResponseEntity<Map<String, Object>> countries(@PathVariable String code) {
    ResponseEntity<Map<String, Object>> entity = null;
    try {
        Country country = repo.selectDetail(code);
        entity = handleSuccess(country);
    } catch (RuntimeException e) {
        entity = handleException(e);
    }
    return entity;
}

 

국가 정보  추가

이번에는 request body를 통해서 들어오는 JSON 데이터를 이용해서 국가 정보를 추가해 보자.

@PostMapping("/countries")
public ResponseEntity<Map<String, Object>> insertCountry(@RequestBody Country country) {
    ResponseEntity<Map<String, Object>> entity = null;
    try {
        Integer result = repo.insert(country);
        entity = handleSuccess(result);
    } catch (RuntimeException e) {
        entity = handleException(e);
    }
    return entity;
}

 

Talend를 이용해 테스트할 때 이전과 달라지는 점은 METHOD에 POST로 설정하고 Headers 부분에 Content를 application/json으로 설정한다. 파라미터는 BODY 영역에 JSON 포맷을 맞춰서 입력한다.

 

테스트가 성공적으로 이뤄진다면 아래의 결과를 받아볼 수 있다.

 

국가 정보 수정

다음은 PUT을 이용해서 국가 정보를 수정해 보자.

@PutMapping("/countries")
public ResponseEntity<Map<String, Object>> updateCountry(@RequestBody Country country) {
    ResponseEntity<Map<String, Object>> entity = null;
    try {
        Integer result = repo.update(country);
        entity = handleSuccess(result);
    } catch (RuntimeException e) {
        entity = handleException(e);
    }
    return entity;
}

테스트는 METHOD 부분만 PUT으로 변경하면 추가와 동일하므로 생략한다.

 

국가 정보 삭제

마지막으로 DELETE를 통해 국가 정보를 삭제해 보자.

@DeleteMapping("/countries/{code}")
public ResponseEntity<Map<String, Object>> deleteCountry(@PathVariable String code) {
    ResponseEntity<Map<String, Object>> entity = null;
    try {
        Integer result = repo.delete(code);
        entity = handleSuccess(result);
    } catch (RuntimeException e) {
        entity = handleException(e);
    }
    return entity;
}

 

 

cross-domain request 처리

REST로 개발된 서비스는 많은 경우 웹에서는 JavaScript를 이용한 AJAX로 요청하는 경우가 많다. 그런데 JavaScript의 요청은 보안에 취약해서 여러 제약사항들이 존재하고 그중 하나가 SOP 정책이다.

 

SOP와 대책

SOP(Same Origin Policy: 동일 근원 정책)은 JavaScript에서 Ajax를 이용해 서버 자원을 호출할 때 동일한 scheme, 동일한 도메인, 동일한 포트에서의 요청에 대한 데이터 전송만을 허용한다는 점이다.

 

이 경우 아래와 같은 오류가 발생한다.

XMLHttpRequest cannot load http://127.0.0.1:8080/~~~. 
No 'Access-Control-Allow-Origin' header is present on the requested resource. 
Origin 'http://localhost:8080' is therefore not allowed access.

 

위 상황에 대해 좀 더 궁금하다면?

https://goodteacher.tistory.com/297

 

SOP 문제와 CORS 처리

SOP 문제와 처리 AJAX를 생각없이 사용하다 보면 가장 많이 접하는 오류 중 하나가 SOP 오류이다. Access to XMLHttpRequest at 'http://openapi.molit.go.kr/OpenAPI_ToolInstallPackage/service/rest/RTMSOBJSvc/getRTMSDataSvcAptTradeDe

goodteacher.tistory.com

 

이 문제를 처리하기 위해서 서버에서 명시적으로 다른 도메인에서의 접근을 허용해줘야 한다. 이때 @CrossOrigin 애너테이션을 사용한다.

@RestController
@RequestMapping("/api")
@Slf4j
@CrossOrigin("*")
public class RestServiceController {...}

 

@CrossOrigin의 value에는 접근을 허용할 origin들을 배열 형태로 적어주는데 모든 접근을 허용하려면 *를 사용한다.

 

하지만 만약........ 내가 서버를 수정할 권한이 없고 서버에서 @CrossOrigin 처리를 해주지 않으면 어떻게 될까? 이런 경우는 RestTemplate을 사용할 수 있다.

https://goodteacher.tistory.com/267?category=828440 

 

03. RestTemplate

RestTemplate 가끔 REST API를 자바 영역에서 사용해야할 경우가 있다. 예를 들어 허용되지 않은 도메인에서 REST 서비스를 이용해야 할 때도 마찬가지이다. 자바 영역에서 REST 서비스를 편리하게 사용

goodteacher.tistory.com

 

'Spring MVC > 04.Rest' 카테고리의 다른 글

[swagger]swagger와 interceptor  (0) 2021.12.09
[springboot]CORS 설정  (0) 2021.09.08
[spring boot]MockMVC 테스트시 response content의 한글 깨짐  (0) 2021.08.30
03. RestTemplate  (0) 2020.07.14
02. REST를 위한 단위 테스트  (0) 2020.07.13
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.