REST API
REST API를 공부하면서 이응준님의 "그런 REST API로 괜찮은가"를 참조했다.
https://tv.naver.com/v/2292653
REST API란 지금까지 client가 api요청을 보내면 그 값을 JSON, XML로 반환해주는 api정도?? 로만 인식하고 있었다.
영상을 들어보니 REST API라는 말이 맞을 수도 있고 틀릴 수도 있다는데 REST API를 만든 로이필딩씨께서는 self-descriptive messages, HATEOAS를 충족하지 못하면 REST API가 아니라고 한다..?
REST API이기 위한 몇가지 조건이 있다.
Uniform Interface의 제약조건
identification of resources
resource가 uri로 식별되면 된다. (ex : api/users/name 이런식으로 uri상에서 정보가 나타야한다는것 같다.)
manipulation of resources through representations
representation전송을 통해서 resource를 조작해야 한다. (GET, POST, UPDATE ...)
ex) POST 요청인데 데이터를 삭제하는 요청은 없어야 한다.
self-descriptive messages
메시지는 스스로를 설명해야한다.
GET / HTTP/1.1
- 이 HTTP 요청 메시지는 뭔가 빠져있어서 self-descriptive하지 못하다.
GET / HTTP/1.1
Host : www.example.org
- 목적지를 추가하면 이제 self-descriptive 하다라고 말할 수 있다.
HTTP/1.1 200 OK
[ {"op" : "remove", "path" : "a/b/c/"} ]
- self-descriptive하지 못하다. 어떻게 해석해야할지 모르기 때문이다.
HTTP/1.1 200 OK
Content-Type : application/json
[ {"op" : "remove", "path" : "a/b/c/"} ]
- 대괄호, 중괄호의 의미가 뭐인지 이해할 수 있기 때문에 파싱이가능해지고 문법을 해석할 수 있게 된다. 하지만 이것만으로는 부족한데 여기서 의미하는 "op"와 "path"의 의미를 모르기 때문이다.
HTTP/1.1 200 OK
Content-Type : application/json-patch+json
[ {"op" : "remove", "path" : "a/b/c/"} ]
- json patch + json이라는 미디어 타입으로 정의되어 있는 메시지 이기 때문에 json patch라는 명세를 찾아가서 이것을 이해한다음에 메시지를 해석하면 올바르게 이 메시지의 의미를 해석할 수 있음
- 메시지 내용으로 온전히 해석이 가능해야한다.
HATEOAS(hypermedia as the engine of applicatoin state)
- HATEOAS : 애플리케이션의 상태는 Hyperlink를 이용해 전이되어야 한다.
HTTP/1.1 200 OK
Content-Type : text/html
<html>
<head></head>
<body><a href="/test">test</a></body>
</html>
- html 같은 경우는 a link 로 연결 되어 있기 때문에 HATEOAS를 만족한다.
HTTP/1.1 200 OK
Content-Type : application.json
Link : </articles/1>; rel="previous",
</articles/3>; rel="next";
{
"title" : "The second article",
"contents" : "blah blah ..."
}
- json 같은 경우에도 LINK를 이용해서 URI 정보를 인지할 수 있다.
Spring HATEOAS란?
- Hypermedia As The Engine Of Application State를 구현하기 위해 편리한 기능들을 제공해주는 tool(라이브러리)이다.
- Hypermedia As The Engine Of Application State는 Rest API를 만들 때, 서버가 리소스에 대한 정보를 제공할 때,
- 그 리소스와 연관이 되어있는 링크 정보들까지 같이 제공하고,
- 클라이언트는 제공이 된 연관된 링크 정보를 바탕으로 리소스에 접근한다.
대표적인 기능
- 링크 만드는 기능
- 리소스 만드는 기능
- 링크 찾아주는 기능
의존성
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
<version>0.25.1.RELEASE</version>
</dependency>
링크
{
"content":"Hello, World!",
"_links":{
"self":{
"href":"http://localhost:8080/greeting?name=World"
}
}
}
다음과 같이 어떤 특정 콘텐츠안에 _links라는 항목안에 여러 링크들을 자동적으로 추가해준다.
Controller
@GetMapping
public ResponseEntity searchUser(){
URI createUri = linkTo(methodOn(UserController.class).searchUser()).toUri();
List<User> users = userService.selectAll();
UserResource userResource = new UserResource(users);
userResource.add(linkTo(UserController.class).withSelfRel());
userResource.add(linkTo(UserController.class).withRel("user-update"));
return ResponseEntity.created(createUri).body(userResource);
}
TestController
@Test
@TestDescription("전체 유저를 조회하는 테스트")
public void searchUsers() throws Exception {
MockMvc.perform(get("/api/users/")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaTypes.HAL_JSON))
.andDo(print())
.andExpect(status().isCreated());
}
실행결과
이런식으로 REST API를 나타낼수 있다.
Errors -> Json 변환 안되는 이슈
잘못된 입력값에서 대해서 Errors를 처리할 수 있다. 하지만 body안에 아무처리 없이 넣어주면 errors는 json으로 반환되지 않는다.
@PostMapping
public ResponseEntity createUser(@RequestBody @Valid UserDto userDto,Errors errors){
if(errors.hasErrors()) {
return ResponseEntity.badRequest().body(errors);
}
User user = modelMapper.map(userDto, User.class);
URI createUri = linkTo(UserController.class).toUri();
return ResponseEntity.created(createUri).body(user);
}
Spring 에서는 JsonCompenent를 제공해준다. 다음과 같이 JsonSerializer를 직접 상속받아서 반환할 json을 하나식 처리해주면 된다.
@JsonComponent
public class ErrorsSerializer extends JsonSerializer<Errors> {
@Override
public void serialize(Errors errors, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartArray();
errors.getFieldErrors().stream().forEach(e -> {
try {
gen.writeStartObject();
gen.writeStringField("field", e.getField());
gen.writeStringField("objectName", e.getObjectName());
gen.writeStringField("code", e.getCode());
gen.writeStringField("defaultMessage", e.getDefaultMessage());
Object rejectedValue = e.getRejectedValue();
if (rejectedValue != null) {
gen.writeStringField("rejectedValue", rejectedValue.toString());
}
gen.writeEndObject();
} catch (IOException e1) {
e1.printStackTrace();
}
});
errors.getGlobalErrors().stream().forEach(e -> {
try {
gen.writeStartObject();
gen.writeStringField("objectName", e.getObjectName());
gen.writeStringField("code", e.getCode());
gen.writeStringField("defaultMessage", e.getDefaultMessage());
gen.writeEndObject();
} catch (IOException e1) {
e1.printStackTrace();
}
});
gen.writeEndArray();
}
}
'개인공부' 카테고리의 다른 글
Socket 정리 (0) | 2020.10.04 |
---|---|
클라이언트 token 보안 전략 (0) | 2020.10.04 |
CSRF, CORS 개념 (0) | 2020.10.04 |
SpringSecurity 정리 및 샘플코드 (0) | 2020.10.04 |
SpringSecurity 패스워드 암호화(bcrypt) (0) | 2020.10.04 |