본문 바로가기

JAVA/Spring Boot

REST API

반응형

REST API

1) 모델 클래스 생성

package ee.swan.web.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Todo {
    private int id;
    private String title;
}

2) 컨트롤러 클래스 생성

package ee.swan.web;

import ee.swan.web.dto.Todo;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/basic")
public class BasicController {
    private final AtomicInteger counter = new AtomicInteger();

    @GetMapping("/todo")
    public Todo basic() {
        return new Todo(counter.incrementAndGet(), "분석하기");
    }
}
  • RestController 애너테이션은 기존 컨트롤러 애너테이션에서 ResponseBody 애너테이션을 추가.
  • RestController을 사용하면 별도의 ResponseBody 애너테이션을 사용하지 않고 REST API 생성 가능.
  • ResponseBody는 실행경과에 대한 처리를 위한 애너테이션.
  • ResponseBody가 있으면 실행 결과는 View를 거치지 않고 HTTP Response Body에 직접 입력.
  • 응답에 실행결과를 작성하게 된 상태에서 MappingJacksonHttpMessageConverter를 통해 JSON형태로 결과로 표현.

결과

{"id":1,"title":"분석하기"}

스프링 3.1 부터는 JSON 표현에 별도의 메시지 컨버팅 설정을 하지 않아도 JAXB2와 Jackson 라이브러리만 클래스패스에 추가되어 있으면 JSON으로 자동 변환이 되었는데, 스프링 부트에서는 해당 라이브러리를 이미 포함하고 있으므로 JSON으로 표현된것이다.

자바의 동시성 문제를 처리하기 위해 java.util.concurrent.atomic 패키지.
Long 타입으로 사용하면 서로 다른 스레드에서 하나의 변수에 대해 값을 쓰거나 읽기 때문에 문제가 발생 가능.
따라서 AtomicLong을 사용하면 Long 타입의 변수에 대해서 스레드 세이프 하게 처리됨.

POST 메서드

  • REST API에서는 POST 메서드는 새로운 리소스를 생성할때 사용.
  • GET은 기본적으로 요청에 대해서 같은 응답임을 보장.
  • POST은 같은 응답을 보장하지 않음.
  • 브라우저에서도 GET 방식으로 요청을 하고 난 뒤에 '뒤로가기' 버튼을 클릭해도 아무런 변화가 없음.
  • POST일때는 '뒤로가기' 버튼을 클릭하면 '양식을 다시 제출확인'이란 문구가 모든 브라우저에서 공통적으로 발생.

3) POST 메서드 생성

    @PostMapping("/todo")
    public Todo postBasic(@RequestParam("todoTitle") String todoTitle) {
        return new Todo(counter.incrementAndGet(), todoTitle);
    }
  • /basic/todo를 엔드포인트로 하고 메서드를 POST로 지정.
  • @RequestParam 애너테이션의 값을 키로 해서 클라이언트에서 파라미터 값을 전달받음.
  • POST는 URL을 직접 호출할수 없음.
  • GET은 body가 없어서 URL을 직접 호출 가능.
  • POST경우에는 요청이 몸체가 되므로 POST 메서드가 사용하는 폼을 만들거나 별도의 도구를 사용해야 함.

응답 헤더 활용

  • REST API에서 응답 헤더는 클라이언트에게 메타 정보로 활용될수 있다.
  • 클라이언트에서 헤더 정보를 먼저 읽을수 있으므로 본문 내용을 읽을 필요가 없어서 통신 효율이 좋고 요청결과에 대한 명확한 결과를 전달할수 있다.
  • 스프링에서는 응답 헤더에 대한 구현체로 ResponseEntity 클래스를 제공한다.
  • ResponseEntity 클래스는 HttpEntity를 상속받는 클래스로 Http 응답에 대한 상태 값을 표현할수 있다.

4) ResponseEntity 메서드 생성

    @PostMapping("/todo3")
    public ResponseEntity<Todo> postBasicReponseEntity(@RequestParam("title") String title) {
        return new ResponseEntity(new Todo(counter.incrementAndGet(), title), HttpStatus.CREATED);
    }
  • postBasicResponseEntity 메서드를 만들고 반환할때 ResponseEntity를 사용해서 상태 값을 나타낸다.

