DongDD's IT

[Spring] Spring REST 본문

프로그래밍/Spring

[Spring] Spring REST

DongDD 2019. 4. 14. 01:11

[Spring] Spring REST




REST API



- 클라이언트-서버간 데이터를 주고받기 위한 Architecture

- 제공할 데이터를 resource로 추출하고 resource에 대한 CRUD를 제공하기 위해 REST API 사용

- CRUD : Create, Read, Update, Delete


ROA(Resource Oriented Architecture)


- RESTful Web 구축을 위한 architecture 정의


1. 리소스 제공

- web에서 클라이언트에게 제공할 데이터를 resource로 공개


2. URI를 통한 resource 식별

- resource에 uri를 할당해 resource 접근 허용


3. HTTP 메소드를 통한 CRUD

- GET : Read

- POST : Create

- PUT : Update

- DELETE : Delete


4. 포맷

- Json 포맷이나 xml 포맷 사용


5. 응답 상태 코드

- 1xx : 처리 중임을 알리는 상태 코드

- 2xx : 처리 완료를 알리는 상태 코드

- 3xx : 추가적인 처리가 필요함을 알리는 상태 코드

- 4xx : 클라이언트 측 요청의 문제가 있음을 알리는 상태 코드

- 5xx : 서버 측 처리에 문제가 있음을 알리는 상태 코드


6. Stateless 통신

- 서버가 세션을 생성하지 않고 데이터만으로 resource 조작


7. HATEOAS

- resource에 사용 가능한 uri를 전달하는 방식

- 클라이언트가 미리 uri를 알 필요가 없음 → 클라이언트-서버 결합도 낮춤


Spring Rest Api


1

- View를 사용하지 않고 HttpMessageConverter를 통해 request를 해석하고 response를 생성


1. HttpMessageConverter

- Request body를 Java 객체로 변환하고 Java 객체를 response body로 변환


2. Resource 클래스

- Java 클래스 형태로 생성


1
2
3
4
5
6
public class TestResource {
    private String id;
    private String msg;
}
 
 
cs



Rest Controller


- Spring에서 제공하는 @RestController 사용

- @Controller와 @ResponseBody를 사용할 수도 있음


설정


- pom.xml 추가


1
2
3
4
5
6
7
8
9
10
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.8</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.9.6</version>
</dependency>
cs

- json<->객체로 변환하기 위해 추가(databind)

- LocalDate를 json으로 사용하기 위해 추가(jsr310)

- json 데이터에 대해 405 error가 뜨는 경우.

1
2
3
4
5
<properties>
        <java-version>11</java-version>
        <maven.compiler.target>11</maven.compiler.target>
        <maven.compiler.source>11</maven.compiler.source>
</properties>
cs

- properties에 java version, source, target을 맞춰주면 해결


기능


1. 선언형

- 요청 매핑

- 요청 데이터 취득

- 입력 값 검증


2. 프로그래밍형

- 비즈니스 로직 호출

- Response 반환


Controller와 차이점


- 요청, 응답 데이터는 HttpMessageConverter를 통해 획득, 반환

- 입력 값 검증 예외는 예외 핸들러에서 공통으로 수행

→ Handler인자로 BindingResult를 받으면, 해당 핸들러안으로 들어와서 로직 수행


RestController CRUD Example


1. Model(TestModel)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 
public class TestModel {
    private Long id;
    private String name;
    @JsonFormat(pattern = "yyyyMMdd")
    private LocalDate localDate;
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public LocalDate getLocalDate() {
        return localDate;
    }
 
    public void setLocalDate(LocalDate localDate) {
        this.localDate = localDate;
    }
}
 
cs

- Long형 id와 String형 name을 갖는 class 생성

- 날짜를 기록할 수 있는 LocalDate 변수 생성

- @JsonFormat을 사용하여 "yyyyMMdd"형식으로 된 값만 받음


2. Create(POST)

1
2
3
4
5
6
7
8
9
10
11
12
private Map<Long, TestModel> infoList = new HashMap<>();
 
@RequestMapping(value = "/", method = RequestMethod.POST)
public void createInfo(@Validated @RequestBody TestModel testModel) {
    if(testModel.getId() <= 0) {
        throw new TestException(HttpStatus.BAD_REQUEST, "잘못된 ID 입니다.");
    }
    if(infoList.get(testModel.getId()) != null) {
        throw new TestException(HttpStatus.CONFLICT, "이미 존재하는 ID 입니다.");
    }
    infoList.put(testModel.getId(),testModel);
}
cs

