강의/Java Spring Boot

todolist CRUD 기능 구현하기

studylida 2025. 1. 26. 03:19

먼저 CREATE, 그러니까 투두를 추가하는 기능을 먼저 구현해보자.

 

버튼을 만드는 코드는 딱히 어렵지 않다 `<a href="add-todo">Add Todo</a>` 버튼이 있으면 좋겠다~ 하는 위치에 추가해주면 된다.

 

디자인이 별로다 하면 이전 게시글에서 배웠던 부트스트랩을 사용할 있다. `<a href="add-todo" class="btn btn-success">Add Todo</a>` 같이 사용할 있다.

 

부트스트랩에 관한 정보는 Introduction · Bootstrap v5.0 (getbootstrap.com)에서 확인할 있다. 모르는 있으면 저기서 찾아보자.

 

TodoController 가서 버튼을 클릭했을 작동할 논리를 만들어보자

 

```

@RequestMapping(value="add-todo", method=RequestMethod.GET)

public String showNewTodoPage() {

return "todo";

}

 

 

@RequestMapping(value="add-todo", method=RequestMethod.POST)

public String showNewTodoPage() {

return "redirect:list-todos";

}

 

 

```

 

위의 논리가 작동할 반환할 todo.jsp 만들어보자.

```

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

 

<html>

<head>

<title>Add Todo Page</title>

<link href="webjars/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet"

</head>

<body>

<div class="container">

<h1>Enter Todo Details</h1>

<form method="post">

Description: <input type="text" name="description"/>

<input type="submit" class="btn btn-success"/>

</form>

</div>

 

<script src="webjars/bootstrap/5.1.3/js/bootstrap.min.js"></script>

<script src="webjars/jquery/3.6.0/jquery.min.js"></script>

</body>

</html>

```

 

이제 이렇게 추가한 Todo 우리가 만든 TodoList 삽입되도록 하면 된다. 해당 논리는 TodoService 작성되며 아래와 같다.

 

```

priavte Static int todoCount = 0;

 

static {

todos.add(new Todo(++todoCount, "in28minutes", "Learn AWS", LocalDate.now().plusYear(1), false));

todos.add(new Todo(++todoCount, "in28minutes", "Learn DevOps", LocalDate.now().plusYear(1), false));

todos.add(new Todo(++todoCount, "in28minutes", "Learn Full Stack Development", LocalDate.now().plusYear(1), false));

}

 

public void addTodo(String username, String description, LocalDate targetDate, bool done) {

Todo todo = new Todo(++todoCount, username, description, targetDate, done);

todos.add(todo);

}

```

 

로직을 활용하기 위해 수정한 코드는 다음과 같다.

 

```

@RequestMapping(value="add-todo", method=RequestMethod.POST)

public String addNewTodo(@RequestParam String username, ModelMap model) {

String username = (String)model.get("name");

todoService.addTodo(username, description, LocalDate.now().plusYear(1), false);

 

return "redirect:list-todos";

}

```

 

이렇게 성공적으로 투두를 추가하는 기능을 만들었다.

 

이제 투두에 입력되는 값을 검사하는 기능을 만들어보자.

 

간단하게는 프론트에서 required 기능을 통해 값이 입력되지 않는 것을 막을 있다.

 

```

<form method="post">

Description: <input type="text" name="description"/ required="required">

<input type="submit" class="btn btn-success"/>

</form>

```

 

다만 이러한 검증은 사용자가 무력화시키기 쉽다.

 

때문에 서버 단에서의 검증을 해주는 좋다.

 

이를 위해서

  1. 검증 관련 의존성 추가
  2. 양방향 바인딩 구현
  3. Bean 검증 구현
  4. 뷰에 검증 오류 표시

 

단계를 걸칠 것이다.

 

이를 위한 starter 프로젝트가 있다. pom.xml에서 의존성 추가를 해주자.

 

```

<dependency>

<groupId>org.spirngframework.boot</groupId>

<artifactId>spring-boot-starter-validation</artifactId>

</dependency>

```

 

이제 커맨드 (Command Bean, Form Backing Object)이라는 개념을 사용해서 양방향 바인딩을 구현해보려고 한다.

 

이렇게만 써놓으면 지금 하려는 뭔지 이해가 갈테니 예시를 들어서 설명해보겠다.

 