스프링에서 URI 템플릿 활용

  • 일관성 있는 REST API를 만들기 위해서 URI 템플릿을 사용한다.

PathVariable을 이용한 URL 표현

  • 스프링에서 PathVariable을 이용해서 URI 템플릿을 구현할수 있다.
    @GetMapping("/totos/{todoId}")
    public Todo getPath(@PathVariable("todoId") int todoId) {
        Todo todo1 = new Todo(1L, "분석하기");
        Todo todo2 = new Todo(2L, "설계하기");
        Todo todo3 = new Todo(3L, "테스트하기");

        Map<Integer, Todo> todoMap = new HashMap<>();
        todoMap.put(1, todo1);
        todoMap.put(2, todo2);
        todoMap.put(3, todo3);

        return todoMap.get(todoId);
    }

HATEOAS

HATEOAS를 이용한 자기주소 정보 표현

  • 웹에서 리소스의 위치는 URI를 이용해서 알수 있다.
  • 스프링의 PathVariable을 이용해서 실제로 여러 요소 중에서 특정 요소를 URI 템플릿을 이용해서 표현하는 방법이 있다.
  • REST API에서 데이터를 요청할때는 리소스의 위치에 대해서 알수 있게 된다.
  • 그런데 REST API를 사용하는 클라이언트들은 하나의 트랜잭션 안에서 여러 API를 사용하는 경우도 있고 기본이 되는 URI은 공통으로 사용하지만 하위 파라미터나 하위 URL에 대해서는 인지하지 못하는 경우가 많다.
  • 전체 URI 요청 파라미터에 따라서 변경될수 있기 때문이다.
  • 스프링에선 HATEOAS를 이용해서 해당 결과를 얻을수 있는 전체 URI를 반환할수 있다.

1) HATEOAS 설정

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-hateoas</artifactId>
        </dependency>

2) 모델 클래스 수정

  • Todo 모델클래스가 EntityModel 클래스를 상속받도록 한다.
  • TodoResource 클래스를 생성하면서 EntityModel 상속받는다.
package ee.swan.web.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.hateoas.RepresentationModel;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TodoResource extends RepresentationModel {
    private String title;
}

3) 컨트롤러 메서드 추가

    @GetMapping("/todoh")
    public ResponseEntity<TodoResource> geth(@RequestParam("title") String title) {
        TodoResource todoResource = new TodoResource(title);
        todoResource.add(linkTo(methodOn(BasicController.class).geth(title)).withSelfRel());
        return new ResponseEntity(todoResource, HttpStatus.OK);
    }
  • todoResource 인스턴스를 생성후 링크 정보 추가를 위해 linkTo 메서드로 BasicController클래스의 geth() 메서드를 매핑한다.
  • 그후 withSelfRel 메서드를 이용해 URL 정보를 만들고 add 메서드로 해당 정보를 추가한다.

결과

{
  "title": "coding",
  "_links": {
    "self": {
      "href": "http://localhost:8080/basic/todoh?title=coding"
    }
  }
}

4) 다른 방식으로 사용

    @GetMapping("/todoh2")
    public EntityModel<Todo> todoh2(@RequestParam("title") String title) {
        Todo todo = new Todo();
        todo.setTitle(title);
        EntityModel<Todo> todoEntityModel = EntityModel.of(todo);
        todoEntityModel.add(linkTo(methodOn(BasicController.class).todoh2(title)).withSelfRel());
        return todoEntityModel;
    }

    @GetMapping("/todoh3")
    public ResponseEntity<TodoResource> todoh3(@RequestParam("title") String title) {
        TodoResource todoResource = new TodoResource(title);
        todoResource.add(Link.of("http://localhost:8080/basic/todoh3?title="+title));
        return new ResponseEntity(todoResource, HttpStatus.OK);
    }    
  • Representation models 변경

버전업
ResourceSupport -> RepresentationModel
Resource -> EntityModel
Resources -> CollectionModel
PagedResources -> PagedModel

REST API 문서화

1) swagger 설정 및 라이브러리 추가

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.4.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.4.0</version>
        </dependency>

2) SwaggerConfig

package ee.swan.config;

