Spring HATEOAS
레퍼런스 : https://docs.spring.io/spring-hateoas/docs/current/reference/html/
- HATEOAS 원칙을 따르는 Rest 표현을 쉽게 작성할 수 있는 API를 제공하는 라이브러리
- 크게 링크를 만드는 기능과 리소스를 만드는 기능으로 나눌 수 있음
HATEOAS(in wikipedia)
REST 어플리케이션 아키텍처의 콤포넌트
Hypermedia As The Engine Of Application State
HATEOAS를 사용하면 클라이언트는
(하이퍼 미디어를 통해 정보를 동적으로 제공하는)
응용프로그램 서버와 interact(상호작용함)클라이언트는 하이버미디어의 일반적인 이해를 넘어서는 지식이 거의 필요치 않음
CORBA의 클라이언트 서버처럼 인터페이스 설명 언어(IDL)를 이용한 고정 인터페이스를
사용해서 상호작용하는 것과 반대라고 볼 수 있음HATEOAS가 서버 클라언트를 나누는 방식은 서버기능을 독립적으로 발전 가능하게
어플리케이션의 상태의 변화에 따라 링크 정보가 바뀜
잔액 조회시 예금,출금, 송금등 의 링크가 따라가지만 예금이 마이너스인 경우
에는 링크가 예금만 생긴다던지설정
- 스프링 부트 미사용시 @EnableHypermediaSupprort등의 애노테이션 등을 사용해야함
(HATOAS 레퍼런스 하단 참조) - 스프링 부트 사용시 자동 설정됨
- 스프링 부트 미사용시 @EnableHypermediaSupprort등의 애노테이션 등을 사용해야함
리소스 : 응답본문 + 링크, 응답본문이 다시 리소스로 중첩도 가능
1 | Link link = new Link("http://localhost:8080/something"); |
- link 설명
- new로 생성 가능
- href, rel 재설정 가능
- 기본 rel은 self
- linkTo()를 이용해서 컨트롤러와 메소드에 매핑된 url을 읽어올 수 있음
- 구조
- HREF : URI 설정
- rel :
- self(default)-자기 자신
- profile - 응답 몬문에 대한 문서로 링크
- 이후 현재 API 상태이전을 할수 있는 어플리케이션 링크(예제에서는 2개)
- events
- update
1. TDD 시작
1 | //응답에 각각에 해당하는 링크 요소가 존재하는지 |
- ResourceSupport를 상속하면 링크 추가가 매우 쉬워진다
1 | //원래 컨트롤러 부분 |
- 설명
- resultEvent를 베이스로 만든 eventResource는 이제 링크 추가가 가능
- 컨트롤러 클래스의 매핑 URI를 읽어와서 query-events 이름으로 rel 생성
- Self 링크 주소는 어디에?
- URI를 만드는 linkTo(EventController.class).slash(resultEvent.getId())
가 정확한 링크임을 이용 - variable로 추출해서 재사용해보자
- URI를 만드는 linkTo(EventController.class).slash(resultEvent.getId())
1 | ControllerLinkBuilder selfLinkBuilder = |
- 설명
- 재사용을 위해서 selfLinkBuilder로 variable로 추출
- 추출한 selfLinkBuilder를 self링크와 ‘update-event’ 릴레이션으로 add
- 같은 url?
- 무방함
- update는 PUT 메소드이기 때문에 같은 URI여도 무방하다
- 링크 자체에 어떤 메소드인지 constraint 할 순 없음
테스트하면 깨진다
받으려고하던 필드가 Event로 wrapping되서 나오기 떄문
응답 본문에 event로 감싸져서 나오는 이유
- ResponseEntity에서 리턴한 개게는 EventResource객체
- 이 객체를 Json으로 컨버팅 하는 것은 objectMapper가 해주는 일
- objectMapper가 사용하는 Serializer? BeanSerializer
- BeanSerializer는 기본적으로 객체의 필드 이름을 사용한다
- 그래서 event를 사용하게 된것
- 그리고 해당 Event가 여러 다른 필드로 구성된 Composite field이므로 event안에
구성 필드 값을 다 넣어준 것
만약 event로 감싸고 싶지 않다면? 여러가지 방법이 있을 수 있음
- EventResource 안에 Event타입이 아니라 Event 내부의 composite한 필드를 넣는다
- 이경우 생성자에 일일히 받아와야하나? 번거로워짐
- @JsonUnwrapped
- EventResource안의 Event필드 위에 @JsonUnwrapped 애노테이션
- 해당 필드의 wrapping을 벗겨준다
@JsonUnwrapped 사용해서 래핑을 벗겨준다 1
2
3
4
5
6
7public class EventResource extends ResourceSupport {
private Event event;
public EventResource(Event event) {this.event = event;}
public Event getEvent() {return event;}
} - 이렇게 하면 테스트는 성공
- 코드량이 늘어났음. 다른 쉬운 방법?
- extends Resource
사용 방법
- 레퍼런스에는 존재하지 않는 방법
- 스프링 hateoas에 Resource
라는 ResourceSupport를 상속하는 하위 클래스
가 존재한다Resource 정의 부분 소스 1
2
3
4
5
6
7
8
9
public class Resource<T> extends ResourceSupport {
private final T content;
//{중간 생략}
//Getter
public T getContent() {return content;}
설명
- Resource
는 ResourceSupport를 상속하고 있다 - T를 content로 받아오고 있다
- Getter인 getContent()메소드 위에 이미 @JsonUnwrapped이 붙어 있기 때문에
이를 이용하면 개발자가 굳이 @JsonUnwrapped을 사용할 필요가 없을 듯하다
- Resource
1 | import org.springframework.hateoas.Link; |
- 설명
- 기존 EventResource를 ResourceSupport대신 Resource
를 상속 - 생성자 구현 , 기능 추가?
- 예를들어 현재 self 링크 거는 부분을 이쪽에 옮긴다면?
- 기존 EventResource를 ResourceSupport대신 Resource
1 | public class EventResource extends Resource<Event> { |
1 | { |
Spring REST Docs
- 정확하고 읽기 쉬운 Restful서비스를 위한 문서를 생성할 떄 도와줌
- Spring Mvc Test로 자동생성된 snippets들과 직접 작성한 문서들을 결합해서 문서화
- 스프링만 사용한다면 MockMvc생성 할떄 설정을 추가 해야한다
스프링 사용시 MockMvc생성에 설정부분을 추가한 before 메서드 추가 1
2
3
4
5
6
7
8
9
10
11private MockMvc mockMvc;
private WebApplicationContext context;
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation))
.build();
} - 스프링 부트 사용시?
- @AutoConfigureRestDocs를 테스트위에 설정
- 위의 작업이 알아서 자동으로
1 | mockMvc.perform(post("/api/events") |
설명
- @AutoConfigureRestDocs를 테스트에 설정하였다
- 그러면 이제 MockMvc는 RestDocsMockMvc 가 된다
- 그후 위처럼 하면 target 밑에 create-event에 대한 snippets을 생성한다
- request,response, 그리고 각 body에 대한 adocs를 생성하는 것을 확인 가능하다
RestDocsMockMvc 커스터마이징
- 현재 반환되는 body는 포매팅이 안되어서 JSON 본문을 읽기가 어렵다
- 포매팅을 위해선 기본 제공되는 RestDocsMockMvc의 커스터마이징이 필요
- RestDocsMockMvcConfigurationCustomizer를 이용해서 커스터마이징이 가능하다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RestDocsConfiguration {
public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer(){
return new RestDocsMockMvcConfigurationCustomizer() {
public void customize(MockMvcRestDocumentationConfigurer configurer) {
//configurer만 구현하면 된다
configurer.operationPreprocessors()
.withRequestDefaults(prettyPrint())
.withResponseDefaults(prettyPrint());
}
}
}
}
//인텔리J는 람다로 표현을 바꿔준다
public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() {
return configurer -> configurer.operationPreprocessors()
.withRequestDefaults(prettyPrint())
.withResponseDefaults(prettyPrint());
}
설명
- @TestConfiguration은 Test에서만 쓰는 @Configuration이라고 보면 된다
- RestDocsMockMvcConfigurationCustomizer 빈을 등록할 때 customize만 구현
- RestDocsMockMvcConfigurationCustomizer가 FunctionalInterface이므로
인텔리J는 자동으로 람다를 추천 - configure만 구현 하면 된다
- withRequestDefaults, withResponseDefaults로 요청과 응답에 대한 기본
operationPreprocessors를 설정 할 수 있다 - prettyPrint()는 요청과 응답을 보기 좋게 찍어주는 operationPreprocessors를 리턴
- 실제 이 자바 설정 파일을 적용하기 위해 Test에서 @Import한다
- 해보면 이제 만들어진 요청 본문 문서, 응답 본문 문서 모두 가독성이 좋게 되어있다
Request, REsponse Field와 헤더의 문서화
API 문서 만들기를 하는 도중이다
- 요청 본문 문서화(앞에서 만듬)
- 응답 본문 문서화(앞에서 만듬)
- 링크 문서화
- self
- 상태이전: 예제에서는 query-events- update-event
- profile 링크
- 요청 헤더 문서화
- 요청 필드 문서화
- 응답 헤더 문서화
- 응답 필드 문서화
문서화 방법
링크 문서화: links() + linkWithRel()
요청 헤더 문서화: requestHeaders() + headerWithName()
요청 필드 문서화: requestFields() + fieldWithPath()
응답 헤더 문서화: responseHedaers() + headerWithName()
응답 필드 문서화: responseFields() + fieldWithPath()
링크문서화
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// 링크, 요청헤더, 요청필드, 응답 헤더 문서화
.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 event")
),
requestHeaders(
headerWithName(HttpHeaders.ACCEPT).description("accept header"),
headerWithName(HttpHeaders.CONTENT_TYPE).description("content type")
),
requestFields(
fieldWithPath("name").description("Name of new Event"),
fieldWithPath("description").description("description of new Event"),
fieldWithPath("beginEnrollmentDateTime").description("beginEnrollmentDateTime of new Event"),
fieldWithPath("closeEnrollmentDateTime").description("closeEnrollmentDateTime of new Event"),
fieldWithPath("beginEventDateTime").description("beginEventDateTime of new Event"),
fieldWithPath("endEventDateTime").description("endEventDateTime of new Event"),
fieldWithPath("location").description("location of new Event"),
fieldWithPath("basePrice").description("basePrice of new Event"),
fieldWithPath("maxPrice").description("maxPrice of new Event"),
fieldWithPath("limitOfEnrollment").description("limitOfEnrollment of new Event")
),
responseHeaders(
headerWithName(HttpHeaders.CONTENT_TYPE).description("content type"),
headerWithName(HttpHeaders.LOCATION).description("Location")
),
relaxedResponseFields(
fieldWithPath("id").description("Id of new Event"),
fieldWithPath("name").description("Name of new Event"),
fieldWithPath("description").description("description of new Event"),
fieldWithPath("beginEnrollmentDateTime").description("beginEnrollmentDateTime of new Event"),
fieldWithPath("closeEnrollmentDateTime").description("closeEnrollmentDateTime of new Event"),
fieldWithPath("beginEventDateTime").description("beginEventDateTime of new Event"),
fieldWithPath("endEventDateTime").description("endEventDateTime of new Event"),
fieldWithPath("location").description("location of new Event"),
fieldWithPath("basePrice").description("basePrice of new Event"),
fieldWithPath("maxPrice").description("maxPrice of new Event"),
fieldWithPath("limitOfEnrollment").description("limitOfEnrollment of new Event"),
fieldWithPath("offline").description("offline of new Event"),
fieldWithPath("free").description("free of new Event"),
fieldWithPath("eventStatus").description("eventStatus of new Event")
)
));설명
- 원래 document(“create-event”)만 있던 것 뒤에 links가 더 붙어 있다
- document(String identifier, Snippet… snippets) 구조이기 때문에 계속적으로
스니펫을 추가 가능하다 - links()는 LinkDescriptor들을 인자로 가진다
- 응답에서 추출된 링크들이 이 LinkDescriptor들을사용되서 문서화된다,
- 이렇게 하면 이제 links.adoc가 만들어진다
- adoc파일 안에는 linkWithRel로 정의한 3가지의 정보가 들어있다
- requestHeaders()안에 headerWithName으로 HeaderDescriptor를 인자로 넣었다
- requestField는 기본 snippets의 http-request를 보고 만드는게 편하다
- responseHeaders, responseFields도 비슷한 방법으로 가능하다
- 응답을 responseFields가 아닌 relaxedResponseFields를 쓰는 이유?
- responseFields로 하면 에러 발생
- 응답 안의
_links
에 대한 description을 요구 - 응답은 링크 문서화의 links()로 이미 만들어 주었다
- 그럼에도 응답의 일부라고 생각하고 요구하는 것
- relaxed접두사가 붙으면 해당 필드 전부를 하지 않아도 된다
- relax접두사의 장단점
- 장점: 문서 일부분만 테스트 가능
- 단점: (일부만 기술하므로)정확한 문서를 만들지 못한다
1 | fieldWithPath("_links.self.href").description("Link to self"), |
- 설명
- Maven plugin 3개가 필요
- asciidoctor-maven-plugin
- html을 생성한다
- 내용을 보면 prepare-package 페이즈에 asciidoc을 처리하는 goal을 끼워놓음
- process-asciidoc
- 이 플러그인이 제공하는 Maven goal
- src/main/asciidoc하위 모든 adoc파일을 html로 만든다
- target/generated-docs밑에 생성
- spring-restdocs-asciidoctor
- maven-resources-plugin
- prepare-package 페이즈에 copy-resources 라는 goal을 실행
- 설정한것 처럼 getnerated-docs의 내용을 static/docs로 옮긴다
- 만든 html이 있어야하므로 asciidoctor-maven-plugin이 먼저 실행되어야 한다
- 같은 페이즈이니 pom.xml의 순서에 따라 순서가 달라지니 순서에 유의
- static으로 옮겨졌으니 스프링부트의 정적리소스 지원 기능으로 아무 설정 없이
해당 html을 요청할 수 있다
- asciidoctor-maven-plugin
- 처음에 인텔리J에서 Spring Rest Docs를 의존성을 추가하면
maven-resources-plugin만 추가하면 된다
추가가 된다 - 2.0.3RELEASE가 안된다.. 2.0.2RELEASE로 하면 된다. 왜 안되는건지
- asciidoc 밑에 템플릿용 파일을 끼워넣으면 위에서 설명한 절차에 따른 html이 생성
된다
- Maven plugin 3개가 필요
1 | === 이벤트 생성 |
- 설명
- asscidoc/index.adoc 일부분
- html로 생성할때 operation::create-event에서 generated-snippets에 만든
스니펫을 참조한다
이제 저 html을 profile 링크로 추가하면 된다
1 | public class EventResource extends Resource<Event> { |
DB 분리
test할 때는 H2, 서비스시 PostgresQL
현재 방법
- application.properties를 src/resource, test/resource 2곳에 마련
- 두곳에 각각 다른 DB 설정
- test시 덮어 써버림
- 중복 코드가 너무 많음
해결 방법
- 덮어 쓰지 않게 다른 이름으로 ex)application-test.properties
- @ActiveProfiles(“test”)
- 프로파일 때 배웠던것처럼 applicaion-{프로파일이름}.properties를 적용이됨
- 이제 공통부분을 중복하지 않아도 됨
index
다른 리소스에 대한 링크를 제공한다
1 |
|
1 |
|
- 설명
- TDD 흉내로 테스트먼저 만들고 성공하도록 완성
- 이렇게 하면 해당 /api시 evnets에 대한 링크가 제공된다
에러 리소스
- BadRequest에 대한 테스트를 만들었고 그에 대한 응답과 바디도 만들었지만
이 에러 응답에 첫페이지만 갈 수 있다 - 에러를 받고 다음 상태 전이 할 수 있는 링크가 없음
- 에러가 났을때도 인덱스로 가는 링크가 있다면 좋을 것이다
- BadRequest나 에러에 대한 테스트 조건에 위의 테스트를 넣고
해당 에러 핸들러의 에러 응답 작성시 링크를 추가하도록 하면 될 것이다- 테스트 조건:
.andExpect(jsonPath("_links.index").exists())
- 테스트 조건:
1 | public class ErrorsResource extends Resource<Errors> { |
- 컨트롤러의 BadReqeust던질때 ErrorsResource로 래핑해서 던지면 됨
return ResponseEntity.badRequest().body(new ErrorsResource(errors))
- 테스트 시 에러 발생 :
$[0].xxx
부분- error가 content안에 들어가있는 에러에 Unwrapped가 되지 않아서 발생
- Reource
에는 @JsonUnwrapped가 있어서 자동으로 Unwrapped가 된다 했었다 - @JsonUnwrapped 애노테이션 주석을 읽어보면 JSON Array에는 애노테이션 적용이
안된다고 나와있다 - 굳이하려면 Serializer를 재정의 하면 된다
- 이정도에선 그냥 테스트를 content안을 보게 하는게 나을 듯
$[0].xxx
->content[0].xxx
로 수정
또 다른 에러 발견 및 통과
- 다른 테스트는 다 통과하였는데 createEvent_BadRequest_wrongValue에서만
에러가 발생하였다 - 에러 발생 이유 application.properties에 정의한 objectMapper 커스텀 프로퍼티
spring.jackson.deserialization.fail-on-unknown-properties=true
- 이 프로퍼티를 true한 경우 JsonMappingException를 일으킨다
- 이 에러는 Exception이므로 기존 error에는 처리할 수 없다
- 나 같은 경우에는 이전 Errors를 다루둣이 Exception을 다룰 수 있는
ExceptionResource,와 ExceptionSerializer를 생성하였다 - 그리고 컨트롤러에 에러 핸들러를 추가하면 끝
- 코드로 보면
1 | //컨트롤러에 있는 Exception핸들러 부분 |
이제 이러면 해당 에러에도 index가 링크로 담겨있는 것을 확인할 수 있다
이벤트 목록 조회 API 만들기
페이지, 정렬
- 스프링 Data JPA가 제공하는 Pageable사용
1 | //테스트 코드 |
- 설명
- 파라미터로 page,size, sort를 받는다
- 이를 Pageable안에 매핑되서 사용
- page는 0부터 시작하기 때문에 현재 테스트는 2번째 페이지를 호출한 것이다
- size는 한 페이지의 아이템 길이로 현재 10개로 되어있다
- sort는 name을 내림차순으로 소트하도록 주었다
page 의 리턴의 경우 아래와 같은 형태로 리턴된다 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{
"content": [
{
//10개 Event Array
}
],
"pageable": {
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"offset": 10,
"pageNumber": 1,
"pageSize": 10,
"unpaged": false,
"paged": true
},
"totalElements": 30,
"totalPages": 3,
"last": false,
"size": 10,
"number": 1,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"numberOfElements": 10,
"first": false,
"empty": false
}
- findall()리턴시 Page
가 리턴됨 - sort pageable 정보가 들어 있다
- 파라미터와 매칭 되는 부분 잘 읽어 보기
페이지 링크
- 앞페이지나 뒷페이지에 대한 정보가 없음
- 페이지를 리소스로 바꿔서 링크로 추가가 필요하다
- 간단하게 생각하면 new EventResource(Event event)를 해주면 되는데
현재 테스트 데이터 30개를 전부 이렇게 순회하면서 직접 할 것인가?
더 좋은 방법이 있는데 - 유용한 것이 Spring Data JPA가 제공해주는 PagedResourcesAssembler라는 것이 있음
- 이 리소스어셈블러를 이용하면 page 인스턴스를 간단하게 pagedResource로 바꿀 수
있음PagedResourcesAssembler라는를 적용한 방법 1
2
3
4
5
6
public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler){
Page<Event> page = eventRepository.findAll(pageable);
PagedResources<Resource<Event>> pagedResources = assembler.toResource(page);
return ResponseEntity.ok(pagedResources);
} - 설명
- 이전에 리턴하던 findall()리턴값은 Page
형태였다 - PagedResourcesAssembler
- implements ResourceAssembler<Page
, PagedResources<Resource >> - 상속받고 있는 ResourceAssembler<T, D extends ResourceSupport>은
기본적으로 도메인 T를 ResourceSupport D로 컨버팅 하기 위한 어셈블러 - PagedResourcesAssembler는 정의에 보이다시피 페이지에 쓰일 수 있게 되어있음
- Page
를 받은 후 PagedResources<Resource > 변환됨을 주의
- implements ResourceAssembler<Page
- 이전에 리턴하던 findall()리턴값은 Page
1 | { |
- 설명
_embedded
라는 것안에 List가 들어갔다- 페이지와 링크 정보가 추가가 되어있음을 확인할 수 있다
- HATEOAS ? No, 각 이벤트들의 링크가 없음
HATEOAS 적용을 위한 소스 수정 1
2
3
4
5
6
7
8
public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler){
Page<Event> page = eventRepository.findAll(pageable);
// PagedResources<Resource<Event>> pagedResources = assembler.toResource(page);
PagedResources<EventResource> pagedResources = assembler.toResource(page, e -> new EventResource(e));
pagedResources.add(new Link("/docs/index.html#resources-events-list").withRel("profile"));
return ResponseEntity.ok(pagedResources);
}
- 설명
- 이전에 있던 소스 assembler.toResource(page)의 동작을 잘 생각해보자
PagedResourcesAssembler클래스의 메소드 일부 1
2
3
4
5
6
7
8//1. 이전에 있던 소스 동작 , 자동으로 2번으로
public PagedResources<Resource<T>> toResource(Page<T> entity) {
return toResource(entity, it -> new Resource<>(it));
}
// 2번
public <R extends ResourceSupport> PagedResources<R> toResource(Page<T> page, ResourceAssembler<T, R> assembler) {
return createResource(page, assembler, Optional.empty());
} - 즉 이전 소스를 넣으면 알아서 자동으로 new Resource로 만들고 있음
- 이를 직접 사용하도록 2번호출을 하되 new EventResource(e)로 하고 있다
- 이렇게 하면 이전에 EventResource에 정의한 링크들이 붙을 것이다
- 이제 프로필 링크까지 테스트 해도 통과한다
- 각각의 프로필이 다르기 때문에 EventResource에서 추가해주는 링크에서 프로필 제외
- 문서 완성은…나중에
- 이전에 있던 소스 assembler.toResource(page)의 동작을 잘 생각해보자
1 |
|
이벤트 조회 API
1 |
|
- 설명
- 테스트는 2가지
1 |
|
- 설명
- 컨트롤러에 RequestMapping주소가 있으면 메소드의 주소와 컴바인되서 연결
- 네임 홀더내용은 PathVariable로 가져올 수 있다
- 만약 가져온 값이 비었으면 404 에러 발생하도록 -> empty 테스트 만족
- 비어있지않으면 Optional에서 Event를 빼서 resource로 만든다
- 프로필 링크를 추가한후 반환한다
이벤트 수정 API
- 지금까지 예제의 총 재활용
1 |
|
- 설명
- 테스트들을 잘 살펴볼 것 - 지금까지의 테스트 코드와 비슷
- 온전한 event생성을 위해 generatedEvent에서 온전한 event생성
구현체는 다음과 같다
@PutMapping("/{id}")
public ResponseEntity updateEvent(@PathVariable Long id,
@RequestBody @Valid EventDto eventDto,
Errors errors){
Optional<Event> optionalEvent = eventRepository.findById(id);
if(optionalEvent.isEmpty()){
return ResponseEntity.notFound().build();
}
if(errors.hasErrors()){
errors.getAllErrors().forEach(objectError -> {
log.info("**ERROR: "+objectError);
});
return badRequestWithErrorsResource(errors);
}
eventValidator.validate(eventDto,errors);
if(errors.hasErrors()){
errors.getAllErrors().forEach(objectError -> {
log.info("**ERROR: "+objectError);
});
return badRequestWithErrorsResource(errors);
}
Event existingEvent = optionalEvent.get();
modelMapper.map(eventDto,existingEvent);
Event updatedEvent = eventRepository.save(existingEvent);
EventResource eventResource = new EventResource(updatedEvent);
eventResource.add(new Link("/docs/index.html#resources-events-update").withRel("profile"));
return ResponseEntity.ok().body(eventResource);
}
- 설명
- 역시 지금까지 보아왔던 것의 종합판
- valid가 끝나면 들어온값으로 원래 값을 덮어 씌우면 된다
- modelMapper를 이용해 갚을 덮어 씌운 후 반영을 위해 update()
- 그리고 저장한후 해당 이벤트를 프로필 링크 추가한 리소스 형태로 반환
이로써 API 구현은 끝이지만 끝이 아님? 응?