HeidiSQl을 실행한다.
왼쪽 하단에 있는 신규 버튼을 누르고 세션 이름을 설정해줍니다.
저장 후 열기를 눌러주면 아래와 같은 창이 나옵니다.
mydb라는 이름의 새로운 데이터베이스를 생성해줍니다.
데이터베이스 생성 후 사용자 아이콘을 새로운 사용자를 만든 후 객체 추가 버튼을 눌러 방금 만든 데이터베이스를 적용해준 뒤 전체 권한을 줍니다.
LocalDB를 연결 해제한 후 새로 만든 사용자(myadmin)으로 로그인 해준 뒤
게시판에 사용할 테이블을 만들어줍니다.
JPA와 MariaDB 사용을 위해 pom.xml에 의존성을 추가해줍니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.1.2</version>
</dependency>
현재 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.project</groupId>
<artifactId>myhome</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>myhome</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.properties에 DB사용을 위한 코드를 추가합니다.
데이터베이스 이름, 사용자, 비밀번호, 드라이버
spring.datasource.url=jdbc:mariadb://localhost:3306/mydb
spring.datasource.username=myadmin
spring.datasource.password=(비밀번호)
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
com.project.myhome 밑에 model 패키지를 생성한 후 Board라는 클래스를 생성해줍니다.
package com.project.myhome.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
@Entity
@Data
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String title;
private String content;
}
JPA사용을 위해 com.project.myhome 밑에 repository 폴더를 생성한 후 BoardRepository 인터페이스를 생성합니다.
package com.project.myhome.repository;
import com.project.myhome.model.Board;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BoardRepository extends JpaRepository<Board, Long> {
}
BoardRepository을 이용해서 테이블의 정보를 가져오기 위해 BoardController를 수정해줍니다.
package com.project.myhome.controller;
import com.project.myhome.model.Board;
import com.project.myhome.repository.BoardRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@RequestMapping("/board")
public class BoardController {
@Autowired
private BoardRepository boardRepository;
@GetMapping("/list")
public String list(Model model){
List<Board> boards = boardRepository.findAll();
model.addAttribute("boards", boards);
return "board/list";
}
}
list.html을 수정해서 JPA가 잘 동작하는지 확인을 해줍니다.
<div class="container">
<h2>게시판</h2>
<div>총 건수 : <span th:text="${#lists.size(boards)}"></span></div>
</div>
board 테이블에서 데이터를 클릭 후 우클릭을 해서 행삽입을 눌러줍니다.
title에 제목 content에 내용을 적고 적용을 눌러줍니다.
테이블에 적용을 누르게 되면 게시판 창에 총 건수가 0에서 1로 바뀌게 됩니다.
bootstrap 홈페이지에서 table을 검색한 후 샘플 코드를 가져옵니다.
가져온 코드를 list.html에 붙여넣고 반복문으로 테이블에서 값을 가져오기 위해 th:each를 사용합니다.
<div class="container">
<h2>게시판</h2>
<div>총 건수 : <span th:text="${#lists.size(boards)}"></span></div>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">번호</th>
<th scope="col">제목</th>
<th scope="col">작성자</th>
</tr>
</thead>
<tbody>
<tr th:each="board : ${boards}">
<td th:text="${board.id}"></td>
<td th:text="${board.title}"></td>
<td>홍길동</td>
</tr>
</tbody>
</table>
</div>
화면에 제대로 표시되는 것을 알 수 있습니다.
글 쓰기 버튼을 만들기 위해 bootstrap에서 버튼 스타일을 가져와 list.html에 붙여 넣고 우측 정렬을 해줍니다.
<div class="text-right">
<button type="button" class="btn btn-primary">글 작성</button>
</div>
글 작성 버튼을 동작하게 하기 위해 form.html 파일을 생성합니다.
(bootstrap에 있는 textarea 코드를 가져온다.)
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/common :: head('게시판')">
<!-- Required meta tags -->
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">
<!-- Bootstrap CSS -->
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" rel="stylesheet">
<link href="starter-template.css" rel="stylesheet" th:href="@{/starter-template.css}">
<title>게시판</title>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top" th:replace="fragments/common :: menu('board')">
<a class="navbar-brand" href="#">Spring Boot Tutorial</a>
<button aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
data-target="#navbarsExampleDefault" data-toggle="collapse" type="button">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">게시판</a>
</li>
</ul>
</div>
</nav>
<div class="container">
<h2>게시판</h2>
<form action="#" th:action="@{/board/form}" th:object="${board}" method="post">
<div class="form-group">
<label for="title">제목</label>
<input type="text" class="form-control" id="title" th:field="*{title}" placeholder="제목을 입력하세요.">
</div>
<div class="form-group">
<label for="content">내용</label>
<textarea class="form-control" id="content" rows="3" th:field="*{content}"></textarea>
</div>
<div class="text-right">
<button type="submit" class="btn btn-primary">확인</button>
<a type="button" class="btn btn-primary" th:href="@{/board/list}">취소</a>
</div>
</form>
</div>
</div>
<script crossorigin="anonymous"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script>
<script crossorigin="anonymous"
integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct"
src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
BoardController 수정
확인을 누르면 데이터베이스에 저장되면서 다시 게시판 창으로 돌아오는 기능을 추가합니다.
@GetMapping("/form")
public String form(Model model){
model.addAttribute("board", new Board());
return "board/form";
}
@PostMapping("/form")
public String form(@ModelAttribute Board board){
boardRepository.save(board);
return "redirect:/board/list";
}
list.html 수정
<div class="text-right">
<a type="button" class="btn btn-primary" th:href="@{/board/form}">글 작성</a>
</div>
작성한 글이 DB에 제대로 전달 되고 다시 list.html창으로 돌아가는 것을 확인할 수 있습니다.
list.html td태그에 href 속성을 추가해줍니다.
<tr th:each="board : ${boards}">
<td th:text="${board.id}">Mark</td>
<td><a th:text="${board.title}" th:href="@{/board/form(id=${board.id})}">Otto</a></td>
<td>홍길동</td>
</tr>
BoardController 수정
required 속성을 사용하여 필수인지 확인
새글을 작성할 때는 parameter가 필요 없다.
제대로 값을 저장했는지 어노테이션을 통해 확인할 수 있습니다.
@GetMapping("/form")
public String form(Model model, @RequestParam(required = false) Long id){
if(id == null){
model.addAttribute("board", new Board());
}
else {
Board board = boardRepository.findById(id).orElse(null);
model.addAttribute("board", board);
}
return "board/form";
}
@PostMapping("/form")
public String form(@ModelAttribute Board board){
boardRepository.save(board);
return "redirect:/board/list";
}
Board.java에 paramer 검증을 위한 어노테이션을 추가합니다.
package com.project.myhome.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Entity
@Data
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@NotNull
@Size(min=2,max=50)
private String title;
@NotNull
@Size(min = 1, max = 15000)
private String content;
}
BoardController에 parameter 검증을 위한 @Vaild를 추가해줍니다.
@PostMapping("/form")
public String form(@Valid Board board, BindingResult bindingResult){
if (bindingResult.hasErrors()) {
return "board/form";
}
boardRepository.save(board);
return "redirect:/board/list";
}
어노테이션으로 정의한 값에 부합하지 않는 parameter가 들어오면 인터페이스에서 오류가 났다고 알려주기 위해 form.html을 수정합니다.
bootstrap에서
<div class="invalid-feedback">
Please provide a valid city.
</div>
위의 코드를 가져오고 spring.io 가이드에서 가져온 코드를 사용합니다.
th:if="${#fields.hasErrors('name')}" th:errors="*{name}"
<div class="form-group">
<label for="title">제목</label>
<input type="text" class="form-control"
th:classappend="${#fields.hasErrors('title')} ? 'is-invalid'" id="title"
th:field="*{title}" placeholder="제목을 입력하세요.">
<div class="invalid-feedback" th:if="${#fields.hasErrors('title')}" th:errors="*{title}">
제목은 2글자 이상 50글자 이하로 작성해야 합니다.
</div>
</div>
<div class="form-group">
<label for="content">내용</label>
<textarea class="form-control" id="content"
th:classappend="${#fields.hasErrors('content')} ? 'is-invalid'"
rows="3" th:field="*{content}"></textarea>
<div class="invalid-feedback" th:if="${#fields.hasErrors('content')}" th:errors="*{content}">
내용은 1글자 이상 15000글자 이하로 작성해야 합니다.
</div>
</div>
제목에 한글자만 입력시 에러가 뜨는 것을 확인할 수 있습니다.
어노테이션으로 값을 제한하는 것은 자유도의 제약이 있기 때문에 자바 클래스로 값을 제한을 하기 위해 validator 패지키를 생성 한 후 패키지 아래에 BoardValidator라는 클래스를 생성합니다.
content의 값이 비어있을 경우 작동하게 만드는 코드입니다.
package com.project.myhome.validator;
import com.project.myhome.model.Board;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.thymeleaf.util.StringUtils;
@Component
public class BoardValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Board.class.equals(clazz);
}
@Override
public void validate(Object obj, Errors errors) {
Board b = (Board) obj;
if(StringUtils.isEmpty(b.getContent())){
errors.rejectValue("content", "key", "내용을 입력하세요");
}
}
}
BoardController도 수정을 해줍니다.
validate 함수를 호출해서 값을 확인합니다.
package com.project.myhome.controller;
import com.project.myhome.model.Board;
import com.project.myhome.repository.BoardRepository;
import com.project.myhome.validator.BoardValidator;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@Controller
@RequestMapping("/board")
public class BoardController {
@Autowired
private BoardRepository boardRepository;
@Autowired
private BoardValidator boardValidator;
@GetMapping("/list")
public String list(Model model){
List<Board> boards = boardRepository.findAll();
model.addAttribute("boards", boards);
return "board/list";
}
@GetMapping("/form")
public String form(Model model, @RequestParam(required = false) Long id){
if(id == null){
model.addAttribute("board", new Board());
}
else {
Board board = boardRepository.findById(id).orElse(null);
model.addAttribute("board", board);
}
return "board/form";
}
@PostMapping("/form")
public String form(@Valid Board board, BindingResult bindingResult){
boardValidator.validate(board, bindingResult);
if (bindingResult.hasErrors()) {
return "board/form";
}
boardRepository.save(board);
return "redirect:/board/list";
}
}
BindingResult는 스프링이 제공하는 검증 오류를 보관하는 객체입니다.
제대로 작동하는지 확인하기 위해 Board클래스도 수정을 해줍니다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@NotNull
@Size(min=2,max=50, message = "제목은 2자 이상 5자 이하입니다.")
private String title;
private String content;
제목에 있는 에러메시지는 어노테이션을 통해 만들어진 메시지이고 내용에 있는 에러 메시지는 validate 함수에 있는 메시지입니다.