import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.hateoas.client.LinkDiscoverer;
import org.springframework.hateoas.client.LinkDiscoverers;
import org.springframework.hateoas.mediatype.collectionjson.CollectionJsonLinkDiscoverer;
import org.springframework.plugin.core.SimplePluginRegistry;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import static springfox.documentation.builders.PathSelectors.regex;

@Configuration
@EnableSwagger2
public class SwagggerConfig {

//    @Bean
//    public UiConfiguration uiConfig() {
//        return UiConfiguration.DEFAULT;
//    }

    @Primary
    @Bean
    public LinkDiscoverers discoverers() {
        List<LinkDiscoverer> plugins = new ArrayList<>();
        plugins.add(new CollectionJsonLinkDiscoverer());
        return new LinkDiscoverers(SimplePluginRegistry.create(plugins));
    }

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                //.paths(regex("/basic/.*"))
                .build()
                .apiInfo(metadata());
    }

    private ApiInfo metadata() {
        return new ApiInfoBuilder()
                .title("eeswan's spring boot")
                .description("spring boot Rest API")
                .version("1.0")
                .build();
    }
}

REST 클라이언트 개발

  • MSA가 유행하면서 업무별로 API를 만들어서 서로 API 간 통신을 통해 데이터를 주고 받는경우가 많다.
  • 때때로 API 서버를 만들기 위해 다른 서버 API를 연동해야 한다.
  • 스프링 프레임워크에서는 REST 서버와 연동을 위해 RestTemplate를 제공한다.

RestTemplate

  • 기존에도 Apache HttpClient, OKHttp와 같은 클라이언트 라이브러리들이 많이 있었다.
  • RestTemplate은 스프링 MVC 라이브러리에 포함되는 클래스로 스프링 3.2이상이면 사용할수 있다.
  • 대개 클라이언트 라이브러리들의 제공 형태는 get, post와 같은 HTTP 메서드에서 대응되는 메서드를 제공해주는 형태다.
  • 그래서 데이터를 요청하는 측면에서 사용성에 차이가 없다.
  • 그런데 REST API와 연동시에 HTTP 요청을 보내는것뿐만 아니라 실제로 응답에 JSON 데이터를 파싱하고 모델 객체와 매핑하는 것이 중요하다.
  • RestTemplate은 다양한 메서지를 커버터를 이미 내장하고 있어서 JSON 응답을 Map 또는 모델 클래스로 간편하게 변환해서 사용할수 있다.

RestTemplate는 Apache httpClient에 대한 의존성을 가지고 있다.

1) RestTemplate 의존성

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

RestTemplate을 사용할때는 실제로 소스상에서는 Apache HttpClient의 CloseableHttpClient를 사용하는데,
CloseableHttpClient는 HttpClient를 CloseableHttpClient 타입으로 가지고 있다.
그래서 httpClient와 관련된 설정들은 HttpComponentsClientHttpRequestFactory 클래스의 인스턴스를 생성해서 set 메서드를 이용해 설정한다.

또다른 방법으로는 RequestConfig를 이용해서 설정한 후 RequestConfig를 HttpComponentsClientHttpRequestFactory 클래스 생성시에 전달하는 방법이 있다.

두 방법 모두 ClientHttpRequestFactory 클래스에 대한 인스턴스를 만든 후에 값을 설정해야 하므로 ClientHttpRequestFactory 클래스의 인스턴스를 생성할수 있는 메서드를 만든다.

2) ClientHttpRequestFactory 인스턴스 메서드 생성 방법1

private ClientHttpRequestFactory getClientHttpRequestFactory() {
    HttpComponentsClientHttpRequestFactory clientHttpRequestFactory =
        new HttpComponentsClientHttpRequestFactory();

        clientHttpRequestFactory.setConnectionRequestTimeout(5000);
        clientHttpRequestFactory.setReadTimeout(5000);
        return clientHttpRequestFactory;
}
  • clientHttpRequestFactory 인스턴스를 생성 한 후에 set 메서드를 이용해서 ConnectionRequestTimeout과 ReadTimeout 옵션을 설정한다.

  • Timeout의 단위는 밀리초이다.

  • 이렇게 설정한 옵션은 restTemplate.setReqeustFactory(getClientHttpRequestFactory())와 같이 설정할수 있다.

  • RequestConfig도 ClientHttpRequestFactory 인스턴스를 만드는 것과 동일하다

  • 다만 추가적으로 RequestConfig 인스턴스와 CloseableHttpClient 인스턴스를 생성해야 한다.