- 해당 URI로 POST 요청 시, Create 수행

- 값을 저장할 Map을 생성

- POST 요청이 올 시, Map에 값을 저장

- 추가하려는 id가 존재할 경우, exception을 throw

- 잘못된 id가 들어올 경우, exception을 throw

- 결과(성공)

2


- 결과(실패)

3


3. Read(GET)

1) 전체 읽어오기

1
2
3
4
@RequestMapping(value = "/", method = RequestMethod.GET)
public List<TestModel> getAllInfo() {
    return new ArrayList<>(infoList.values());
}
cs

- 해당 URI로 GET요청을 보낼 시, Map을 List로 바꿔 전달(Read)

- 결과

4

2) 특정 id 정보 읽어오기

1
2
3
4
5
6
7
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public TestModel getInfo(@PathVariable Long id) {
    if(infoList.get(id) == null) {
        throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다.");
    }
    return infoList.get(id);
}
cs

- 해당 URI로 GET 요청 시, Read 수행

- @PathVariable을 사용하여 id 값을 받음

- map에 해당 id가 존재하지 않을 경우 exception을 throw

- 있다면, 해당 id를 가진 객체 정보를 return

- 결과(성공)

5


- 결과(실패)

6


4. Update(PUT)

1
2
3
4
5
6
7
@RequestMapping(value = "/", method = RequestMethod.PUT)
public void updateInfo(@Validated @RequestBody TestModel testModel) {
    if(infoList.get(testModel.getId()) == null) {
        throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다.");
    }
    infoList.put(testModel.getId(),testModel);
}
cs

- 해당 URI로 PUT 요청 시, Update 수행

- map에 해당 id가 존재하지 않을 경우, exception을 throw

- 존재한다면 해당 id의 정보를 변경

- 결과(성공)

7

8

- 결과(실패)

9


5. Delete(DELETE)


1
2
3
4
5
6
7
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
public void deleteInfo(@PathVariable Long id) {
    if(infoList.get(id) == null){
        throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다.");
    }
    infoList.remove(id);
}
cs

- 해당 URI로 DELETE 요청 시, delete 수행

- @PathVariable을 사용하여 id 값을 받음

- map에 해당 id가 존재하지 않을 경우, exception을 throw

- 존재한다면 해당 id의 정보를 삭제

- 결과(성공)

10

11


- 결과(실패)

12


6. Exception 처리

1) TestException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TestException extends RuntimeException {
    private HttpStatus httpStatus;
    private String msg;
    public HttpStatus getHttpStatus() {
        return httpStatus;
    }
 
    public void setHttpStatus(HttpStatus httpStatus) {
        this.httpStatus = httpStatus;
    }
 
    public String getMsg() {
        return msg;
    }
 
    public void setMsg(String msg) {
        this.msg = msg;
    }
 
    public TestException(HttpStatus httpStatus, String msg) {
        this.msg = msg;
        this.httpStatus = httpStatus;
    }
}
cs

- 예외 처리를 위해 RuntimeException을 상속받아 생성

- HttpStatus와 msg를 가진 클래스 생성


2) TestExceptionHandler

1
2
3
4
5
6
7
8
@RestControllerAdvice
public class TestExceptionHandler {
 
    @ExceptionHandler(value = TestException.class)
    public ResponseEntity handleException(TestException e) {
        return ResponseEntity.status(e.getHttpStatus())
.contentType(MediaType.APPLICATION_JSON_UTF8).body(e.getMsg());
    }
}
cs

- @RestControllerAdivce를 사용하여 전역에서 처리할 Exception Handler 지정

- @ExceptionHandler를 통해 TestException이 발생하는 경우, 해당 handler에서 모두 잡음

- @ExceptionHandler의 value에 해당 핸들러에서 처리하고 싶은 Exception class를 지정

- 전달된 Exception의 Httpstatus와 msg를 전달

- 일반 String으로 body가 전달되기 때문에, 한글이 깨져 content-type에 "application/json;charset=UTF-8" 추가


