개요
여기에서는 Spring Boot + JPA + HSQLDB + Thymeleaf를 사용하여 Spring MVC CRUD Web Application을 만드는 방법을 보여준다.
개발환경은 다음과 같다:
● Spring Tool Suite 3 (Version: 3.9.11)
● Spring Boot Version : 2.7.10
● Java : JDK 8
※ 관련글 목록: http://yellow.kr/lifeView.jsp?s=spring
작성할 웹 애플리케이션 설명
만들려는 예제 Web Application은 책 관리 시스템으로 다음과 같은 기능을 구현한다:
● 모든 책 정보 리스트를 보여준다.
● 새 책 정보 등록
● 책 정보 상세 조회
● 책 정보 수정
● 책 정보 삭제
예제 Application의 Flow는 다음과 같다:
Spring Boot project 생성
● eclips에서 Spring Boot project를 생성한다.
File > New > Project… > Spring Boot > Spring Starter Project
Name, Group, Package에 적당한 내용을 입력하고 [Next] 클릭
● 필요한 Dependency들을 선택한다.
Spring Web, JPA, HyperSQL, Thymeleaf를 선택한다.
[Finish] 클릭
● Project 생성 됨
yellow-jpa 라는 이름으로 Project가 생성되었고, YellowJpaApplication.java 가 생성되었다.
package com.yellow.jpa;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class YellowJpaApplication {
public static void main(String[] args) {
SpringApplication.run(YellowJpaApplication.class, args);
}
}
예제 애플리케이션이 완성되면 Project Structure은 다음과 같다.
Maven Dependencies
pom.xml을 보면 다음과 같은 dependency를 확인할 수 있다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<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.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
JPA
JPA는 Java Persistence API의 약자로, Java EE 및 Java SE 환경에서 데이터베이스와 상호작용하기 위한 자바 ORM(Object-Relational Mapping) 기술이다.
JPA는 데이터베이스와 자바 객체 간의 매핑 작업을 자동으로 처리하며, 데이터베이스에 대한 저수준 작업을 직접 처리하는 대신 개발자가 객체 중심적인 방식으로 데이터를 다룰 수 있도록 도와준다.
Spring Boot에서 JPA를 사용하면, 애플리케이션에서 ORM을 사용해 데이터베이스에 데이터를 영속화하고 관리할 수 있다. 또한, JPA는 트랜잭션 처리, 캐시 관리, 객체지향적인 데이터베이스 작업 등을 지원하여 개발자가 간편하게 데이터베이스와 상호작용할 수 있도록 한다.
HSQLDB는 관계형 데이터베이스 관리 시스템(RDBMS) 중 하나로, Java에서 구현된 경량 데이터베이스이다. HSQLDB를 Spring Boot에서 JPA와 함께 사용하면, 애플리케이션 개발 시 테스트 용도로 내장 데이터베이스로 사용하거나, 별도의 데이터베이스를 설치하지 않고도 개발할 수 있다.
따라서 Spring Boot에서 JPA와 HSQLDB를 함께 사용하면, 효율적이고 편리한 개발을 할 수 있다.
Spring Boot에서 JPA를 사용하기 위해서는 프로젝트에 JPA 종속성을 추가한다. 아래는 Maven을 사용하는 경우의 예시이다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
HSQLDB
HSQLDB는 Java로 작성된 관계형 데이터베이스 관리 시스템(RDBMS)이다. HSQLDB는 고성능 인메모리 데이터베이스로서, 메모리 상에 데이터를 저장하거나 파일로 저장할 수 있다. HSQLDB는 ACID(Atomicity, Consistency, Isolation, Durability)를 보장하며, SQL 표준을 지원한다. 또한 JDBC 드라이버를 제공하여 Java 언어와의 통합이 용이하다.
HSQLDB는 다양한 용도로 사용될 수 있다. 가장 일반적인 용도는 테스트를 위한 인메모리 데이터베이스이다. 테스트를 위한 데이터베이스를 별도로 설치할 필요 없이 HSQLDB를 사용하여 테스트를 수행할 수 있다. 또한 작은 규모의 애플리케이션 개발에 적합하다. HSQLDB는 경량화되어 있으며, 자바 플랫폼에 포함되어 있기 때문에 자바 애플리케이션에 쉽게 통합할 수 있다.
HSQLDB의 장점을 정리하면 다음과 같다:
가벼운 인메모리 데이터베이스: HSQLDB는 인메모리 데이터베이스로 동작할 수 있어서 메모리 사용량이 적다.
빠른 개발: Spring Boot에서 HSQLDB를 사용하면 개발자는 데이터베이스를 설치하거나 관리하지 않아도 된다. 이로 인해 개발 속도가 빨라진다.
테스트 용이성: HSQLDB는 인메모리 데이터베이스로 동작할 수 있기 때문에 테스트를 위한 데이터베이스를 쉽게 구축할 수 있다.
다양한 데이터베이스 지원: HSQLDB는 다양한 데이터베이스를 지원한다. 예를 들어 MySQL과 호환되는 SQL을 지원하며, Oracle과 PostgreSQL과 같은 데이터베이스와의 호환성도 좋다.
Spring Boot에서 인메모리 HSQLDB를 사용할 경우 application.properties 파일에 별도의 HSQLDB 관련 설정이 필요하지 않다.
Spring Boot의 의존성 관리 기능을 이용하여 다음과 같이 HSQLDB 라이브러리를 프로젝트에 추가한다.
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
HSQLDB 데이터베이스 초기화
일반적으로 Spring Boot에서 데이터 초기화를 위해 데이터베이스 DDL 및 DML을 실행하려면 schema.sql 또는 data.sql 파일을 사용한다.
schema.sql 파일은 데이터베이스 스키마를 생성하고 수정하는 데 사용된다. 이 파일은 스프링 부트 애플리케이션을 시작할 때 한 번 실행된다.
data.sql 파일은 데이터베이스 초기 데이터를 삽입하는 데 사용된다. 이 파일은 schema.sql이 실행된 후 실행된다.
보통 src/main/resources 폴더 내에 schema.sql 또는 data.sql 파일을 만들어놓으면 스프링 부트가 자동으로 이 파일들을 읽어서 데이터베이스 초기화 작업을 수행한다. 만약 다른 폴더에 위치한 파일을 사용하려면 application.properties에 파일 경로를 지정할 수 있다.
● schema.sql
DROP TABLE book IF EXISTS;
CREATE TABLE book (
book_id VARCHAR(4),
title VARCHAR(40),
author VARCHAR(40),
publisher VARCHAR(40),
release_date VARCHAR(8),
isbn VARCHAR(13),
PRIMARY KEY(book_id)
);
● data.sql
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1001','장기20세기','조반니 아리기','그린비','20140520','9788976827821');
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1002','신의 지문','그레이엄 핸콕','까치','20170120','9788972916307');
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1003','신화의 이미지','조지프 캠벨','살림출판사','20060220','9788952204776');
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1004','블랙아테나 1','마틴 버낼','소나무','20060110','9788971395479');
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1005','판다의 엄지','스티븐 제이 굴드','사이언스북스','20160520','9788983717788');
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1006','이기적 유전자','리처드 도킨스','을유문화사','20181020','9788932473901');
JPA를 사용하는 경우에는 application.properties에 다음과 같은 설정을 한다.
spring.jpa.defer-datasource-initialization=true
Model, Repository, Service
● Book.java
package com.yellow.jpa;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="book")
public class Book {
@Id
@Column(name="book_id")
private String bookId;
@Column(nullable=false)
private String title;
@Column(nullable=false)
private String author;
private String publisher;
@Column(name="release_date")
private String releaseDate;
private String isbn;
public Book() {}
public Book(String bookId, String title, String author, String publisher, String releaseDate, String isbn) {
this.setBookId(bookId);
this.setTitle(title);
this.setAuthor(author);
this.setPublisher(publisher);
this.setReleaseDate(releaseDate);
this.setIsbn(isbn);
}
public String getBookId() {
return bookId;
}
public void setBookId(String bookId) {
this.bookId = bookId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getPublisher() {
return publisher;
}
public void setPublisher(String publisher) {
this.publisher = publisher;
}
public String getReleaseDate() {
return releaseDate;
}
public void setReleaseDate(String releaseDate) {
this.releaseDate = releaseDate;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
@Override
public String toString() {
return "bookId:" + bookId + ",title:" + title + ",author:" + author
+ ",publisher:" + publisher + ",releaseDate:" + releaseDate + ",isbn:" + isbn;
}
}
● BookRepository.java
package com.yellow.jpa;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book,String> {
}
● BookService.java
package com.yellow.jpa;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
public Book getBookById(String id) {
Optional<Book> book = bookRepository.findById(id);
return book.get();
}
public void saveBook(Book book) {
bookRepository.save(book);
}
public void updateBook(Book book) {
bookRepository.save(book);
}
public void deleteBook(String id) {
bookRepository.deleteById(id);
}
}
Contoller, View(Thymeleaf)
● BookController.java
package com.yellow.jpa;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class BookController {
private BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping("/")
public String index() {
return "index";
}
@GetMapping("/listBook")
public String viewBookList(Model model) {
List<Book> books = bookService.getAllBooks();
model.addAttribute("allBooks", books);
return "listBook";
}
@GetMapping("/viewBook/{id}")
public String viewBook(@PathVariable("id") String id, Model model) {
Book book = bookService.getBookById(id);
model.addAttribute("book", book);
return "viewBook";
}
@GetMapping("/addViewBook")
public String addViewBook() {
return "addViewBook";
}
@PostMapping("/addBook")
public String addBook(@ModelAttribute Book book) {
bookService.saveBook(book);
return "redirect:/listBook";
}
@GetMapping("/updateViewBook/{id}")
public String updateViewBook(@PathVariable("id") String id, Model model) {
Book book = bookService.getBookById(id);
model.addAttribute("book", book);
return "updateViewBook";
}
@PostMapping("/updateBook/{id}")
public String updateBook(@PathVariable("id") String id, @ModelAttribute Book book) {
bookService.updateBook(book);
return "redirect:/listBook";
}
@GetMapping("/deleteBook/{id}")
public String deleteBook(@PathVariable("id") String id) {
bookService.deleteBook(id);
return "redirect:/listBook";
}
}
● index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Main Page</title>
</head>
<body>
<h1>Main Page</h1>
<p><a href="/listBook">List Book</a></p>
<p><a href="/addViewBook">Register Book</a></p>
</body>
</html>
● listBook.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Book List</title>
</head>
<body>
<h1>Book List</h1>
<br/>
<div>
<table border="1">
<tr>
<th>ID</th>
<th>제목</th>
<th>저자</th>
<th>Action</th>
</tr>
<tr th:each ="book : ${allBooks}">
<td><a th:href="@{/viewBook/{id}(id=${book.bookId})}" th:text="${book.bookId}"></a></td>
<td th:text="${book.title}"></td>
<td th:text="${book.author}"></td>
<td>
<a th:href="@{/updateViewBook/{id}(id=${book.bookId})}" >수정</a>,
<a th:href="@{/deleteBook/{id}(id=${book.bookId})}" >삭제</a>
</td>
</tr>
</table>
</div>
<br/><br/>
<a href="/">Main Page</a>
</body>
</html>
● viewBook.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>View Book</title>
</head>
<body>
<h1>Book Detail View</h1>
<div>
<span>Book ID : </span>
<span th:text="${book.bookId}"></span>
</div>
<div>
<span>제 목 : </span>
<span th:text="${book.title}"></span>
</div>
<div>
<span>저 자 : </span>
<span th:text="${book.author}"></span>
</div>
<div>
<span>출판사 : </span>
<span th:text="${book.publisher}"></span>
</div>
<div>
<span>출판일 : </span>
<span th:text="${book.releaseDate}"></span>
</div>
<div>
<span>ISBN : </span>
<span th:text="${book.isbn}"></span>
</div>
<br>
<br>
<div>
<a th:href="@{/updateViewBook/{id}(id=${book.bookId})}" >수정</a> /
<a th:href="@{/deleteBook/{id}(id=${book.bookId})}" >삭제</a>
</div>
<p><br></p>
<p><a href="/listBook">List Book</a></p>
</body>
</html>
● addViewBook.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Add a New Book</title>
</head>
<body>
<h1>Add a New Book</h1>
<form method="POST" action="/addBook">
<div>
<label>Book ID</label>
<input type="text" id="bookId" name="bookId">
</div>
<div>
<label>제 목 </label>
<input type="text" id="title" name="title">
</div>
<div>
<label>저 자 </label>
<input type="text" id="author" name="author">
</div>
<div>
<label>출판사</label>
<input type="text" id="publisher" name="publisher">
</div>
<div>
<label>출판일</label>
<input type="text" id="releaseDate" name="releaseDate">
</div>
<div>
<label>ISBN </label>
<input type="text" id="isbn" name="isbn">
</div>
<div>
<p><br></p>
<input type="submit" id="addNew" value="Submit">
</div>
</form>
<p><br></p>
<p><a href="/">Main</a></p>
</body>
</html>
● updateViewBook.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Update a Book</title>
</head>
<body>
<h1>Update a Book</h1>
<form th:action="@{/updateBook/{id}(id=${book.bookId})}" method="POST" >
<div>
<label>Book ID :</label>
<span th:text="${book.bookId}"></span>
<input type="hidden" th:field="*{book.bookId}" >
</div>
<div>
<label>제 목 </label>
<input type="text" th:field="*{book.title}" >
</div>
<div>
<label>저 자 </label>
<input type="text" th:field="*{book.author}" >
</div>
<div>
<label>출판사</label>
<input type="text" th:field="*{book.publisher}" >
</div>
<div>
<label>출판일</label>
<input type="text" th:field="*{book.releaseDate}" >
</div>
<div>
<label>ISBN </label>
<input type="text" th:field="*{book.isbn}" >
</div>
<div>
<p><br></p>
<input type="submit" id="addNew" value="Submit">
</div>
</form>
<p><br></p>
<p><a href="/">Main</a></p>
</body>
</html>
Application 실행
Project를 선택한 후, Run AS > Spring Boot App 를 하면 내장 Tomcat 서버에 배포된다. 브라우저에서 다음의 URL을 실행한다.
● http://localhost:8080/
● http://localhost:8080/listBook
● http://localhost:8080/addViewBook
● http://localhost:8080/listBook
● http://localhost:8080/viewBook/1004
● http://localhost:8080/updateViewBook/1004
● http://localhost:8080/listBook
등등…