2) ClientHttpRequestFactory 인스턴스 메서드 생성 방법2

private ClientHttpRequestFactory getRestConfigHttpRequestFactory() {
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(5000)
                .setConnectionRequestTimeout(5000)
                .setSocketTimeout(5000)
                .build();
        CloseableHttpClient httpClient = HttpClientBuilder
                .create()
                .setDefaultRequestConfig(config)
                .build();
        return new HttpComponentsClientHttpRequestFactory(httpClient);
    }
  • RequestConfig 인스턴스를 생성한 후에 HttpClientBuilder을 이용해서 CloseableHttpClient 인스턴스를 생성하고 setDefaultRequestConfig 메서드로 config 값을 설정한 뒤에 HttpComponentsClientHttpRequestFactory 생성자에 httpClient 인스턴스를 전달한다.

3) 자바 기반 설정

package ee.swan.config;

import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ClientConfig {

    @Bean
    public ClientHttpRequestFactory clientHttpRequestFactory() {
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(5000)
                .setConnectionRequestTimeout(5000)
                .setSocketTimeout(5000)
                .build();
        CloseableHttpClient httpClient = HttpClientBuilder
                .create()
                .setDefaultRequestConfig(config)
                .build();
        return new HttpComponentsClientHttpRequestFactory(httpClient);
    }

    @Bean
    RestTemplate restTemplate() {
        return new RestTemplate(clientHttpRequestFactory());
    }
}

UriComponentBuilder 활용

  • REST API를 연동할때 가장 기본이 되는 정보는 URI이다.
  • URI는 도메인정보 API URI와 같은 형태로 되어 있고, 파라미터를 포함해서 전달하거나 URL PATH에 특정 키값이 있어야 하는 경우가 있다.
  • 다양한 API URL을 매핑하기 위해 문자열 조합하다보면 가독성이 떨어진다.
  • UriComponentBuilder 사용하면 편리하다.

UriComponentBuilder 생성

  • UriComponentBuilder의 생성자 접근 제한자는 protected로 되어있다.
  • 그래서 생성자를 직접 호출해서 사용할수 없고 newInstance 메서드를 이용해서 생성한다.
        UriComponentsBuilder.newInstance()
                .scheme("http")
                .host("movie.naver.com")
                .port(80)
                .path("/movie/sdb/rank/rmovie.nhn")
                .build()
                .encode()
                .toUri();

http:, ftp: 같은 URL 앞에 콜론 기호는 프로토콜을 나타낸다.
UriComponentBuilder에 scheme 정보에 해당한다.
https를 사용하는 API의 경우 scheme을 https로변경 한다.

Host는 도메인 정보 naver.com 서버 도메인 정보에 해당
http 프로토콜을 사용하는 경우 80포트를 사용한다.
API 서버에서 특정 port를 사용하는 경우에 해당 port를 입력하면된다.
path는 컨트롤러의 RequstMapping 애너테이션의 URL 정보에 해당한다.

buil()와 path() 사이에 queryParam을 추가해서 사용할수 있다.

        UriComponentsBuilder.newInstance()
                .scheme("http")
                .host("test.book.com")
                .port(80)
                .path("/book/{bookId}")
                .build().expand(bookId)
                .encode()
                .toUri();

PathVariable이 포함된 URL로 만들수 있다.
uri로 {}로 감싼 뒤에 expand 메서드를 추가로 변수값을 전달한다.

HTTP 메서드별 RestTemplate 메서드 명세

메서드명 HTTP 호출방식 반환타입
getForObject GET 객체
getForEntity GET HttpResonseEntity
postForObject POST 객체
postForEntity POST HttpResonseEntity
delete DELETE 없음
put PUT 없음
exchange 사용자 지정 사용자 지정

4) User 클래스 추가

package ee.swan.model;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@ApiModel
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private int userNo;
    private String userId;
    private String email;
    private String uname;
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate regDate;


}

5) UserController

package ee.swan.web;

