ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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으로 개발을 진행 해 보도록 하겠다.

    예상독자

    목표

    • 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 메서드를 이용해 삭제하는 기능을 구현 해 볼 것이다. 그리고 그 다음에는 드디어! 임베디드 몽고디비가 아닌 도커에 몽고디비를 설치하고 스프링 부트를 몽고디비에 연결 해 볼 것이다.

    댓글

f.software engineer @ All Right Reserved