우리는 지금 @RequestParam 사용해 바인딩하고 있다. 여기서 필드가 20 30개가 된다면 코드가 매우 복잡해질 것이다.

 

때문에 우리가 바인딩하려는 필드를 모두 추가하는 대신에 직접 Todo Bean 바인딩하려는 것이다.

 

이렇게 하면 굳이 특정 필드를 코드에 포함하지 않아도 되고, 원하는 필드만 사용할 있으며, 나중에 사용하고 싶은 필드가 추가로 생겼을 코드를 수정하지 않아도 된다.

 

이를 적용한 코드는 아래와 같다.

 

```

@RequestMapping(value="add-todo", method=RequestMethod.POST)

public String addNewTodo(ModelMap model, Todo todo) {

String username = (String)model.get("name");

todoService.addTodo(username, todo.getDescription(), LocalDate.now().plusYear(1), false);

 

return "redirect:list-todos";

}

```

 

이제 양식 보조 객체를 jsp에서도 사용하는 법을 알아보자.

 

검색창에 spring form tag library documentation 검색하면 View technologies 페이지가 나온다.

 

여기서 form tag library 검색하면 나오는 코드를 jsp 파일에 삽입하면 된다.

```

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

 

...

<form:form method="post" modelAttribute="todo">

Description: <form:input type="text" path="description" required="required"/>

<input type="submit" class="btn btn-success"/>

</form:form>

```

 

이렇게 하고 실행해보면 오류가 난다. 이유는 todo.jsp 호출하는 showNewTodoPage에서 todo 속성을 모델에 설정하지 않았기 때문이다.

 

오류를 해결하기 위해 아래와 같이 수정할 있다.

```

@RequestMapping(value="add-todo", method=RequestMethod.GET)

public String showNewTodoPage(ModelMap model, Todo todo) {

String username = (String)model.get("name");

Todo todo = new Todo(0, username, "", LocalDate.now().plusYear(1), false);

 

model.put("user",todo);

return "todo";

}

```

 

이렇게 하고 실행하면 오류가 발생한다. 디버깅 해보면 id 필드와 done 필드에 null값이 들어온다고 한다.

 

id done 사용자가 입력하게 하고 싶진 않은데, 우리가 미리 지정해둔 값이 들어가면 좋겠다. 이럴 때는 `type=hidden` 이용할 있다.

 

...

<form:form method="post" modelAttribute="todo">

Description: <form:input type="text" path="description" required="required"/>

<form:input type="hidden" path="id"/>

<form:input type="hidden" path="done"/>

<input type="submit" class="btn btn-success"/>

</form:form>

```

 

이렇게 해서 검증 시스템을 구현했다.

 

이해를 위해 어떻게 돌아가는지 자세히 살펴보자.

 

버튼을 클릭하면 add-todo url 입력되고, 이를 매핑한 컨트롤러의 논리가 동작하여 Add Todo 페이지가 로딩된다. 이때는 GET 요청이 전송되는데, 과정에서 todo 생성하고, 그걸 사용자가 입력할 양식에 매핑한다. 진짜인지 알아보기 위해서 todo 만들 description 란에 test라고 적어보자.

 

그리고 실행하면 양식에 test라고 적혀있는 있다.

 

그런데 아까 양방향 바인딩이라고 하지 않았는가? 그럼 나머지 방향은 무엇일까? 그건 사용자가 양식에 입력하는 값이 POST 요청에 매핑되는 것이 나머지 방향이다.

 

양식에 값을 입력하고 submit 버튼을 누르면 Todos 값이 추가된 확인할 있다.

 

이렇게 하면 코드가 유연해지고, 오류가 발생했을 찾아내기 쉬워진다.

 

이제 Bean 검증을 추가해보자. 이는 매우 간단하다. Todo.java 가서 @Size 어노테이션을 활용하면 된다.

 

```

private int id;

private String username;

@Size(min=10, message="Enter atleast 10 characters")

private String description;

private LocalDate targetDate;

private bool done;

```

 

그리고 Controller 가서 검증을 적용하고 싶은 Bean 앞에 @Valid 어노테이션을 붙여주면 된다.

 

이외에도 여러 검증 방식이 있다. jakarta.validation.constraints에서 확인해보고 사용하자.

 

```