Controller Code


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@RestController
@RequestMapping(value = "/test")
public class TestController {
 
    private Map<Long, TestModel> infoList = new HashMap<>();
 
    @RequestMapping(value = "/", method = RequestMethod.POST)
    public void createInfo(@Validated @RequestBody TestModel testModel) {
        if(testModel.getId() <= 0) {
            throw new TestException(HttpStatus.BAD_REQUEST, "잘못된 ID 입니다.");
        }
        if(infoList.get(testModel.getId()) != null) {
            throw new TestException(HttpStatus.CONFLICT, "이미 존재하는 ID 입니다.");
        }
        infoList.put(testModel.getId(),testModel);
    }
 
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public List<TestModel> getAllInfo() {
        return new ArrayList<>(infoList.values());
    }
 
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public TestModel getInfo(@PathVariable Long id) {
        if(infoList.get(id) == null) {
            throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다.");
        }
        return infoList.get(id);
    }
 
    @RequestMapping(value = "/", method = RequestMethod.PUT)
    public void updateInfo(@Validated @RequestBody TestModel testModel) {
        if(infoList.get(testModel.getId()) == null) {
            throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다.");
        }
        infoList.put(testModel.getId(),testModel);
    }
 
    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public void deleteInfo(@PathVariable Long id) {
        if(infoList.get(id) == null){
            throw new TestException(HttpStatus.NOT_FOUND, "없는 ID 입니다.");
        }
        infoList.remove(id);
    }
}
cs

- @RestController를 사용하여 REST로 사용할 Controller 지정

- @RequestMapping을 사용하여 기본 URI를 "/test"로 설정


CORS(Cross Origin Resource Sharing)


- 웹 페이지에서 요청 시, 해당 페이지의 요청을 받은 서버가 아닌 다른 도메인 서버 resource에 접근하기 위한 메커니즘


1. Annotation

- Controller, handler 단위

- Controller, handler에 @CrossOrigin 사용


1) 종류

13

- exposedHeaders : Browser에서 Client가 받을 수 있는 response header 지정

- maxAge : preflight 요청에 대한 응답을 캐시할 시간 지정(default = 1800초)

- allowCredentials : 인증 정보를 취급할지에 대한 여부(default = true)

- allowedHeaders : 실제 요청에서 사용될 수 있는 header 지정(preflight 요청 시, 해당 값으로 검증)

- methods : 지원 가능한 request method 지정(default = requestmapping에 지정된 method들)

- origins(value) : 요청을 허용할 주소:포트 지정


2. Config

- Application 단위

- 설정 파일에 추가

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebMvc
@ComponentScan("com")
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("path");
    }
}
cs


URIBuilder


- Spring에서 제공하는 URI 생성을 위한 컴포넌트


1. UriConentsBuilder

- URI를 생성하기 위한 Component

1
2
3
4
5
6
@RequestMapping(value = "/test", method = RequestMethod.GET)
public URI test() {
    return UriComponentsBuilder.newInstance().path("/aaaa/bb/{c}")
            .buildAndExpand(10)
            .toUri();
}
cs

- path로 URI 템플릿 생성

- buildAndExpand로 path variable에 들어갈 값 지정

- 결과

14

2. MvcUriComponentsBuilder

- URI를 생성하기위한 Component

1
2
3
4
5
@RequestMapping(value = "/test", method = RequestMethod.GET)
public URI test() {
    return MvcUriComponentsBuilder.fromController(TestController.class)
                   .path("/test/{a}").buildAndExpand(123).toUri();
}
cs

- fromController로 Controller에 지정된 기본 URL 가져옴

- path로 URI 생성

- 결과

15


Jackson


- JSON 포맷을 제어하기 위한 라이브러리

- 포맷 제어를 위한 annotation 제공


ObjectMapper


- Jackson이 제공하는 클래스로 JSON과 자바 객체를 서로 변환

- 기본적으로 지원되지만 커스텀하여 사용할 수 있음

- dateformat 등 JSON을 다루기 위해 필요한 것들을 제공


ex)

