-
[To-Do 앱]스프링 부트(Spring Boot) RESTful API - PUT & Vue.js/Node.js Update 기능 구현웹 어플리케이션 2019. 3. 4. 15:06
지난번 포스트까지 해서 우리는 Spring Boot로 RESTful API를 만들고, Node.js와 Vue.js로 Frontend 서버를 만들어 보았다. 이전 포스트까지 전부 진행했다면 이제부터는 혼자 원하는 기능을 만들어가도 된다. 이번 포스트에서는 RESTful API의 메서드 중 하나인 PUT을 만들고 서비스 레벨부터 UI레벨까지 full stack으로 개발을 진행 해 보도록 하겠다.
예상독자
- IntelliJ, Webstorm, Atom 등 자바스크립트 IDE중 하나를 설치했다.
- 자바스크립트를 좀 안다.
- 백엔드는 알아서 구현할 수 있거나 아래의 튜토리얼들을 마쳤다.
- [To-Do 앱]스프링부트(SpringBoot) 웹 어플리케이션)
- [To-Do 앱] 스프링 부트(Spring Boot) RESTful API - GET
- [To-Do 앱] 스프링 부트(Spring Boot) RESTful API - POST
- [To-Do 앱]vue.js와 node.js를 이용해 웹 앱 만들기
- [To-Do 앱]Vue.js/Node.js 앱 에서 API Call 하기 (Axios)
- [To-Do 앱]Vue.js/Node.js Bootstrap-vue를 이용한 UI 구현
- 도커에 스프링 앱을 올리고 싶다면: 스프링 부트 도커에 올리기 (Dockerizing Spring Boot App) 참고
목표
- Backend - Spring Boot
- Service
- Controller
- CORS 에러 해결
- Frontend - node.js/vue.js
- method - update
- template - checkbox
- v-if/else를 이용한 UI
Backend - Spring Boot
백엔드를 개발하는 순서는 어떤 개발방법을 따르느냐에 따라 다르다. 보통 Test-Driven Development 개발방법에서는 클래스와 메서드의 껍데기를 정의 한 후, Unit Tests를 작성하고 이후에 클래스/메서드의 내부를 구현한다. 이 포스트에서는 TDD 개발방법을 사용하지 않으므로, 그냥 서비스 레벨부터 구현하도록 하겠다.
Service
ToDoItem을 수정하기 위해 ToDoItemService.java에 update메서드를 추가하도록 하자.
public ToDoItem update(final ToDoItem toDoItem) {
if (toDoItem == null) {
throw new NullPointerException("To Do Item cannot be null");
}
final ToDoItem original = toDoItemRepository.findById(toDoItem.getId())
.orElseThrow(() -> new RuntimeException("Entity Not Found"));
original.setTitle(toDoItem.getTitle());
original.setDone(toDoItem.isDone());
return toDoItemRepository.save(original);
}이 메서드가 하는 일은 1) 인자로 수정 할 toDoItem을 받는다. 이 toDoItem의 id를 이용해 기존 toDoItem을 찾는다. 유저에게서 넘어온 toDoItem을 바로 저장하면 안된다. 해당 아이템이 데이터베이스에 존재하는지 안하는지 먼저 확인해야하고, 또 보안 문제를 예방하기 위한 목적도 있다. 보통은 유저에게서 넘어온 toDoItem을 검사하고(validation) 해당 내용의 값을 복사하여 집어넣는다. 여기서는 검사 기능이 없으니 바로 복사하여 save하도록 한다.
참고를 위해 ToDoItemService.java를 첨부한다.
package com.fsoftwareengineer.MySpringApp.ToDoItem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ToDoItemService {
@Autowired
private ToDoItemRepository toDoItemRepository;
public ToDoItem get(final String id) {
// do id validation
return toDoItemRepository.findById(id).orElse(null);
}
public ToDoItem create(final ToDoItem toDoItem) {
if (toDoItem == null) {
throw new NullPointerException("To Do Item cannot be null.");
}
return toDoItemRepository.insert(toDoItem);
}
public ToDoItem update(final ToDoItem toDoItem) {
if (toDoItem == null) {
throw new NullPointerException("To Do Item cannot be null");
}
final ToDoItem original = toDoItemRepository.findById(toDoItem.getId())
.orElseThrow(() -> new RuntimeException("Entity Not Found"));
original.setTitle(toDoItem.getTitle());
original.setDone(toDoItem.isDone());
return toDoItemRepository.save(original);
}
public List<ToDoItem> getAll() {
return toDoItemRepository.findAll();
}
}Controller
update서비스를 만들었다면 이제 update서비스를 이용할 controller의 메서드를 만들어 주어야 한다.
@RequestMapping(method = RequestMethod.PUT)
public @ResponseBody ToDoItemResponse update(@RequestBody final ToDoItemRequest toDoItemRequest) {
List<String> errors = new ArrayList<>();
ToDoItem toDoItem = ToDoItemAdapter.toToDoItem(toDoItemRequest);
try {
toDoItem = toDoItemService.update(toDoItem);
} catch (final Exception e) {
errors.add(e.getMessage());
e.printStackTrace();
}
return ToDoItemAdapter.toToDoItemResponse(toDoItem, errors);
}이 메서드의 구현은 create과 아주 비슷하다. 우리는 create하는 대신에 update를 할 예정이다. RequestMethod를 PUT으로 바꾸고, toDoItemService.update메서드를 부른다. PUT을 사용하지 않고 POST만 사용하는 어플리케이션들의 경우에는 upsert(update+insert)라고해서 없으면 create을 있으면 update를 하는 메서드 하나로 생성/수정을 구현하는 경우도 있다. 일단 이렇게 했으면 컨트롤러 만드는 것에 성공 한 것이다.
참고를 위해 ToDoItemController.java 전부를 첨부한다.
package com.fsoftwareengineer.MySpringApp.ToDoItem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/todo")
public class ToDoItemController {
@Autowired
private ToDoItemService toDoItemService;
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
public @ResponseBody ToDoItemResponse get(@PathVariable(value="id") String id) {
List<String> errors = new ArrayList<>();
ToDoItem toDoItem = null;
try {
toDoItem = toDoItemService.get(id);
} catch (final Exception e) {
errors.add(e.getMessage());
}
return ToDoItemAdapter.toToDoItemResponse(toDoItem, errors);
}
@RequestMapping(method = RequestMethod.GET)
public @ResponseBody List<ToDoItemResponse> getAll() {
List<String> errors = new ArrayList<>();
List<ToDoItem> toDoItems = toDoItemService.getAll();
List<ToDoItemResponse> toDoItemResponses = new ArrayList<>();
toDoItems.stream().forEach(toDoItem -> {
toDoItemResponses.add(ToDoItemAdapter.toToDoItemResponse(toDoItem, errors));
});
return toDoItemResponses;
}
@RequestMapping(method = RequestMethod.POST)
public @ResponseBody ToDoItemResponse create(@RequestBody final ToDoItemRequest toDoItemRequest) {
List<String> errors = new ArrayList<>();
ToDoItem toDoItem = ToDoItemAdapter.toToDoItem(toDoItemRequest);
try {
toDoItem = toDoItemService.create(toDoItem);
} catch (final Exception e) {
errors.add(e.getMessage());
e.printStackTrace();
}
return ToDoItemAdapter.toToDoItemResponse(toDoItem, errors);
}
@RequestMapping(method = RequestMethod.PUT)
public @ResponseBody ToDoItemResponse update(@RequestBody final ToDoItemRequest toDoItemRequest) {
List<String> errors = new ArrayList<>();
ToDoItem toDoItem = ToDoItemAdapter.toToDoItem(toDoItemRequest);
try {
toDoItem = toDoItemService.update(toDoItem);
} catch (final Exception e) {
errors.add(e.getMessage());
e.printStackTrace();
}
return ToDoItemAdapter.toToDoItemResponse(toDoItem, errors);
}
}잠깐 짚고 넘어가야 할 부분이 있다. 우리는 ToDoItemAdapter를 이용해 ToDoItemRequest를 ToDoItem으로 변환한다. update를 할 시에는 id가 반드시 필요하므로 id를 제대로 adapt하는지 확인 해 보자. ToDoItemAdapter.java의 toToDoItem메서드가 아래를 아래처럼 수정하라.
public static ToDoItem toToDoItem(final ToDoItemRequest toDoItemRequest) {
if(toDoItemRequest == null) {
return null;
}
return ToDoItem.builder()
.id(toDoItemRequest.getId()) // 이 부분을 반드시 추가 해 주어야 한다.
.title(toDoItemRequest.getTitle())
.done(toDoItemRequest.isDone())
.build();
}id를 넘기는 부분이 없어서 위와 같이 메서드를 수정했다.
CORS 에러 해결
이렇게 하면 될 줄 알고 실행 했는데 안돼서 당황 했을 것이다. 다양한 리퀘스트 메서드를 사용하려면 WebConfig에 사용 할 메서드를 명시해 주어야 한다. WebConfig.java로 가서 아래와 같이 allowedMethods를 추가 해 준다. 길지 않으니 소스 전체을 첨부하도록 하겠다.
package com.fsoftwareengineer.MySpringApp.Config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowedOrigins("http://127.0.0.1:8080")
.allowedOrigins("http://localhost:8080");
}
}이렇게 하고 gradle bootRun을 이용해 스프링 부트 앱을 실행시켜보도록 하자. 아무런 에러가 나지 않는다면 이제 frontend를 개발하러 가면 된다.
Frontend - node.js/vue.js
Frontend에서 개발 할 부분은 다음과 같다. 첫번째로 axios를 이용해 PUT메서드를 call하는 메서드부분을 작성한다. 두번째로 template에서 checkbox를 체크시 앞에서 만든 메서드를 call하는 부분을 작성한다. 세번째로 체크된 To Do 아이템에는 회색과 strikethrough를 적용한다.
method - update
우리가 만들 메서드의 이름은 markDone이다. 이 메서드는 toDoItem의 done이 true -> false로 false -> true로 변경한다. 그리고 변경된 아이템을 PUT메서드를 이용해 Spring Boot 백엔드 서버로 리퀘스트를 보낸다. Hello.vue에 다음과 같은 메서드를 추가하도록 하자.
markDone: function (toDoItem) {
if (!toDoItem) return // null이면 그냥 리턴한다.
let vm = this
toDoItem.done = !toDoItem.done // true -> false, false -> true로 변경한다.
axios.put(baseUrl, toDoItem) // 백엔드 서버로 리퀘스트를 보낸다.
.then(response => {
vm.initToDoList() // 리스트를 갱신한다.
})
.catch(error => {
console.log(error)
})
}참고를 위해 스크립트 전체를 첨부하도록 하겠다.
<script>
import axios from 'axios'
let baseUrl = 'http://127.0.0.1:5000/todo/'
export default {
name: 'hello',
data: () => {
return {
toDoItems: [],
newToDoItemRequest: {}
}
},
methods: {
initToDoList: function () {
let vm = this
axios.get(baseUrl)
.then(response => {
vm.toDoItems = response.data.map(r => r.data)
})
.catch(e => {
console.log('error : ', e)
})
},
createToDo: function (event) {
event.preventDefault()
let vm = this
if (!vm.newToDoItemRequest.title) return
axios.post(baseUrl, vm.newToDoItemRequest)
.then(response => {
vm.initToDoList()
vm.newToDoItemRequest = {}
})
.catch(error => {
console.log(error)
})
},
markDone: function (toDoItem) {
if (!toDoItem) return
let vm = this
toDoItem.done = !toDoItem.done
axios.put(baseUrl, toDoItem)
.then(response => {
vm.initToDoList()
})
.catch(error => {
console.log(error)
})
}
},
created () {
this.initToDoList()
}
}
</script>참고! 이 포스트를 작성하면서 내가 약간 refactoring을 한 부분이 있다. 어디냐면 initToDoList부분이다. 혹시 현재까지의 본인의 코드와 이 포스트의 코드가 다르다면 initToDoList부분을 수정해야 할 것이다.
template - checkbox
위처럼 메서드를 만들었다면 이제 체크박스 클릭시 이 메서드를 불러야 한다. 그냥 부르는게 아니라 이 메서드에 체크된 부분의 toDoItem을 넣어줘야 한다.
b-list-group을 아래처럼 수정 해 주자.
<b-list-group v-if="toDoItems && toDoItems.length">
<b-list-group-item
v-for="toDoItem of toDoItems"
v-bind:data="toDoItem.id"
v-bind:key="toDoItem.id" style="display: flex;">
<b-form-checkbox
v-model="toDoItem.done"
v-on:change="markDone(toDoItem)">
</b-form-checkbox>
{{toDoItem.title}}
</b-list-group-item>
</b-list-group>유심히 봐야 할 부분은 v-model과 v-on:change부분이다. v-model에 toDoItem.done을 넣으면 done이 true일때는 체크가 되고 done이 false일때는 체크가 안 될 것이다. v-on:change에는 아까 만들었던 markDone을 연결 해 주어야 한다. 그리고 markDone이 눌리면 눌린 toDoItem을 markDone의 파라미터로 넘겨주어야한다.
여기까지 했으면 테스트를 해 보아라. 체크박스를 누르고 새로고침을 해도 체크박스가 여전히 체크되어있는지 확인 해 봐라.
참고를 위해 템플릿 전체를 첨부한다.
<template>
<div class="hello">
<b-card
header="오늘 해야 할 일"
style="max-width: 40rem; margin: auto; margin-top: 10vh;"
class="mb-2"
border-variant="info"
align="left">
<b-form-group id="to-do-input">
<b-container fluid>
<b-row class="my-1">
<b-col sm="10">
<b-form-input v-model="newToDoItemRequest.title" type="text"
placeholder="새 할 일을 적으세요." @keyup.enter="createToDo"/>
</b-col>
<b-col sm="2">
<b-button variant="outline-primary" v-on:click="createToDo">추가</b-button>
</b-col>
</b-row>
</b-container>
</b-form-group>
<b-list-group v-if="toDoItems && toDoItems.length">
<b-list-group-item
v-for="toDoItem of toDoItems"
v-bind:data="toDoItem.id"
v-bind:key="toDoItem.id" style="display: flex;">
<b-form-checkbox
v-model="toDoItem.done"
v-on:change="markDone(toDoItem)">
</b-form-checkbox>
{{toDoItem.title}}
</b-list-group-item>
</b-list-group>
</b-card>
</div>
</template>v-if/else를 이용한 UI
이제 UI를 더 가꾸기 위해 체크된 아이템은 회색처리를 하고 strikethrough로 완료되었음을 표시 해 보자. <b-list-group-item></b-list-group-item>의 내부를 다음과 같이 수정 해 보자.
<b-form-checkbox
v-model="toDoItem.done"
v-on:change="markDone(toDoItem)">
</b-form-checkbox>
<span v-if="toDoItem.done" style="text-decoration: line-through; color:#D3D3D3;"> {{toDoItem.title}}</span>
<span v-else>{{toDoItem.title}}</span>
</b-list-group-item>달라진게 무엇인가? 바로 span이 생겼다는 것이다. 현재 두가지 span을 추가 했다. 첫번째 span은 v-if="toDoItem.done"이 존재한다. 무슨 뜻인가? 이 toDoItem.done이 true이면 이 span을 이용해라라는 뜻이다. 완료된 toDoItem일 경우 text-decoration이 line-through이고 color가 #D3D3D3 (grey)로 스타일된 span을 보여줄 것이다. v-else가 있는 span은 done이 false일 경우 그냥 title을 보여 줄 것이다. 여기까지 잘 따라왔다면 여러분은 아래와 같은 테스트를 할 수 있을 것이다.
[To Do List 테스트]
끝
이번 포스트에서는 PUT RESTful API의 벡엔드를 구현하고 프론트엔드의 기능까지해서 full stack으로 개발 해 보았다. 다음 포스트에서는 Delete 메서드를 이용해 삭제하는 기능을 구현 해 볼 것이다. 그리고 그 다음에는 드디어! 임베디드 몽고디비가 아닌 도커에 몽고디비를 설치하고 스프링 부트를 몽고디비에 연결 해 볼 것이다.
'웹 어플리케이션' 카테고리의 다른 글
스프링 부트(Spring Boot) + 몽고디비(Mongo DB) 도커(Docker)에 올리기 (5) 2019.03.05 [To-Do 앱]Vue.js/Node.js To Do Item 추가 기능 만들기 (1) 2019.02.28 [To-Do 앱]Vue.js/Node.js Bootstrap-vue를 이용한 UI 구현 (1) 2019.02.27 [To-Do 앱]Vue.js/Node.js 앱 에서 API Call 하기 (Axios) (2) 2019.02.26 스프링 부트 도커에 올리기(Dockerizing Spring Boot App) (3) 2019.02.25 댓글