@RequestMapping(value="add-todo", method=RequestMethod.POST)

public String addNewTodo(ModelMap model, @Valid Todo todo) {

String username = (String)model.get("name");

todoService.addTodo(username, todo.getDescription(), LocalDate.now().plusYear(1), false);

 

return "redirect:list-todos";

}

```

 

이렇게 하고 실행하면, 10글자가 되지 않도록 검색할 에러페이지로 넘어가는 확인할 있다. 그리고 에러페이지를 살펴보면 우리가 아까 에러메시지로 설정했던 `Enter atleast 10 characters` 확인할 있다. 하지만 우리가 원하는 이게 아니라 검증조건을 통과할 없는 입력이 주어졌을 , 메시지를 띄우고 다시 입력할 기회를 주는 것이다.

 

이는 BindResult 속성을 통해 해결할 있다. 아래와 같이 코드를 작성하자.

 

```

@RequestMapping(value="add-todo", method=RequestMethod.POST)

public String addNewTodo(ModelMap model, @Valid Todo todo, BindResult result) {

if(result.hasError()) {

return "todo";

}

 

String username = (String)model.get("name");

todoService.addTodo(username, todo.getDescription(), LocalDate.now().plusYear(1), false);

 

return "redirect:list-todos";

}

```

 

이렇게 하면 에러페이지로 넘어가는 막을 있다.

 

이제 jsp에서 폼에 추가적인 코드를 작성해서 사용자에게 메시지를 띄워보자.

 

...

<form:form method="post" modelAttribute="todo">

Description: <form:input type="text" path="description" required="required"/>

<form:errors path="description"/>

<form:input type="hidden" path="id" cssClass="text-warning"/>

<form:input type="hidden" path="done"/>

<input type="submit" class="btn btn-success"/>

</form:form>

```

 

일반적인 html 태그의 경우 꾸미기 위해 css 사용하지만 <form:errors path="description"/> 스프링 태그다. Spring 태그를 꾸미기 위해서는 cssClass 사용한다.

 

해서 CREATE 기능을 완성했다.

 

이제 삭제 기능을 구현해보자. 버튼을 구현하는 역시 어렵지 않다. 다만 차이가 있다. CREATE 달리 DELETE 특정 todo 대응되어야 하기 때문에 하나씩 버튼이 있어야 한다.

 

```

<table>

<thead>

<tr>

<th>ID</th>

<th>Description</th>

<th>Target Date</th>

<th>Is Done?</th>

<th></th>

</tr>

</thead>

<tbody>

<c:forEach items="${todos}" var="todo>

<tr>

<td>${todo.id}</td>

<td>${todo.description}</td>

<td>${todo.targetDate}</td>

<td>${todo.done}</td>

<td><a href="delete-todo" class="btn btn-warning">DELETE ${todo.id}</a></td>

</tr>

</tbody>

</table>

```

 

이제 삭제 논리를 작성해보자.

 

```

public void deleteById(int id) {

predicate<? super Todo> predicate = todo -> todo.getId() == id;

todos.removeIf(predicate);

}

```

 

removeIf 괄호안의 조건을 만족하는 녀석을 지운다. 그리고 위의 predicate 인터페이스의 인스턴스를 생성하는 람다 표현식으로, 해당 인터페이스는 주어진 조건에 맞는지 검사하는 역할을 한다.

 

이해가 가면 함수형 프로그래밍에 대해 잠깐 배워보고 오자.

 

그리고 논리를 실행할 delete-todo url 대응되는 함수를 만들어보자.

 

```

@RequestMapping("delete-todo")

public String deleteTodo(@RequestParam int id) {

todoService.deleteById(id);

return "redirect:list-todos";

}

 

```

 

이렇게 하면 된다

 

UPDATE 기능 구현도 어렵지 않다.

 

버튼은 이전에 삭제 버튼을 구현할 때처럼 해주면 된다.

 

```

public Todo findById(int id) {

Predicate<? super Todo> predicate = todo->todo.getId() == id;

Todo todo = todos.stream().filter(predicate).findFirst().get();

return todo;

}

 

public void updateTodo(@Valid Todo todo) {

deleteById(todo);

todos.add(todo);

}

```

 