import ee.swan.model.User;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    private static List<User> userList = new ArrayList<>();
    {
        userList.add(new User(1, "test01", "test1@naver.com", "test1", LocalDate.of(2020, 8, 1)));
        userList.add(new User(2, "test02", "test2@naver.com", "test2", LocalDate.of(2020, 8, 2)));
        userList.add(new User(3, "test03", "test3@naver.com", "test3", LocalDate.of(2020, 8, 3)));
    }

    @RequestMapping("/user/{userNo}")
    public ResponseEntity<User> getUserInfo(@PathVariable int userNo) {
        User user = userList.get(userNo);
        return new ResponseEntity<>(user, HttpStatus.OK);
    }
}

자바에서 초기화 블록을 제공

  1. 클래스 초기화 블록
  2. 인스턴스 초기화 블록

중괄호만 선언하면 인스턴스 초기화 블록
static {}과 같이 선언하면 클래스 초기화 블록

인스턴스 초기화 블록안에 있는 내용은 클래스의 생성자 호출 이전에 실행된다.
GetForObject 메서드를 테스트하기 위해 단건 user 정보를 반환하는 메서드를 추가했다.

http://localhost:8080/user/2
{
userNo: 3,
userId: "test03",
email: "test3@naver.com",
uname: "test3",
regDate: "2020-08-03",
}

6) 테스트 코드

package ee.swan.web;

import ee.swan.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.web.client.RestTemplate;

@SpringBootTest
class UserControllerTest {

    @Autowired
    private RestTemplate restTemplate;

    @Test
    public void testGetUserById() {
        String url = "http://localhost:8080/user/1";
        User user = restTemplate.getForObject(url, User.class);
        System.out.println("등록일:" + user.getRegDate() + ", " + user.getUname() + ", " + user.getUserId());
    }

}

결과

등록일:2020-08-02, test2, test02

리스트 응답 파싱

  • 데이터가 응답으로 리스트로 여러개가 온다면?
  • exchange 메서드를 이용해 Map이나 List와 같은 타입을 받으면된다.

7) UserController 메서드 추가 및 테스트

    @RequestMapping("/user")
    public ResponseEntity<List<User>> getUserList() {
        Map<String, Object> resultList = new HashMap<>();
        resultList.put("result", userList);
        return new ResponseEntity(resultList, HttpStatus.OK);
    }

    @Test
    public void testGetUsers() {
        String url = "http://localhost:8080/user";
        ResponseEntity<Map<String, List<User>>> result =
                restTemplate.exchange(url, HttpMethod.GET, null, 
                      new ParameterizedTypeReference<Map<String, List<User>>>() {});

        Map<String, List<User>> tempMap = result.getBody();
        ArrayList<User> resultArr = (ArrayList<User>) tempMap.get("result");
        for (User user : resultArr) {
            System.out.println(user.getUname());
        }
    }
  • ParameterizedTypeReference 는 스프링 3.2부터 사용 가능하고 타입을 제네릭으로 정의하는것을 도와준다.
  • Exchange 메서드를 선언을 보면, responseType을 지정할때 ParameterizedTypeReference 타입으로 선언하거나 Map.class와 같이 클래스를 선언하도록 되어 있다.
  • 그런데 Map.class와 같이 클래스로 파라미터를 입력하면 map으 받을수 있지만 'cannot select parameterize type'이라는 오류와 함께 제네릭으로 파라미터를 선언할수 없다.
  • 따라서 데이터를 읽을수 있지만 user 타입으로 저장할수 없다.
  • 그래서 parameterizeType을 이용해서 실제로 전달받을 데이터형을 Map<String, List>로 지정하고 responseEntity로 리턴된 결과를 map을 변환 후에 map에서 result 키를 찾아서 ArrayList에 저장하는 형태다.

출저 - 스프링 부트로 배우는 자바 웹 개발
저자 - 윤석진

반응형

'JAVA > Spring Boot' 카테고리의 다른 글

메이븐 멀티 프로젝트 구성  (0) 2020.09.04
Spring Data JPA 사용 및 설정  (0) 2020.08.22
스프링 부트 기본설정  (0) 2020.08.20
스프링부트 보안 OAuth2  (0) 2018.04.12
스프링 부트 보안 (예제)  (0) 2018.04.07