일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 프로그래밍
- 체크인미팅
- 위키북스
- UNICON
- 티스토리챌린지
- 도커
- 프리티어
- 자바개발자
- 42서울
- 인디게임
- AWS
- 라피신
- 백엔드
- 개발공부
- 배포
- 인프라
- EC2
- 스프링부트
- 게임개발동아리
- 온라인테스트
- 오블완
- 생활코딩
- UNICON2023
- RDS
- Developer
- 전국대학생게임개발동아리연합회
- UNIDEV
- 스프링
- CICD
- 백엔드개발자
- Today
- Total
Hyun's Wonderwall
[GDSC Study] 스프링 기반 REST API 개발 - 3주차 | 섹션 3. HATEOAS와 Self-Describtive Message 적용 본문
[GDSC Study] 스프링 기반 REST API 개발 - 3주차 | 섹션 3. HATEOAS와 Self-Describtive Message 적용
Hyun_! 2023. 11. 20. 06:55GDSC Ewha 5기_ Spring Boot 스터디
- 스터디 커리큘럼: 백기선, "스프링 기반 REST API 개발"
- 3주차 과제 - 섹션 3. HATEOAS와 Self-Describtive Message 적용
3. (1) 스프링 HATEOAS 소개
3. (2) 스프링 HATEOAS 적용
3. (3) 스프링 REST Docs 소개
3. (4) 스프링 REST Docs 적용
3. (5) 스프링 REST Docs 각종 문서 조각 생성하기
3. (6) 스프링 REST Docs 문서 빌드
3. (7) 테스트용 DB와 설정 분리하기
3. (8) API 인덱스 만들기
3. (1) 스프링 HATEOAS 소개
Spring HATEOAS: HATEOAS 원칙을 따르는 RESTful API를 개발할 때 REST representation 생성을 편리하게 하도록도와주는 라이브러리
- HATEOAS(Hypermedia as the Engine of Application State): 클라이언트가 서버로부터 받은 리소스에 대해 어떠한 동작을 수행할 수 있도록 Hypermedia를 사용해 링크를 동적으로 제공하는 것. RESTful API 디자인 원칙 중 하나.
- 링크 만드는 기능
- linkTo()
- methodOn(), slash()로 slash 표현할 수도 있고 등등... - 리소스 만드는 기능 (리소스: 응답 본문 데이터 + 링크 정보)
- 링크 찾아주는 기능
- 링크
- href: uri, url
- rel: 현재 리소스와의 관계를 나타냄 (self, profile, ...)
3. (2) 스프링 HATEOAS 적용
RESTful API로 만들려면 링크 정보를 받을 수 있어야 한다
EventResource 클래스 만듬
getEvent()
EntityModel<T> : 도메인 객체를 감싸고, 그에 링크를 추가하는 객체. (Resource + Links)
- .add()로 링크 추가 (이 섹션에서는 EventResources, ErrorsResources에 쓰임)
- 참고: https://kkambi.tistory.com/126
ResourceSupport is now RepresentationModel
Resource is now EntityModel
Resources is now CollectionModel
PagedResources is now PagedModel
ControllerLinkBuilder is now WebMvcLinkBuilder //self, update 추가 시
https://velog.io/@mmeo0205/REST-API-HATEOAS
https://stackoverflow.com/questions/60762394/where-is-spring-resourcesupport
# EventController
Event event = modelMapper.map(eventDto, Event.class);
event.update();
Event newEvent = this.eventRepository.save(event);
WebMvcLinkBuilder selfLinkBuilder = linkTo(EventController.class).slash(newEvent.getId());
URI createdUri = selfLinkBuilder.toUri();
EventResource eventResource = new EventResource(event);
eventResource.add(linkTo(EventController.class).withRel("query-events"));
eventResource.add(selfLinkBuilder.withSelfRel()); // Self
eventResource.add(selfLinkBuilder.withRel("update-event")); // Update
return ResponseEntity.created(createdUri).body(eventResource);
/*package me.lsh.restapidemo.events;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import org.springframework.hateoas.RepresentationModel;
public class EventResource extends RepresentationModel {
@JsonUnwrapped
private Event event;
public EventResource(Event event) {
this.event = event;
}
public Event getEvent() {
return event;
}
}*/
package me.lsh.restapidemo.events;
import org.springframework.hateoas.EntityModel;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
public class EventResource extends EntityModel<Event> {
public EventResource(Event event) {
super(event);
add(linkTo(EventController.class).slash(event.getId()).withSelfRel());
}
}
//빈 아니고 매번 converting해서 써야하는 객체
Matchers.not() 이거 어디서 붙인거지...
.andExpect(jsonPath("free").value(false)) 잘못할뻔
private EventStatus eventStatus = EventStatus.DRAFT; 초기화 세팅
실수로 놓칠 뻔 한 것: enum과 문자열을 비교해 버림 https://www.inflearn.com/questions/167701/%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%8B%9C-enum-%EA%B0%92-%EB%B6%88%EC%9D%BC%EC%B9%98
.andExpect(jsonPath("eventStatus").value(EventStatus.DRAFT.name()))
https://backtony.github.io/spring/2021-04-16-spring-api-1/
3. (3) 스프링 REST Docs 소개
Spring MVC Test 실행시 사용한 요청과 응답, 헤더 같은 정보를 사용해 REST API 문서 조각(스니펫)을 만들어주는 라이브러리.
Ascii Docker라는 툴 사용해 플레인 텍스트 문서 -> AScii-DAQ 문법으로 만든 Snippet들 조합 -> HTML 문서로 만들어줌
REST Docs 자동 설정 - (Spring Boot) 테스트 위에 이 애노테이션 붙이기만 하면 됨
- @AutoConfigureRestDocs
실제 사용할 때는 .andDo(document(...)) <- document() 메소드로 현재 이 테스트를 실행한 결과 생성되는 스니펫을 어떤 디렉토리 아래에, 어떤 이름으로 만들지 지정
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import static org.springframework.restdocs.headers.HeaderDocumentation.*;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class) //Bean 불러오기
public class EventControllerTests {
@Autowired
MockMvc mockMvc; //가짜 요청 만들어서 보내고 응답 확인할 수 있는 Test 만들수있음.
// Slicing Test 웹과 관련된 빈들만 테스트. dispatch sublet이란거 만들어야 함, 웹서버 띄우지x
@Autowired
ObjectMapper objectMapper;
@Test
@TestDescription("정상적으로 이벤트를 생성하는 테스트")//JUnit5는 이런 기능 Annotation 지원
public void createEvent() throws Exception {
EventDto event = EventDto.builder()
.name("Spring")
.description("REST API Devlopment with Spring")
.beginEnrollmentDateTime(LocalDateTime.of(2023, 11, 14, 8, 0))
.closeEnrollmentDateTime(LocalDateTime.of(2023, 11, 14, 22, 0))
.beginEventDateTime(LocalDateTime.of(2023, 11, 15, 19, 0))
.endEventDateTime(LocalDateTime.of(2023, 11, 15, 21, 0))
.basePrice(100)
.maxPrice(200)
.limitOfEnrollment(100)
.location("이대역")
.build();
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaTypes.HAL_JSON) //확장자 처럼 만들음??
.content(objectMapper.writeValueAsString(event))) //안에다 주는게 요청, 하고나면 응답 나옴
.andDo(print()) //어떤 요청, 어떤 응답 콘솔에서 볼수있
.andExpect(status().isCreated())
.andExpect(jsonPath("id").exists())
.andExpect(header().exists(HttpHeaders.LOCATION))
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE))
.andExpect(jsonPath("free").value(false))
.andExpect(jsonPath("offline").value(true)) //기본: false
.andExpect(jsonPath("eventStatus").value(EventStatus.DRAFT.name()))
.andExpect(jsonPath("_links.self").exists())
.andExpect(jsonPath("_links.query-events").exists())
.andExpect(jsonPath("_links.update-event").exists())
.andDo(document("create-event",
links(
linkWithRel("self").description("link to self"),
linkWithRel("query-events").description("link to query events"),
linkWithRel("update-event").description("link to update an existing event")
),
requestHeaders(
headerWithName(HttpHeaders.ACCEPT).description("accept header"),
headerWithName(HttpHeaders.CONTENT_TYPE).description("content type header")
),
requestFields(
fieldWithPath("name").description("name of new event"),
fieldWithPath("description").description("description of new event"),
fieldWithPath("beginEnrollmentDateTime").description("date time of begin of enrollment"),
fieldWithPath("closeEnrollmentDateTime").description("date time of close of enrollment"),
fieldWithPath("beginEventDateTime").description("date time of begin of new event"),
fieldWithPath("endEventDateTime").description("date time of end of new event"),
fieldWithPath("location").description("location of new event"),
fieldWithPath("basePrice").description("base price of new event"),
fieldWithPath("maxPrice").description("max price of new event"),
fieldWithPath("limitOfEnrollment").description("limit of enrollment")
),
responseHeaders(
headerWithName(HttpHeaders.LOCATION).description("Location header"),
headerWithName(HttpHeaders.CONTENT_TYPE).description("Content type")
),
responseFields( //relaxedResponseFields 사용시 문서 일부분만 확인
fieldWithPath("id").description("identifier of new event"),
fieldWithPath("name").description("name of new event"),
fieldWithPath("description").description("description of new event"),
fieldWithPath("beginEnrollmentDateTime").description("date time of begin of enrollment"),
fieldWithPath("closeEnrollmentDateTime").description("date time of close of enrollment"),
fieldWithPath("beginEventDateTime").description("date time of begin of new event"),
fieldWithPath("endEventDateTime").description("date time of end of new event"),
fieldWithPath("location").description("location of new event"),
fieldWithPath("basePrice").description("base price of new event"),
fieldWithPath("maxPrice").description("max price of new event"),
fieldWithPath("limitOfEnrollment").description("limit of enrollment"),
fieldWithPath("free").description("it tells if this event is free or not"),
fieldWithPath("offline").description("it tells if this event is offline event or not"),
fieldWithPath("eventStatus").description("eventStatus"),
//오류 해결 위해 재검사
fieldWithPath("_links.self.href").description("link to self"),
fieldWithPath("_links.query-events.href").description("link to query event lists"),
fieldWithPath("_links.update-event.href").description("link to update an existing event")
)
))
;
}
}
3. (4) 스프링 REST Docs 적용
3. (6) 스프링 REST Docs 문서 빌드
new Link()는 더이상 지원되지 않는다고 해서 Link.of() 사용해 해결했다
# EventController 클래스의 createEvent 메서드 중 일부
eventResource.add(Link.of("/docs/index.html#resources-events-create").withRel("profile"));
# 한편 EventResource Link... links 오류나서 이렇게 사용중
public EventResource(Event event) {
super(event);
add(linkTo(EventController.class).slash(event.getId()).withSelfRel());
}
EventResource 부분
https://velog.io/@shdbtjd8/Spring-put-%EC%9A%94%EC%B2%AD-%EA%B5%AC%ED%98%84
전체적으로 읽어보기
https://backtony.github.io/spring/2021-04-16-spring-api-1/
3.7 DB 관련
https://hongddo.tistory.com/244
H2 데이터베이스 연동관련 (지금보다 H2 쓰는 S-Day스터디에서 읽어봐야할듯)
https://hoestory.tistory.com/3
https://unhosted.tistory.com/83
3. (7) 테스트용 DB와 설정 분리하기
애플리케이션에서 사용하는 DB와 테스트에서 사용하는 DB를 구분하는 방법, 각각 다른 DB를 사용하는 방법을 살펴보자.
설정 파일의 중복을 피하는 방법도 알아볼 것
Docker 사용할 것.
Docker for Windows 설치함
docker run --name ndb -p 5432:5432 -e POSTGRES_PASSWORD=pass -d postgres
ndb라는 이름으로 컨테이너 하나 뜸 (영상에서는 rest인데 문서에는 ndb로 되어 있어서 ndb로 함..)
3. (8) API 인덱스 만들기
IndexContorller
package me.lsh.restapidemo.index;
import me.lsh.restapidemo.events.EventController;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
@RestController
public class IndexController {
@GetMapping("/api")
public RepresentationModel index() {
var index = new RepresentationModel<>();
index.add(linkTo(EventController.class).withRel("events"));
return index; // '\api'로 요청했을 때 이 핸들러가 적절하게 201 응답으로 이 링크 정보를 보냄
}
}
IndexControllerTests
package me.lsh.restapidemo.index;
import me.lsh.restapidemo.common.RestDocsConfiguration;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class) //Bean 불러오기
public class IndexControllerTest {
@Autowired
MockMvc mOckMvc;
@Test
public void index() throws Exception {
this.mOckMvc.perform(get("/api"))
.andExpect(status().isOk())
.andExpect(jsonPath("_links.events").exists())
;
}
}
ErrorsResource로 이제 new로 넘길 수 있음
package me.lsh.restapidemo.common;
import me.lsh.restapidemo.index.IndexController;
import org.springframework.hateoas.EntityModel;
import org.springframework.validation.Errors;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
public class ErrorsResource extends EntityModel<Errors> {
public ErrorsResource(Errors content) {
super(content);
add(linkTo(methodOn(IndexController.class).index()).withRel("index"));
}
}
'Study > Java, Spring' 카테고리의 다른 글
[GDSC Study] 스프링 기반 REST API 개발 - 5주차 | 섹션 5. REST API 보안 적용 (0) | 2023.12.27 |
---|---|
[GDSC Study] 스프링 기반 REST API 개발 - 4주차 | 섹션 4. 이벤트 조회 및 수정 REST API 개발 (0) | 2023.11.28 |
[GDSC Study] 스프링 기반 REST API 개발 - 2주차 | 섹션 2. 이벤트 생성 API 개발 (0) | 2023.11.14 |
[GDSC Study] 스프링 기반 REST API 개발 - 1주차 (섹션 0, 1) (0) | 2023.11.07 |
[백엔드] 스터디 - Spring Boot 어노테이션 몇가지 정리 (0) | 2023.05.15 |