```

@RequestMapping(value="update-todo" method=RequestMethod.GET)

public String showUpdateTodoPage(@RequestParam int id, ModelMap model) {

Todo todo = todoService.findById(id);

model.addAttribute("todo", todo);

 

return "todo";

}

 

@RequestMapping(value="update-todo", method=RequestMethod.POST)

public String updateTodoPage(ModelMap model, @Valid Todo todo, BindResult result) {

if(result.hasError()) {

return "todo";

}

 

String username = (String)model.get("name");

todo.setUsername(username);

todoService.updateTodo(todo);

return "redirect:list-todos";

}

```

 

이렇게 하면 업데이트 기능이 만들어진 확인할 있다. 다만, 날짜를 정해주지 않아서 그런지 날짜가 사라진 것을 확인할 있다.

 

이걸 수정하기 위해, todo.jsp 목표날짜에 관한 추가하려고 한다.

 

본격적으로 코드를 작성하기 전에 application.properties 가서 `spring.mvc.format.date=yyyy-MM-dd` 입력해주자. 날짜 포맷은 여러 가지가 있기 때문에 명시적으로 작성해서 통일해주면 좋다.

 

그리고 아래처럼 코드를 작성해주자.

 

...

<form:form method="post" modelAttribute="todo">

<fieldset class="mb-3">

<form:label path="description">Description</form:label>

<form:input type="text" path="description" required="required"/>

<form:errors path="description" cssClass="text-warning"/>

</fieldset>

 

<fieldset class="mb-3">

<form:label path="description">Target Date</form:label>

<form:input type="text" path="targetDate" required="required"/>

<form:errors path="targetDate" cssClass="text-warning"/>

</fieldset>

 

 

<form:input type="hidden" path="id"/>

 

<form:input type="hidden" path="done"/>

 

<input type="submit" class="btn btn-success"/>

</form:form>

```

 

이렇게 하면 날짜를 입력할 있게 되고, 아까처럼 값이 입력되지 않는다.

 

이제 addNewTodo에서 날짜에 관해 하드코딩한 것을 수정해주자.

 

```

@RequestMapping(value="add-todo", method=RequestMethod.POST)

public String addNewTodo(ModelMap model, @Valid Todo todo, BindResult result) {

if(result.hasError()) {

return "todo";

}

 

String username = (String)model.get("name");

todoService.addTodo(username, todo.getDescription(), todo.getTargetDate(), false);

 

return "redirect:list-todos";

}

```

 

이렇게 해서 기능 구현은 끝났다.

 

그런데 여기서 조금 나아가서 날짜를 고를 달력 팝업 창이 뜨면 좋겠다는 생각 드나?

 

들어도 상관없다 내가 거니까

 

우리는 이를 위해서 Bootstrap Datepicker 사용할 것이다. pom.xml 아래 의존성을 추가하자.

 

```

<dependency>

<groupId>org.webjars</groupId>

<artifactId>bootstrap-datepicker</artifactId>

<version>1.9.0</version>

</dependency>

```

 

그리고 Maven이나 외부라이브러리에 가서 bootstrap datepicker 관한 경로를 찾거나 인텔리제이 기준 double shift 누른 `bootstrap-datepicker` 검색해서 경로 찾자. 이클립스는 기억 난다. 알아서 찾아라

 

그리고 todo.jsp body 밑에 `<script src="webjars/bootstrap-datepicker/1.9.0/js/bootstrap-datepicker.min.js"/>` 추가하고, head `<link href="webjars/bootstrap-datepicker/1.9.0/css/bootstrap-datepikcer.standalone.min.css" rel="stylesheet"> 추가해주자.

 

이제 검색창에 bootstrap-datepicker 검색하면 나오는 사이트에 들어가 Configuration 있는 코드를 복붙하여 todo.jsp body 맨밑에 아래와 같이 복붙한 수정해주자.

 

```

<script src="webjars/bootstrap-datepicker/1.9.0/js/bootstrap-datepicker.min.js"></script>

<script type="text/javascript">

$('#targetDate').datepicker({

format: 'yyyy-mm-dd'

});

</script>

```

 

이제 모든 기능이 정상적으로 작동하는 확인할 있다.

 

! ! ! !