1
2
3
4
5
6
7
8
9
10
11
@Bean
@Primary
public ObjectMapper objectMapper() {
    SimpleModule simpleModule = new SimpleModule();
    simpleModule.addSerializer(LocalDate.class
               new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
    simpleModule.addDeserializer(LocalDate.class
               new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyyMMdd")));
 
    return Jackson2ObjectMapperBuilder.json().modules(simpleModule).build();
}
cs

- Deserializer를 "yyyyMMdd"형식으로 등록(Json → Object)

- Serializer를 "yyyy-MM-dd"형식으로 등록(Object → Json)

- WebMvcConfig 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableWebMvc
@ComponentScan("com")
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private ObjectMapper objectMapper;
 
    @Bean
    public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setObjectMapper(objectMapper);
        return converter;
    }
 
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(jackson2HttpMessageConverter());
    }
}
cs

- ObjectMapper를 converter로 등록

- 결과

16


REST Client


- Spring Application 내에서 REST Api를 호출하기 위한 방법

- Spring 3.0 이후에 제공되는 RestTemplate 이용


RestTemplate


- REST API를 호출하기 위한 클래스

- HTTP Client의 역할도 함


1. 사용법

1
2
3
4
5
@RequestMapping(value="/template", method = RequestMethod.GET)
public TestModel templateGet() {
    RestTemplate restTemplate = new RestTemplate();
    return restTemplate.getForObject("http://localhost:8080/test/{id}",
TestModel.class1);
}
cs

- RestTemplate 클래스 이용

- getForObject로 해당 URL에서 객체를 얻어올 수 있음

- post, get, delete, put 등 다양한 http method를 method로 제공

- 결과

17


2. RestTemplate Component

1) HttpMessageConverter

- Java 객체 <-> Json 간 서로 변환해주는 역할

- RestTemplate method 호출 시, 가장 먼저 거침


2) ClientHttpRequestFactory

- 요청을 전송할 객체를 만들기 위한 인터페이스

- ClientHttpRequest가 메시지 전송 역할 담당


3) ClientHttpRequestInterceptor

- HTTP 통신 전후에 공통 처리를 하기 위한 인터페이스


4) ResponseErrorHandler

- 에러 처리를 위한 인터페이스


3. RequestEntity/ResponseEntity

1) RequestEntity

- Request Header 설정이나 body 설정 등 통신에서 필요한 것을 제공

ex)

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping(value="/template", method = RequestMethod.POST)
public void templatePostTest() {
    TestModel testModel = new TestModel();
    testModel.setId(Long.valueOf(123));
    testModel.setName("test");
    testModel.setLocalDate(LocalDate.now());
 
    RequestEntity<TestModel> requestEntity = RequestEntity
            .post(URI.create("http://localhost:8080/test/"))
            .contentType(MediaType.APPLICATION_JSON)
            .body(testModel);
}
cs

- RequestEntity를 생성

- POST 요청 Method를 사용하게 함

- Content-Type을 "application/json"으로 지정

- body에 TestModel 객체를 삽입


2) ResponseEntity

- Response header나 http 통신 정보를 얻기 위해 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping(value="/template", method = RequestMethod.POST)
public void templatePostTest() {
    TestModel testModel = new TestModel();
    testModel.setId(Long.valueOf(123));
    testModel.setName("test");
    testModel.setLocalDate(LocalDate.now());
 
    RequestEntity<TestModel> requestEntity = RequestEntity
            .post(URI.create("http://localhost:8080/test/"))
            .contentType(MediaType.APPLICATION_JSON)
            .body(testModel);
 
    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<Void> responseEntity = restTemplate
            .exchange(requestEntity, Void.class);
}
cs

- 생성했던 RequestEntity를 RestTemplate를 이용해 실행

- 응답으로 ResponseEntity를 받음

- ResponseEntity로 HttpStatus, Header 등을 조회


Timeout


- RestTemplate 사용 시, 타임아웃을 지정할 수 있음

1
2
3
4
5
6
7
@RequestMapping(value="/template", method = RequestMethod.POST)
public void templatePostTest() {
    SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();
    simpleClientHttpRequestFactory.setConnectTimeout(1000);
    simpleClientHttpRequestFactory.setReadTimeout(1000);
    RestTemplate restTemplate = new RestTemplate(simpleClientHttpRequestFactory);
}
cs

- SimpleClientHttpRequestFactory 클래스를 이용하여 Connection Timeout과 Read Timeout 설정

- RestTemplate의 생성자 인자로 넘겨줌

Comments